open-memex 0.1.0 → 0.3.0-alpha
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/AGENTS.md +62 -10
- package/CONTRIBUTING.md +31 -0
- package/README.md +250 -38
- package/README.zh-CN.md +307 -0
- package/bin/open-memex.js +28 -0
- package/docs/SCOPES.md +81 -0
- package/docs/V2-DESIGN.md +588 -0
- package/package.json +12 -3
- package/scripts/smoke-mcp.ts +135 -0
- package/scripts/smoke-pure.ts +250 -9
- package/src/capture/keywords.ts +28 -15
- package/src/cli.ts +347 -26
- package/src/config.ts +91 -13
- package/src/doctor.ts +161 -0
- package/src/index.ts +34 -13
- package/src/init.ts +304 -0
- package/src/mcp.ts +133 -0
- package/src/redact.ts +255 -4
- package/src/retrieve/cjk.ts +63 -0
- package/src/retrieve/inject.ts +2 -2
- package/src/retrieve/search.ts +115 -28
- package/src/scope.ts +7 -2
- package/src/store/db.ts +62 -14
- package/src/store/lifecycle.ts +280 -0
- package/src/store/markdown.ts +163 -11
- package/src/store/sync.ts +53 -9
- package/src/store/v2migrate.ts +190 -0
- package/src/tools/memory.ts +32 -146
- package/src/tools/ops.ts +259 -0
- package/PLAN.md +0 -168
package/src/store/sync.ts
CHANGED
|
@@ -1,17 +1,31 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import { db } from "./db.ts";
|
|
3
|
-
import {
|
|
3
|
+
import { cjkIndexText } from "../retrieve/cjk.ts";
|
|
4
|
+
import { contentHash, repairChain } from "./lifecycle.ts";
|
|
5
|
+
import {
|
|
6
|
+
iterMemoryFiles,
|
|
7
|
+
readMemoryFile,
|
|
8
|
+
timeToMs,
|
|
9
|
+
type MemoryFile,
|
|
10
|
+
} from "./markdown.ts";
|
|
4
11
|
|
|
5
12
|
const UPSERT_SQL = `
|
|
6
|
-
INSERT INTO memories (id, scope_key,
|
|
7
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
13
|
+
INSERT INTO memories (id, scope_key, scope, visibility, project_name, type, role, importance, status, tags, content, cjk, content_hash, superseded_by, source, file_path, mtime_ms, created_at, updated_at)
|
|
14
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
8
15
|
ON CONFLICT(id) DO UPDATE SET
|
|
9
16
|
scope_key = excluded.scope_key,
|
|
10
|
-
|
|
17
|
+
scope = excluded.scope,
|
|
18
|
+
visibility = excluded.visibility,
|
|
11
19
|
project_name = excluded.project_name,
|
|
12
20
|
type = excluded.type,
|
|
21
|
+
role = excluded.role,
|
|
22
|
+
importance = excluded.importance,
|
|
23
|
+
status = excluded.status,
|
|
13
24
|
tags = excluded.tags,
|
|
14
25
|
content = excluded.content,
|
|
26
|
+
cjk = excluded.cjk,
|
|
27
|
+
content_hash = excluded.content_hash,
|
|
28
|
+
superseded_by = excluded.superseded_by,
|
|
15
29
|
source = excluded.source,
|
|
16
30
|
file_path = excluded.file_path,
|
|
17
31
|
mtime_ms = excluded.mtime_ms,
|
|
@@ -19,20 +33,50 @@ const UPSERT_SQL = `
|
|
|
19
33
|
`;
|
|
20
34
|
|
|
21
35
|
export function upsertFromFile(mf: MemoryFile): void {
|
|
22
|
-
|
|
36
|
+
// Chain integrity (§3.3): every write path through Core validates the
|
|
37
|
+
// supersedes/superseded_by pair and auto-completes a missing side.
|
|
38
|
+
const { warnings, repaired } = repairChain(mf);
|
|
39
|
+
for (const w of warnings) console.warn(`[open-memex] chain: ${w}`);
|
|
40
|
+
const mtimeOf = (m: MemoryFile): number => {
|
|
41
|
+
try {
|
|
42
|
+
return fs.statSync(m.filePath).mtimeMs;
|
|
43
|
+
} catch {
|
|
44
|
+
return m.mtimeMs; // file vanished mid-repair; keep the old mtime
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
writeRow(
|
|
48
|
+
mf,
|
|
49
|
+
repaired.some((r) => r.filePath === mf.filePath) ? mtimeOf(mf) : mf.mtimeMs,
|
|
50
|
+
);
|
|
51
|
+
for (const r of repaired) {
|
|
52
|
+
if (r.filePath === mf.filePath) continue;
|
|
53
|
+
writeRow(r, mtimeOf(r));
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function writeRow(mf: MemoryFile, mtimeMs: number): void {
|
|
58
|
+
const { fm, body, filePath } = mf;
|
|
59
|
+
const tags = (fm.tags ?? []).join(",");
|
|
23
60
|
db().prepare(UPSERT_SQL).run(
|
|
24
61
|
fm.id,
|
|
25
62
|
fm.scope_key,
|
|
26
|
-
fm.
|
|
63
|
+
fm.scope,
|
|
64
|
+
fm.visibility,
|
|
27
65
|
fm.project_name,
|
|
28
66
|
fm.type,
|
|
29
|
-
|
|
67
|
+
fm.role,
|
|
68
|
+
fm.importance,
|
|
69
|
+
fm.status,
|
|
70
|
+
tags,
|
|
30
71
|
body,
|
|
72
|
+
cjkIndexText(body + "\n" + tags),
|
|
73
|
+
contentHash(body),
|
|
74
|
+
fm.superseded_by ?? null,
|
|
31
75
|
fm.source ?? "",
|
|
32
76
|
filePath,
|
|
33
77
|
mtimeMs,
|
|
34
|
-
fm.created_at,
|
|
35
|
-
fm.updated_at,
|
|
78
|
+
timeToMs(fm.created_at),
|
|
79
|
+
timeToMs(fm.updated_at),
|
|
36
80
|
);
|
|
37
81
|
}
|
|
38
82
|
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { paths } from "../paths.ts";
|
|
4
|
+
import {
|
|
5
|
+
parseRawFrontmatter,
|
|
6
|
+
normalizeFrontmatter,
|
|
7
|
+
isTaxonomyType,
|
|
8
|
+
serialize,
|
|
9
|
+
type Frontmatter,
|
|
10
|
+
} from "./markdown.ts";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* v1 → v2 file migration (V2-DESIGN §19). Explicit only — run via
|
|
14
|
+
* `open-memex migrate --to-v2`. Converts frontmatter in place:
|
|
15
|
+
*
|
|
16
|
+
* - scope rename: v1 `user` → v2 `personal` (scope, scope_key, and the
|
|
17
|
+
* storage dir `memories/user/` → `memories/personal/`)
|
|
18
|
+
* - times: epoch ms → RFC 3339
|
|
19
|
+
* - `priority: N` → `importance: low|normal|high`
|
|
20
|
+
* - `type: instruction` → `type: knowledge` + `role: instruction`
|
|
21
|
+
* - unknown v1 types → `knowledge` (v2 taxonomy, §3.1)
|
|
22
|
+
* - adds `schema_version: 2`, `visibility`, `role`, `status`
|
|
23
|
+
*
|
|
24
|
+
* Legacy `my-o-memory` data roots (§19/D13: `.my-o-memory/` →
|
|
25
|
+
* `.open-memex/`, `~/.my-o-memory/` → `~/.open-memex/`) are merged into the
|
|
26
|
+
* current root; the old dir is kept as a dated backup.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
const V2_MARKER = /^schema_version:\s*2(\s|$)/m;
|
|
30
|
+
|
|
31
|
+
export function isV2File(raw: string): boolean {
|
|
32
|
+
return V2_MARKER.test(raw);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface ConversionPlan {
|
|
36
|
+
fm: Frontmatter;
|
|
37
|
+
body: string;
|
|
38
|
+
fromPath: string;
|
|
39
|
+
toPath: string;
|
|
40
|
+
changes: string[];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Plan the v1 → v2 conversion of one file. Returns null when the file is
|
|
45
|
+
* already v2 (nothing to do). Pure — no disk writes.
|
|
46
|
+
*/
|
|
47
|
+
export function planConversion(
|
|
48
|
+
filePath: string,
|
|
49
|
+
memoriesRoot: string,
|
|
50
|
+
): ConversionPlan | null {
|
|
51
|
+
const raw = fs.readFileSync(filePath, "utf8");
|
|
52
|
+
if (isV2File(raw)) return null;
|
|
53
|
+
const parsed = parseRawFrontmatter(raw);
|
|
54
|
+
if (!parsed) return null;
|
|
55
|
+
const { rawFm, body } = parsed;
|
|
56
|
+
|
|
57
|
+
const fm = normalizeFrontmatter(rawFm);
|
|
58
|
+
const changes: string[] = [];
|
|
59
|
+
|
|
60
|
+
// scope rename
|
|
61
|
+
if (rawFm.scope_kind === "user" || rawFm.scope_key === "user") {
|
|
62
|
+
changes.push("scope user → personal");
|
|
63
|
+
} else if (!rawFm.scope) {
|
|
64
|
+
changes.push(`scope → ${fm.scope} (default)`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// storage dir move follows scope_key
|
|
68
|
+
const toPath = path.join(memoriesRoot, fm.scope_key, path.basename(filePath));
|
|
69
|
+
if (toPath !== filePath) {
|
|
70
|
+
changes.push(
|
|
71
|
+
`file moved: ${path.basename(path.dirname(filePath))}/ → ${fm.scope_key}/`,
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// visibility default
|
|
76
|
+
if (!rawFm.visibility) {
|
|
77
|
+
changes.push(`visibility → ${fm.visibility} (default)`);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// type taxonomy
|
|
81
|
+
const rawType = rawFm.type;
|
|
82
|
+
if (rawType === "instruction") {
|
|
83
|
+
changes.push("type instruction → knowledge, role → instruction (D11)");
|
|
84
|
+
} else if (typeof rawType === "string" && rawType && !isTaxonomyType(rawType)) {
|
|
85
|
+
changes.push(`type "${rawType}" → knowledge (not in v2 taxonomy)`);
|
|
86
|
+
fm.type = "knowledge";
|
|
87
|
+
} else if (!rawType) {
|
|
88
|
+
changes.push(`type → ${fm.type} (default)`);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// priority → importance
|
|
92
|
+
if (typeof rawFm.priority === "number") {
|
|
93
|
+
changes.push(`priority ${rawFm.priority} → importance ${fm.importance}`);
|
|
94
|
+
} else if (!rawFm.importance) {
|
|
95
|
+
changes.push(`importance → ${fm.importance} (default)`);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// times
|
|
99
|
+
if (typeof rawFm.created_at === "number") {
|
|
100
|
+
changes.push("created_at/updated_at: epoch ms → RFC 3339");
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// status default
|
|
104
|
+
if (!rawFm.status) {
|
|
105
|
+
changes.push(`status → ${fm.status} (default)`);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
changes.push("schema_version → 2");
|
|
109
|
+
return { fm, body, fromPath: filePath, toPath, changes };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export interface MigrateV2Result {
|
|
113
|
+
scanned: number;
|
|
114
|
+
converted: number;
|
|
115
|
+
skippedV2: number;
|
|
116
|
+
plans: ConversionPlan[];
|
|
117
|
+
legacyBackup: string | null;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Merge a legacy `my-o-memory` data root into the current `open-memex` root.
|
|
122
|
+
* The old dir is renamed to a dated backup — never deleted. Returns the
|
|
123
|
+
* backup path, or null when no legacy dir exists.
|
|
124
|
+
*/
|
|
125
|
+
function migrateLegacyDataRoot(dryRun: boolean): string | null {
|
|
126
|
+
const { root } = paths();
|
|
127
|
+
const legacy = path.join(path.dirname(root), "my-o-memory");
|
|
128
|
+
if (!fs.existsSync(legacy) || !fs.statSync(legacy).isDirectory()) return null;
|
|
129
|
+
|
|
130
|
+
const stamp = new Date().toISOString().slice(0, 10);
|
|
131
|
+
const backup = `${legacy}.backup-${stamp}`;
|
|
132
|
+
if (!dryRun) {
|
|
133
|
+
const legacyMem = path.join(legacy, "memories");
|
|
134
|
+
if (fs.existsSync(legacyMem)) {
|
|
135
|
+
for (const entry of fs.readdirSync(legacyMem)) {
|
|
136
|
+
const src = path.join(legacyMem, entry);
|
|
137
|
+
const dst = path.join(root, "memories", entry);
|
|
138
|
+
if (!fs.existsSync(dst)) {
|
|
139
|
+
fs.renameSync(src, dst);
|
|
140
|
+
} else {
|
|
141
|
+
// merge file-by-file, never overwrite
|
|
142
|
+
for (const name of fs.readdirSync(src)) {
|
|
143
|
+
const s = path.join(src, name);
|
|
144
|
+
const d = path.join(dst, name);
|
|
145
|
+
if (!fs.existsSync(d)) fs.renameSync(s, d);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
fs.renameSync(legacy, backup);
|
|
151
|
+
}
|
|
152
|
+
return backup;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export function migrateV2(opts: { dryRun: boolean }): MigrateV2Result {
|
|
156
|
+
const { memories: memoriesRoot } = paths();
|
|
157
|
+
const result: MigrateV2Result = {
|
|
158
|
+
scanned: 0,
|
|
159
|
+
converted: 0,
|
|
160
|
+
skippedV2: 0,
|
|
161
|
+
plans: [],
|
|
162
|
+
legacyBackup: null,
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
result.legacyBackup = migrateLegacyDataRoot(opts.dryRun);
|
|
166
|
+
|
|
167
|
+
if (!fs.existsSync(memoriesRoot)) return result;
|
|
168
|
+
for (const entry of fs.readdirSync(memoriesRoot, { withFileTypes: true })) {
|
|
169
|
+
if (!entry.isDirectory()) continue;
|
|
170
|
+
const dir = path.join(memoriesRoot, entry.name);
|
|
171
|
+
for (const name of fs.readdirSync(dir)) {
|
|
172
|
+
if (!name.endsWith(".md")) continue;
|
|
173
|
+
const filePath = path.join(dir, name);
|
|
174
|
+
result.scanned++;
|
|
175
|
+
const plan = planConversion(filePath, memoriesRoot);
|
|
176
|
+
if (!plan) {
|
|
177
|
+
result.skippedV2++;
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
if (!opts.dryRun) {
|
|
181
|
+
fs.mkdirSync(path.dirname(plan.toPath), { recursive: true });
|
|
182
|
+
fs.writeFileSync(plan.toPath, serialize(plan.fm, plan.body), "utf8");
|
|
183
|
+
if (plan.toPath !== plan.fromPath) fs.unlinkSync(plan.fromPath);
|
|
184
|
+
}
|
|
185
|
+
result.converted++;
|
|
186
|
+
result.plans.push(plan);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
return result;
|
|
190
|
+
}
|
package/src/tools/memory.ts
CHANGED
|
@@ -1,174 +1,60 @@
|
|
|
1
1
|
import { tool } from "@opencode-ai/plugin/tool";
|
|
2
2
|
import type { Scope } from "../scope.ts";
|
|
3
3
|
import type { MyOMemoryConfig } from "../config.ts";
|
|
4
|
-
import { USER_SCOPE } from "../scope.ts";
|
|
5
|
-
import { search, list } from "../retrieve/search.ts";
|
|
6
4
|
import {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
const MEMORY_TYPES = [
|
|
20
|
-
"note",
|
|
21
|
-
"preference",
|
|
22
|
-
"project-config",
|
|
23
|
-
"architecture",
|
|
24
|
-
"error-solution",
|
|
25
|
-
"learned-pattern",
|
|
26
|
-
"conversation",
|
|
27
|
-
] as const;
|
|
5
|
+
addMemory,
|
|
6
|
+
searchMemories,
|
|
7
|
+
listMemories,
|
|
8
|
+
supersedeMemory,
|
|
9
|
+
forgetMemory,
|
|
10
|
+
memoryAddArgs,
|
|
11
|
+
memorySearchArgs,
|
|
12
|
+
memoryListArgs,
|
|
13
|
+
memorySupersedeArgs,
|
|
14
|
+
memoryForgetArgs,
|
|
15
|
+
TOOL_DESCRIPTIONS,
|
|
16
|
+
} from "./ops.ts";
|
|
28
17
|
|
|
29
18
|
export function makeTools(getScope: () => Scope, cfg: MyOMemoryConfig) {
|
|
30
|
-
const scopeArg = z
|
|
31
|
-
.enum(["project", "user"])
|
|
32
|
-
.optional()
|
|
33
|
-
.describe(
|
|
34
|
-
"Memory scope. `project` = tied to this repo. `user` = global across all your projects. Default: project.",
|
|
35
|
-
);
|
|
36
|
-
|
|
37
|
-
function resolveScope(kind?: "project" | "user"): Scope {
|
|
38
|
-
return kind === "user" ? USER_SCOPE : getScope();
|
|
39
|
-
}
|
|
40
|
-
|
|
41
19
|
const memory_add = tool({
|
|
42
|
-
description:
|
|
43
|
-
|
|
44
|
-
args: {
|
|
45
|
-
content: z.string().min(1).describe("The fact to remember. One idea per memory."),
|
|
46
|
-
type: z
|
|
47
|
-
.enum(MEMORY_TYPES)
|
|
48
|
-
.optional()
|
|
49
|
-
.describe("Category of memory. Default: note."),
|
|
50
|
-
scope: scopeArg,
|
|
51
|
-
tags: z.array(z.string()).optional().describe("Optional tags for filtering."),
|
|
52
|
-
},
|
|
20
|
+
description: TOOL_DESCRIPTIONS.memory_add,
|
|
21
|
+
args: memoryAddArgs,
|
|
53
22
|
async execute(args) {
|
|
54
|
-
|
|
55
|
-
args.content,
|
|
56
|
-
cfg.redactPatterns,
|
|
57
|
-
);
|
|
58
|
-
if (hadSecret) {
|
|
59
|
-
return {
|
|
60
|
-
title: "memory: rejected (secret detected)",
|
|
61
|
-
output: `Refused to save: content matched a secret pattern (${matchedPattern}). Wrap the sensitive part in <private>...</private> tags or paraphrase, then try again.`,
|
|
62
|
-
};
|
|
63
|
-
}
|
|
64
|
-
const s = resolveScope(args.scope);
|
|
65
|
-
const now = Date.now();
|
|
66
|
-
const fm: Frontmatter = {
|
|
67
|
-
id: ulid(),
|
|
68
|
-
scope_key: s.key,
|
|
69
|
-
scope_kind: s.kind,
|
|
70
|
-
project_name: s.projectName,
|
|
71
|
-
type: args.type ?? "note",
|
|
72
|
-
tags: args.tags ?? [],
|
|
73
|
-
source: "tool",
|
|
74
|
-
created_at: now,
|
|
75
|
-
updated_at: now,
|
|
76
|
-
};
|
|
77
|
-
const { filePath } = writeMemoryFile(fm, redacted);
|
|
78
|
-
const mf = readMemoryFile(filePath);
|
|
79
|
-
if (mf) upsertFromFile(mf);
|
|
80
|
-
return {
|
|
81
|
-
title: `memory: saved ${fm.id}`,
|
|
82
|
-
output: `Saved to ${s.key} as ${fm.type}. id=${fm.id}`,
|
|
83
|
-
};
|
|
23
|
+
return addMemory(getScope, cfg, args);
|
|
84
24
|
},
|
|
85
25
|
});
|
|
86
26
|
|
|
87
27
|
const memory_search = tool({
|
|
88
|
-
description:
|
|
89
|
-
|
|
90
|
-
args: {
|
|
91
|
-
query: z
|
|
92
|
-
.string()
|
|
93
|
-
.min(1)
|
|
94
|
-
.describe("Free-text query. File paths, error strings, identifiers work well."),
|
|
95
|
-
scope: z
|
|
96
|
-
.enum(["project", "user", "both"])
|
|
97
|
-
.optional()
|
|
98
|
-
.describe("Which scope(s) to search. Default: both."),
|
|
99
|
-
type: z.string().optional().describe("Restrict to memories of this type."),
|
|
100
|
-
limit: z.number().int().min(1).max(50).optional(),
|
|
101
|
-
},
|
|
28
|
+
description: TOOL_DESCRIPTIONS.memory_search,
|
|
29
|
+
args: memorySearchArgs,
|
|
102
30
|
async execute(args) {
|
|
103
|
-
|
|
104
|
-
const scopeKeys =
|
|
105
|
-
args.scope === "user"
|
|
106
|
-
? [USER_SCOPE.key]
|
|
107
|
-
: args.scope === "project"
|
|
108
|
-
? [project.key]
|
|
109
|
-
: [project.key, USER_SCOPE.key];
|
|
110
|
-
const hits = search(args.query, {
|
|
111
|
-
scopeKeys,
|
|
112
|
-
limit: args.limit,
|
|
113
|
-
type: args.type,
|
|
114
|
-
});
|
|
115
|
-
if (hits.length === 0) {
|
|
116
|
-
return {
|
|
117
|
-
title: "memory: 0 results",
|
|
118
|
-
output: `No memories matched "${args.query}".`,
|
|
119
|
-
};
|
|
120
|
-
}
|
|
121
|
-
const lines = hits.map(
|
|
122
|
-
(h) =>
|
|
123
|
-
`- [${h.scope_key === USER_SCOPE.key ? "user" : "project"}/${h.type}] id=${h.id}\n ${h.snippet.replace(/\s+/g, " ").trim()}`,
|
|
124
|
-
);
|
|
125
|
-
return {
|
|
126
|
-
title: `memory: ${hits.length} result(s)`,
|
|
127
|
-
output: lines.join("\n"),
|
|
128
|
-
};
|
|
31
|
+
return searchMemories(getScope, args);
|
|
129
32
|
},
|
|
130
33
|
});
|
|
131
34
|
|
|
132
35
|
const memory_list = tool({
|
|
133
|
-
description:
|
|
134
|
-
args:
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
limit: z.number().int().min(1).max(100).optional(),
|
|
36
|
+
description: TOOL_DESCRIPTIONS.memory_list,
|
|
37
|
+
args: memoryListArgs,
|
|
38
|
+
async execute(args) {
|
|
39
|
+
return listMemories(getScope, args);
|
|
138
40
|
},
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const memory_supersede = tool({
|
|
44
|
+
description: TOOL_DESCRIPTIONS.memory_supersede,
|
|
45
|
+
args: memorySupersedeArgs,
|
|
139
46
|
async execute(args) {
|
|
140
|
-
|
|
141
|
-
const hits = list(s.key, { type: args.type, limit: args.limit });
|
|
142
|
-
if (hits.length === 0) {
|
|
143
|
-
return { title: "memory: empty", output: `No memories in scope ${s.key}.` };
|
|
144
|
-
}
|
|
145
|
-
const lines = hits.map(
|
|
146
|
-
(h) => `- [${h.type}] id=${h.id} — ${h.snippet.replace(/\s+/g, " ").trim()}`,
|
|
147
|
-
);
|
|
148
|
-
return {
|
|
149
|
-
title: `memory: ${hits.length} in ${s.key}`,
|
|
150
|
-
output: lines.join("\n"),
|
|
151
|
-
};
|
|
47
|
+
return supersedeMemory(cfg, args);
|
|
152
48
|
},
|
|
153
49
|
});
|
|
154
50
|
|
|
155
51
|
const memory_forget = tool({
|
|
156
|
-
description:
|
|
157
|
-
args:
|
|
158
|
-
id: z.string().min(1),
|
|
159
|
-
},
|
|
52
|
+
description: TOOL_DESCRIPTIONS.memory_forget,
|
|
53
|
+
args: memoryForgetArgs,
|
|
160
54
|
async execute(args) {
|
|
161
|
-
|
|
162
|
-
.prepare(`SELECT scope_key FROM memories WHERE id = ?`)
|
|
163
|
-
.get(args.id) as { scope_key: string } | undefined;
|
|
164
|
-
if (!row) {
|
|
165
|
-
return { title: "memory: not found", output: `No memory with id ${args.id}.` };
|
|
166
|
-
}
|
|
167
|
-
deleteMemoryFile(row.scope_key, args.id);
|
|
168
|
-
deleteFromIndex(args.id);
|
|
169
|
-
return { title: "memory: forgotten", output: `Deleted ${args.id}.` };
|
|
55
|
+
return forgetMemory(args);
|
|
170
56
|
},
|
|
171
57
|
});
|
|
172
58
|
|
|
173
|
-
return { memory_add, memory_search, memory_list, memory_forget };
|
|
59
|
+
return { memory_add, memory_search, memory_list, memory_forget, memory_supersede };
|
|
174
60
|
}
|