@openacme/memory 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/LICENSE +21 -0
- package/dist/freshness.d.ts +14 -0
- package/dist/freshness.d.ts.map +1 -0
- package/dist/freshness.js +37 -0
- package/dist/freshness.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -0
- package/dist/scan.d.ts +29 -0
- package/dist/scan.d.ts.map +1 -0
- package/dist/scan.js +108 -0
- package/dist/scan.js.map +1 -0
- package/dist/store.d.ts +153 -0
- package/dist/store.d.ts.map +1 -0
- package/dist/store.js +620 -0
- package/dist/store.js.map +1 -0
- package/dist/threat-patterns.d.ts +11 -0
- package/dist/threat-patterns.d.ts.map +1 -0
- package/dist/threat-patterns.js +41 -0
- package/dist/threat-patterns.js.map +1 -0
- package/dist/threat-scanner.d.ts +21 -0
- package/dist/threat-scanner.d.ts.map +1 -0
- package/dist/threat-scanner.js +35 -0
- package/dist/threat-scanner.js.map +1 -0
- package/package.json +44 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Utkarsh Kanwat
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stale-memory mitigation. CC `memdir/memoryAge.ts` lift. Human age
|
|
3
|
+
* strings (not ISO) because models are poor at date arithmetic — "47
|
|
4
|
+
* days ago" triggers staleness reasoning that a raw timestamp doesn't.
|
|
5
|
+
*/
|
|
6
|
+
/** Floor-rounded days since mtime. Future mtimes clamp to 0. */
|
|
7
|
+
export declare function memoryAgeDays(mtimeMs: number): number;
|
|
8
|
+
export declare function memoryAge(mtimeMs: number): string;
|
|
9
|
+
/** Plain-text caveat for entries >1 day old. Recall blocks use this
|
|
10
|
+
* inside their own wrapper; standalone callers want `memoryFreshnessNote`. */
|
|
11
|
+
export declare function memoryFreshnessText(mtimeMs: number): string;
|
|
12
|
+
/** Wrapped variant for `view` of standalone entry files. */
|
|
13
|
+
export declare function memoryFreshnessNote(mtimeMs: number): string;
|
|
14
|
+
//# sourceMappingURL=freshness.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"freshness.d.ts","sourceRoot":"","sources":["../src/freshness.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAIH,gEAAgE;AAChE,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAErD;AAED,wBAAgB,SAAS,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAKjD;AAED;+EAC+E;AAC/E,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAS3D;AAED,4DAA4D;AAC5D,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAI3D"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stale-memory mitigation. CC `memdir/memoryAge.ts` lift. Human age
|
|
3
|
+
* strings (not ISO) because models are poor at date arithmetic — "47
|
|
4
|
+
* days ago" triggers staleness reasoning that a raw timestamp doesn't.
|
|
5
|
+
*/
|
|
6
|
+
const ONE_DAY_MS = 86_400_000;
|
|
7
|
+
/** Floor-rounded days since mtime. Future mtimes clamp to 0. */
|
|
8
|
+
export function memoryAgeDays(mtimeMs) {
|
|
9
|
+
return Math.max(0, Math.floor((Date.now() - mtimeMs) / ONE_DAY_MS));
|
|
10
|
+
}
|
|
11
|
+
export function memoryAge(mtimeMs) {
|
|
12
|
+
const d = memoryAgeDays(mtimeMs);
|
|
13
|
+
if (d === 0)
|
|
14
|
+
return "today";
|
|
15
|
+
if (d === 1)
|
|
16
|
+
return "yesterday";
|
|
17
|
+
return `${d} days ago`;
|
|
18
|
+
}
|
|
19
|
+
/** Plain-text caveat for entries >1 day old. Recall blocks use this
|
|
20
|
+
* inside their own wrapper; standalone callers want `memoryFreshnessNote`. */
|
|
21
|
+
export function memoryFreshnessText(mtimeMs) {
|
|
22
|
+
const d = memoryAgeDays(mtimeMs);
|
|
23
|
+
if (d <= 1)
|
|
24
|
+
return "";
|
|
25
|
+
return (`This memory is ${d} days old. ` +
|
|
26
|
+
`Memories are point-in-time observations, not live state — ` +
|
|
27
|
+
`claims about code behavior or file:line citations may be outdated. ` +
|
|
28
|
+
`Verify against current code before asserting as fact.`);
|
|
29
|
+
}
|
|
30
|
+
/** Wrapped variant for `view` of standalone entry files. */
|
|
31
|
+
export function memoryFreshnessNote(mtimeMs) {
|
|
32
|
+
const text = memoryFreshnessText(mtimeMs);
|
|
33
|
+
if (!text)
|
|
34
|
+
return "";
|
|
35
|
+
return `<system-reminder>${text}</system-reminder>\n`;
|
|
36
|
+
}
|
|
37
|
+
//# sourceMappingURL=freshness.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"freshness.js","sourceRoot":"","sources":["../src/freshness.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,MAAM,UAAU,GAAG,UAAU,CAAC;AAE9B,gEAAgE;AAChE,MAAM,UAAU,aAAa,CAAC,OAAe;IAC3C,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,OAAO,CAAC,GAAG,UAAU,CAAC,CAAC,CAAC;AACtE,CAAC;AAED,MAAM,UAAU,SAAS,CAAC,OAAe;IACvC,MAAM,CAAC,GAAG,aAAa,CAAC,OAAO,CAAC,CAAC;IACjC,IAAI,CAAC,KAAK,CAAC;QAAE,OAAO,OAAO,CAAC;IAC5B,IAAI,CAAC,KAAK,CAAC;QAAE,OAAO,WAAW,CAAC;IAChC,OAAO,GAAG,CAAC,WAAW,CAAC;AACzB,CAAC;AAED;+EAC+E;AAC/E,MAAM,UAAU,mBAAmB,CAAC,OAAe;IACjD,MAAM,CAAC,GAAG,aAAa,CAAC,OAAO,CAAC,CAAC;IACjC,IAAI,CAAC,IAAI,CAAC;QAAE,OAAO,EAAE,CAAC;IACtB,OAAO,CACL,kBAAkB,CAAC,aAAa;QAChC,4DAA4D;QAC5D,qEAAqE;QACrE,uDAAuD,CACxD,CAAC;AACJ,CAAC;AAED,4DAA4D;AAC5D,MAAM,UAAU,mBAAmB,CAAC,OAAe;IACjD,MAAM,IAAI,GAAG,mBAAmB,CAAC,OAAO,CAAC,CAAC;IAC1C,IAAI,CAAC,IAAI;QAAE,OAAO,EAAE,CAAC;IACrB,OAAO,oBAAoB,IAAI,sBAAsB,CAAC;AACxD,CAAC"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { MemoryStore, DEFAULT_MEMORY_CHAR_LIMIT, type IndexSnapshot, } from "./store.js";
|
|
2
|
+
export { memoryAge, memoryAgeDays, memoryFreshnessNote, memoryFreshnessText, } from "./freshness.js";
|
|
3
|
+
export { scanMemoryFiles, formatMemoryManifest, parseFrontmatterDescription, type MemoryHeader, } from "./scan.js";
|
|
4
|
+
export { scanMemoryContent, type ScanResult } from "./threat-scanner.js";
|
|
5
|
+
export { MEMORY_THREAT_PATTERNS, INVISIBLE_CHARS } from "./threat-patterns.js";
|
|
6
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,WAAW,EACX,yBAAyB,EACzB,KAAK,aAAa,GACnB,MAAM,YAAY,CAAC;AAEpB,OAAO,EACL,SAAS,EACT,aAAa,EACb,mBAAmB,EACnB,mBAAmB,GACpB,MAAM,gBAAgB,CAAC;AAExB,OAAO,EACL,eAAe,EACf,oBAAoB,EACpB,2BAA2B,EAC3B,KAAK,YAAY,GAClB,MAAM,WAAW,CAAC;AAEnB,OAAO,EAAE,iBAAiB,EAAE,KAAK,UAAU,EAAE,MAAM,qBAAqB,CAAC;AAEzE,OAAO,EAAE,sBAAsB,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { MemoryStore, DEFAULT_MEMORY_CHAR_LIMIT, } from "./store.js";
|
|
2
|
+
export { memoryAge, memoryAgeDays, memoryFreshnessNote, memoryFreshnessText, } from "./freshness.js";
|
|
3
|
+
export { scanMemoryFiles, formatMemoryManifest, parseFrontmatterDescription, } from "./scan.js";
|
|
4
|
+
export { scanMemoryContent } from "./threat-scanner.js";
|
|
5
|
+
export { MEMORY_THREAT_PATTERNS, INVISIBLE_CHARS } from "./threat-patterns.js";
|
|
6
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,WAAW,EACX,yBAAyB,GAE1B,MAAM,YAAY,CAAC;AAEpB,OAAO,EACL,SAAS,EACT,aAAa,EACb,mBAAmB,EACnB,mBAAmB,GACpB,MAAM,gBAAgB,CAAC;AAExB,OAAO,EACL,eAAe,EACf,oBAAoB,EACpB,2BAA2B,GAE5B,MAAM,WAAW,CAAC;AAEnB,OAAO,EAAE,iBAAiB,EAAmB,MAAM,qBAAqB,CAAC;AAEzE,OAAO,EAAE,sBAAsB,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC"}
|
package/dist/scan.d.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Memory-directory scan + manifest formatting. Shared by the recall
|
|
3
|
+
* selector and the extractor. CC `memdir/memoryScan.ts` lift; we drop
|
|
4
|
+
* the type taxonomy and parse `description` via a tight regex (keeps
|
|
5
|
+
* @openacme/memory dependency-free).
|
|
6
|
+
*/
|
|
7
|
+
export interface MemoryHeader {
|
|
8
|
+
/** Path relative to memoryDir. */
|
|
9
|
+
filename: string;
|
|
10
|
+
/** Absolute filesystem path. */
|
|
11
|
+
filePath: string;
|
|
12
|
+
mtimeMs: number;
|
|
13
|
+
description: string | null;
|
|
14
|
+
}
|
|
15
|
+
declare function readHead(filePath: string, maxLines: number): Promise<string>;
|
|
16
|
+
/** Pull the `description` field out of YAML frontmatter, if present. */
|
|
17
|
+
export declare function parseFrontmatterDescription(head: string): string | null;
|
|
18
|
+
/** Recursive scan, sorted newest-first, capped at MAX_MEMORY_FILES.
|
|
19
|
+
* Best-effort: per-file read failures dropped silently. */
|
|
20
|
+
export declare function scanMemoryFiles(memoryDir: string, signal?: AbortSignal): Promise<MemoryHeader[]>;
|
|
21
|
+
/** Format: `- <filename> (<iso-ts>): <description?>` per line. */
|
|
22
|
+
export declare function formatMemoryManifest(headers: MemoryHeader[]): string;
|
|
23
|
+
export declare const __test: {
|
|
24
|
+
readHead: typeof readHead;
|
|
25
|
+
MAX_MEMORY_FILES: number;
|
|
26
|
+
FRONTMATTER_MAX_LINES: number;
|
|
27
|
+
};
|
|
28
|
+
export {};
|
|
29
|
+
//# sourceMappingURL=scan.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scan.d.ts","sourceRoot":"","sources":["../src/scan.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAUH,MAAM,WAAW,YAAY;IAC3B,kCAAkC;IAClC,QAAQ,EAAE,MAAM,CAAC;IACjB,gCAAgC;IAChC,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;CAC5B;AAED,iBAAe,QAAQ,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CA8B3E;AAED,wEAAwE;AACxE,wBAAgB,2BAA2B,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAUvE;AAED;4DAC4D;AAC5D,wBAAsB,eAAe,CACnC,SAAS,EAAE,MAAM,EACjB,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,YAAY,EAAE,CAAC,CAuCzB;AAED,kEAAkE;AAClE,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,YAAY,EAAE,GAAG,MAAM,CASpE;AAED,eAAO,MAAM,MAAM;;;;CAAwD,CAAC"}
|
package/dist/scan.js
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Memory-directory scan + manifest formatting. Shared by the recall
|
|
3
|
+
* selector and the extractor. CC `memdir/memoryScan.ts` lift; we drop
|
|
4
|
+
* the type taxonomy and parse `description` via a tight regex (keeps
|
|
5
|
+
* @openacme/memory dependency-free).
|
|
6
|
+
*/
|
|
7
|
+
import { readdir, stat } from "node:fs/promises";
|
|
8
|
+
import { basename, join } from "node:path";
|
|
9
|
+
import { createReadStream } from "node:fs";
|
|
10
|
+
const MAX_MEMORY_FILES = 200;
|
|
11
|
+
const FRONTMATTER_MAX_LINES = 30;
|
|
12
|
+
const INDEX_FILE = "MEMORY.md";
|
|
13
|
+
async function readHead(filePath, maxLines) {
|
|
14
|
+
return new Promise((resolve, reject) => {
|
|
15
|
+
const chunks = [];
|
|
16
|
+
let lines = 0;
|
|
17
|
+
let buf = "";
|
|
18
|
+
const stream = createReadStream(filePath, { encoding: "utf-8" });
|
|
19
|
+
let settled = false;
|
|
20
|
+
const finish = (s) => {
|
|
21
|
+
if (settled)
|
|
22
|
+
return;
|
|
23
|
+
settled = true;
|
|
24
|
+
stream.destroy();
|
|
25
|
+
resolve(s);
|
|
26
|
+
};
|
|
27
|
+
stream.on("data", (chunk) => {
|
|
28
|
+
buf += chunk;
|
|
29
|
+
let idx;
|
|
30
|
+
while ((idx = buf.indexOf("\n")) >= 0 && lines < maxLines) {
|
|
31
|
+
chunks.push(buf.slice(0, idx + 1));
|
|
32
|
+
buf = buf.slice(idx + 1);
|
|
33
|
+
lines++;
|
|
34
|
+
}
|
|
35
|
+
if (lines >= maxLines)
|
|
36
|
+
finish(chunks.join(""));
|
|
37
|
+
});
|
|
38
|
+
stream.on("end", () => finish(chunks.join("") + buf));
|
|
39
|
+
stream.on("error", (e) => {
|
|
40
|
+
if (settled)
|
|
41
|
+
return;
|
|
42
|
+
settled = true;
|
|
43
|
+
reject(e);
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
/** Pull the `description` field out of YAML frontmatter, if present. */
|
|
48
|
+
export function parseFrontmatterDescription(head) {
|
|
49
|
+
if (!head.startsWith("---"))
|
|
50
|
+
return null;
|
|
51
|
+
const close = head.indexOf("\n---", 3);
|
|
52
|
+
if (close < 0)
|
|
53
|
+
return null;
|
|
54
|
+
const block = head.slice(3, close);
|
|
55
|
+
// Tolerate optional surrounding quotes.
|
|
56
|
+
const m = /^description:\s*(?:"([^"]*)"|'([^']*)'|(.*))\s*$/m.exec(block);
|
|
57
|
+
if (!m)
|
|
58
|
+
return null;
|
|
59
|
+
const v = (m[1] ?? m[2] ?? m[3] ?? "").trim();
|
|
60
|
+
return v.length > 0 ? v : null;
|
|
61
|
+
}
|
|
62
|
+
/** Recursive scan, sorted newest-first, capped at MAX_MEMORY_FILES.
|
|
63
|
+
* Best-effort: per-file read failures dropped silently. */
|
|
64
|
+
export async function scanMemoryFiles(memoryDir, signal) {
|
|
65
|
+
let entries;
|
|
66
|
+
try {
|
|
67
|
+
entries = await readdir(memoryDir, { recursive: true });
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
return [];
|
|
71
|
+
}
|
|
72
|
+
if (signal?.aborted)
|
|
73
|
+
return [];
|
|
74
|
+
const candidates = entries.filter((f) => f.endsWith(".md") &&
|
|
75
|
+
basename(f) !== INDEX_FILE &&
|
|
76
|
+
!f.split("/").some((seg) => seg.startsWith(".")));
|
|
77
|
+
const results = await Promise.allSettled(candidates.map(async (rel) => {
|
|
78
|
+
const filePath = join(memoryDir, rel);
|
|
79
|
+
const [head, st] = await Promise.all([
|
|
80
|
+
readHead(filePath, FRONTMATTER_MAX_LINES),
|
|
81
|
+
stat(filePath),
|
|
82
|
+
]);
|
|
83
|
+
return {
|
|
84
|
+
filename: rel,
|
|
85
|
+
filePath,
|
|
86
|
+
mtimeMs: st.mtimeMs,
|
|
87
|
+
description: parseFrontmatterDescription(head),
|
|
88
|
+
};
|
|
89
|
+
}));
|
|
90
|
+
return results
|
|
91
|
+
.filter((r) => r.status === "fulfilled")
|
|
92
|
+
.map((r) => r.value)
|
|
93
|
+
.sort((a, b) => b.mtimeMs - a.mtimeMs)
|
|
94
|
+
.slice(0, MAX_MEMORY_FILES);
|
|
95
|
+
}
|
|
96
|
+
/** Format: `- <filename> (<iso-ts>): <description?>` per line. */
|
|
97
|
+
export function formatMemoryManifest(headers) {
|
|
98
|
+
return headers
|
|
99
|
+
.map((m) => {
|
|
100
|
+
const ts = new Date(m.mtimeMs).toISOString();
|
|
101
|
+
return m.description
|
|
102
|
+
? `- ${m.filename} (${ts}): ${m.description}`
|
|
103
|
+
: `- ${m.filename} (${ts})`;
|
|
104
|
+
})
|
|
105
|
+
.join("\n");
|
|
106
|
+
}
|
|
107
|
+
export const __test = { readHead, MAX_MEMORY_FILES, FRONTMATTER_MAX_LINES };
|
|
108
|
+
//# sourceMappingURL=scan.js.map
|
package/dist/scan.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scan.js","sourceRoot":"","sources":["../src/scan.ts"],"names":[],"mappings":"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"}
|
package/dist/store.d.ts
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-agent persistent memory store — directory-backed, six-op shape
|
|
3
|
+
* matching Anthropic's `memory_20250818` tool spec.
|
|
4
|
+
*
|
|
5
|
+
* Layout per agent:
|
|
6
|
+
*
|
|
7
|
+
* <agentsDir>/<agentId>/memory/
|
|
8
|
+
* MEMORY.md ← always-injected index of one-line pointers
|
|
9
|
+
* <topic>.md ← agent-created entry files (read on demand)
|
|
10
|
+
* <subdir>/<topic>.md ← agent-organized; we don't enforce layout
|
|
11
|
+
*
|
|
12
|
+
* Tool calls reference paths in the agent's virtual view: `/memories/...`.
|
|
13
|
+
* The store translates to filesystem paths internally and rejects anything
|
|
14
|
+
* that escapes the per-agent root (path-traversal protection per Anthropic
|
|
15
|
+
* security note).
|
|
16
|
+
*
|
|
17
|
+
* Concurrency: per-agent in-process async mutex serializes
|
|
18
|
+
* read-modify-write. Cross-process safety is NOT provided — atomic rename
|
|
19
|
+
* keeps readers consistent, and we assume one process owns a given dataDir
|
|
20
|
+
* at a time.
|
|
21
|
+
*
|
|
22
|
+
* Caps:
|
|
23
|
+
* - Per-file line cap = 999,999 (Anthropic spec verbatim).
|
|
24
|
+
* - MEMORY.md index has a write-time char cap (`memoryCharLimit`,
|
|
25
|
+
* default 2200 — Hermes default). Writes that would push it over
|
|
26
|
+
* return OpenAcme's existing guidance string ("Memory at X/Y
|
|
27
|
+
* chars. ...Replace or remove existing entries first.") so the
|
|
28
|
+
* agent has consolidation pressure rather than infinite append.
|
|
29
|
+
* - Per-entry files have NO char cap (they're not auto-injected;
|
|
30
|
+
* only the per-file line cap applies).
|
|
31
|
+
*
|
|
32
|
+
* Return strings: each op returns the EXACT string Anthropic specifies
|
|
33
|
+
* (line-number format, directory listing format, error wording). Models
|
|
34
|
+
* are trained against those literals; matching them is what makes
|
|
35
|
+
* `memory_20250818`-trained behavior carry over without prompt
|
|
36
|
+
* engineering.
|
|
37
|
+
*/
|
|
38
|
+
/** Hermes default. Single source of truth for the index char cap. */
|
|
39
|
+
export declare const DEFAULT_MEMORY_CHAR_LIMIT = 2200;
|
|
40
|
+
export interface IndexSnapshot {
|
|
41
|
+
/** Raw `MEMORY.md` content, untruncated. Empty string if the file is
|
|
42
|
+
* missing or empty. */
|
|
43
|
+
content: string;
|
|
44
|
+
/** Byte length of `content`. */
|
|
45
|
+
used: number;
|
|
46
|
+
/** Configured char cap (write-time). */
|
|
47
|
+
limit: number;
|
|
48
|
+
/** Number of `.md` files under `<agentDir>/memory/` excluding
|
|
49
|
+
* `MEMORY.md` itself. Used by prompt builder to decide when to append
|
|
50
|
+
* Anthropic's "keep your memory folder organized" instruction. */
|
|
51
|
+
entryCount: number;
|
|
52
|
+
}
|
|
53
|
+
declare function formatBytes(n: number): string;
|
|
54
|
+
/** Format a 1-indexed line number in 6-char right-aligned form (Anthropic). */
|
|
55
|
+
declare function fmtLineNo(n: number): string;
|
|
56
|
+
/** Render file contents with Anthropic's line-number format. */
|
|
57
|
+
declare function withLineNumbers(content: string): string;
|
|
58
|
+
/**
|
|
59
|
+
* Per-agent memory store. One instance per `agentsDir` — multiple
|
|
60
|
+
* instances with different roots get independent mutex maps, which
|
|
61
|
+
* matters for tests and multi-tenant deployments.
|
|
62
|
+
*/
|
|
63
|
+
export declare class MemoryStore {
|
|
64
|
+
readonly agentsDir: string;
|
|
65
|
+
private readonly inFlight;
|
|
66
|
+
constructor(agentsDir: string);
|
|
67
|
+
/** Absolute path to an agent's memory directory. Does not create it. */
|
|
68
|
+
dirPath(agentId: string): string;
|
|
69
|
+
/** Absolute path to the index file for an agent. Does not create it. */
|
|
70
|
+
indexPath(agentId: string): string;
|
|
71
|
+
/**
|
|
72
|
+
* Translate a virtual `/memories/...` path (as the agent sees it) to
|
|
73
|
+
* an absolute filesystem path under `<agentsDir>/<agentId>/memory/`.
|
|
74
|
+
* Rejects:
|
|
75
|
+
* - Paths not starting with `/memories`
|
|
76
|
+
* - URL-encoded traversal sequences (`%2e%2e%2f`, etc.)
|
|
77
|
+
* - Resolved paths that escape the per-agent root
|
|
78
|
+
*
|
|
79
|
+
* Returns the absolute path on success, or an error string in the
|
|
80
|
+
* same wording Anthropic uses for "path does not exist" so the model
|
|
81
|
+
* sees a familiar shape.
|
|
82
|
+
*/
|
|
83
|
+
private translatePath;
|
|
84
|
+
/** True if the virtual path points at the index file specifically. */
|
|
85
|
+
private isIndexPath;
|
|
86
|
+
/**
|
|
87
|
+
* Snapshot of the agent's index for system-prompt injection.
|
|
88
|
+
* Synchronous; safe for render paths. Counts entry files for the
|
|
89
|
+
* prompt builder's "cluttered memory" trigger.
|
|
90
|
+
*/
|
|
91
|
+
readIndex(agentId: string, charLimit: number): IndexSnapshot;
|
|
92
|
+
/**
|
|
93
|
+
* `view` — show directory contents (2 levels deep) or file contents
|
|
94
|
+
* with optional line range. For entry files (anything other than
|
|
95
|
+
* `MEMORY.md`) older than 1 day, prepends `memoryFreshnessNote`.
|
|
96
|
+
*
|
|
97
|
+
* Synchronous; no mutex needed (read-only).
|
|
98
|
+
*/
|
|
99
|
+
view(agentId: string, virtualPath: string, viewRange?: readonly [number, number]): string;
|
|
100
|
+
/**
|
|
101
|
+
* Directory listing in Anthropic's format, adapted for OpenAcme:
|
|
102
|
+
* the `node_modules` exclusion clause from the upstream spec is
|
|
103
|
+
* dropped because OpenAcme's memory dir can never contain one (no
|
|
104
|
+
* shell access, no package manager runs against it). We still
|
|
105
|
+
* exclude hidden dotfiles so internal artifacts (future
|
|
106
|
+
* coordination locks, sidecar state) stay out of the agent's view.
|
|
107
|
+
*/
|
|
108
|
+
private viewDirectory;
|
|
109
|
+
/**
|
|
110
|
+
* `create` — create a new file. Errors if a file already exists at
|
|
111
|
+
* the target path. For writes targeting `MEMORY.md`, enforces the
|
|
112
|
+
* write-time char cap with OpenAcme's guidance string. For all
|
|
113
|
+
* files, enforces the 999,999-line cap.
|
|
114
|
+
*/
|
|
115
|
+
create(agentId: string, virtualPath: string, fileText: string, indexCharLimit?: number): Promise<string>;
|
|
116
|
+
/**
|
|
117
|
+
* `str_replace` — replace `oldStr` with `newStr` in a file. Errors
|
|
118
|
+
* on missing file, missing match, or multiple matches (Anthropic's
|
|
119
|
+
* exact wording).
|
|
120
|
+
*/
|
|
121
|
+
strReplace(agentId: string, virtualPath: string, oldStr: string, newStr: string, indexCharLimit?: number): Promise<string>;
|
|
122
|
+
/**
|
|
123
|
+
* `insert` — insert text at a specific line in a file. `insertLine` is
|
|
124
|
+
* 0-indexed in Anthropic's spec (0 = before first line, N = after last).
|
|
125
|
+
*/
|
|
126
|
+
insert(agentId: string, virtualPath: string, insertLine: number, insertText: string, indexCharLimit?: number): Promise<string>;
|
|
127
|
+
/**
|
|
128
|
+
* `delete` — recursively delete a file or directory. Anthropic's spec
|
|
129
|
+
* says "Deletes the directory and all its contents recursively" so
|
|
130
|
+
* we use `rm -rf` semantics for directories.
|
|
131
|
+
*/
|
|
132
|
+
delete(agentId: string, virtualPath: string): Promise<string>;
|
|
133
|
+
/**
|
|
134
|
+
* `rename` — move or rename a file/directory. Errors if source is
|
|
135
|
+
* missing or destination already exists (no overwrite).
|
|
136
|
+
*/
|
|
137
|
+
rename(agentId: string, oldVirtualPath: string, newVirtualPath: string): Promise<string>;
|
|
138
|
+
/** Atomic write — tmp file + fsync + rename. */
|
|
139
|
+
private writeFileAtomic;
|
|
140
|
+
/**
|
|
141
|
+
* Per-agent mutex. Map stores only the settle-completion of each
|
|
142
|
+
* outstanding write, never its rejection — so a thrown error inside
|
|
143
|
+
* one writer can't poison the chain for the next.
|
|
144
|
+
*/
|
|
145
|
+
private withMutex;
|
|
146
|
+
}
|
|
147
|
+
export declare const __test: {
|
|
148
|
+
withLineNumbers: typeof withLineNumbers;
|
|
149
|
+
formatBytes: typeof formatBytes;
|
|
150
|
+
fmtLineNo: typeof fmtLineNo;
|
|
151
|
+
};
|
|
152
|
+
export {};
|
|
153
|
+
//# sourceMappingURL=store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AAmBH,qEAAqE;AACrE,eAAO,MAAM,yBAAyB,OAAO,CAAC;AAQ9C,MAAM,WAAW,aAAa;IAC5B;4BACwB;IACxB,OAAO,EAAE,MAAM,CAAC;IAChB,gCAAgC;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,wCAAwC;IACxC,KAAK,EAAE,MAAM,CAAC;IACd;;uEAEmE;IACnE,UAAU,EAAE,MAAM,CAAC;CACpB;AAID,iBAAS,WAAW,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAKtC;AAED,+EAA+E;AAC/E,iBAAS,SAAS,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAEpC;AAED,gEAAgE;AAChE,iBAAS,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAGhD;AAiCD;;;;GAIG;AACH,qBAAa,WAAW;IAGV,QAAQ,CAAC,SAAS,EAAE,MAAM;IAFtC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAoC;gBAExC,SAAS,EAAE,MAAM;IAItC,wEAAwE;IACxE,OAAO,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM;IAShC,wEAAwE;IACxE,SAAS,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM;IAIlC;;;;;;;;;;;OAWG;IACH,OAAO,CAAC,aAAa;IA8CrB,sEAAsE;IACtE,OAAO,CAAC,WAAW;IASnB;;;;OAIG;IACH,SAAS,CAAC,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,aAAa;IAiC5D;;;;;;OAMG;IACH,IAAI,CACF,OAAO,EAAE,MAAM,EACf,WAAW,EAAE,MAAM,EACnB,SAAS,CAAC,EAAE,SAAS,CAAC,MAAM,EAAE,MAAM,CAAC,GACpC,MAAM;IAoET;;;;;;;OAOG;IACH,OAAO,CAAC,aAAa;IA2DrB;;;;;OAKG;IACG,MAAM,CACV,OAAO,EAAE,MAAM,EACf,WAAW,EAAE,MAAM,EACnB,QAAQ,EAAE,MAAM,EAChB,cAAc,CAAC,EAAE,MAAM,GACtB,OAAO,CAAC,MAAM,CAAC;IAqClB;;;;OAIG;IACG,UAAU,CACd,OAAO,EAAE,MAAM,EACf,WAAW,EAAE,MAAM,EACnB,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,MAAM,EACd,cAAc,CAAC,EAAE,MAAM,GACtB,OAAO,CAAC,MAAM,CAAC;IAkElB;;;OAGG;IACG,MAAM,CACV,OAAO,EAAE,MAAM,EACf,WAAW,EAAE,MAAM,EACnB,UAAU,EAAE,MAAM,EAClB,UAAU,EAAE,MAAM,EAClB,cAAc,CAAC,EAAE,MAAM,GACtB,OAAO,CAAC,MAAM,CAAC;IAwDlB;;;;OAIG;IACG,MAAM,CAAC,OAAO,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IA0BnE;;;OAGG;IACG,MAAM,CACV,OAAO,EAAE,MAAM,EACf,cAAc,EAAE,MAAM,EACtB,cAAc,EAAE,MAAM,GACrB,OAAO,CAAC,MAAM,CAAC;IA6BlB,gDAAgD;YAClC,eAAe;IAiC7B;;;;OAIG;YACW,SAAS;CAYxB;AAGD,eAAO,MAAM,MAAM;;;;CAA8C,CAAC"}
|
package/dist/store.js
ADDED
|
@@ -0,0 +1,620 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-agent persistent memory store — directory-backed, six-op shape
|
|
3
|
+
* matching Anthropic's `memory_20250818` tool spec.
|
|
4
|
+
*
|
|
5
|
+
* Layout per agent:
|
|
6
|
+
*
|
|
7
|
+
* <agentsDir>/<agentId>/memory/
|
|
8
|
+
* MEMORY.md ← always-injected index of one-line pointers
|
|
9
|
+
* <topic>.md ← agent-created entry files (read on demand)
|
|
10
|
+
* <subdir>/<topic>.md ← agent-organized; we don't enforce layout
|
|
11
|
+
*
|
|
12
|
+
* Tool calls reference paths in the agent's virtual view: `/memories/...`.
|
|
13
|
+
* The store translates to filesystem paths internally and rejects anything
|
|
14
|
+
* that escapes the per-agent root (path-traversal protection per Anthropic
|
|
15
|
+
* security note).
|
|
16
|
+
*
|
|
17
|
+
* Concurrency: per-agent in-process async mutex serializes
|
|
18
|
+
* read-modify-write. Cross-process safety is NOT provided — atomic rename
|
|
19
|
+
* keeps readers consistent, and we assume one process owns a given dataDir
|
|
20
|
+
* at a time.
|
|
21
|
+
*
|
|
22
|
+
* Caps:
|
|
23
|
+
* - Per-file line cap = 999,999 (Anthropic spec verbatim).
|
|
24
|
+
* - MEMORY.md index has a write-time char cap (`memoryCharLimit`,
|
|
25
|
+
* default 2200 — Hermes default). Writes that would push it over
|
|
26
|
+
* return OpenAcme's existing guidance string ("Memory at X/Y
|
|
27
|
+
* chars. ...Replace or remove existing entries first.") so the
|
|
28
|
+
* agent has consolidation pressure rather than infinite append.
|
|
29
|
+
* - Per-entry files have NO char cap (they're not auto-injected;
|
|
30
|
+
* only the per-file line cap applies).
|
|
31
|
+
*
|
|
32
|
+
* Return strings: each op returns the EXACT string Anthropic specifies
|
|
33
|
+
* (line-number format, directory listing format, error wording). Models
|
|
34
|
+
* are trained against those literals; matching them is what makes
|
|
35
|
+
* `memory_20250818`-trained behavior carry over without prompt
|
|
36
|
+
* engineering.
|
|
37
|
+
*/
|
|
38
|
+
import { randomBytes } from "node:crypto";
|
|
39
|
+
import * as fs from "node:fs";
|
|
40
|
+
import * as fsp from "node:fs/promises";
|
|
41
|
+
import * as path from "node:path";
|
|
42
|
+
import { memoryFreshnessNote } from "./freshness.js";
|
|
43
|
+
// ── Constants ──────────────────────────────────────────────────────────
|
|
44
|
+
const MEMORIES_ROOT = "/memories";
|
|
45
|
+
const MEMORY_DIR = "memory";
|
|
46
|
+
const INDEX_FILE = "MEMORY.md";
|
|
47
|
+
const TMP_PREFIX = ".mem_";
|
|
48
|
+
/** Per-file line cap (Anthropic spec). */
|
|
49
|
+
const MAX_FILE_LINES = 999_999;
|
|
50
|
+
/** Hermes default. Single source of truth for the index char cap. */
|
|
51
|
+
export const DEFAULT_MEMORY_CHAR_LIMIT = 2200;
|
|
52
|
+
/** Same constraint agent-store uses; duplicated here so this module stays
|
|
53
|
+
* independent of `@openacme/config`. */
|
|
54
|
+
const SAFE_ID = /^[A-Za-z0-9][A-Za-z0-9_.-]*$/;
|
|
55
|
+
// ── Pure helpers ───────────────────────────────────────────────────────
|
|
56
|
+
function formatBytes(n) {
|
|
57
|
+
if (n < 1024)
|
|
58
|
+
return `${n}`;
|
|
59
|
+
if (n < 1024 * 1024)
|
|
60
|
+
return `${(n / 1024).toFixed(1)}K`;
|
|
61
|
+
if (n < 1024 * 1024 * 1024)
|
|
62
|
+
return `${(n / 1024 / 1024).toFixed(1)}M`;
|
|
63
|
+
return `${(n / 1024 / 1024 / 1024).toFixed(1)}G`;
|
|
64
|
+
}
|
|
65
|
+
/** Format a 1-indexed line number in 6-char right-aligned form (Anthropic). */
|
|
66
|
+
function fmtLineNo(n) {
|
|
67
|
+
return String(n).padStart(6, " ");
|
|
68
|
+
}
|
|
69
|
+
/** Render file contents with Anthropic's line-number format. */
|
|
70
|
+
function withLineNumbers(content) {
|
|
71
|
+
const lines = content.length === 0 ? [""] : content.split("\n");
|
|
72
|
+
return lines.map((line, i) => `${fmtLineNo(i + 1)}\t${line}`).join("\n");
|
|
73
|
+
}
|
|
74
|
+
/** Find ALL line numbers (1-indexed) where `needle` appears in `haystack`. */
|
|
75
|
+
function findOccurrenceLines(haystack, needle) {
|
|
76
|
+
const lines = [];
|
|
77
|
+
let from = 0;
|
|
78
|
+
while (true) {
|
|
79
|
+
const idx = haystack.indexOf(needle, from);
|
|
80
|
+
if (idx < 0)
|
|
81
|
+
break;
|
|
82
|
+
// 1-indexed line number of the occurrence start
|
|
83
|
+
const lineNo = haystack.slice(0, idx).split("\n").length;
|
|
84
|
+
lines.push(lineNo);
|
|
85
|
+
from = idx + needle.length;
|
|
86
|
+
}
|
|
87
|
+
return lines;
|
|
88
|
+
}
|
|
89
|
+
/** Count occurrences of `needle` in `haystack`. */
|
|
90
|
+
function countOccurrences(haystack, needle) {
|
|
91
|
+
if (needle.length === 0)
|
|
92
|
+
return 0;
|
|
93
|
+
let count = 0;
|
|
94
|
+
let from = 0;
|
|
95
|
+
while (true) {
|
|
96
|
+
const idx = haystack.indexOf(needle, from);
|
|
97
|
+
if (idx < 0)
|
|
98
|
+
break;
|
|
99
|
+
count++;
|
|
100
|
+
from = idx + needle.length;
|
|
101
|
+
}
|
|
102
|
+
return count;
|
|
103
|
+
}
|
|
104
|
+
// ── MemoryStore ────────────────────────────────────────────────────────
|
|
105
|
+
/**
|
|
106
|
+
* Per-agent memory store. One instance per `agentsDir` — multiple
|
|
107
|
+
* instances with different roots get independent mutex maps, which
|
|
108
|
+
* matters for tests and multi-tenant deployments.
|
|
109
|
+
*/
|
|
110
|
+
export class MemoryStore {
|
|
111
|
+
agentsDir;
|
|
112
|
+
inFlight = new Map();
|
|
113
|
+
constructor(agentsDir) {
|
|
114
|
+
this.agentsDir = agentsDir;
|
|
115
|
+
}
|
|
116
|
+
// ── Path helpers ─────────────────────────────────────────────────────
|
|
117
|
+
/** Absolute path to an agent's memory directory. Does not create it. */
|
|
118
|
+
dirPath(agentId) {
|
|
119
|
+
if (!SAFE_ID.test(agentId)) {
|
|
120
|
+
throw new Error(`Invalid agent id ${JSON.stringify(agentId)}: must match ${SAFE_ID}`);
|
|
121
|
+
}
|
|
122
|
+
return path.join(this.agentsDir, agentId, MEMORY_DIR);
|
|
123
|
+
}
|
|
124
|
+
/** Absolute path to the index file for an agent. Does not create it. */
|
|
125
|
+
indexPath(agentId) {
|
|
126
|
+
return path.join(this.dirPath(agentId), INDEX_FILE);
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Translate a virtual `/memories/...` path (as the agent sees it) to
|
|
130
|
+
* an absolute filesystem path under `<agentsDir>/<agentId>/memory/`.
|
|
131
|
+
* Rejects:
|
|
132
|
+
* - Paths not starting with `/memories`
|
|
133
|
+
* - URL-encoded traversal sequences (`%2e%2e%2f`, etc.)
|
|
134
|
+
* - Resolved paths that escape the per-agent root
|
|
135
|
+
*
|
|
136
|
+
* Returns the absolute path on success, or an error string in the
|
|
137
|
+
* same wording Anthropic uses for "path does not exist" so the model
|
|
138
|
+
* sees a familiar shape.
|
|
139
|
+
*/
|
|
140
|
+
translatePath(agentId, virtualPath) {
|
|
141
|
+
if (typeof virtualPath !== "string" || virtualPath.length === 0) {
|
|
142
|
+
return {
|
|
143
|
+
ok: false,
|
|
144
|
+
error: `The path ${virtualPath} does not exist. Please provide a valid path.`,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
// Reject URL-encoded traversal before any normalization
|
|
148
|
+
const lowered = virtualPath.toLowerCase();
|
|
149
|
+
if (lowered.includes("%2e") || lowered.includes("%2f") || lowered.includes("%5c")) {
|
|
150
|
+
return {
|
|
151
|
+
ok: false,
|
|
152
|
+
error: `The path ${virtualPath} does not exist. Please provide a valid path.`,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
if (!virtualPath.startsWith(MEMORIES_ROOT)) {
|
|
156
|
+
return {
|
|
157
|
+
ok: false,
|
|
158
|
+
error: `The path ${virtualPath} does not exist. Please provide a valid path.`,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
// Strip /memories prefix; remainder may be "" (root), "/foo", "/foo/bar.md"
|
|
162
|
+
const remainder = virtualPath.slice(MEMORIES_ROOT.length);
|
|
163
|
+
if (remainder.length > 0 && !remainder.startsWith("/")) {
|
|
164
|
+
// e.g. /memoriesfoo — not actually under /memories
|
|
165
|
+
return {
|
|
166
|
+
ok: false,
|
|
167
|
+
error: `The path ${virtualPath} does not exist. Please provide a valid path.`,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
const root = this.dirPath(agentId);
|
|
171
|
+
const abs = path.resolve(root, "." + remainder);
|
|
172
|
+
// Defense in depth: ensure the resolved path is still under root.
|
|
173
|
+
const rel = path.relative(root, abs);
|
|
174
|
+
if (rel.startsWith("..") || path.isAbsolute(rel)) {
|
|
175
|
+
return {
|
|
176
|
+
ok: false,
|
|
177
|
+
error: `The path ${virtualPath} does not exist. Please provide a valid path.`,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
return { ok: true, abs };
|
|
181
|
+
}
|
|
182
|
+
/** True if the virtual path points at the index file specifically. */
|
|
183
|
+
isIndexPath(virtualPath) {
|
|
184
|
+
return (virtualPath === `${MEMORIES_ROOT}/${INDEX_FILE}` ||
|
|
185
|
+
virtualPath === `${MEMORIES_ROOT}/${INDEX_FILE}/`);
|
|
186
|
+
}
|
|
187
|
+
// ── Index accessor (for prompt builder) ──────────────────────────────
|
|
188
|
+
/**
|
|
189
|
+
* Snapshot of the agent's index for system-prompt injection.
|
|
190
|
+
* Synchronous; safe for render paths. Counts entry files for the
|
|
191
|
+
* prompt builder's "cluttered memory" trigger.
|
|
192
|
+
*/
|
|
193
|
+
readIndex(agentId, charLimit) {
|
|
194
|
+
const indexFile = this.indexPath(agentId);
|
|
195
|
+
let content = "";
|
|
196
|
+
try {
|
|
197
|
+
content = fs.readFileSync(indexFile, "utf-8");
|
|
198
|
+
}
|
|
199
|
+
catch (e) {
|
|
200
|
+
if (e.code !== "ENOENT")
|
|
201
|
+
throw e;
|
|
202
|
+
}
|
|
203
|
+
const trimmed = content.trim();
|
|
204
|
+
const used = trimmed.length;
|
|
205
|
+
// Walk the dir for entry files (non-recursive — rare but possible
|
|
206
|
+
// sub-dirs are counted under the prompt-builder's "cluttered" cap
|
|
207
|
+
// via reading the same scan elsewhere; for the simple count signal
|
|
208
|
+
// a flat enumeration is enough).
|
|
209
|
+
let entryCount = 0;
|
|
210
|
+
const dir = this.dirPath(agentId);
|
|
211
|
+
try {
|
|
212
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
213
|
+
for (const e of entries) {
|
|
214
|
+
if (e.isFile() && e.name.endsWith(".md") && e.name !== INDEX_FILE) {
|
|
215
|
+
entryCount++;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
catch (e) {
|
|
220
|
+
if (e.code !== "ENOENT")
|
|
221
|
+
throw e;
|
|
222
|
+
}
|
|
223
|
+
return { content: trimmed, used, limit: charLimit, entryCount };
|
|
224
|
+
}
|
|
225
|
+
// ── view ─────────────────────────────────────────────────────────────
|
|
226
|
+
/**
|
|
227
|
+
* `view` — show directory contents (2 levels deep) or file contents
|
|
228
|
+
* with optional line range. For entry files (anything other than
|
|
229
|
+
* `MEMORY.md`) older than 1 day, prepends `memoryFreshnessNote`.
|
|
230
|
+
*
|
|
231
|
+
* Synchronous; no mutex needed (read-only).
|
|
232
|
+
*/
|
|
233
|
+
view(agentId, virtualPath, viewRange) {
|
|
234
|
+
const t = this.translatePath(agentId, virtualPath);
|
|
235
|
+
if (!t.ok)
|
|
236
|
+
return t.error;
|
|
237
|
+
const abs = t.abs;
|
|
238
|
+
// Special case: viewing the root `/memories` of an agent that hasn't
|
|
239
|
+
// touched memory yet. Return the empty-listing form rather than
|
|
240
|
+
// "does not exist" — matches Anthropic's example (the header is
|
|
241
|
+
// emitted even for empty memory dirs) and matches the agent's
|
|
242
|
+
// mental model that the dir always exists.
|
|
243
|
+
const isRoot = virtualPath === MEMORIES_ROOT || virtualPath === `${MEMORIES_ROOT}/`;
|
|
244
|
+
let stat;
|
|
245
|
+
try {
|
|
246
|
+
stat = fs.statSync(abs);
|
|
247
|
+
}
|
|
248
|
+
catch (e) {
|
|
249
|
+
if (e.code === "ENOENT") {
|
|
250
|
+
if (isRoot)
|
|
251
|
+
return this.viewDirectory(agentId, virtualPath, abs);
|
|
252
|
+
return `The path ${virtualPath} does not exist. Please provide a valid path.`;
|
|
253
|
+
}
|
|
254
|
+
throw e;
|
|
255
|
+
}
|
|
256
|
+
if (stat.isDirectory()) {
|
|
257
|
+
return this.viewDirectory(agentId, virtualPath, abs);
|
|
258
|
+
}
|
|
259
|
+
// File path. Read content; honor optional view_range.
|
|
260
|
+
let raw;
|
|
261
|
+
try {
|
|
262
|
+
raw = fs.readFileSync(abs, "utf-8");
|
|
263
|
+
}
|
|
264
|
+
catch (e) {
|
|
265
|
+
if (e.code === "ENOENT") {
|
|
266
|
+
return `The path ${virtualPath} does not exist. Please provide a valid path.`;
|
|
267
|
+
}
|
|
268
|
+
throw e;
|
|
269
|
+
}
|
|
270
|
+
const lines = raw.split("\n");
|
|
271
|
+
if (lines.length > MAX_FILE_LINES) {
|
|
272
|
+
return `File ${virtualPath} exceeds maximum line limit of 999,999 lines.`;
|
|
273
|
+
}
|
|
274
|
+
let displayLines = lines;
|
|
275
|
+
let startOffset = 0;
|
|
276
|
+
if (viewRange) {
|
|
277
|
+
const [from, to] = viewRange;
|
|
278
|
+
const start = Math.max(1, from) - 1;
|
|
279
|
+
const end = to <= 0 ? lines.length : Math.min(lines.length, to);
|
|
280
|
+
displayLines = lines.slice(start, end);
|
|
281
|
+
startOffset = start;
|
|
282
|
+
}
|
|
283
|
+
const numbered = displayLines
|
|
284
|
+
.map((line, i) => `${fmtLineNo(i + 1 + startOffset)}\t${line}`)
|
|
285
|
+
.join("\n");
|
|
286
|
+
const header = `Here's the content of ${virtualPath} with line numbers:`;
|
|
287
|
+
let out = `${header}\n${numbered}`;
|
|
288
|
+
// Freshness wrap for entry files (not the index).
|
|
289
|
+
if (!this.isIndexPath(virtualPath)) {
|
|
290
|
+
const note = memoryFreshnessNote(stat.mtimeMs);
|
|
291
|
+
if (note.length > 0)
|
|
292
|
+
out = note + out;
|
|
293
|
+
}
|
|
294
|
+
return out;
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* Directory listing in Anthropic's format, adapted for OpenAcme:
|
|
298
|
+
* the `node_modules` exclusion clause from the upstream spec is
|
|
299
|
+
* dropped because OpenAcme's memory dir can never contain one (no
|
|
300
|
+
* shell access, no package manager runs against it). We still
|
|
301
|
+
* exclude hidden dotfiles so internal artifacts (future
|
|
302
|
+
* coordination locks, sidecar state) stay out of the agent's view.
|
|
303
|
+
*/
|
|
304
|
+
viewDirectory(agentId, virtualPath, absRoot) {
|
|
305
|
+
const lines = [
|
|
306
|
+
`Here're the files and directories up to 2 levels deep in ${virtualPath}, excluding hidden items:`,
|
|
307
|
+
];
|
|
308
|
+
// Root entry — total size of contents at depth ≤ 2.
|
|
309
|
+
let rootBytes = 0;
|
|
310
|
+
const out = [];
|
|
311
|
+
const collect = (currentAbs, currentVirtual, depth) => {
|
|
312
|
+
if (depth > 2)
|
|
313
|
+
return;
|
|
314
|
+
let dirents;
|
|
315
|
+
try {
|
|
316
|
+
dirents = fs.readdirSync(currentAbs, { withFileTypes: true });
|
|
317
|
+
}
|
|
318
|
+
catch {
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
for (const d of dirents) {
|
|
322
|
+
if (d.name.startsWith("."))
|
|
323
|
+
continue;
|
|
324
|
+
const childAbs = path.join(currentAbs, d.name);
|
|
325
|
+
const childVirtual = `${currentVirtual}/${d.name}`;
|
|
326
|
+
if (d.isDirectory()) {
|
|
327
|
+
out.push({ virtual: childVirtual, bytes: 0 });
|
|
328
|
+
collect(childAbs, childVirtual, depth + 1);
|
|
329
|
+
}
|
|
330
|
+
else if (d.isFile()) {
|
|
331
|
+
let size = 0;
|
|
332
|
+
try {
|
|
333
|
+
size = fs.statSync(childAbs).size;
|
|
334
|
+
}
|
|
335
|
+
catch {
|
|
336
|
+
// ignore — listing should not fail because one file vanished
|
|
337
|
+
}
|
|
338
|
+
out.push({ virtual: childVirtual, bytes: size });
|
|
339
|
+
rootBytes += size;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
};
|
|
343
|
+
// Ensure the dir exists; if not, fabricate an empty listing rather
|
|
344
|
+
// than erroring (Anthropic's example shows the root header even when
|
|
345
|
+
// there are no children — empty memory dir is normal).
|
|
346
|
+
if (fs.existsSync(absRoot)) {
|
|
347
|
+
collect(absRoot, virtualPath.replace(/\/$/, ""), 1);
|
|
348
|
+
}
|
|
349
|
+
lines.push(`${formatBytes(rootBytes)}\t${virtualPath.replace(/\/$/, "")}`);
|
|
350
|
+
for (const e of out) {
|
|
351
|
+
lines.push(`${formatBytes(e.bytes)}\t${e.virtual}`);
|
|
352
|
+
}
|
|
353
|
+
return lines.join("\n");
|
|
354
|
+
}
|
|
355
|
+
// ── create ───────────────────────────────────────────────────────────
|
|
356
|
+
/**
|
|
357
|
+
* `create` — create a new file. Errors if a file already exists at
|
|
358
|
+
* the target path. For writes targeting `MEMORY.md`, enforces the
|
|
359
|
+
* write-time char cap with OpenAcme's guidance string. For all
|
|
360
|
+
* files, enforces the 999,999-line cap.
|
|
361
|
+
*/
|
|
362
|
+
async create(agentId, virtualPath, fileText, indexCharLimit) {
|
|
363
|
+
const t = this.translatePath(agentId, virtualPath);
|
|
364
|
+
if (!t.ok)
|
|
365
|
+
return t.error;
|
|
366
|
+
const abs = t.abs;
|
|
367
|
+
return this.withMutex(agentId, async () => {
|
|
368
|
+
// Existence check
|
|
369
|
+
try {
|
|
370
|
+
await fsp.stat(abs);
|
|
371
|
+
return `Error: File ${virtualPath} already exists`;
|
|
372
|
+
}
|
|
373
|
+
catch (e) {
|
|
374
|
+
if (e.code !== "ENOENT")
|
|
375
|
+
throw e;
|
|
376
|
+
}
|
|
377
|
+
const lineCount = fileText.split("\n").length;
|
|
378
|
+
if (lineCount > MAX_FILE_LINES) {
|
|
379
|
+
return `File ${virtualPath} exceeds maximum line limit of 999,999 lines.`;
|
|
380
|
+
}
|
|
381
|
+
// Write-time cap on MEMORY.md
|
|
382
|
+
if (this.isIndexPath(virtualPath) && typeof indexCharLimit === "number") {
|
|
383
|
+
if (fileText.trim().length > indexCharLimit) {
|
|
384
|
+
return (`Memory at 0/${indexCharLimit} chars. Adding this entry ` +
|
|
385
|
+
`(${fileText.trim().length} chars) would exceed the limit. ` +
|
|
386
|
+
`Replace or remove existing entries first.`);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
await this.writeFileAtomic(abs, fileText);
|
|
390
|
+
return `File created successfully at: ${virtualPath}`;
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
// ── str_replace ──────────────────────────────────────────────────────
|
|
394
|
+
/**
|
|
395
|
+
* `str_replace` — replace `oldStr` with `newStr` in a file. Errors
|
|
396
|
+
* on missing file, missing match, or multiple matches (Anthropic's
|
|
397
|
+
* exact wording).
|
|
398
|
+
*/
|
|
399
|
+
async strReplace(agentId, virtualPath, oldStr, newStr, indexCharLimit) {
|
|
400
|
+
const t = this.translatePath(agentId, virtualPath);
|
|
401
|
+
if (!t.ok)
|
|
402
|
+
return `Error: ${t.error}`;
|
|
403
|
+
const abs = t.abs;
|
|
404
|
+
return this.withMutex(agentId, async () => {
|
|
405
|
+
let stat;
|
|
406
|
+
try {
|
|
407
|
+
stat = await fsp.stat(abs);
|
|
408
|
+
}
|
|
409
|
+
catch (e) {
|
|
410
|
+
if (e.code === "ENOENT") {
|
|
411
|
+
return `Error: The path ${virtualPath} does not exist. Please provide a valid path.`;
|
|
412
|
+
}
|
|
413
|
+
throw e;
|
|
414
|
+
}
|
|
415
|
+
// Directory passed → "file does not exist" error per Anthropic spec
|
|
416
|
+
if (stat.isDirectory()) {
|
|
417
|
+
return `Error: The path ${virtualPath} does not exist. Please provide a valid path.`;
|
|
418
|
+
}
|
|
419
|
+
const content = await fsp.readFile(abs, "utf-8");
|
|
420
|
+
const occurrences = countOccurrences(content, oldStr);
|
|
421
|
+
if (occurrences === 0) {
|
|
422
|
+
return `No replacement was performed, old_str \`${oldStr}\` did not appear verbatim in ${virtualPath}.`;
|
|
423
|
+
}
|
|
424
|
+
if (occurrences > 1) {
|
|
425
|
+
const lineNos = findOccurrenceLines(content, oldStr);
|
|
426
|
+
return `No replacement was performed. Multiple occurrences of old_str \`${oldStr}\` in lines: ${lineNos.join(", ")}. Please ensure it is unique`;
|
|
427
|
+
}
|
|
428
|
+
const next = content.replace(oldStr, newStr);
|
|
429
|
+
// Line-cap check on the result
|
|
430
|
+
if (next.split("\n").length > MAX_FILE_LINES) {
|
|
431
|
+
return `File ${virtualPath} exceeds maximum line limit of 999,999 lines.`;
|
|
432
|
+
}
|
|
433
|
+
// Write-time cap on MEMORY.md
|
|
434
|
+
if (this.isIndexPath(virtualPath) && typeof indexCharLimit === "number") {
|
|
435
|
+
const newLen = next.trim().length;
|
|
436
|
+
const oldLen = content.trim().length;
|
|
437
|
+
if (newLen > indexCharLimit) {
|
|
438
|
+
return (`Memory at ${oldLen}/${indexCharLimit} chars. Replacement would push it to ` +
|
|
439
|
+
`${newLen}/${indexCharLimit}. Shorten or remove other entries first.`);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
await this.writeFileAtomic(abs, next);
|
|
443
|
+
// Return success + a snippet-with-line-numbers per Anthropic spec
|
|
444
|
+
const snippetLineNo = content.slice(0, content.indexOf(oldStr)).split("\n").length;
|
|
445
|
+
const snippetLines = next.split("\n");
|
|
446
|
+
const start = Math.max(0, snippetLineNo - 3);
|
|
447
|
+
const end = Math.min(snippetLines.length, snippetLineNo + 5);
|
|
448
|
+
const snippet = snippetLines
|
|
449
|
+
.slice(start, end)
|
|
450
|
+
.map((line, i) => `${fmtLineNo(i + 1 + start)}\t${line}`)
|
|
451
|
+
.join("\n");
|
|
452
|
+
return `The memory file has been edited.\n${snippet}`;
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
// ── insert ───────────────────────────────────────────────────────────
|
|
456
|
+
/**
|
|
457
|
+
* `insert` — insert text at a specific line in a file. `insertLine` is
|
|
458
|
+
* 0-indexed in Anthropic's spec (0 = before first line, N = after last).
|
|
459
|
+
*/
|
|
460
|
+
async insert(agentId, virtualPath, insertLine, insertText, indexCharLimit) {
|
|
461
|
+
const t = this.translatePath(agentId, virtualPath);
|
|
462
|
+
if (!t.ok)
|
|
463
|
+
return `Error: ${t.error}`;
|
|
464
|
+
const abs = t.abs;
|
|
465
|
+
return this.withMutex(agentId, async () => {
|
|
466
|
+
let stat;
|
|
467
|
+
try {
|
|
468
|
+
stat = await fsp.stat(abs);
|
|
469
|
+
}
|
|
470
|
+
catch (e) {
|
|
471
|
+
if (e.code === "ENOENT") {
|
|
472
|
+
return `Error: The path ${virtualPath} does not exist`;
|
|
473
|
+
}
|
|
474
|
+
throw e;
|
|
475
|
+
}
|
|
476
|
+
if (stat.isDirectory()) {
|
|
477
|
+
return `Error: The path ${virtualPath} does not exist`;
|
|
478
|
+
}
|
|
479
|
+
const content = await fsp.readFile(abs, "utf-8");
|
|
480
|
+
const lines = content.split("\n");
|
|
481
|
+
const nLines = lines.length;
|
|
482
|
+
if (insertLine < 0 || insertLine > nLines) {
|
|
483
|
+
return `Error: Invalid \`insert_line\` parameter: ${insertLine}. It should be within the range of lines of the file: [0, ${nLines}]`;
|
|
484
|
+
}
|
|
485
|
+
// Splice in insertText (treated as a single block — caller is
|
|
486
|
+
// responsible for trailing newlines if they want a clean break).
|
|
487
|
+
const next = [
|
|
488
|
+
...lines.slice(0, insertLine),
|
|
489
|
+
insertText,
|
|
490
|
+
...lines.slice(insertLine),
|
|
491
|
+
].join("\n");
|
|
492
|
+
if (next.split("\n").length > MAX_FILE_LINES) {
|
|
493
|
+
return `File ${virtualPath} exceeds maximum line limit of 999,999 lines.`;
|
|
494
|
+
}
|
|
495
|
+
if (this.isIndexPath(virtualPath) && typeof indexCharLimit === "number") {
|
|
496
|
+
const newLen = next.trim().length;
|
|
497
|
+
const oldLen = content.trim().length;
|
|
498
|
+
if (newLen > indexCharLimit) {
|
|
499
|
+
return (`Memory at ${oldLen}/${indexCharLimit} chars. Insertion would push it to ` +
|
|
500
|
+
`${newLen}/${indexCharLimit}. Shorten or remove other entries first.`);
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
await this.writeFileAtomic(abs, next);
|
|
504
|
+
return `The file ${virtualPath} has been edited.`;
|
|
505
|
+
});
|
|
506
|
+
}
|
|
507
|
+
// ── delete ───────────────────────────────────────────────────────────
|
|
508
|
+
/**
|
|
509
|
+
* `delete` — recursively delete a file or directory. Anthropic's spec
|
|
510
|
+
* says "Deletes the directory and all its contents recursively" so
|
|
511
|
+
* we use `rm -rf` semantics for directories.
|
|
512
|
+
*/
|
|
513
|
+
async delete(agentId, virtualPath) {
|
|
514
|
+
const t = this.translatePath(agentId, virtualPath);
|
|
515
|
+
if (!t.ok)
|
|
516
|
+
return `Error: ${t.error}`;
|
|
517
|
+
const abs = t.abs;
|
|
518
|
+
return this.withMutex(agentId, async () => {
|
|
519
|
+
let stat;
|
|
520
|
+
try {
|
|
521
|
+
stat = await fsp.stat(abs);
|
|
522
|
+
}
|
|
523
|
+
catch (e) {
|
|
524
|
+
if (e.code === "ENOENT") {
|
|
525
|
+
return `Error: The path ${virtualPath} does not exist`;
|
|
526
|
+
}
|
|
527
|
+
throw e;
|
|
528
|
+
}
|
|
529
|
+
if (stat.isDirectory()) {
|
|
530
|
+
await fsp.rm(abs, { recursive: true, force: true });
|
|
531
|
+
}
|
|
532
|
+
else {
|
|
533
|
+
await fsp.unlink(abs);
|
|
534
|
+
}
|
|
535
|
+
return `Successfully deleted ${virtualPath}`;
|
|
536
|
+
});
|
|
537
|
+
}
|
|
538
|
+
// ── rename ───────────────────────────────────────────────────────────
|
|
539
|
+
/**
|
|
540
|
+
* `rename` — move or rename a file/directory. Errors if source is
|
|
541
|
+
* missing or destination already exists (no overwrite).
|
|
542
|
+
*/
|
|
543
|
+
async rename(agentId, oldVirtualPath, newVirtualPath) {
|
|
544
|
+
const t1 = this.translatePath(agentId, oldVirtualPath);
|
|
545
|
+
if (!t1.ok)
|
|
546
|
+
return `Error: ${t1.error.replace("does not exist. Please provide a valid path.", "does not exist")}`;
|
|
547
|
+
const t2 = this.translatePath(agentId, newVirtualPath);
|
|
548
|
+
if (!t2.ok)
|
|
549
|
+
return `Error: ${t2.error.replace("does not exist. Please provide a valid path.", "does not exist")}`;
|
|
550
|
+
return this.withMutex(agentId, async () => {
|
|
551
|
+
try {
|
|
552
|
+
await fsp.stat(t1.abs);
|
|
553
|
+
}
|
|
554
|
+
catch (e) {
|
|
555
|
+
if (e.code === "ENOENT") {
|
|
556
|
+
return `Error: The path ${oldVirtualPath} does not exist`;
|
|
557
|
+
}
|
|
558
|
+
throw e;
|
|
559
|
+
}
|
|
560
|
+
try {
|
|
561
|
+
await fsp.stat(t2.abs);
|
|
562
|
+
return `Error: The destination ${newVirtualPath} already exists`;
|
|
563
|
+
}
|
|
564
|
+
catch (e) {
|
|
565
|
+
if (e.code !== "ENOENT")
|
|
566
|
+
throw e;
|
|
567
|
+
}
|
|
568
|
+
await fsp.mkdir(path.dirname(t2.abs), { recursive: true });
|
|
569
|
+
await fsp.rename(t1.abs, t2.abs);
|
|
570
|
+
return `Successfully renamed ${oldVirtualPath} to ${newVirtualPath}`;
|
|
571
|
+
});
|
|
572
|
+
}
|
|
573
|
+
// ── Internals ────────────────────────────────────────────────────────
|
|
574
|
+
/** Atomic write — tmp file + fsync + rename. */
|
|
575
|
+
async writeFileAtomic(absPath, content) {
|
|
576
|
+
const dir = path.dirname(absPath);
|
|
577
|
+
await fsp.mkdir(dir, { recursive: true });
|
|
578
|
+
const tmp = path.join(dir, `${TMP_PREFIX}${randomBytes(8).toString("hex")}.tmp`);
|
|
579
|
+
let fh = null;
|
|
580
|
+
try {
|
|
581
|
+
fh = await fsp.open(tmp, "w");
|
|
582
|
+
await fh.writeFile(content, "utf-8");
|
|
583
|
+
await fh.sync();
|
|
584
|
+
await fh.close();
|
|
585
|
+
fh = null;
|
|
586
|
+
await fsp.rename(tmp, absPath);
|
|
587
|
+
}
|
|
588
|
+
catch (e) {
|
|
589
|
+
if (fh) {
|
|
590
|
+
try {
|
|
591
|
+
await fh.close();
|
|
592
|
+
}
|
|
593
|
+
catch {
|
|
594
|
+
// ignore
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
try {
|
|
598
|
+
await fsp.unlink(tmp);
|
|
599
|
+
}
|
|
600
|
+
catch {
|
|
601
|
+
// ignore — best-effort cleanup
|
|
602
|
+
}
|
|
603
|
+
throw e;
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
/**
|
|
607
|
+
* Per-agent mutex. Map stores only the settle-completion of each
|
|
608
|
+
* outstanding write, never its rejection — so a thrown error inside
|
|
609
|
+
* one writer can't poison the chain for the next.
|
|
610
|
+
*/
|
|
611
|
+
async withMutex(agentId, work) {
|
|
612
|
+
const prev = this.inFlight.get(agentId) ?? Promise.resolve();
|
|
613
|
+
const result = prev.then(work, work);
|
|
614
|
+
this.inFlight.set(agentId, result.then(() => undefined, () => undefined));
|
|
615
|
+
return result;
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
// Export so tests can verify the line-numbering helper.
|
|
619
|
+
export const __test = { withLineNumbers, formatBytes, fmtLineNo };
|
|
620
|
+
//# sourceMappingURL=store.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"store.js","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Threat-pattern data for the memory scanner. Ported verbatim from Hermes
|
|
3
|
+
* `tools/memory_tool.py:67-89`. Kept in its own file (no logic) so the
|
|
4
|
+
* pattern set is easy to audit and update independently of the scanner.
|
|
5
|
+
*
|
|
6
|
+
* `re.IGNORECASE` becomes the JS `i` flag; the regex syntax itself is
|
|
7
|
+
* identical (no Python-only features in these patterns).
|
|
8
|
+
*/
|
|
9
|
+
export declare const MEMORY_THREAT_PATTERNS: ReadonlyArray<readonly [RegExp, string]>;
|
|
10
|
+
export declare const INVISIBLE_CHARS: ReadonlySet<string>;
|
|
11
|
+
//# sourceMappingURL=threat-patterns.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"threat-patterns.d.ts","sourceRoot":"","sources":["../src/threat-patterns.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,eAAO,MAAM,sBAAsB,EAAE,aAAa,CAAC,SAAS,CAAC,MAAM,EAAE,MAAM,CAAC,CAmB3E,CAAC;AAEF,eAAO,MAAM,eAAe,EAAE,WAAW,CAAC,MAAM,CAW9C,CAAC"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Threat-pattern data for the memory scanner. Ported verbatim from Hermes
|
|
3
|
+
* `tools/memory_tool.py:67-89`. Kept in its own file (no logic) so the
|
|
4
|
+
* pattern set is easy to audit and update independently of the scanner.
|
|
5
|
+
*
|
|
6
|
+
* `re.IGNORECASE` becomes the JS `i` flag; the regex syntax itself is
|
|
7
|
+
* identical (no Python-only features in these patterns).
|
|
8
|
+
*/
|
|
9
|
+
export const MEMORY_THREAT_PATTERNS = [
|
|
10
|
+
// Prompt injection
|
|
11
|
+
[/ignore\s+(previous|all|above|prior)\s+instructions/i, "prompt_injection"],
|
|
12
|
+
[/you\s+are\s+now\s+/i, "role_hijack"],
|
|
13
|
+
[/do\s+not\s+tell\s+the\s+user/i, "deception_hide"],
|
|
14
|
+
[/system\s+prompt\s+override/i, "sys_prompt_override"],
|
|
15
|
+
[/disregard\s+(your|all|any)\s+(instructions|rules|guidelines)/i, "disregard_rules"],
|
|
16
|
+
[
|
|
17
|
+
/act\s+as\s+(if|though)\s+you\s+(have\s+no|don't\s+have)\s+(restrictions|limits|rules)/i,
|
|
18
|
+
"bypass_restrictions",
|
|
19
|
+
],
|
|
20
|
+
// Exfiltration via curl/wget with secrets
|
|
21
|
+
[/curl\s+[^\n]*\$\{?\w*(KEY|TOKEN|SECRET|PASSWORD|CREDENTIAL|API)/i, "exfil_curl"],
|
|
22
|
+
[/wget\s+[^\n]*\$\{?\w*(KEY|TOKEN|SECRET|PASSWORD|CREDENTIAL|API)/i, "exfil_wget"],
|
|
23
|
+
[/cat\s+[^\n]*(\.env|credentials|\.netrc|\.pgpass|\.npmrc|\.pypirc)/i, "read_secrets"],
|
|
24
|
+
// Persistence via shell rc / ssh
|
|
25
|
+
[/authorized_keys/i, "ssh_backdoor"],
|
|
26
|
+
[/\$HOME\/\.ssh|~\/\.ssh/i, "ssh_access"],
|
|
27
|
+
[/\$HOME\/\.openacme\/\.env|~\/\.openacme\/\.env/i, "openacme_env"],
|
|
28
|
+
];
|
|
29
|
+
export const INVISIBLE_CHARS = new Set([
|
|
30
|
+
"",
|
|
31
|
+
"",
|
|
32
|
+
"",
|
|
33
|
+
"",
|
|
34
|
+
"",
|
|
35
|
+
"",
|
|
36
|
+
"",
|
|
37
|
+
"",
|
|
38
|
+
"",
|
|
39
|
+
"",
|
|
40
|
+
]);
|
|
41
|
+
//# sourceMappingURL=threat-patterns.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"threat-patterns.js","sourceRoot":"","sources":["../src/threat-patterns.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,MAAM,CAAC,MAAM,sBAAsB,GAA6C;IAC9E,mBAAmB;IACnB,CAAC,qDAAqD,EAAE,kBAAkB,CAAC;IAC3E,CAAC,qBAAqB,EAAE,aAAa,CAAC;IACtC,CAAC,+BAA+B,EAAE,gBAAgB,CAAC;IACnD,CAAC,6BAA6B,EAAE,qBAAqB,CAAC;IACtD,CAAC,+DAA+D,EAAE,iBAAiB,CAAC;IACpF;QACE,wFAAwF;QACxF,qBAAqB;KACtB;IACD,0CAA0C;IAC1C,CAAC,kEAAkE,EAAE,YAAY,CAAC;IAClF,CAAC,kEAAkE,EAAE,YAAY,CAAC;IAClF,CAAC,oEAAoE,EAAE,cAAc,CAAC;IACtF,iCAAiC;IACjC,CAAC,kBAAkB,EAAE,cAAc,CAAC;IACpC,CAAC,yBAAyB,EAAE,YAAY,CAAC;IACzC,CAAC,iDAAiD,EAAE,cAAc,CAAC;CACpE,CAAC;AAEF,MAAM,CAAC,MAAM,eAAe,GAAwB,IAAI,GAAG,CAAS;IAClE,GAAG;IACH,GAAG;IACH,GAAG;IACH,GAAG;IACH,GAAG;IACH,GAAG;IACH,GAAG;IACH,GAAG;IACH,GAAG;IACH,GAAG;CACJ,CAAC,CAAC"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Memory content threat scanner.
|
|
3
|
+
*
|
|
4
|
+
* Memory entries get injected into the system prompt verbatim, so they're
|
|
5
|
+
* a privileged channel — anything that would prompt-inject the agent or
|
|
6
|
+
* exfiltrate secrets must be blocked at the gate. Scanner is a pure
|
|
7
|
+
* function over the pattern set in `./threat-patterns.ts`.
|
|
8
|
+
*/
|
|
9
|
+
export type ScanResult = {
|
|
10
|
+
ok: true;
|
|
11
|
+
} | {
|
|
12
|
+
ok: false;
|
|
13
|
+
reason: string;
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* Scan content destined for MEMORY.md. Returns `{ok:true}` if safe;
|
|
17
|
+
* otherwise an error string in the same format Hermes uses (so error
|
|
18
|
+
* wording stays consistent between ports).
|
|
19
|
+
*/
|
|
20
|
+
export declare function scanMemoryContent(content: string): ScanResult;
|
|
21
|
+
//# sourceMappingURL=threat-scanner.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"threat-scanner.d.ts","sourceRoot":"","sources":["../src/threat-scanner.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAIH,MAAM,MAAM,UAAU,GAClB;IAAE,EAAE,EAAE,IAAI,CAAA;CAAE,GACZ;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAElC;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,GAAG,UAAU,CAqB7D"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Memory content threat scanner.
|
|
3
|
+
*
|
|
4
|
+
* Memory entries get injected into the system prompt verbatim, so they're
|
|
5
|
+
* a privileged channel — anything that would prompt-inject the agent or
|
|
6
|
+
* exfiltrate secrets must be blocked at the gate. Scanner is a pure
|
|
7
|
+
* function over the pattern set in `./threat-patterns.ts`.
|
|
8
|
+
*/
|
|
9
|
+
import { INVISIBLE_CHARS, MEMORY_THREAT_PATTERNS } from "./threat-patterns.js";
|
|
10
|
+
/**
|
|
11
|
+
* Scan content destined for MEMORY.md. Returns `{ok:true}` if safe;
|
|
12
|
+
* otherwise an error string in the same format Hermes uses (so error
|
|
13
|
+
* wording stays consistent between ports).
|
|
14
|
+
*/
|
|
15
|
+
export function scanMemoryContent(content) {
|
|
16
|
+
for (const ch of content) {
|
|
17
|
+
if (INVISIBLE_CHARS.has(ch)) {
|
|
18
|
+
const codepoint = ch.codePointAt(0).toString(16).toUpperCase().padStart(4, "0");
|
|
19
|
+
return {
|
|
20
|
+
ok: false,
|
|
21
|
+
reason: `Blocked: content contains invisible unicode character U+${codepoint} (possible injection).`,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
for (const [pattern, id] of MEMORY_THREAT_PATTERNS) {
|
|
26
|
+
if (pattern.test(content)) {
|
|
27
|
+
return {
|
|
28
|
+
ok: false,
|
|
29
|
+
reason: `Blocked: content matches threat pattern '${id}'. Memory entries are injected into the system prompt and must not contain injection or exfiltration payloads.`,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return { ok: true };
|
|
34
|
+
}
|
|
35
|
+
//# sourceMappingURL=threat-scanner.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"threat-scanner.js","sourceRoot":"","sources":["../src/threat-scanner.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,eAAe,EAAE,sBAAsB,EAAE,MAAM,sBAAsB,CAAC;AAM/E;;;;GAIG;AACH,MAAM,UAAU,iBAAiB,CAAC,OAAe;IAC/C,KAAK,MAAM,EAAE,IAAI,OAAO,EAAE,CAAC;QACzB,IAAI,eAAe,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE,CAAC;YAC5B,MAAM,SAAS,GAAG,EAAE,CAAC,WAAW,CAAC,CAAC,CAAE,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,WAAW,EAAE,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;YACjF,OAAO;gBACL,EAAE,EAAE,KAAK;gBACT,MAAM,EAAE,2DAA2D,SAAS,wBAAwB;aACrG,CAAC;QACJ,CAAC;IACH,CAAC;IAED,KAAK,MAAM,CAAC,OAAO,EAAE,EAAE,CAAC,IAAI,sBAAsB,EAAE,CAAC;QACnD,IAAI,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC;YAC1B,OAAO;gBACL,EAAE,EAAE,KAAK;gBACT,MAAM,EAAE,4CAA4C,EAAE,gHAAgH;aACvK,CAAC;QACJ,CAAC;IACH,CAAC;IAED,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,CAAC;AACtB,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@openacme/memory",
|
|
3
|
+
"version": "0.4.0",
|
|
4
|
+
"description": "Per-agent persistent memory for OpenAcme — bounded MEMORY.md store with atomic writes and threat scanning.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Utkarsh Kanwat",
|
|
7
|
+
"homepage": "https://github.com/ukanwat/OpenAcme/tree/main/packages/memory#readme",
|
|
8
|
+
"bugs": "https://github.com/ukanwat/OpenAcme/issues",
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://github.com/ukanwat/OpenAcme.git",
|
|
12
|
+
"directory": "packages/memory"
|
|
13
|
+
},
|
|
14
|
+
"publishConfig": {
|
|
15
|
+
"access": "public",
|
|
16
|
+
"provenance": false
|
|
17
|
+
},
|
|
18
|
+
"type": "module",
|
|
19
|
+
"main": "./dist/index.js",
|
|
20
|
+
"types": "./dist/index.d.ts",
|
|
21
|
+
"exports": {
|
|
22
|
+
".": {
|
|
23
|
+
"types": "./dist/index.d.ts",
|
|
24
|
+
"import": "./dist/index.js"
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
"files": [
|
|
28
|
+
"dist",
|
|
29
|
+
"README.md",
|
|
30
|
+
"LICENSE"
|
|
31
|
+
],
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"@types/node": "^22.15.3",
|
|
34
|
+
"typescript": "5.9.2",
|
|
35
|
+
"vitest": "^2.1.9",
|
|
36
|
+
"@repo/typescript-config": "0.0.0"
|
|
37
|
+
},
|
|
38
|
+
"scripts": {
|
|
39
|
+
"build": "tsc --build",
|
|
40
|
+
"check-types": "tsc --noEmit",
|
|
41
|
+
"dev": "tsc --watch",
|
|
42
|
+
"test": "vitest run"
|
|
43
|
+
}
|
|
44
|
+
}
|