neotoma 0.4.2 → 0.4.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +43 -0
- package/dist/actions.d.ts.map +1 -1
- package/dist/actions.js +332 -150
- package/dist/actions.js.map +1 -1
- package/dist/cli/commands/mirror.d.ts +52 -0
- package/dist/cli/commands/mirror.d.ts.map +1 -0
- package/dist/cli/commands/mirror.js +147 -0
- package/dist/cli/commands/mirror.js.map +1 -0
- package/dist/cli/config.d.ts +8 -0
- package/dist/cli/config.d.ts.map +1 -1
- package/dist/cli/config.js.map +1 -1
- package/dist/cli/doctor.d.ts +76 -0
- package/dist/cli/doctor.d.ts.map +1 -0
- package/dist/cli/doctor.js +345 -0
- package/dist/cli/doctor.js.map +1 -0
- package/dist/cli/hooks.d.ts +45 -0
- package/dist/cli/hooks.d.ts.map +1 -0
- package/dist/cli/hooks.js +273 -0
- package/dist/cli/hooks.js.map +1 -0
- package/dist/cli/hooks_detect.d.ts +53 -0
- package/dist/cli/hooks_detect.d.ts.map +1 -0
- package/dist/cli/hooks_detect.js +220 -0
- package/dist/cli/hooks_detect.js.map +1 -0
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +595 -49
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/permissions.d.ts +46 -0
- package/dist/cli/permissions.d.ts.map +1 -0
- package/dist/cli/permissions.js +191 -0
- package/dist/cli/permissions.js.map +1 -0
- package/dist/cli/setup.d.ts +46 -0
- package/dist/cli/setup.d.ts.map +1 -0
- package/dist/cli/setup.js +105 -0
- package/dist/cli/setup.js.map +1 -0
- package/dist/core/operations.d.ts +145 -0
- package/dist/core/operations.d.ts.map +1 -0
- package/dist/core/operations.js +106 -0
- package/dist/core/operations.js.map +1 -0
- package/dist/openclaw_entry.d.ts +45 -0
- package/dist/openclaw_entry.d.ts.map +1 -0
- package/dist/openclaw_entry.js +75 -0
- package/dist/openclaw_entry.js.map +1 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +107 -748
- package/dist/server.js.map +1 -1
- package/dist/services/batch_correction.d.ts +129 -0
- package/dist/services/batch_correction.d.ts.map +1 -0
- package/dist/services/batch_correction.js +231 -0
- package/dist/services/batch_correction.js.map +1 -0
- package/dist/services/canonical_markdown.d.ts +145 -0
- package/dist/services/canonical_markdown.d.ts.map +1 -0
- package/dist/services/canonical_markdown.js +425 -0
- package/dist/services/canonical_markdown.js.map +1 -0
- package/dist/services/canonical_mirror.d.ts +127 -0
- package/dist/services/canonical_mirror.d.ts.map +1 -0
- package/dist/services/canonical_mirror.js +1047 -0
- package/dist/services/canonical_mirror.js.map +1 -0
- package/dist/services/canonical_mirror_git.d.ts +71 -0
- package/dist/services/canonical_mirror_git.d.ts.map +1 -0
- package/dist/services/canonical_mirror_git.js +211 -0
- package/dist/services/canonical_mirror_git.js.map +1 -0
- package/dist/services/entity_resolution.d.ts +17 -1
- package/dist/services/entity_resolution.d.ts.map +1 -1
- package/dist/services/entity_resolution.js +97 -24
- package/dist/services/entity_resolution.js.map +1 -1
- package/dist/services/entity_type_equivalence.d.ts +40 -0
- package/dist/services/entity_type_equivalence.d.ts.map +1 -0
- package/dist/services/entity_type_equivalence.js +109 -0
- package/dist/services/entity_type_equivalence.js.map +1 -0
- package/dist/services/entity_type_guard.d.ts +52 -0
- package/dist/services/entity_type_guard.d.ts.map +1 -0
- package/dist/services/entity_type_guard.js +204 -0
- package/dist/services/entity_type_guard.js.map +1 -0
- package/dist/services/file_text_extraction.d.ts.map +1 -1
- package/dist/services/file_text_extraction.js +5 -0
- package/dist/services/file_text_extraction.js.map +1 -1
- package/dist/services/flat_packed_detection.d.ts +32 -0
- package/dist/services/flat_packed_detection.d.ts.map +1 -0
- package/dist/services/flat_packed_detection.js +96 -0
- package/dist/services/flat_packed_detection.js.map +1 -0
- package/dist/services/interpretation.d.ts.map +1 -1
- package/dist/services/interpretation.js +9 -0
- package/dist/services/interpretation.js.map +1 -1
- package/dist/services/memory_export.d.ts +56 -0
- package/dist/services/memory_export.d.ts.map +1 -0
- package/dist/services/memory_export.js +295 -0
- package/dist/services/memory_export.js.map +1 -0
- package/dist/services/raw_fragments.js +1 -1
- package/dist/services/raw_fragments.js.map +1 -1
- package/dist/services/raw_storage.d.ts +6 -0
- package/dist/services/raw_storage.d.ts.map +1 -1
- package/dist/services/raw_storage.js +26 -0
- package/dist/services/raw_storage.js.map +1 -1
- package/dist/services/schema_definitions.d.ts.map +1 -1
- package/dist/services/schema_definitions.js +37 -0
- package/dist/services/schema_definitions.js.map +1 -1
- package/dist/services/schema_reference_linking.d.ts +38 -0
- package/dist/services/schema_reference_linking.d.ts.map +1 -0
- package/dist/services/schema_reference_linking.js +130 -0
- package/dist/services/schema_reference_linking.js.map +1 -0
- package/dist/services/schema_registry.d.ts +39 -0
- package/dist/services/schema_registry.d.ts.map +1 -1
- package/dist/services/schema_registry.js +91 -2
- package/dist/services/schema_registry.js.map +1 -1
- package/dist/services/snapshot_computation.d.ts.map +1 -1
- package/dist/services/snapshot_computation.js +10 -0
- package/dist/services/snapshot_computation.js.map +1 -1
- package/dist/services/timeline_events.d.ts +24 -5
- package/dist/services/timeline_events.d.ts.map +1 -1
- package/dist/services/timeline_events.js +89 -13
- package/dist/services/timeline_events.js.map +1 -1
- package/dist/shared/action_schemas.d.ts +18 -2
- package/dist/shared/action_schemas.d.ts.map +1 -1
- package/dist/shared/action_schemas.js +8 -0
- package/dist/shared/action_schemas.js.map +1 -1
- package/dist/shared/local_transport.d.ts +13 -0
- package/dist/shared/local_transport.d.ts.map +1 -1
- package/dist/shared/local_transport.js +89 -6
- package/dist/shared/local_transport.js.map +1 -1
- package/dist/tool_definitions.d.ts +22 -0
- package/dist/tool_definitions.d.ts.map +1 -0
- package/dist/tool_definitions.js +747 -0
- package/dist/tool_definitions.js.map +1 -0
- package/openapi.yaml +13 -0
- package/openclaw.plugin.json +87 -0
- package/package.json +31 -1
- package/skills/query-memory/SKILL.md +67 -0
- package/skills/recover-sqlite-database/SKILL.md +46 -0
- package/skills/store-data/SKILL.md +58 -0
package/dist/cli/index.js
CHANGED
|
@@ -18,6 +18,7 @@ import { createApiClient } from "../shared/api_client.js";
|
|
|
18
18
|
import { getOpenApiOperationMapping } from "../shared/contract_mappings.js";
|
|
19
19
|
import { getEntityDisplayName } from "../shared/entity_display_name.js";
|
|
20
20
|
import { getRecordDisplaySummary } from "../shared/record_display_summary.js";
|
|
21
|
+
import { getMimeTypeFromExtension } from "../services/file_text_extraction.js";
|
|
21
22
|
import { CONFIG_DIR, CONFIG_PATH, CANDIDATE_API_PORTS, USER_ENV_PATH, clearConfig, discoverApiInstances, isTokenExpired, isProd, readConfig, rememberKnownApiPort, resolveBaseUrl, waitForApiReady, waitForHealth, writeConfig, } from "./config.js";
|
|
22
23
|
import { getLatestFromRegistry, isUpdateAvailable, formatUpgradeCommand, } from "../version_check.js";
|
|
23
24
|
import { shutdownLocalTransport } from "../shared/local_transport.js";
|
|
@@ -43,6 +44,10 @@ const BANNER_ANSI = {
|
|
|
43
44
|
clearLineFull: `${ESC}[2K`,
|
|
44
45
|
};
|
|
45
46
|
const MAX_SESSION_HISTORY = 100;
|
|
47
|
+
function inferMimeTypeForPath(filePath) {
|
|
48
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
49
|
+
return getMimeTypeFromExtension(ext) ?? "application/octet-stream";
|
|
50
|
+
}
|
|
46
51
|
export function getPromptPlaceholder(buffer, placeholderText = "/ for commands") {
|
|
47
52
|
return placeholderText.length > 0 && buffer.length === 0 ? placeholderText : "";
|
|
48
53
|
}
|
|
@@ -3661,9 +3666,28 @@ const initCommand = program
|
|
|
3661
3666
|
.option("--use-current-dir-targets <choice>", "When npm package is detected, choose whether to use current directory targets: yes/no")
|
|
3662
3667
|
.option("--key-source <source>", "For key_derived auth: create, existing, or skip (skip corresponding prompt when provided)")
|
|
3663
3668
|
.option("--key-path <path>", "Path to an existing key file for key_derived auth")
|
|
3669
|
+
.option("--idempotent", "If already initialized, exit 0 with a report instead of prompting or failing")
|
|
3664
3670
|
.action(async (opts) => {
|
|
3665
3671
|
try {
|
|
3666
3672
|
const outputMode = resolveOutputMode();
|
|
3673
|
+
if (opts.idempotent) {
|
|
3674
|
+
try {
|
|
3675
|
+
const { runDoctor } = await import("./doctor.js");
|
|
3676
|
+
const report = await runDoctor({ cwd: process.cwd() });
|
|
3677
|
+
if (report.data.initialized && !opts.force) {
|
|
3678
|
+
writeOutput({
|
|
3679
|
+
ok: true,
|
|
3680
|
+
already_initialized: true,
|
|
3681
|
+
data: report.data,
|
|
3682
|
+
neotoma: { version: report.neotoma.version },
|
|
3683
|
+
}, outputMode);
|
|
3684
|
+
return;
|
|
3685
|
+
}
|
|
3686
|
+
}
|
|
3687
|
+
catch {
|
|
3688
|
+
// Fall through to normal init on any detection error.
|
|
3689
|
+
}
|
|
3690
|
+
}
|
|
3667
3691
|
const interactiveRequested = Boolean(opts.interactive || opts.advanced);
|
|
3668
3692
|
let useAdvancedPrompts = interactiveRequested;
|
|
3669
3693
|
const applyDefaultsWithoutPrompts = Boolean(opts.yes || !interactiveRequested);
|
|
@@ -6786,6 +6810,111 @@ program
|
|
|
6786
6810
|
process.on("SIGINT", onExit);
|
|
6787
6811
|
process.on("SIGTERM", onExit);
|
|
6788
6812
|
});
|
|
6813
|
+
program
|
|
6814
|
+
.command("doctor")
|
|
6815
|
+
.description("Consolidated diagnostics for agent-led onboarding (install/runtime/api/mcp/cli-instructions/permissions)")
|
|
6816
|
+
.action(async () => {
|
|
6817
|
+
const outputMode = resolveOutputMode();
|
|
6818
|
+
const { runDoctor } = await import("./doctor.js");
|
|
6819
|
+
const report = await runDoctor({ cwd: process.cwd() });
|
|
6820
|
+
writeOutput(report, outputMode);
|
|
6821
|
+
});
|
|
6822
|
+
program
|
|
6823
|
+
.command("setup")
|
|
6824
|
+
.description("One-shot setup: init (idempotent) + MCP + CLI instructions + permissions for the current harness")
|
|
6825
|
+
.option("--tool <tool>", "Target harness (claude-code|cursor|codex|openclaw|claude-desktop)")
|
|
6826
|
+
.option("--scope <scope>", "Permission scope (project|user|both)", "project")
|
|
6827
|
+
.option("--dry-run", "Plan the setup without writing files", false)
|
|
6828
|
+
.option("--yes", "Assume yes for prompts (currently unused; runners default to non-interactive)", false)
|
|
6829
|
+
.option("--skip-permissions", "Skip permission-file writes", false)
|
|
6830
|
+
.action(async (opts) => {
|
|
6831
|
+
const outputMode = resolveOutputMode();
|
|
6832
|
+
const { runSetup } = await import("./setup.js");
|
|
6833
|
+
const report = await runSetup({
|
|
6834
|
+
tool: opts.tool ?? null,
|
|
6835
|
+
dryRun: Boolean(opts.dryRun),
|
|
6836
|
+
yes: Boolean(opts.yes),
|
|
6837
|
+
skipPermissions: Boolean(opts.skipPermissions),
|
|
6838
|
+
scope: opts.scope ?? "project",
|
|
6839
|
+
cwd: process.cwd(),
|
|
6840
|
+
});
|
|
6841
|
+
const exitCode = report.overall_ok ? 0 : 1;
|
|
6842
|
+
writeOutput(report, outputMode);
|
|
6843
|
+
process.exitCode = exitCode;
|
|
6844
|
+
});
|
|
6845
|
+
// `neotoma hooks` — lifecycle hook install/uninstall/status. Deliberately
|
|
6846
|
+
// separate from `neotoma setup`: hooks are offered during activation (after
|
|
6847
|
+
// the first-Aha timeline) rather than at install time. See install.md.
|
|
6848
|
+
const hooksCommand = program
|
|
6849
|
+
.command("hooks")
|
|
6850
|
+
.description("Install Neotoma lifecycle hooks for a supported harness (cursor|claude-code|codex|opencode|claude-agent-sdk). Offered during activation, not setup.");
|
|
6851
|
+
hooksCommand
|
|
6852
|
+
.command("status")
|
|
6853
|
+
.description("Print per-harness hook installation state (uses `neotoma doctor`).")
|
|
6854
|
+
.action(async () => {
|
|
6855
|
+
const outputMode = resolveOutputMode();
|
|
6856
|
+
const { runHooksStatus } = await import("./hooks.js");
|
|
6857
|
+
const status = await runHooksStatus({ cwd: process.cwd() });
|
|
6858
|
+
writeOutput(status, outputMode);
|
|
6859
|
+
});
|
|
6860
|
+
hooksCommand
|
|
6861
|
+
.command("install")
|
|
6862
|
+
.description("Install Neotoma hooks for the given harness after running guardrails.")
|
|
6863
|
+
.requiredOption("--tool <tool>", "Target harness (cursor|claude-code|codex|opencode|claude-agent-sdk)")
|
|
6864
|
+
.option("--dry-run", "Plan the install without writing files", false)
|
|
6865
|
+
.option("--yes", "Skip the confirmation prompt", false)
|
|
6866
|
+
.option("--force", "Proceed even when another hook plugin is already present", false)
|
|
6867
|
+
.action(async (opts) => {
|
|
6868
|
+
const outputMode = resolveOutputMode();
|
|
6869
|
+
const { runHooksInstall } = await import("./hooks.js");
|
|
6870
|
+
const { toHookHarness } = await import("./hooks_detect.js");
|
|
6871
|
+
const tool = toHookHarness(opts.tool);
|
|
6872
|
+
if (!tool) {
|
|
6873
|
+
writeOutput({
|
|
6874
|
+
ok: false,
|
|
6875
|
+
action: "install",
|
|
6876
|
+
message: `Unsupported --tool value: ${opts.tool}. Expected one of cursor|claude-code|codex|opencode|claude-agent-sdk.`,
|
|
6877
|
+
}, outputMode);
|
|
6878
|
+
process.exitCode = 1;
|
|
6879
|
+
return;
|
|
6880
|
+
}
|
|
6881
|
+
const result = await runHooksInstall({
|
|
6882
|
+
tool,
|
|
6883
|
+
cwd: process.cwd(),
|
|
6884
|
+
dryRun: Boolean(opts.dryRun),
|
|
6885
|
+
yes: Boolean(opts.yes),
|
|
6886
|
+
force: Boolean(opts.force),
|
|
6887
|
+
});
|
|
6888
|
+
writeOutput(result, outputMode);
|
|
6889
|
+
process.exitCode = result.ok ? 0 : 1;
|
|
6890
|
+
});
|
|
6891
|
+
hooksCommand
|
|
6892
|
+
.command("uninstall")
|
|
6893
|
+
.description("Remove Neotoma hook entries for the given harness (leaves other plugins in place).")
|
|
6894
|
+
.requiredOption("--tool <tool>", "Target harness (cursor|claude-code|codex|opencode|claude-agent-sdk)")
|
|
6895
|
+
.option("--dry-run", "Plan the uninstall without writing files", false)
|
|
6896
|
+
.action(async (opts) => {
|
|
6897
|
+
const outputMode = resolveOutputMode();
|
|
6898
|
+
const { runHooksUninstall } = await import("./hooks.js");
|
|
6899
|
+
const { toHookHarness } = await import("./hooks_detect.js");
|
|
6900
|
+
const tool = toHookHarness(opts.tool);
|
|
6901
|
+
if (!tool) {
|
|
6902
|
+
writeOutput({
|
|
6903
|
+
ok: false,
|
|
6904
|
+
action: "uninstall",
|
|
6905
|
+
message: `Unsupported --tool value: ${opts.tool}. Expected one of cursor|claude-code|codex|opencode|claude-agent-sdk.`,
|
|
6906
|
+
}, outputMode);
|
|
6907
|
+
process.exitCode = 1;
|
|
6908
|
+
return;
|
|
6909
|
+
}
|
|
6910
|
+
const result = await runHooksUninstall({
|
|
6911
|
+
tool,
|
|
6912
|
+
cwd: process.cwd(),
|
|
6913
|
+
dryRun: Boolean(opts.dryRun),
|
|
6914
|
+
});
|
|
6915
|
+
writeOutput(result, outputMode);
|
|
6916
|
+
process.exitCode = result.ok ? 0 : 1;
|
|
6917
|
+
});
|
|
6789
6918
|
const storageCommand = program.command("storage").description("Storage locations and file paths");
|
|
6790
6919
|
storageCommand
|
|
6791
6920
|
.command("info")
|
|
@@ -7397,6 +7526,109 @@ backupCommand
|
|
|
7397
7526
|
}
|
|
7398
7527
|
}
|
|
7399
7528
|
});
|
|
7529
|
+
// ── Mirror ────────────────────────────────────────────────────────────────
|
|
7530
|
+
const mirrorCommand = program
|
|
7531
|
+
.command("mirror")
|
|
7532
|
+
.description("Filesystem markdown mirror of entities/relationships/sources/timeline/schemas");
|
|
7533
|
+
mirrorCommand
|
|
7534
|
+
.command("enable")
|
|
7535
|
+
.description("Enable the markdown mirror and optionally configure path/kinds/git")
|
|
7536
|
+
.option("--path <dir>", "Mirror root directory (default: data/mirror)")
|
|
7537
|
+
.option("--kinds <list>", "Comma-separated kinds to mirror (entities,relationships,sources,timeline,schemas)")
|
|
7538
|
+
.option("--git", "Enable git-backed mirror (Phase 3)")
|
|
7539
|
+
.option("--no-git", "Disable git-backed mirror")
|
|
7540
|
+
.action(async (opts) => {
|
|
7541
|
+
const outputMode = resolveOutputMode();
|
|
7542
|
+
try {
|
|
7543
|
+
const { runMirrorEnable, formatMirrorConfig } = await import("./commands/mirror.js");
|
|
7544
|
+
const cfg = await runMirrorEnable({
|
|
7545
|
+
path: opts.path,
|
|
7546
|
+
kinds: opts.kinds,
|
|
7547
|
+
git: opts.git === true ? true : undefined,
|
|
7548
|
+
noGit: opts.git === false ? true : undefined,
|
|
7549
|
+
});
|
|
7550
|
+
if (outputMode === "json") {
|
|
7551
|
+
writeOutput(cfg, outputMode);
|
|
7552
|
+
}
|
|
7553
|
+
else {
|
|
7554
|
+
process.stdout.write(formatMirrorConfig(cfg) + "\n");
|
|
7555
|
+
process.stdout.write("\nRun `neotoma mirror rebuild` to populate the mirror from the database.\n");
|
|
7556
|
+
}
|
|
7557
|
+
}
|
|
7558
|
+
catch (err) {
|
|
7559
|
+
writeCliError(err instanceof Error ? err.message : String(err));
|
|
7560
|
+
process.exitCode = 1;
|
|
7561
|
+
}
|
|
7562
|
+
});
|
|
7563
|
+
mirrorCommand
|
|
7564
|
+
.command("disable")
|
|
7565
|
+
.description("Disable the markdown mirror (existing files are left in place)")
|
|
7566
|
+
.action(async () => {
|
|
7567
|
+
const outputMode = resolveOutputMode();
|
|
7568
|
+
try {
|
|
7569
|
+
const { runMirrorDisable, formatMirrorConfig } = await import("./commands/mirror.js");
|
|
7570
|
+
const cfg = await runMirrorDisable();
|
|
7571
|
+
if (outputMode === "json") {
|
|
7572
|
+
writeOutput(cfg, outputMode);
|
|
7573
|
+
}
|
|
7574
|
+
else {
|
|
7575
|
+
process.stdout.write(formatMirrorConfig(cfg) + "\n");
|
|
7576
|
+
}
|
|
7577
|
+
}
|
|
7578
|
+
catch (err) {
|
|
7579
|
+
writeCliError(err instanceof Error ? err.message : String(err));
|
|
7580
|
+
process.exitCode = 1;
|
|
7581
|
+
}
|
|
7582
|
+
});
|
|
7583
|
+
mirrorCommand
|
|
7584
|
+
.command("rebuild")
|
|
7585
|
+
.description("Regenerate markdown files for one or all mirror kinds from the database")
|
|
7586
|
+
.option("--kind <kind>", "One of: all, entities, relationships, sources, timeline, schemas", "all")
|
|
7587
|
+
.option("--entity-type <type>", "Restrict entity rebuild to a single entity_type")
|
|
7588
|
+
.option("--entity-id <id>", "Restrict entity rebuild to a single entity_id")
|
|
7589
|
+
.option("--clean", "Remove stale files in the mirror that no longer correspond to database rows", false)
|
|
7590
|
+
.action(async (opts) => {
|
|
7591
|
+
const outputMode = resolveOutputMode();
|
|
7592
|
+
try {
|
|
7593
|
+
const { runMirrorRebuild, formatRebuildReport } = await import("./commands/mirror.js");
|
|
7594
|
+
const result = await runMirrorRebuild({
|
|
7595
|
+
kind: opts.kind,
|
|
7596
|
+
entityType: opts.entityType,
|
|
7597
|
+
entityId: opts.entityId,
|
|
7598
|
+
clean: Boolean(opts.clean),
|
|
7599
|
+
});
|
|
7600
|
+
if (outputMode === "json") {
|
|
7601
|
+
writeOutput(result, outputMode);
|
|
7602
|
+
}
|
|
7603
|
+
else {
|
|
7604
|
+
process.stdout.write(formatRebuildReport(result) + "\n");
|
|
7605
|
+
}
|
|
7606
|
+
}
|
|
7607
|
+
catch (err) {
|
|
7608
|
+
writeCliError(err instanceof Error ? err.message : String(err));
|
|
7609
|
+
process.exitCode = 1;
|
|
7610
|
+
}
|
|
7611
|
+
});
|
|
7612
|
+
mirrorCommand
|
|
7613
|
+
.command("status")
|
|
7614
|
+
.description("Show mirror configuration and counts of mirrored files by kind")
|
|
7615
|
+
.action(async () => {
|
|
7616
|
+
const outputMode = resolveOutputMode();
|
|
7617
|
+
try {
|
|
7618
|
+
const { runMirrorStatus, formatMirrorStatus } = await import("./commands/mirror.js");
|
|
7619
|
+
const status = await runMirrorStatus();
|
|
7620
|
+
if (outputMode === "json") {
|
|
7621
|
+
writeOutput(status, outputMode);
|
|
7622
|
+
}
|
|
7623
|
+
else {
|
|
7624
|
+
process.stdout.write(formatMirrorStatus(status) + "\n");
|
|
7625
|
+
}
|
|
7626
|
+
}
|
|
7627
|
+
catch (err) {
|
|
7628
|
+
writeCliError(err instanceof Error ? err.message : String(err));
|
|
7629
|
+
process.exitCode = 1;
|
|
7630
|
+
}
|
|
7631
|
+
});
|
|
7400
7632
|
// ── Logs ──────────────────────────────────────────────────────────────────
|
|
7401
7633
|
const logsCommand = program.command("logs").description("View and decrypt persistent log files");
|
|
7402
7634
|
logsCommand
|
|
@@ -7693,6 +7925,18 @@ apiCommand
|
|
|
7693
7925
|
process.stderr.write(`neotoma api start: ${error}\n`);
|
|
7694
7926
|
return;
|
|
7695
7927
|
}
|
|
7928
|
+
// Persist preferred_env so plain CLI commands (e.g. `neotoma store`) route to
|
|
7929
|
+
// the same SQLite DB file as the running API, even when no --env flag or env
|
|
7930
|
+
// var is set. Read by `resolveLocalTransportEnv` in src/shared/local_transport.ts.
|
|
7931
|
+
try {
|
|
7932
|
+
const existingConfig = await readConfig();
|
|
7933
|
+
if (existingConfig.preferred_env !== envOpt) {
|
|
7934
|
+
await writeConfig({ ...existingConfig, preferred_env: envOpt });
|
|
7935
|
+
}
|
|
7936
|
+
}
|
|
7937
|
+
catch {
|
|
7938
|
+
// Non-fatal: preferred_env is an optimization, not required for api start.
|
|
7939
|
+
}
|
|
7696
7940
|
const effectiveEnv = envOpt === "prod" ? "production" : "development";
|
|
7697
7941
|
const apiLogsDir = path.join(CONFIG_DIR, effectiveEnv === "production" ? "logs_prod" : "logs");
|
|
7698
7942
|
const apiLogPath = path.join(apiLogsDir, "api.log");
|
|
@@ -8790,6 +9034,8 @@ schemasCommand
|
|
|
8790
9034
|
params: {
|
|
8791
9035
|
query: {
|
|
8792
9036
|
entity_type: opts.entityType,
|
|
9037
|
+
user_id: opts.userId,
|
|
9038
|
+
user_specific: opts.userSpecific,
|
|
8793
9039
|
limit: Number(opts.limit),
|
|
8794
9040
|
offset: Number(opts.offset),
|
|
8795
9041
|
},
|
|
@@ -8942,14 +9188,74 @@ schemasCommand
|
|
|
8942
9188
|
}
|
|
8943
9189
|
fieldsToRemove = parsed;
|
|
8944
9190
|
}
|
|
9191
|
+
const { data: existingSchemaData, error: existingSchemaError } = await api.GET("/schemas/{entity_type}", {
|
|
9192
|
+
params: {
|
|
9193
|
+
path: { entity_type: entityType },
|
|
9194
|
+
query: { user_id: opts.userId },
|
|
9195
|
+
},
|
|
9196
|
+
});
|
|
9197
|
+
const existingSchema = !existingSchemaError && existingSchemaData
|
|
9198
|
+
? existingSchemaData
|
|
9199
|
+
: null;
|
|
9200
|
+
if (!existingSchema && fieldsToAdd && fieldsToAdd.length > 0) {
|
|
9201
|
+
const removalSet = new Set(fieldsToRemove || []);
|
|
9202
|
+
const bootstrapFields = Object.fromEntries(fieldsToAdd
|
|
9203
|
+
.filter((field) => !removalSet.has(field.field_name))
|
|
9204
|
+
.map((field) => [
|
|
9205
|
+
field.field_name,
|
|
9206
|
+
{ type: field.field_type, required: field.required ?? false },
|
|
9207
|
+
]));
|
|
9208
|
+
if (Object.keys(bootstrapFields).length === 0) {
|
|
9209
|
+
throw new Error("at least one field must remain after removals");
|
|
9210
|
+
}
|
|
9211
|
+
const bootstrapPolicies = Object.fromEntries(fieldsToAdd
|
|
9212
|
+
.filter((field) => !removalSet.has(field.field_name))
|
|
9213
|
+
.map((field) => [
|
|
9214
|
+
field.field_name,
|
|
9215
|
+
{
|
|
9216
|
+
strategy: field.reducer_strategy || "last_write",
|
|
9217
|
+
tie_breaker: "observed_at",
|
|
9218
|
+
},
|
|
9219
|
+
]));
|
|
9220
|
+
const { data, error } = await api.POST("/register_schema", {
|
|
9221
|
+
body: {
|
|
9222
|
+
entity_type: entityType,
|
|
9223
|
+
schema_definition: { fields: bootstrapFields },
|
|
9224
|
+
reducer_config: { merge_policies: bootstrapPolicies },
|
|
9225
|
+
schema_version: opts.schemaVersion,
|
|
9226
|
+
activate: true,
|
|
9227
|
+
user_id: opts.userId,
|
|
9228
|
+
user_specific: opts.userSpecific,
|
|
9229
|
+
},
|
|
9230
|
+
});
|
|
9231
|
+
if (error)
|
|
9232
|
+
throw new Error("Failed to update schema");
|
|
9233
|
+
const registerResult = data;
|
|
9234
|
+
if (registerResult &&
|
|
9235
|
+
typeof registerResult["schema"] === "object" &&
|
|
9236
|
+
registerResult["schema"] !== null) {
|
|
9237
|
+
const schemaObj = registerResult["schema"];
|
|
9238
|
+
const normalizedSchema = {
|
|
9239
|
+
...schemaObj,
|
|
9240
|
+
fields: schemaObj["schema_definition"]?.["fields"] ??
|
|
9241
|
+
schemaObj["fields"],
|
|
9242
|
+
};
|
|
9243
|
+
writeOutput({ ...registerResult, schema: normalizedSchema }, outputMode);
|
|
9244
|
+
}
|
|
9245
|
+
else {
|
|
9246
|
+
writeOutput(data, outputMode);
|
|
9247
|
+
}
|
|
9248
|
+
return;
|
|
9249
|
+
}
|
|
8945
9250
|
const body = {
|
|
8946
9251
|
entity_type: entityType,
|
|
8947
9252
|
user_id: opts.userId,
|
|
8948
|
-
activate: opts.activate,
|
|
8949
9253
|
migrate_existing: opts.migrateExisting,
|
|
8950
9254
|
schema_version: opts.schemaVersion,
|
|
8951
9255
|
user_specific: opts.userSpecific,
|
|
8952
9256
|
};
|
|
9257
|
+
if (opts.activate)
|
|
9258
|
+
body.activate = true;
|
|
8953
9259
|
if (fieldsToAdd)
|
|
8954
9260
|
body.fields_to_add = fieldsToAdd;
|
|
8955
9261
|
if (fieldsToRemove)
|
|
@@ -9021,7 +9327,7 @@ schemasCommand
|
|
|
9021
9327
|
]));
|
|
9022
9328
|
}
|
|
9023
9329
|
const schemaDefinition = { fields: schemaFields };
|
|
9024
|
-
const reducerConfig = {};
|
|
9330
|
+
const reducerConfig = { merge_policies: {} };
|
|
9025
9331
|
const { data, error } = await api.POST("/register_schema", {
|
|
9026
9332
|
body: {
|
|
9027
9333
|
entity_type: entityType,
|
|
@@ -9057,8 +9363,7 @@ program
|
|
|
9057
9363
|
.option("--set <types>", "Comma-separated data types to prioritize (e.g. project_files,chat_transcripts,meeting_notes,notes,code,email,financial,custom)")
|
|
9058
9364
|
.option("--show", "Show current preferences", false)
|
|
9059
9365
|
.option("--reset", "Reset preferences to default", false)
|
|
9060
|
-
.action(async (
|
|
9061
|
-
const opts = cmd.opts();
|
|
9366
|
+
.action(async (opts) => {
|
|
9062
9367
|
const config = await readConfig();
|
|
9063
9368
|
const token = await getCliToken();
|
|
9064
9369
|
const api = createApiClient({
|
|
@@ -9146,8 +9451,7 @@ program
|
|
|
9146
9451
|
.option("--top <n>", "Number of top candidates to show", "20")
|
|
9147
9452
|
.option("--mode <mode>", "Discovery mode: quick, guided, full", "quick")
|
|
9148
9453
|
.option("--output <format>", "Output format: text, json", "text")
|
|
9149
|
-
.action(async (paths,
|
|
9150
|
-
const opts = cmd.opts();
|
|
9454
|
+
.action(async (paths, opts) => {
|
|
9151
9455
|
const { discover, formatDiscoveryOutput, formatDiscoveryJson } = await import("./discovery.js");
|
|
9152
9456
|
const scanPaths = paths.length > 0
|
|
9153
9457
|
? paths
|
|
@@ -9172,8 +9476,7 @@ program
|
|
|
9172
9476
|
.option("--preview", "Preview extracted data without storing", false)
|
|
9173
9477
|
.option("--limit <n>", "Process only the N most recent conversations")
|
|
9174
9478
|
.option("--filter <term>", "Only process conversations containing this term")
|
|
9175
|
-
.action(async (filePath,
|
|
9176
|
-
const opts = cmd.opts();
|
|
9479
|
+
.action(async (filePath, opts) => {
|
|
9177
9480
|
const { parseTranscript, formatTranscriptPreview } = await import("./transcript_parser.js");
|
|
9178
9481
|
const result = await parseTranscript({
|
|
9179
9482
|
filePath,
|
|
@@ -9223,39 +9526,33 @@ program
|
|
|
9223
9526
|
}
|
|
9224
9527
|
let unstructuredBody = {};
|
|
9225
9528
|
if (hasUnstructured) {
|
|
9226
|
-
let fileBuffer;
|
|
9227
9529
|
let originalFilename;
|
|
9228
9530
|
if (opts.filePath) {
|
|
9229
9531
|
const resolvedPath = path.isAbsolute(opts.filePath)
|
|
9230
9532
|
? opts.filePath
|
|
9231
9533
|
: path.resolve(process.cwd(), opts.filePath);
|
|
9232
|
-
|
|
9534
|
+
await fs.access(resolvedPath);
|
|
9233
9535
|
originalFilename = path.basename(resolvedPath);
|
|
9536
|
+
unstructuredBody = {
|
|
9537
|
+
file_path: resolvedPath,
|
|
9538
|
+
mime_type: inferMimeTypeForPath(resolvedPath),
|
|
9539
|
+
original_filename: originalFilename,
|
|
9540
|
+
};
|
|
9234
9541
|
}
|
|
9235
9542
|
else {
|
|
9236
|
-
fileBuffer = Buffer.from(opts.fileContent, "utf-8");
|
|
9543
|
+
const fileBuffer = Buffer.from(opts.fileContent, "utf-8");
|
|
9237
9544
|
originalFilename = undefined;
|
|
9545
|
+
unstructuredBody = {
|
|
9546
|
+
file_content: fileBuffer.toString("base64"),
|
|
9547
|
+
mime_type: "text/plain",
|
|
9548
|
+
original_filename: originalFilename,
|
|
9549
|
+
};
|
|
9238
9550
|
}
|
|
9239
|
-
const ext = originalFilename ? path.extname(originalFilename).toLowerCase() : "";
|
|
9240
|
-
const mimeMap = {
|
|
9241
|
-
".json": "application/json",
|
|
9242
|
-
".txt": "text/plain",
|
|
9243
|
-
".csv": "text/csv",
|
|
9244
|
-
".md": "text/markdown",
|
|
9245
|
-
".pdf": "application/pdf",
|
|
9246
|
-
};
|
|
9247
|
-
const mimeType = mimeMap[ext] ?? "application/octet-stream";
|
|
9248
|
-
const contentBase64 = fileBuffer.toString("base64");
|
|
9249
|
-
unstructuredBody = {
|
|
9250
|
-
file_content: contentBase64,
|
|
9251
|
-
mime_type: mimeType,
|
|
9252
|
-
original_filename: originalFilename,
|
|
9253
|
-
};
|
|
9254
9551
|
if (opts.fileIdempotencyKey) {
|
|
9255
9552
|
unstructuredBody.file_idempotency_key = opts.fileIdempotencyKey;
|
|
9256
9553
|
}
|
|
9257
9554
|
else if (!hasStructured && opts.idempotencyKey) {
|
|
9258
|
-
unstructuredBody.
|
|
9555
|
+
unstructuredBody.file_idempotency_key = opts.idempotencyKey;
|
|
9259
9556
|
}
|
|
9260
9557
|
}
|
|
9261
9558
|
if (entities && !Array.isArray(entities)) {
|
|
@@ -9523,23 +9820,9 @@ program
|
|
|
9523
9820
|
if (!stat?.isFile()) {
|
|
9524
9821
|
throw new Error(`File not found or not a file: ${resolvedPath}`);
|
|
9525
9822
|
}
|
|
9526
|
-
const fileBuffer = await fs.readFile(resolvedPath);
|
|
9527
9823
|
const originalFilename = path.basename(resolvedPath);
|
|
9528
|
-
const
|
|
9529
|
-
const
|
|
9530
|
-
".pdf": "application/pdf",
|
|
9531
|
-
".txt": "text/plain",
|
|
9532
|
-
".csv": "text/csv",
|
|
9533
|
-
".json": "application/json",
|
|
9534
|
-
".md": "text/markdown",
|
|
9535
|
-
".html": "text/html",
|
|
9536
|
-
".xml": "application/xml",
|
|
9537
|
-
".jpg": "image/jpeg",
|
|
9538
|
-
".jpeg": "image/jpeg",
|
|
9539
|
-
".png": "image/png",
|
|
9540
|
-
".gif": "image/gif",
|
|
9541
|
-
};
|
|
9542
|
-
const mimeType = opts.mimeType ?? mimeMap[ext] ?? "application/octet-stream";
|
|
9824
|
+
const fileBuffer = await fs.readFile(resolvedPath);
|
|
9825
|
+
const mimeType = opts.mimeType ?? inferMimeTypeForPath(resolvedPath);
|
|
9543
9826
|
if (opts.local) {
|
|
9544
9827
|
// Run the store+interpretation pipeline in-process without an API server.
|
|
9545
9828
|
try {
|
|
@@ -9578,16 +9861,13 @@ program
|
|
|
9578
9861
|
baseUrl: await resolveBaseUrl(program.opts().baseUrl, config),
|
|
9579
9862
|
token,
|
|
9580
9863
|
});
|
|
9581
|
-
const base64 = fileBuffer.toString("base64");
|
|
9582
9864
|
const body = {
|
|
9583
|
-
|
|
9865
|
+
file_path: resolvedPath,
|
|
9584
9866
|
mime_type: mimeType,
|
|
9585
9867
|
original_filename: originalFilename,
|
|
9868
|
+
file_idempotency_key: opts.idempotencyKey,
|
|
9586
9869
|
};
|
|
9587
|
-
|
|
9588
|
-
body.idempotency_key = opts.idempotencyKey;
|
|
9589
|
-
}
|
|
9590
|
-
const { data, error } = await api.POST("/store/unstructured", { body });
|
|
9870
|
+
const { data, error } = await api.POST("/store", { body });
|
|
9591
9871
|
if (error)
|
|
9592
9872
|
throw new Error("Failed to upload file");
|
|
9593
9873
|
writeOutput(data, outputMode);
|
|
@@ -9712,6 +9992,270 @@ correctionsCommand
|
|
|
9712
9992
|
const correctionId = result?.observation?.id ?? result?.correction_id ?? idempotencyKey;
|
|
9713
9993
|
writeOutput({ correction_id: correctionId, entity_id: entityId, success: result?.success ?? true }, outputMode);
|
|
9714
9994
|
});
|
|
9995
|
+
// ---------------------------------------------------------------------------
|
|
9996
|
+
// neotoma edit <id>
|
|
9997
|
+
//
|
|
9998
|
+
// One-shot structured editor: opens $EDITOR on a YAML view of the entity
|
|
9999
|
+
// snapshot, diffs on save, and pushes an atomic batch correction through the
|
|
10000
|
+
// shared applyBatchCorrection service (same backend the Inspector Edit tab
|
|
10001
|
+
// uses). Conflicts surface as a resolvable prompt so the user can re-edit
|
|
10002
|
+
// against the rebased snapshot instead of overwriting blindly.
|
|
10003
|
+
// ---------------------------------------------------------------------------
|
|
10004
|
+
program
|
|
10005
|
+
.command("edit")
|
|
10006
|
+
.description("Edit an entity's fields in $EDITOR and save as a batch correction")
|
|
10007
|
+
.argument("<id>", "Entity ID to edit")
|
|
10008
|
+
.option("--user-id <userId>", "User ID")
|
|
10009
|
+
.option("--overwrite", "Overwrite conflicting changes without prompting")
|
|
10010
|
+
.option("--editor <cmd>", "Override editor command (defaults to $EDITOR)")
|
|
10011
|
+
.action(async (entityId, opts) => {
|
|
10012
|
+
const outputMode = resolveOutputMode();
|
|
10013
|
+
const config = await readConfig();
|
|
10014
|
+
const token = await getCliToken();
|
|
10015
|
+
const api = createApiClient({
|
|
10016
|
+
baseUrl: await resolveBaseUrl(program.opts().baseUrl, config),
|
|
10017
|
+
token,
|
|
10018
|
+
});
|
|
10019
|
+
const yaml = await import("js-yaml");
|
|
10020
|
+
async function fetchEntity() {
|
|
10021
|
+
const { data, error, response } = await api.GET("/entities/{id}", {
|
|
10022
|
+
params: { path: { id: entityId } },
|
|
10023
|
+
});
|
|
10024
|
+
if (error) {
|
|
10025
|
+
const status = response?.status;
|
|
10026
|
+
throw new Error(status
|
|
10027
|
+
? `Failed to load entity: ${status} ${formatApiError(error)}`
|
|
10028
|
+
: `Failed to load entity: ${formatApiError(error)}`);
|
|
10029
|
+
}
|
|
10030
|
+
const body = (data ?? {});
|
|
10031
|
+
return {
|
|
10032
|
+
entity_type: body.entity_type ?? body.type ?? "unknown",
|
|
10033
|
+
snapshot: body.snapshot ??
|
|
10034
|
+
body.computed_snapshot ??
|
|
10035
|
+
{},
|
|
10036
|
+
last_observation_at: body.last_observation_at ?? null,
|
|
10037
|
+
};
|
|
10038
|
+
}
|
|
10039
|
+
function buildEditorBuffer(entity_type, last_observation_at, snapshot) {
|
|
10040
|
+
// Deterministic header plus alphabetized snapshot so repeated edits
|
|
10041
|
+
// produce stable diffs. Trailing '---' is kept so the user sees where
|
|
10042
|
+
// the editable region ends.
|
|
10043
|
+
const sortedSnapshot = Object.fromEntries(Object.keys(snapshot)
|
|
10044
|
+
.sort()
|
|
10045
|
+
.map((k) => [k, snapshot[k]]));
|
|
10046
|
+
const yamlBody = yaml.dump(sortedSnapshot, {
|
|
10047
|
+
noRefs: true,
|
|
10048
|
+
sortKeys: true,
|
|
10049
|
+
lineWidth: 120,
|
|
10050
|
+
});
|
|
10051
|
+
return [
|
|
10052
|
+
`# neotoma edit ${entityId}`,
|
|
10053
|
+
`# entity_type: ${entity_type}`,
|
|
10054
|
+
`# base_last_observation_at: ${last_observation_at ?? "null"}`,
|
|
10055
|
+
"# Edit field values below. Lines starting with '#' are comments.",
|
|
10056
|
+
"# Save and exit to apply changes; quit without saving to cancel.",
|
|
10057
|
+
"---",
|
|
10058
|
+
yamlBody.trimEnd(),
|
|
10059
|
+
"",
|
|
10060
|
+
].join("\n");
|
|
10061
|
+
}
|
|
10062
|
+
function parseEditorBuffer(text) {
|
|
10063
|
+
// Strip comment-only lines before the first non-comment; yaml already
|
|
10064
|
+
// skips blank lines and leading `---`.
|
|
10065
|
+
const stripped = text
|
|
10066
|
+
.split("\n")
|
|
10067
|
+
.filter((l) => !l.startsWith("#"))
|
|
10068
|
+
.join("\n");
|
|
10069
|
+
const parsed = yaml.load(stripped);
|
|
10070
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
10071
|
+
return {};
|
|
10072
|
+
}
|
|
10073
|
+
return parsed;
|
|
10074
|
+
}
|
|
10075
|
+
async function runEditorOn(filePath) {
|
|
10076
|
+
const editor = opts.editor ?? process.env.EDITOR ?? process.env.VISUAL ?? "vi";
|
|
10077
|
+
// Let the editor inherit the TTY so the user gets an actual interactive
|
|
10078
|
+
// session. spawnSync would block this process.
|
|
10079
|
+
const [cmd, ...cmdArgs] = editor.split(" ");
|
|
10080
|
+
await new Promise((resolve, reject) => {
|
|
10081
|
+
const child = spawn(cmd, [...cmdArgs, filePath], { stdio: "inherit" });
|
|
10082
|
+
child.on("error", reject);
|
|
10083
|
+
child.on("exit", (code) => code === 0 ? resolve() : reject(new Error(`Editor exited with code ${code}`)));
|
|
10084
|
+
});
|
|
10085
|
+
}
|
|
10086
|
+
const first = await fetchEntity();
|
|
10087
|
+
let current = first;
|
|
10088
|
+
let buffer = buildEditorBuffer(current.entity_type, current.last_observation_at, current.snapshot);
|
|
10089
|
+
// A modest loop to recover from conflicts: re-fetch, re-present the
|
|
10090
|
+
// user's edits on top of the rebased snapshot, let them re-save.
|
|
10091
|
+
let attempts = 0;
|
|
10092
|
+
while (attempts < 5) {
|
|
10093
|
+
attempts += 1;
|
|
10094
|
+
const tmpPath = path.join(os.tmpdir(), `neotoma-edit-${entityId}-${Date.now()}-${attempts}.yaml`);
|
|
10095
|
+
await fs.writeFile(tmpPath, buffer, "utf-8");
|
|
10096
|
+
try {
|
|
10097
|
+
await runEditorOn(tmpPath);
|
|
10098
|
+
const newText = await fs.readFile(tmpPath, "utf-8");
|
|
10099
|
+
if (newText === buffer) {
|
|
10100
|
+
writeOutput({ success: true, status: "no_changes", entity_id: entityId }, outputMode);
|
|
10101
|
+
return;
|
|
10102
|
+
}
|
|
10103
|
+
let desired;
|
|
10104
|
+
try {
|
|
10105
|
+
desired = parseEditorBuffer(newText);
|
|
10106
|
+
}
|
|
10107
|
+
catch (err) {
|
|
10108
|
+
throw new Error(`Failed to parse YAML: ${err instanceof Error ? err.message : String(err)}. ` +
|
|
10109
|
+
`File left at ${tmpPath}.`);
|
|
10110
|
+
}
|
|
10111
|
+
const changes = [];
|
|
10112
|
+
const desiredKeys = new Set(Object.keys(desired));
|
|
10113
|
+
for (const field of Array.from(desiredKeys).sort()) {
|
|
10114
|
+
const next = desired[field];
|
|
10115
|
+
const prev = current.snapshot[field];
|
|
10116
|
+
if (JSON.stringify(next) !== JSON.stringify(prev)) {
|
|
10117
|
+
changes.push({ field, value: next });
|
|
10118
|
+
}
|
|
10119
|
+
}
|
|
10120
|
+
if (changes.length === 0) {
|
|
10121
|
+
writeOutput({ success: true, status: "no_changes", entity_id: entityId }, outputMode);
|
|
10122
|
+
return;
|
|
10123
|
+
}
|
|
10124
|
+
// /entities/:id/batch_correct is not yet in the OpenAPI spec, so we
|
|
10125
|
+
// call it via raw fetch. It returns a structured JSON body whose
|
|
10126
|
+
// `status` discriminates `applied` / `conflict` / `validation_error`.
|
|
10127
|
+
const baseUrl = (await resolveBaseUrl(program.opts().baseUrl, config)).replace(/\/$/, "");
|
|
10128
|
+
const headers = {
|
|
10129
|
+
"Content-Type": "application/json",
|
|
10130
|
+
};
|
|
10131
|
+
if (token)
|
|
10132
|
+
headers["Authorization"] = `Bearer ${token}`;
|
|
10133
|
+
const rawResponse = await fetch(`${baseUrl}/entities/${encodeURIComponent(entityId)}/batch_correct`, {
|
|
10134
|
+
method: "POST",
|
|
10135
|
+
headers,
|
|
10136
|
+
body: JSON.stringify({
|
|
10137
|
+
changes,
|
|
10138
|
+
expected_last_observation_at: current.last_observation_at,
|
|
10139
|
+
overwrite: opts.overwrite === true,
|
|
10140
|
+
idempotency_prefix: `cli-edit-${entityId}-${Date.now()}`,
|
|
10141
|
+
user_id: opts.userId,
|
|
10142
|
+
}),
|
|
10143
|
+
});
|
|
10144
|
+
const status = rawResponse.status;
|
|
10145
|
+
let resultBody = {};
|
|
10146
|
+
try {
|
|
10147
|
+
resultBody = (await rawResponse.json());
|
|
10148
|
+
}
|
|
10149
|
+
catch {
|
|
10150
|
+
/* empty body */
|
|
10151
|
+
}
|
|
10152
|
+
const resultStatus = resultBody.status ?? "";
|
|
10153
|
+
if (status === 409 || resultStatus === "conflict") {
|
|
10154
|
+
if (opts.overwrite) {
|
|
10155
|
+
// Shouldn't happen (server skips conflict check with overwrite)
|
|
10156
|
+
// but be defensive.
|
|
10157
|
+
throw new Error(`Server reported conflict despite --overwrite. Body: ${JSON.stringify(resultBody)}`);
|
|
10158
|
+
}
|
|
10159
|
+
process.stderr.write(warn(`Conflict: entity was modified since you started editing. Rebasing.\n`));
|
|
10160
|
+
// Rebase: refetch, rebuild the buffer with the latest snapshot but
|
|
10161
|
+
// preserve the user's edits where they still differ.
|
|
10162
|
+
const latest = await fetchEntity();
|
|
10163
|
+
const merged = { ...latest.snapshot };
|
|
10164
|
+
for (const { field, value } of changes) {
|
|
10165
|
+
merged[field] = value;
|
|
10166
|
+
}
|
|
10167
|
+
current = latest;
|
|
10168
|
+
buffer = buildEditorBuffer(latest.entity_type, latest.last_observation_at, merged);
|
|
10169
|
+
continue;
|
|
10170
|
+
}
|
|
10171
|
+
if (status === 400 || resultStatus === "validation_error") {
|
|
10172
|
+
const errs = (resultBody.validation_errors ?? []);
|
|
10173
|
+
throw new Error(`Validation failed:\n${errs
|
|
10174
|
+
.map((ve) => ` - ${ve.field}: ${ve.message}`)
|
|
10175
|
+
.join("\n") || JSON.stringify(resultBody)}`);
|
|
10176
|
+
}
|
|
10177
|
+
if (!rawResponse.ok) {
|
|
10178
|
+
throw new Error(`Batch correction failed: ${status} ${JSON.stringify(resultBody)}`);
|
|
10179
|
+
}
|
|
10180
|
+
const applied = Array.isArray(resultBody.applied)
|
|
10181
|
+
? resultBody.applied.length
|
|
10182
|
+
: changes.length;
|
|
10183
|
+
writeOutput({
|
|
10184
|
+
success: true,
|
|
10185
|
+
status: "applied",
|
|
10186
|
+
entity_id: entityId,
|
|
10187
|
+
fields_changed: changes.map((c) => c.field),
|
|
10188
|
+
applied_count: applied,
|
|
10189
|
+
last_observation_at: resultBody.last_observation_at ?? null,
|
|
10190
|
+
}, outputMode);
|
|
10191
|
+
return;
|
|
10192
|
+
}
|
|
10193
|
+
finally {
|
|
10194
|
+
// Keep the file around on throw for debugging; clean up on success
|
|
10195
|
+
// paths by attempting unlink.
|
|
10196
|
+
try {
|
|
10197
|
+
await fs.unlink(tmpPath);
|
|
10198
|
+
}
|
|
10199
|
+
catch {
|
|
10200
|
+
/* ignore */
|
|
10201
|
+
}
|
|
10202
|
+
}
|
|
10203
|
+
}
|
|
10204
|
+
throw new Error("Exceeded maximum conflict-rebase attempts (5).");
|
|
10205
|
+
});
|
|
10206
|
+
// ---------------------------------------------------------------------------
|
|
10207
|
+
// neotoma memory-export
|
|
10208
|
+
//
|
|
10209
|
+
// Writes a bounded single-file MEMORY.md using the same canonical renderer
|
|
10210
|
+
// that backs MCP `retrieve_entity_snapshot`. Shape and defaults match Phase 5
|
|
10211
|
+
// of the markdown-memory-ceiling plan.
|
|
10212
|
+
// ---------------------------------------------------------------------------
|
|
10213
|
+
program
|
|
10214
|
+
.command("memory-export")
|
|
10215
|
+
.description("Export a bounded MEMORY.md for agent harnesses that expect a single file")
|
|
10216
|
+
.option("--path <path>", "Output path", "MEMORY.md")
|
|
10217
|
+
.option("--limit-lines <n>", "Hard line cap (0 disables truncation)", "200")
|
|
10218
|
+
.option("--include-types <list>", "Comma-separated entity types to include (default: all non-bookkeeping)")
|
|
10219
|
+
.option("--exclude-types <list>", "Comma-separated entity types to exclude (applied in addition to bookkeeping filter)")
|
|
10220
|
+
.option("--include-bookkeeping", "Include chat bookkeeping (conversation, agent_message). Excluded by default.")
|
|
10221
|
+
.option("--order <strategy>", "Ordering strategy: importance | recency", "importance")
|
|
10222
|
+
.option("--max-field-chars <n>", "Per-field character cap for long string values (0 disables)", "400")
|
|
10223
|
+
.option("--user-id <userId>", "Scope export to a specific user")
|
|
10224
|
+
.action(async (opts) => {
|
|
10225
|
+
const outputMode = resolveOutputMode();
|
|
10226
|
+
const order = (opts.order ?? "importance").toLowerCase();
|
|
10227
|
+
if (order !== "recency" && order !== "importance") {
|
|
10228
|
+
throw new Error(`Invalid --order: ${opts.order}. Allowed: importance, recency`);
|
|
10229
|
+
}
|
|
10230
|
+
const limitLines = Number.parseInt(String(opts.limitLines ?? "200"), 10);
|
|
10231
|
+
if (!Number.isFinite(limitLines)) {
|
|
10232
|
+
throw new Error(`Invalid --limit-lines: ${opts.limitLines}`);
|
|
10233
|
+
}
|
|
10234
|
+
const maxFieldChars = Number.parseInt(String(opts.maxFieldChars ?? "400"), 10);
|
|
10235
|
+
if (!Number.isFinite(maxFieldChars) || maxFieldChars < 0) {
|
|
10236
|
+
throw new Error(`Invalid --max-field-chars: ${opts.maxFieldChars}`);
|
|
10237
|
+
}
|
|
10238
|
+
const splitCsv = (raw) => raw
|
|
10239
|
+
? raw
|
|
10240
|
+
.split(",")
|
|
10241
|
+
.map((s) => s.trim())
|
|
10242
|
+
.filter((s) => s.length > 0)
|
|
10243
|
+
: undefined;
|
|
10244
|
+
const includeTypes = splitCsv(opts.includeTypes);
|
|
10245
|
+
const excludeTypes = splitCsv(opts.excludeTypes);
|
|
10246
|
+
const { exportMemory } = await import("../services/memory_export.js");
|
|
10247
|
+
const result = await exportMemory({
|
|
10248
|
+
path: opts.path,
|
|
10249
|
+
limit_lines: limitLines,
|
|
10250
|
+
include_types: includeTypes,
|
|
10251
|
+
exclude_types: excludeTypes,
|
|
10252
|
+
include_bookkeeping: opts.includeBookkeeping === true,
|
|
10253
|
+
order: order,
|
|
10254
|
+
max_field_chars: maxFieldChars,
|
|
10255
|
+
user_id: opts.userId,
|
|
10256
|
+
});
|
|
10257
|
+
writeOutput(result, outputMode);
|
|
10258
|
+
});
|
|
9715
10259
|
const statsCmd = program.command("stats").description("Dashboard and entity statistics");
|
|
9716
10260
|
statsCmd.action(async () => {
|
|
9717
10261
|
const outputMode = resolveOutputMode();
|
|
@@ -12045,12 +12589,14 @@ export async function runCli(argv = process.argv) {
|
|
|
12045
12589
|
"api",
|
|
12046
12590
|
"backup",
|
|
12047
12591
|
"dev",
|
|
12592
|
+
"doctor",
|
|
12048
12593
|
"entities",
|
|
12049
12594
|
"logs",
|
|
12050
12595
|
"mcp",
|
|
12051
12596
|
"observations",
|
|
12052
12597
|
"relationships",
|
|
12053
12598
|
"schemas",
|
|
12599
|
+
"setup",
|
|
12054
12600
|
"sources",
|
|
12055
12601
|
"storage",
|
|
12056
12602
|
"timeline",
|