mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-19 00:52:10 -06:00
623a015928
* fix(memory-wiki): preserve user edits when rolling back ChatGPT imports Rollback deleted created pages and overwrote updated pages unconditionally, destroying content the user added after the import with no recovery copy. Import runs now record a content hash of each written page after vault compile, and rollback preserves any page whose current content no longer matches into the run's recovered directory before deleting or restoring. Legacy run records without hashes preserve unconditionally. Fixes #116457 * fix(memory-wiki): move pages aside atomically during rollback Rollback now renames the live page into the recovery location before inspecting it, so a concurrent external save cannot land between the content read and the delete or snapshot restore. Matching pages drop the moved copy; mismatching pages keep it as the recovery file. * fix(memory-wiki): make rollback snapshot restore collision-safe The snapshot restore wrote directly to the page path after the move-aside, so a page recreated by an editor in that window was overwritten with no recovery copy. Restore now creates the snapshot exclusively and on collision moves the recreated page aside and retries; recovery filenames are uniqued so a second move-aside cannot clobber an earlier preserved copy. * fix(memory-wiki): record rollback hashes at write time * fix(memory-wiki): make ChatGPT rollback retry-safe * fix(memory-wiki): fence ChatGPT rollback phases --------- Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
1273 lines
42 KiB
TypeScript
1273 lines
42 KiB
TypeScript
// Memory Wiki plugin module implements cli behavior.
|
|
import fs from "node:fs/promises";
|
|
import type { Command } from "commander";
|
|
import { callGatewayFromCli } from "openclaw/plugin-sdk/gateway-runtime";
|
|
import { parseStrictPositiveInteger } from "openclaw/plugin-sdk/number-runtime";
|
|
import {
|
|
isRecord,
|
|
normalizeStringEntries,
|
|
uniqueStrings,
|
|
} from "openclaw/plugin-sdk/string-coerce-runtime";
|
|
import { truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
|
|
import type { OpenClawConfig } from "../api.js";
|
|
import { applyMemoryWikiMutation } from "./apply.js";
|
|
import {
|
|
importChatGptConversations,
|
|
rollbackChatGptImportRun,
|
|
type ChatGptImportResult,
|
|
type ChatGptRollbackResult,
|
|
} from "./chatgpt-import.js";
|
|
import { compileMemoryWikiVault } from "./compile.js";
|
|
import {
|
|
resolveMemoryWikiAgentConfig,
|
|
WIKI_SEARCH_BACKENDS,
|
|
WIKI_SEARCH_CORPORA,
|
|
type MemoryWikiConfigResolver,
|
|
type ResolvedMemoryWikiConfig,
|
|
} from "./config.js";
|
|
import { ingestMemoryWikiSource } from "./ingest.js";
|
|
import { lintMemoryWikiVault } from "./lint.js";
|
|
import {
|
|
probeObsidianCli,
|
|
runObsidianCommand,
|
|
runObsidianDaily,
|
|
runObsidianOpen,
|
|
runObsidianSearch,
|
|
} from "./obsidian.js";
|
|
import { formatOkfImportSummary, importMemoryWikiOkfBundle } from "./okf.js";
|
|
import {
|
|
getMemoryWikiPage,
|
|
searchMemoryWiki,
|
|
WIKI_SEARCH_MODES,
|
|
type WikiSearchMode,
|
|
} from "./query.js";
|
|
import { syncMemoryWikiImportedSources } from "./source-sync.js";
|
|
import type { MemoryWikiImportedSourceSyncResult } from "./source-sync.js";
|
|
import {
|
|
buildMemoryWikiDoctorReport,
|
|
renderMemoryWikiDoctor,
|
|
renderMemoryWikiStatus,
|
|
type MemoryWikiDoctorReport,
|
|
type MemoryWikiStatus,
|
|
resolveMemoryWikiStatus,
|
|
} from "./status.js";
|
|
import { initializeMemoryWikiVault } from "./vault.js";
|
|
|
|
const WIKI_GATEWAY_TIMEOUT_MS = "30000";
|
|
const GATEWAY_TERMINAL_STRING_MAX_CHARS = 2_000;
|
|
const GATEWAY_RESPONSE_MAX_ARRAY_ITEMS = 10_000;
|
|
const GATEWAY_RESPONSE_MAX_STRING_CHARS = 10_000;
|
|
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;
|
|
|
|
type WikiStatusCommandOptions = {
|
|
json?: boolean;
|
|
};
|
|
|
|
type WikiDoctorCommandOptions = {
|
|
json?: boolean;
|
|
};
|
|
|
|
type WikiInitCommandOptions = {
|
|
json?: boolean;
|
|
};
|
|
|
|
type WikiCompileCommandOptions = {
|
|
json?: boolean;
|
|
};
|
|
|
|
type WikiLintCommandOptions = {
|
|
json?: boolean;
|
|
};
|
|
|
|
type WikiIngestCommandOptions = {
|
|
json?: boolean;
|
|
title?: string;
|
|
};
|
|
|
|
type WikiOkfImportCommandOptions = {
|
|
json?: boolean;
|
|
};
|
|
|
|
type WikiSearchCommandOptions = {
|
|
json?: boolean;
|
|
maxResults?: number;
|
|
backend?: ResolvedMemoryWikiConfig["search"]["backend"];
|
|
corpus?: ResolvedMemoryWikiConfig["search"]["corpus"];
|
|
mode?: WikiSearchMode;
|
|
};
|
|
|
|
type WikiGetCommandOptions = {
|
|
json?: boolean;
|
|
from?: number;
|
|
lines?: number;
|
|
backend?: ResolvedMemoryWikiConfig["search"]["backend"];
|
|
corpus?: ResolvedMemoryWikiConfig["search"]["corpus"];
|
|
};
|
|
|
|
type WikiApplySynthesisCommandOptions = {
|
|
json?: boolean;
|
|
body?: string;
|
|
bodyFile?: string;
|
|
sourceId?: string[];
|
|
contradiction?: string[];
|
|
question?: string[];
|
|
confidence?: number;
|
|
status?: string;
|
|
};
|
|
|
|
type WikiApplyMetadataCommandOptions = {
|
|
json?: boolean;
|
|
sourceId?: string[];
|
|
contradiction?: string[];
|
|
question?: string[];
|
|
confidence?: number;
|
|
clearConfidence?: boolean;
|
|
status?: string;
|
|
};
|
|
|
|
type WikiBridgeImportCommandOptions = {
|
|
json?: boolean;
|
|
};
|
|
|
|
type WikiUnsafeLocalImportCommandOptions = {
|
|
json?: boolean;
|
|
};
|
|
|
|
type WikiChatGptImportCommandOptions = {
|
|
json?: boolean;
|
|
dryRun?: boolean;
|
|
export?: string;
|
|
};
|
|
|
|
type WikiChatGptRollbackCommandOptions = {
|
|
json?: boolean;
|
|
};
|
|
|
|
type WikiObsidianSearchCommandOptions = {
|
|
json?: boolean;
|
|
};
|
|
|
|
type WikiObsidianOpenCommandOptions = {
|
|
json?: boolean;
|
|
};
|
|
|
|
type WikiObsidianCommandCommandOptions = {
|
|
json?: boolean;
|
|
};
|
|
|
|
type WikiObsidianDailyCommandOptions = {
|
|
json?: boolean;
|
|
};
|
|
|
|
type WikiCommandOptions = {
|
|
agent?: string;
|
|
};
|
|
|
|
type MemoryWikiCliRegistration = {
|
|
config: ResolvedMemoryWikiConfig;
|
|
resolveConfig?: MemoryWikiConfigResolver;
|
|
getAppConfig?: () => OpenClawConfig | undefined;
|
|
};
|
|
|
|
function sanitizeGatewayStringForTerminal(value: string): string {
|
|
const truncated =
|
|
value.length > GATEWAY_TERMINAL_STRING_MAX_CHARS
|
|
? truncateUtf16Safe(value, GATEWAY_TERMINAL_STRING_MAX_CHARS)
|
|
: value;
|
|
const sanitized = truncated
|
|
.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: string): string {
|
|
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: string, writer: Pick<NodeJS.WriteStream, "write"> = process.stdout) {
|
|
writer.write(output.endsWith("\n") ? output : `${output}\n`);
|
|
}
|
|
|
|
function shouldRouteBridgeRuntimeThroughGateway(config: ResolvedMemoryWikiConfig): boolean {
|
|
return (
|
|
config.vaultMode === "bridge" && config.bridge.enabled && config.bridge.readMemoryArtifacts
|
|
);
|
|
}
|
|
|
|
function isBoundedGatewayString(
|
|
value: unknown,
|
|
maxChars = GATEWAY_RESPONSE_MAX_STRING_CHARS,
|
|
): value is string {
|
|
return typeof value === "string" && value.length <= maxChars;
|
|
}
|
|
|
|
function isStringArray(
|
|
value: unknown,
|
|
maxChars = GATEWAY_RESPONSE_MAX_STRING_CHARS,
|
|
): value is string[] {
|
|
return (
|
|
Array.isArray(value) &&
|
|
value.length <= GATEWAY_RESPONSE_MAX_ARRAY_ITEMS &&
|
|
value.every((item) => isBoundedGatewayString(item, maxChars))
|
|
);
|
|
}
|
|
|
|
function hasNumberFields(value: Record<string, unknown>, keys: readonly string[]): boolean {
|
|
return keys.every((key) => typeof value[key] === "number");
|
|
}
|
|
|
|
function isWarningList(value: unknown): boolean {
|
|
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: unknown): value is MemoryWikiStatus {
|
|
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: unknown): value is MemoryWikiDoctorReport {
|
|
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: unknown): value is MemoryWikiImportedSourceSyncResult {
|
|
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: "wiki.status" | "wiki.doctor" | "wiki.bridge.import",
|
|
value: unknown,
|
|
): MemoryWikiStatus | MemoryWikiDoctorReport | MemoryWikiImportedSourceSyncResult {
|
|
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: "wiki.status", agentId?: string): Promise<MemoryWikiStatus>;
|
|
async function callWikiGateway(
|
|
method: "wiki.doctor",
|
|
agentId?: string,
|
|
): Promise<MemoryWikiDoctorReport>;
|
|
async function callWikiGateway(
|
|
method: "wiki.bridge.import",
|
|
agentId?: string,
|
|
): Promise<MemoryWikiImportedSourceSyncResult>;
|
|
async function callWikiGateway(
|
|
method: "wiki.status" | "wiki.doctor" | "wiki.bridge.import",
|
|
agentId?: string,
|
|
) {
|
|
const result = await callGatewayFromCli(
|
|
method,
|
|
{ timeout: WIKI_GATEWAY_TIMEOUT_MS },
|
|
agentId ? { agentId } : undefined,
|
|
{ progress: false },
|
|
);
|
|
return validateWikiGatewayResult(method, result);
|
|
}
|
|
|
|
function normalizeCliStringList(values?: string[]): string[] | undefined {
|
|
if (!values) {
|
|
return undefined;
|
|
}
|
|
const uniqueValues = uniqueStrings(normalizeStringEntries(values));
|
|
return uniqueValues.length > 0 ? uniqueValues : undefined;
|
|
}
|
|
|
|
function collectCliValues(value: string, acc: string[] = []) {
|
|
acc.push(value);
|
|
return acc;
|
|
}
|
|
|
|
function parseWikiSearchEnumOption<T extends string>(
|
|
value: string,
|
|
allowed: readonly T[],
|
|
label: string,
|
|
): T {
|
|
if ((allowed as readonly string[]).includes(value)) {
|
|
return value as T;
|
|
}
|
|
throw new Error(`Invalid ${label}: ${value}. Expected one of: ${allowed.join(", ")}`);
|
|
}
|
|
|
|
async function resolveWikiApplyBody(params: { body?: string; bodyFile?: string }): Promise<string> {
|
|
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.");
|
|
}
|
|
|
|
type MemoryWikiMutationResult = Awaited<ReturnType<typeof applyMemoryWikiMutation>>;
|
|
|
|
function formatMemoryWikiMutationSummary(result: MemoryWikiMutationResult, json?: boolean): string {
|
|
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<T>(
|
|
result: T,
|
|
json: boolean | undefined,
|
|
render: (result: T) => string,
|
|
): string {
|
|
return json ? JSON.stringify(result, null, 2) : render(result);
|
|
}
|
|
|
|
function formatGatewayJsonOrText<T>(
|
|
result: T,
|
|
json: boolean | undefined,
|
|
render: (result: T) => string,
|
|
): string {
|
|
return json
|
|
? escapeGatewayJsonForTerminal(JSON.stringify(result, null, 2))
|
|
: sanitizeGatewayStringForTerminal(render(result));
|
|
}
|
|
|
|
async function runWikiCommandWithSummary<T>(params: {
|
|
json?: boolean;
|
|
stdout?: Pick<NodeJS.WriteStream, "write">;
|
|
run: () => Promise<T>;
|
|
render: (result: T) => string;
|
|
}): Promise<T> {
|
|
const result = await params.run();
|
|
writeOutput(formatJsonOrText(result, params.json, params.render), params.stdout);
|
|
return result;
|
|
}
|
|
|
|
async function runSyncedWikiCommandWithSummary<T>(params: {
|
|
config: ResolvedMemoryWikiConfig;
|
|
appConfig?: OpenClawConfig;
|
|
json?: boolean;
|
|
stdout?: Pick<NodeJS.WriteStream, "write">;
|
|
run: () => Promise<T>;
|
|
render: (result: T) => string;
|
|
}): Promise<T> {
|
|
await syncMemoryWikiImportedSources({ config: params.config, appConfig: params.appConfig });
|
|
return runWikiCommandWithSummary(params);
|
|
}
|
|
|
|
function addWikiSearchConfigOptions<T extends Command>(command: T): T {
|
|
return command
|
|
.option(
|
|
"--backend <backend>",
|
|
`Search backend (${WIKI_SEARCH_BACKENDS.join(", ")})`,
|
|
(value: string) => parseWikiSearchEnumOption(value, WIKI_SEARCH_BACKENDS, "backend"),
|
|
)
|
|
.option(
|
|
"--corpus <corpus>",
|
|
`Search corpus (${WIKI_SEARCH_CORPORA.join(", ")})`,
|
|
(value: string) => parseWikiSearchEnumOption(value, WIKI_SEARCH_CORPORA, "corpus"),
|
|
);
|
|
}
|
|
|
|
function invalidCliArgument(message: string): Error & { code: string; exitCode: number } {
|
|
const error = new Error(message) as Error & { code: string; exitCode: number };
|
|
error.name = "InvalidArgumentError";
|
|
// Commander recognizes parser failures by code; keep the import type-only for bundled plugin deps.
|
|
error.code = "commander.invalidArgument";
|
|
error.exitCode = 1;
|
|
return error;
|
|
}
|
|
|
|
function parseWikiConfidenceOption(value: string): number {
|
|
const trimmed = value.trim();
|
|
const confidence = /^[+-]?(?:\d+(?:\.\d+)?|\.\d+)$/.test(trimmed) ? Number(trimmed) : Number.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: string, flag: string): number {
|
|
const parsed = parseStrictPositiveInteger(value);
|
|
if (parsed === undefined) {
|
|
throw invalidCliArgument(`${flag} must be a positive integer.`);
|
|
}
|
|
return parsed;
|
|
}
|
|
|
|
function addWikiApplyMutationOptions<T extends Command>(command: T): T {
|
|
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: {
|
|
config: ResolvedMemoryWikiConfig;
|
|
appConfig?: OpenClawConfig;
|
|
agentId?: string;
|
|
json?: boolean;
|
|
stdout?: Pick<NodeJS.WriteStream, "write">;
|
|
}) {
|
|
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: {
|
|
config: ResolvedMemoryWikiConfig;
|
|
appConfig?: OpenClawConfig;
|
|
agentId?: string;
|
|
json?: boolean;
|
|
stdout?: Pick<NodeJS.WriteStream, "write">;
|
|
}) {
|
|
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: {
|
|
config: ResolvedMemoryWikiConfig;
|
|
json?: boolean;
|
|
stdout?: Pick<NodeJS.WriteStream, "write">;
|
|
}) {
|
|
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: {
|
|
config: ResolvedMemoryWikiConfig;
|
|
appConfig?: OpenClawConfig;
|
|
json?: boolean;
|
|
stdout?: Pick<NodeJS.WriteStream, "write">;
|
|
}) {
|
|
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: {
|
|
config: ResolvedMemoryWikiConfig;
|
|
appConfig?: OpenClawConfig;
|
|
json?: boolean;
|
|
stdout?: Pick<NodeJS.WriteStream, "write">;
|
|
}) {
|
|
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: {
|
|
config: ResolvedMemoryWikiConfig;
|
|
inputPath: string;
|
|
title?: string;
|
|
json?: boolean;
|
|
stdout?: Pick<NodeJS.WriteStream, "write">;
|
|
}) {
|
|
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: {
|
|
config: ResolvedMemoryWikiConfig;
|
|
bundlePath: string;
|
|
json?: boolean;
|
|
stdout?: Pick<NodeJS.WriteStream, "write">;
|
|
}) {
|
|
return runWikiCommandWithSummary({
|
|
json: params.json,
|
|
stdout: params.stdout,
|
|
run: () =>
|
|
importMemoryWikiOkfBundle({
|
|
config: params.config,
|
|
bundlePath: params.bundlePath,
|
|
}),
|
|
render: formatOkfImportSummary,
|
|
});
|
|
}
|
|
|
|
async function runWikiSearch(params: {
|
|
config: ResolvedMemoryWikiConfig;
|
|
appConfig?: OpenClawConfig;
|
|
agentId?: string;
|
|
query: string;
|
|
maxResults?: number;
|
|
searchBackend?: ResolvedMemoryWikiConfig["search"]["backend"];
|
|
searchCorpus?: ResolvedMemoryWikiConfig["search"]["corpus"];
|
|
mode?: WikiSearchMode;
|
|
json?: boolean;
|
|
stdout?: Pick<NodeJS.WriteStream, "write">;
|
|
}) {
|
|
if (params.mode && !(WIKI_SEARCH_MODES as readonly string[]).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,
|
|
});
|
|
const summary = 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");
|
|
writeOutput(summary, params.stdout);
|
|
return results;
|
|
}
|
|
|
|
async function runWikiGet(params: {
|
|
config: ResolvedMemoryWikiConfig;
|
|
appConfig?: OpenClawConfig;
|
|
agentId?: string;
|
|
lookup: string;
|
|
fromLine?: number;
|
|
lineCount?: number;
|
|
searchBackend?: ResolvedMemoryWikiConfig["search"]["backend"];
|
|
searchCorpus?: ResolvedMemoryWikiConfig["search"]["corpus"];
|
|
json?: boolean;
|
|
stdout?: Pick<NodeJS.WriteStream, "write">;
|
|
}) {
|
|
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,
|
|
});
|
|
const summary = params.json
|
|
? JSON.stringify(result, null, 2)
|
|
: (result?.content ?? `Wiki page not found: ${params.lookup}`);
|
|
writeOutput(summary, params.stdout);
|
|
return result;
|
|
}
|
|
|
|
async function runWikiApplySynthesis(params: {
|
|
config: ResolvedMemoryWikiConfig;
|
|
appConfig?: OpenClawConfig;
|
|
title: string;
|
|
body?: string;
|
|
bodyFile?: string;
|
|
sourceIds?: string[];
|
|
contradictions?: string[];
|
|
questions?: string[];
|
|
confidence?: number;
|
|
status?: string;
|
|
json?: boolean;
|
|
stdout?: Pick<NodeJS.WriteStream, "write">;
|
|
}) {
|
|
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: {
|
|
config: ResolvedMemoryWikiConfig;
|
|
appConfig?: OpenClawConfig;
|
|
lookup: string;
|
|
sourceIds?: string[];
|
|
contradictions?: string[];
|
|
questions?: string[];
|
|
confidence?: number;
|
|
clearConfidence?: boolean;
|
|
status?: string;
|
|
json?: boolean;
|
|
stdout?: Pick<NodeJS.WriteStream, "write">;
|
|
}) {
|
|
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: {
|
|
config: ResolvedMemoryWikiConfig;
|
|
appConfig?: OpenClawConfig;
|
|
agentId?: string;
|
|
json?: boolean;
|
|
stdout?: Pick<NodeJS.WriteStream, "write">;
|
|
}) {
|
|
const render = (value: MemoryWikiImportedSourceSyncResult) =>
|
|
`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: {
|
|
config: ResolvedMemoryWikiConfig;
|
|
appConfig?: OpenClawConfig;
|
|
json?: boolean;
|
|
stdout?: Pick<NodeJS.WriteStream, "write">;
|
|
}) {
|
|
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: {
|
|
config: ResolvedMemoryWikiConfig;
|
|
json?: boolean;
|
|
stdout?: Pick<NodeJS.WriteStream, "write">;
|
|
}) {
|
|
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: ResolvedMemoryWikiConfig) {
|
|
if (config.vault.scope === "agent") {
|
|
throw new Error("Official Obsidian CLI actions do not support memory-wiki vault.scope=agent.");
|
|
}
|
|
}
|
|
|
|
async function runWikiObsidianSearch(params: {
|
|
config: ResolvedMemoryWikiConfig;
|
|
query: string;
|
|
json?: boolean;
|
|
stdout?: Pick<NodeJS.WriteStream, "write">;
|
|
}) {
|
|
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: {
|
|
config: ResolvedMemoryWikiConfig;
|
|
vaultPath: string;
|
|
json?: boolean;
|
|
stdout?: Pick<NodeJS.WriteStream, "write">;
|
|
}) {
|
|
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: {
|
|
config: ResolvedMemoryWikiConfig;
|
|
id: string;
|
|
json?: boolean;
|
|
stdout?: Pick<NodeJS.WriteStream, "write">;
|
|
}) {
|
|
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: {
|
|
config: ResolvedMemoryWikiConfig;
|
|
json?: boolean;
|
|
stdout?: Pick<NodeJS.WriteStream, "write">;
|
|
}) {
|
|
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: ChatGptImportResult): string {
|
|
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: ChatGptRollbackResult): string {
|
|
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: {
|
|
config: ResolvedMemoryWikiConfig;
|
|
exportPath: string;
|
|
dryRun?: boolean;
|
|
json?: boolean;
|
|
stdout?: Pick<NodeJS.WriteStream, "write">;
|
|
}) {
|
|
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: {
|
|
config: ResolvedMemoryWikiConfig;
|
|
runId: string;
|
|
json?: boolean;
|
|
stdout?: Pick<NodeJS.WriteStream, "write">;
|
|
}) {
|
|
return runWikiCommandWithSummary({
|
|
json: params.json,
|
|
stdout: params.stdout,
|
|
run: () =>
|
|
rollbackChatGptImportRun({
|
|
config: params.config,
|
|
runId: params.runId,
|
|
}),
|
|
render: formatChatGptRollbackSummary,
|
|
});
|
|
}
|
|
|
|
export function registerWikiCli(program: Command, registration: MemoryWikiCliRegistration) {
|
|
const resolveConfig: MemoryWikiConfigResolver =
|
|
registration.resolveConfig ??
|
|
((agentId, currentAppConfig) =>
|
|
resolveMemoryWikiAgentConfig({
|
|
config: registration.config,
|
|
appConfig: currentAppConfig,
|
|
...(agentId ? { agentId } : {}),
|
|
}));
|
|
let commandContext:
|
|
| { agentId?: string; appConfig?: OpenClawConfig; config: ResolvedMemoryWikiConfig }
|
|
| undefined;
|
|
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", () => {
|
|
const requestedAgentId = wiki.opts<WikiCommandOptions>().agent?.trim() || undefined;
|
|
const currentAppConfig = registration.getAppConfig?.();
|
|
const config = resolveConfig(requestedAgentId, currentAppConfig);
|
|
const agentId = config.agentId ?? requestedAgentId;
|
|
commandContext = {
|
|
config,
|
|
...(currentAppConfig ? { appConfig: currentAppConfig } : {}),
|
|
...(agentId ? { agentId } : {}),
|
|
};
|
|
});
|
|
|
|
wiki
|
|
.command("status")
|
|
.description("Show wiki vault status")
|
|
.option("--json", "Print JSON")
|
|
.action(async (opts: WikiStatusCommandOptions) => {
|
|
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("--json", "Print JSON")
|
|
.action(async (opts: WikiDoctorCommandOptions) => {
|
|
const { agentId, appConfig, config } = requireCommandContext();
|
|
await runWikiDoctor({ config, appConfig, agentId, json: opts.json });
|
|
});
|
|
|
|
wiki
|
|
.command("init")
|
|
.description("Initialize the wiki vault layout")
|
|
.option("--json", "Print JSON")
|
|
.action(async (opts: WikiInitCommandOptions) => {
|
|
const { config } = requireCommandContext();
|
|
await runWikiInit({ config, json: opts.json });
|
|
});
|
|
|
|
wiki
|
|
.command("compile")
|
|
.description("Refresh generated wiki indexes")
|
|
.option("--json", "Print JSON")
|
|
.action(async (opts: WikiCompileCommandOptions) => {
|
|
const { appConfig, config } = requireCommandContext();
|
|
await runWikiCompile({ config, appConfig, json: opts.json });
|
|
});
|
|
|
|
wiki
|
|
.command("lint")
|
|
.description("Lint the wiki vault and write a report")
|
|
.option("--json", "Print JSON")
|
|
.action(async (opts: WikiLintCommandOptions) => {
|
|
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("--title <title>", "Override the source title")
|
|
.option("--json", "Print JSON")
|
|
.action(async (inputPath: string, opts: WikiIngestCommandOptions) => {
|
|
const { config } = requireCommandContext();
|
|
await runWikiIngest({ config, inputPath, title: opts.title, json: opts.json });
|
|
});
|
|
|
|
const okf = wiki.command("okf").description("Import Open Knowledge Format bundles");
|
|
okf
|
|
.command("import")
|
|
.description("Import an unpacked OKF bundle into wiki concept pages")
|
|
.argument("<path>", "OKF bundle directory")
|
|
.option("--json", "Print JSON")
|
|
.action(async (bundlePath: string, opts: WikiOkfImportCommandOptions) => {
|
|
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("--max-results <n>", "Maximum results", (value: string) =>
|
|
parseWikiPositiveIntegerOption(value, "--max-results"),
|
|
)
|
|
.option("--mode <mode>", `Search mode (${WIKI_SEARCH_MODES.join(", ")})`),
|
|
)
|
|
.option("--json", "Print JSON")
|
|
.action(async (query: string, opts: WikiSearchCommandOptions) => {
|
|
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("--from <n>", "Start line", (value: string) =>
|
|
parseWikiPositiveIntegerOption(value, "--from"),
|
|
)
|
|
.option("--lines <n>", "Number of lines", (value: string) =>
|
|
parseWikiPositiveIntegerOption(value, "--lines"),
|
|
),
|
|
)
|
|
.option("--json", "Print JSON")
|
|
.action(async (lookup: string, opts: WikiGetCommandOptions) => {
|
|
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("--body <text>", "Summary body text")
|
|
.option("--body-file <path>", "Read summary body text from a file"),
|
|
)
|
|
.option("--json", "Print JSON")
|
|
.action(async (title: string, opts: WikiApplySynthesisCommandOptions) => {
|
|
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("--clear-confidence", "Remove any stored confidence value")
|
|
.option("--json", "Print JSON")
|
|
.action(async (lookup: string, opts: WikiApplyMetadataCommandOptions) => {
|
|
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,
|
|
});
|
|
});
|
|
|
|
const bridge = wiki
|
|
.command("bridge")
|
|
.description("Import public memory artifacts into the wiki vault");
|
|
bridge
|
|
.command("import")
|
|
.description("Sync bridge-backed memory artifacts into wiki source pages")
|
|
.option("--json", "Print JSON")
|
|
.action(async (opts: WikiBridgeImportCommandOptions) => {
|
|
const { agentId, appConfig, config } = requireCommandContext();
|
|
await runWikiBridgeImport({ config, appConfig, agentId, json: opts.json });
|
|
});
|
|
|
|
const unsafeLocal = wiki
|
|
.command("unsafe-local")
|
|
.description("Import explicitly configured private local paths into wiki source pages");
|
|
unsafeLocal
|
|
.command("import")
|
|
.description("Sync unsafe-local configured paths into wiki source pages")
|
|
.option("--json", "Print JSON")
|
|
.action(async (opts: WikiUnsafeLocalImportCommandOptions) => {
|
|
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("--dry-run", "Preview changes without writing", false)
|
|
.option("--json", "Print JSON")
|
|
.action(async (opts: WikiChatGptImportCommandOptions) => {
|
|
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("--json", "Print JSON")
|
|
.action(async (runId: string, opts: WikiChatGptRollbackCommandOptions) => {
|
|
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: WikiStatusCommandOptions) => {
|
|
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: string, opts: WikiObsidianSearchCommandOptions) => {
|
|
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: string, opts: WikiObsidianOpenCommandOptions) => {
|
|
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: string, opts: WikiObsidianCommandCommandOptions) => {
|
|
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: WikiObsidianDailyCommandOptions) => {
|
|
const { config } = requireCommandContext();
|
|
await runWikiObsidianDailyCli({ config, json: opts.json });
|
|
});
|
|
}
|
|
/* oxlint-disable max-lines -- TODO: split this grandfathered oversized file. */
|