/**
 * SF Command — `/memory`
 *
 * Subcommands:
 *   list            — show recent active memories
 *   show <id>       — print one memory
 *   ingest <uri>    — persist a source row (file path, URL, or "-" for stdin-piped note)
 *   note "<text>"   — persist an inline note as a source
 *   forget <id>     — supersede a memory (CAP_EXCEEDED sentinel)
 *   stats           — category / scope counts + source count
 *   sources         — list recent memory_sources rows
 *   extract <src>   — dispatch an agent turn that distils a source into memories
 */
import { readFileSync, writeFileSync } from "node:fs";
import { resolve as resolvePath } from "node:path";
import { projectRoot } from "./commands/context.js";
import { getErrorMessage } from "./error-utils.js";
import {
	ingestFile,
	ingestNote,
	ingestUrl,
	summarizeIngest,
} from "./memory-ingest.js";
import { createMemoryRelation, listRelationsFor } from "./memory-relations.js";
import { getMemorySource, listMemorySources } from "./memory-source-store.js";
import {
	createMemory,
	decayStaleMemories,
	enforceMemoryCap,
	getActiveMemories,
	getActiveMemoriesRanked,
	getRelevantMemoriesRanked,
	supersedeMemory,
} from "./memory-store.js";
import { _getAdapter, isDbAvailable } from "./sf-db.js";

function parseArgs(raw) {
	const tokens = splitArgs(raw);
	const sub = (tokens.shift() ?? "list").toLowerCase();
	const positional = [];
	const tags = [];
	let scope;
	let extract = false;
	for (let i = 0; i < tokens.length; i++) {
		const tok = tokens[i];
		if (tok === "--tag" && i + 1 < tokens.length) {
			tags.push(
				...tokens[++i]
					.split(",")
					.map((t) => t.trim())
					.filter(Boolean),
			);
			continue;
		}
		if (tok.startsWith("--tag=")) {
			tags.push(
				...tok
					.slice("--tag=".length)
					.split(",")
					.map((t) => t.trim())
					.filter(Boolean),
			);
			continue;
		}
		if (tok === "--scope" && i + 1 < tokens.length) {
			scope = tokens[++i];
			continue;
		}
		if (tok.startsWith("--scope=")) {
			scope = tok.slice("--scope=".length);
			continue;
		}
		if (tok === "--extract") {
			extract = true;
			continue;
		}
		if (tok === "--no-extract") {
			extract = false;
			continue;
		}
		positional.push(tok);
	}
	return { sub, positional, tags, scope, extract };
}
function splitArgs(raw) {
	const tokens = [];
	const re = /"([^"]*)"|'([^']*)'|(\S+)/g;
	let match;
	while ((match = re.exec(raw)) !== null) {
		tokens.push(match[1] ?? match[2] ?? match[3]);
	}
	return tokens;
}
function truncate(text, max) {
	if (text.length <= max) return text;
	return `${text.slice(0, max - 1)}…`;
}
// ─── Handler ────────────────────────────────────────────────────────────────
export async function handleMemory(args, ctx, pi) {
	const parsed = parseArgs(args);
	// `/memory` or `/memory help`
	if (parsed.sub === "" || parsed.sub === "help") {
		ctx.ui.notify(usage(), "info");
		return;
	}
	// Most subcommands need the DB.
	await ensureDb();
	switch (parsed.sub) {
		case "list":
			handleList(ctx);
			return;
		case "search":
			await handleSearch(ctx, parsed);
			return;
		case "show":
			handleShow(ctx, parsed.positional[0]);
			return;
		case "forget":
			handleForget(ctx, parsed.positional[0]);
			return;
		case "stats":
			handleStats(ctx);
			return;
		case "status":
			await handleStatus(ctx);
			return;
		case "sources":
			handleSources(ctx);
			return;
		case "note":
			await handleNote(ctx, parsed);
			return;
		case "ingest":
			await handleIngest(ctx, parsed);
			return;
		case "extract":
			handleExtractSource(ctx, pi, parsed.positional[0]);
			return;
		case "export":
			handleExport(ctx, parsed.positional[0]);
			return;
		case "import":
			handleImport(ctx, parsed.positional[0]);
			return;
		case "decay":
			handleDecay(ctx);
			return;
		case "cap":
			handleCap(ctx, parsed.positional[0]);
			return;
		case "backfill":
			await handleBackfill(ctx);
			return;
		default:
			ctx.ui.notify(
				`Unknown subcommand "${parsed.sub}". ${usage()}`,
				"warning",
			);
			return;
	}
}
function usage() {
	return [
		"Usage: /memory <subcommand>",
		"  list                    list recent active memories",
		'  search "<query>"        embedding-ranked search (gateway-aware; static fallback)',
		"  show <MEM###>           print one memory",
		"  forget <MEM###>         supersede a memory",
		"  stats                   counts by category / sources / edges",
		"  status                  live gateway, embedding coverage, and search-mode diagnostics",
		"  sources                 list recent memory_sources",
		'  note "<text>"           ingest an inline note as a source',
		"  ingest <path|url>       ingest a local file path or URL",
		"  extract <SRC-xxx>       dispatch an LLM turn to extract memories from a source",
		"  export <path.json>      dump memories + relations + sources to JSON",
		"  import <path.json>      load a previous export (idempotent)",
		"  decay                   run the stale-memory decay pass immediately",
		"  cap [N]                 enforce the memory cap (default 50)",
		"  backfill                embed queued memories now using the configured gateway",
		"",
		"Options: --tag a,b   --scope project|global|<custom>   --extract",
	].join("\n");
}
async function ensureDb() {
	const { ensureDbOpen } = await import("./bootstrap/dynamic-tools.js");
	await ensureDbOpen(projectRoot());
}
function handleList(ctx) {
	if (!isDbAvailable()) {
		ctx.ui.notify("No SF database available.", "warning");
		return;
	}
	const memories = getActiveMemoriesRanked(50);
	if (memories.length === 0) {
		ctx.ui.notify("No active memories.", "info");
		return;
	}
	const lines = memories.map(
		(m) =>
			`- [${m.id}] (${m.category}, conf ${m.confidence.toFixed(2)}, hits ${m.hit_count}) ${truncate(m.content, 100)}`,
	);
	ctx.ui.notify(lines.join("\n"), "info");
}
async function handleSearch(ctx, parsed) {
	if (!isDbAvailable()) {
		ctx.ui.notify("No SF database available.", "warning");
		return;
	}
	const query = parsed.positional.join(" ").trim();
	if (!query) {
		ctx.ui.notify(
			'Usage: /memory search "<query>"  (uses embeddings when SF_LLM_GATEWAY_KEY is set; static fallback otherwise)',
			"warning",
		);
		return;
	}
	const memories = await getRelevantMemoriesRanked(query, 10);
	if (memories.length === 0) {
		ctx.ui.notify("No matches.", "info");
		return;
	}
	const { loadGatewayConfigFromEnv } = await import("./memory-embeddings.js");
	const gatewayConfig = loadGatewayConfigFromEnv();
	const usingEmbeddings = !!gatewayConfig;
	const usingRerank = !!gatewayConfig?.rerankModel;
	const rankLabel = usingRerank
		? "embedding+rerank-ranked"
		: usingEmbeddings
			? "embedding-ranked"
			: "static rank — set SF_LLM_GATEWAY_KEY for embeddings";
	const header = `Top ${memories.length} memories for "${truncate(query, 60)}" (${rankLabel}):`;
	const lines = [header];
	for (const m of memories) {
		lines.push(
			`  [${m.id}] (${m.category}, conf ${m.confidence.toFixed(2)}) ${truncate(m.content, 100)}`,
		);
	}
	ctx.ui.notify(lines.join("\n"), "info");
}
function handleShow(ctx, id) {
	if (!id) {
		ctx.ui.notify("Usage: /memory show <MEM###>", "warning");
		return;
	}
	const adapter = _getAdapter();
	if (!adapter) {
		ctx.ui.notify("No SF database available.", "warning");
		return;
	}
	const row = adapter
		.prepare("SELECT * FROM memories WHERE id = :id")
		.get({ ":id": id });
	if (!row) {
		ctx.ui.notify(`Memory not found: ${id}`, "warning");
		return;
	}
	const tags = row["tags"] ? safeJsonArray(row["tags"]) : [];
	const lines = [
		`ID: ${row["id"]}`,
		`Category: ${row["category"]}`,
		`Confidence: ${Number(row["confidence"]).toFixed(2)}`,
		`Hits: ${row["hit_count"]}`,
		`Created: ${row["created_at"]}`,
		`Updated: ${row["updated_at"]}`,
		tags.length > 0 ? `Tags: ${tags.join(", ")}` : null,
		row["superseded_by"] ? `Superseded by: ${row["superseded_by"]}` : null,
		row["source_unit_type"]
			? `Source: ${row["source_unit_type"]}/${row["source_unit_id"]}`
			: null,
		"",
		String(row["content"]),
	]
		.filter((line) => line !== null)
		.join("\n");
	ctx.ui.notify(lines, "info");
}
function handleForget(ctx, id) {
	if (!id) {
		ctx.ui.notify("Usage: /memory forget <MEM###>", "warning");
		return;
	}
	const ok = supersedeMemory(id, "CAP_EXCEEDED");
	if (!ok) {
		ctx.ui.notify(`Failed to forget ${id}.`, "warning");
		return;
	}
	ctx.ui.notify(`Forgot ${id}.`, "info");
}
function handleStats(ctx) {
	const adapter = _getAdapter();
	if (!adapter) {
		ctx.ui.notify("No SF database available.", "warning");
		return;
	}
	try {
		const activeRow = adapter
			.prepare(
				"SELECT count(*) as cnt FROM memories WHERE superseded_by IS NULL",
			)
			.get();
		const supersededRow = adapter
			.prepare(
				"SELECT count(*) as cnt FROM memories WHERE superseded_by IS NOT NULL",
			)
			.get();
		const byCategory = adapter
			.prepare(
				"SELECT category, count(*) as cnt FROM memories WHERE superseded_by IS NULL GROUP BY category ORDER BY cnt DESC",
			)
			.all();
		const sourcesRow = adapter
			.prepare("SELECT count(*) as cnt FROM memory_sources")
			.get();
		const sourcesByKind = adapter
			.prepare(
				"SELECT kind, count(*) as cnt FROM memory_sources GROUP BY kind ORDER BY cnt DESC",
			)
			.all();
		const relationsRow = adapter
			.prepare("SELECT count(*) as cnt FROM memory_relations")
			.get();
		const relationsByRel = adapter
			.prepare(
				"SELECT rel, count(*) as cnt FROM memory_relations GROUP BY rel ORDER BY cnt DESC",
			)
			.all();
		const embeddingsRow = adapter
			.prepare("SELECT count(*) as cnt FROM memory_embeddings")
			.get();
		const embeddedActiveRow = adapter
			.prepare(`SELECT count(*) as cnt FROM memory_embeddings e
         JOIN memories m ON m.id = e.memory_id
         WHERE m.superseded_by IS NULL`)
			.get();
		const activeCount = activeRow?.["cnt"] ?? 0;
		const embeddedActive = embeddedActiveRow?.["cnt"] ?? 0;
		const coverage =
			activeCount > 0
				? `${Math.round((embeddedActive / activeCount) * 100)}%`
				: "n/a";
		const out = [
			`Active memories: ${activeCount}`,
			`Superseded: ${supersededRow?.["cnt"] ?? 0}`,
			"",
			"By category:",
			...byCategory.map((row) => `  ${row["category"]}: ${row["cnt"]}`),
			"",
			`Memory sources: ${sourcesRow?.["cnt"] ?? 0}`,
			...sourcesByKind.map((row) => `  ${row["kind"]}: ${row["cnt"]}`),
			"",
			`Relations: ${relationsRow?.["cnt"] ?? 0}`,
			...relationsByRel.map((row) => `  ${row["rel"]}: ${row["cnt"]}`),
			"",
			`Embeddings: ${embeddingsRow?.["cnt"] ?? 0} total, ${embeddedActive} active (coverage ${coverage})`,
		].join("\n");
		ctx.ui.notify(out, "info");
	} catch (err) {
		ctx.ui.notify(`Stats failed: ${err.message}`, "warning");
	}
}
async function handleStatus(ctx) {
	const adapter = _getAdapter();
	if (!adapter) {
		ctx.ui.notify("No SF database available.", "warning");
		return;
	}
	const dbStatus = readMemoryDbStatus(adapter);
	const { loadGatewayConfigFromEnv, createGatewayEmbedFn, rerankCandidates } =
		await import("./memory-embeddings.js");
	const gatewayConfig = loadGatewayConfigFromEnv();
	let embeddingProbe = { ok: false, status: "not_configured", latencyMs: null };
	let rerankProbe = { ok: false, status: "not_configured", latencyMs: null };
	if (gatewayConfig) {
		embeddingProbe = await probeEmbedding(gatewayConfig, createGatewayEmbedFn);
		rerankProbe = await probeRerank(gatewayConfig, rerankCandidates);
	}
	const searchMode = !gatewayConfig
		? "static"
		: embeddingProbe.ok && rerankProbe.ok
			? "embedding+rerank"
			: embeddingProbe.ok
				? "embedding"
				: "static";
	const lines = [
		"SF memory status",
		"",
		"Gateway:",
		gatewayConfig ? `  key: yes (${gatewayConfig.keySource})` : "  key: no",
		`  url: ${gatewayConfig?.url ?? "n/a"}${gatewayConfig ? ` (${gatewayConfig.urlSource})` : ""}`,
		`  embed model: ${gatewayConfig?.embeddingModel ?? "n/a"}`,
		`  rerank model: ${gatewayConfig?.rerankModel ?? "n/a"}`,
		"",
		"Live probes:",
		`  embeddings: ${formatProbe(embeddingProbe)}`,
		`  rerank: ${formatProbe(rerankProbe)}`,
		"",
		"Coverage:",
		`  active memories: ${dbStatus.activeCount}`,
		`  embedded active: ${dbStatus.embeddedActive}`,
		`  embedding coverage: ${dbStatus.coverage}`,
		`  queued for backfill: ${dbStatus.unembeddedActive}`,
		`  stored embeddings: ${dbStatus.embeddingsTotal}`,
		"",
		"Extraction:",
		`  processed units: ${dbStatus.processedUnits}`,
		`  attempts: ${dbStatus.extractionAttempts}`,
		`  last processed: ${formatMemoryStatusRow(dbStatus.lastProcessedUnit, "unit_key", "processed_at")}`,
		`  last attempt: ${formatMemoryAttempt(dbStatus.lastExtractionAttempt)}`,
		"",
		"Backfill:",
		"  trigger: agent_end",
		"  max per turn: 50",
		"  batch size: 16",
		"",
		`Effective search mode: ${searchMode}`,
	];
	ctx.ui.notify(lines.join("\n"), "info");
}
async function handleBackfill(ctx) {
	const adapter = _getAdapter();
	if (!adapter) {
		ctx.ui.notify("No SF database available.", "warning");
		return;
	}
	const before = readMemoryDbStatus(adapter);
	const { loadGatewayConfigFromEnv } = await import("./memory-embeddings.js");
	const gatewayConfig = loadGatewayConfigFromEnv();
	if (!gatewayConfig) {
		ctx.ui.notify(
			"Memory backfill unavailable: no llm-gateway key configured.",
			"warning",
		);
		return;
	}
	const { runEmbeddingBackfill } = await import("./memory-embeddings.js");
	const embedded = await runEmbeddingBackfill();
	const after = readMemoryDbStatus(adapter);
	ctx.ui.notify(
		[
			`Memory backfill embedded ${embedded} memor${embedded === 1 ? "y" : "ies"}.`,
			`Coverage: ${before.embeddedActive}/${before.activeCount} (${before.coverage}) -> ${after.embeddedActive}/${after.activeCount} (${after.coverage})`,
			`Queued for backfill: ${after.unembeddedActive}`,
		].join("\n"),
		embedded > 0 ? "info" : "warning",
	);
}
function readMemoryDbStatus(adapter) {
	const activeCount =
		adapter
			.prepare(
				"SELECT count(*) as cnt FROM memories WHERE superseded_by IS NULL",
			)
			.get()?.["cnt"] ?? 0;
	const embeddingsTotal =
		adapter.prepare("SELECT count(*) as cnt FROM memory_embeddings").get()?.[
			"cnt"
		] ?? 0;
	const embeddedActive =
		adapter
			.prepare(`SELECT count(*) as cnt FROM memory_embeddings e
         JOIN memories m ON m.id = e.memory_id
         WHERE m.superseded_by IS NULL`)
			.get()?.["cnt"] ?? 0;
	const unembeddedActive = Math.max(0, activeCount - embeddedActive);
	const coverage =
		activeCount > 0
			? `${Math.round((embeddedActive / activeCount) * 100)}%`
			: "n/a";
	const processedUnits =
		adapter
			.prepare("SELECT count(*) as cnt FROM memory_processed_units")
			.get()?.["cnt"] ?? 0;
	const extractionAttempts =
		adapter
			.prepare("SELECT count(*) as cnt FROM memory_extraction_attempts")
			.get()?.["cnt"] ?? 0;
	const lastProcessedUnit =
		adapter
			.prepare(`SELECT unit_key, activity_file, processed_at
        FROM memory_processed_units
        ORDER BY processed_at DESC
        LIMIT 1`)
			.get() ?? null;
	const lastExtractionAttempt =
		adapter
			.prepare(`SELECT unit_key, status, reason, error, created_at
        FROM memory_extraction_attempts
        ORDER BY created_at DESC, id DESC
        LIMIT 1`)
			.get() ?? null;
	return {
		activeCount,
		embeddedActive,
		embeddingsTotal,
		unembeddedActive,
		coverage,
		processedUnits,
		extractionAttempts,
		lastProcessedUnit,
		lastExtractionAttempt,
	};
}
function formatMemoryStatusRow(row, keyField, timeField) {
	if (!row) return "none";
	return `${row[keyField]} at ${row[timeField]}`;
}
function formatMemoryAttempt(row) {
	if (!row) return "none";
	const reason = row["reason"] ? ` (${row["reason"]})` : "";
	const error = row["error"] ? ` error=${row["error"]}` : "";
	return `${row["unit_key"]}: ${row["status"]}${reason} at ${row["created_at"]}${error}`;
}
async function probeEmbedding(gatewayConfig, createGatewayEmbedFn) {
	const startedAt = Date.now();
	try {
		const embedFn = createGatewayEmbedFn(
			{ ...gatewayConfig, timeoutMs: 10_000 },
			{ instruction: gatewayConfig.queryInstruction },
		);
		const vectors = await embedFn(["sf memory status embedding probe"]);
		const dim = vectors[0]?.length ?? 0;
		if (dim <= 0) {
			return {
				ok: false,
				status: "empty_vector",
				latencyMs: Date.now() - startedAt,
			};
		}
		return {
			ok: true,
			status: `ok (${dim} dims)`,
			latencyMs: Date.now() - startedAt,
		};
	} catch (error) {
		return {
			ok: false,
			status: sanitizeProbeError(error),
			latencyMs: Date.now() - startedAt,
		};
	}
}
async function probeRerank(gatewayConfig, rerankCandidates) {
	const startedAt = Date.now();
	try {
		const scores = await rerankCandidates(
			{ ...gatewayConfig, timeoutMs: 10_000 },
			"sf memory status rerank probe",
			[
				{ id: "a", text: "sf memory uses gateway reranking" },
				{ id: "b", text: "unrelated filler document" },
			],
		);
		if (!scores) {
			return {
				ok: false,
				status: "unavailable",
				latencyMs: Date.now() - startedAt,
			};
		}
		return {
			ok: scores.length === 2,
			status: `ok (${scores.length} scores)`,
			latencyMs: Date.now() - startedAt,
		};
	} catch (error) {
		return {
			ok: false,
			status: sanitizeProbeError(error),
			latencyMs: Date.now() - startedAt,
		};
	}
}
function formatProbe(probe) {
	const suffix =
		typeof probe.latencyMs === "number" ? `, ${probe.latencyMs}ms` : "";
	return `${probe.ok ? "ok" : "not ok"} (${probe.status}${suffix})`;
}
function sanitizeProbeError(error) {
	const message = getErrorMessage(error);
	return message.replace(/Bearer\s+[A-Za-z0-9._~+/=-]+/g, "Bearer [redacted]");
}
function handleExport(ctx, target) {
	if (!target) {
		ctx.ui.notify("Usage: /memory export <path.json>", "warning");
		return;
	}
	try {
		const active = getActiveMemories();
		const relations = active.flatMap((m) =>
			listRelationsFor(m.id).filter((r) => r.from === m.id),
		);
		const sources = listMemorySources(500);
		const payload = {
			version: 1,
			exported_at: new Date().toISOString(),
			memories: active.map((m) => ({
				id: m.id,
				category: m.category,
				content: m.content,
				confidence: m.confidence,
				hit_count: m.hit_count,
				source_unit_type: m.source_unit_type,
				source_unit_id: m.source_unit_id,
				created_at: m.created_at,
				updated_at: m.updated_at,
			})),
			relations: relations.map((r) => ({
				from: r.from,
				to: r.to,
				rel: r.rel,
				confidence: r.confidence,
			})),
			sources,
		};
		const abs = resolvePath(process.cwd(), target);
		writeFileSync(abs, JSON.stringify(payload, null, 2), "utf-8");
		ctx.ui.notify(
			`Exported ${payload.memories.length} memories, ${payload.relations.length} relations, ${payload.sources.length} sources → ${abs}`,
			"info",
		);
	} catch (err) {
		ctx.ui.notify(`Export failed: ${err.message}`, "error");
	}
}
function handleImport(ctx, target) {
	if (!target) {
		ctx.ui.notify("Usage: /memory import <path.json>", "warning");
		return;
	}
	try {
		const abs = resolvePath(process.cwd(), target);
		const raw = readFileSync(abs, "utf-8");
		const parsed = JSON.parse(raw);
		let memoryCount = 0;
		let relationCount = 0;
		for (const mem of parsed.memories ?? []) {
			if (!mem.category || !mem.content) continue;
			const id = createMemory({
				category: mem.category,
				content: mem.content,
				confidence: mem.confidence,
			});
			if (id) memoryCount++;
		}
		for (const rel of parsed.relations ?? []) {
			if (!rel.from || !rel.to || !rel.rel) continue;
			if (createMemoryRelation(rel.from, rel.to, rel.rel, rel.confidence)) {
				relationCount++;
			}
		}
		ctx.ui.notify(
			`Imported ${memoryCount} memories and ${relationCount} relations.`,
			"info",
		);
	} catch (err) {
		ctx.ui.notify(`Import failed: ${err.message}`, "error");
	}
}
function handleDecay(ctx) {
	decayStaleMemories(20);
	ctx.ui.notify("Decay pass complete.", "info");
}
function handleCap(ctx, arg) {
	const max = arg ? Number.parseInt(arg, 10) : 50;
	if (!Number.isFinite(max) || max < 1) {
		ctx.ui.notify("Usage: /memory cap <max>  (default 50)", "warning");
		return;
	}
	enforceMemoryCap(max);
	ctx.ui.notify(`Enforced memory cap of ${max}.`, "info");
}
function handleSources(ctx) {
	const sources = listMemorySources(30);
	if (sources.length === 0) {
		ctx.ui.notify(
			"No memory sources yet. Use `/memory ingest <path|url>` to add one.",
			"info",
		);
		return;
	}
	const lines = sources.map(
		(s) =>
			`- ${s.id} [${s.kind}${s.scope !== "project" ? `/${s.scope}` : ""}] ${truncate(s.title ?? s.uri ?? s.content, 100)}`,
	);
	ctx.ui.notify(lines.join("\n"), "info");
}
async function handleNote(ctx, args) {
	const text = args.positional.join(" ").trim();
	if (!text) {
		ctx.ui.notify('Usage: /memory note "your note"', "warning");
		return;
	}
	try {
		const result = await ingestNote(text, null, {
			scope: args.scope,
			tags: args.tags,
			extract: false,
		});
		ctx.ui.notify(summarizeIngest(result), "info");
	} catch (err) {
		ctx.ui.notify(`Note ingest failed: ${err.message}`, "error");
	}
}
async function handleIngest(ctx, args) {
	const target = args.positional[0];
	if (!target) {
		ctx.ui.notify(
			"Usage: /memory ingest <path|url> [--tag a,b] [--scope project|global]",
			"warning",
		);
		return;
	}
	try {
		const isUrl = /^https?:\/\//i.test(target);
		const result = isUrl
			? await ingestUrl(target, null, {
					scope: args.scope,
					tags: args.tags,
					extract: false,
				})
			: await ingestFile(target, null, {
					scope: args.scope,
					tags: args.tags,
					extract: false,
				});
		ctx.ui.notify(summarizeIngest(result), "info");
		if (args.extract && result.sourceId) {
			ctx.ui.notify(
				`(Use \`/memory extract ${result.sourceId}\` to trigger extraction manually.)`,
				"info",
			);
		}
	} catch (err) {
		ctx.ui.notify(`Ingest failed: ${err.message}`, "error");
	}
}
function handleExtractSource(ctx, pi, id) {
	if (!id) {
		ctx.ui.notify("Usage: /memory extract <SRC-xxx>", "warning");
		return;
	}
	const source = getMemorySource(id);
	if (!source) {
		ctx.ui.notify(`Source not found: ${id}`, "warning");
		return;
	}
	const prompt = buildExtractPrompt(source);
	ctx.ui.notify(`Dispatching extraction turn for ${id}...`, "info");
	pi.sendMessage(
		{ customType: "sf-memory-extract", content: prompt, display: false },
		{ triggerTurn: true },
	);
}
function buildExtractPrompt(source) {
	const header = [
		`## Memory extraction request`,
		``,
		`Source: ${source.id} (${source.kind})`,
		source.title ? `Title: ${source.title}` : null,
		source.uri ? `URI: ${source.uri}` : null,
	]
		.filter(Boolean)
		.join("\n");
	return [
		header,
		"",
		"Read the content below and call the `log_reasoning` tool once per durable insight",
		"(architecture, convention, gotcha, preference, environment, pattern). Skip one-off details,",
		"temporary state, and anything secret. Keep each memory to 1–3 sentences.",
		"",
		"---",
		"",
		source.content,
	].join("\n");
}
function safeJsonArray(raw) {
	try {
		const parsed = JSON.parse(raw);
		return Array.isArray(parsed)
			? parsed.filter((t) => typeof t === "string")
			: [];
	} catch {
		return [];
	}
}
// projectRoot is imported so tests can mock it via the same path as other commands.
export const _internals = { projectRoot };
