@y0shua1ee/dsh-auto-memory 0.3.2 → 0.4.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/lib/tool.d.ts ADDED
@@ -0,0 +1,63 @@
1
+ import { MemoryStore } from "./store.js";
2
+ import type { ListEntry } from "./store.js";
3
+ import type { Agent, TextContent, ToolExec } from "./types.js";
4
+ declare const COMMANDS: readonly ["view", "create", "str_replace", "insert", "delete", "rename"];
5
+ type MemoryCommand = (typeof COMMANDS)[number];
6
+ /** A project resolved for the calling session. */
7
+ export interface MemoryProject {
8
+ store: MemoryStore;
9
+ }
10
+ export type ProjectFor = (agent: Agent, signal?: AbortSignal) => Promise<MemoryProject>;
11
+ export interface MemoryToolArgs {
12
+ command: MemoryCommand;
13
+ path?: unknown;
14
+ file_text?: unknown;
15
+ old_str?: unknown;
16
+ new_str?: unknown;
17
+ insert_line?: unknown;
18
+ insert_text?: unknown;
19
+ new_path?: unknown;
20
+ numbered?: unknown;
21
+ }
22
+ export interface MemoryToolResult {
23
+ ok: true;
24
+ output: string;
25
+ path?: string;
26
+ changed?: boolean;
27
+ bytes?: number;
28
+ lines?: number;
29
+ entries?: ListEntry[];
30
+ }
31
+ interface ToolParameter {
32
+ type: string;
33
+ required?: boolean;
34
+ enum?: readonly string[];
35
+ description?: string;
36
+ }
37
+ export interface MemoryToolOptions {
38
+ name: string;
39
+ description: string;
40
+ parameters: Record<string, ToolParameter>;
41
+ output: {
42
+ schema: Record<string, unknown>;
43
+ render: (args: MemoryToolArgs, value: MemoryToolResult) => TextContent[];
44
+ };
45
+ presentCall: (args: MemoryToolArgs) => {
46
+ card: string;
47
+ title: string;
48
+ kind: string;
49
+ rawInput: string;
50
+ };
51
+ execute: (args: Record<string, unknown>, exec?: ToolExec) => Promise<MemoryToolResult>;
52
+ }
53
+ /**
54
+ * Build the `memory` tool definition options.
55
+ *
56
+ * The returned object is passed to `defineTool` by the plugin entry. Keeping it
57
+ * free of `defineTool` imports lets the command semantics be tested without a
58
+ * running harness.
59
+ */
60
+ export declare function buildMemoryToolOptions({ projectFor }: {
61
+ projectFor: ProjectFor;
62
+ }): MemoryToolOptions;
63
+ export {};
package/lib/tool.js CHANGED
@@ -1,253 +1,253 @@
1
1
  import { INDEX_FILE, MemoryError, MemoryStore, VIRTUAL_ROOT } from "./store.js";
2
-
3
2
  const COMMANDS = ["view", "create", "str_replace", "insert", "delete", "rename"];
4
-
5
3
  function formatBytes(bytes) {
6
- if (bytes < 1024) return `${bytes} B`;
7
- if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
8
- return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
4
+ if (bytes < 1024)
5
+ return `${bytes} B`;
6
+ if (bytes < 1024 * 1024)
7
+ return `${(bytes / 1024).toFixed(1)} KB`;
8
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
9
9
  }
10
-
11
10
  function requireString(value, name) {
12
- if (typeof value !== "string") {
13
- throw new MemoryError(`${name} is required and must be a string`, "MEMORY_INVALID_ARGUMENT");
14
- }
15
- return value;
11
+ if (typeof value !== "string") {
12
+ throw new MemoryError(`${name} is required and must be a string`, "MEMORY_INVALID_ARGUMENT");
13
+ }
14
+ return value;
16
15
  }
17
-
18
16
  function renderListing(relativePath, entries) {
19
- if (entries.length === 0) {
20
- return relativePath.length === 0
21
- ? `${VIRTUAL_ROOT} is empty. Create a memory file with memory create; it is indexed in ${VIRTUAL_ROOT}/${INDEX_FILE} automatically.`
22
- : `${VIRTUAL_ROOT}/${relativePath} is empty.`;
23
- }
24
- const files = entries.filter((entry) => entry.kind === "file");
25
- const bytes = files.reduce((total, entry) => total + entry.bytes, 0);
26
- const heading = relativePath.length === 0 ? VIRTUAL_ROOT : `${VIRTUAL_ROOT}/${relativePath}`;
27
- const lines = [`${heading} (${files.length} ${files.length === 1 ? "file" : "files"}, ${formatBytes(bytes)})`];
28
- for (const entry of entries) {
29
- const suffix = entry.kind === "directory" ? "/" : ` (${formatBytes(entry.bytes)})`;
30
- lines.push(`- ${entry.path}${suffix}`);
31
- }
32
- return lines.join("\n");
17
+ if (entries.length === 0) {
18
+ return relativePath.length === 0
19
+ ? `${VIRTUAL_ROOT} is empty. Create a memory file with memory create; it is indexed in ${VIRTUAL_ROOT}/${INDEX_FILE} automatically.`
20
+ : `${VIRTUAL_ROOT}/${relativePath} is empty.`;
21
+ }
22
+ const files = entries.filter((entry) => entry.kind === "file");
23
+ const bytes = files.reduce((total, entry) => total + entry.bytes, 0);
24
+ const heading = relativePath.length === 0 ? VIRTUAL_ROOT : `${VIRTUAL_ROOT}/${relativePath}`;
25
+ const lines = [`${heading} (${files.length} ${files.length === 1 ? "file" : "files"}, ${formatBytes(bytes)})`];
26
+ for (const entry of entries) {
27
+ const suffix = entry.kind === "directory" ? "/" : ` (${formatBytes(entry.bytes)})`;
28
+ lines.push(`- ${entry.path}${suffix}`);
29
+ }
30
+ return lines.join("\n");
33
31
  }
34
-
35
32
  function indexNote({ indexed, indexWarning, action }) {
36
- if (indexWarning !== undefined) return indexWarning;
37
- if (indexed) return `Updated MEMORY.md (${action}).`;
38
- return `MEMORY.md already links this file (${action}).`;
33
+ if (indexWarning !== undefined)
34
+ return indexWarning;
35
+ if (indexed)
36
+ return `Updated MEMORY.md (${action}).`;
37
+ return `MEMORY.md already links this file (${action}).`;
39
38
  }
40
-
41
39
  function frontmatterNote(problems) {
42
- return problems !== undefined && problems.length > 0
43
- ? [`Frontmatter: ${problems.join("; ")}. Fix it with str_replace, or rename the file when the name and path disagree.`]
44
- : [];
40
+ return problems !== undefined && problems.length > 0
41
+ ? [`Frontmatter: ${problems.join("; ")}. Fix it with str_replace, or rename the file when the name and path disagree.`]
42
+ : [];
45
43
  }
46
-
47
44
  function editIndexNote({ indexed, indexWarning }) {
48
- if (indexWarning !== undefined) return [indexWarning];
49
- return indexed ? ["Updated its MEMORY.md line to match the new frontmatter."] : [];
45
+ if (indexWarning !== undefined)
46
+ return [indexWarning];
47
+ return indexed ? ["Updated its MEMORY.md line to match the new frontmatter."] : [];
48
+ }
49
+ function budgetNote(warning) {
50
+ return warning !== undefined && warning.length > 0 ? [warning] : [];
50
51
  }
51
-
52
52
  async function viewToolOutput(store, relativePath, numbered) {
53
- const result = await store.view(relativePath);
54
- if (result.kind === "directory") {
53
+ const result = await store.view(relativePath);
54
+ if (result.kind === "directory") {
55
+ return {
56
+ ok: true,
57
+ output: renderListing(relativePath, result.entries),
58
+ path: relativePath.length === 0 ? VIRTUAL_ROOT : `${VIRTUAL_ROOT}/${relativePath}`,
59
+ entries: result.entries
60
+ };
61
+ }
62
+ const output = numbered
63
+ ? result.content
64
+ .split("\n")
65
+ .map((line, index) => `${String(index + 1).padStart(4, " ")}: ${line}`)
66
+ .join("\n")
67
+ : result.content;
55
68
  return {
56
- ok: true,
57
- output: renderListing(relativePath, result.entries),
58
- path: relativePath.length === 0 ? VIRTUAL_ROOT : `${VIRTUAL_ROOT}/${relativePath}`,
59
- entries: result.entries
69
+ ok: true,
70
+ output,
71
+ path: `${VIRTUAL_ROOT}/${relativePath}`,
72
+ bytes: result.bytes,
73
+ lines: result.lines
60
74
  };
61
- }
62
- const output = numbered
63
- ? result.content
64
- .split("\n")
65
- .map((line, index) => `${String(index + 1).padStart(4, " ")}: ${line}`)
66
- .join("\n")
67
- : result.content;
68
- return {
69
- ok: true,
70
- output,
71
- path: `${VIRTUAL_ROOT}/${relativePath}`,
72
- bytes: result.bytes,
73
- lines: result.lines
74
- };
75
75
  }
76
-
77
76
  /**
78
77
  * Build the `memory` tool definition options.
79
78
  *
80
79
  * The returned object is passed to `defineTool` by the plugin entry. Keeping it
81
80
  * free of `defineTool` imports lets the command semantics be tested without a
82
81
  * running harness.
83
- *
84
- * @param {object} input - tool wiring.
85
- * @param {(agent: object, signal: AbortSignal) => Promise<object>} input.projectFor - resolve the calling session's project.
86
- * @returns {object} tool definition options.
87
82
  */
88
83
  export function buildMemoryToolOptions({ projectFor }) {
89
- return {
90
- name: "memory",
91
- description:
92
- "Read and maintain this project's persistent memory, stored outside the repository under ~/.dsh/auto-memory. " +
93
- "Paths are virtual: /memories is the directory and /memories/MEMORY.md is the index loaded into future sessions. " +
94
- "Use view to list or read, create for a new .md file, str_replace to change exactly one occurrence, insert to add lines, delete to remove a file, and rename to move one. " +
95
- "Each memory file holds one fact, is named <name>.md, and starts with frontmatter: name, description, and metadata.type (user, feedback, project, or reference). " +
96
- "Save durable preferences, corrections, decisions, constraints, and pitfalls; never save secrets or transient task state.",
97
- parameters: {
98
- command: {
99
- type: "string",
100
- required: true,
101
- enum: COMMANDS,
102
- description: "Operation to perform."
103
- },
104
- path: {
105
- type: "string",
106
- description: `Virtual path such as ${VIRTUAL_ROOT}/${INDEX_FILE} or ${VIRTUAL_ROOT}/prefer-pnpm.md. Required for every command except a root view.`
107
- },
108
- file_text: {
109
- type: "string",
110
- description:
111
- "For create: the complete file content. A memory file starts with ---, name: <kebab-case>, description: <one line>, metadata:, type: <user|feedback|project|reference> (indented), ---, then the body. Creating MEMORY.md with empty text writes the standard index header."
112
- },
113
- old_str: {
114
- type: "string",
115
- description: "For str_replace: text that must occur exactly once in the file."
116
- },
117
- new_str: {
118
- type: "string",
119
- description: "For str_replace: replacement text; omit or pass an empty string to delete the matched text."
120
- },
121
- insert_line: {
122
- type: "number",
123
- description: "For insert: zero-based line index at which to insert; 0 inserts before the first line."
124
- },
125
- insert_text: {
126
- type: "string",
127
- description: "For insert: text to insert as one or more lines."
128
- },
129
- new_path: {
130
- type: "string",
131
- description: "For rename: the destination virtual path."
132
- },
133
- numbered: {
134
- type: "boolean",
135
- description: "For view of a file: prefix each line with its 1-based line number."
136
- }
137
- },
138
- output: {
139
- schema: {
140
- type: "object",
141
- additionalProperties: false,
142
- properties: {
143
- ok: { type: "boolean", required: true },
144
- output: { type: "string", required: true },
145
- path: { type: "string" },
146
- changed: { type: "boolean" },
147
- bytes: { type: "number" },
148
- lines: { type: "number" },
149
- entries: {
150
- type: "array",
151
- items: {
152
- type: "object",
153
- additionalProperties: false,
154
- properties: {
155
- path: { type: "string", required: true },
156
- kind: { type: "string", required: true, enum: ["file", "directory"] },
157
- bytes: { type: "number", required: true }
158
- }
84
+ return {
85
+ name: "memory",
86
+ description: "Read and maintain this project's persistent memory, stored outside the repository under ~/.dsh/auto-memory. " +
87
+ "Paths are virtual: /memories is the directory and /memories/MEMORY.md is the index loaded into future sessions. " +
88
+ "Use view to list or read, create for a new .md file, str_replace to change exactly one occurrence, insert to add lines, delete to remove a file, and rename to move one. " +
89
+ "Each memory file holds one fact, is named <name>.md, and starts with frontmatter: name, description, and metadata.type (user, feedback, project, or reference). " +
90
+ "Save durable preferences, corrections, decisions, constraints, and pitfalls; never save secrets or transient task state.",
91
+ parameters: {
92
+ command: {
93
+ type: "string",
94
+ required: true,
95
+ enum: COMMANDS,
96
+ description: "Operation to perform."
97
+ },
98
+ path: {
99
+ type: "string",
100
+ description: `Virtual path such as ${VIRTUAL_ROOT}/${INDEX_FILE} or ${VIRTUAL_ROOT}/prefer-pnpm.md. Required for every command except a root view.`
101
+ },
102
+ file_text: {
103
+ type: "string",
104
+ description: "For create: the complete file content. A memory file starts with ---, name: <kebab-case>, description: <one line>, metadata:, type: <user|feedback|project|reference> (indented), ---, then the body. Creating MEMORY.md with empty text writes the standard index header."
105
+ },
106
+ old_str: {
107
+ type: "string",
108
+ description: "For str_replace: text that must occur exactly once in the file."
109
+ },
110
+ new_str: {
111
+ type: "string",
112
+ description: "For str_replace: replacement text; omit or pass an empty string to delete the matched text."
113
+ },
114
+ insert_line: {
115
+ type: "number",
116
+ description: "For insert: zero-based line index at which to insert; 0 inserts before the first line."
117
+ },
118
+ insert_text: {
119
+ type: "string",
120
+ description: "For insert: text to insert as one or more lines."
121
+ },
122
+ new_path: {
123
+ type: "string",
124
+ description: "For rename: the destination virtual path."
125
+ },
126
+ numbered: {
127
+ type: "boolean",
128
+ description: "For view of a file: prefix each line with its 1-based line number."
159
129
  }
160
- }
161
- }
162
- },
163
- render: (_args, value) => [{ type: "text", text: value.output }]
164
- },
165
- presentCall: (args) => ({
166
- card: "generic",
167
- title: args.command === "view" ? "Read project memory" : `${args.command} project memory`,
168
- kind: args.command === "view" ? "read" : "other",
169
- rawInput: args.path ?? VIRTUAL_ROOT
170
- }),
171
- execute: async (args, exec) => {
172
- const agent = exec?.agent;
173
- if (agent === undefined) {
174
- throw new MemoryError("memory requires a calling agent", "MEMORY_NO_AGENT");
175
- }
176
- const project = await projectFor(agent, exec?.signal);
177
- const store = project.store;
178
- const command = args.command;
179
- if (command === "view") {
180
- return viewToolOutput(store, MemoryStore.parseVirtualPath(args.path ?? VIRTUAL_ROOT), args.numbered === true);
181
- }
182
- const relativePath = MemoryStore.parseVirtualPath(requireString(args.path, "path"));
183
- if (command === "create") {
184
- const fileText = args.file_text === undefined ? "" : requireString(args.file_text, "file_text");
185
- const result = await store.create(relativePath, fileText);
186
- const output = [
187
- `Created ${result.path} (${formatBytes(result.bytes)}, ${result.lines} lines).`,
188
- result.indexWarning ??
189
- (relativePath === INDEX_FILE
190
- ? "This is the index injected into future sessions."
191
- : indexNote({ ...result, action: "created" })),
192
- ...frontmatterNote(result.frontmatterProblems)
193
- ].join("\n");
194
- return { ok: true, output, path: result.path, changed: true, bytes: result.bytes, lines: result.lines };
195
- }
196
- if (command === "str_replace") {
197
- const oldStr = requireString(args.old_str, "old_str");
198
- const newStr = args.new_str === undefined ? "" : requireString(args.new_str, "new_str");
199
- const result = await store.strReplace(relativePath, oldStr, newStr);
200
- return {
201
- ok: true,
202
- output: [
203
- `Edited ${result.path} (${formatBytes(result.bytes)}, ${result.lines} lines).`,
204
- ...editIndexNote(result),
205
- ...frontmatterNote(result.frontmatterProblems)
206
- ].join("\n"),
207
- path: result.path,
208
- changed: true,
209
- bytes: result.bytes,
210
- lines: result.lines
211
- };
212
- }
213
- if (command === "insert") {
214
- if (!Number.isSafeInteger(args.insert_line)) {
215
- throw new MemoryError("insert_line must be a non-negative integer", "MEMORY_INVALID_ARGUMENT");
130
+ },
131
+ output: {
132
+ schema: {
133
+ type: "object",
134
+ additionalProperties: false,
135
+ properties: {
136
+ ok: { type: "boolean", required: true },
137
+ output: { type: "string", required: true },
138
+ path: { type: "string" },
139
+ changed: { type: "boolean" },
140
+ bytes: { type: "number" },
141
+ lines: { type: "number" },
142
+ entries: {
143
+ type: "array",
144
+ items: {
145
+ type: "object",
146
+ additionalProperties: false,
147
+ properties: {
148
+ path: { type: "string", required: true },
149
+ kind: { type: "string", required: true, enum: ["file", "directory"] },
150
+ bytes: { type: "number", required: true }
151
+ }
152
+ }
153
+ }
154
+ }
155
+ },
156
+ render: (_args, value) => [
157
+ { type: "text", text: value.output }
158
+ ]
159
+ },
160
+ presentCall: (args) => ({
161
+ card: "generic",
162
+ title: args.command === "view" ? "Read project memory" : `${args.command} project memory`,
163
+ kind: args.command === "view" ? "read" : "other",
164
+ rawInput: typeof args.path === "string" ? args.path : VIRTUAL_ROOT
165
+ }),
166
+ execute: async (args, exec) => {
167
+ const agent = exec?.agent;
168
+ if (agent === undefined) {
169
+ throw new MemoryError("memory requires a calling agent", "MEMORY_NO_AGENT");
170
+ }
171
+ const project = await projectFor(agent, exec?.signal);
172
+ const store = project.store;
173
+ const command = args.command;
174
+ if (command === "view") {
175
+ return viewToolOutput(store, MemoryStore.parseVirtualPath(args.path ?? VIRTUAL_ROOT), args.numbered === true);
176
+ }
177
+ const relativePath = MemoryStore.parseVirtualPath(requireString(args.path, "path"));
178
+ if (command === "create") {
179
+ const fileText = args.file_text === undefined ? "" : requireString(args.file_text, "file_text");
180
+ const result = await store.create(relativePath, fileText);
181
+ const output = [
182
+ `Created ${result.path} (${formatBytes(result.bytes)}, ${result.lines} lines).`,
183
+ result.indexWarning ??
184
+ (relativePath === INDEX_FILE
185
+ ? "This is the index injected into future sessions."
186
+ : indexNote({ ...result, action: "created" })),
187
+ ...frontmatterNote(result.frontmatterProblems),
188
+ ...budgetNote(result.indexBudgetWarning)
189
+ ].join("\n");
190
+ return { ok: true, output, path: result.path, changed: true, bytes: result.bytes, lines: result.lines };
191
+ }
192
+ if (command === "str_replace") {
193
+ const oldStr = requireString(args.old_str, "old_str");
194
+ const newStr = args.new_str === undefined ? "" : requireString(args.new_str, "new_str");
195
+ const result = await store.strReplace(relativePath, oldStr, newStr);
196
+ return {
197
+ ok: true,
198
+ output: [
199
+ `Edited ${result.path} (${formatBytes(result.bytes)}, ${result.lines} lines).`,
200
+ ...editIndexNote(result),
201
+ ...frontmatterNote(result.frontmatterProblems),
202
+ ...budgetNote(result.indexBudgetWarning)
203
+ ].join("\n"),
204
+ path: result.path,
205
+ changed: true,
206
+ bytes: result.bytes,
207
+ lines: result.lines
208
+ };
209
+ }
210
+ if (command === "insert") {
211
+ if (!Number.isSafeInteger(args.insert_line)) {
212
+ throw new MemoryError("insert_line must be a non-negative integer", "MEMORY_INVALID_ARGUMENT");
213
+ }
214
+ const insertText = requireString(args.insert_text, "insert_text");
215
+ const result = await store.insert(relativePath, args.insert_line, insertText);
216
+ return {
217
+ ok: true,
218
+ output: [
219
+ `Inserted text at line ${result.insertedAt} in ${result.path} (${formatBytes(result.bytes)}, ${result.lines} lines).`,
220
+ ...editIndexNote(result),
221
+ ...frontmatterNote(result.frontmatterProblems),
222
+ ...budgetNote(result.indexBudgetWarning)
223
+ ].join("\n"),
224
+ path: result.path,
225
+ changed: true,
226
+ bytes: result.bytes,
227
+ lines: result.lines
228
+ };
229
+ }
230
+ if (command === "delete") {
231
+ const result = await store.delete(relativePath);
232
+ const output = [
233
+ `Deleted ${result.path}.`,
234
+ result.indexWarning ?? (result.indexed ? "Removed its MEMORY.md index line." : "No MEMORY.md index line referenced it."),
235
+ ...budgetNote(result.indexBudgetWarning)
236
+ ].join("\n");
237
+ return { ok: true, output, path: result.path, changed: true };
238
+ }
239
+ if (command === "rename") {
240
+ const newPath = requireString(args.new_path, "new_path");
241
+ const result = await store.rename(relativePath, MemoryStore.parseVirtualPath(newPath));
242
+ const output = [
243
+ `Renamed ${result.previousPath} to ${result.path}.`,
244
+ result.indexWarning ?? (result.indexed ? "Updated its MEMORY.md link." : "No MEMORY.md link referenced it."),
245
+ ...frontmatterNote(result.frontmatterProblems),
246
+ ...budgetNote(result.indexBudgetWarning)
247
+ ].join("\n");
248
+ return { ok: true, output, path: result.path, changed: true };
249
+ }
250
+ throw new MemoryError(`unknown command ${JSON.stringify(command)}`, "MEMORY_INVALID_ARGUMENT");
216
251
  }
217
- const insertText = requireString(args.insert_text, "insert_text");
218
- const result = await store.insert(relativePath, args.insert_line, insertText);
219
- return {
220
- ok: true,
221
- output: [
222
- `Inserted text at line ${result.insertedAt} in ${result.path} (${formatBytes(result.bytes)}, ${result.lines} lines).`,
223
- ...editIndexNote(result),
224
- ...frontmatterNote(result.frontmatterProblems)
225
- ].join("\n"),
226
- path: result.path,
227
- changed: true,
228
- bytes: result.bytes,
229
- lines: result.lines
230
- };
231
- }
232
- if (command === "delete") {
233
- const result = await store.delete(relativePath);
234
- const output = [
235
- `Deleted ${result.path}.`,
236
- result.indexWarning ?? (result.indexed ? "Removed its MEMORY.md index line." : "No MEMORY.md index line referenced it.")
237
- ].join("\n");
238
- return { ok: true, output, path: result.path, changed: true };
239
- }
240
- if (command === "rename") {
241
- const newPath = requireString(args.new_path, "new_path");
242
- const result = await store.rename(relativePath, MemoryStore.parseVirtualPath(newPath));
243
- const output = [
244
- `Renamed ${result.previousPath} to ${result.path}.`,
245
- result.indexWarning ?? (result.indexed ? "Updated its MEMORY.md link." : "No MEMORY.md link referenced it."),
246
- ...frontmatterNote(result.frontmatterProblems)
247
- ].join("\n");
248
- return { ok: true, output, path: result.path, changed: true };
249
- }
250
- throw new MemoryError(`unknown command ${JSON.stringify(command)}`, "MEMORY_INVALID_ARGUMENT");
251
- }
252
- };
252
+ };
253
253
  }