@ai-ecoverse/slicc-agent 3.13.0 → 3.14.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.
Files changed (68) hide show
  1. package/README.md +20 -3
  2. package/bin/agent +1 -1
  3. package/dist/agents.js.map +2 -2
  4. package/dist/client.js +5 -1
  5. package/dist/client.js.map +2 -2
  6. package/dist/control.js +14 -1
  7. package/dist/control.js.map +2 -2
  8. package/dist/host.js +6 -2
  9. package/dist/host.js.map +2 -2
  10. package/dist/main.js +19 -2
  11. package/dist/main.js.map +2 -2
  12. package/dist/memory/context.js +91 -0
  13. package/dist/memory/context.js.map +7 -0
  14. package/dist/memory/format.js +251 -0
  15. package/dist/memory/format.js.map +7 -0
  16. package/dist/memory/index.js +32 -0
  17. package/dist/memory/index.js.map +7 -0
  18. package/dist/memory/prompt.js +175 -0
  19. package/dist/memory/prompt.js.map +7 -0
  20. package/dist/memory/runtime.js +194 -0
  21. package/dist/memory/runtime.js.map +7 -0
  22. package/dist/memory/store.js +153 -0
  23. package/dist/memory/store.js.map +7 -0
  24. package/dist/roles/frontmatter.js +9 -4
  25. package/dist/roles/frontmatter.js.map +2 -2
  26. package/dist/roles/roles.js +55 -11
  27. package/dist/roles/roles.js.map +2 -2
  28. package/dist/scoops/cli.js +3 -0
  29. package/dist/scoops/cli.js.map +2 -2
  30. package/dist/scoops/index.js +3 -2
  31. package/dist/scoops/index.js.map +2 -2
  32. package/dist/scoops/requests.js +1 -1
  33. package/dist/scoops/requests.js.map +2 -2
  34. package/dist/scoops/service.js +3 -0
  35. package/dist/scoops/service.js.map +2 -2
  36. package/dist/scoops/sync.js +1 -0
  37. package/dist/scoops/sync.js.map +2 -2
  38. package/dist/services.js.map +2 -2
  39. package/dist/skills/index.js +8 -1
  40. package/dist/skills/index.js.map +2 -2
  41. package/dist/spectrum/agent-port.js +0 -2
  42. package/dist/spectrum/agent-port.js.map +2 -2
  43. package/dist/spectrum/index.js +5 -1
  44. package/dist/spectrum/index.js.map +3 -3
  45. package/dist/spectrum/memory-port.js +64 -0
  46. package/dist/spectrum/memory-port.js.map +7 -0
  47. package/dist/types/agents.d.ts +7 -0
  48. package/dist/types/client.d.ts +3 -0
  49. package/dist/types/control.d.ts +2 -1
  50. package/dist/types/host.d.ts +2 -0
  51. package/dist/types/memory/context.d.ts +27 -0
  52. package/dist/types/memory/format.d.ts +67 -0
  53. package/dist/types/memory/index.d.ts +12 -0
  54. package/dist/types/memory/prompt.d.ts +48 -0
  55. package/dist/types/memory/runtime.d.ts +45 -0
  56. package/dist/types/memory/store.d.ts +41 -0
  57. package/dist/types/roles/frontmatter.d.ts +1 -1
  58. package/dist/types/roles/roles.d.ts +8 -0
  59. package/dist/types/scoops/cli.d.ts +1 -0
  60. package/dist/types/scoops/index.d.ts +1 -0
  61. package/dist/types/scoops/requests.d.ts +1 -1
  62. package/dist/types/scoops/service.d.ts +1 -0
  63. package/dist/types/services.d.ts +15 -0
  64. package/dist/types/spectrum/agent-port.d.ts +0 -1
  65. package/dist/types/spectrum/index.d.ts +3 -0
  66. package/dist/types/spectrum/memory-port.d.ts +12 -0
  67. package/package.json +4 -4
  68. package/packages/vfs-root/skills/memory/SKILL.md +48 -0
@@ -0,0 +1,194 @@
1
+ import {
2
+ replicatedState
3
+ } from "@earendil-works/chord";
4
+ import { workspace } from "../scoops/service.js";
5
+ import { MEMORY_FILE } from "./format.js";
6
+ import { MEMORY_WRITE } from "./prompt.js";
7
+ import {
8
+ GLOBAL,
9
+ MemoryFiles,
10
+ memoryRoot,
11
+ placeOf,
12
+ ROLE_PREFIX,
13
+ rolePlace,
14
+ roleRoot
15
+ } from "./store.js";
16
+ const USAGE = `usage:
17
+ memory show [<scope>] print a memory file (yours by default)
18
+ memory scopes every memory scope, its entries and its file
19
+
20
+ Scopes: global (everyone), a cone's id, or role:<path> for a role's memory.
21
+ Agents write memory with the ${MEMORY_WRITE} tool; the files are plain markdown you can edit.
22
+ `;
23
+ async function roleDirs(env, dir, prefix, context, depth = 0) {
24
+ const listed = await env.listDir(dir, context);
25
+ if (!listed.ok || depth > 4) return [];
26
+ const out = [];
27
+ for (const info of listed.value) {
28
+ if (info.kind !== "directory") continue;
29
+ const path = prefix ? `${prefix}/${info.name}` : info.name;
30
+ const found = await env.exists(`${dir}/${info.name}/${MEMORY_FILE}`, context);
31
+ if (found.ok && found.value) out.push(path);
32
+ out.push(...await roleDirs(env, `${dir}/${info.name}`, path, context, depth + 1));
33
+ }
34
+ return out;
35
+ }
36
+ async function listPlaces(options, context) {
37
+ const { agents, env, home } = options;
38
+ const state = agents.state();
39
+ const out = [{ ...placeOf(home, GLOBAL), name: "Everyone" }];
40
+ const cones = new Set(Object.keys(state.cones));
41
+ const stored = await env.listDir(memoryRoot(home), context);
42
+ if (stored.ok) {
43
+ for (const info of stored.value) if (info.kind === "directory") cones.add(info.name);
44
+ }
45
+ for (const id of cones) {
46
+ const place = placeOf(home, id);
47
+ if (place) out.push({ ...place, group: "cones", name: state.cones[id]?.name ?? id });
48
+ }
49
+ const roles = new Set(await roleDirs(env, roleRoot(home), "", context));
50
+ const known = await options.roles?.(context).catch(() => void 0);
51
+ for (const role of known?.roles ?? [])
52
+ if (role.memory?.scope === "user") roles.add(role.memory.path);
53
+ for (const path of [...roles].sort()) {
54
+ const place = placeOf(home, `${ROLE_PREFIX}${path}`);
55
+ if (place) out.push({ ...place, group: "roles", name: path.split("/").at(-1) ?? path });
56
+ }
57
+ return out;
58
+ }
59
+ async function callerPlace(options, caller, context) {
60
+ const { agents, env, home } = options;
61
+ const record = caller ? agents.state().scoops[caller] : void 0;
62
+ if (record?.memory)
63
+ return rolePlace(env, home, record.memory, record.origin ?? workspace(record.folder), context);
64
+ const cone = caller?.startsWith("cone:") ? caller.slice("cone:".length) : agents.activeCone();
65
+ return placeOf(home, cone) ?? { problem: `there is no memory scope ${cone}` };
66
+ }
67
+ function placeFor(home, scope) {
68
+ const place = placeOf(home, scope);
69
+ if (!place) throw new Error(`there is no memory scope ${scope}`);
70
+ return place;
71
+ }
72
+ async function save(parts, draft, context) {
73
+ const { files, memories, options } = parts;
74
+ const place = placeFor(options.home, draft.scope);
75
+ const previous = draft.id ? memories.value.find((item) => item.id === draft.id) : void 0;
76
+ const moved = previous && previous.scope !== draft.scope;
77
+ try {
78
+ const written = await files.change(
79
+ place,
80
+ {
81
+ kind: "save",
82
+ ...previous && !moved ? { id: previous.id } : {},
83
+ section: draft.section,
84
+ title: draft.title,
85
+ body: draft.body,
86
+ tag: draft.tag
87
+ },
88
+ context
89
+ );
90
+ if (previous && moved)
91
+ await files.change(
92
+ placeFor(options.home, previous.scope),
93
+ { kind: "remove", id: previous.id },
94
+ context
95
+ );
96
+ return written.entry;
97
+ } finally {
98
+ await parts.reload(context).catch(() => void 0);
99
+ }
100
+ }
101
+ async function command(parts, argv, caller, context) {
102
+ const { files, memories, scopes, options } = parts;
103
+ const verb = argv[0] ?? "help";
104
+ if (verb === "scopes") {
105
+ await parts.reload(context);
106
+ const rows = scopes.value.map((scope) => {
107
+ const count = memories.value.filter((item) => item.scope === scope.id).length;
108
+ const file = placeFor(options.home, scope.id).file;
109
+ return `${scope.id} ${scope.label} ${count} entr${count === 1 ? "y" : "ies"} ${file}`;
110
+ });
111
+ return { code: 0, out: `${rows.join("\n")}
112
+ ` };
113
+ }
114
+ if (verb !== "show") return { code: verb === "help" || verb === "--help" ? 0 : 2, out: USAGE };
115
+ const place = argv[1] ? placeOf(options.home, argv[1]) ?? { problem: `there is no memory scope ${argv[1]}` } : await callerPlace(options, caller, context);
116
+ if ("problem" in place) return { code: 1, out: `memory: ${place.problem}
117
+ ` };
118
+ const read = await files.read(place.file, context);
119
+ return {
120
+ code: 0,
121
+ out: read?.text.trim() ? `# ${place.file}
122
+
123
+ ${read.text.trimEnd()}
124
+ ` : `${place.file} is empty
125
+ `
126
+ };
127
+ }
128
+ async function attachMemory(options, connect, context) {
129
+ const { agents, env, home } = options;
130
+ const files = new MemoryFiles(env);
131
+ const memories = replicatedState([]);
132
+ const scopes = replicatedState([]);
133
+ const reload = async (using) => {
134
+ const found = await listPlaces(options, using);
135
+ const entries = [];
136
+ for (const place of found) entries.push(...await files.entries(place, using));
137
+ scopes.replace(
138
+ using,
139
+ found.map((place) => ({
140
+ id: place.scope,
141
+ label: place.name,
142
+ ...place.group ? { group: place.group } : {}
143
+ }))
144
+ );
145
+ memories.replace(using, entries);
146
+ };
147
+ const parts = { options, files, memories, scopes, reload };
148
+ connect({ agents, env, home, files, changed: reload });
149
+ await reload(context).catch(() => void 0);
150
+ let timer;
151
+ const schedule = () => {
152
+ clearTimeout(timer);
153
+ timer = setTimeout(() => void reload(context).catch(() => void 0), options.reloadMs);
154
+ };
155
+ const offCone = agents.onCone(schedule);
156
+ const watched = await env.watch(
157
+ [
158
+ { path: memoryRoot(home), recursive: true },
159
+ { path: roleRoot(home), recursive: true }
160
+ ],
161
+ schedule,
162
+ context
163
+ );
164
+ const watcher = watched.ok ? watched.value : void 0;
165
+ return {
166
+ memories,
167
+ scopes,
168
+ save: (draft, using) => save(parts, draft, using),
169
+ async remove(id, using) {
170
+ const entry = memories.value.find((item) => item.id === id);
171
+ if (!entry) return false;
172
+ const written = await files.change(
173
+ placeFor(home, entry.scope),
174
+ { kind: "remove", id },
175
+ using
176
+ );
177
+ await reload(using);
178
+ return written.found;
179
+ },
180
+ command: (argv, caller, using) => command(parts, argv, caller, using),
181
+ reload,
182
+ async close(using) {
183
+ offCone();
184
+ clearTimeout(timer);
185
+ await watcher?.close(using);
186
+ }
187
+ };
188
+ }
189
+ export {
190
+ USAGE,
191
+ attachMemory,
192
+ listPlaces
193
+ };
194
+ //# sourceMappingURL=runtime.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/memory/runtime.ts"],
4
+ "sourcesContent": ["import {\n type Context,\n type MutableReplicatedState,\n type ReplicatedState,\n replicatedState,\n} from '@earendil-works/chord';\nimport type { ExecutionEnv, FileWatcher } from '@earendil-works/pi-durable/env';\nimport type { Agents } from '../agents.ts';\nimport type { Role } from '../roles/roles.ts';\nimport { workspace } from '../scoops/service.ts';\nimport { MEMORY_FILE, type MemoryEntry, type MemoryScope, type MemoryTag } from './format.ts';\nimport type { Host } from './prompt.ts';\nimport { MEMORY_WRITE } from './prompt.ts';\nimport {\n GLOBAL,\n MemoryFiles,\n memoryRoot,\n type Place,\n placeOf,\n ROLE_PREFIX,\n rolePlace,\n roleRoot,\n} from './store.ts';\n\nexport type Answer = { code: number; out: string };\n\nexport interface MemoryDraft {\n id?: string;\n scope: string;\n section: string;\n title: string;\n body: string;\n tag: MemoryTag | null;\n}\n\nexport interface MemoryRuntime {\n readonly memories: ReplicatedState<MemoryEntry[]>;\n readonly scopes: ReplicatedState<MemoryScope[]>;\n save(draft: MemoryDraft, context: Context): Promise<MemoryEntry>;\n remove(id: string, context: Context): Promise<boolean>;\n command(argv: readonly string[], caller: string | null, context: Context): Promise<Answer>;\n reload(context: Context): Promise<void>;\n close(context: Context): Promise<void>;\n}\n\nexport interface MemoryAttach {\n agents: Agents;\n env: ExecutionEnv;\n home: string;\n reloadMs: number;\n roles?: (context: Context) => Promise<{ roles: readonly Role[] } | undefined>;\n}\n\nexport const USAGE = `usage:\n memory show [<scope>] print a memory file (yours by default)\n memory scopes every memory scope, its entries and its file\n\nScopes: global (everyone), a cone's id, or role:<path> for a role's memory.\nAgents write memory with the ${MEMORY_WRITE} tool; the files are plain markdown you can edit.\n`;\n\ntype Listed = Place & { group?: 'cones' | 'roles'; name: string };\n\nasync function roleDirs(\n env: ExecutionEnv,\n dir: string,\n prefix: string,\n context: Context,\n depth = 0\n): Promise<string[]> {\n const listed = await env.listDir(dir, context);\n if (!listed.ok || depth > 4) return [];\n const out: string[] = [];\n for (const info of listed.value) {\n if (info.kind !== 'directory') continue;\n const path = prefix ? `${prefix}/${info.name}` : info.name;\n const found = await env.exists(`${dir}/${info.name}/${MEMORY_FILE}`, context);\n if (found.ok && found.value) out.push(path);\n out.push(...(await roleDirs(env, `${dir}/${info.name}`, path, context, depth + 1)));\n }\n return out;\n}\n\nexport async function listPlaces(options: MemoryAttach, context: Context): Promise<Listed[]> {\n const { agents, env, home } = options;\n const state = agents.state();\n const out: Listed[] = [{ ...(placeOf(home, GLOBAL) as Place), name: 'Everyone' }];\n const cones = new Set(Object.keys(state.cones));\n const stored = await env.listDir(memoryRoot(home), context);\n if (stored.ok)\n for (const info of stored.value) if (info.kind === 'directory') cones.add(info.name);\n for (const id of cones) {\n const place = placeOf(home, id);\n if (place) out.push({ ...place, group: 'cones', name: state.cones[id]?.name ?? id });\n }\n const roles = new Set(await roleDirs(env, roleRoot(home), '', context));\n const known = await options.roles?.(context).catch(() => undefined);\n for (const role of known?.roles ?? [])\n if (role.memory?.scope === 'user') roles.add(role.memory.path);\n for (const path of [...roles].sort()) {\n const place = placeOf(home, `${ROLE_PREFIX}${path}`);\n if (place) out.push({ ...place, group: 'roles', name: path.split('/').at(-1) ?? path });\n }\n return out;\n}\n\nasync function callerPlace(\n options: MemoryAttach,\n caller: string | null,\n context: Context\n): Promise<Place | { problem: string }> {\n const { agents, env, home } = options;\n const record = caller ? agents.state().scoops[caller] : undefined;\n if (record?.memory)\n return rolePlace(env, home, record.memory, record.origin ?? workspace(record.folder), context);\n const cone = caller?.startsWith('cone:') ? caller.slice('cone:'.length) : agents.activeCone();\n return placeOf(home, cone) ?? { problem: `there is no memory scope ${cone}` };\n}\n\ntype Parts = {\n options: MemoryAttach;\n files: MemoryFiles;\n memories: MutableReplicatedState<MemoryEntry[]>;\n scopes: MutableReplicatedState<MemoryScope[]>;\n reload: (context: Context) => Promise<void>;\n};\n\nfunction placeFor(home: string, scope: string): Place {\n const place = placeOf(home, scope);\n if (!place) throw new Error(`there is no memory scope ${scope}`);\n return place;\n}\n\nasync function save(parts: Parts, draft: MemoryDraft, context: Context): Promise<MemoryEntry> {\n const { files, memories, options } = parts;\n const place = placeFor(options.home, draft.scope);\n const previous = draft.id ? memories.value.find((item) => item.id === draft.id) : undefined;\n const moved = previous && previous.scope !== draft.scope;\n try {\n const written = await files.change(\n place,\n {\n kind: 'save',\n ...(previous && !moved ? { id: previous.id } : {}),\n section: draft.section,\n title: draft.title,\n body: draft.body,\n tag: draft.tag,\n },\n context\n );\n if (previous && moved)\n await files.change(\n placeFor(options.home, previous.scope),\n { kind: 'remove', id: previous.id },\n context\n );\n return written.entry as MemoryEntry;\n } finally {\n await parts.reload(context).catch(() => undefined);\n }\n}\n\nasync function command(\n parts: Parts,\n argv: readonly string[],\n caller: string | null,\n context: Context\n): Promise<Answer> {\n const { files, memories, scopes, options } = parts;\n const verb = argv[0] ?? 'help';\n if (verb === 'scopes') {\n await parts.reload(context);\n const rows = scopes.value.map((scope) => {\n const count = memories.value.filter((item) => item.scope === scope.id).length;\n const file = placeFor(options.home, scope.id).file;\n return `${scope.id}\\t${scope.label}\\t${count} entr${count === 1 ? 'y' : 'ies'}\\t${file}`;\n });\n return { code: 0, out: `${rows.join('\\n')}\\n` };\n }\n if (verb !== 'show') return { code: verb === 'help' || verb === '--help' ? 0 : 2, out: USAGE };\n const place = argv[1]\n ? (placeOf(options.home, argv[1]) ?? { problem: `there is no memory scope ${argv[1]}` })\n : await callerPlace(options, caller, context);\n if ('problem' in place) return { code: 1, out: `memory: ${place.problem}\\n` };\n const read = await files.read(place.file, context);\n return {\n code: 0,\n out: read?.text.trim()\n ? `# ${place.file}\\n\\n${read.text.trimEnd()}\\n`\n : `${place.file} is empty\\n`,\n };\n}\n\nexport async function attachMemory(\n options: MemoryAttach,\n connect: (host: Host) => void,\n context: Context\n): Promise<MemoryRuntime> {\n const { agents, env, home } = options;\n const files = new MemoryFiles(env);\n const memories = replicatedState<MemoryEntry[]>([]);\n const scopes = replicatedState<MemoryScope[]>([]);\n const reload = async (using: Context) => {\n const found = await listPlaces(options, using);\n const entries: MemoryEntry[] = [];\n for (const place of found) entries.push(...(await files.entries(place, using)));\n scopes.replace(\n using,\n found.map((place) => ({\n id: place.scope,\n label: place.name,\n ...(place.group ? { group: place.group } : {}),\n }))\n );\n memories.replace(using, entries);\n };\n const parts: Parts = { options, files, memories, scopes, reload };\n connect({ agents, env, home, files, changed: reload });\n await reload(context).catch(() => undefined);\n let timer: ReturnType<typeof setTimeout> | undefined;\n const schedule = () => {\n clearTimeout(timer);\n timer = setTimeout(() => void reload(context).catch(() => undefined), options.reloadMs);\n };\n const offCone = agents.onCone(schedule);\n const watched = await env.watch(\n [\n { path: memoryRoot(home), recursive: true },\n { path: roleRoot(home), recursive: true },\n ],\n schedule,\n context\n );\n const watcher: FileWatcher | undefined = watched.ok ? watched.value : undefined;\n return {\n memories,\n scopes,\n save: (draft, using) => save(parts, draft, using),\n async remove(id, using) {\n const entry = memories.value.find((item) => item.id === id);\n if (!entry) return false;\n const written = await files.change(\n placeFor(home, entry.scope),\n { kind: 'remove', id },\n using\n );\n await reload(using);\n return written.found;\n },\n command: (argv, caller, using) => command(parts, argv, caller, using),\n reload,\n async close(using) {\n offCone();\n clearTimeout(timer);\n await watcher?.close(using);\n },\n };\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -0,0 +1,153 @@
1
+ import { trusted } from "./context.js";
2
+ import {
3
+ applyChange,
4
+ byteLength,
5
+ MAX_BYTES,
6
+ MEMORY_FILE,
7
+ memoryEntries,
8
+ oneLine,
9
+ parseMemory,
10
+ redactSecrets,
11
+ serializeMemory
12
+ } from "./format.js";
13
+ const GLOBAL = "global";
14
+ const ROLE_PREFIX = "role:";
15
+ function memoryRoot(home) {
16
+ return `${home}/.pi/agent/memory`;
17
+ }
18
+ function roleRoot(home) {
19
+ return `${home}/.pi/agent/agent-memory`;
20
+ }
21
+ function safePath(path) {
22
+ const parts = path.split("/");
23
+ if (!parts.length || parts.some((part) => !/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(part)))
24
+ return null;
25
+ return parts.join("/");
26
+ }
27
+ function placeOf(home, scope) {
28
+ if (scope === GLOBAL)
29
+ return { scope, file: `${memoryRoot(home)}/${MEMORY_FILE}`, label: "everyone" };
30
+ if (scope.startsWith(ROLE_PREFIX) && scope.includes("@")) {
31
+ const [named, root] = scope.slice(ROLE_PREFIX.length).split("@");
32
+ const path = safePath(named);
33
+ const inside = root.startsWith("/") && safePath(root.slice(1)) !== null && trusted(root);
34
+ return path && inside ? {
35
+ scope,
36
+ file: `${root}/.pi/agent-memory/${path}/${MEMORY_FILE}`,
37
+ label: `role ${path} in ${root}`
38
+ } : null;
39
+ }
40
+ if (scope.startsWith(ROLE_PREFIX)) {
41
+ const path = safePath(scope.slice(ROLE_PREFIX.length));
42
+ return path ? { scope, file: `${roleRoot(home)}/${path}/${MEMORY_FILE}`, label: `role ${path}` } : null;
43
+ }
44
+ const cone = safePath(scope);
45
+ return cone && !cone.includes("/") ? { scope, file: `${memoryRoot(home)}/${cone}/${MEMORY_FILE}`, label: `cone ${cone}` } : null;
46
+ }
47
+ async function projectRoot(env, cwd, context) {
48
+ let dir = cwd.replace(/\/+$/, "") || "/";
49
+ for (; ; ) {
50
+ for (const marker of [".pi", ".git"]) {
51
+ const found = await env.exists(`${dir === "/" ? "" : dir}/${marker}`, context);
52
+ if (found.ok && found.value) return dir;
53
+ }
54
+ if (dir === "/") return null;
55
+ const cut = dir.lastIndexOf("/");
56
+ dir = cut <= 0 ? "/" : dir.slice(0, cut);
57
+ }
58
+ }
59
+ async function rolePlace(env, home, memory, cwd, context) {
60
+ const path = safePath(memory.path);
61
+ if (!path) return { problem: `memory path "${memory.path}" is not a safe relative path` };
62
+ if (memory.scope === "user") return placeOf(home, `${ROLE_PREFIX}${path}`);
63
+ const root = await projectRoot(env, cwd, context);
64
+ if (!root) return { problem: `no project root (.pi or .git) above ${cwd}` };
65
+ if (!trusted(root))
66
+ return {
67
+ problem: `the project root ${root} is outside /home; it gets memory once SLICC can trust a folder`
68
+ };
69
+ return placeOf(home, `${ROLE_PREFIX}${path}@${root}`);
70
+ }
71
+ function dirOf(file) {
72
+ return file.slice(0, file.lastIndexOf("/")) || "/";
73
+ }
74
+ class MemoryFiles {
75
+ #env;
76
+ #chains = /* @__PURE__ */ new Map();
77
+ constructor(env) {
78
+ this.#env = env;
79
+ }
80
+ async #safe(file, context) {
81
+ for (let path = file; ; path = dirOf(path)) {
82
+ const real = await this.#env.canonicalPath(path, context);
83
+ if (real.ok) return real.value === path;
84
+ if (path === "/") return true;
85
+ }
86
+ }
87
+ async read(file, context) {
88
+ if (!await this.#safe(file, context)) return null;
89
+ const read = await this.#env.readTextFile(file, context);
90
+ if (!read.ok) return null;
91
+ const info = await this.#env.fileInfo(file, context);
92
+ return { text: read.value, updatedAt: info.ok ? info.value.mtimeMs : 0 };
93
+ }
94
+ async entries(place, context) {
95
+ const read = await this.read(place.file, context);
96
+ return read ? memoryEntries(parseMemory(read.text), place.scope, read.updatedAt) : [];
97
+ }
98
+ change(place, change, context) {
99
+ const previous = this.#chains.get(place.file) ?? Promise.resolve();
100
+ const next = previous.catch(() => void 0).then(() => this.#apply(place, change, context));
101
+ this.#chains.set(place.file, next);
102
+ void next.finally(() => {
103
+ if (this.#chains.get(place.file) === next) this.#chains.delete(place.file);
104
+ }).catch(() => void 0);
105
+ return next;
106
+ }
107
+ async #apply(place, change, context) {
108
+ if (!await this.#safe(place.file, context))
109
+ throw new Error(`${place.file} is a symbolic link or under one; memory is not written there`);
110
+ const read = await this.#env.readTextFile(place.file, context);
111
+ const before = read.ok ? read.value : "";
112
+ let redacted = 0;
113
+ const clean = (text) => {
114
+ const result = redactSecrets(text);
115
+ redacted += result.count;
116
+ return result.text;
117
+ };
118
+ const safe = change.kind === "save" ? {
119
+ ...change,
120
+ section: oneLine(clean(change.section)) || "Notes",
121
+ title: oneLine(clean(change.title)) || "Untitled",
122
+ body: clean(change.body)
123
+ } : change;
124
+ const { doc, found } = applyChange(parseMemory(before), place.scope, safe);
125
+ const after = serializeMemory(doc);
126
+ const bytes = byteLength(after);
127
+ if (bytes > MAX_BYTES && bytes >= byteLength(before))
128
+ throw new Error(
129
+ `${place.file} would be ${bytes} bytes; memory files hold ${MAX_BYTES}. Remove or shorten entries first (a write over the limit must make the file smaller).`
130
+ );
131
+ if (change.kind === "remove" && !found)
132
+ return { entry: null, found, bytes: byteLength(before), redacted, file: place.file };
133
+ await this.#env.createDir(dirOf(place.file), { recursive: true }, context);
134
+ const written = await this.#env.writeFile(place.file, after, context);
135
+ if (!written.ok) throw new Error(written.error.message);
136
+ const entry = safe.kind === "save" ? memoryEntries(doc, place.scope, Date.now()).find(
137
+ (item) => item.source === "entry" && item.section.toLowerCase() === safe.section.toLowerCase() && item.title.toLowerCase() === safe.title.toLowerCase()
138
+ ) : null;
139
+ return { entry, found, bytes, redacted, file: place.file };
140
+ }
141
+ }
142
+ export {
143
+ GLOBAL,
144
+ MemoryFiles,
145
+ ROLE_PREFIX,
146
+ memoryRoot,
147
+ placeOf,
148
+ projectRoot,
149
+ rolePlace,
150
+ roleRoot,
151
+ safePath
152
+ };
153
+ //# sourceMappingURL=store.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/memory/store.ts"],
4
+ "sourcesContent": ["import type { Context } from '@earendil-works/chord';\nimport type { ExecutionEnv } from '@earendil-works/pi-durable/env';\nimport { trusted } from './context.ts';\nimport {\n applyChange,\n byteLength,\n type Change,\n MAX_BYTES,\n MEMORY_FILE,\n type MemoryEntry,\n memoryEntries,\n oneLine,\n parseMemory,\n redactSecrets,\n serializeMemory,\n} from './format.ts';\n\nexport const GLOBAL = 'global';\nexport const ROLE_PREFIX = 'role:';\n\nexport type RoleMemory = { scope: 'user' | 'project'; path: string };\n\nexport type Place = { scope: string; file: string; label: string };\n\nexport type Written = {\n entry: MemoryEntry | null;\n found: boolean;\n bytes: number;\n redacted: number;\n file: string;\n};\n\nexport function memoryRoot(home: string): string {\n return `${home}/.pi/agent/memory`;\n}\n\nexport function roleRoot(home: string): string {\n return `${home}/.pi/agent/agent-memory`;\n}\n\nexport function safePath(path: string): string | null {\n const parts = path.split('/');\n if (!parts.length || parts.some((part) => !/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(part)))\n return null;\n return parts.join('/');\n}\n\nexport function placeOf(home: string, scope: string): Place | null {\n if (scope === GLOBAL)\n return { scope, file: `${memoryRoot(home)}/${MEMORY_FILE}`, label: 'everyone' };\n if (scope.startsWith(ROLE_PREFIX) && scope.includes('@')) {\n const [named, root] = scope.slice(ROLE_PREFIX.length).split('@') as [string, string];\n const path = safePath(named);\n const inside = root.startsWith('/') && safePath(root.slice(1)) !== null && trusted(root);\n return path && inside\n ? {\n scope,\n file: `${root}/.pi/agent-memory/${path}/${MEMORY_FILE}`,\n label: `role ${path} in ${root}`,\n }\n : null;\n }\n if (scope.startsWith(ROLE_PREFIX)) {\n const path = safePath(scope.slice(ROLE_PREFIX.length));\n return path\n ? { scope, file: `${roleRoot(home)}/${path}/${MEMORY_FILE}`, label: `role ${path}` }\n : null;\n }\n const cone = safePath(scope);\n return cone && !cone.includes('/')\n ? { scope, file: `${memoryRoot(home)}/${cone}/${MEMORY_FILE}`, label: `cone ${cone}` }\n : null;\n}\n\nexport async function projectRoot(\n env: Pick<ExecutionEnv, 'exists'>,\n cwd: string,\n context: Context\n): Promise<string | null> {\n let dir = cwd.replace(/\\/+$/, '') || '/';\n for (;;) {\n for (const marker of ['.pi', '.git']) {\n const found = await env.exists(`${dir === '/' ? '' : dir}/${marker}`, context);\n if (found.ok && found.value) return dir;\n }\n if (dir === '/') return null;\n const cut = dir.lastIndexOf('/');\n dir = cut <= 0 ? '/' : dir.slice(0, cut);\n }\n}\n\nexport async function rolePlace(\n env: Pick<ExecutionEnv, 'exists'>,\n home: string,\n memory: RoleMemory,\n cwd: string,\n context: Context\n): Promise<Place | { problem: string }> {\n const path = safePath(memory.path);\n if (!path) return { problem: `memory path \"${memory.path}\" is not a safe relative path` };\n if (memory.scope === 'user') return placeOf(home, `${ROLE_PREFIX}${path}`) as Place;\n const root = await projectRoot(env, cwd, context);\n if (!root) return { problem: `no project root (.pi or .git) above ${cwd}` };\n if (!trusted(root))\n return {\n problem: `the project root ${root} is outside /home; it gets memory once SLICC can trust a folder`,\n };\n return placeOf(home, `${ROLE_PREFIX}${path}@${root}`) as Place;\n}\n\ntype Files = Pick<\n ExecutionEnv,\n 'readTextFile' | 'writeFile' | 'createDir' | 'canonicalPath' | 'fileInfo'\n>;\n\nfunction dirOf(file: string): string {\n return file.slice(0, file.lastIndexOf('/')) || '/';\n}\n\nexport class MemoryFiles {\n readonly #env: Files;\n readonly #chains = new Map<string, Promise<unknown>>();\n\n constructor(env: Files) {\n this.#env = env;\n }\n\n async #safe(file: string, context: Context): Promise<boolean> {\n for (let path = file; ; path = dirOf(path)) {\n const real = await this.#env.canonicalPath(path, context);\n if (real.ok) return real.value === path;\n if (path === '/') return true;\n }\n }\n\n async read(file: string, context: Context): Promise<{ text: string; updatedAt: number } | null> {\n if (!(await this.#safe(file, context))) return null;\n const read = await this.#env.readTextFile(file, context);\n if (!read.ok) return null;\n const info = await this.#env.fileInfo(file, context);\n return { text: read.value, updatedAt: info.ok ? info.value.mtimeMs : 0 };\n }\n\n async entries(place: Place, context: Context): Promise<MemoryEntry[]> {\n const read = await this.read(place.file, context);\n return read ? memoryEntries(parseMemory(read.text), place.scope, read.updatedAt) : [];\n }\n\n change(place: Place, change: Change, context: Context): Promise<Written> {\n const previous = this.#chains.get(place.file) ?? Promise.resolve();\n const next = previous.catch(() => undefined).then(() => this.#apply(place, change, context));\n this.#chains.set(place.file, next);\n void next\n .finally(() => {\n if (this.#chains.get(place.file) === next) this.#chains.delete(place.file);\n })\n .catch(() => undefined);\n return next;\n }\n\n async #apply(place: Place, change: Change, context: Context): Promise<Written> {\n if (!(await this.#safe(place.file, context)))\n throw new Error(`${place.file} is a symbolic link or under one; memory is not written there`);\n const read = await this.#env.readTextFile(place.file, context);\n const before = read.ok ? read.value : '';\n let redacted = 0;\n const clean = (text: string) => {\n const result = redactSecrets(text);\n redacted += result.count;\n return result.text;\n };\n const safe: Change =\n change.kind === 'save'\n ? {\n ...change,\n section: oneLine(clean(change.section)) || 'Notes',\n title: oneLine(clean(change.title)) || 'Untitled',\n body: clean(change.body),\n }\n : change;\n const { doc, found } = applyChange(parseMemory(before), place.scope, safe);\n const after = serializeMemory(doc);\n const bytes = byteLength(after);\n if (bytes > MAX_BYTES && bytes >= byteLength(before))\n throw new Error(\n `${place.file} would be ${bytes} bytes; memory files hold ${MAX_BYTES}. Remove or shorten entries first (a write over the limit must make the file smaller).`\n );\n if (change.kind === 'remove' && !found)\n return { entry: null, found, bytes: byteLength(before), redacted, file: place.file };\n await this.#env.createDir(dirOf(place.file), { recursive: true }, context);\n const written = await this.#env.writeFile(place.file, after, context);\n if (!written.ok) throw new Error(written.error.message);\n const entry =\n safe.kind === 'save'\n ? (memoryEntries(doc, place.scope, Date.now()).find(\n (item) =>\n item.source === 'entry' &&\n item.section.toLowerCase() === safe.section.toLowerCase() &&\n item.title.toLowerCase() === safe.title.toLowerCase()\n ) as MemoryEntry)\n : null;\n return { entry, found, bytes, redacted, file: place.file };\n }\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -52,10 +52,11 @@ function line(state, text) {
52
52
  if (!text.trim() || text.trimStart().startsWith("#")) return;
53
53
  const item = /^\s+-\s+(.*)$/.exec(text);
54
54
  if (item && list) state.fields[list].push(unquote(item[1].trim()));
55
- else if (/^\s/.test(text) && list) state.nested.add(list);
55
+ else if (/^\s/.test(text) && list)
56
+ state.nested.set(list, [...state.nested.get(list) ?? [], text.trim()]);
56
57
  else pair(state, text);
57
58
  }
58
- function parseFrontmatter(text) {
59
+ function parseFrontmatter(text, mappings = []) {
59
60
  const normalized = text.replace(/\r\n/g, "\n");
60
61
  const match = /^---\n([\s\S]*?)\n---(?:\n|$)/.exec(normalized);
61
62
  if (!match)
@@ -63,13 +64,17 @@ function parseFrontmatter(text) {
63
64
  const state = {
64
65
  fields: {},
65
66
  problems: [],
66
- nested: /* @__PURE__ */ new Set(),
67
+ nested: /* @__PURE__ */ new Map(),
67
68
  list: void 0,
68
69
  block: void 0
69
70
  };
70
71
  for (const text2 of match[1].split("\n")) line(state, text2);
71
72
  if (state.block) state.fields[state.block.key] = blockValue(state.block);
72
- for (const key of state.nested) {
73
+ for (const [key, lines] of state.nested) {
74
+ if (mappings.includes(key)) {
75
+ state.fields[key] = `{ ${lines.join(", ")} }`;
76
+ continue;
77
+ }
73
78
  delete state.fields[key];
74
79
  state.problems.push(`"${key}" has nested values, which SLICC doesn't read`);
75
80
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/roles/frontmatter.ts"],
4
- "sourcesContent": ["export type FrontmatterValue = string | number | boolean | string[];\n\nexport interface Frontmatter {\n fields: Record<string, FrontmatterValue>;\n body: string;\n problems: string[];\n}\n\nfunction unquote(value: string): string {\n const quoted = /^([\"'])(.*)\\1$/s.exec(value);\n if (!quoted) return value;\n return quoted[1] === '\"'\n ? (quoted[2] as string).replace(/\\\\([\"\\\\])/g, '$1')\n : (quoted[2] as string).replace(/''/g, \"'\");\n}\n\nfunction scalar(raw: string): FrontmatterValue {\n const value = raw.trim();\n if (/^\\[.*\\]$/s.test(value))\n return value\n .slice(1, -1)\n .split(',')\n .map((item) => unquote(item.trim()))\n .filter(Boolean);\n if (value === 'true' || value === 'false') return value === 'true';\n if (/^-?\\d+(\\.\\d+)?$/.test(value)) return Number(value);\n return unquote(value);\n}\n\ntype Block = { key: string; folded: boolean; strip: boolean; lines: string[] };\n\nconst BLOCK = /^([|>])([+-]?)$/;\n\nfunction blockValue(block: Block): string {\n const indents = block.lines\n .filter((line) => line.trim())\n .map((line) => (/^\\s*/.exec(line) as RegExpExecArray)[0].length);\n const indent = indents.length ? Math.min(...indents) : 0;\n const lines = block.lines.map((line) => line.slice(indent));\n while (lines.length && !lines.at(-1)?.trim()) lines.pop();\n const text = block.folded\n ? lines.reduce(\n (out, line) =>\n !line.trim()\n ? `${out}\\n`\n : out && !out.endsWith('\\n')\n ? `${out} ${line}`\n : `${out}${line}`,\n ''\n )\n : lines.join('\\n');\n return block.strip || !text ? text : `${text}\\n`;\n}\n\ntype State = {\n fields: Record<string, FrontmatterValue>;\n problems: string[];\n nested: Set<string>;\n list: string | undefined;\n block: Block | undefined;\n};\n\nfunction pair(state: State, line: string): void {\n const found = /^([A-Za-z][\\w-]*):(?:\\s+(.*))?$/.exec(line);\n state.list = undefined;\n if (!found) {\n state.problems.push(`can't read the line \"${line.trim()}\"`);\n return;\n }\n const key = found[1] as string;\n const value = found[2]?.trim() ?? '';\n const style = BLOCK.exec(value);\n if (style) state.block = { key, folded: style[1] === '>', strip: style[2] === '-', lines: [] };\n else if (value === '') {\n state.fields[key] = [];\n state.list = key;\n } else state.fields[key] = scalar(value);\n}\n\nfunction line(state: State, text: string): void {\n const { block, list } = state;\n if (block && (!text.trim() || /^\\s/.test(text))) {\n block.lines.push(text);\n return;\n }\n if (block) state.fields[block.key] = blockValue(block);\n state.block = undefined;\n if (!text.trim() || text.trimStart().startsWith('#')) return;\n const item = /^\\s+-\\s+(.*)$/.exec(text);\n if (item && list) (state.fields[list] as string[]).push(unquote((item[1] as string).trim()));\n else if (/^\\s/.test(text) && list) state.nested.add(list);\n else pair(state, text);\n}\n\nexport function parseFrontmatter(text: string): Frontmatter {\n const normalized = text.replace(/\\r\\n/g, '\\n');\n const match = /^---\\n([\\s\\S]*?)\\n---(?:\\n|$)/.exec(normalized);\n if (!match)\n return { fields: {}, body: normalized.trim(), problems: ['no frontmatter between --- lines'] };\n const state: State = {\n fields: {},\n problems: [],\n nested: new Set(),\n list: undefined,\n block: undefined,\n };\n for (const text of (match[1] as string).split('\\n')) line(state, text);\n if (state.block) state.fields[state.block.key] = blockValue(state.block);\n for (const key of state.nested) {\n delete state.fields[key];\n state.problems.push(`\"${key}\" has nested values, which SLICC doesn't read`);\n }\n return {\n fields: state.fields,\n body: normalized.slice(match[0].length).trim(),\n problems: state.problems,\n };\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["export type FrontmatterValue = string | number | boolean | string[];\n\nexport interface Frontmatter {\n fields: Record<string, FrontmatterValue>;\n body: string;\n problems: string[];\n}\n\nfunction unquote(value: string): string {\n const quoted = /^([\"'])(.*)\\1$/s.exec(value);\n if (!quoted) return value;\n return quoted[1] === '\"'\n ? (quoted[2] as string).replace(/\\\\([\"\\\\])/g, '$1')\n : (quoted[2] as string).replace(/''/g, \"'\");\n}\n\nfunction scalar(raw: string): FrontmatterValue {\n const value = raw.trim();\n if (/^\\[.*\\]$/s.test(value))\n return value\n .slice(1, -1)\n .split(',')\n .map((item) => unquote(item.trim()))\n .filter(Boolean);\n if (value === 'true' || value === 'false') return value === 'true';\n if (/^-?\\d+(\\.\\d+)?$/.test(value)) return Number(value);\n return unquote(value);\n}\n\ntype Block = { key: string; folded: boolean; strip: boolean; lines: string[] };\n\nconst BLOCK = /^([|>])([+-]?)$/;\n\nfunction blockValue(block: Block): string {\n const indents = block.lines\n .filter((line) => line.trim())\n .map((line) => (/^\\s*/.exec(line) as RegExpExecArray)[0].length);\n const indent = indents.length ? Math.min(...indents) : 0;\n const lines = block.lines.map((line) => line.slice(indent));\n while (lines.length && !lines.at(-1)?.trim()) lines.pop();\n const text = block.folded\n ? lines.reduce(\n (out, line) =>\n !line.trim()\n ? `${out}\\n`\n : out && !out.endsWith('\\n')\n ? `${out} ${line}`\n : `${out}${line}`,\n ''\n )\n : lines.join('\\n');\n return block.strip || !text ? text : `${text}\\n`;\n}\n\ntype State = {\n fields: Record<string, FrontmatterValue>;\n problems: string[];\n nested: Map<string, string[]>;\n list: string | undefined;\n block: Block | undefined;\n};\n\nfunction pair(state: State, line: string): void {\n const found = /^([A-Za-z][\\w-]*):(?:\\s+(.*))?$/.exec(line);\n state.list = undefined;\n if (!found) {\n state.problems.push(`can't read the line \"${line.trim()}\"`);\n return;\n }\n const key = found[1] as string;\n const value = found[2]?.trim() ?? '';\n const style = BLOCK.exec(value);\n if (style) state.block = { key, folded: style[1] === '>', strip: style[2] === '-', lines: [] };\n else if (value === '') {\n state.fields[key] = [];\n state.list = key;\n } else state.fields[key] = scalar(value);\n}\n\nfunction line(state: State, text: string): void {\n const { block, list } = state;\n if (block && (!text.trim() || /^\\s/.test(text))) {\n block.lines.push(text);\n return;\n }\n if (block) state.fields[block.key] = blockValue(block);\n state.block = undefined;\n if (!text.trim() || text.trimStart().startsWith('#')) return;\n const item = /^\\s+-\\s+(.*)$/.exec(text);\n if (item && list) (state.fields[list] as string[]).push(unquote((item[1] as string).trim()));\n else if (/^\\s/.test(text) && list)\n state.nested.set(list, [...(state.nested.get(list) ?? []), text.trim()]);\n else pair(state, text);\n}\n\nexport function parseFrontmatter(text: string, mappings: readonly string[] = []): Frontmatter {\n const normalized = text.replace(/\\r\\n/g, '\\n');\n const match = /^---\\n([\\s\\S]*?)\\n---(?:\\n|$)/.exec(normalized);\n if (!match)\n return { fields: {}, body: normalized.trim(), problems: ['no frontmatter between --- lines'] };\n const state: State = {\n fields: {},\n problems: [],\n nested: new Map(),\n list: undefined,\n block: undefined,\n };\n for (const text of (match[1] as string).split('\\n')) line(state, text);\n if (state.block) state.fields[state.block.key] = blockValue(state.block);\n for (const [key, lines] of state.nested) {\n if (mappings.includes(key)) {\n state.fields[key] = `{ ${lines.join(', ')} }`;\n continue;\n }\n delete state.fields[key];\n state.problems.push(`\"${key}\" has nested values, which SLICC doesn't read`);\n }\n return {\n fields: state.fields,\n body: normalized.slice(match[0].length).trim(),\n problems: state.problems,\n };\n}\n"],
5
+ "mappings": "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",
6
6
  "names": ["line", "text"]
7
7
  }
@@ -7,8 +7,10 @@ const TOOLS = {
7
7
  bash: "bash",
8
8
  grep: "bash",
9
9
  find: "bash",
10
- ls: "bash"
10
+ ls: "bash",
11
+ memory_write: "memory_write"
11
12
  };
13
+ const WRITERS = /* @__PURE__ */ new Set(["write", "edit", "bash"]);
12
14
  const THINKING = /* @__PURE__ */ new Set(["off", "minimal", "low", "medium", "high", "xhigh"]);
13
15
  const APPLIED = /* @__PURE__ */ new Set([
14
16
  "name",
@@ -22,13 +24,12 @@ const APPLIED = /* @__PURE__ */ new Set([
22
24
  "package",
23
25
  "advertise",
24
26
  "inheritProjectContext",
27
+ "inheritGlobalContext",
25
28
  "inheritSkills",
29
+ "memory",
26
30
  "skills",
27
31
  "defaultReads"
28
32
  ]);
29
- const LATER = {
30
- memory: "per-agent memory comes with SLICC\u2019s memory panel"
31
- };
32
33
  function list(value) {
33
34
  if (Array.isArray(value)) return value;
34
35
  if (typeof value === "string")
@@ -45,8 +46,46 @@ function tools(names, where, warnings) {
45
46
  }
46
47
  return mapped;
47
48
  }
49
+ function roleMemory(value) {
50
+ if (typeof value !== "string") return void 0;
51
+ const inline = /^\{(.*)\}$/s.exec(value.trim());
52
+ const pairs = /* @__PURE__ */ new Map();
53
+ for (const part of (inline ? inline[1] : value).split(/[,\n]/)) {
54
+ const found = /^\s*([\w-]+)\s*:\s*(.*?)\s*$/.exec(part);
55
+ if (found) pairs.set(found[1], found[2].replace(/^(["'])(.*)\1$/, "$2"));
56
+ }
57
+ const scope = pairs.get("scope");
58
+ const path = pairs.get("path");
59
+ if (scope !== "user" && scope !== "project" || !path) return void 0;
60
+ return { scope, path };
61
+ }
62
+ function withMemoryWrite(listed, memory) {
63
+ if (memory && listed?.some((tool) => WRITERS.has(tool)) && !listed.includes("memory_write"))
64
+ return [...listed, "memory_write"];
65
+ return listed;
66
+ }
67
+ function memoryFields(fields, path, warnings) {
68
+ const memory = fields.memory === void 0 ? void 0 : roleMemory(fields.memory);
69
+ if (fields.memory !== void 0 && !memory)
70
+ warnings.push(`${path}: memory needs a scope (user or project) and a path; ignored`);
71
+ const listed = withMemoryWrite(
72
+ fields.tools !== void 0 ? tools(list(fields.tools), path, warnings) : void 0,
73
+ memory
74
+ );
75
+ const inherits = fields.inheritProjectContext !== void 0 || fields.inheritGlobalContext !== void 0;
76
+ return {
77
+ ...memory ? { memory } : {},
78
+ listed,
79
+ ...inherits ? {
80
+ context: {
81
+ project: fields.inheritProjectContext !== false,
82
+ global: fields.inheritGlobalContext === true
83
+ }
84
+ } : {}
85
+ };
86
+ }
48
87
  function parseRole(text, source, path, warnings) {
49
- const { fields, body, problems } = parseFrontmatter(text);
88
+ const { fields, body, problems } = parseFrontmatter(text, ["memory"]);
50
89
  for (const problem of problems) warnings.push(`${path}: ${problem}`);
51
90
  const name = fields.name;
52
91
  const description = fields.description;
@@ -56,23 +95,24 @@ function parseRole(text, source, path, warnings) {
56
95
  }
57
96
  for (const key of Object.keys(fields))
58
97
  if (!APPLIED.has(key))
59
- warnings.push(
60
- `${path}: "${key}" ${LATER[key] ? `is not read yet (${LATER[key]})` : "isn't supported in SLICC; it's ignored"}`
61
- );
98
+ warnings.push(`${path}: "${key}" isn't supported in SLICC; it's ignored`);
62
99
  const thinking = typeof fields.thinking === "string" && THINKING.has(fields.thinking) ? fields.thinking : void 0;
63
100
  if (fields.thinking !== void 0 && !thinking)
64
101
  warnings.push(`${path}: thinking "${String(fields.thinking)}" is unknown; ignored`);
102
+ const { memory, listed, context } = memoryFields(fields, path, warnings);
65
103
  const pkg = typeof fields.package === "string" && fields.package ? `${fields.package}.` : "";
66
104
  return {
67
105
  name: `${pkg}${name}`,
68
106
  description,
69
107
  prompt: body,
70
108
  mode: fields.systemPromptMode === "append" ? "append" : "replace",
71
- ...fields.tools !== void 0 ? { tools: tools(list(fields.tools), path, warnings) } : {},
109
+ ...listed ? { tools: listed } : {},
72
110
  ...typeof fields.model === "string" ? { model: fields.model } : {},
73
111
  ...thinking ? { thinking } : {},
74
112
  aliases: list(fields.aliases),
75
113
  ...typeof fields.output === "string" ? { output: fields.output } : {},
114
+ ...memory ? { memory } : {},
115
+ ...context ? { context } : {},
76
116
  source,
77
117
  path
78
118
  };
@@ -122,7 +162,10 @@ function apply(role, override, warnings) {
122
162
  if (typeof override.description === "string") next.description = override.description;
123
163
  if (typeof override.systemPrompt === "string") next.prompt = override.systemPrompt;
124
164
  if (Array.isArray(override.tools))
125
- next.tools = tools(override.tools.map(String), `override for ${role.name}`, warnings);
165
+ next.tools = withMemoryWrite(
166
+ tools(override.tools.map(String), `override for ${role.name}`, warnings),
167
+ role.memory
168
+ );
126
169
  return next;
127
170
  }
128
171
  const ORDER = ["builtin", "package", "user", "project"];
@@ -200,6 +243,7 @@ export {
200
243
  loadRoles,
201
244
  manifestDirs,
202
245
  packageDirs,
203
- parseRole
246
+ parseRole,
247
+ roleMemory
204
248
  };
205
249
  //# sourceMappingURL=roles.js.map