akemon 0.3.5 → 0.3.6
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/DATA_POLICY.md +120 -0
- package/README.md +25 -0
- package/TRADEMARK.md +74 -0
- package/dist/cli.js +230 -39
- package/dist/server.js +96 -3
- package/dist/software-agent-peripheral.js +147 -9
- package/dist/software-agent-result-cli.js +69 -0
- package/dist/software-agent-stream-cli.js +23 -0
- package/dist/work-memory.js +295 -0
- package/package.json +5 -3
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
import { randomUUID } from "crypto";
|
|
2
|
+
import { appendFile, mkdir, readdir, readFile } from "fs/promises";
|
|
3
|
+
import { basename, dirname, join } from "path";
|
|
4
|
+
import { localNow } from "./self.js";
|
|
5
|
+
import { redactSecrets, redactText } from "./redaction.js";
|
|
6
|
+
const DEFAULT_CONTEXT_BUDGET = 12_000;
|
|
7
|
+
const MIN_CONTEXT_BUDGET = 1_000;
|
|
8
|
+
const MAX_CONTEXT_BUDGET = 80_000;
|
|
9
|
+
const MAX_SECTION_CHARS = 4_000;
|
|
10
|
+
const MAX_INDEX_FILES = 200;
|
|
11
|
+
const MAX_INDEX_DEPTH = 4;
|
|
12
|
+
const WORK_DIR = "work";
|
|
13
|
+
const WORK_INBOX_FILE = "inbox.md";
|
|
14
|
+
const DEFAULT_CONTEXT_FILES = [
|
|
15
|
+
"README.md",
|
|
16
|
+
"current.md",
|
|
17
|
+
"handoff.md",
|
|
18
|
+
"decisions.md",
|
|
19
|
+
"commands.md",
|
|
20
|
+
"notes.md",
|
|
21
|
+
WORK_INBOX_FILE,
|
|
22
|
+
];
|
|
23
|
+
const INDEX_FILE_EXTENSIONS = new Set([
|
|
24
|
+
".md",
|
|
25
|
+
".txt",
|
|
26
|
+
".json",
|
|
27
|
+
".jsonl",
|
|
28
|
+
".yaml",
|
|
29
|
+
".yml",
|
|
30
|
+
]);
|
|
31
|
+
export function workMemoryDir(workdir, agentName) {
|
|
32
|
+
return join(workdir, ".akemon", "agents", cleanAgentName(agentName), WORK_DIR);
|
|
33
|
+
}
|
|
34
|
+
export function workMemoryInboxPath(workdir, agentName) {
|
|
35
|
+
return join(workMemoryDir(workdir, agentName), WORK_INBOX_FILE);
|
|
36
|
+
}
|
|
37
|
+
export async function buildWorkMemoryContext(opts) {
|
|
38
|
+
const budget = normalizeBudget(opts.budget);
|
|
39
|
+
const agentName = cleanAgentName(opts.agentName);
|
|
40
|
+
const root = workMemoryDir(opts.workdir, agentName);
|
|
41
|
+
const generatedAt = localNow();
|
|
42
|
+
const purpose = cleanSingleLine(opts.purpose || "external software-agent work context", 180);
|
|
43
|
+
const sections = await collectWorkContextSections(root);
|
|
44
|
+
const text = renderWorkMemoryContext({
|
|
45
|
+
agentName,
|
|
46
|
+
workdir: opts.workdir,
|
|
47
|
+
workMemoryDir: root,
|
|
48
|
+
generatedAt,
|
|
49
|
+
purpose,
|
|
50
|
+
budget,
|
|
51
|
+
sections,
|
|
52
|
+
});
|
|
53
|
+
return {
|
|
54
|
+
agentName,
|
|
55
|
+
workdir: opts.workdir,
|
|
56
|
+
workMemoryDir: root,
|
|
57
|
+
generatedAt,
|
|
58
|
+
purpose,
|
|
59
|
+
budget,
|
|
60
|
+
sections,
|
|
61
|
+
text: fitText(text, budget),
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
export async function appendWorkMemoryNote(input) {
|
|
65
|
+
const text = cleanMultiline(input.text, 8_000);
|
|
66
|
+
if (!text)
|
|
67
|
+
throw new Error("Missing required work memory note text");
|
|
68
|
+
const note = {
|
|
69
|
+
id: `work_${Date.now()}_${randomUUID().slice(0, 8)}`,
|
|
70
|
+
ts: localNow(),
|
|
71
|
+
agentName: cleanAgentName(input.agentName),
|
|
72
|
+
source: cleanToken(input.source || "user", "source", 80),
|
|
73
|
+
kind: cleanToken(input.kind || "note", "kind", 80),
|
|
74
|
+
text,
|
|
75
|
+
};
|
|
76
|
+
const sessionId = cleanOptionalToken(input.sessionId, "sessionId", 120);
|
|
77
|
+
if (sessionId)
|
|
78
|
+
note.sessionId = sessionId;
|
|
79
|
+
const target = cleanOptionalPathHint(input.target);
|
|
80
|
+
if (target)
|
|
81
|
+
note.target = target;
|
|
82
|
+
const redacted = redactSecrets(note);
|
|
83
|
+
const path = target
|
|
84
|
+
? join(workMemoryDir(input.workdir, note.agentName), target)
|
|
85
|
+
: workMemoryInboxPath(input.workdir, note.agentName);
|
|
86
|
+
await mkdir(dirname(path), { recursive: true });
|
|
87
|
+
await appendFile(path, renderWorkMemoryNote(redacted), "utf-8");
|
|
88
|
+
return { note: redacted, path };
|
|
89
|
+
}
|
|
90
|
+
async function collectWorkContextSections(root) {
|
|
91
|
+
const sections = [];
|
|
92
|
+
for (const file of DEFAULT_CONTEXT_FILES) {
|
|
93
|
+
const section = await readWorkContextFile(root, file);
|
|
94
|
+
if (section)
|
|
95
|
+
sections.push(section);
|
|
96
|
+
}
|
|
97
|
+
const index = await buildWorkFileIndex(root);
|
|
98
|
+
if (index)
|
|
99
|
+
sections.push(index);
|
|
100
|
+
return sections;
|
|
101
|
+
}
|
|
102
|
+
async function readWorkContextFile(root, relativePath) {
|
|
103
|
+
const path = join(root, relativePath);
|
|
104
|
+
let raw;
|
|
105
|
+
try {
|
|
106
|
+
raw = await readFile(path, "utf-8");
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
const redacted = redactText(selectUsefulFileContent(relativePath, raw));
|
|
112
|
+
const trimmed = redacted.trim();
|
|
113
|
+
const content = fitText(trimmed, MAX_SECTION_CHARS);
|
|
114
|
+
return {
|
|
115
|
+
title: relativePath,
|
|
116
|
+
path,
|
|
117
|
+
relativePath,
|
|
118
|
+
chars: trimmed.length,
|
|
119
|
+
truncated: content.length < trimmed.length,
|
|
120
|
+
content,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
async function buildWorkFileIndex(root) {
|
|
124
|
+
const files = await listWorkMemoryFiles(root);
|
|
125
|
+
if (!files.length)
|
|
126
|
+
return null;
|
|
127
|
+
const content = files.map((file) => `- ${file}`).join("\n");
|
|
128
|
+
return {
|
|
129
|
+
title: "work memory file index",
|
|
130
|
+
path: root,
|
|
131
|
+
relativePath: ".",
|
|
132
|
+
chars: content.length,
|
|
133
|
+
truncated: files.length >= MAX_INDEX_FILES,
|
|
134
|
+
content,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
async function listWorkMemoryFiles(root) {
|
|
138
|
+
const files = [];
|
|
139
|
+
await walk(root, "", 0, files);
|
|
140
|
+
return files.sort((a, b) => a.localeCompare(b)).slice(0, MAX_INDEX_FILES);
|
|
141
|
+
}
|
|
142
|
+
async function walk(root, relativeDir, depth, files) {
|
|
143
|
+
if (depth > MAX_INDEX_DEPTH || files.length >= MAX_INDEX_FILES)
|
|
144
|
+
return;
|
|
145
|
+
const dir = relativeDir ? join(root, relativeDir) : root;
|
|
146
|
+
let entries;
|
|
147
|
+
try {
|
|
148
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
149
|
+
}
|
|
150
|
+
catch {
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
154
|
+
if (files.length >= MAX_INDEX_FILES)
|
|
155
|
+
return;
|
|
156
|
+
if (entry.name.startsWith("."))
|
|
157
|
+
continue;
|
|
158
|
+
const relativePath = relativeDir ? `${relativeDir}/${entry.name}` : entry.name;
|
|
159
|
+
if (entry.isDirectory()) {
|
|
160
|
+
await walk(root, relativePath, depth + 1, files);
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
if (!entry.isFile())
|
|
164
|
+
continue;
|
|
165
|
+
if (!INDEX_FILE_EXTENSIONS.has(extensionOf(entry.name)))
|
|
166
|
+
continue;
|
|
167
|
+
files.push(relativePath);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
function renderWorkMemoryContext(packet) {
|
|
171
|
+
const lines = [
|
|
172
|
+
"# Akemon Work Memory Context",
|
|
173
|
+
"",
|
|
174
|
+
`Generated at: ${packet.generatedAt}`,
|
|
175
|
+
`Agent: ${packet.agentName}`,
|
|
176
|
+
`Purpose: ${packet.purpose}`,
|
|
177
|
+
`Workdir: ${packet.workdir}`,
|
|
178
|
+
`Work memory directory: ${packet.workMemoryDir}`,
|
|
179
|
+
"",
|
|
180
|
+
"## Boundary",
|
|
181
|
+
"",
|
|
182
|
+
"- This is user-owned work memory for engineering and task continuity.",
|
|
183
|
+
"- External tools such as Codex or Claude Code may read this work directory as task context.",
|
|
184
|
+
"- External tools may update this work directory when the user or task asks them to maintain work memory.",
|
|
185
|
+
"- Do not read or edit Akemon self memory through this work-memory interface.",
|
|
186
|
+
"- Use grep, direct file reading, semantic review, or your own tool workflow as appropriate for the task.",
|
|
187
|
+
"",
|
|
188
|
+
"## Suggested Update Command",
|
|
189
|
+
"",
|
|
190
|
+
"```bash",
|
|
191
|
+
`akemon work-note \"<durable work memory>\" --source codex --kind decision`,
|
|
192
|
+
"```",
|
|
193
|
+
];
|
|
194
|
+
if (!packet.sections.length) {
|
|
195
|
+
lines.push("", "## Included Work Memory", "", "No work memory files were found yet.");
|
|
196
|
+
lines.push("", "Create files under the work memory directory or use `akemon work-note` to append a quick note.");
|
|
197
|
+
return lines.join("\n");
|
|
198
|
+
}
|
|
199
|
+
lines.push("", "## Included Work Memory");
|
|
200
|
+
for (const section of packet.sections) {
|
|
201
|
+
lines.push("");
|
|
202
|
+
lines.push(`### ${section.title}`);
|
|
203
|
+
if (section.relativePath)
|
|
204
|
+
lines.push(`Path: ${section.relativePath}`);
|
|
205
|
+
if (section.truncated)
|
|
206
|
+
lines.push(`Truncated from ${section.chars} chars.`);
|
|
207
|
+
lines.push("");
|
|
208
|
+
lines.push("```");
|
|
209
|
+
lines.push(section.content || "(empty)");
|
|
210
|
+
lines.push("```");
|
|
211
|
+
}
|
|
212
|
+
return lines.join("\n");
|
|
213
|
+
}
|
|
214
|
+
function renderWorkMemoryNote(note) {
|
|
215
|
+
const lines = [
|
|
216
|
+
"",
|
|
217
|
+
`## ${note.ts} ${note.kind}`,
|
|
218
|
+
"",
|
|
219
|
+
`Source: ${note.source}`,
|
|
220
|
+
];
|
|
221
|
+
if (note.sessionId)
|
|
222
|
+
lines.push(`Session: ${note.sessionId}`);
|
|
223
|
+
if (note.target)
|
|
224
|
+
lines.push(`Target: ${note.target}`);
|
|
225
|
+
lines.push("", note.text.trim(), "");
|
|
226
|
+
return lines.join("\n");
|
|
227
|
+
}
|
|
228
|
+
function selectUsefulFileContent(relativePath, raw) {
|
|
229
|
+
if (!relativePath.endsWith(".jsonl"))
|
|
230
|
+
return raw;
|
|
231
|
+
const lines = raw.trim().split(/\r?\n/).filter(Boolean);
|
|
232
|
+
return lines.slice(-20).join("\n");
|
|
233
|
+
}
|
|
234
|
+
function normalizeBudget(value) {
|
|
235
|
+
if (value === undefined || value === null)
|
|
236
|
+
return DEFAULT_CONTEXT_BUDGET;
|
|
237
|
+
if (!Number.isInteger(value) || value <= 0)
|
|
238
|
+
throw new Error("Context budget must be a positive integer");
|
|
239
|
+
return Math.max(MIN_CONTEXT_BUDGET, Math.min(MAX_CONTEXT_BUDGET, value));
|
|
240
|
+
}
|
|
241
|
+
function cleanToken(value, field, maxChars) {
|
|
242
|
+
const cleaned = cleanSingleLine(value, maxChars);
|
|
243
|
+
if (!cleaned)
|
|
244
|
+
throw new Error(`Missing required ${field}`);
|
|
245
|
+
if (!/^[A-Za-z0-9_.:@-]+$/.test(cleaned)) {
|
|
246
|
+
throw new Error(`Invalid ${field}: expected letters, numbers, dot, underscore, colon, at, or hyphen`);
|
|
247
|
+
}
|
|
248
|
+
return cleaned;
|
|
249
|
+
}
|
|
250
|
+
function cleanAgentName(value) {
|
|
251
|
+
const cleaned = cleanSingleLine(value, 120);
|
|
252
|
+
if (!cleaned)
|
|
253
|
+
throw new Error("Missing required agentName");
|
|
254
|
+
if (cleaned === "." || cleaned === ".." || cleaned.includes("/") || cleaned.includes("\\") || cleaned.includes("\0")) {
|
|
255
|
+
throw new Error("Invalid agentName: path separators and NUL bytes are not allowed");
|
|
256
|
+
}
|
|
257
|
+
return cleaned;
|
|
258
|
+
}
|
|
259
|
+
function cleanOptionalToken(value, field, maxChars) {
|
|
260
|
+
if (value === undefined || value === null || value.trim() === "")
|
|
261
|
+
return undefined;
|
|
262
|
+
return cleanToken(value, field, maxChars);
|
|
263
|
+
}
|
|
264
|
+
function cleanOptionalPathHint(value) {
|
|
265
|
+
if (value === undefined || value === null || value.trim() === "")
|
|
266
|
+
return undefined;
|
|
267
|
+
const cleaned = cleanSingleLine(value, 240);
|
|
268
|
+
if (cleaned.includes("\0") || cleaned.startsWith("/") || cleaned.includes("\\") || cleaned.split("/").includes("..")) {
|
|
269
|
+
throw new Error("Invalid target path");
|
|
270
|
+
}
|
|
271
|
+
const base = basename(cleaned);
|
|
272
|
+
if (base === "." || base === "..")
|
|
273
|
+
throw new Error("Invalid target path");
|
|
274
|
+
return cleaned;
|
|
275
|
+
}
|
|
276
|
+
function cleanSingleLine(value, maxChars) {
|
|
277
|
+
return fitText(String(value || "").replace(/\s+/g, " ").trim(), maxChars);
|
|
278
|
+
}
|
|
279
|
+
function cleanMultiline(value, maxChars) {
|
|
280
|
+
return fitText(String(value || "").replace(/\0/g, "").trim(), maxChars);
|
|
281
|
+
}
|
|
282
|
+
function fitText(text, maxChars) {
|
|
283
|
+
if (text.length <= maxChars)
|
|
284
|
+
return text;
|
|
285
|
+
if (maxChars <= 20)
|
|
286
|
+
return text.slice(0, maxChars);
|
|
287
|
+
const omitted = text.length - maxChars;
|
|
288
|
+
const head = Math.floor(maxChars * 0.65);
|
|
289
|
+
const tail = Math.max(0, maxChars - head - 40);
|
|
290
|
+
return `${text.slice(0, head)}\n[truncated ${omitted} chars]\n${text.slice(-tail)}`;
|
|
291
|
+
}
|
|
292
|
+
function extensionOf(file) {
|
|
293
|
+
const index = file.lastIndexOf(".");
|
|
294
|
+
return index >= 0 ? file.slice(index).toLowerCase() : "";
|
|
295
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "akemon",
|
|
3
|
-
"version": "0.3.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.3.6",
|
|
4
|
+
"description": "Local AI companion runtime with memory, modules, relay sync, and software-agent control",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"repository": {
|
|
@@ -23,6 +23,8 @@
|
|
|
23
23
|
"files": [
|
|
24
24
|
"dist",
|
|
25
25
|
"scripts",
|
|
26
|
+
"DATA_POLICY.md",
|
|
27
|
+
"TRADEMARK.md",
|
|
26
28
|
"README.md"
|
|
27
29
|
],
|
|
28
30
|
"scripts": {
|
|
@@ -31,7 +33,7 @@
|
|
|
31
33
|
"start": "node dist/cli.js",
|
|
32
34
|
"postinstall": "node scripts/fix-node-pty.cjs",
|
|
33
35
|
"prepublishOnly": "npm run build",
|
|
34
|
-
"test": "tsc -p tsconfig.test.json && node --test test-dist/*.test.js"
|
|
36
|
+
"test": "rm -rf test-dist && tsc -p tsconfig.test.json && node --test test-dist/*.test.js"
|
|
35
37
|
},
|
|
36
38
|
"dependencies": {
|
|
37
39
|
"@modelcontextprotocol/sdk": "^1.0.0",
|