march-cli 0.1.0
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/bin/march.mjs +13 -0
- package/package.json +36 -0
- package/src/agent/command-exec-tool.mjs +91 -0
- package/src/agent/context-stats-tool.mjs +57 -0
- package/src/agent/editing/diff-apply.mjs +28 -0
- package/src/agent/editing/diff-format.mjs +57 -0
- package/src/agent/file-edit-tool.mjs +276 -0
- package/src/agent/find-tool.mjs +112 -0
- package/src/agent/model-payload-dumper.mjs +201 -0
- package/src/agent/pi-session/pi-session-sidecar-failure.mjs +10 -0
- package/src/agent/provider/payload-messages.mjs +138 -0
- package/src/agent/read-file-tool.mjs +112 -0
- package/src/agent/runner/fast-model.mjs +36 -0
- package/src/agent/runner/runner-cleanup.mjs +12 -0
- package/src/agent/runner/runner-init.mjs +15 -0
- package/src/agent/runner/runner-session-state.mjs +40 -0
- package/src/agent/runner.mjs +266 -0
- package/src/agent/runtime/runner-runtime-host.mjs +73 -0
- package/src/agent/runtime/runtime-factory.mjs +42 -0
- package/src/agent/runtime/runtime-host.mjs +34 -0
- package/src/agent/session/session-auto-name.mjs +41 -0
- package/src/agent/session/session-binding.mjs +12 -0
- package/src/agent/session/session-options.mjs +46 -0
- package/src/agent/tool-names.mjs +1 -0
- package/src/agent/tool-result.mjs +3 -0
- package/src/agent/tools.mjs +54 -0
- package/src/agent/turn/turn-events.mjs +64 -0
- package/src/agent/turn/turn-runner.mjs +103 -0
- package/src/auth/login-command.mjs +90 -0
- package/src/auth/storage.mjs +33 -0
- package/src/cli/args.mjs +71 -0
- package/src/cli/commands/copy-command.mjs +73 -0
- package/src/cli/commands/export-command.mjs +206 -0
- package/src/cli/commands/extensions-command.mjs +53 -0
- package/src/cli/commands/help-command.mjs +7 -0
- package/src/cli/commands/model-command.mjs +110 -0
- package/src/cli/commands/paste-image-command.mjs +43 -0
- package/src/cli/commands/provider-command.mjs +55 -0
- package/src/cli/commands/status-command.mjs +157 -0
- package/src/cli/commands/thinking-command.mjs +80 -0
- package/src/cli/fallback-ui.mjs +156 -0
- package/src/cli/input/attachment-tokens.mjs +20 -0
- package/src/cli/input/autocomplete.mjs +106 -0
- package/src/cli/input/external-editor.mjs +39 -0
- package/src/cli/input/history-store.mjs +35 -0
- package/src/cli/input/image-clipboard.mjs +55 -0
- package/src/cli/input/keybinding-dispatch.mjs +76 -0
- package/src/cli/input/keybindings.mjs +96 -0
- package/src/cli/input/mode-state.mjs +43 -0
- package/src/cli/input/prompt-templates.mjs +84 -0
- package/src/cli/input/select-with-keyboard.mjs +67 -0
- package/src/cli/permissions.mjs +103 -0
- package/src/cli/repl-commands.mjs +86 -0
- package/src/cli/repl-loop.mjs +157 -0
- package/src/cli/selector-list.mjs +21 -0
- package/src/cli/session/pi-session-switch-command.mjs +41 -0
- package/src/cli/session/session-command.mjs +23 -0
- package/src/cli/session/session-list-command.mjs +68 -0
- package/src/cli/session/session-name-command.mjs +26 -0
- package/src/cli/session/session-source-command.mjs +89 -0
- package/src/cli/session/session-switch-command.mjs +1 -0
- package/src/cli/shell/shell-command.mjs +55 -0
- package/src/cli/shell/shell-drawer-controls.mjs +33 -0
- package/src/cli/shell/shell-drawer.mjs +192 -0
- package/src/cli/shell/shell-split-layout.mjs +70 -0
- package/src/cli/slash-commands.mjs +176 -0
- package/src/cli/startup/startup-banner.mjs +17 -0
- package/src/cli/startup/startup-session.mjs +51 -0
- package/src/cli/status-line-updater.mjs +74 -0
- package/src/cli/tool-output.mjs +9 -0
- package/src/cli/tui/editor/external-editor-runner.mjs +24 -0
- package/src/cli/tui/input/mouse-selection-controller.mjs +89 -0
- package/src/cli/tui/input/mouse-tracking.mjs +20 -0
- package/src/cli/tui/layout/main-pane-layout.mjs +38 -0
- package/src/cli/tui/layout/safe-render-boundary.mjs +46 -0
- package/src/cli/tui/markdown-renderer.mjs +279 -0
- package/src/cli/tui/output/scroll-state.mjs +79 -0
- package/src/cli/tui/output/tool-card-renderer.mjs +59 -0
- package/src/cli/tui/output-buffer.mjs +297 -0
- package/src/cli/tui/permission-request-ui.mjs +18 -0
- package/src/cli/tui/recall-rendering.mjs +25 -0
- package/src/cli/tui/select/editor-select-list.mjs +111 -0
- package/src/cli/tui/selection-screen.mjs +212 -0
- package/src/cli/tui/status/retry-status.mjs +72 -0
- package/src/cli/tui/status/spinner-status.mjs +42 -0
- package/src/cli/tui/status/status-bar.mjs +88 -0
- package/src/cli/tui/syntax/highlighting.mjs +277 -0
- package/src/cli/tui/syntax/languages.mjs +91 -0
- package/src/cli/tui/syntax/tree-sitter/bash.highlights.scm +261 -0
- package/src/cli/tui/syntax/tree-sitter/c.highlights.scm +341 -0
- package/src/cli/tui/syntax/tree-sitter/cpp.highlights.scm +268 -0
- package/src/cli/tui/syntax/tree-sitter/csharp.highlights.scm +577 -0
- package/src/cli/tui/syntax/tree-sitter/css.highlights.scm +109 -0
- package/src/cli/tui/syntax/tree-sitter/diff.highlights.scm +49 -0
- package/src/cli/tui/syntax/tree-sitter/go.highlights.scm +254 -0
- package/src/cli/tui/syntax/tree-sitter/html.highlights.scm +13 -0
- package/src/cli/tui/syntax/tree-sitter/java.highlights.scm +330 -0
- package/src/cli/tui/syntax/tree-sitter/json.highlights.scm +38 -0
- package/src/cli/tui/syntax/tree-sitter/php.highlights.scm +203 -0
- package/src/cli/tui/syntax/tree-sitter/python.highlights.scm +137 -0
- package/src/cli/tui/syntax/tree-sitter/ruby.highlights.scm +309 -0
- package/src/cli/tui/syntax/tree-sitter/rust.highlights.scm +531 -0
- package/src/cli/tui/syntax/tree-sitter/toml.highlights.scm +39 -0
- package/src/cli/tui/syntax/tree-sitter/tree-sitter-bash.wasm +0 -0
- package/src/cli/tui/syntax/tree-sitter/tree-sitter-c-sharp.wasm +0 -0
- package/src/cli/tui/syntax/tree-sitter/tree-sitter-c.wasm +0 -0
- package/src/cli/tui/syntax/tree-sitter/tree-sitter-cpp.wasm +0 -0
- package/src/cli/tui/syntax/tree-sitter/tree-sitter-css.wasm +0 -0
- package/src/cli/tui/syntax/tree-sitter/tree-sitter-diff.wasm +0 -0
- package/src/cli/tui/syntax/tree-sitter/tree-sitter-go.wasm +0 -0
- package/src/cli/tui/syntax/tree-sitter/tree-sitter-html.wasm +0 -0
- package/src/cli/tui/syntax/tree-sitter/tree-sitter-java.wasm +0 -0
- package/src/cli/tui/syntax/tree-sitter/tree-sitter-json.wasm +0 -0
- package/src/cli/tui/syntax/tree-sitter/tree-sitter-php.wasm +0 -0
- package/src/cli/tui/syntax/tree-sitter/tree-sitter-python.wasm +0 -0
- package/src/cli/tui/syntax/tree-sitter/tree-sitter-ruby.wasm +0 -0
- package/src/cli/tui/syntax/tree-sitter/tree-sitter-rust.wasm +0 -0
- package/src/cli/tui/syntax/tree-sitter/tree-sitter-toml.wasm +0 -0
- package/src/cli/tui/syntax/tree-sitter/tree-sitter-tsx.wasm +0 -0
- package/src/cli/tui/syntax/tree-sitter/tree-sitter-typescript.wasm +0 -0
- package/src/cli/tui/syntax/tree-sitter/tree-sitter-yaml.wasm +0 -0
- package/src/cli/tui/syntax/tree-sitter/tsx.highlights.scm +35 -0
- package/src/cli/tui/syntax/tree-sitter/typescript.highlights.scm +35 -0
- package/src/cli/tui/syntax/tree-sitter/yaml.highlights.scm +99 -0
- package/src/cli/tui/tool-rendering.mjs +194 -0
- package/src/cli/tui/tui-diff-rendering.mjs +157 -0
- package/src/cli/tui/tui-handlers.mjs +110 -0
- package/src/cli/tui/tui-input-controller.mjs +61 -0
- package/src/cli/tui/ui-theme.mjs +148 -0
- package/src/cli/ui.mjs +299 -0
- package/src/config/config-json.mjs +73 -0
- package/src/config/dotenv.mjs +20 -0
- package/src/config/features.mjs +75 -0
- package/src/config/loader.mjs +109 -0
- package/src/config/settings-command.mjs +97 -0
- package/src/context/diagnostics.mjs +70 -0
- package/src/context/engine.mjs +148 -0
- package/src/context/injections.mjs +26 -0
- package/src/context/project-context.mjs +20 -0
- package/src/context/session-status.mjs +15 -0
- package/src/context/shell-layers.mjs +23 -0
- package/src/context/system-core/base.md +60 -0
- package/src/context/system-core/prompts/deepseek-v4-pro.md +3 -0
- package/src/context/system-core/prompts/default.md +3 -0
- package/src/context/system-core.mjs +35 -0
- package/src/debug/model-context-dumper.mjs +52 -0
- package/src/extensions/discovery.mjs +40 -0
- package/src/extensions/lifecycle-adapter.mjs +210 -0
- package/src/extensions/lifecycle-manifest.mjs +69 -0
- package/src/image-gen/index.mjs +7 -0
- package/src/image-gen/provider.mjs +231 -0
- package/src/image-gen/tool.mjs +84 -0
- package/src/lsp/client.mjs +204 -0
- package/src/lsp/diagnostic-store.mjs +39 -0
- package/src/lsp/servers.mjs +212 -0
- package/src/lsp/service.mjs +65 -0
- package/src/main.mjs +294 -0
- package/src/mcp/client.mjs +195 -0
- package/src/mcp/config.mjs +130 -0
- package/src/mcp/index.mjs +48 -0
- package/src/mcp/tools.mjs +98 -0
- package/src/memory/database.mjs +219 -0
- package/src/memory/glossary.mjs +124 -0
- package/src/memory/graph/graph-cascades.mjs +109 -0
- package/src/memory/graph/graph-diagnostics.mjs +73 -0
- package/src/memory/graph/graph-path-removal.mjs +50 -0
- package/src/memory/graph/graph-path-utils.mjs +17 -0
- package/src/memory/graph/graph-primitives.mjs +103 -0
- package/src/memory/graph/graph-read.mjs +159 -0
- package/src/memory/graph.mjs +282 -0
- package/src/memory/markdown/markdown-delete.mjs +23 -0
- package/src/memory/markdown/markdown-format.mjs +128 -0
- package/src/memory/markdown/markdown-recall.mjs +28 -0
- package/src/memory/markdown/ripgrep.mjs +16 -0
- package/src/memory/markdown/sqlite-index.mjs +87 -0
- package/src/memory/markdown-store.mjs +286 -0
- package/src/memory/markdown-tools.mjs +103 -0
- package/src/memory/search.mjs +142 -0
- package/src/memory/snapshot.mjs +86 -0
- package/src/memory/system-views.mjs +120 -0
- package/src/memory/tools.mjs +282 -0
- package/src/notification/desktop-notifier.mjs +85 -0
- package/src/platform/open-file.mjs +28 -0
- package/src/provider/config-command.mjs +129 -0
- package/src/provider/presets.mjs +72 -0
- package/src/session/attachment-display.mjs +16 -0
- package/src/session/attachment-references.mjs +65 -0
- package/src/session/attachments.mjs +140 -0
- package/src/session/persist.mjs +1 -0
- package/src/session/pi-manager.mjs +34 -0
- package/src/session/session-utils.mjs +16 -0
- package/src/session/sidecar-sync.mjs +19 -0
- package/src/session/sidecar.mjs +68 -0
- package/src/session/transcript.mjs +83 -0
- package/src/session/tree.mjs +42 -0
- package/src/shell/cli-runtime.mjs +11 -0
- package/src/shell/hints.mjs +12 -0
- package/src/shell/node-pty-adapter.mjs +81 -0
- package/src/shell/runtime-state.mjs +126 -0
- package/src/shell/runtime.mjs +244 -0
- package/src/shell/screen-buffer.mjs +136 -0
- package/src/shell/tool-read.mjs +74 -0
- package/src/shell/tools.mjs +299 -0
- package/src/supergrok/actions/image-generate.mjs +60 -0
- package/src/supergrok/actions/search.mjs +78 -0
- package/src/supergrok/auth.mjs +36 -0
- package/src/supergrok/constants.mjs +18 -0
- package/src/supergrok/oauth-provider.mjs +278 -0
- package/src/supergrok/provider.mjs +36 -0
- package/src/supergrok/response.mjs +76 -0
- package/src/supergrok/tool.mjs +61 -0
- package/src/text/ansi.mjs +3 -0
- package/src/web/config-command.mjs +43 -0
- package/src/web/fetch.mjs +78 -0
- package/src/web/presets.mjs +16 -0
- package/src/web/search.mjs +83 -0
- package/src/web/tools.mjs +107 -0
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { defineTool } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { Type } from "typebox";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Build a tool name from server + tool: mcp__{server}__{tool}
|
|
6
|
+
*/
|
|
7
|
+
export function buildMcpToolName(server, tool) {
|
|
8
|
+
return `mcp__${sanitize(server)}__${sanitize(tool)}`;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Parse an MCP tool name back into { server, tool }.
|
|
13
|
+
*/
|
|
14
|
+
export function parseMcpToolName(name) {
|
|
15
|
+
const match = name.match(/^mcp__([^_]+(?:_[^_]+)*?)__([^_]+(?:_.+)?)$/);
|
|
16
|
+
if (!match) return null;
|
|
17
|
+
return { server: match[1], tool: match[2] };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function sanitize(s) {
|
|
21
|
+
return s.replace(/[^a-zA-Z0-9_-]/g, "_").replace(/__+/g, "_");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Map JSON Schema type to TypeBox type.
|
|
26
|
+
* Handles the common MCP parameter schemas.
|
|
27
|
+
*/
|
|
28
|
+
function schemaToTypeBox(schema) {
|
|
29
|
+
if (!schema || !schema.properties) return Type.Object({});
|
|
30
|
+
|
|
31
|
+
const props = {};
|
|
32
|
+
const required = new Set(schema.required ?? []);
|
|
33
|
+
|
|
34
|
+
for (const [key, prop] of Object.entries(schema.properties)) {
|
|
35
|
+
let box;
|
|
36
|
+
switch (prop.type) {
|
|
37
|
+
case "string":
|
|
38
|
+
box = Type.String();
|
|
39
|
+
break;
|
|
40
|
+
case "number":
|
|
41
|
+
case "integer":
|
|
42
|
+
box = Type.Number();
|
|
43
|
+
break;
|
|
44
|
+
case "boolean":
|
|
45
|
+
box = Type.Boolean();
|
|
46
|
+
break;
|
|
47
|
+
case "array":
|
|
48
|
+
box = Type.Array(
|
|
49
|
+
prop.items ? schemaToTypeBox(prop.items) : Type.Any(),
|
|
50
|
+
);
|
|
51
|
+
break;
|
|
52
|
+
case "object":
|
|
53
|
+
box = schemaToTypeBox(prop);
|
|
54
|
+
break;
|
|
55
|
+
default:
|
|
56
|
+
box = Type.Any();
|
|
57
|
+
}
|
|
58
|
+
if (prop.description) box = box({ description: prop.description });
|
|
59
|
+
if (!required.has(key)) box = Type.Optional(box);
|
|
60
|
+
props[key] = box;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return Type.Object(props);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Convert an MCP tool definition to a March-compatible tool.
|
|
68
|
+
*/
|
|
69
|
+
export function convertMcpTool(serverName, mcpTool, clientManager) {
|
|
70
|
+
const fullName = buildMcpToolName(serverName, mcpTool.name);
|
|
71
|
+
const description = mcpTool.description ?? `MCP tool: ${serverName}/${mcpTool.name}`;
|
|
72
|
+
|
|
73
|
+
return defineTool({
|
|
74
|
+
name: fullName,
|
|
75
|
+
label: `MCP:${serverName}/${mcpTool.name}`,
|
|
76
|
+
description: `[MCP ${serverName}] ${description}`,
|
|
77
|
+
parameters: schemaToTypeBox(mcpTool.inputSchema),
|
|
78
|
+
execute: async (_toolCallId, params) => {
|
|
79
|
+
const result = await clientManager.callTool(serverName, mcpTool.name, params);
|
|
80
|
+
return {
|
|
81
|
+
content: [{ type: "text", text: result.content }],
|
|
82
|
+
details: result.details,
|
|
83
|
+
};
|
|
84
|
+
},
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Batch-convert all tools from all connected servers.
|
|
90
|
+
*/
|
|
91
|
+
export function convertAllMcpTools(clientManager) {
|
|
92
|
+
const tools = [];
|
|
93
|
+
const allTools = clientManager.getAllTools();
|
|
94
|
+
for (const { server, tool } of allTools) {
|
|
95
|
+
tools.push(convertMcpTool(server, tool, clientManager));
|
|
96
|
+
}
|
|
97
|
+
return tools;
|
|
98
|
+
}
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import { DatabaseSync } from "node:sqlite";
|
|
2
|
+
import { mkdirSync } from "node:fs";
|
|
3
|
+
import { dirname } from "node:path";
|
|
4
|
+
|
|
5
|
+
export const ROOT_NODE_UUID = "00000000-0000-0000-0000-000000000000";
|
|
6
|
+
|
|
7
|
+
const SCHEMA = `
|
|
8
|
+
CREATE TABLE IF NOT EXISTS nodes (
|
|
9
|
+
uuid TEXT PRIMARY KEY,
|
|
10
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
11
|
+
last_accessed_at TEXT
|
|
12
|
+
);
|
|
13
|
+
|
|
14
|
+
CREATE TABLE IF NOT EXISTS memories (
|
|
15
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
16
|
+
node_uuid TEXT NOT NULL REFERENCES nodes(uuid),
|
|
17
|
+
content TEXT NOT NULL,
|
|
18
|
+
deprecated INTEGER NOT NULL DEFAULT 0,
|
|
19
|
+
migrated_to INTEGER REFERENCES memories(id),
|
|
20
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
CREATE TABLE IF NOT EXISTS edges (
|
|
24
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
25
|
+
parent_uuid TEXT NOT NULL REFERENCES nodes(uuid),
|
|
26
|
+
child_uuid TEXT NOT NULL REFERENCES nodes(uuid),
|
|
27
|
+
name TEXT NOT NULL,
|
|
28
|
+
priority INTEGER NOT NULL DEFAULT 0,
|
|
29
|
+
disclosure TEXT
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
CREATE TABLE IF NOT EXISTS paths (
|
|
33
|
+
namespace TEXT NOT NULL DEFAULT '',
|
|
34
|
+
domain TEXT NOT NULL DEFAULT 'core',
|
|
35
|
+
path TEXT NOT NULL,
|
|
36
|
+
edge_id INTEGER NOT NULL REFERENCES edges(id),
|
|
37
|
+
node_uuid TEXT NOT NULL REFERENCES nodes(uuid),
|
|
38
|
+
PRIMARY KEY (namespace, domain, path)
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
CREATE TABLE IF NOT EXISTS glossary_keywords (
|
|
42
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
43
|
+
keyword TEXT NOT NULL,
|
|
44
|
+
node_uuid TEXT NOT NULL REFERENCES nodes(uuid),
|
|
45
|
+
namespace TEXT NOT NULL DEFAULT ''
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
CREATE TABLE IF NOT EXISTS search_documents (
|
|
49
|
+
node_uuid TEXT NOT NULL,
|
|
50
|
+
namespace TEXT NOT NULL DEFAULT '',
|
|
51
|
+
content TEXT NOT NULL,
|
|
52
|
+
PRIMARY KEY (node_uuid, namespace)
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
CREATE TABLE IF NOT EXISTS memory_access_log (
|
|
56
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
57
|
+
node_uuid TEXT NOT NULL REFERENCES nodes(uuid),
|
|
58
|
+
namespace TEXT NOT NULL DEFAULT '',
|
|
59
|
+
context TEXT,
|
|
60
|
+
accessed_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
CREATE INDEX IF NOT EXISTS idx_memories_node ON memories(node_uuid, deprecated);
|
|
64
|
+
CREATE INDEX IF NOT EXISTS idx_memories_migrated ON memories(migrated_to);
|
|
65
|
+
CREATE INDEX IF NOT EXISTS idx_edges_parent ON edges(parent_uuid);
|
|
66
|
+
CREATE INDEX IF NOT EXISTS idx_edges_child ON edges(child_uuid);
|
|
67
|
+
CREATE INDEX IF NOT EXISTS idx_paths_node ON paths(node_uuid);
|
|
68
|
+
CREATE INDEX IF NOT EXISTS idx_paths_edge ON paths(edge_id);
|
|
69
|
+
CREATE INDEX IF NOT EXISTS idx_glossary_node ON glossary_keywords(node_uuid);
|
|
70
|
+
CREATE INDEX IF NOT EXISTS idx_glossary_keyword ON glossary_keywords(keyword);
|
|
71
|
+
`;
|
|
72
|
+
|
|
73
|
+
export function openDatabase(dbPath) {
|
|
74
|
+
mkdirSync(dirname(dbPath), { recursive: true });
|
|
75
|
+
const db = new DatabaseSync(dbPath);
|
|
76
|
+
db.exec("PRAGMA journal_mode = WAL");
|
|
77
|
+
db.exec("PRAGMA foreign_keys = ON");
|
|
78
|
+
db.exec(SCHEMA);
|
|
79
|
+
ensureRootNode(db);
|
|
80
|
+
return db;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function ensureRootNode(db) {
|
|
84
|
+
const row = db.prepare("SELECT uuid FROM nodes WHERE uuid = ?").get(ROOT_NODE_UUID);
|
|
85
|
+
if (!row) {
|
|
86
|
+
db.prepare("INSERT INTO nodes (uuid) VALUES (?)").run(ROOT_NODE_UUID);
|
|
87
|
+
db.prepare("INSERT INTO memories (node_uuid, content, deprecated) VALUES (?, ?, 0)").run(ROOT_NODE_UUID, "Root node");
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ── Node operations ────────────────────────────────────────────────────
|
|
92
|
+
|
|
93
|
+
export function createNode(db, uuid) {
|
|
94
|
+
db.prepare("INSERT OR IGNORE INTO nodes (uuid) VALUES (?)").run(uuid);
|
|
95
|
+
return { uuid };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function getNode(db, uuid) {
|
|
99
|
+
return db.prepare("SELECT * FROM nodes WHERE uuid = ?").get(uuid);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function touchNode(db, uuid) {
|
|
103
|
+
db.prepare("UPDATE nodes SET last_accessed_at = datetime('now') WHERE uuid = ?").run(uuid);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// ── Memory operations ───────────────────────────────────────────────────
|
|
107
|
+
|
|
108
|
+
export function createMemory(db, nodeUuid, content) {
|
|
109
|
+
const result = db.prepare("INSERT INTO memories (node_uuid, content) VALUES (?, ?)").run(nodeUuid, content);
|
|
110
|
+
touchNode(db, nodeUuid);
|
|
111
|
+
return { id: Number(result.lastInsertRowid), node_uuid: nodeUuid, content };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function getCurrentMemory(db, nodeUuid) {
|
|
115
|
+
return db.prepare("SELECT * FROM memories WHERE node_uuid = ? AND deprecated = 0 ORDER BY id DESC LIMIT 1").get(nodeUuid);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function getMemoryHistory(db, nodeUuid) {
|
|
119
|
+
return db.prepare("SELECT * FROM memories WHERE node_uuid = ? ORDER BY id DESC").all(nodeUuid);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function updateMemory(db, nodeUuid, newContent) {
|
|
123
|
+
const current = getCurrentMemory(db, nodeUuid);
|
|
124
|
+
if (!current) return null;
|
|
125
|
+
db.prepare("UPDATE memories SET deprecated = 1 WHERE id = ?").run(current.id);
|
|
126
|
+
const result = db.prepare("INSERT INTO memories (node_uuid, content) VALUES (?, ?)").run(nodeUuid, newContent);
|
|
127
|
+
db.prepare("UPDATE memories SET migrated_to = ? WHERE id = ?").run(Number(result.lastInsertRowid), current.id);
|
|
128
|
+
touchNode(db, nodeUuid);
|
|
129
|
+
return { id: Number(result.lastInsertRowid), node_uuid: nodeUuid, content: newContent, previous_id: current.id };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function deleteMemory(db, nodeUuid) {
|
|
133
|
+
const pathRows = db.prepare("SELECT edge_id FROM paths WHERE node_uuid = ?").all(nodeUuid);
|
|
134
|
+
db.prepare("DELETE FROM paths WHERE node_uuid = ?").run(nodeUuid);
|
|
135
|
+
for (const row of pathRows) {
|
|
136
|
+
try { db.prepare("DELETE FROM edges WHERE id = ?").run(row.edge_id); } catch {}
|
|
137
|
+
}
|
|
138
|
+
db.prepare("DELETE FROM edges WHERE parent_uuid = ? OR child_uuid = ?").run(nodeUuid, nodeUuid);
|
|
139
|
+
db.prepare("DELETE FROM glossary_keywords WHERE node_uuid = ?").run(nodeUuid);
|
|
140
|
+
db.prepare("DELETE FROM search_documents WHERE node_uuid = ?").run(nodeUuid);
|
|
141
|
+
db.prepare("DELETE FROM memory_access_log WHERE node_uuid = ?").run(nodeUuid);
|
|
142
|
+
db.prepare("DELETE FROM memories WHERE node_uuid = ?").run(nodeUuid);
|
|
143
|
+
const result = db.prepare("DELETE FROM nodes WHERE uuid = ? AND uuid != ?").run(nodeUuid, ROOT_NODE_UUID);
|
|
144
|
+
return result.changes > 0;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// ── Edge operations ─────────────────────────────────────────────────────
|
|
148
|
+
|
|
149
|
+
export function createEdge(db, parentUuid, childUuid, { name = "related", priority = 0, disclosure = null } = {}) {
|
|
150
|
+
const result = db.prepare(
|
|
151
|
+
"INSERT INTO edges (parent_uuid, child_uuid, name, priority, disclosure) VALUES (?, ?, ?, ?, ?)",
|
|
152
|
+
).run(parentUuid, childUuid, name, priority, disclosure);
|
|
153
|
+
return { id: Number(result.lastInsertRowid), parent_uuid: parentUuid, child_uuid: childUuid, name };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export function getEdges(db, nodeUuid, { direction = "both" } = {}) {
|
|
157
|
+
if (direction === "children") return db.prepare("SELECT * FROM edges WHERE parent_uuid = ? ORDER BY priority DESC").all(nodeUuid);
|
|
158
|
+
if (direction === "parents") return db.prepare("SELECT * FROM edges WHERE child_uuid = ? ORDER BY priority DESC").all(nodeUuid);
|
|
159
|
+
return db.prepare("SELECT * FROM edges WHERE parent_uuid = ? OR child_uuid = ? ORDER BY priority DESC").all(nodeUuid, nodeUuid);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export function deleteEdge(db, edgeId) {
|
|
163
|
+
db.prepare("DELETE FROM paths WHERE edge_id = ?").run(edgeId);
|
|
164
|
+
return db.prepare("DELETE FROM edges WHERE id = ?").run(edgeId).changes > 0;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// ── Path operations ─────────────────────────────────────────────────────
|
|
168
|
+
|
|
169
|
+
export function createPath(db, { namespace = "", domain = "core", path, edgeId, nodeUuid }) {
|
|
170
|
+
db.prepare("INSERT OR REPLACE INTO paths (namespace, domain, path, edge_id, node_uuid) VALUES (?, ?, ?, ?, ?)").run(namespace, domain, path, edgeId, nodeUuid);
|
|
171
|
+
return { namespace, domain, path, edge_id: edgeId, node_uuid: nodeUuid };
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export function getPath(db, namespace, domain, path) {
|
|
175
|
+
return db.prepare("SELECT * FROM paths WHERE namespace = ? AND domain = ? AND path = ?").get(namespace, domain, path);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export function getPathsForNode(db, nodeUuid) {
|
|
179
|
+
return db.prepare("SELECT * FROM paths WHERE node_uuid = ?").all(nodeUuid);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export function listPaths(db, namespace = "", domain = "core", parentPath = "") {
|
|
183
|
+
const prefix = parentPath ? `${parentPath}/` : "";
|
|
184
|
+
return db.prepare("SELECT * FROM paths WHERE namespace = ? AND domain = ? AND path LIKE ? ORDER BY path").all(namespace, domain, `${prefix}%`);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// ── Glossary operations ─────────────────────────────────────────────────
|
|
188
|
+
|
|
189
|
+
export function addGlossaryKeyword(db, keyword, nodeUuid, namespace = "") {
|
|
190
|
+
db.prepare("INSERT OR IGNORE INTO glossary_keywords (keyword, node_uuid, namespace) VALUES (?, ?, ?)").run(keyword, nodeUuid, namespace);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export function findNodeByKeyword(db, keyword, namespace = "") {
|
|
194
|
+
return db.prepare("SELECT * FROM glossary_keywords WHERE keyword = ? AND (namespace = ? OR namespace = '')").get(keyword, namespace);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export function getGlossaryKeywords(db, namespace = "") {
|
|
198
|
+
return db.prepare("SELECT * FROM glossary_keywords WHERE namespace = ? OR namespace = '' ORDER BY keyword").all(namespace);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// ── Search operations ───────────────────────────────────────────────────
|
|
202
|
+
|
|
203
|
+
export function indexSearchDocument(db, nodeUuid, content, namespace = "") {
|
|
204
|
+
db.prepare("INSERT OR REPLACE INTO search_documents (node_uuid, namespace, content) VALUES (?, ?, ?)").run(nodeUuid, namespace, content);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export function searchByContent(db, query, namespace = "") {
|
|
208
|
+
return db.prepare("SELECT * FROM search_documents WHERE (namespace = ? OR namespace = '') AND content LIKE ?").all(namespace, `%${query}%`);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// ── Access log ──────────────────────────────────────────────────────────
|
|
212
|
+
|
|
213
|
+
export function logAccess(db, nodeUuid, context = null, namespace = "") {
|
|
214
|
+
db.prepare("INSERT INTO memory_access_log (node_uuid, namespace, context) VALUES (?, ?, ?)").run(nodeUuid, namespace, context);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export function getRecentAccesses(db, limit = 20) {
|
|
218
|
+
return db.prepare("SELECT * FROM memory_access_log ORDER BY accessed_at DESC LIMIT ?").all(limit);
|
|
219
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
export class GlossaryService {
|
|
2
|
+
constructor(db, namespace = "") {
|
|
3
|
+
this.db = db;
|
|
4
|
+
this.namespace = namespace;
|
|
5
|
+
this.fingerprint = null;
|
|
6
|
+
this.automaton = null;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
// ── Aho-Corasick automaton ────────────────────────────────────────
|
|
10
|
+
|
|
11
|
+
#ensureAutomaton() {
|
|
12
|
+
const fp = this.#computeFingerprint();
|
|
13
|
+
if (fp === this.fingerprint && this.automaton) return;
|
|
14
|
+
this.fingerprint = fp;
|
|
15
|
+
this.automaton = this.#buildAutomaton();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
#computeFingerprint() {
|
|
19
|
+
const row = this.db.prepare(
|
|
20
|
+
"SELECT COUNT(*) AS cnt, MAX(id) AS max_id FROM glossary_keywords WHERE namespace = ? OR namespace = 'global'"
|
|
21
|
+
).get(this.namespace);
|
|
22
|
+
return `${row.cnt}:${row.max_id}`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
#buildAutomaton() {
|
|
26
|
+
const keywords = this.db.prepare(
|
|
27
|
+
"SELECT id, keyword, node_uuid FROM glossary_keywords WHERE namespace = ? OR namespace = 'global'"
|
|
28
|
+
).all(this.namespace);
|
|
29
|
+
|
|
30
|
+
// Build trie
|
|
31
|
+
const go = [new Map()];
|
|
32
|
+
const fail = [0];
|
|
33
|
+
const output = [new Map()];
|
|
34
|
+
|
|
35
|
+
for (const kw of keywords) {
|
|
36
|
+
let state = 0;
|
|
37
|
+
for (const ch of kw.keyword) {
|
|
38
|
+
let next = go[state].get(ch);
|
|
39
|
+
if (next === undefined) {
|
|
40
|
+
next = go.length;
|
|
41
|
+
go.push(new Map());
|
|
42
|
+
fail.push(0);
|
|
43
|
+
output.push(new Map());
|
|
44
|
+
go[state].set(ch, next);
|
|
45
|
+
}
|
|
46
|
+
state = next;
|
|
47
|
+
}
|
|
48
|
+
output[state].set(kw.id, kw.node_uuid);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Build failure links (BFS)
|
|
52
|
+
const queue = [];
|
|
53
|
+
for (const [ch, next] of go[0]) {
|
|
54
|
+
fail[next] = 0;
|
|
55
|
+
queue.push(next);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
while (queue.length > 0) {
|
|
59
|
+
const r = queue.shift();
|
|
60
|
+
for (const [ch, s] of go[r]) {
|
|
61
|
+
queue.push(s);
|
|
62
|
+
let f = fail[r];
|
|
63
|
+
while (f > 0 && !go[f].has(ch)) f = fail[f];
|
|
64
|
+
fail[s] = go[f].has(ch) ? go[f].get(ch) : 0;
|
|
65
|
+
for (const [kwId, nodeUuid] of output[fail[s]]) {
|
|
66
|
+
output[s].set(kwId, nodeUuid);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return { go, fail, output };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// ── Public API ─────────────────────────────────────────────────────
|
|
75
|
+
|
|
76
|
+
addKeyword(keyword, nodeUuid, namespace = "") {
|
|
77
|
+
this.db.prepare(
|
|
78
|
+
"INSERT OR IGNORE INTO glossary_keywords (keyword, node_uuid, namespace) VALUES (?, ?, ?)"
|
|
79
|
+
).run(keyword, nodeUuid, namespace);
|
|
80
|
+
this.fingerprint = null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
removeKeyword(keywordId) {
|
|
84
|
+
this.db.prepare("DELETE FROM glossary_keywords WHERE id = ?").run(keywordId);
|
|
85
|
+
this.fingerprint = null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
findInContent(content) {
|
|
89
|
+
if (!content) return [];
|
|
90
|
+
this.#ensureAutomaton();
|
|
91
|
+
if (!this.automaton) return [];
|
|
92
|
+
|
|
93
|
+
const { go, fail, output } = this.automaton;
|
|
94
|
+
const matches = [];
|
|
95
|
+
const seen = new Set();
|
|
96
|
+
let state = 0;
|
|
97
|
+
|
|
98
|
+
for (let i = 0; i < content.length; i++) {
|
|
99
|
+
const ch = content[i];
|
|
100
|
+
while (state > 0 && !go[state].has(ch)) state = fail[state];
|
|
101
|
+
state = go[state].has(ch) ? go[state].get(ch) : 0;
|
|
102
|
+
|
|
103
|
+
for (const [kwId, nodeUuid] of output[state]) {
|
|
104
|
+
if (seen.has(kwId)) continue;
|
|
105
|
+
seen.add(kwId);
|
|
106
|
+
matches.push({ keyword_id: kwId, node_uuid: nodeUuid });
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return matches;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
getAllKeywords() {
|
|
114
|
+
return this.db.prepare(
|
|
115
|
+
"SELECT * FROM glossary_keywords WHERE namespace = ? OR namespace = 'global' ORDER BY keyword"
|
|
116
|
+
).all(this.namespace);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
getKeywordsForNode(nodeUuid) {
|
|
120
|
+
return this.db.prepare(
|
|
121
|
+
"SELECT * FROM glossary_keywords WHERE node_uuid = ? AND (namespace = ? OR namespace = 'global')"
|
|
122
|
+
).all(nodeUuid, this.namespace);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { ROOT_NODE_UUID } from "../database.mjs";
|
|
2
|
+
import { escapeLikePath } from "./graph-path-utils.mjs";
|
|
3
|
+
import {
|
|
4
|
+
countPathsForEdge,
|
|
5
|
+
insertPath,
|
|
6
|
+
} from "./graph-primitives.mjs";
|
|
7
|
+
|
|
8
|
+
export function deprecateNodeMemories(db, nodeUuid, successorId = null) {
|
|
9
|
+
const conditions = ["node_uuid = ?", "deprecated = 0"];
|
|
10
|
+
const params = [nodeUuid];
|
|
11
|
+
if (successorId !== null) {
|
|
12
|
+
conditions.push("id != ?");
|
|
13
|
+
params.push(successorId);
|
|
14
|
+
}
|
|
15
|
+
const ids = db.prepare(
|
|
16
|
+
`SELECT id FROM memories WHERE ${conditions.join(" AND ")}`
|
|
17
|
+
).all(...params).map(r => r.id);
|
|
18
|
+
|
|
19
|
+
if (ids.length > 0) {
|
|
20
|
+
const placeholders = ids.map(() => "?").join(",");
|
|
21
|
+
db.prepare(
|
|
22
|
+
`UPDATE memories SET deprecated = 1, migrated_to = ? WHERE id IN (${placeholders})`
|
|
23
|
+
).run(successorId, ...ids);
|
|
24
|
+
}
|
|
25
|
+
return ids;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function cascadeCreatePaths(db, nodeUuid, domain, basePath, namespace = "", visited = new Set()) {
|
|
29
|
+
if (visited.has(nodeUuid)) return;
|
|
30
|
+
visited.add(nodeUuid);
|
|
31
|
+
try {
|
|
32
|
+
const childEdges = db.prepare(
|
|
33
|
+
"SELECT * FROM edges WHERE parent_uuid = ?"
|
|
34
|
+
).all(nodeUuid);
|
|
35
|
+
for (const edge of childEdges) {
|
|
36
|
+
const childPath = `${basePath}/${edge.name}`;
|
|
37
|
+
insertPath(db, namespace, domain, childPath, edge.id, edge.child_uuid);
|
|
38
|
+
cascadeCreatePaths(db, edge.child_uuid, domain, childPath, namespace, visited);
|
|
39
|
+
}
|
|
40
|
+
} finally {
|
|
41
|
+
visited.delete(nodeUuid);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function deleteSubtreePaths(db, domain, path, namespace = "") {
|
|
46
|
+
const safe = escapeLikePath(path);
|
|
47
|
+
const rows = db.prepare(`
|
|
48
|
+
SELECT namespace, domain, path, edge_id, node_uuid
|
|
49
|
+
FROM paths
|
|
50
|
+
WHERE namespace = ? AND domain = ? AND (path = ? OR path LIKE ? ESCAPE '\\')
|
|
51
|
+
`).all(namespace, domain, path, `${safe}/%`);
|
|
52
|
+
|
|
53
|
+
for (const row of rows) {
|
|
54
|
+
db.prepare(
|
|
55
|
+
"DELETE FROM paths WHERE namespace = ? AND domain = ? AND path = ?"
|
|
56
|
+
).run(row.namespace, row.domain, row.path);
|
|
57
|
+
}
|
|
58
|
+
return rows;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function cascadeDeleteEdge(db, edge) {
|
|
62
|
+
const edgePaths = db.prepare(
|
|
63
|
+
"SELECT * FROM paths WHERE edge_id = ?"
|
|
64
|
+
).all(edge.id);
|
|
65
|
+
|
|
66
|
+
for (const path of edgePaths) {
|
|
67
|
+
deleteSubtreePaths(db, path.domain, path.path, path.namespace);
|
|
68
|
+
}
|
|
69
|
+
db.prepare("DELETE FROM edges WHERE id = ?").run(edge.id);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function cascadeDeleteNode(db, nodeUuid) {
|
|
73
|
+
if (nodeUuid === ROOT_NODE_UUID) return null;
|
|
74
|
+
|
|
75
|
+
const edges = db.prepare(
|
|
76
|
+
"SELECT * FROM edges WHERE parent_uuid = ? OR child_uuid = ?"
|
|
77
|
+
).all(nodeUuid, nodeUuid);
|
|
78
|
+
for (const edge of edges) {
|
|
79
|
+
cascadeDeleteEdge(db, edge);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
db.prepare("DELETE FROM memories WHERE node_uuid = ?").run(nodeUuid);
|
|
83
|
+
db.prepare("DELETE FROM glossary_keywords WHERE node_uuid = ?").run(nodeUuid);
|
|
84
|
+
db.prepare("DELETE FROM nodes WHERE uuid = ?").run(nodeUuid);
|
|
85
|
+
return { deleted: nodeUuid };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function gcEdgeIfPathless(db, edge) {
|
|
89
|
+
if (countPathsForEdge(db, edge.id) > 0) return null;
|
|
90
|
+
db.prepare("DELETE FROM edges WHERE id = ?").run(edge.id);
|
|
91
|
+
return { edge_id: edge.id, parent_uuid: edge.parent_uuid, child_uuid: edge.child_uuid };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function gcNodeSoft(db, nodeUuid) {
|
|
95
|
+
if (nodeUuid === ROOT_NODE_UUID) return;
|
|
96
|
+
|
|
97
|
+
const row = db.prepare(
|
|
98
|
+
"SELECT COUNT(*) AS cnt FROM paths WHERE node_uuid = ?"
|
|
99
|
+
).get(nodeUuid);
|
|
100
|
+
if (row.cnt > 0) return;
|
|
101
|
+
|
|
102
|
+
const incoming = db.prepare("SELECT * FROM edges WHERE child_uuid = ?").all(nodeUuid);
|
|
103
|
+
for (const edge of incoming) gcEdgeIfPathless(db, edge);
|
|
104
|
+
|
|
105
|
+
const outgoing = db.prepare("SELECT * FROM edges WHERE parent_uuid = ?").all(nodeUuid);
|
|
106
|
+
for (const edge of outgoing) cascadeDeleteEdge(db, edge);
|
|
107
|
+
|
|
108
|
+
deprecateNodeMemories(db, nodeUuid);
|
|
109
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { ROOT_NODE_UUID } from "../database.mjs";
|
|
2
|
+
|
|
3
|
+
export function getGraphDiagnostics(db, namespace = "", daysStale = 30, maxChildren = 10) {
|
|
4
|
+
const cutoff = new Date(Date.now() - daysStale * 86400000).toISOString();
|
|
5
|
+
|
|
6
|
+
const allRows = db.prepare(`
|
|
7
|
+
SELECT p.domain, p.path, p.node_uuid, n.created_at, n.last_accessed_at,
|
|
8
|
+
e.priority, m.id AS memory_id
|
|
9
|
+
FROM paths p
|
|
10
|
+
JOIN edges e ON p.edge_id = e.id
|
|
11
|
+
JOIN nodes n ON n.uuid = e.child_uuid
|
|
12
|
+
JOIN memories m ON m.node_uuid = n.uuid AND m.deprecated = 0
|
|
13
|
+
WHERE p.namespace = ?
|
|
14
|
+
`).all(namespace);
|
|
15
|
+
|
|
16
|
+
const staleNodes = {};
|
|
17
|
+
for (const row of allRows) {
|
|
18
|
+
const effective = row.last_accessed_at ?? row.created_at ?? cutoff;
|
|
19
|
+
if (effective >= cutoff) continue;
|
|
20
|
+
const staleDays = Math.round((Date.now() - new Date(effective).getTime()) / 86400000);
|
|
21
|
+
const key = row.node_uuid;
|
|
22
|
+
if (!staleNodes[key] || (row.priority ?? 999) < (staleNodes[key].priority ?? 999)) {
|
|
23
|
+
staleNodes[key] = {
|
|
24
|
+
uuid: row.node_uuid,
|
|
25
|
+
uri: `${row.domain}://${row.path}`,
|
|
26
|
+
created_at: row.created_at,
|
|
27
|
+
last_accessed_at: row.last_accessed_at,
|
|
28
|
+
stale_days: staleDays,
|
|
29
|
+
priority: row.priority,
|
|
30
|
+
title: row.path.split("/").pop(),
|
|
31
|
+
memory_id: row.memory_id,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const crowdedRows = db.prepare(`
|
|
37
|
+
SELECT e.parent_uuid, COUNT(DISTINCT e.child_uuid) AS child_count
|
|
38
|
+
FROM edges e
|
|
39
|
+
JOIN paths p ON p.edge_id = e.id
|
|
40
|
+
WHERE p.namespace = ?
|
|
41
|
+
GROUP BY e.parent_uuid
|
|
42
|
+
HAVING child_count > ?
|
|
43
|
+
`).all(namespace, maxChildren);
|
|
44
|
+
|
|
45
|
+
const crowdedParents = {};
|
|
46
|
+
for (const row of crowdedRows) {
|
|
47
|
+
if (row.parent_uuid === ROOT_NODE_UUID) {
|
|
48
|
+
crowdedParents[row.parent_uuid] = {
|
|
49
|
+
uuid: row.parent_uuid,
|
|
50
|
+
uri: "core://",
|
|
51
|
+
title: "(root)",
|
|
52
|
+
child_count: row.child_count,
|
|
53
|
+
};
|
|
54
|
+
} else {
|
|
55
|
+
const p = db.prepare(
|
|
56
|
+
"SELECT domain, path FROM paths WHERE node_uuid = ? AND namespace = ? LIMIT 1"
|
|
57
|
+
).get(row.parent_uuid, namespace);
|
|
58
|
+
if (p) {
|
|
59
|
+
crowdedParents[row.parent_uuid] = {
|
|
60
|
+
uuid: row.parent_uuid,
|
|
61
|
+
uri: `${p.domain}://${p.path}`,
|
|
62
|
+
title: p.path.split("/").pop(),
|
|
63
|
+
child_count: row.child_count,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
stale_nodes: Object.values(staleNodes).sort((a, b) => (a.last_accessed_at ?? a.created_at ?? "").localeCompare(b.last_accessed_at ?? b.created_at ?? "")),
|
|
71
|
+
crowded_nodes: Object.values(crowdedParents).sort((a, b) => b.child_count - a.child_count),
|
|
72
|
+
};
|
|
73
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { deleteSubtreePaths, gcEdgeIfPathless, gcNodeSoft } from "./graph-cascades.mjs";
|
|
2
|
+
import { escapeLikePath, graphUri } from "./graph-path-utils.mjs";
|
|
3
|
+
import { resolveGraphPath } from "./graph-primitives.mjs";
|
|
4
|
+
|
|
5
|
+
export function removeGraphPath(db, path, domain = "core", namespace = "") {
|
|
6
|
+
if (path === "") throw new Error("Cannot remove root path.");
|
|
7
|
+
|
|
8
|
+
const target = resolveGraphPath(db, path, domain, namespace);
|
|
9
|
+
if (!target) throw new Error(`Path '${graphUri(domain, path)}' not found`);
|
|
10
|
+
|
|
11
|
+
const targetNodeUuid = target.node_uuid;
|
|
12
|
+
const targetEdge = target.edge;
|
|
13
|
+
if (!targetEdge) throw new Error(`Path '${domain}://${path}' has no edge.`);
|
|
14
|
+
|
|
15
|
+
const wouldOrphan = findWouldOrphanChildren(db, { path, domain, namespace, targetNodeUuid });
|
|
16
|
+
if (wouldOrphan.length > 0) {
|
|
17
|
+
const details = wouldOrphan.map(e => `'${e.name}' (${e.child_uuid.slice(0, 8)}...)`).join(", ");
|
|
18
|
+
throw new Error(`Cannot remove '${graphUri(domain, path)}': children would become unreachable: ${details}`);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
deleteSubtreePaths(db, domain, path, namespace);
|
|
22
|
+
gcEdgeIfPathless(db, targetEdge);
|
|
23
|
+
gcNodeSoft(db, targetNodeUuid);
|
|
24
|
+
return { deleted: graphUri(domain, path) };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function findWouldOrphanChildren(db, { path, domain, namespace, targetNodeUuid }) {
|
|
28
|
+
const childEdges = db.prepare("SELECT * FROM edges WHERE parent_uuid = ?").all(targetNodeUuid);
|
|
29
|
+
const wouldOrphan = [];
|
|
30
|
+
const safe = escapeLikePath(path);
|
|
31
|
+
|
|
32
|
+
for (const childEdge of childEdges) {
|
|
33
|
+
const surviving = db.prepare(`
|
|
34
|
+
SELECT COUNT(*) AS cnt FROM paths
|
|
35
|
+
WHERE node_uuid = ?
|
|
36
|
+
AND NOT (domain = ? AND (path = ? OR path LIKE ? ESCAPE '\\'))
|
|
37
|
+
`).get(childEdge.child_uuid, domain, path, `${safe}/%`);
|
|
38
|
+
if (surviving.cnt > 0) continue;
|
|
39
|
+
|
|
40
|
+
const targetSurviving = db.prepare(`
|
|
41
|
+
SELECT * FROM paths
|
|
42
|
+
WHERE node_uuid = ? AND namespace = ?
|
|
43
|
+
AND NOT (domain = ? AND (path = ? OR path LIKE ? ESCAPE '\\'))
|
|
44
|
+
ORDER BY CASE WHEN domain = ? THEN 0 ELSE 1 END, path
|
|
45
|
+
LIMIT 1
|
|
46
|
+
`).get(targetNodeUuid, namespace, domain, path, `${safe}/%`, domain);
|
|
47
|
+
if (!targetSurviving) wouldOrphan.push(childEdge);
|
|
48
|
+
}
|
|
49
|
+
return wouldOrphan;
|
|
50
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export function escapeLikePath(path) {
|
|
2
|
+
return String(path).replace(/[%_]/g, "\\$&");
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export function graphUri(domain, path) {
|
|
6
|
+
return `${domain}://${path}`;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function leafName(path) {
|
|
10
|
+
return String(path).split("/").pop();
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function pathExists(db, namespace, domain, path) {
|
|
14
|
+
return Boolean(db.prepare(
|
|
15
|
+
"SELECT 1 FROM paths WHERE namespace = ? AND domain = ? AND path = ?"
|
|
16
|
+
).get(namespace, domain, path));
|
|
17
|
+
}
|