UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

1,875 lines 83.5 kB
import { R as timestampMsToIsoString, w as parseStrictPositiveInteger } from "./number-coercion-CLj0HTDM.js";
import { c as isRecord, r as asNullableRecord } from "./record-coerce-DItp3I4t.js";
import { l as normalizeOptionalString } from "./string-coerce-CIXf7egm.js";
import { c as normalizeSingleOrTrimmedStringList, d as normalizeStringEntries, y as uniqueStrings } from "./string-normalization-DsCfAx8q.js";
import { r as truncateUtf16Safe } from "./utf16-slice-D_ngcYKd.js";
import { t as FsSafeError, w as root } from "./fs-safe-B6pvPGnf.js";
import { r as isPathInside } from "./path-guards-Cp-mGr3-.js";
import { y as resolveDefaultAgentId } from "./agent-scope-config-DcbEhP0R.js";
import { n as callGatewayFromCli } from "./gateway-rpc-C9fDQ0vT.js";
import "./number-runtime-Cy4drVnh.js";
import "./string-coerce-runtime-GQa0ehRA.js";
import "./file-access-runtime-CfcyqU8y.js";
import "./security-runtime-Ckf0kc0h.js";
import "./gateway-runtime-BkyVf3uU.js";
import "./memory-host-core-BuVChYR2.js";
import { n as withTrailingNewline, t as replaceManagedMarkdownBlock } from "./memory-host-markdown-mHNl3RAL.js";
import "./text-utility-runtime-BjzvUG99.js";
import { C as getMemoryWikiPage, D as appendMemoryWikiLog, E as resolveMemoryWikiTimestamp, S as WIKI_SEARCH_MODES, T as initializeMemoryWikiVault, a as syncMemoryWikiImportedSources, b as withMemoryWikiVaultMutation, c as writeGuardedVaultPage, d as runObsidianDaily, f as runObsidianOpen, g as applyMemoryWikiMutation, h as ingestMemoryWikiSource, i as resolveMemoryWikiStatus, l as probeObsidianCli, m as lintMemoryWikiVault, n as renderMemoryWikiDoctor, p as runObsidianSearch, r as renderMemoryWikiStatus, s as isRegularFileStat, t as buildMemoryWikiDoctorReport, u as runObsidianCommand, v as compileMemoryWikiVault, w as searchMemoryWiki } from "./status-mk9AERH5.js";
import { i as resolveMemoryWikiAgentConfig, n as WIKI_SEARCH_BACKENDS, r as WIKI_SEARCH_CORPORA } from "./config-CpK-zpn0.js";
import { E as createWikiPageFilename, L as slugifyWikiSegment, P as renderWikiMarkdown, T as WIKI_RELATED_START_MARKER, c as readMemoryWikiImportRunRecord, i as countMemoryWikiImportRunStateRows, j as parseWikiMarkdown, l as resolveMemoryWikiImportRunsDir, t as MEMORY_WIKI_IMPORT_RUN_STATE_MAX_ENTRIES, u as writeMemoryWikiImportRunRecord, w as WIKI_RELATED_END_MARKER, z as walkMemoryWikiDirectory } from "./import-runs-state-COPJ30nf.js";
import path from "node:path";
import fs from "node:fs/promises";
import { createHash, randomUUID } from "node:crypto";
//#region extensions/memory-wiki/src/chatgpt-import.ts
const CHATGPT_PREFERENCE_SIGNAL_RE = /\b(prefer|prefers|preference|want|wants|need|needs|avoid|avoids|hate|hates|love|loves|default to|should default to|always use|don't want|does not want|likes|dislikes)\b/i;
const HUMAN_START_MARKER = "<!-- openclaw:human:start -->";
const HUMAN_END_MARKER = "<!-- openclaw:human:end -->";
const MAX_ROLLBACK_RECREATE_ATTEMPTS = 32;
const CHATGPT_RISK_RULES = [
	{
		label: "relationships",
		pattern: /\b(relationship|dating|breakup|jealous|sex|intimacy|partner|apology|trust|boyfriend|girlfriend|husband|wife)\b/i
	},
	{
		label: "health",
		pattern: /\b(supplement|medication|diagnosis|symptom|therapy|depression|anxiety|mri|migraine|injury|pain|cortisol|sleep)\b/i
	},
	{
		label: "legal_tax",
		pattern: /\b(contract|tax|legal|law|lawsuit|visa|immigration|license|insurance|claim|non-residence|residency)\b/i
	},
	{
		label: "finance",
		pattern: /\b(investment|invest|portfolio|dividend|yield|coupon|valuation|mortgage|loan|crypto|covered call|call option|put option)\b/i
	},
	{
		label: "drugs",
		pattern: /\b(vape|weed|cannabis|nicotine|opioid|ketamine)\b/i
	}
];
function normalizeWhitespace(value) {
	return value.trim().replace(/\s+/g, " ");
}
function isMissingConversationPageError(error) {
	return asNullableRecord(error)?.code === "ENOENT";
}
async function readExistingConversationPage(absolutePath) {
	try {
		return await fs.readFile(absolutePath, "utf8");
	} catch {
		try {
			return await fs.readFile(absolutePath, "utf8");
		} catch (retryError) {
			if (isMissingConversationPageError(retryError)) return "";
			throw retryError;
		}
	}
}
function resolveConversationSourcePath(exportInputPath) {
	const resolved = path.resolve(exportInputPath);
	return {
		exportPath: resolved,
		conversationsPath: resolved.endsWith(".json") ? resolved : path.join(resolved, "conversations.json")
	};
}
async function loadConversations(exportInputPath) {
	const { exportPath, conversationsPath } = resolveConversationSourcePath(exportInputPath);
	const raw = await fs.readFile(conversationsPath, "utf8");
	const parsed = JSON.parse(raw);
	const conversations = Array.isArray(parsed) ? parsed : Object.values(asNullableRecord(parsed) ?? {}).find(Array.isArray);
	if (!conversations) throw new Error(`Unrecognized ChatGPT conversations export format: ${conversationsPath}`);
	return {
		exportPath,
		conversationsPath,
		conversations: conversations.filter(isRecord)
	};
}
function isoFromUnix(raw) {
	if (typeof raw !== "number" && typeof raw !== "string") return;
	const numeric = Number(raw);
	if (!Number.isFinite(numeric)) return;
	return timestampMsToIsoString(numeric * 1e3);
}
function cleanMessageText(value) {
	const trimmed = value.trim();
	if (!trimmed) return "";
	if ((trimmed.includes("asset_pointer") || trimmed.includes("image_asset_pointer") || trimmed.includes("dalle") || trimmed.includes("file_service")) && trimmed.length > 40) return "";
	if (trimmed.startsWith("{") && trimmed.length > 80 && (trimmed.includes(":") || trimmed.includes("content_type"))) {
		const textMatch = trimmed.match(/["']text["']\s*:\s*(["'])(.+?)\1/s);
		return textMatch?.[2] ? normalizeWhitespace(textMatch[2]) : "";
	}
	return trimmed;
}
function extractMessageText(message) {
	const content = asNullableRecord(message.content);
	if (content) {
		const parts = content.parts;
		if (Array.isArray(parts)) {
			const collected = [];
			for (const part of parts) {
				if (typeof part === "string") {
					const cleaned = cleanMessageText(part);
					if (cleaned) collected.push(cleaned);
					continue;
				}
				const partRecord = asNullableRecord(part);
				if (partRecord && typeof partRecord.text === "string" && partRecord.text.trim()) collected.push(partRecord.text.trim());
			}
			return collected.join("\n").trim();
		}
		if (typeof content.text === "string") return cleanMessageText(content.text);
	}
	return typeof message.text === "string" ? cleanMessageText(message.text) : "";
}
function activeBranchMessages(conversation) {
	const mapping = asNullableRecord(conversation.mapping);
	if (!mapping) return [];
	let currentNode = typeof conversation.current_node === "string" ? conversation.current_node : void 0;
	const seen = /* @__PURE__ */ new Set();
	const chain = [];
	while (currentNode && !seen.has(currentNode)) {
		seen.add(currentNode);
		const node = asNullableRecord(mapping[currentNode]);
		if (!node) break;
		const message = asNullableRecord(node.message);
		if (message) {
			const author = asNullableRecord(message.author);
			const role = typeof author?.role === "string" ? author.role : "unknown";
			const text = extractMessageText(message);
			if (text) chain.push({
				role,
				text
			});
		}
		currentNode = typeof node.parent === "string" ? node.parent : void 0;
	}
	return chain.toReversed();
}
function inferRisk(title, sampleText) {
	const blob = `${title}\n${sampleText}`.toLowerCase();
	const reasons = CHATGPT_RISK_RULES.filter((rule) => rule.pattern.test(blob)).map((rule) => rule.label);
	if (reasons.length > 0) return {
		level: "high",
		reasons: uniqueStrings(reasons)
	};
	if (/\b(career|job|salary|interview|offer|resume|cover letter)\b/i.test(blob)) return {
		level: "medium",
		reasons: ["work_career"]
	};
	return {
		level: "low",
		reasons: []
	};
}
function inferLabels(title, sampleText) {
	const blob = `${title}\n${sampleText}`.toLowerCase();
	const labels = /* @__PURE__ */ new Set(["domain/personal"]);
	const addAreaTopic = (area, topics) => {
		labels.add(area);
		for (const topic of topics) labels.add(topic);
	};
	const hasTranslation = /\b(translate|translation|traduc\w*|traducc\w*|traduç\w*|traducci[oó]n|traduccio|traducció|traduzione)\b/i.test(blob);
	const hasLearning = /\b(anki|flashcards?|grammar|conjugat\w*|declension|pronunciation|vocab(?:ular(?:y|io))?|lesson|tutor|teacher|jlpt|kanji|hiragana|katakana|study|learn|practice)\b/i.test(blob);
	const hasLanguageName = /\b(japanese|portuguese|catalan|castellano|espa[nñ]ol|franc[eé]s|french|italian|german|spanish)\b/i.test(blob);
	if (hasTranslation) labels.add("topic/translation");
	if (hasLearning || hasLanguageName && /\b(learn|study|practice|lesson|tutor|grammar)\b/i.test(blob)) addAreaTopic("area/language-learning", ["topic/language-learning"]);
	if (/\b(hike|trail|hotel|flight|trip|travel|airport|itinerary|booking|airbnb|train|stay)\b/i.test(blob)) {
		labels.add("area/travel");
		labels.add("topic/travel");
	}
	if (/\b(recipe|cook|cooking|bread|sourdough|pizza|espresso|coffee|mousse|cast iron|meatballs?)\b/i.test(blob)) addAreaTopic("area/cooking", ["topic/cooking"]);
	if (/\b(garden|orchard|plant|soil|compost|agroforestry|permaculture|mulch|beds?|irrigation|seeds?)\b/i.test(blob)) addAreaTopic("area/gardening", ["topic/gardening"]);
	if (/\b(dating|relationship|partner|jealous|breakup|trust)\b/i.test(blob)) addAreaTopic("area/relationships", ["topic/relationships"]);
	if (/\b(investment|invest|portfolio|dividend|yield|coupon|valuation|return|mortgage|loan|kraken|crypto|covered call|call option|put option|option chain|bond|stocks?)\b/i.test(blob)) addAreaTopic("area/finance", ["topic/finance"]);
	if (/\b(contract|mou|tax|impuesto|legal|law|lawsuit|visa|immigration|license|licencia|dispute|claim|insurance|non-residence|residency)\b/i.test(blob)) addAreaTopic("area/legal-tax", ["topic/legal-tax"]);
	if (/\b(supplement|medication|diagnos(?:is|e)|symptom|therapy|depress(?:ion|ed)|anxiet(?:y|ies)|mri|migraine|injur(?:y|ies)|pain|cortisol|sleep|dentist|dermatolog(?:ist|y))\b/i.test(blob)) addAreaTopic("area/health", ["topic/health"]);
	if (/\b(book (an )?appointment|rebook|open (a )?new account|driving test|exam|gestor(?:a)?|itv)\b/i.test(blob)) addAreaTopic("area/life-admin", ["topic/life-admin"]);
	if (/\b(frc|robot|robotics|wpilib|limelight|chiefdelphi)\b/i.test(blob)) addAreaTopic("area/work", ["topic/robotics"]);
	else if (/\b(docker|git|python|node|npm|pip|sql|postgres|api|bug|stack trace|permission denied)\b/i.test(blob)) addAreaTopic("area/work", ["topic/software"]);
	else if (/\b(job|interview|cover letter|resume|cv)\b/i.test(blob)) addAreaTopic("area/work", ["topic/career"]);
	if (/\b(wifi|wi-fi|starlink|router|mesh|network|orbi|milesight|coverage)\b/i.test(blob)) addAreaTopic("area/home", ["topic/home-infrastructure"]);
	if (/\b(p38|range rover|porsche|bmw|bobcat|excavator|auger|trailer|chainsaw|stihl)\b/i.test(blob)) addAreaTopic("area/vehicles", ["topic/vehicles"]);
	if (![...labels].some((label) => label.startsWith("area/"))) labels.add("area/other");
	return [...labels];
}
function collectPreferenceSignals(userTexts) {
	const signals = [];
	const seen = /* @__PURE__ */ new Set();
	for (const text of userTexts.slice(0, 25)) for (const rawLine of text.split(/\r?\n/)) {
		const line = normalizeWhitespace(rawLine);
		if (!line || !CHATGPT_PREFERENCE_SIGNAL_RE.test(line)) continue;
		const key = line.toLowerCase();
		if (seen.has(key)) continue;
		seen.add(key);
		signals.push(line);
		if (signals.length >= 10) return signals;
	}
	return signals;
}
function buildTranscript(messages) {
	if (messages.length === 0) return "_No active-branch transcript could be reconstructed._";
	return messages.flatMap((message) => [
		`### ${message.role[0]?.toUpperCase() ?? "U"}${message.role.slice(1)}`,
		"",
		message.text,
		""
	]).join("\n").trim();
}
function resolveConversationPagePath(record) {
	const conversationSlug = record.conversationId.replace(/[^a-zA-Z0-9]/g, "").toLowerCase();
	const pageId = `source.chatgpt.${conversationSlug || createHash("sha1").update(record.conversationId).digest("hex").slice(0, 12)}`;
	const datePrefix = record.createdAt?.slice(0, 10) ?? "undated";
	const shortId = conversationSlug.slice(0, 8) || "export";
	return {
		pageId,
		pagePath: path.join("sources", `chatgpt-${datePrefix}-${conversationSlug || shortId}.md`).replace(/\\/g, "/")
	};
}
function toConversationRecord(conversation, sourcePath) {
	const conversationId = typeof conversation.conversation_id === "string" ? conversation.conversation_id.trim() : "";
	if (!conversationId) return null;
	const title = typeof conversation.title === "string" && conversation.title.trim() ? conversation.title.trim() : "Untitled conversation";
	const transcript = activeBranchMessages(conversation);
	const userTexts = transcript.filter((entry) => entry.role === "user").map((entry) => entry.text);
	const assistantTexts = transcript.filter((entry) => entry.role === "assistant");
	const sampleText = userTexts.slice(0, 6).join("\n");
	const risk = inferRisk(title, sampleText);
	const labels = inferLabels(title, sampleText);
	const { pageId, pagePath } = resolveConversationPagePath({
		conversationId,
		createdAt: isoFromUnix(conversation.create_time)
	});
	return {
		conversationId,
		title,
		createdAt: isoFromUnix(conversation.create_time),
		updatedAt: isoFromUnix(conversation.update_time) ?? isoFromUnix(conversation.create_time),
		sourcePath,
		pageId,
		pagePath,
		labels,
		risk,
		userMessageCount: userTexts.length,
		assistantMessageCount: assistantTexts.length,
		preferenceSignals: risk.level === "low" ? collectPreferenceSignals(userTexts) : [],
		firstUserLine: userTexts[0]?.split(/\r?\n/)[0]?.trim(),
		lastUserLine: userTexts.at(-1)?.split(/\r?\n/)[0]?.trim(),
		transcript
	};
}
function renderConversationPage(record) {
	const autoDigestLines = record.risk.level === "low" ? [
		`- User messages: ${record.userMessageCount}`,
		`- Assistant messages: ${record.assistantMessageCount}`,
		...record.firstUserLine ? [`- First user line: ${record.firstUserLine}`] : [],
		...record.lastUserLine ? [`- Last user line: ${record.lastUserLine}`] : [],
		...record.preferenceSignals.length > 0 ? ["- Preference signals:", ...record.preferenceSignals.map((line) => `  - ${line}`)] : ["- Preference signals: none detected"]
	] : ["- Auto digest withheld from durable-candidate generation until reviewed.", `- Risk reasons: ${record.risk.reasons.length > 0 ? record.risk.reasons.join(", ") : "none recorded"}`];
	return renderWikiMarkdown({
		frontmatter: {
			pageType: "source",
			id: record.pageId,
			title: `ChatGPT Export: ${record.title}`,
			sourceType: "chatgpt-export",
			sourceSystem: "chatgpt",
			sourcePath: record.sourcePath,
			conversationId: record.conversationId,
			riskLevel: record.risk.level,
			riskReasons: record.risk.reasons,
			labels: record.labels,
			status: "draft",
			...record.createdAt ? { createdAt: record.createdAt } : {},
			...record.updatedAt ? { updatedAt: record.updatedAt } : {}
		},
		body: [
			`# ChatGPT Export: ${record.title}`,
			"",
			"## Source",
			`- Conversation id: \`${record.conversationId}\``,
			`- Export file: \`${record.sourcePath}\``,
			...record.createdAt ? [`- Created: ${record.createdAt}`] : [],
			...record.updatedAt ? [`- Updated: ${record.updatedAt}`] : [],
			"",
			"## Auto Triage",
			`- Risk level: \`${record.risk.level}\``,
			`- Labels: ${record.labels.join(", ")}`,
			`- Active-branch messages: ${record.transcript.length}`,
			"",
			"## Auto Digest",
			...autoDigestLines,
			"",
			"## Active Branch Transcript",
			buildTranscript(record.transcript),
			"",
			"## Notes",
			HUMAN_START_MARKER,
			HUMAN_END_MARKER,
			""
		].join("\n")
	});
}
function replaceSimpleManagedBlock(params) {
	const escapedStart = params.startMarker.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
	const escapedEnd = params.endMarker.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
	const blockPattern = new RegExp(`${escapedStart}[\\s\\S]*?${escapedEnd}`);
	return params.original.replace(blockPattern, () => params.replacement);
}
function extractSimpleManagedBlock(params) {
	const escapedStart = params.startMarker.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
	const escapedEnd = params.endMarker.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
	const blockPattern = new RegExp(`${escapedStart}[\\s\\S]*?${escapedEnd}`);
	return params.body.match(blockPattern)?.[0] ?? null;
}
function extractManagedBlockBody(params) {
	const escapedStart = params.startMarker.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
	const escapedEnd = params.endMarker.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
	const blockPattern = new RegExp(`${escapedStart}\\n?([\\s\\S]*?)\\n?${escapedEnd}`);
	const captured = params.body.match(blockPattern)?.[1];
	return typeof captured === "string" ? captured.trim() : null;
}
function preserveExistingPageBlocks(rendered, existing) {
	if (!existing.trim()) return withTrailingNewline(rendered);
	const parsedExisting = parseWikiMarkdown(existing);
	const parsedRendered = parseWikiMarkdown(rendered);
	let nextBody = parsedRendered.body;
	const humanBlock = extractSimpleManagedBlock({
		body: parsedExisting.body,
		startMarker: HUMAN_START_MARKER,
		endMarker: HUMAN_END_MARKER
	});
	if (humanBlock) nextBody = replaceSimpleManagedBlock({
		original: nextBody,
		startMarker: HUMAN_START_MARKER,
		endMarker: HUMAN_END_MARKER,
		replacement: humanBlock
	});
	const relatedBody = extractManagedBlockBody({
		body: parsedExisting.body,
		startMarker: WIKI_RELATED_START_MARKER,
		endMarker: WIKI_RELATED_END_MARKER
	});
	if (relatedBody) nextBody = replaceManagedMarkdownBlock({
		original: nextBody,
		heading: "## Related",
		startMarker: WIKI_RELATED_START_MARKER,
		endMarker: WIKI_RELATED_END_MARKER,
		body: relatedBody
	});
	return withTrailingNewline(renderWikiMarkdown({
		frontmatter: parsedRendered.frontmatter,
		body: nextBody
	}));
}
function buildRunId(exportPath, nowIso) {
	const seed = `${exportPath}:${nowIso}:${Math.random()}`;
	return `chatgpt-${createHash("sha1").update(seed).digest("hex").slice(0, 12)}`;
}
function normalizeConversationActions(records, operations) {
	return records.map((record) => ({
		conversationId: record.conversationId,
		title: record.title,
		pagePath: record.pagePath,
		operation: operations.get(record.pagePath) ?? "skip",
		riskLevel: record.risk.level,
		labels: record.labels,
		userMessageCount: record.userMessageCount,
		assistantMessageCount: record.assistantMessageCount,
		preferenceSignals: record.preferenceSignals
	}));
}
async function writeImportRunRecord(vaultRoot, record) {
	await writeMemoryWikiImportRunRecord(vaultRoot, record);
}
async function readImportRunRecord(vaultRoot, runId) {
	const record = await readMemoryWikiImportRunRecord(vaultRoot, runId);
	if (!record) throw new Error(`Memory Wiki import run not found: ${runId}`);
	return record;
}
const MACHINE_RELATED_BLOCK_PATTERN = new RegExp(`(?:^|\\n\\n)## Related\\n${WIKI_RELATED_START_MARKER}\\n[\\s\\S]*?\\n${WIKI_RELATED_END_MARKER}`, "g");
function hashChatGptImportContent(content) {
	const importOwnedContent = content.replace(MACHINE_RELATED_BLOCK_PATTERN, "");
	return createHash("sha256").update(importOwnedContent, "utf8").digest("hex");
}
async function writeTrackedImportPage(params) {
	const absolutePath = path.join(params.vaultRoot, params.relativePath);
	if (params.existing === params.rendered) return "skip";
	const contentHash = hashChatGptImportContent(params.rendered);
	await fs.mkdir(path.dirname(absolutePath), { recursive: true });
	if (!params.existing) {
		await fs.writeFile(absolutePath, params.rendered, "utf8");
		params.record.createdPaths.push({
			path: params.relativePath,
			contentHash
		});
		return "create";
	}
	const snapshotHash = createHash("sha1").update(params.relativePath).digest("hex").slice(0, 12);
	const snapshotRelativePath = path.join("snapshots", `${snapshotHash}.md`).replace(/\\/g, "/");
	const snapshotAbsolutePath = path.join(params.runDir, snapshotRelativePath);
	await fs.mkdir(path.dirname(snapshotAbsolutePath), { recursive: true });
	await fs.writeFile(snapshotAbsolutePath, params.existing, "utf8");
	await fs.writeFile(absolutePath, params.rendered, "utf8");
	params.record.updatedPaths.push({
		path: params.relativePath,
		snapshotPath: snapshotRelativePath,
		contentHash
	});
	return "update";
}
async function importChatGptConversationsUnlocked(params) {
	await initializeMemoryWikiVault(params.config, { nowMs: params.nowMs });
	const { exportPath, conversationsPath, conversations } = await loadConversations(params.exportPath);
	const records = conversations.map((conversation) => toConversationRecord(conversation, conversationsPath)).filter((entry) => entry !== null).toSorted((left, right) => left.pagePath.localeCompare(right.pagePath));
	const operations = /* @__PURE__ */ new Map();
	let createdCount = 0;
	let updatedCount = 0;
	let skippedCount = 0;
	let runId;
	const nowIso = resolveMemoryWikiTimestamp(params.nowMs);
	const importPlans = [];
	for (const record of records) {
		const rendered = renderConversationPage(record);
		const existing = await readExistingConversationPage(path.join(params.config.vault.path, record.pagePath));
		const stabilized = preserveExistingPageBlocks(rendered, existing);
		const operation = existing === stabilized ? "skip" : existing ? "update" : "create";
		operations.set(record.pagePath, operation);
		if (operation === "create") createdCount += 1;
		else if (operation === "update") updatedCount += 1;
		else skippedCount += 1;
		importPlans.push({
			relativePath: record.pagePath,
			existing,
			rendered: stabilized,
			operation
		});
	}
	let importRunRecord;
	const changedCount = createdCount + updatedCount;
	if (!params.dryRun && changedCount > 0) {
		const requiredStateRows = 1 + changedCount;
		const projectedStateRows = await countMemoryWikiImportRunStateRows() + requiredStateRows;
		if (projectedStateRows > 2e4) throw new Error(`Memory Wiki ChatGPT import exceeds SQLite import-run entry limit (${projectedStateRows}/${MEMORY_WIKI_IMPORT_RUN_STATE_MAX_ENTRIES})`);
		runId = buildRunId(exportPath, nowIso);
		const importRunDir = path.join(resolveMemoryWikiImportRunsDir(params.config.vault.path), runId);
		importRunRecord = {
			version: 1,
			runId,
			importType: "chatgpt",
			exportPath,
			sourcePath: conversationsPath,
			appliedAt: nowIso,
			conversationCount: records.length,
			createdCount,
			updatedCount,
			skippedCount,
			createdPaths: [],
			updatedPaths: []
		};
		for (const plan of importPlans) {
			if (plan.operation === "skip") continue;
			await writeTrackedImportPage({
				vaultRoot: params.config.vault.path,
				runDir: importRunDir,
				relativePath: plan.relativePath,
				existing: plan.existing,
				rendered: plan.rendered,
				record: importRunRecord
			});
		}
	}
	let indexUpdatedFiles = [];
	if (!params.dryRun && importRunRecord) {
		if (importRunRecord.createdPaths.length > 0 || importRunRecord.updatedPaths.length > 0) {
			await writeImportRunRecord(params.config.vault.path, importRunRecord);
			indexUpdatedFiles = (await compileMemoryWikiVault(params.config).catch((error) => {
				const message = error instanceof Error ? error.message : String(error);
				throw new Error(`Memory Wiki ChatGPT import run ${importRunRecord.runId} changed source pages, but vault compilation failed: ${message}. After fixing the compile error, run \`openclaw wiki chatgpt rollback ${importRunRecord.runId}\` to restore the imported pages.`, { cause: error });
			})).updatedFiles;
			await appendMemoryWikiLog(params.config.vault.path, {
				type: "ingest",
				timestamp: nowIso,
				details: {
					sourceType: "chatgpt-export",
					runId: importRunRecord.runId,
					exportPath,
					sourcePath: conversationsPath,
					conversationCount: records.length,
					createdCount: importRunRecord.createdPaths.length,
					updatedCount: importRunRecord.updatedPaths.length,
					skippedCount
				}
			});
		} else runId = void 0;
	}
	return {
		dryRun: Boolean(params.dryRun),
		exportPath,
		sourcePath: conversationsPath,
		conversationCount: records.length,
		createdCount,
		updatedCount,
		skippedCount,
		actions: normalizeConversationActions(records, operations),
		pagePaths: records.map((record) => record.pagePath),
		...runId ? { runId } : {},
		indexUpdatedFiles
	};
}
async function importChatGptConversations(params) {
	return await withMemoryWikiVaultMutation(params.config.vault.path, () => importChatGptConversationsUnlocked(params));
}
function toVaultRelativePath(vaultRoot, absolutePath) {
	return path.relative(vaultRoot, absolutePath).replace(/\\/g, "/");
}
function recoverySlotPrefix(ref) {
	return `${ref.kind}-${ref.index}-${createHash("sha256").update(ref.entry.path).digest("hex")}-`;
}
function resolveContainedImportPath(root, relativePath, label) {
	if (!relativePath || path.isAbsolute(relativePath) || path.win32.isAbsolute(relativePath)) throw new Error(`${label} must be a relative path: ${relativePath}`);
	const resolvedRoot = path.resolve(root);
	const resolvedPath = path.resolve(resolvedRoot, relativePath);
	if (!isPathInside(resolvedRoot, resolvedPath)) throw new Error(`${label} must stay inside ${resolvedRoot}: ${relativePath}`);
	return resolvedPath;
}
function buildRollbackEntryRefs(record) {
	return [...record.createdPaths.map((entry, index) => ({
		entry,
		kind: "created",
		index
	})), ...record.updatedPaths.map((entry, index) => ({
		entry,
		kind: "updated",
		index
	}))];
}
function listPreservedPaths(record) {
	return [...record.createdPaths, ...record.updatedPaths].flatMap((entry) => (entry.recoveryPaths ?? []).map((recoveryPath) => ({
		path: entry.path,
		recoveryPath
	})));
}
function isFsSafeErrorCode(error, code) {
	return error instanceof FsSafeError && error.code === code;
}
async function reserveRecoverySlot(runRoot, ref) {
	await runRoot.mkdir("recovered");
	const slotRelativePath = path.posix.join("recovered", `${recoverySlotPrefix(ref)}${randomUUID()}`);
	await runRoot.mkdir(slotRelativePath);
	return {
		slotRelativePath,
		contentRelativePath: path.posix.join(slotRelativePath, "content")
	};
}
async function removeEmptyRecoverySlot(runRoot, slotRelativePath) {
	try {
		if ((await runRoot.list(slotRelativePath)).length > 0) return;
		await runRoot.remove(slotRelativePath);
	} catch (error) {
		if (isFsSafeErrorCode(error, "not-found")) return;
		throw error;
	}
}
function recordRecoveryPath(entry, recoveryPath) {
	const recoveryPaths = entry.recoveryPaths ?? [];
	if (recoveryPaths.includes(recoveryPath)) return false;
	entry.recoveryPaths = [...recoveryPaths, recoveryPath];
	return true;
}
async function moveTargetToRecovery(params) {
	resolveContainedImportPath(params.vaultRoot.rootDir, params.ref.entry.path, "Memory Wiki import page path");
	for (;;) {
		const slot = await reserveRecoverySlot(params.runRoot, params.ref);
		const recoveryVaultRelativePath = path.posix.join(params.runRelativePath, slot.contentRelativePath);
		try {
			await params.vaultRoot.move(params.ref.entry.path, recoveryVaultRelativePath);
			return {
				ref: params.ref,
				...slot
			};
		} catch (error) {
			await removeEmptyRecoverySlot(params.runRoot, slot.slotRelativePath);
			if (isFsSafeErrorCode(error, "not-found")) return null;
			if (isFsSafeErrorCode(error, "already-exists")) continue;
			throw error;
		}
	}
}
async function scanRecoverySlots(params) {
	const refs = buildRollbackEntryRefs(params.record);
	const refsByPrefix = new Map(refs.map((ref) => [recoverySlotPrefix(ref), ref]));
	let slots;
	try {
		slots = await params.runRoot.list("recovered", { withFileTypes: true });
	} catch (error) {
		if (isFsSafeErrorCode(error, "not-found")) return [];
		throw error;
	}
	const recovered = [];
	for (const slot of slots) {
		if (!slot.isDirectory || slot.isSymbolicLink) continue;
		const prefix = /^(?:created|updated)-\d+-[a-f0-9]{64}-/.exec(slot.name)?.[0];
		const ref = prefix ? refsByPrefix.get(prefix) : void 0;
		if (!ref) continue;
		const slotRelativePath = path.posix.join("recovered", slot.name);
		recovered.push({
			ref,
			slotRelativePath,
			contentRelativePath: path.posix.join(slotRelativePath, "content")
		});
	}
	return recovered;
}
async function classifyRecoverySlots(params) {
	const seen = /* @__PURE__ */ new Set();
	let recordChanged = false;
	for (const slot of params.slots) {
		if (seen.has(slot.contentRelativePath)) continue;
		seen.add(slot.contentRelativePath);
		const { entry } = slot.ref;
		const recoveryPath = toVaultRelativePath(params.vaultRoot, path.join(params.runRoot.rootDir, slot.contentRelativePath));
		if (entry.recoveryPaths?.includes(recoveryPath)) continue;
		let recoveryHash = null;
		try {
			const stat = await params.runRoot.stat(slot.contentRelativePath);
			if (stat.isFile && !stat.isSymbolicLink) recoveryHash = hashChatGptImportContent(await params.runRoot.readText(slot.contentRelativePath));
		} catch (error) {
			if (isFsSafeErrorCode(error, "not-found")) {
				await removeEmptyRecoverySlot(params.runRoot, slot.slotRelativePath);
				continue;
			}
			throw error;
		}
		if (entry.contentHash && recoveryHash === entry.contentHash) {
			await params.runRoot.remove(slot.contentRelativePath);
			await removeEmptyRecoverySlot(params.runRoot, slot.slotRelativePath);
			continue;
		}
		recordChanged = recordRecoveryPath(entry, recoveryPath) || recordChanged;
	}
	return recordChanged;
}
async function targetMatchesSnapshot(vaultRoot, targetRelativePath, snapshot) {
	try {
		return hashChatGptImportContent(await vaultRoot.readText(targetRelativePath)) === hashChatGptImportContent(snapshot);
	} catch (error) {
		if (isFsSafeErrorCode(error, "not-found")) return false;
		throw error;
	}
}
async function rollbackChatGptImportRunUnlocked(params) {
	const vaultRoot = params.config.vault.path;
	const record = await readImportRunRecord(vaultRoot, params.runId);
	if (record.rolledBackAt) return {
		runId: record.runId,
		removedCount: 0,
		restoredCount: 0,
		preservedPaths: listPreservedPaths(record),
		pagePaths: [...record.createdPaths.map((entry) => entry.path), ...record.updatedPaths.map((entry) => entry.path)].toSorted((left, right) => left.localeCompare(right)),
		indexUpdatedFiles: [],
		alreadyRolledBack: true
	};
	const runDir = resolveContainedImportPath(resolveMemoryWikiImportRunsDir(vaultRoot), record.runId, "Memory Wiki import run id");
	const refs = buildRollbackEntryRefs(record);
	for (const ref of refs) {
		resolveContainedImportPath(vaultRoot, ref.entry.path, "Memory Wiki import page path");
		if (ref.entry.snapshotPath) resolveContainedImportPath(runDir, ref.entry.snapshotPath, "Memory Wiki import snapshot path");
	}
	if (!record.rollbackStartedAt) {
		record.rollbackStartedAt = (/* @__PURE__ */ new Date()).toISOString();
		await writeImportRunRecord(vaultRoot, record);
	}
	if (!record.rollbackTargetsFinalizedAt) {
		await initializeMemoryWikiVault(params.config);
		const vaultFs = await root(vaultRoot);
		const runRelativePath = toVaultRelativePath(vaultRoot, runDir);
		await vaultFs.mkdir(runRelativePath);
		const runFs = await root(runDir);
		if (await classifyRecoverySlots({
			vaultRoot,
			runRoot: runFs,
			slots: await scanRecoverySlots({
				runRoot: runFs,
				record
			})
		})) await writeImportRunRecord(vaultRoot, record);
		for (const ref of refs.filter((candidate) => candidate.kind === "created")) {
			let removed = false;
			for (let attempt = 0; attempt < MAX_ROLLBACK_RECREATE_ATTEMPTS; attempt += 1) {
				const slot = await moveTargetToRecovery({
					vaultRoot: vaultFs,
					runRoot: runFs,
					runRelativePath,
					ref
				});
				if (!slot) {
					removed = true;
					break;
				}
				if (await classifyRecoverySlots({
					vaultRoot,
					runRoot: runFs,
					slots: [slot]
				})) await writeImportRunRecord(vaultRoot, record);
			}
			if (!removed) throw new Error(`Memory Wiki rollback could not remove ${ref.entry.path} after ${MAX_ROLLBACK_RECREATE_ATTEMPTS} concurrent recreations`);
		}
		for (const ref of refs.filter((candidate) => candidate.kind === "updated")) {
			const { entry } = ref;
			if (!entry.snapshotPath) continue;
			resolveContainedImportPath(runDir, entry.snapshotPath, "Memory Wiki import snapshot path");
			const snapshot = await runFs.readText(entry.snapshotPath);
			resolveContainedImportPath(vaultRoot, entry.path, "Memory Wiki import page path");
			let restored = false;
			for (let attempt = 0; attempt < MAX_ROLLBACK_RECREATE_ATTEMPTS; attempt += 1) {
				if (await targetMatchesSnapshot(vaultFs, entry.path, snapshot)) {
					restored = true;
					break;
				}
				const slot = await moveTargetToRecovery({
					vaultRoot: vaultFs,
					runRoot: runFs,
					runRelativePath,
					ref
				});
				if (slot) {
					if (await classifyRecoverySlots({
						vaultRoot,
						runRoot: runFs,
						slots: [slot]
					})) await writeImportRunRecord(vaultRoot, record);
				}
				try {
					await vaultFs.create(entry.path, snapshot, {
						encoding: "utf8",
						mkdir: true
					});
					restored = true;
					break;
				} catch (error) {
					if (!isFsSafeErrorCode(error, "already-exists")) throw error;
				}
			}
			if (!restored) throw new Error(`Memory Wiki rollback could not restore ${entry.path} after ${MAX_ROLLBACK_RECREATE_ATTEMPTS} concurrent recreations`);
		}
		record.rollbackTargetsFinalizedAt = (/* @__PURE__ */ new Date()).toISOString();
		await writeImportRunRecord(vaultRoot, record);
	}
	const compile = await compileMemoryWikiVault(params.config, { sourcePageWrites: "preserve" });
	record.rolledBackAt = (/* @__PURE__ */ new Date()).toISOString();
	await writeImportRunRecord(vaultRoot, record);
	const preservedPaths = listPreservedPaths(record);
	const removedCount = record.createdPaths.length;
	const restoredCount = record.updatedPaths.filter((entry) => entry.snapshotPath).length;
	await appendMemoryWikiLog(vaultRoot, {
		type: "ingest",
		timestamp: record.rolledBackAt,
		details: {
			sourceType: "chatgpt-export",
			runId: record.runId,
			rollback: true,
			removedCount,
			restoredCount,
			preservedCount: preservedPaths.length
		}
	}).catch(() => void 0);
	return {
		runId: record.runId,
		removedCount,
		restoredCount,
		preservedPaths,
		pagePaths: [...record.createdPaths.map((entry) => entry.path), ...record.updatedPaths.map((entry) => entry.path)].toSorted((left, right) => left.localeCompare(right)),
		indexUpdatedFiles: compile.updatedFiles,
		alreadyRolledBack: false
	};
}
async function rollbackChatGptImportRun(params) {
	return await withMemoryWikiVaultMutation(params.config.vault.path, () => rollbackChatGptImportRunUnlocked(params));
}
//#endregion
//#region extensions/memory-wiki/src/okf.ts
const OKF_RESERVED_FILENAMES = /* @__PURE__ */ new Set(["index.md", "log.md"]);
const OKF_MARKDOWN_LINK_PATTERN = /(!?)\[([^\]]*)\]\(([^)]+)\)/g;
const OKF_FENCE_PATTERN = /^ {0,3}(`{3,}|~{3,})/;
const OKF_RELATED_SECTION_PATTERN = new RegExp(`\\n+## Related\\n${WIKI_RELATED_START_MARKER}[\\s\\S]*?${WIKI_RELATED_END_MARKER}\\n?`, "g");
const OKF_VOLATILE_TIMESTAMP_LINE_PATTERN = /^(?:importedAt|updatedAt): .*\n/gm;
const OKF_HASH_CHARS = 8;
function toPosixPath(value) {
	return value.split(path.sep).join("/");
}
function trimMarkdownExtension(value) {
	return value.replace(/\.md$/i, "");
}
function createOkfBundleKey(params) {
	const producerId = normalizeOptionalString(params.rootFrontmatter.id) ?? normalizeOptionalString(params.rootFrontmatter.okf_id);
	if (producerId) return slugifyWikiSegment(producerId);
	const label = normalizeOptionalString(params.rootFrontmatter.name) ?? normalizeOptionalString(params.rootFrontmatter.title) ?? params.bundleName;
	const hash = createHash("sha1").update(params.bundlePath).digest("hex").slice(0, OKF_HASH_CHARS);
	return `${slugifyWikiSegment(label)}-${hash}`;
}
function createOkfPageStem(bundleKey, conceptId) {
	return `okf-${bundleKey}-${slugifyWikiSegment(conceptId.replace(/\//g, "-"))}-${createHash("sha1").update(conceptId).digest("hex").slice(0, OKF_HASH_CHARS)}`;
}
function createOkfPageIdentity(bundleKey, conceptId) {
	const fileName = createWikiPageFilename(createOkfPageStem(bundleKey, conceptId));
	return {
		pageId: `concept.${trimMarkdownExtension(fileName)}`,
		pagePath: `concepts/${fileName}`
	};
}
async function collectOkfMarkdownFiles(rootDir, warnings) {
	const entries = await walkMemoryWikiDirectory(rootDir, "", {
		entryFilter: (entry) => entry.kind === "directory" && [".git", "node_modules"].includes(path.basename(entry.relativePath)) ? "skip-subtree" : "include",
		onDirectoryError: "skip-and-report"
	});
	const files = [];
	for (const entry of entries) {
		if (entry.kind === "directory-error") {
			warnings.push({
				code: "unreadable-entry",
				path: toPosixPath(entry.relativePath) || ".",
				message: entry.error instanceof Error ? entry.error.message : "Unable to read OKF directory."
			});
			continue;
		}
		if (entry.kind === "file" && entry.relativePath.endsWith(".md")) files.push(toPosixPath(entry.relativePath));
	}
	return files.toSorted((left, right) => left.localeCompare(right));
}
function parseOkfMarkdown(content, relativePath) {
	const normalizedContent = content.replace(/\r\n/g, "\n");
	try {
		return parseWikiMarkdown(normalizedContent);
	} catch (err) {
		return {
			frontmatter: {},
			body: normalizedContent,
			warning: {
				code: "invalid-concept",
				path: relativePath,
				message: err instanceof Error ? err.message : "Unable to parse OKF frontmatter."
			}
		};
	}
}
async function readOkfTextFile(params) {
	const root$1 = await root(params.bundlePath);
	const stat = await root$1.stat(params.relativePath).catch((err) => {
		params.warnings.push({
			code: "unreadable-entry",
			path: params.relativePath,
			message: err instanceof Error ? err.message : "Unable to read OKF concept."
		});
		return null;
	});
	if (!stat) return null;
	if (!isRegularFileStat(stat)) {
		params.warnings.push({
			code: "unreadable-entry",
			path: params.relativePath,
			message: "Refusing to import OKF concept through non-regular or hardlinked file."
		});
		return null;
	}
	return await root$1.readText(params.relativePath).catch((err) => {
		params.warnings.push({
			code: "unreadable-entry",
			path: params.relativePath,
			message: err instanceof Error ? err.message : "Unable to read OKF concept."
		});
		return null;
	});
}
function deriveOkfTitle(relativePath, frontmatter) {
	return normalizeOptionalString(frontmatter.title) ?? path.posix.basename(relativePath, ".md").replace(/[-_]+/g, " ").trim() ?? trimMarkdownExtension(relativePath);
}
function normalizeOkfConcept(params) {
	const parsed = parseOkfMarkdown(params.content, params.relativePath);
	if (parsed.warning) return { warning: parsed.warning };
	const type = normalizeOptionalString(parsed.frontmatter.type);
	if (!type) return { warning: {
		code: "missing-type",
		path: params.relativePath,
		message: "OKF concept is missing required non-empty type frontmatter."
	} };
	const conceptId = trimMarkdownExtension(params.relativePath);
	const timestamp = normalizeOptionalString(parsed.frontmatter.timestamp);
	return { concept: {
		conceptId,
		relativePath: params.relativePath,
		absolutePath: path.join(params.bundlePath, params.relativePath),
		frontmatter: parsed.frontmatter,
		body: parsed.body,
		type,
		title: deriveOkfTitle(params.relativePath, parsed.frontmatter),
		...normalizeOptionalString(parsed.frontmatter.description) ? { description: normalizeOptionalString(parsed.frontmatter.description) } : {},
		...normalizeOptionalString(parsed.frontmatter.resource) ? { resource: normalizeOptionalString(parsed.frontmatter.resource) } : {},
		tags: normalizeSingleOrTrimmedStringList(parsed.frontmatter.tags),
		...timestamp ? { timestamp } : {}
	} };
}
function splitMarkdownLinkDestination(target) {
	const trimmed = target.trim();
	if (trimmed.startsWith("<")) {
		const end = trimmed.indexOf(">");
		if (end > 0) return {
			destination: trimmed.slice(1, end),
			titleSuffix: trimmed.slice(end + 1)
		};
	}
	const match = trimmed.match(/^(\S+)(\s+[\s\S]+)?$/);
	return {
		destination: match?.[1] ?? trimmed,
		titleSuffix: match?.[2] ?? ""
	};
}
function resolveOkfMarkdownTarget(sourceRelativePath, target) {
	const { destination } = splitMarkdownLinkDestination(target);
	const trimmed = destination.trim();
	if (!trimmed || trimmed.startsWith("#") || /^[a-z][a-z0-9+.-]*:/i.test(trimmed)) return null;
	const rawTargetWithoutSuffix = trimmed.split("#")[0]?.split("?")[0]?.replace(/\\/g, "/").trim();
	const targetWithoutSuffix = safeDecodeOkfLinkPath(rawTargetWithoutSuffix);
	if (!targetWithoutSuffix || !targetWithoutSuffix.endsWith(".md")) return null;
	const conceptId = trimMarkdownExtension(targetWithoutSuffix.startsWith("/") ? path.posix.normalize(targetWithoutSuffix.slice(1)) : path.posix.normalize(path.posix.join(path.posix.dirname(sourceRelativePath), targetWithoutSuffix)));
	return conceptId.startsWith("../") ? null : conceptId;
}
function safeDecodeOkfLinkPath(value) {
	if (!value) return "";
	try {
		return decodeURIComponent(value);
	} catch {
		return value;
	}
}
function getMarkdownDestinationSuffix(destination) {
	const queryIndex = destination.indexOf("?");
	const fragmentIndex = destination.indexOf("#");
	const suffixIndex = queryIndex === -1 ? fragmentIndex : fragmentIndex === -1 ? queryIndex : Math.min(queryIndex, fragmentIndex);
	return suffixIndex === -1 ? "" : destination.slice(suffixIndex);
}
function rewriteOkfMarkdownLinks(params) {
	const linkedConceptIds = [];
	const rewriteLinks = (markdown) => markdown.replace(OKF_MARKDOWN_LINK_PATTERN, (match, imagePrefix, label, rawTarget) => {
		const conceptId = resolveOkfMarkdownTarget(params.sourceRelativePath, rawTarget);
		if (!conceptId) return match;
		const target = params.pageByConceptId.get(conceptId);
		if (!target) return match;
		linkedConceptIds.push(conceptId);
		const { destination, titleSuffix } = splitMarkdownLinkDestination(rawTarget);
		return `${imagePrefix}[${label}](${path.posix.relative(path.posix.dirname(params.sourcePagePath), target.pagePath)}${getMarkdownDestinationSuffix(destination)}${titleSuffix})`;
	});
	return {
		body: rewriteMarkdownOutsideCode(params.body, rewriteLinks),
		linkedConceptIds: uniqueStrings(linkedConceptIds)
	};
}
function rewriteMarkdownLineOutsideInlineCode(line, rewriteLinks) {
	let result = "";
	let cursor = 0;
	while (cursor < line.length) {
		const codeStart = line.indexOf("`", cursor);
		if (codeStart === -1) {
			result += rewriteLinks(line.slice(cursor));
			break;
		}
		result += rewriteLinks(line.slice(cursor, codeStart));
		const delimiter = line.slice(codeStart).match(/^`+/)?.[0] ?? "`";
		const codeEnd = line.indexOf(delimiter, codeStart + delimiter.length);
		if (codeEnd === -1) {
			result += line.slice(codeStart);
			break;
		}
		result += line.slice(codeStart, codeEnd + delimiter.length);
		cursor = codeEnd + delimiter.length;
	}
	return result;
}
function rewriteMarkdownOutsideCode(markdown, rewriteLinks) {
	const lines = markdown.split(/(\n)/);
	let inFence = false;
	let fenceDelimiter = "";
	return lines.map((line) => {
		if (line === "\n") return line;
		const fenceMatch = line.match(OKF_FENCE_PATTERN);
		if (fenceMatch) {
			const delimiter = fenceMatch[1] ?? "";
			const closesFence = inFence && delimiter.startsWith(fenceDelimiter[0] ?? "") && delimiter.length >= fenceDelimiter.length;
			if (!inFence) {
				inFence = true;
				fenceDelimiter = delimiter;
			} else if (closesFence) {
				inFence = false;
				fenceDelimiter = "";
			}
			return line;
		}
		return inFence ? line : rewriteMarkdownLineOutsideInlineCode(line, rewriteLinks);
	}).join("");
}
function normalizeOkfRenderedPageForComparison(content) {
	const withoutRelated = content.replace(OKF_RELATED_SECTION_PATTERN, "\n");
	const frontmatterMatch = withoutRelated.match(/^---\n([\s\S]*?)\n---\n?/);
	if (!frontmatterMatch) return withoutRelated.trimEnd();
	const normalizedFrontmatter = frontmatterMatch[1]?.replace(OKF_VOLATILE_TIMESTAMP_LINE_PATTERN, "") ?? "";
	return `---\n${normalizedFrontmatter.endsWith("\n") ? normalizedFrontmatter : `${normalizedFrontmatter}\n`}---\n${withoutRelated.slice(frontmatterMatch[0].length)}`.trimEnd();
}
async function writeOkfConceptPage(params) {
	const vault = await root(params.vaultRoot);
	const pageStat = await vault.stat(params.pagePath).catch((error) => {
		if (error instanceof FsSafeError && (error.code === "not-found" || error.code === "path-alias")) return null;
		throw error;
	});
	const existing = pageStat ? await vault.readText(params.pagePath).catch(() => "") : "";
	if (existing === params.content || normalizeOkfRenderedPageForComparison(existing) === normalizeOkfRenderedPageForComparison(params.content)) return {
		changed: false,
		created: !pageStat
	};
	await writeGuardedVaultPage({
		vault,
		pagePath: params.pagePath,
		content: params.content,
		pageStat,
		pageLabel: "OKF concept page"
	});
	return {
		changed: true,
		created: !pageStat
	};
}
async function removeStaleOkfConceptPages(params) {
	const vault = await root(params.vaultRoot);
	const entries = await walkMemoryWikiDirectory(params.vaultRoot, "concepts", {
		maxDepth: 0,
		entryFilter: (entry) => entry.kind === "directory" ? "skip-subtree" : "include",
		onDirectoryError: "skip-and-report"
	});
	const removedPagePaths = [];
	for (const entry of entries) {
		const entryName = path.basename(entry.relativePath);
		if (entry.kind !== "file" || !entryName.endsWith(".md") || entryName === "index.md") continue;
		const pagePath = `concepts/${entryName}`;
		if (params.currentPagePaths.has(pagePath)) continue;
		const raw = await vault.readText(pagePath).catch(() => "");
		const okf = parseWikiMarkdown(raw).frontmatter.okf;
		if (okf && typeof okf === "object" && !Array.isArray(okf) && okf.bundleKey === params.bundleKey) {
			await vault.remove(pagePath);
			removedPagePaths.push(pagePath);
		}
	}
	return removedPagePaths;
}
function readRootOkfMetadata(params) {
	if (!params.rootIndex) return { key: createOkfBundleKey({
		rootFrontmatter: {},
		bundleName: params.bundleName,
		bundlePath: params.bundlePath
	}) };
	const parsed = parseOkfMarkdown(params.rootIndex, "index.md");
	return {
		key: createOkfBundleKey({
			rootFrontmatter: parsed.frontmatter,
			bundleName: params.bundleName,
			bundlePath: params.bundlePath
		}),
		...normalizeOptionalString(parsed.frontmatter.okf_version) ? { version: normalizeOptionalString(parsed.frontmatter.okf_version) } : {}
	};
}
function formatOkfImportSummary(result) {
	return `Imported ${result.importedCount} OKF concept${result.importedCount === 1 ? "" : "s"} from ${result.bundlePath} into memory wiki. Updated ${result.updatedCount}; removed ${result.removedCount}; skipped ${result.skippedCount}; refreshed ${result.indexUpdatedFiles.length} index file${result.indexUpdatedFiles.length === 1 ? "" : "s"}.`;
}
async function importMemoryWikiOkfBundle(params) {
	await initializeMemoryWikiVault(params.config, { nowMs: params.nowMs });
	const bundlePath = path.resolve(params.bundlePath);
	if (!(await fs.stat(bundlePath)).isDirectory()) throw new Error("wiki okf import expects an unpacked OKF bundle directory.");
	const warnings = [];
	const markdownFiles = await collectOkfMarkdownFiles(bundlePath, warnings);
	const concepts = [];
	let rootIndexContent;
	for (const relativePath of markdownFiles) {
		if (relativePath === "index.md") rootIndexContent = await readOkfTextFile({
			bundlePath,
			relativePath,
			warnings
		}) ?? void 0;
		if (OKF_RESERVED_FILENAMES.has(path.posix.basename(relativePath))) continue;
		const content = await readOkfTextFile({
			bundlePath,
			relativePath,
			warnings
		});
		if (content === null) continue;
		const normalized = normalizeOkfConcept({
			bundlePath,
			relativePath,
			content
		});
		if (normalized.warning) {
			warnings.push(normalized.warning);
			continue;
		}
		if (normalized.concept) concepts.push(normalized.concept);
	}
	const timestamp = resolveMemoryWikiTimestamp(params.nowMs);
	const bundleName = path.basename(bundlePath);
	const bundleMetadata = readRootOkfMetadata({
		rootIndex: rootIndexContent,
		bundleName,
		bundlePath
	});
	const bundleKey = bundleMetadata.key;
	const pageByConceptId = /* @__PURE__ */ new Map();
	for (const concept of concepts) pageByConceptId.set(concept.conceptId, {
		...createOkfPageIdentity(bundleKey, concept.conceptId),
		title: concept.title
	});
	const importedPages = [];
	let updatedCount = 0;
	await fs.mkdir(path.join(params.config.vault.path, "concepts"), { recursive: true });
	for (const concept of concepts.toSorted((left, right) => left.conceptId.localeCompare(right.conceptId))) {
		const page = pageByConceptId.get(concept.conceptId);
		if (!page) continue;
		const rewritten = rewriteOkfMarkdownLinks({
			body: concept.body,
			sourcePagePath: page.pagePath,
			sourceRelativePath: concept.relativePath,
			pageByConceptId
		});
		const relationships = rewritten.linkedConceptIds.flatMap((conceptId) => {
			const target = pageByConceptId.get(conceptId);
			return target ? [{
				targetId: target.pageId,
				targetPath: target.pagePath,
				targetTitle: target.title,
				kind: "okf-link",
				evidenceKind: "okf-markdown-link"
			}] : [];
		});
		const frontmatter = {
			pageType: "concept",
			id: page.pageId,
			title: concept.title,
			sourceType: "okf",
			provenanceMode: "okf-import",
			sourcePath: concept.absolutePath,
			okfConceptId: concept.conceptId,
			okfType: concept.type,
			sourceIds: [`source.okf.${bundleKey}`],
			importedAt: timestamp,
			updatedAt: concept.timestamp ?? timestamp,
			status: "active",
			...concept.description ? { description: concept.description } : {},
			...concept.resource ? { resource: concept.resource } : {},
			...concept.tags.length > 0 ? { tags: concept.tags } : {},
			...concept.timestamp ? { okfTimestamp: concept.timestamp } : {},
			...relationships.length > 0 ? { relationships } : {},
			okf: {
				...bundleMetadata.version ? { version: bundleMetadata.version } : {},
				bundleName,
				bundleKey,
				conceptId: concept.conceptId,
				sourceRelativePath: concept.relativePath,
				frontmatter: concept.frontmatter
			}
		};
		const writeResult = await writeOkfConceptPage({
			vaultRoot: params.config.vault.path,
			pagePath: page.pagePath,
			content: renderWikiMarkdown({
				frontmatter,
				body: rewritten.body
			})
		});
		if (!writeResult.created && writeResult.changed) updatedCount++;
		importedPages.push({
			conceptId: concept.conceptId,
			sourcePath: concept.absolutePath,
			pageId: page.pageId,
			pagePath: page.pagePath,
			title: concept.title,
			created: writeResult.created
		});
	}
	const currentPagePaths = new Set(importedPages.map((page) => page.pagePath));
	const removedPagePaths = warnings.length === 0 ? await removeStaleOkfConceptPages({
		vaultRoot: params.config.vault.path,
		bundleKey,
		currentPagePaths
	}) : [];
	await appendMemoryWikiLog(params.config.vault.path, {
		type: "okf-import",
		timestamp,
		details: {
			bundlePath,
			bundleName,
			importedCount: importedPages.length,
			updatedCount,
			removedCount: removedPagePaths.length,
			skippedCount: warnings.length,
			pagePaths: importedPages.map((page) => page.pagePath),
			removedPagePaths
		}
	});
	const compile = await compileMemoryWikiVault(params.config);
	return {
		bundlePath,
		bundleName,
		...bundleMetadata.version ? { okfVersion: bundleMetadata.version } : {},
		importedCount: importedPages.length,
		updatedCount,
		removedCount: removedPagePaths.length,
		skippedCount: warnings.length,
		pagePaths: importedPages.map((page) => page.pagePath),
		removedPagePaths,
		warnings,
		indexUpdatedFiles: compile.updatedFiles
	};
}
//#endregion
//#region extensions/memory-wiki/src/cli.ts
const WIKI_GATEWAY_TIMEOUT_MS = "30000";
const GATEWAY_TERMINAL_STRING_MAX_CHARS = 2e3;
const GATEWAY_RESPONSE_MAX_ARRAY_ITEMS = 1e4;
const GATEWAY_RESPONSE_MAX_STRING_CHARS = 1e4;
const GATEWAY_RESPONSE_MAX_CODE_CHARS = 256;
const ANSI_ESCAPE_SEQUENCE_PATTERN = new RegExp(String.raw`(?:\x1B\[[0-?]*[ -/]*[@-~]|\x1B[@-Z\\-_]|\x9B[0-?]*[ -/]*[@-~])`, "g");
const TERMINAL_CONTROL_CHARACTER_PATTERN = new RegExp(String.raw`[\x00-\x1F\x7F-\x9F]+`, "g");
const UNICODE_FORMAT_CONTROL_PATTERN = /[\u061C\u200B-\u200F\u202A-\u202E\u2060-\u206F\uFEFF]/g;
function sanitizeGatewayStringForTerminal(value) {
	const sanitized = (value.length > GATEWAY_TERMINAL_STRING_MAX_CHARS ? truncateUtf16Safe(value, GATEWAY_TERMINAL_STRING_MAX_CHARS) : value).replace(ANSI_ESCAPE_SEQUENCE_PATTERN, "").replace(TERMINAL_CONTROL_CHARACTER_PATTERN, " ").replace(UNICODE_FORMAT_CONTROL_PATTERN, "");
	return value.length > GATEWAY_TERMINAL_STRING_MAX_CHARS ? `${sanitized}... [truncated]` : sanitized;
}
function escapeGatewayJsonForTerminal(json) {
	return json.replace(UNICODE_FORMAT_CONTROL_PATTERN, (char) => {
		const codePoint = char.codePointAt(0);
		return typeof codePoint === "number" ? `\\u${codePoint.toString(16).padStart(4, "0")}` : "";
	});
}
function writeOutput(output, writer = process.stdout) {
	writer.write(output.endsWith("\n") ? output : `${output}\n`);
}
function shouldRouteBridgeRuntimeThroughGateway(config) {
	return config.vaultMode === "bridge" && config.bridge.enabled && config.bridge.readMemoryArtifacts;
}
function isBoundedGatewayString(value, maxChars = GATEWAY_RESPONSE_MAX_STRING_CHARS) {
	return typeof value === "string" && value.length <= maxChars;
}
function isStringArray(value, maxChars = GATEWAY_RESPONSE_MAX_STRING_CHARS) {
	return Array.isArray(value) && value.length <= GATEWAY_RESPONSE_MAX_ARRAY_ITEMS && value.every((item) => isBoundedGatewayString(item, maxChars));
}
function hasNumberFields(value, keys) {
	return keys.every((key) => typeof value[key] === "number");
}
function isWarningList(value) {
	return Array.isArray(value) && value.length <= GATEWAY_RESPONSE_MAX_ARRAY_ITEMS && value.every((item) => isRecord(item) && isBoundedGatewayString(item.code, GATEWAY_RESPONSE_MAX_CODE_CHARS) && isBoundedGatewayString(item.message));
}
function isMemoryWikiStatus(value) {
	if (!isRecord(value)) return false;
	const bridge = value.bridge;
	const obsidianCli = value.obsidianCli;
	const unsafeLocal = value.unsafeLocal;
	const pageCounts = value.pageCounts;
	const sourceCounts = value.sourceCounts;
	return isBoundedGatewayString(value.vaultScope, GATEWAY_RESPONSE_MAX_CODE_CHARS) && (isBoundedGatewayString(value.agentId, GATEWAY_RESPONSE_MAX_CODE_CHARS) || value.agentId === null) && isBoundedGatewayString(value.vaultMode, GATEWAY_RESPONSE_MAX_CODE_CHARS) && isBoundedGatewayString(value.renderMode, GATEWAY_RESPONSE_MAX_CODE_CHARS) && isBoundedGatewayString(value.vaultPath) && typeof value.vaultExists === "boolean" && (typeof value.bridgePublicArtifactCount === "number" || value.bridgePublicArtifactCount === null) && isRecord(bridge) && typeof bridge.enabled === "boolean" && isRecord(obsidianCli) && typeof obsidianCli.enabled === "boolean" && typeof obsidianCli.requested === "boolean" && typeof obsidianCli.available === "boolean" && (isBoundedGatewayString(obsidianCli.command) || obsidianCli.command === null) && isRecord(unsafeLocal) && typeof unsafeLocal.allowPrivateMemoryCoreAccess === "boolean" && typeof unsafeLocal.pathCount === "number" && isRecord(pageCounts) && hasNumberFields(pageCounts, [
		"source",
		"entity",
		"concept",
		"synthesis",
		"report"
	]) && isRecord(sourceCounts) && hasNumberFields(sourceCounts, [
		"native",
		"bridge",
		"bridgeEvents",
		"unsafeLocal",
		"other"
	]) && isWarningList(value.warnings);
}
function isMemoryWikiDoctorReport(value) {
	return isRecord(value) && typeof value.healthy === "boolean" && typeof value.warningCount === "number" && isMemoryWikiStatus(value.status) && Array.isArray(value.fixes) && value.fixes.length <= GATEWAY_RESPONSE_MAX_ARRAY_ITEMS && value.fixes.every((item) => isRecord(item) && isBoundedGatewayString(item.code, GATEWAY_RESPONSE_MAX_CODE_CHARS) && isBoundedGatewayString(item.message));
}
function isMemoryWikiImportResult(value) {
	return isRecord(value) && hasNumberFields(value, [
		"importedCount",
		"updatedCount",
		"skippedCount",
		"removedCount",
		"artifactCount",
		"workspaces"
	]) && isStringArray(value.pagePaths) && typeof value.indexesRefreshed === "boolean" && isStringArray(value.indexUpdatedFiles) && isBoundedGatewayString(value.indexRefreshReason, GATEWAY_RESPONSE_MAX_CODE_CHARS);
}
function validateWikiGatewayResult(method, value) {
	if (method === "wiki.status" && isMemoryWikiStatus(value)) return value;
	if (method === "wiki.doctor" && isMemoryWikiDoctorReport(value)) return value;
	if (method === "wiki.bridge.import" && isMemoryWikiImportResult(value)) return value;
	throw new Error(`Invalid Gateway response for ${method}.`);
}
async function callWikiGateway(method, agentId) {
	return validateWikiGatewayResult(method, await callGatewayFromCli(method, { timeout: WIKI_GATEWAY_TIMEOUT_MS }, agentId ? { agentId } : void 0, { progress: false }));
}
function normalizeCliStringList(values) {
	if (!values) return;
	const uniqueValues = uniqueStrings(normalizeStringEntries(values));
	return uniqueValues.length > 0 ? uniqueValues : void 0;
}
function collectCliValues(value, acc = []) {
	acc.push(value);
	return acc;
}
function parseWikiSearchEnumOption(value, allowed, label) {
	if (allowed.includes(value)) return value;
	throw new Error(`Invalid ${label}: ${value}. Expected one of: ${allowed.join(", ")}`);
}
async function resolveWikiApplyBody(params) {
	if (params.body?.trim()) return params.body;
	if (params.bodyFile?.trim()) return await fs.readFile(params.bodyFile, "utf8");
	throw new Error("wiki apply synthesis requires --body or --body-file.");
}
function formatMemoryWikiMutationSummary(result, json) {
	if (json) return JSON.stringify(result, null, 2);
	return `${result.changed ? "Updated" : "No changes for"} ${result.pagePath} via ${result.operation}. ${result.compile.updatedFiles.length > 0 ? `Refreshed ${result.compile.updatedFiles.length} index file${result.compile.updatedFiles.length === 1 ? "" : "s"}.` : "Indexes unchanged."}`;
}
function formatJsonOrText(result, json, render) {
	return json ? JSON.stringify(result, null, 2) : render(result);
}
function formatGatewayJsonOrText(result, json, render) {
	return json ? escapeGatewayJsonForTerminal(JSON.stringify(result, null, 2)) : sanitizeGatewayStringForTerminal(render(result));
}
async function runWikiCommandWithSummary(params) {
	const result = await params.run();
	writeOutput(formatJsonOrText(result, params.json, params.render), params.stdout);
	return result;
}
async function runSyncedWikiCommandWithSummary(params) {
	await syncMemoryWikiImportedSources({
		config: params.config,
		appConfig: params.appConfig
	});
	return runWikiCommandWithSummary(params);
}
function addWikiSearchConfigOptions(command) {
	return command.option("--backend <backend>", `Search backend (${WIKI_SEARCH_BACKENDS.join(", ")})`, (value) => parseWikiSearchEnumOption(value, WIKI_SEARCH_BACKENDS, "backend")).option("--corpus <corpus>", `Search corpus (${WIKI_SEARCH_CORPORA.join(", ")})`, (value) => parseWikiSearchEnumOption(value, WIKI_SEARCH_CORPORA, "corpus"));
}
function invalidCliArgument(message) {
	const error = new Error(message);
	error.name = "InvalidArgumentError";
	error.code = "commander.invalidArgument";
	error.exitCode = 1;
	return error;
}
function parseWikiConfidenceOption(value) {
	const trimmed = value.trim();
	const confidence = /^[+-]?(?:\d+(?:\.\d+)?|\.\d+)$/.test(trimmed) ? Number(trimmed) : NaN;
	if (!Number.isFinite(confidence) || confidence < 0 || confidence > 1) throw invalidCliArgument("--confidence must be a number between 0 and 1.");
	return confidence;
}
function parseWikiPositiveIntegerOption(value, flag) {
	const parsed = parseStrictPositiveInteger(value);
	if (parsed === void 0) throw invalidCliArgument(`${flag} must be a positive integer.`);
	return parsed;
}
function addWikiApplyMutationOptions(command) {
	return command.option("--source-id <id>", "Source id", collectCliValues).option("--contradiction <text>", "Contradiction note", collectCliValues).option("--question <text>", "Open question", collectCliValues).option("--confidence <n>", "Confidence score between 0 and 1", parseWikiConfidenceOption).option("--status <status>", "Page status");
}
async function runWikiStatus(params) {
	const routeThroughGateway = shouldRouteBridgeRuntimeThroughGateway(params.config);
	const status = routeThroughGateway ? await callWikiGateway("wiki.status", params.agentId) : await (async () => {
		await syncMemoryWikiImportedSources({
			config: params.config,
			appConfig: params.appConfig
		});
		return await resolveMemoryWikiStatus(params.config, { appConfig: params.appConfig });
	})();
	writeOutput(routeThroughGateway ? formatGatewayJsonOrText(status, params.json, renderMemoryWikiStatus) : formatJsonOrText(status, params.json, renderMemoryWikiStatus), params.stdout);
	return status;
}
async function runWikiDoctor(params) {
	const routeThroughGateway = shouldRouteBridgeRuntimeThroughGateway(params.config);
	const report = routeThroughGateway ? await callWikiGateway("wiki.doctor", params.agentId) : await (async () => {
		await syncMemoryWikiImportedSources({
			config: params.config,
			appConfig: params.appConfig
		});
		return buildMemoryWikiDoctorReport(await resolveMemoryWikiStatus(params.config, { appConfig: params.appConfig }));
	})();
	if (!report.healthy) process.exitCode = 1;
	writeOutput(routeThroughGateway ? formatGatewayJsonOrText(report, params.json, renderMemoryWikiDoctor) : formatJsonOrText(report, params.json, renderMemoryWikiDoctor), params.stdout);
	return report;
}
async function runWikiInit(params) {
	return runWikiCommandWithSummary({
		json: params.json,
		stdout: params.stdout,
		run: () => initializeMemoryWikiVault(params.config),
		render: (value) => `Initialized wiki vault at ${value.rootDir} (${value.createdDirectories.length} dirs, ${value.createdFiles.length} files).`
	});
}
async function runWikiCompile(params) {
	return runSyncedWikiCommandWithSummary({
		config: params.config,
		appConfig: params.appConfig,
		json: params.json,
		stdout: params.stdout,
		run: () => compileMemoryWikiVault(params.config),
		render: (value) => `Compiled wiki vault at ${value.vaultRoot} (${value.pages.length} pages, ${value.updatedFiles.length} indexes updated).`
	});
}
async function runWikiLint(params) {
	return runSyncedWikiCommandWithSummary({
		config: params.config,
		appConfig: params.appConfig,
		json: params.json,
		stdout: params.stdout,
		run: () => lintMemoryWikiVault(params.config),
		render: (value) => `Linted wiki vault at ${value.vaultRoot} (${value.issueCount} issues, report: ${value.reportPath}).`
	});
}
async function runWikiIngest(params) {
	return runWikiCommandWithSummary({
		json: params.json,
		stdout: params.stdout,
		run: () => ingestMemoryWikiSource({
			config: params.config,
			inputPath: params.inputPath,
			title: params.title
		}),
		render: (value) => `Ingested ${value.sourcePath} into ${value.pagePath}. Refreshed ${value.indexUpdatedFiles.length} index file${value.indexUpdatedFiles.length === 1 ? "" : "s"}.`
	});
}
async function runWikiOkfImport(params) {
	return runWikiCommandWithSummary({
		json: params.json,
		stdout: params.stdout,
		run: () => importMemoryWikiOkfBundle({
			config: params.config,
			bundlePath: params.bundlePath
		}),
		render: formatOkfImportSummary
	});
}
async function runWikiSearch(params) {
	if (params.mode && !WIKI_SEARCH_MODES.includes(params.mode)) throw new Error(`wiki search --mode must be one of: ${WIKI_SEARCH_MODES.join(", ")}.`);
	await syncMemoryWikiImportedSources({
		config: params.config,
		appConfig: params.appConfig
	});
	const results = await searchMemoryWiki({
		config: params.config,
		appConfig: params.appConfig,
		...params.agentId ? { agentId: params.agentId } : {},
		query: params.query,
		maxResults: params.maxResults,
		searchBackend: params.searchBackend,
		searchCorpus: params.searchCorpus,
		mode: params.mode
	});
	writeOutput(params.json ? JSON.stringify(results, null, 2) : results.length === 0 ? "No wiki or memory results." : results.map((result, index) => `${index + 1}. ${result.title} (${result.corpus}/${result.kind})\nPath: ${result.path}${typeof result.startLine === "number" && typeof result.endLine === "number" ? `\nLines: ${result.startLine}-${result.endLine}` : ""}${result.provenanceLabel ? `\nProvenance: ${result.provenanceLabel}` : ""}${result.matchedClaimId ? `\nClaim: ${result.matchedClaimId}` : ""}${result.evidenceKinds && result.evidenceKinds.length > 0 ? `\nEvidence: ${result.evidenceKinds.join(", ")}` : ""}\nSnippet: ${result.snippet}`).join("\n\n"), params.stdout);
	return results;
}
async function runWikiGet(params) {
	await syncMemoryWikiImportedSources({
		config: params.config,
		appConfig: params.appConfig
	});
	const result = await getMemoryWikiPage({
		config: params.config,
		appConfig: params.appConfig,
		...params.agentId ? { agentId: params.agentId } : {},
		lookup: params.lookup,
		fromLine: params.fromLine,
		lineCount: params.lineCount,
		searchBackend: params.searchBackend,
		searchCorpus: params.searchCorpus
	});
	writeOutput(params.json ? JSON.stringify(result, null, 2) : result?.content ?? `Wiki page not found: ${params.lookup}`, params.stdout);
	return result;
}
async function runWikiApplySynthesis(params) {
	const sourceIds = normalizeCliStringList(params.sourceIds);
	if (!sourceIds) throw new Error("wiki apply synthesis requires at least one --source-id.");
	const body = await resolveWikiApplyBody({
		body: params.body,
		bodyFile: params.bodyFile
	});
	await syncMemoryWikiImportedSources({
		config: params.config,
		appConfig: params.appConfig
	});
	const result = await applyMemoryWikiMutation({
		config: params.config,
		mutation: {
			op: "create_synthesis",
			title: params.title,
			body,
			sourceIds,
			...normalizeCliStringList(params.contradictions) ? { contradictions: normalizeCliStringList(params.contradictions) } : {},
			...normalizeCliStringList(params.questions) ? { questions: normalizeCliStringList(params.questions) } : {},
			...typeof params.confidence === "number" ? { confidence: params.confidence } : {},
			...params.status?.trim() ? { status: params.status.trim() } : {}
		}
	});
	writeOutput(formatMemoryWikiMutationSummary(result, params.json), params.stdout);
	return result;
}
async function runWikiApplyMetadata(params) {
	await syncMemoryWikiImportedSources({
		config: params.config,
		appConfig: params.appConfig
	});
	const result = await applyMemoryWikiMutation({
		config: params.config,
		mutation: {
			op: "update_metadata",
			lookup: params.lookup,
			...normalizeCliStringList(params.sourceIds) ? { sourceIds: normalizeCliStringList(params.sourceIds) } : {},
			...normalizeCliStringList(params.contradictions) ? { contradictions: normalizeCliStringList(params.contradictions) } : {},
			...normalizeCliStringList(params.questions) ? { questions: normalizeCliStringList(params.questions) } : {},
			...params.clearConfidence ? { confidence: null } : typeof params.confidence === "number" ? { confidence: params.confidence } : {},
			...params.status?.trim() ? { status: params.status.trim() } : {}
		}
	});
	writeOutput(formatMemoryWikiMutationSummary(result, params.json), params.stdout);
	return result;
}
async function runWikiBridgeImport(params) {
	const render = (value) => `Bridge import synced ${value.artifactCount} artifacts across ${value.workspaces} workspaces (${value.importedCount} new, ${value.updatedCount} updated, ${value.skippedCount} unchanged, ${value.removedCount} removed). Indexes ${value.indexesRefreshed ? `refreshed (${value.indexUpdatedFiles.length} files)` : `not refreshed (${value.indexRefreshReason})`}.`;
	if (shouldRouteBridgeRuntimeThroughGateway(params.config)) {
		const result = await callWikiGateway("wiki.bridge.import", params.agentId);
		writeOutput(formatGatewayJsonOrText(result, params.json, render), params.stdout);
		return result;
	}
	return runWikiCommandWithSummary({
		json: params.json,
		stdout: params.stdout,
		run: () => syncMemoryWikiImportedSources({
			config: params.config,
			appConfig: params.appConfig
		}),
		render
	});
}
async function runWikiUnsafeLocalImport(params) {
	if (params.config.vault.scope === "agent") throw new Error("Unsafe-local import does not support memory-wiki vault.scope=agent.");
	return runWikiCommandWithSummary({
		json: params.json,
		stdout: params.stdout,
		run: () => syncMemoryWikiImportedSources({
			config: params.config,
			appConfig: params.appConfig
		}),
		render: (value) => `Unsafe-local import synced ${value.artifactCount} artifacts (${value.importedCount} new, ${value.updatedCount} updated, ${value.skippedCount} unchanged, ${value.removedCount} removed). Indexes ${value.indexesRefreshed ? `refreshed (${value.indexUpdatedFiles.length} files)` : `not refreshed (${value.indexRefreshReason})`}.`
	});
}
async function runWikiObsidianStatus(params) {
	return runWikiCommandWithSummary({
		json: params.json,
		stdout: params.stdout,
		run: () => probeObsidianCli(),
		render: (value) => value.available ? `Obsidian CLI available at ${value.command}` : "Obsidian CLI is not available on PATH."
	});
}
function assertOfficialObsidianCliSupported(config) {
	if (config.vault.scope === "agent") throw new Error("Official Obsidian CLI actions do not support memory-wiki vault.scope=agent.");
}
async function runWikiObsidianSearch(params) {
	assertOfficialObsidianCliSupported(params.config);
	return runWikiCommandWithSummary({
		json: params.json,
		stdout: params.stdout,
		run: () => runObsidianSearch({
			config: params.config,
			query: params.query
		}),
		render: (value) => value.stdout.trim()
	});
}
async function runWikiObsidianOpenCli(params) {
	assertOfficialObsidianCliSupported(params.config);
	return runWikiCommandWithSummary({
		json: params.json,
		stdout: params.stdout,
		run: () => runObsidianOpen({
			config: params.config,
			vaultPath: params.vaultPath
		}),
		render: (value) => value.stdout.trim() || "Opened in Obsidian."
	});
}
async function runWikiObsidianCommandCli(params) {
	assertOfficialObsidianCliSupported(params.config);
	return runWikiCommandWithSummary({
		json: params.json,
		stdout: params.stdout,
		run: () => runObsidianCommand({
			config: params.config,
			id: params.id
		}),
		render: (value) => value.stdout.trim() || "Command sent to Obsidian."
	});
}
async function runWikiObsidianDailyCli(params) {
	assertOfficialObsidianCliSupported(params.config);
	return runWikiCommandWithSummary({
		json: params.json,
		stdout: params.stdout,
		run: () => runObsidianDaily({ config: params.config }),
		render: (value) => value.stdout.trim() || "Opened today's daily note."
	});
}
function formatChatGptImportSummary(result) {
	if (result.dryRun) return `ChatGPT import dry run scanned ${result.conversationCount} conversations (${result.createdCount} new, ${result.updatedCount} updated, ${result.skippedCount} unchanged).`;
	const runSuffix = result.runId ? ` Run id: ${result.runId}.` : "";
	return `ChatGPT import applied ${result.conversationCount} conversations (${result.createdCount} new, ${result.updatedCount} updated, ${result.skippedCount} unchanged). Refreshed ${result.indexUpdatedFiles.length} index file${result.indexUpdatedFiles.length === 1 ? "" : "s"}.${runSuffix}`;
}
function formatChatGptRollbackSummary(result) {
	const preservedNote = result.preservedPaths.length > 0 ? ` Preserved ${result.preservedPaths.length} page${result.preservedPaths.length === 1 ? "" : "s"} edited after import: ${result.preservedPaths.map((entry) => entry.recoveryPath).join(", ")}.` : "";
	if (result.alreadyRolledBack) return `ChatGPT import run ${result.runId} was already rolled back.${preservedNote}`;
	return `Rolled back ChatGPT import run ${result.runId} (${result.removedCount} removed, ${result.restoredCount} restored).${preservedNote} Refreshed ${result.indexUpdatedFiles.length} index file${result.indexUpdatedFiles.length === 1 ? "" : "s"}.`;
}
async function runWikiChatGptImport(params) {
	return runWikiCommandWithSummary({
		json: params.json,
		stdout: params.stdout,
		run: () => importChatGptConversations({
			config: params.config,
			exportPath: params.exportPath,
			dryRun: params.dryRun
		}),
		render: formatChatGptImportSummary
	});
}
async function runWikiChatGptRollback(params) {
	return runWikiCommandWithSummary({
		json: params.json,
		stdout: params.stdout,
		run: () => rollbackChatGptImportRun({
			config: params.config,
			runId: params.runId
		}),
		render: formatChatGptRollbackSummary
	});
}
function registerWikiCli(program, registration) {
	const resolveConfig = registration.resolveConfig ?? ((agentId, currentAppConfig) => resolveMemoryWikiAgentConfig({
		config: registration.config,
		appConfig: currentAppConfig,
		...agentId ? { agentId } : {}
	}));
	let commandContext;
	const requireCommandContext = () => {
		if (!commandContext) throw new Error("Memory Wiki CLI agent context was not resolved.");
		return commandContext;
	};
	const wiki = program.command("wiki").description("Inspect and initialize the memory wiki vault").option("--agent <id>", "Agent id for agent-scoped wiki vaults");
	wiki.hook("preAction", (_thisCommand, actionCommand) => {
		const needsAgent = actionCommand.options.some((option) => option.long === "--agent");
		const requestedAgentId = actionCommand.opts().agent?.trim() || wiki.opts().agent?.trim() || void 0;
		const currentAppConfig = registration.getAppConfig?.();
		let agentId = requestedAgentId;
		if (needsAgent && !agentId && (registration.config.vault.scope === "agent" || currentAppConfig)) try {
			agentId = resolveDefaultAgentId(currentAppConfig ?? {});
		} catch {
			throw new Error("No default memory-wiki agent is configured. Pass --agent <id>, or add an agent with `openclaw agents add`.");
		}
		const config = needsAgent ? resolveConfig(agentId, currentAppConfig) : registration.config;
		agentId = config.agentId ?? agentId;
		commandContext = {
			config,
			...currentAppConfig ? { appConfig: currentAppConfig } : {},
			...agentId ? { agentId } : {}
		};
	});
	wiki.command("status").description("Show wiki vault status").option("--agent <id>", "Agent id (default: configured default agent)").option("--json", "Print JSON").action(async (opts) => {
		const { agentId, appConfig, config } = requireCommandContext();
		await runWikiStatus({
			config,
			appConfig,
			agentId,
			json: opts.json
		});
	});
	wiki.command("doctor").description("Audit wiki vault setup and report actionable fixes").option("--agent <id>", "Agent id (default: configured default agent)").option("--json", "Print JSON").action(async (opts) => {
		const { agentId, appConfig, config } = requireCommandContext();
		await runWikiDoctor({
			config,
			appConfig,
			agentId,
			json: opts.json
		});
	});
	wiki.command("init").description("Initialize the wiki vault layout").option("--agent <id>", "Agent id (default: configured default agent)").option("--json", "Print JSON").action(async (opts) => {
		const { config } = requireCommandContext();
		await runWikiInit({
			config,
			json: opts.json
		});
	});
	wiki.command("compile").description("Refresh generated wiki indexes").option("--agent <id>", "Agent id (default: configured default agent)").option("--json", "Print JSON").action(async (opts) => {
		const { appConfig, config } = requireCommandContext();
		await runWikiCompile({
			config,
			appConfig,
			json: opts.json
		});
	});
	wiki.command("lint").description("Lint the wiki vault and write a report").option("--agent <id>", "Agent id (default: configured default agent)").option("--json", "Print JSON").action(async (opts) => {
		const { appConfig, config } = requireCommandContext();
		await runWikiLint({
			config,
			appConfig,
			json: opts.json
		});
	});
	wiki.command("ingest").description("Ingest a local file into the wiki sources folder").argument("<path>", "Local file path to ingest").option("--agent <id>", "Agent id (default: configured default agent)").option("--title <title>", "Override the source title").option("--json", "Print JSON").action(async (inputPath, opts) => {
		const { config } = requireCommandContext();
		await runWikiIngest({
			config,
			inputPath,
			title: opts.title,
			json: opts.json
		});
	});
	wiki.command("okf").description("Import Open Knowledge Format bundles").command("import").description("Import an unpacked OKF bundle into wiki concept pages").argument("<path>", "OKF bundle directory").option("--agent <id>", "Agent id (default: configured default agent)").option("--json", "Print JSON").action(async (bundlePath, opts) => {
		const { config } = requireCommandContext();
		await runWikiOkfImport({
			config,
			bundlePath,
			json: opts.json
		});
	});
	addWikiSearchConfigOptions(wiki.command("search").description("Search wiki pages and, when configured, the active memory corpus").argument("<query>", "Search query").option("--agent <id>", "Agent id (default: configured default agent)").option("--max-results <n>", "Maximum results", (value) => parseWikiPositiveIntegerOption(value, "--max-results")).option("--mode <mode>", `Search mode (${WIKI_SEARCH_MODES.join(", ")})`)).option("--json", "Print JSON").action(async (query, opts) => {
		const { agentId, appConfig, config } = requireCommandContext();
		await runWikiSearch({
			config,
			appConfig,
			agentId,
			query,
			maxResults: opts.maxResults,
			searchBackend: opts.backend,
			searchCorpus: opts.corpus,
			mode: opts.mode,
			json: opts.json
		});
	});
	addWikiSearchConfigOptions(wiki.command("get").description("Read a wiki page by id or relative path, with optional active-memory fallback").argument("<lookup>", "Relative path or page id").option("--agent <id>", "Agent id (default: configured default agent)").option("--from <n>", "Start line", (value) => parseWikiPositiveIntegerOption(value, "--from")).option("--lines <n>", "Number of lines", (value) => parseWikiPositiveIntegerOption(value, "--lines"))).option("--json", "Print JSON").action(async (lookup, opts) => {
		const { agentId, appConfig, config } = requireCommandContext();
		await runWikiGet({
			config,
			appConfig,
			agentId,
			lookup,
			fromLine: opts.from,
			lineCount: opts.lines,
			searchBackend: opts.backend,
			searchCorpus: opts.corpus,
			json: opts.json
		});
	});
	const apply = wiki.command("apply").description("Apply narrow wiki mutations");
	addWikiApplyMutationOptions(apply.command("synthesis").description("Create or refresh a synthesis page with managed summary content").argument("<title>", "Synthesis title").option("--agent <id>", "Agent id (default: configured default agent)").option("--body <text>", "Summary body text").option("--body-file <path>", "Read summary body text from a file")).option("--json", "Print JSON").action(async (title, opts) => {
		const { appConfig, config } = requireCommandContext();
		await runWikiApplySynthesis({
			config,
			appConfig,
			title,
			body: opts.body,
			bodyFile: opts.bodyFile,
			sourceIds: opts.sourceId,
			contradictions: opts.contradiction,
			questions: opts.question,
			confidence: opts.confidence,
			status: opts.status,
			json: opts.json
		});
	});
	addWikiApplyMutationOptions(apply.command("metadata").description("Update metadata on an existing page").argument("<lookup>", "Relative path or page id").option("--agent <id>", "Agent id (default: configured default agent)")).option("--clear-confidence", "Remove any stored confidence value").option("--json", "Print JSON").action(async (lookup, opts) => {
		const { appConfig, config } = requireCommandContext();
		await runWikiApplyMetadata({
			config,
			appConfig,
			lookup,
			sourceIds: opts.sourceId,
			contradictions: opts.contradiction,
			questions: opts.question,
			confidence: opts.confidence,
			clearConfidence: opts.clearConfidence,
			status: opts.status,
			json: opts.json
		});
	});
	wiki.command("bridge").description("Import public memory artifacts into the wiki vault").command("import").description("Sync bridge-backed memory artifacts into wiki source pages").option("--agent <id>", "Agent id (default: configured default agent)").option("--json", "Print JSON").action(async (opts) => {
		const { agentId, appConfig, config } = requireCommandContext();
		await runWikiBridgeImport({
			config,
			appConfig,
			agentId,
			json: opts.json
		});
	});
	wiki.command("unsafe-local").description("Import explicitly configured private local paths into wiki source pages").command("import").description("Sync unsafe-local configured paths into wiki source pages").option("--json", "Print JSON").action(async (opts) => {
		const { appConfig, config } = requireCommandContext();
		await runWikiUnsafeLocalImport({
			config,
			appConfig,
			json: opts.json
		});
	});
	const chatgpt = wiki.command("chatgpt").description("Import ChatGPT export history into wiki source pages");
	chatgpt.command("import").description("Import a ChatGPT export into draft wiki source pages").requiredOption("--export <path>", "ChatGPT export directory or conversations.json path").option("--agent <id>", "Agent id (default: configured default agent)").option("--dry-run", "Preview changes without writing", false).option("--json", "Print JSON").action(async (opts) => {
		const { config } = requireCommandContext();
		await runWikiChatGptImport({
			config,
			exportPath: opts.export,
			dryRun: opts.dryRun,
			json: opts.json
		});
	});
	chatgpt.command("rollback").description("Roll back a previously applied ChatGPT import run").argument("<run-id>", "Import run id").option("--agent <id>", "Agent id (default: configured default agent)").option("--json", "Print JSON").action(async (runId, opts) => {
		const { config } = requireCommandContext();
		await runWikiChatGptRollback({
			config,
			runId,
			json: opts.json
		});
	});
	const obsidian = wiki.command("obsidian").description("Run official Obsidian CLI helpers");
	obsidian.command("status").description("Probe the Obsidian CLI").option("--json", "Print JSON").action(async (opts) => {
		const { config } = requireCommandContext();
		await runWikiObsidianStatus({
			config,
			json: opts.json
		});
	});
	obsidian.command("search").description("Search the current Obsidian vault").argument("<query>", "Search query").option("--json", "Print JSON").action(async (query, opts) => {
		const { config } = requireCommandContext();
		await runWikiObsidianSearch({
			config,
			query,
			json: opts.json
		});
	});
	obsidian.command("open").description("Open a file in Obsidian by vault-relative path").argument("<path>", "Vault-relative path").option("--json", "Print JSON").action(async (vaultPath, opts) => {
		const { config } = requireCommandContext();
		await runWikiObsidianOpenCli({
			config,
			vaultPath,
			json: opts.json
		});
	});
	obsidian.command("command").description("Execute an Obsidian command palette command by id").argument("<id>", "Obsidian command id").option("--json", "Print JSON").action(async (id, opts) => {
		const { config } = requireCommandContext();
		await runWikiObsidianCommandCli({
			config,
			id,
			json: opts.json
		});
	});
	obsidian.command("daily").description("Open today's daily note in Obsidian").option("--json", "Print JSON").action(async (opts) => {
		const { config } = requireCommandContext();
		await runWikiObsidianDailyCli({
			config,
			json: opts.json
		});
	});
}
//#endregion
export { registerWikiCli };