@openvulcan/dsh-vulcancode 0.0.0-stage → 0.4.3
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/LICENSE +21 -0
- package/README.md +126 -2
- package/assets/openvulcan-icon.svg +33 -0
- package/cordis.patch.yml +19 -0
- package/node_modules/@openvulcan/dsh-decision-guard/LICENSE +21 -0
- package/node_modules/@openvulcan/dsh-decision-guard/README.md +14 -0
- package/node_modules/@openvulcan/dsh-decision-guard/assets/decision-guard-icon.png +0 -0
- package/node_modules/@openvulcan/dsh-decision-guard/lib/calls.d.ts +40 -0
- package/node_modules/@openvulcan/dsh-decision-guard/lib/calls.js +150 -0
- package/node_modules/@openvulcan/dsh-decision-guard/lib/client.js +2250 -0
- package/node_modules/@openvulcan/dsh-decision-guard/lib/decide.d.ts +90 -0
- package/node_modules/@openvulcan/dsh-decision-guard/lib/decide.js +108 -0
- package/node_modules/@openvulcan/dsh-decision-guard/lib/index.d.ts +129 -0
- package/node_modules/@openvulcan/dsh-decision-guard/lib/index.js +572 -0
- package/node_modules/@openvulcan/dsh-decision-guard/lib/intent.d.ts +22 -0
- package/node_modules/@openvulcan/dsh-decision-guard/lib/intent.js +77 -0
- package/node_modules/@openvulcan/dsh-decision-guard/lib/panel.d.ts +158 -0
- package/node_modules/@openvulcan/dsh-decision-guard/lib/panel.js +340 -0
- package/node_modules/@openvulcan/dsh-decision-guard/lib/records.d.ts +133 -0
- package/node_modules/@openvulcan/dsh-decision-guard/lib/records.js +186 -0
- package/node_modules/@openvulcan/dsh-decision-guard/lib/review.d.ts +55 -0
- package/node_modules/@openvulcan/dsh-decision-guard/lib/review.js +202 -0
- package/node_modules/@openvulcan/dsh-decision-guard/lib/session-state.d.ts +62 -0
- package/node_modules/@openvulcan/dsh-decision-guard/lib/session-state.js +155 -0
- package/node_modules/@openvulcan/dsh-decision-guard/lib/version.d.ts +2 -0
- package/node_modules/@openvulcan/dsh-decision-guard/lib/version.js +4 -0
- package/node_modules/@openvulcan/dsh-decision-guard/package.json +68 -0
- package/node_modules/@openvulcan/dsh-vulcan-account/LICENSE +21 -0
- package/node_modules/@openvulcan/dsh-vulcan-account/assets/account-icon.png +0 -0
- package/node_modules/@openvulcan/dsh-vulcan-account/lib/client.js +573 -0
- package/node_modules/@openvulcan/dsh-vulcan-account/lib/index.d.ts +92 -0
- package/node_modules/@openvulcan/dsh-vulcan-account/lib/index.js +194 -0
- package/node_modules/@openvulcan/dsh-vulcan-account/package.json +57 -0
- package/node_modules/@openvulcan/dsh-vulcan-answers/LICENSE +21 -0
- package/node_modules/@openvulcan/dsh-vulcan-answers/assets/answers-icon.png +0 -0
- package/node_modules/@openvulcan/dsh-vulcan-answers/assets/prompts/answer-mode-interactive.md +79 -0
- package/node_modules/@openvulcan/dsh-vulcan-answers/assets/prompts/answer-mode-standard.md +1 -0
- package/node_modules/@openvulcan/dsh-vulcan-answers/lib/answer-model.d.ts +258 -0
- package/node_modules/@openvulcan/dsh-vulcan-answers/lib/answer-model.js +698 -0
- package/node_modules/@openvulcan/dsh-vulcan-answers/lib/client.js +671 -0
- package/node_modules/@openvulcan/dsh-vulcan-answers/lib/fences.d.ts +29 -0
- package/node_modules/@openvulcan/dsh-vulcan-answers/lib/fences.js +70 -0
- package/node_modules/@openvulcan/dsh-vulcan-answers/lib/index.d.ts +19 -0
- package/node_modules/@openvulcan/dsh-vulcan-answers/lib/index.js +52 -0
- package/node_modules/@openvulcan/dsh-vulcan-answers/lib/limits.d.ts +9 -0
- package/node_modules/@openvulcan/dsh-vulcan-answers/lib/limits.js +9 -0
- package/node_modules/@openvulcan/dsh-vulcan-answers/lib/prompt.d.ts +22 -0
- package/node_modules/@openvulcan/dsh-vulcan-answers/lib/prompt.js +39 -0
- package/node_modules/@openvulcan/dsh-vulcan-answers/lib/reply-format.d.ts +20 -0
- package/node_modules/@openvulcan/dsh-vulcan-answers/lib/reply-format.js +17 -0
- package/node_modules/@openvulcan/dsh-vulcan-answers/lib/session-state.d.ts +66 -0
- package/node_modules/@openvulcan/dsh-vulcan-answers/lib/session-state.js +118 -0
- package/node_modules/@openvulcan/dsh-vulcan-answers/package.json +59 -0
- package/node_modules/@openvulcan/dsh-vulcan-luaskills/LICENSE +21 -0
- package/node_modules/@openvulcan/dsh-vulcan-luaskills/assets/luaskills-icon.svg +1 -0
- package/node_modules/@openvulcan/dsh-vulcan-luaskills/lib/client.js +949 -0
- package/node_modules/@openvulcan/dsh-vulcan-luaskills/lib/constants.d.ts +46 -0
- package/node_modules/@openvulcan/dsh-vulcan-luaskills/lib/constants.js +51 -0
- package/node_modules/@openvulcan/dsh-vulcan-luaskills/lib/engine.d.ts +55 -0
- package/node_modules/@openvulcan/dsh-vulcan-luaskills/lib/engine.js +149 -0
- package/node_modules/@openvulcan/dsh-vulcan-luaskills/lib/hub.d.ts +49 -0
- package/node_modules/@openvulcan/dsh-vulcan-luaskills/lib/hub.js +116 -0
- package/node_modules/@openvulcan/dsh-vulcan-luaskills/lib/index.d.ts +26 -0
- package/node_modules/@openvulcan/dsh-vulcan-luaskills/lib/index.js +276 -0
- package/node_modules/@openvulcan/dsh-vulcan-luaskills/lib/protocol.d.ts +65 -0
- package/node_modules/@openvulcan/dsh-vulcan-luaskills/lib/protocol.js +2 -0
- package/node_modules/@openvulcan/dsh-vulcan-luaskills/lib/runtime.d.ts +111 -0
- package/node_modules/@openvulcan/dsh-vulcan-luaskills/lib/runtime.js +281 -0
- package/node_modules/@openvulcan/dsh-vulcan-luaskills/lib/sdk-install.d.ts +26 -0
- package/node_modules/@openvulcan/dsh-vulcan-luaskills/lib/sdk-install.js +87 -0
- package/node_modules/@openvulcan/dsh-vulcan-luaskills/lib/settings.d.ts +79 -0
- package/node_modules/@openvulcan/dsh-vulcan-luaskills/lib/settings.js +165 -0
- package/node_modules/@openvulcan/dsh-vulcan-luaskills/lib/skills.d.ts +34 -0
- package/node_modules/@openvulcan/dsh-vulcan-luaskills/lib/skills.js +61 -0
- package/node_modules/@openvulcan/dsh-vulcan-luaskills/lib/tools.d.ts +59 -0
- package/node_modules/@openvulcan/dsh-vulcan-luaskills/lib/tools.js +107 -0
- package/node_modules/@openvulcan/dsh-vulcan-luaskills/lib/worker.d.ts +10 -0
- package/node_modules/@openvulcan/dsh-vulcan-luaskills/lib/worker.js +140 -0
- package/node_modules/@openvulcan/dsh-vulcan-luaskills/package.json +57 -0
- package/node_modules/@openvulcan/dsh-vulcan-memory/LICENSE +21 -0
- package/node_modules/@openvulcan/dsh-vulcan-memory/assets/markdown-memory-guide.md +21 -0
- package/node_modules/@openvulcan/dsh-vulcan-memory/assets/memory-curator/SKILL.md +31 -0
- package/node_modules/@openvulcan/dsh-vulcan-memory/assets/memory-icon.png +0 -0
- package/node_modules/@openvulcan/dsh-vulcan-memory/lib/client.js +1090 -0
- package/node_modules/@openvulcan/dsh-vulcan-memory/lib/index.d.ts +30 -0
- package/node_modules/@openvulcan/dsh-vulcan-memory/lib/index.js +240 -0
- package/node_modules/@openvulcan/dsh-vulcan-memory/lib/inject.d.ts +30 -0
- package/node_modules/@openvulcan/dsh-vulcan-memory/lib/inject.js +46 -0
- package/node_modules/@openvulcan/dsh-vulcan-memory/lib/markdown/constants.d.ts +23 -0
- package/node_modules/@openvulcan/dsh-vulcan-memory/lib/markdown/constants.js +25 -0
- package/node_modules/@openvulcan/dsh-vulcan-memory/lib/markdown/document.d.ts +108 -0
- package/node_modules/@openvulcan/dsh-vulcan-memory/lib/markdown/document.js +373 -0
- package/node_modules/@openvulcan/dsh-vulcan-memory/lib/markdown/errors.d.ts +14 -0
- package/node_modules/@openvulcan/dsh-vulcan-memory/lib/markdown/errors.js +31 -0
- package/node_modules/@openvulcan/dsh-vulcan-memory/lib/markdown/index.d.ts +196 -0
- package/node_modules/@openvulcan/dsh-vulcan-memory/lib/markdown/index.js +204 -0
- package/node_modules/@openvulcan/dsh-vulcan-memory/lib/markdown/operations.d.ts +92 -0
- package/node_modules/@openvulcan/dsh-vulcan-memory/lib/markdown/operations.js +472 -0
- package/node_modules/@openvulcan/dsh-vulcan-memory/lib/markdown/package.d.ts +126 -0
- package/node_modules/@openvulcan/dsh-vulcan-memory/lib/markdown/package.js +553 -0
- package/node_modules/@openvulcan/dsh-vulcan-memory/lib/markdown/registry.d.ts +56 -0
- package/node_modules/@openvulcan/dsh-vulcan-memory/lib/markdown/registry.js +182 -0
- package/node_modules/@openvulcan/dsh-vulcan-memory/lib/markdown/scope.d.ts +13 -0
- package/node_modules/@openvulcan/dsh-vulcan-memory/lib/markdown/scope.js +24 -0
- package/node_modules/@openvulcan/dsh-vulcan-memory/lib/markdown/search.d.ts +65 -0
- package/node_modules/@openvulcan/dsh-vulcan-memory/lib/markdown/search.js +227 -0
- package/node_modules/@openvulcan/dsh-vulcan-memory/lib/markdown/snapshot.d.ts +34 -0
- package/node_modules/@openvulcan/dsh-vulcan-memory/lib/markdown/snapshot.js +171 -0
- package/node_modules/@openvulcan/dsh-vulcan-memory/lib/markdown/store.d.ts +72 -0
- package/node_modules/@openvulcan/dsh-vulcan-memory/lib/markdown/store.js +250 -0
- package/node_modules/@openvulcan/dsh-vulcan-memory/lib/markdown/timestamp.d.ts +6 -0
- package/node_modules/@openvulcan/dsh-vulcan-memory/lib/markdown/timestamp.js +12 -0
- package/node_modules/@openvulcan/dsh-vulcan-memory/lib/markdown/tokens.d.ts +8 -0
- package/node_modules/@openvulcan/dsh-vulcan-memory/lib/markdown/tokens.js +38 -0
- package/node_modules/@openvulcan/dsh-vulcan-memory/lib/page.d.ts +132 -0
- package/node_modules/@openvulcan/dsh-vulcan-memory/lib/page.js +235 -0
- package/node_modules/@openvulcan/dsh-vulcan-memory/lib/project.d.ts +14 -0
- package/node_modules/@openvulcan/dsh-vulcan-memory/lib/project.js +27 -0
- package/node_modules/@openvulcan/dsh-vulcan-memory/lib/settings.d.ts +29 -0
- package/node_modules/@openvulcan/dsh-vulcan-memory/lib/settings.js +62 -0
- package/node_modules/@openvulcan/dsh-vulcan-memory/package.json +63 -0
- package/node_modules/@openvulcan/dsh-vulcan-reasoning/LICENSE +21 -0
- package/node_modules/@openvulcan/dsh-vulcan-reasoning/lib/client.js +509 -0
- package/node_modules/@openvulcan/dsh-vulcan-reasoning/lib/index.d.ts +6 -0
- package/node_modules/@openvulcan/dsh-vulcan-reasoning/lib/index.js +8 -0
- package/node_modules/@openvulcan/dsh-vulcan-reasoning/package.json +42 -0
- package/node_modules/@openvulcan/dsh-vulcan-style/LICENSE +21 -0
- package/node_modules/@openvulcan/dsh-vulcan-style/assets/style-icon.png +0 -0
- package/node_modules/@openvulcan/dsh-vulcan-style/lib/client.js +1121 -0
- package/node_modules/@openvulcan/dsh-vulcan-style/lib/index.d.ts +34 -0
- package/node_modules/@openvulcan/dsh-vulcan-style/lib/index.js +42 -0
- package/node_modules/@openvulcan/dsh-vulcan-style/package.json +51 -0
- package/node_modules/@openvulcan/openvulcan-account/LICENSE +21 -0
- package/node_modules/@openvulcan/openvulcan-account/lib/account.d.ts +140 -0
- package/node_modules/@openvulcan/openvulcan-account/lib/account.js +347 -0
- package/node_modules/@openvulcan/openvulcan-account/lib/index.d.ts +1 -0
- package/node_modules/@openvulcan/openvulcan-account/lib/index.js +1 -0
- package/node_modules/@openvulcan/openvulcan-account/package.json +29 -0
- package/node_modules/@openvulcan/vulcan-decision/LICENSE +21 -0
- package/node_modules/@openvulcan/vulcan-decision/README.md +23 -0
- package/node_modules/@openvulcan/vulcan-decision/bin/vulcan-decision.js +4 -0
- package/node_modules/@openvulcan/vulcan-decision/lib/cli.d.ts +5 -0
- package/node_modules/@openvulcan/vulcan-decision/lib/cli.js +267 -0
- package/node_modules/@openvulcan/vulcan-decision/lib/cloud/edge.d.ts +211 -0
- package/node_modules/@openvulcan/vulcan-decision/lib/cloud/edge.js +594 -0
- package/node_modules/@openvulcan/vulcan-decision/lib/download.d.ts +36 -0
- package/node_modules/@openvulcan/vulcan-decision/lib/download.js +99 -0
- package/node_modules/@openvulcan/vulcan-decision/lib/engine.d.ts +37 -0
- package/node_modules/@openvulcan/vulcan-decision/lib/engine.js +39 -0
- package/node_modules/@openvulcan/vulcan-decision/lib/hardware.d.ts +105 -0
- package/node_modules/@openvulcan/vulcan-decision/lib/hardware.js +207 -0
- package/node_modules/@openvulcan/vulcan-decision/lib/home.d.ts +26 -0
- package/node_modules/@openvulcan/vulcan-decision/lib/home.js +54 -0
- package/node_modules/@openvulcan/vulcan-decision/lib/index.d.ts +17 -0
- package/node_modules/@openvulcan/vulcan-decision/lib/index.js +17 -0
- package/node_modules/@openvulcan/vulcan-decision/lib/local/engine.d.ts +54 -0
- package/node_modules/@openvulcan/vulcan-decision/lib/local/engine.js +157 -0
- package/node_modules/@openvulcan/vulcan-decision/lib/local/models.d.ts +126 -0
- package/node_modules/@openvulcan/vulcan-decision/lib/local/models.js +261 -0
- package/node_modules/@openvulcan/vulcan-decision/lib/local/packs.d.ts +102 -0
- package/node_modules/@openvulcan/vulcan-decision/lib/local/packs.js +195 -0
- package/node_modules/@openvulcan/vulcan-decision/lib/local/worker.d.ts +61 -0
- package/node_modules/@openvulcan/vulcan-decision/lib/local/worker.js +123 -0
- package/node_modules/@openvulcan/vulcan-decision/lib/protocol.d.ts +74 -0
- package/node_modules/@openvulcan/vulcan-decision/lib/protocol.js +31 -0
- package/node_modules/@openvulcan/vulcan-decision/lib/render.d.ts +15 -0
- package/node_modules/@openvulcan/vulcan-decision/lib/render.js +33 -0
- package/node_modules/@openvulcan/vulcan-decision/lib/version.d.ts +2 -0
- package/node_modules/@openvulcan/vulcan-decision/lib/version.js +4 -0
- package/node_modules/@openvulcan/vulcan-decision/package.json +40 -0
- package/node_modules/@openvulcan/vulcan-decision/python/vulcan_decision_worker.py +363 -0
- package/node_modules/@openvulcan/vulcan-decision/runtime-packs/onnx-cpu.json +627 -0
- package/node_modules/@openvulcan/vulcan-decision/runtime-packs/onnx-cuda.json +423 -0
- package/node_modules/@openvulcan/vulcan-decision/runtime-packs/torch-cpu.json +1047 -0
- package/node_modules/@openvulcan/vulcan-decision/runtime-packs/torch-cu128.json +647 -0
- package/package.json +55 -4
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/** Memory projects and their bindings to host projects (`registry.rs`); stored as `markdown/registry.json`. */
|
|
2
|
+
import { randomBytes } from 'node:crypto';
|
|
3
|
+
import { MAX_TITLE_CHARS, REGISTRY_FORMAT_VERSION } from './constants.js';
|
|
4
|
+
import { MemoryError } from './errors.js';
|
|
5
|
+
export const REGISTRY_FILE = 'registry.json';
|
|
6
|
+
export class MemoryRegistry {
|
|
7
|
+
projects;
|
|
8
|
+
ignoredHostProjectIds;
|
|
9
|
+
constructor(projects = [],
|
|
10
|
+
/** Host projects whose orphan prompt the user dismissed. */
|
|
11
|
+
ignoredHostProjectIds = []) {
|
|
12
|
+
this.projects = projects;
|
|
13
|
+
this.ignoredHostProjectIds = ignoredHostProjectIds;
|
|
14
|
+
}
|
|
15
|
+
/** Parse registry file text; a format other than version 1 is refused. */
|
|
16
|
+
static parse(text, path) {
|
|
17
|
+
let value;
|
|
18
|
+
try {
|
|
19
|
+
value = JSON.parse(text);
|
|
20
|
+
}
|
|
21
|
+
catch (error) {
|
|
22
|
+
throw MemoryError.json(path, error instanceof Error ? error.message : String(error));
|
|
23
|
+
}
|
|
24
|
+
if (!isRecord(value) || value['version'] !== REGISTRY_FORMAT_VERSION) {
|
|
25
|
+
throw new MemoryError('unsupported_format', `Memory registry \`${path}\` must declare format version ${REGISTRY_FORMAT_VERSION}.`);
|
|
26
|
+
}
|
|
27
|
+
const projects = value['projects'];
|
|
28
|
+
const ignored = value['ignoredHostProjectIds'];
|
|
29
|
+
if (!Array.isArray(projects) || !Array.isArray(ignored) || !ignored.every(item => typeof item === 'string')) {
|
|
30
|
+
throw MemoryError.json(path, 'expected `projects` and `ignoredHostProjectIds` arrays');
|
|
31
|
+
}
|
|
32
|
+
return new MemoryRegistry(projects.map(project => parseProject(project, path)), ignored);
|
|
33
|
+
}
|
|
34
|
+
/** The versioned file text, without the final newline. */
|
|
35
|
+
toJson() {
|
|
36
|
+
return JSON.stringify({
|
|
37
|
+
version: REGISTRY_FORMAT_VERSION,
|
|
38
|
+
projects: this.projects.map(project => ({
|
|
39
|
+
id: project.id,
|
|
40
|
+
name: project.name,
|
|
41
|
+
hostProjectIds: project.hostProjectIds,
|
|
42
|
+
paths: project.paths,
|
|
43
|
+
...project.firstCommit === undefined ? {} : { firstCommit: project.firstCommit },
|
|
44
|
+
memoryEnabled: project.memoryEnabled,
|
|
45
|
+
created: project.created,
|
|
46
|
+
updated: project.updated,
|
|
47
|
+
})),
|
|
48
|
+
ignoredHostProjectIds: this.ignoredHostProjectIds,
|
|
49
|
+
}, null, 2);
|
|
50
|
+
}
|
|
51
|
+
projectForHost(hostProjectId) {
|
|
52
|
+
return this.projects.find(project => project.hostProjectIds.includes(hostProjectId));
|
|
53
|
+
}
|
|
54
|
+
project(id) {
|
|
55
|
+
return this.projects.find(project => project.id === id);
|
|
56
|
+
}
|
|
57
|
+
projectOrThrow(id) {
|
|
58
|
+
const project = this.project(id);
|
|
59
|
+
if (project === undefined)
|
|
60
|
+
throw MemoryError.invalid(`Memory project \`${id}\` does not exist.`);
|
|
61
|
+
return project;
|
|
62
|
+
}
|
|
63
|
+
/** Create a memory project bound to a host project and return its id. */
|
|
64
|
+
createForHost(hostProjectId, rootPath, firstCommit, now) {
|
|
65
|
+
this.unbindHost(hostProjectId);
|
|
66
|
+
const id = this.unusedProjectId();
|
|
67
|
+
this.projects.push({
|
|
68
|
+
id,
|
|
69
|
+
name: directoryName(rootPath),
|
|
70
|
+
hostProjectIds: [hostProjectId],
|
|
71
|
+
paths: [rootPath],
|
|
72
|
+
...firstCommit === undefined ? {} : { firstCommit },
|
|
73
|
+
memoryEnabled: true,
|
|
74
|
+
created: now,
|
|
75
|
+
updated: now,
|
|
76
|
+
});
|
|
77
|
+
this.ignoredHostProjectIds = this.ignoredHostProjectIds.filter(ignored => ignored !== hostProjectId);
|
|
78
|
+
return id;
|
|
79
|
+
}
|
|
80
|
+
/** Create a memory project bound to no host project, such as one restored from an export package. */
|
|
81
|
+
createUnbound(name, paths, firstCommit, now) {
|
|
82
|
+
const id = this.unusedProjectId();
|
|
83
|
+
this.projects.push({
|
|
84
|
+
id,
|
|
85
|
+
name,
|
|
86
|
+
hostProjectIds: [],
|
|
87
|
+
paths,
|
|
88
|
+
...firstCommit === undefined ? {} : { firstCommit },
|
|
89
|
+
memoryEnabled: true,
|
|
90
|
+
created: now,
|
|
91
|
+
updated: now,
|
|
92
|
+
});
|
|
93
|
+
return id;
|
|
94
|
+
}
|
|
95
|
+
/** Bind a host project to an existing memory project and record the path. */
|
|
96
|
+
link(memoryProjectId, hostProjectId, rootPath, now) {
|
|
97
|
+
this.projectOrThrow(memoryProjectId);
|
|
98
|
+
this.unbindHost(hostProjectId);
|
|
99
|
+
const project = this.projectOrThrow(memoryProjectId);
|
|
100
|
+
project.hostProjectIds.push(hostProjectId);
|
|
101
|
+
if (!project.paths.includes(rootPath))
|
|
102
|
+
project.paths.push(rootPath);
|
|
103
|
+
project.updated = now;
|
|
104
|
+
this.ignoredHostProjectIds = this.ignoredHostProjectIds.filter(ignored => ignored !== hostProjectId);
|
|
105
|
+
}
|
|
106
|
+
ignoreHost(hostProjectId) {
|
|
107
|
+
if (!this.ignoredHostProjectIds.includes(hostProjectId))
|
|
108
|
+
this.ignoredHostProjectIds.push(hostProjectId);
|
|
109
|
+
}
|
|
110
|
+
setEnabled(memoryProjectId, enabled, now) {
|
|
111
|
+
const project = this.projectOrThrow(memoryProjectId);
|
|
112
|
+
project.memoryEnabled = enabled;
|
|
113
|
+
project.updated = now;
|
|
114
|
+
}
|
|
115
|
+
rename(memoryProjectId, name, now) {
|
|
116
|
+
const trimmed = name.trim();
|
|
117
|
+
if (trimmed === '' || [...trimmed].length > MAX_TITLE_CHARS) {
|
|
118
|
+
throw MemoryError.invalid(`A memory project name must have 1 to ${MAX_TITLE_CHARS} characters.`);
|
|
119
|
+
}
|
|
120
|
+
const project = this.projectOrThrow(memoryProjectId);
|
|
121
|
+
project.name = trimmed;
|
|
122
|
+
project.updated = now;
|
|
123
|
+
}
|
|
124
|
+
/** Orphaned projects ranked for a host project, or nothing once it is bound or dismissed. */
|
|
125
|
+
orphanCandidates(hostProjectId, rootPath, firstCommit, pathExists) {
|
|
126
|
+
if (this.projectForHost(hostProjectId) !== undefined || this.ignoredHostProjectIds.includes(hostProjectId))
|
|
127
|
+
return [];
|
|
128
|
+
const name = directoryName(rootPath).toLowerCase();
|
|
129
|
+
const score = (candidate) => candidate.reasons.reduce((sum, reason) => sum + (reason === 'same_first_commit' ? 2 : 1), 0);
|
|
130
|
+
return this.projects
|
|
131
|
+
.filter(project => project.paths.length > 0 && !project.paths.some(pathExists))
|
|
132
|
+
.map((project) => {
|
|
133
|
+
const reasons = [];
|
|
134
|
+
if (project.paths.some(path => directoryName(path).toLowerCase() === name))
|
|
135
|
+
reasons.push('same_directory_name');
|
|
136
|
+
if (firstCommit !== undefined && project.firstCommit === firstCommit)
|
|
137
|
+
reasons.push('same_first_commit');
|
|
138
|
+
return { project, reasons };
|
|
139
|
+
})
|
|
140
|
+
.sort((left, right) => score(right) - score(left) || compare(right.project.updated, left.project.updated));
|
|
141
|
+
}
|
|
142
|
+
unbindHost(hostProjectId) {
|
|
143
|
+
for (const project of this.projects)
|
|
144
|
+
project.hostProjectIds = project.hostProjectIds.filter(bound => bound !== hostProjectId);
|
|
145
|
+
}
|
|
146
|
+
unusedProjectId() {
|
|
147
|
+
for (;;) {
|
|
148
|
+
const candidate = `p-${randomHex(10)}`;
|
|
149
|
+
if (this.project(candidate) === undefined)
|
|
150
|
+
return candidate;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
/** The last path component, accepting both separators. */
|
|
155
|
+
export function directoryName(path) {
|
|
156
|
+
const trimmed = path.replace(/[/\\]+$/, '');
|
|
157
|
+
return trimmed.split(/[/\\]/).pop() ?? path;
|
|
158
|
+
}
|
|
159
|
+
/** Lowercase hex of the given length, like the prefix of a simple UUID. */
|
|
160
|
+
export function randomHex(length) {
|
|
161
|
+
return randomBytes(Math.ceil(length / 2)).toString('hex').slice(0, length);
|
|
162
|
+
}
|
|
163
|
+
/** Ordinal string comparison, the order Rust's `str::cmp` gives. */
|
|
164
|
+
export function compare(left, right) {
|
|
165
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
166
|
+
}
|
|
167
|
+
function parseProject(value, path) {
|
|
168
|
+
const fields = ['id', 'name', 'hostProjectIds', 'paths', 'firstCommit', 'memoryEnabled', 'created', 'updated'];
|
|
169
|
+
if (!isRecord(value) || Object.keys(value).some(key => !fields.includes(key)))
|
|
170
|
+
throw MemoryError.json(path, 'a project has unknown fields');
|
|
171
|
+
const { id, name, hostProjectIds, paths, firstCommit, memoryEnabled, created, updated } = value;
|
|
172
|
+
const strings = (items) => Array.isArray(items) && items.every(item => typeof item === 'string');
|
|
173
|
+
if (typeof id !== 'string' || typeof name !== 'string' || !strings(hostProjectIds) || !strings(paths)
|
|
174
|
+
|| (firstCommit !== undefined && typeof firstCommit !== 'string') || typeof memoryEnabled !== 'boolean'
|
|
175
|
+
|| typeof created !== 'string' || typeof updated !== 'string') {
|
|
176
|
+
throw MemoryError.json(path, `project ${JSON.stringify(id)} has fields of the wrong type`);
|
|
177
|
+
}
|
|
178
|
+
return { id, name, hostProjectIds, paths, ...firstCommit === undefined ? {} : { firstCommit }, memoryEnabled, created, updated };
|
|
179
|
+
}
|
|
180
|
+
export function isRecord(value) {
|
|
181
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
182
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** The three memory scopes and their preset sections (`scope.rs`). */
|
|
2
|
+
export type MemoryScope = 'user' | 'space' | 'project';
|
|
3
|
+
/** Every scope in display order. */
|
|
4
|
+
export declare const SCOPES: readonly MemoryScope[];
|
|
5
|
+
/** Stable key of the base section injected in full for space and project. */
|
|
6
|
+
export declare const BASE_SECTION_KEY = "base";
|
|
7
|
+
export declare function isScope(value: unknown): value is MemoryScope;
|
|
8
|
+
/** Conflict priority; higher wins (project > space > user). */
|
|
9
|
+
export declare function scopePriority(scope: MemoryScope): number;
|
|
10
|
+
/** Whether the scope separates an injected base section from a catalog. */
|
|
11
|
+
export declare function hasBaseSection(scope: MemoryScope): boolean;
|
|
12
|
+
/** `[key, default title]` of the sections a new file of the scope starts with. */
|
|
13
|
+
export declare function presetSections(scope: MemoryScope): readonly (readonly [string, string])[];
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/** The three memory scopes and their preset sections (`scope.rs`). */
|
|
2
|
+
/** Every scope in display order. */
|
|
3
|
+
export const SCOPES = ['user', 'space', 'project'];
|
|
4
|
+
/** Stable key of the base section injected in full for space and project. */
|
|
5
|
+
export const BASE_SECTION_KEY = 'base';
|
|
6
|
+
export function isScope(value) {
|
|
7
|
+
return value === 'user' || value === 'space' || value === 'project';
|
|
8
|
+
}
|
|
9
|
+
/** Conflict priority; higher wins (project > space > user). */
|
|
10
|
+
export function scopePriority(scope) {
|
|
11
|
+
return SCOPES.indexOf(scope);
|
|
12
|
+
}
|
|
13
|
+
/** Whether the scope separates an injected base section from a catalog. */
|
|
14
|
+
export function hasBaseSection(scope) {
|
|
15
|
+
return scope !== 'user';
|
|
16
|
+
}
|
|
17
|
+
/** `[key, default title]` of the sections a new file of the scope starts with. */
|
|
18
|
+
export function presetSections(scope) {
|
|
19
|
+
switch (scope) {
|
|
20
|
+
case 'user': return [['identity', 'Identity'], ['communication', 'Communication'], ['expertise', 'Expertise']];
|
|
21
|
+
case 'space': return [[BASE_SECTION_KEY, 'Base'], ['conventions', 'Conventions'], ['environment', 'Environment'], ['references', 'References']];
|
|
22
|
+
case 'project': return [[BASE_SECTION_KEY, 'Base'], ['architecture', 'Architecture'], ['constraints', 'Constraints'], ['pitfalls', 'Pitfalls'], ['in-progress', 'In progress']];
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/** Node search and outline (`search.rs`), modelled on `rg` → owner and `markdown-menu`. */
|
|
2
|
+
import { type NodeView } from './document.js';
|
|
3
|
+
import { type MemoryScope } from './scope.js';
|
|
4
|
+
import { type ScopeDocuments } from './store.js';
|
|
5
|
+
/** Arguments of `memory_search`. */
|
|
6
|
+
export interface SearchRequest {
|
|
7
|
+
/** Patterns combined with OR. */
|
|
8
|
+
readonly patterns?: readonly string[];
|
|
9
|
+
/** Treat patterns as literal text. */
|
|
10
|
+
readonly fixedStrings?: boolean;
|
|
11
|
+
/** Case-insensitive matching; defaults to true. */
|
|
12
|
+
readonly ignoreCase?: boolean;
|
|
13
|
+
/** Scopes to search; empty means every available scope. */
|
|
14
|
+
readonly scopes?: readonly MemoryScope[];
|
|
15
|
+
/** Section key or H1 title filter. */
|
|
16
|
+
readonly section?: string;
|
|
17
|
+
/** Title path prefix filter, segments separated by `/` or `›`. */
|
|
18
|
+
readonly pathPrefix?: string;
|
|
19
|
+
/** Exact node ids to read instead of patterns. */
|
|
20
|
+
readonly ids?: readonly string[];
|
|
21
|
+
/** Nodes per page. */
|
|
22
|
+
readonly limit?: number;
|
|
23
|
+
/** Continuation cursor from a previous result. */
|
|
24
|
+
readonly cursor?: string;
|
|
25
|
+
}
|
|
26
|
+
/** Arguments of `memory_outline`. */
|
|
27
|
+
export interface OutlineRequest {
|
|
28
|
+
readonly scopes?: readonly MemoryScope[];
|
|
29
|
+
readonly section?: string;
|
|
30
|
+
readonly pathPrefix?: string;
|
|
31
|
+
/** Deepest heading level to list, 1 to 4. */
|
|
32
|
+
readonly depth?: number;
|
|
33
|
+
}
|
|
34
|
+
/** One matched or requested node. */
|
|
35
|
+
export interface SearchHit {
|
|
36
|
+
readonly view: NodeView;
|
|
37
|
+
/** Number of distinct patterns that matched. */
|
|
38
|
+
readonly matchedPatterns: number;
|
|
39
|
+
readonly titleHit: boolean;
|
|
40
|
+
/** Zero-based body line indexes with a match. */
|
|
41
|
+
readonly hitLines: ReadonlySet<number>;
|
|
42
|
+
/** One-based page shown. */
|
|
43
|
+
readonly page: number;
|
|
44
|
+
readonly pages: number;
|
|
45
|
+
/** Body text of the shown page with hit lines marked. */
|
|
46
|
+
readonly text: string;
|
|
47
|
+
}
|
|
48
|
+
/** One page of search results. */
|
|
49
|
+
export interface SearchPage {
|
|
50
|
+
readonly hits: readonly SearchHit[];
|
|
51
|
+
/** Total matched nodes. */
|
|
52
|
+
readonly total: number;
|
|
53
|
+
/** Offset of the first hit on this page. */
|
|
54
|
+
readonly offset: number;
|
|
55
|
+
/** Number of patterns in the query; zero for an id read. */
|
|
56
|
+
readonly patternCount: number;
|
|
57
|
+
/** Notes about unavailable scopes. */
|
|
58
|
+
readonly notes: readonly string[];
|
|
59
|
+
}
|
|
60
|
+
/** Search the visible scopes and return one page of hits. */
|
|
61
|
+
export declare function searchNodes(documents: ScopeDocuments, request: SearchRequest): SearchPage;
|
|
62
|
+
/** Render a search page as model-facing text. */
|
|
63
|
+
export declare function renderSearchPage(page: SearchPage): string;
|
|
64
|
+
/** Render the heading tree of the visible scopes. */
|
|
65
|
+
export declare function renderOutline(documents: ScopeDocuments, request: OutlineRequest): string;
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
/** Node search and outline (`search.rs`), modelled on `rg` → owner and `markdown-menu`. */
|
|
2
|
+
import { DEFAULT_SEARCH_LIMIT, MAX_NODE_LEVEL, MAX_SEARCH_LIMIT, MAX_SEARCH_PATTERNS, NODE_PAGE_BYTES, byteLength } from './constants.js';
|
|
3
|
+
import { lines as bodyLines, viewTitle } from './document.js';
|
|
4
|
+
import { MemoryError } from './errors.js';
|
|
5
|
+
import { compare } from './registry.js';
|
|
6
|
+
import { SCOPES, scopePriority } from './scope.js';
|
|
7
|
+
import { scopeDocument } from './store.js';
|
|
8
|
+
/** Marker placed before body lines that contain a match. */
|
|
9
|
+
const HIT_MARKER = '>> ';
|
|
10
|
+
/** Search the visible scopes and return one page of hits. */
|
|
11
|
+
export function searchNodes(documents, request) {
|
|
12
|
+
const patterns = request.patterns ?? [];
|
|
13
|
+
const ids = request.ids ?? [];
|
|
14
|
+
const hasPatterns = patterns.some(pattern => pattern !== '');
|
|
15
|
+
if (hasPatterns === (ids.length > 0))
|
|
16
|
+
throw MemoryError.invalid('Give either `patterns` or `ids`, not both and not neither.');
|
|
17
|
+
if (patterns.length > MAX_SEARCH_PATTERNS)
|
|
18
|
+
throw MemoryError.invalid(`At most ${MAX_SEARCH_PATTERNS} patterns are allowed.`);
|
|
19
|
+
const limit = Math.min(Math.max(request.limit ?? DEFAULT_SEARCH_LIMIT, 1), MAX_SEARCH_LIMIT);
|
|
20
|
+
const { views: visible, notes } = visibleViews(documents, request.scopes ?? []);
|
|
21
|
+
const views = filterViews(visible, request.section, request.pathPrefix);
|
|
22
|
+
if (ids.length > 0) {
|
|
23
|
+
const pageRequest = parseCursor(request.cursor);
|
|
24
|
+
if (pageRequest > 1 && ids.length !== 1)
|
|
25
|
+
throw MemoryError.invalid('A page cursor with `ids` needs exactly one id.');
|
|
26
|
+
const hits = ids.map(id => {
|
|
27
|
+
const view = views.find(candidate => candidate.id === id);
|
|
28
|
+
if (view === undefined)
|
|
29
|
+
throw MemoryError.notFound(id);
|
|
30
|
+
return pagedHit(view, 0, false, new Set(), Math.max(pageRequest, 1));
|
|
31
|
+
});
|
|
32
|
+
return { hits, total: hits.length, offset: 0, patternCount: 0, notes };
|
|
33
|
+
}
|
|
34
|
+
const ignoreCase = request.ignoreCase ?? true;
|
|
35
|
+
const regexes = patterns.filter(pattern => pattern !== '').map(pattern => compile(pattern, request.fixedStrings ?? false, ignoreCase));
|
|
36
|
+
const matches = [];
|
|
37
|
+
for (const view of views) {
|
|
38
|
+
const title = viewTitle(view);
|
|
39
|
+
const body = bodyLines(view.body);
|
|
40
|
+
const matched = new Set();
|
|
41
|
+
const hitLines = new Set();
|
|
42
|
+
let titleHit = false;
|
|
43
|
+
regexes.forEach((regex, index) => {
|
|
44
|
+
if (regex.test(title)) {
|
|
45
|
+
matched.add(index);
|
|
46
|
+
titleHit = true;
|
|
47
|
+
}
|
|
48
|
+
body.forEach((line, lineIndex) => {
|
|
49
|
+
if (regex.test(line)) {
|
|
50
|
+
matched.add(index);
|
|
51
|
+
hitLines.add(lineIndex);
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
if (matched.size > 0)
|
|
56
|
+
matches.push({ count: matched.size, titleHit, hitLines, view });
|
|
57
|
+
}
|
|
58
|
+
matches.sort((left, right) => right.count - left.count
|
|
59
|
+
|| Number(right.titleHit) - Number(left.titleHit)
|
|
60
|
+
|| scopePriority(right.view.scope) - scopePriority(left.view.scope)
|
|
61
|
+
|| compare(updated(right.view), updated(left.view)));
|
|
62
|
+
const offset = parseCursor(request.cursor) - 1;
|
|
63
|
+
const hits = matches.slice(offset, offset + limit).map(match => pagedHit(match.view, match.count, match.titleHit, match.hitLines, undefined));
|
|
64
|
+
return { hits, total: matches.length, offset, patternCount: regexes.length, notes };
|
|
65
|
+
}
|
|
66
|
+
/** Render a search page as model-facing text. */
|
|
67
|
+
export function renderSearchPage(page) {
|
|
68
|
+
const output = [...page.notes];
|
|
69
|
+
if (page.total === 0) {
|
|
70
|
+
output.push('No memory node matched. Try other wording or synonyms, or list titles with memory_outline.');
|
|
71
|
+
return output.join('\n');
|
|
72
|
+
}
|
|
73
|
+
if (page.patternCount > 0)
|
|
74
|
+
output.push(`${page.total} node(s) matched; showing ${page.offset + 1}-${page.offset + page.hits.length}.`);
|
|
75
|
+
for (const hit of page.hits) {
|
|
76
|
+
const view = hit.view;
|
|
77
|
+
output.push('', `## [${view.scope}] ${view.path.join(' › ')}`);
|
|
78
|
+
const facts = [`id: ${view.id}`, `revision: ${view.revision}`];
|
|
79
|
+
if (view.meta !== undefined) {
|
|
80
|
+
facts.push(`updated: ${view.meta.updated.replace('T', ' ').replace(/Z+$/, '')}`, `source: ${view.meta.source}`);
|
|
81
|
+
}
|
|
82
|
+
if (page.patternCount > 0)
|
|
83
|
+
facts.push(`matched: ${hit.matchedPatterns}/${page.patternCount} patterns${hit.titleHit ? ' (title)' : ''}`);
|
|
84
|
+
output.push(facts.join(' · '));
|
|
85
|
+
if (hit.text !== '')
|
|
86
|
+
output.push(hit.text);
|
|
87
|
+
if (view.children.length > 0)
|
|
88
|
+
output.push(`children: ${view.children.join(' · ')}`);
|
|
89
|
+
if (hit.pages > 1) {
|
|
90
|
+
const next = hit.page < hit.pages ? `; continue with ids=["${view.id}"] and cursor="${hit.page + 1}"` : '';
|
|
91
|
+
output.push(`page ${hit.page}/${hit.pages}${next}`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
const shown = page.offset + page.hits.length;
|
|
95
|
+
if (page.patternCount > 0 && shown < page.total)
|
|
96
|
+
output.push('', `More nodes matched; continue with cursor="${shown + 1}".`);
|
|
97
|
+
return output.join('\n');
|
|
98
|
+
}
|
|
99
|
+
/** Render the heading tree of the visible scopes. */
|
|
100
|
+
export function renderOutline(documents, request) {
|
|
101
|
+
const depth = request.depth ?? MAX_NODE_LEVEL;
|
|
102
|
+
if (!Number.isInteger(depth) || depth < 1 || depth > MAX_NODE_LEVEL)
|
|
103
|
+
throw MemoryError.invalid(`\`depth\` must be between 1 and ${MAX_NODE_LEVEL}.`);
|
|
104
|
+
const { views: visible, notes } = visibleViews(documents, request.scopes ?? []);
|
|
105
|
+
const views = filterViews(visible, request.section, request.pathPrefix);
|
|
106
|
+
const output = [...notes];
|
|
107
|
+
for (const scope of SCOPES) {
|
|
108
|
+
const scopeViews = views.filter(view => view.scope === scope);
|
|
109
|
+
if (scopeViews.length === 0)
|
|
110
|
+
continue;
|
|
111
|
+
const label = scope === 'project' && documents.project !== undefined ? `project ${documents.project.project.name}` : scope;
|
|
112
|
+
if (output.length > 0)
|
|
113
|
+
output.push('');
|
|
114
|
+
output.push(`[${label}] ${scopeViews.length} node(s)`);
|
|
115
|
+
for (const view of scopeViews.filter(candidate => candidate.level <= depth)) {
|
|
116
|
+
const facts = [view.id, `r:${view.revision}`];
|
|
117
|
+
if (view.meta !== undefined) {
|
|
118
|
+
facts.push([...view.meta.updated].slice(0, 10).join(''));
|
|
119
|
+
if (view.meta.section !== undefined)
|
|
120
|
+
facts.push(`section:${view.meta.section}`);
|
|
121
|
+
}
|
|
122
|
+
if (view.body !== '')
|
|
123
|
+
facts.push(`${byteLength(view.body)} B`);
|
|
124
|
+
if (view.level === depth && view.descendants > 0)
|
|
125
|
+
facts.push(`+${view.descendants} below`);
|
|
126
|
+
output.push(`${'#'.repeat(view.level)} ${viewTitle(view)} (${facts.join(' · ')})`);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
if (output.length === 0)
|
|
130
|
+
output.push('Memory is empty. Record durable facts with memory_write.');
|
|
131
|
+
return output.join('\n');
|
|
132
|
+
}
|
|
133
|
+
/** Views of the requested scopes, and notes for unavailable ones. */
|
|
134
|
+
function visibleViews(documents, scopes) {
|
|
135
|
+
const views = [];
|
|
136
|
+
const notes = [];
|
|
137
|
+
for (const scope of scopes.length === 0 ? SCOPES : scopes) {
|
|
138
|
+
const document = scopeDocument(documents, scope);
|
|
139
|
+
if (document !== undefined)
|
|
140
|
+
views.push(...document.views(scope));
|
|
141
|
+
else if (scopes.length > 0)
|
|
142
|
+
notes.push('[project] No project memory exists yet; it is created by the first project write.');
|
|
143
|
+
}
|
|
144
|
+
return { views, notes };
|
|
145
|
+
}
|
|
146
|
+
/** Apply section and path-prefix filters. */
|
|
147
|
+
function filterViews(views, section, pathPrefix) {
|
|
148
|
+
const wanted = section?.trim() || undefined;
|
|
149
|
+
const prefix = (pathPrefix ?? '').split(/[/›]/).map(segment => segment.trim().toLowerCase()).filter(segment => segment !== '');
|
|
150
|
+
return views
|
|
151
|
+
.filter(view => wanted === undefined || view.section === wanted || (view.path[0] !== undefined && asciiLower(view.path[0]) === asciiLower(wanted)))
|
|
152
|
+
.filter(view => prefix.length <= view.path.length && prefix.every((segment, index) => view.path[index]?.toLowerCase() === segment));
|
|
153
|
+
}
|
|
154
|
+
/** Rust `eq_ignore_ascii_case` compares with only ASCII letters folded. */
|
|
155
|
+
function asciiLower(text) {
|
|
156
|
+
return text.replace(/[A-Z]/g, letter => letter.toLowerCase());
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Compile one pattern. VulcanCode uses Rust's `regex` crate; JavaScript's Unicode regex accepts
|
|
160
|
+
* the same common syntax (classes, alternation, anchors, `\b`, `\d`, `\w`, `\s`).
|
|
161
|
+
*/
|
|
162
|
+
function compile(pattern, fixed, ignoreCase) {
|
|
163
|
+
const source = fixed ? escapeRegex(pattern) : pattern;
|
|
164
|
+
try {
|
|
165
|
+
return new RegExp(source, ignoreCase ? 'iu' : 'u');
|
|
166
|
+
}
|
|
167
|
+
catch (error) {
|
|
168
|
+
throw MemoryError.invalid(`Pattern \`${pattern}\` is not a valid regex (${error instanceof Error ? error.message : String(error)}); set fixedStrings for literal text.`);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
function escapeRegex(text) {
|
|
172
|
+
return text.replace(/[\\^$.*+?()[\]{}|/]/g, '\\$&');
|
|
173
|
+
}
|
|
174
|
+
/** Parse a one-based cursor; absent means 1. */
|
|
175
|
+
function parseCursor(cursor) {
|
|
176
|
+
const text = cursor?.trim() ?? '';
|
|
177
|
+
if (text === '')
|
|
178
|
+
return 1;
|
|
179
|
+
const value = /^\+?\d+$/.test(text) ? Number(text) : Number.NaN;
|
|
180
|
+
if (!Number.isSafeInteger(value) || value < 1)
|
|
181
|
+
throw MemoryError.invalid(`Cursor \`${text}\` is not a page number.`);
|
|
182
|
+
return value;
|
|
183
|
+
}
|
|
184
|
+
/** A node's update time for ordering; nodes without metadata sort last. */
|
|
185
|
+
function updated(view) {
|
|
186
|
+
return view.meta?.updated ?? '';
|
|
187
|
+
}
|
|
188
|
+
/** A hit showing the requested page, or the page of the first hit line. */
|
|
189
|
+
function pagedHit(view, matchedPatterns, titleHit, hitLines, requestedPage) {
|
|
190
|
+
const body = bodyLines(view.body);
|
|
191
|
+
const pages = pageRanges(body);
|
|
192
|
+
const pageCount = Math.max(pages.length, 1);
|
|
193
|
+
let page;
|
|
194
|
+
if (requestedPage !== undefined) {
|
|
195
|
+
if (requestedPage > pageCount)
|
|
196
|
+
throw MemoryError.invalid(`Node \`${view.id}\` has ${pageCount} page(s).`);
|
|
197
|
+
page = requestedPage;
|
|
198
|
+
}
|
|
199
|
+
else {
|
|
200
|
+
const first = Math.min(...hitLines);
|
|
201
|
+
const index = pages.findIndex(([start, end]) => first >= start && first < end);
|
|
202
|
+
page = index < 0 ? 1 : index + 1;
|
|
203
|
+
}
|
|
204
|
+
const range = pages[page - 1];
|
|
205
|
+
const text = range === undefined
|
|
206
|
+
? ''
|
|
207
|
+
: body.slice(range[0], range[1]).map((line, offset) => hitLines.has(range[0] + offset) ? `${HIT_MARKER}${line}` : line).join('\n');
|
|
208
|
+
return { view, matchedPatterns, titleHit, hitLines, page, pages: pageCount, text };
|
|
209
|
+
}
|
|
210
|
+
/** Split body lines into `[start, end)` pages of at most {@link NODE_PAGE_BYTES}, breaking only between lines. */
|
|
211
|
+
function pageRanges(body) {
|
|
212
|
+
const pages = [];
|
|
213
|
+
let start = 0;
|
|
214
|
+
let bytes = 0;
|
|
215
|
+
body.forEach((line, index) => {
|
|
216
|
+
const cost = byteLength(line) + 1;
|
|
217
|
+
if (index > start && bytes + cost > NODE_PAGE_BYTES) {
|
|
218
|
+
pages.push([start, index]);
|
|
219
|
+
start = index;
|
|
220
|
+
bytes = 0;
|
|
221
|
+
}
|
|
222
|
+
bytes += cost;
|
|
223
|
+
});
|
|
224
|
+
if (start < body.length)
|
|
225
|
+
pages.push([start, body.length]);
|
|
226
|
+
return pages;
|
|
227
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/** Injection snapshot: full-text parts, catalogs and their budget usage (`snapshot.rs`). */
|
|
2
|
+
import type { InjectionBudget } from '../settings.js';
|
|
3
|
+
import { type MemoryDocument } from './document.js';
|
|
4
|
+
import { type MemoryScope } from './scope.js';
|
|
5
|
+
import type { ScopeDocuments } from './store.js';
|
|
6
|
+
/** One budgeted part of the injection. */
|
|
7
|
+
export type BudgetPart = 'user' | 'spaceBase' | 'projectBase' | 'spaceCatalog' | 'projectCatalog';
|
|
8
|
+
/** Usage of one budget part. */
|
|
9
|
+
export interface BudgetUsage {
|
|
10
|
+
readonly part: BudgetPart;
|
|
11
|
+
/** Tokens the full content needs. */
|
|
12
|
+
readonly tokens: number;
|
|
13
|
+
/** Configured budget. */
|
|
14
|
+
readonly budget: number;
|
|
15
|
+
/** Titles left out of a full-text part, or the catalog depth that was dropped. */
|
|
16
|
+
readonly omitted: readonly string[];
|
|
17
|
+
}
|
|
18
|
+
/** Whether content needs more than the budget. */
|
|
19
|
+
export declare function overBudget(usage: BudgetUsage): boolean;
|
|
20
|
+
/** Whether usage reached the warning ratio without exceeding the budget. */
|
|
21
|
+
export declare function nearBudget(usage: BudgetUsage): boolean;
|
|
22
|
+
/** One usage line for a write receipt. */
|
|
23
|
+
export declare function renderUsageLine(usage: BudgetUsage): string;
|
|
24
|
+
/** The text injected at the start of a session and after compaction, with its usage. */
|
|
25
|
+
export interface InjectionSnapshot {
|
|
26
|
+
/** Model-facing snapshot text; empty when every scope is empty. */
|
|
27
|
+
readonly content: string;
|
|
28
|
+
/** Usage of every part that has content. */
|
|
29
|
+
readonly usage: readonly BudgetUsage[];
|
|
30
|
+
}
|
|
31
|
+
/** Usage of the full-text part of a scope, for write receipts. */
|
|
32
|
+
export declare function baseUsage(scope: MemoryScope, document: MemoryDocument, budget: InjectionBudget): BudgetUsage;
|
|
33
|
+
/** The injection snapshot of every visible scope. */
|
|
34
|
+
export declare function buildInjectionSnapshot(documents: ScopeDocuments, budget: InjectionBudget): InjectionSnapshot;
|