@iamem/amem 0.1.2 → 0.2.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/README.md +64 -1
- package/dist/api/routes.js +337 -3
- package/dist/attest.d.ts +13 -0
- package/dist/attest.js +44 -0
- package/dist/capture.js +14 -5
- package/dist/cli.js +291 -6
- package/dist/context.d.ts +10 -1
- package/dist/context.js +105 -3
- package/dist/db.d.ts +141 -0
- package/dist/db.js +398 -0
- package/dist/embed.js +5 -14
- package/dist/estimate.d.ts +25 -1
- package/dist/estimate.js +36 -3
- package/dist/freshness.d.ts +7 -0
- package/dist/freshness.js +8 -1
- package/dist/hook.js +8 -1
- package/dist/hygiene.d.ts +26 -2
- package/dist/hygiene.js +42 -3
- package/dist/install/hosts.d.ts +15 -0
- package/dist/install/hosts.js +82 -4
- package/dist/install/skills.js +10 -5
- package/dist/kinds.d.ts +18 -0
- package/dist/kinds.js +80 -3
- package/dist/license.d.ts +1 -0
- package/dist/license.js +21 -19
- package/dist/mcp.js +221 -0
- package/dist/platforms.js +6 -0
- package/dist/policy.d.ts +6 -0
- package/dist/policy.js +16 -1
- package/dist/remember-contract.js +13 -4
- package/dist/repo-identity.d.ts +10 -0
- package/dist/repo-identity.js +20 -1
- package/dist/skill-capture.d.ts +43 -0
- package/dist/skill-capture.js +146 -0
- package/dist/skills.d.ts +106 -0
- package/dist/skills.js +422 -0
- package/docs/backlog.md +9 -0
- package/package.json +2 -1
- package/scripts/mcp-launch.sh +26 -0
- package/skills/amem-tasks/SKILL.md +100 -0
- package/skills/amem-write-skill/SKILL.md +99 -0
- package/templates/cursor-rule.mdc +16 -6
- package/templates/policy.deny-default.toml +5 -0
- package/templates/policy.example.toml +8 -0
- package/ui-static/app.js +458 -233
- package/ui-static/index.html +11 -34
- package/ui-static/styles.css +310 -1
package/dist/mcp.js
CHANGED
|
@@ -83,6 +83,122 @@ const TOOLS = [
|
|
|
83
83
|
properties: { workspace: { type: "string" } },
|
|
84
84
|
},
|
|
85
85
|
},
|
|
86
|
+
{
|
|
87
|
+
name: "amem_skill_list",
|
|
88
|
+
description: "List stored skills (procedures the agent learned) as an index of names and descriptions only. Cheap — call this first, then amem_skill_view to load the one you need. Skills are longer procedures; use amem_context for small durable facts.",
|
|
89
|
+
inputSchema: {
|
|
90
|
+
type: "object",
|
|
91
|
+
properties: {
|
|
92
|
+
query: {
|
|
93
|
+
type: "string",
|
|
94
|
+
description: "Optional: rank the index against this task description",
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
name: "amem_skill_view",
|
|
101
|
+
description: "Load the full body of one skill by name, or a supporting file inside it. Call this only after amem_skill_list (or the 'Relevant skills' section of an amem context packet) shows a skill worth following.",
|
|
102
|
+
inputSchema: {
|
|
103
|
+
type: "object",
|
|
104
|
+
properties: {
|
|
105
|
+
name: { type: "string", description: "Skill name from amem_skill_list" },
|
|
106
|
+
file: {
|
|
107
|
+
type: "string",
|
|
108
|
+
description: "Optional supporting file, e.g. references/api.md",
|
|
109
|
+
},
|
|
110
|
+
sessionId: {
|
|
111
|
+
type: "string",
|
|
112
|
+
description: "Optional conversation id, so amem can tell whether this procedure actually worked out",
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
required: ["name"],
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
name: "amem_skill_save",
|
|
120
|
+
description: "Save a non-trivial multi-step procedure you just worked out as a reusable skill. Use after solving something worth repeating — a workflow, a dead end you found the way past, or a correction the user gave you. Provide a full SKILL.md in `content`, or a description plus body.",
|
|
121
|
+
inputSchema: {
|
|
122
|
+
type: "object",
|
|
123
|
+
properties: {
|
|
124
|
+
name: {
|
|
125
|
+
type: "string",
|
|
126
|
+
description: "Short slug, e.g. deploy-staging",
|
|
127
|
+
},
|
|
128
|
+
description: {
|
|
129
|
+
type: "string",
|
|
130
|
+
description: "One line on when to use this skill",
|
|
131
|
+
},
|
|
132
|
+
content: {
|
|
133
|
+
type: "string",
|
|
134
|
+
description: "Full SKILL.md, or the markdown body to wrap",
|
|
135
|
+
},
|
|
136
|
+
sessionId: {
|
|
137
|
+
type: "string",
|
|
138
|
+
description: "Optional conversation id, so amem can clear the nudge that prompted this",
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
required: ["name"],
|
|
142
|
+
},
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
name: "amem_task_list",
|
|
146
|
+
description: "List deferred agent tasks (Kanban) for a workspace/repo. Use for work to do later — not durable facts (use amem_remember for those). Default excludes done tasks.",
|
|
147
|
+
inputSchema: {
|
|
148
|
+
type: "object",
|
|
149
|
+
properties: {
|
|
150
|
+
workspace: { type: "string" },
|
|
151
|
+
status: {
|
|
152
|
+
type: "string",
|
|
153
|
+
description: "backlog | next | doing | blocked | done",
|
|
154
|
+
},
|
|
155
|
+
include_done: { type: "boolean" },
|
|
156
|
+
},
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
name: "amem_task_add",
|
|
161
|
+
description: "Create a deferred agent task on the project Kanban (default backlog). Use for work that should not get lost in chat. Durable facts still go through amem_remember.",
|
|
162
|
+
inputSchema: {
|
|
163
|
+
type: "object",
|
|
164
|
+
properties: {
|
|
165
|
+
title: { type: "string" },
|
|
166
|
+
body: { type: "string", description: "Why / notes" },
|
|
167
|
+
workspace: { type: "string" },
|
|
168
|
+
status: { type: "string", description: "backlog | next | doing | blocked | done" },
|
|
169
|
+
anchors: { type: "array", items: { type: "string" } },
|
|
170
|
+
},
|
|
171
|
+
required: ["title"],
|
|
172
|
+
},
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
name: "amem_task_update",
|
|
176
|
+
description: "Update a task title, notes, anchors, or move its Kanban status (backlog/next/doing/blocked/done).",
|
|
177
|
+
inputSchema: {
|
|
178
|
+
type: "object",
|
|
179
|
+
properties: {
|
|
180
|
+
id: { type: "string" },
|
|
181
|
+
workspace: { type: "string" },
|
|
182
|
+
title: { type: "string" },
|
|
183
|
+
body: { type: "string" },
|
|
184
|
+
status: { type: "string" },
|
|
185
|
+
anchors: { type: "array", items: { type: "string" } },
|
|
186
|
+
},
|
|
187
|
+
required: ["id"],
|
|
188
|
+
},
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
name: "amem_task_complete",
|
|
192
|
+
description: "Mark a deferred agent task as done on the Kanban board.",
|
|
193
|
+
inputSchema: {
|
|
194
|
+
type: "object",
|
|
195
|
+
properties: {
|
|
196
|
+
id: { type: "string" },
|
|
197
|
+
workspace: { type: "string" },
|
|
198
|
+
},
|
|
199
|
+
required: ["id"],
|
|
200
|
+
},
|
|
201
|
+
},
|
|
86
202
|
];
|
|
87
203
|
function textResult(text, isError = false) {
|
|
88
204
|
return {
|
|
@@ -127,6 +243,23 @@ function parseDays(value, fallback = 30) {
|
|
|
127
243
|
return String(fallback);
|
|
128
244
|
return String(Math.min(365, Math.floor(n)));
|
|
129
245
|
}
|
|
246
|
+
/**
|
|
247
|
+
* Keep the skill index at level 0. Returning bodies here would defeat the point of having
|
|
248
|
+
* a separate view call, so this hands back names and descriptions only.
|
|
249
|
+
*/
|
|
250
|
+
function compactSkillIndex(body, ranked) {
|
|
251
|
+
const source = (body ?? {});
|
|
252
|
+
const list = ranked ? (source.matches ?? []) : (source.skills ?? []);
|
|
253
|
+
return {
|
|
254
|
+
skills: list.map((s) => ({
|
|
255
|
+
name: s.name,
|
|
256
|
+
description: s.description,
|
|
257
|
+
tags: s.tags,
|
|
258
|
+
uses: s.uses,
|
|
259
|
+
})),
|
|
260
|
+
hint: "Call amem_skill_view with a name to load the full procedure.",
|
|
261
|
+
};
|
|
262
|
+
}
|
|
130
263
|
function compactGraph(body) {
|
|
131
264
|
if (!body || typeof body !== "object")
|
|
132
265
|
return body;
|
|
@@ -255,6 +388,94 @@ function callTool(name, args, fallbackWorkspace) {
|
|
|
255
388
|
const result = api("GET", "/api/status", null, { workspace });
|
|
256
389
|
return apiResult(result);
|
|
257
390
|
}
|
|
391
|
+
if (name === "amem_skill_list") {
|
|
392
|
+
const query = {};
|
|
393
|
+
if (typeof args.query === "string" && args.query.trim())
|
|
394
|
+
query.q = args.query.trim();
|
|
395
|
+
const result = api("GET", "/api/skills", null, { query });
|
|
396
|
+
if (result.status >= 400)
|
|
397
|
+
return apiResult(result);
|
|
398
|
+
return jsonResult(compactSkillIndex(result.body, Boolean(query.q)));
|
|
399
|
+
}
|
|
400
|
+
if (name === "amem_skill_view") {
|
|
401
|
+
const skillName = typeof args.name === "string" ? args.name.trim() : "";
|
|
402
|
+
if (!skillName)
|
|
403
|
+
return textResult("name is required", true);
|
|
404
|
+
const query = { name: skillName };
|
|
405
|
+
if (typeof args.file === "string" && args.file.trim())
|
|
406
|
+
query.file = args.file.trim();
|
|
407
|
+
if (typeof args.sessionId === "string" && args.sessionId.trim()) {
|
|
408
|
+
query.session_id = args.sessionId.trim();
|
|
409
|
+
}
|
|
410
|
+
const result = api("GET", "/api/skills/view", null, { query });
|
|
411
|
+
if (result.status >= 400)
|
|
412
|
+
return apiResult(result);
|
|
413
|
+
const body = result.body;
|
|
414
|
+
// The body is markdown the agent should read as-is, not JSON to re-parse.
|
|
415
|
+
return textResult(typeof body?.content === "string" ? body.content : JSON.stringify(body));
|
|
416
|
+
}
|
|
417
|
+
if (name === "amem_skill_save") {
|
|
418
|
+
const skillName = typeof args.name === "string" ? args.name.trim() : "";
|
|
419
|
+
if (!skillName)
|
|
420
|
+
return textResult("name is required", true);
|
|
421
|
+
const result = api("POST", "/api/skills", {
|
|
422
|
+
name: skillName,
|
|
423
|
+
description: typeof args.description === "string" ? args.description : "",
|
|
424
|
+
content: typeof args.content === "string" ? args.content : "",
|
|
425
|
+
session_id: typeof args.sessionId === "string" ? args.sessionId : undefined,
|
|
426
|
+
}, { workspace });
|
|
427
|
+
return apiResult(result);
|
|
428
|
+
}
|
|
429
|
+
if (name === "amem_task_list") {
|
|
430
|
+
const query = {};
|
|
431
|
+
if (typeof args.status === "string" && args.status.trim())
|
|
432
|
+
query.status = args.status.trim();
|
|
433
|
+
if (args.include_done === true)
|
|
434
|
+
query.include_done = "1";
|
|
435
|
+
const result = api("GET", "/api/tasks", null, { workspace, query });
|
|
436
|
+
if (result.status >= 400)
|
|
437
|
+
return apiResult(result);
|
|
438
|
+
return jsonResult(result.body);
|
|
439
|
+
}
|
|
440
|
+
if (name === "amem_task_add") {
|
|
441
|
+
const title = typeof args.title === "string" ? args.title : "";
|
|
442
|
+
if (!title.trim())
|
|
443
|
+
return textResult("title is required", true);
|
|
444
|
+
const result = api("POST", "/api/tasks", {
|
|
445
|
+
title,
|
|
446
|
+
body: typeof args.body === "string" ? args.body : "",
|
|
447
|
+
status: typeof args.status === "string" ? args.status : "backlog",
|
|
448
|
+
anchors: args.anchors,
|
|
449
|
+
source: "mcp",
|
|
450
|
+
}, { workspace });
|
|
451
|
+
if (result.status >= 400)
|
|
452
|
+
return textResult(JSON.stringify(result.body), true);
|
|
453
|
+
return textResult(JSON.stringify(result.body));
|
|
454
|
+
}
|
|
455
|
+
if (name === "amem_task_update") {
|
|
456
|
+
const id = typeof args.id === "string" ? args.id : "";
|
|
457
|
+
if (!id.trim())
|
|
458
|
+
return textResult("id is required", true);
|
|
459
|
+
const result = api("PATCH", "/api/tasks", {
|
|
460
|
+
id,
|
|
461
|
+
title: typeof args.title === "string" ? args.title : undefined,
|
|
462
|
+
body: typeof args.body === "string" ? args.body : undefined,
|
|
463
|
+
status: typeof args.status === "string" ? args.status : undefined,
|
|
464
|
+
anchors: args.anchors,
|
|
465
|
+
}, { workspace });
|
|
466
|
+
if (result.status >= 400)
|
|
467
|
+
return textResult(JSON.stringify(result.body), true);
|
|
468
|
+
return textResult(JSON.stringify(result.body));
|
|
469
|
+
}
|
|
470
|
+
if (name === "amem_task_complete") {
|
|
471
|
+
const id = typeof args.id === "string" ? args.id : "";
|
|
472
|
+
if (!id.trim())
|
|
473
|
+
return textResult("id is required", true);
|
|
474
|
+
const result = api("POST", "/api/tasks/complete", { id }, { workspace });
|
|
475
|
+
if (result.status >= 400)
|
|
476
|
+
return textResult(JSON.stringify(result.body), true);
|
|
477
|
+
return textResult(JSON.stringify(result.body));
|
|
478
|
+
}
|
|
258
479
|
return textResult(`Unknown tool: ${name}`, true);
|
|
259
480
|
}
|
|
260
481
|
function rpcResult(id, result) {
|
package/dist/platforms.js
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
export const KNOWN_PLATFORMS = [
|
|
3
3
|
{ id: "cursor", label: "Cursor", hint: "Rules, skills, hooks", installs: "cursor" },
|
|
4
4
|
{ id: "claude", label: "Claude Code", hint: "Skills and settings hooks", installs: "claude" },
|
|
5
|
+
{
|
|
6
|
+
id: "claude-desktop",
|
|
7
|
+
label: "Claude Desktop",
|
|
8
|
+
hint: "MCP config installer",
|
|
9
|
+
installs: "host",
|
|
10
|
+
},
|
|
5
11
|
{ id: "copilot", label: "GitHub Copilot", hint: "MCP / HTTP API" },
|
|
6
12
|
{ id: "codex", label: "ChatGPT / Codex", hint: "MCP / HTTP API" },
|
|
7
13
|
{ id: "gemini", label: "Gemini", hint: "MCP / HTTP API" },
|
package/dist/policy.d.ts
CHANGED
|
@@ -8,6 +8,12 @@ export type AmemPolicy = {
|
|
|
8
8
|
deny_claim_patterns: string[];
|
|
9
9
|
/** Draft kinds that may auto-apply without Memory approve (still local). */
|
|
10
10
|
auto_apply_kinds: string[];
|
|
11
|
+
/** Master switch for procedural memory (store, index, injection). */
|
|
12
|
+
skills_enabled: boolean;
|
|
13
|
+
/** Stage agent-authored skills for review instead of writing them straight to disk. */
|
|
14
|
+
skill_write_approval: boolean;
|
|
15
|
+
/** Let session-end suggest skills worth writing up. */
|
|
16
|
+
skill_capture: boolean;
|
|
11
17
|
};
|
|
12
18
|
export type PolicySource = {
|
|
13
19
|
path: string;
|
package/dist/policy.js
CHANGED
|
@@ -22,6 +22,9 @@ export const DEFAULT_POLICY = {
|
|
|
22
22
|
allowed_remote_hosts: [],
|
|
23
23
|
deny_claim_patterns: [],
|
|
24
24
|
auto_apply_kinds: [],
|
|
25
|
+
skills_enabled: true,
|
|
26
|
+
skill_write_approval: false,
|
|
27
|
+
skill_capture: true,
|
|
25
28
|
};
|
|
26
29
|
let cached = null;
|
|
27
30
|
export function clearPolicyCache() {
|
|
@@ -60,6 +63,9 @@ export function parsePolicyToml(raw) {
|
|
|
60
63
|
case "telemetry":
|
|
61
64
|
case "ui_enabled":
|
|
62
65
|
case "allow_export":
|
|
66
|
+
case "skills_enabled":
|
|
67
|
+
case "skill_write_approval":
|
|
68
|
+
case "skill_capture":
|
|
63
69
|
out[key] = parseBool(valueRaw, key, i + 1);
|
|
64
70
|
break;
|
|
65
71
|
case "ui_bind":
|
|
@@ -131,6 +137,9 @@ function mergePolicy(base, overlay) {
|
|
|
131
137
|
allowed_remote_hosts: overlay.allowed_remote_hosts ?? base.allowed_remote_hosts,
|
|
132
138
|
deny_claim_patterns: overlay.deny_claim_patterns ?? base.deny_claim_patterns,
|
|
133
139
|
auto_apply_kinds: overlay.auto_apply_kinds ?? base.auto_apply_kinds,
|
|
140
|
+
skills_enabled: overlay.skills_enabled ?? base.skills_enabled,
|
|
141
|
+
skill_write_approval: overlay.skill_write_approval ?? base.skill_write_approval,
|
|
142
|
+
skill_capture: overlay.skill_capture ?? base.skill_capture,
|
|
134
143
|
};
|
|
135
144
|
}
|
|
136
145
|
/**
|
|
@@ -198,7 +207,13 @@ export function loadPolicy(forceReload = false) {
|
|
|
198
207
|
// Clamp only the exfiltration-relevant knobs — blanking allowed_platforms here would
|
|
199
208
|
// brick every host instead of protecting anything.
|
|
200
209
|
if (sources.some((s) => s.error)) {
|
|
201
|
-
policy = {
|
|
210
|
+
policy = {
|
|
211
|
+
...policy,
|
|
212
|
+
allow_export: false,
|
|
213
|
+
auto_apply_kinds: [],
|
|
214
|
+
// Skills are instructions the agent follows, so an unreadable policy means review.
|
|
215
|
+
skill_write_approval: true,
|
|
216
|
+
};
|
|
202
217
|
}
|
|
203
218
|
cached = { policy, sources };
|
|
204
219
|
return cached;
|
|
@@ -64,11 +64,17 @@ Generic MCP host recipe. Same tools for every client — do not fork per product
|
|
|
64
64
|
|
|
65
65
|
## Connect
|
|
66
66
|
|
|
67
|
-
|
|
67
|
+
Two supported transports. Either is fine — pick one.
|
|
68
|
+
|
|
69
|
+
**stdio (no daemon).** The host spawns amem itself. Let amem write the config:
|
|
70
|
+
|
|
71
|
+
\`amem init --platform claude-desktop\`
|
|
72
|
+
|
|
73
|
+
**HTTP (shared daemon).** Keep \`amem ui\` running, then attach:
|
|
68
74
|
|
|
69
75
|
\`${MCP_URL_TEMPLATE}\`
|
|
70
76
|
|
|
71
|
-
GUI
|
|
77
|
+
Whichever you pick, never configure a GUI host with a bare \`amem\` or \`node\` command. GUI apps are not launched from a login shell, so they inherit a minimal PATH with no Homebrew and no nvm: the connector registers but tool discovery never completes, and the host reports no reason. An absolute path — to the launcher or to the node binary — is what makes stdio work.
|
|
72
78
|
|
|
73
79
|
## Tools
|
|
74
80
|
|
|
@@ -88,8 +94,11 @@ ${ruleLines}
|
|
|
88
94
|
function renderPaste() {
|
|
89
95
|
return `amem remember contract v${REMEMBER_CONTRACT_VERSION}
|
|
90
96
|
|
|
91
|
-
Connect (any MCP host):
|
|
92
|
-
|
|
97
|
+
Connect (any MCP host), either:
|
|
98
|
+
- stdio, no daemon: amem init --platform claude-desktop (or point the host at an absolute launcher path)
|
|
99
|
+
- HTTP, needs amem ui running: ${MCP_URL_TEMPLATE}
|
|
100
|
+
|
|
101
|
+
Never give a GUI host a bare amem or node command — no login PATH, so tool discovery silently fails.
|
|
93
102
|
|
|
94
103
|
Every task:
|
|
95
104
|
1. amem_context — read local memory first (pass workspace=<slug>)
|
package/dist/repo-identity.d.ts
CHANGED
|
@@ -9,6 +9,16 @@ export declare function findGitRoot(startPath?: string): string | null;
|
|
|
9
9
|
/** Normalize remotes so ssh/https clones of the same repo share a key. */
|
|
10
10
|
export declare function normalizeRemoteUrl(url: string): string;
|
|
11
11
|
export declare function detectRepoIdentity(cwd?: string): RepoIdentity;
|
|
12
|
+
/**
|
|
13
|
+
* Why this directory must not become a repo binding, or null if it is fine.
|
|
14
|
+
*
|
|
15
|
+
* detectRepoIdentity falls back to the cwd when there is no git root, so
|
|
16
|
+
* running an init from a home directory would otherwise register the entire
|
|
17
|
+
* home as a "repo" — a binding that matches everything and can never be the
|
|
18
|
+
* right answer. Workspace roots under ~/.amem are created deliberately and are
|
|
19
|
+
* not affected: they are built by workspaceIdentity, not by cwd detection.
|
|
20
|
+
*/
|
|
21
|
+
export declare function unbindableRootReason(rootPath: string): string | null;
|
|
12
22
|
export declare function slugifyWorkspace(name: string): string;
|
|
13
23
|
export declare function workspaceIdentity(name: string, rootPath: string): RepoIdentity;
|
|
14
24
|
export declare function parseWorkspaceSlug(remoteUrl: string | null | undefined): string | null;
|
package/dist/repo-identity.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { execFileSync } from "node:child_process";
|
|
2
2
|
import { createHash, randomUUID } from "node:crypto";
|
|
3
3
|
import { existsSync } from "node:fs";
|
|
4
|
-
import {
|
|
4
|
+
import { homedir } from "node:os";
|
|
5
|
+
import { parse as parsePath, resolve } from "node:path";
|
|
5
6
|
function runGit(cwd, args) {
|
|
6
7
|
try {
|
|
7
8
|
return execFileSync("git", args, {
|
|
@@ -51,6 +52,24 @@ export function detectRepoIdentity(cwd = process.cwd()) {
|
|
|
51
52
|
defaultBranch,
|
|
52
53
|
};
|
|
53
54
|
}
|
|
55
|
+
/**
|
|
56
|
+
* Why this directory must not become a repo binding, or null if it is fine.
|
|
57
|
+
*
|
|
58
|
+
* detectRepoIdentity falls back to the cwd when there is no git root, so
|
|
59
|
+
* running an init from a home directory would otherwise register the entire
|
|
60
|
+
* home as a "repo" — a binding that matches everything and can never be the
|
|
61
|
+
* right answer. Workspace roots under ~/.amem are created deliberately and are
|
|
62
|
+
* not affected: they are built by workspaceIdentity, not by cwd detection.
|
|
63
|
+
*/
|
|
64
|
+
export function unbindableRootReason(rootPath) {
|
|
65
|
+
const resolved = resolve(rootPath);
|
|
66
|
+
const parsed = parsePath(resolved);
|
|
67
|
+
if (resolved === parsed.root)
|
|
68
|
+
return "the filesystem root";
|
|
69
|
+
if (resolved === resolve(homedir()))
|
|
70
|
+
return "your home directory";
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
54
73
|
export function slugifyWorkspace(name) {
|
|
55
74
|
const slug = name
|
|
56
75
|
.trim()
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deciding when a session is worth writing up as a skill.
|
|
3
|
+
*
|
|
4
|
+
* This is tuned for precision, not recall. Memory drafts already taught us that a noisy
|
|
5
|
+
* queue is worse than an empty one — a user facing hundreds of suggestions stops reading
|
|
6
|
+
* all of them. A session must show several independent signals before it earns one
|
|
7
|
+
* suggestion, and a session can never produce more than one.
|
|
8
|
+
*/
|
|
9
|
+
import { type RepoRow, type SkillDraftRow } from "./db.js";
|
|
10
|
+
export type SessionNote = {
|
|
11
|
+
role: string;
|
|
12
|
+
text: string;
|
|
13
|
+
};
|
|
14
|
+
export type SkillOpportunity = {
|
|
15
|
+
title: string;
|
|
16
|
+
summary: string;
|
|
17
|
+
reasons: string[];
|
|
18
|
+
score: number;
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* Score a session against the three triggers Hermes names: a multi-step workflow worth
|
|
22
|
+
* repeating, a dead end the agent found the way past, and a correction from the user.
|
|
23
|
+
* Requires at least two distinct signals plus real procedural content.
|
|
24
|
+
*/
|
|
25
|
+
export declare function detectSkillOpportunity(notes: SessionNote[]): SkillOpportunity | null;
|
|
26
|
+
/**
|
|
27
|
+
* Queue at most one suggestion per session. The agent, not amem, writes the actual
|
|
28
|
+
* SKILL.md — amem has no model, so it only points at the material.
|
|
29
|
+
*/
|
|
30
|
+
export declare function captureSkillSuggestion(input: {
|
|
31
|
+
repo: RepoRow;
|
|
32
|
+
sessionId?: string | null;
|
|
33
|
+
notes: SessionNote[];
|
|
34
|
+
}): SkillDraftRow | null;
|
|
35
|
+
/**
|
|
36
|
+
* A skill was followed this session and things still went wrong — that is the signal the
|
|
37
|
+
* skill itself needs work. Mirrors the existing miss→learn loop, pointed at procedures.
|
|
38
|
+
*/
|
|
39
|
+
export declare function captureSkillRevision(input: {
|
|
40
|
+
repo: RepoRow;
|
|
41
|
+
sessionId?: string | null;
|
|
42
|
+
notes: SessionNote[];
|
|
43
|
+
}): SkillDraftRow | null;
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deciding when a session is worth writing up as a skill.
|
|
3
|
+
*
|
|
4
|
+
* This is tuned for precision, not recall. Memory drafts already taught us that a noisy
|
|
5
|
+
* queue is worse than an empty one — a user facing hundreds of suggestions stops reading
|
|
6
|
+
* all of them. A session must show several independent signals before it earns one
|
|
7
|
+
* suggestion, and a session can never produce more than one.
|
|
8
|
+
*/
|
|
9
|
+
import { insertSkillDraft, listRecentSkillUses, listSkillsUsedInSession, skillDraftExists, } from "./db.js";
|
|
10
|
+
import { loadPolicy } from "./policy.js";
|
|
11
|
+
/** Numbered or bulleted command-ish steps — the shape of a procedure. */
|
|
12
|
+
const STEP_RE = /^\s*(?:\d+[.)]\s+|[-*]\s+)/gm;
|
|
13
|
+
const COMMAND_RE = /\b(?:npm|pnpm|yarn|npx|git|docker|kubectl|make|cargo|go|python3?|node|psql|aws|terraform|ssh|curl)\s+[\w-]/g;
|
|
14
|
+
const ERROR_RE = /\b(?:error|failed|failure|exception|traceback|not found|cannot find|denied|timed? out|broken|does ?n[o']t work)\b/i;
|
|
15
|
+
const RESOLUTION_RE = /\b(?:fixed|resolved|works now|that did it|success(?:ful)?|passing|green|it works|solved|now working)\b/i;
|
|
16
|
+
const CORRECTION_RE = /\b(?:no,|nope|actually|that'?s wrong|not quite|instead of|don'?t do that|wrong approach|try again)\b/i;
|
|
17
|
+
function assistantText(notes) {
|
|
18
|
+
return notes
|
|
19
|
+
.filter((n) => n.role === "assistant")
|
|
20
|
+
.map((n) => n.text)
|
|
21
|
+
.join("\n\n");
|
|
22
|
+
}
|
|
23
|
+
function userText(notes) {
|
|
24
|
+
return notes
|
|
25
|
+
.filter((n) => n.role === "user")
|
|
26
|
+
.map((n) => n.text)
|
|
27
|
+
.join("\n\n");
|
|
28
|
+
}
|
|
29
|
+
function countMatches(text, re) {
|
|
30
|
+
return (text.match(re) ?? []).length;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Score a session against the three triggers Hermes names: a multi-step workflow worth
|
|
34
|
+
* repeating, a dead end the agent found the way past, and a correction from the user.
|
|
35
|
+
* Requires at least two distinct signals plus real procedural content.
|
|
36
|
+
*/
|
|
37
|
+
export function detectSkillOpportunity(notes) {
|
|
38
|
+
if (!Array.isArray(notes) || notes.length < 4)
|
|
39
|
+
return null;
|
|
40
|
+
const assistant = assistantText(notes);
|
|
41
|
+
const user = userText(notes);
|
|
42
|
+
if (assistant.trim().length < 400)
|
|
43
|
+
return null;
|
|
44
|
+
const reasons = [];
|
|
45
|
+
let score = 0;
|
|
46
|
+
const steps = countMatches(assistant, STEP_RE);
|
|
47
|
+
const commands = countMatches(assistant, COMMAND_RE);
|
|
48
|
+
// A procedure needs enumerated steps or repeated concrete commands, not just prose.
|
|
49
|
+
if (steps >= 3) {
|
|
50
|
+
score += 30;
|
|
51
|
+
reasons.push(`${steps} enumerated steps`);
|
|
52
|
+
}
|
|
53
|
+
if (commands >= 3) {
|
|
54
|
+
score += 20;
|
|
55
|
+
reasons.push(`${commands} shell commands`);
|
|
56
|
+
}
|
|
57
|
+
if (steps < 3 && commands < 3)
|
|
58
|
+
return null;
|
|
59
|
+
const hitError = ERROR_RE.test(assistant);
|
|
60
|
+
if (hitError && RESOLUTION_RE.test(assistant)) {
|
|
61
|
+
score += 25;
|
|
62
|
+
reasons.push("hit an error and found the working path");
|
|
63
|
+
}
|
|
64
|
+
if (CORRECTION_RE.test(user)) {
|
|
65
|
+
score += 20;
|
|
66
|
+
reasons.push("user corrected the approach");
|
|
67
|
+
}
|
|
68
|
+
const turns = notes.filter((n) => n.role === "user").length;
|
|
69
|
+
if (turns >= 3) {
|
|
70
|
+
score += 10;
|
|
71
|
+
reasons.push(`${turns} back-and-forth turns`);
|
|
72
|
+
}
|
|
73
|
+
// Two independent signals minimum: one lone heuristic is not evidence of a procedure.
|
|
74
|
+
if (reasons.length < 2 || score < 55)
|
|
75
|
+
return null;
|
|
76
|
+
const firstUser = notes.find((n) => n.role === "user")?.text ?? "";
|
|
77
|
+
const title = firstUser.replace(/\s+/g, " ").trim().slice(0, 96) || "Multi-step workflow";
|
|
78
|
+
return {
|
|
79
|
+
title,
|
|
80
|
+
summary: firstUser.replace(/\s+/g, " ").trim().slice(0, 400),
|
|
81
|
+
reasons,
|
|
82
|
+
score,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Queue at most one suggestion per session. The agent, not amem, writes the actual
|
|
87
|
+
* SKILL.md — amem has no model, so it only points at the material.
|
|
88
|
+
*/
|
|
89
|
+
export function captureSkillSuggestion(input) {
|
|
90
|
+
if (!loadPolicy().policy.skill_capture)
|
|
91
|
+
return null;
|
|
92
|
+
const sid = input.sessionId || "unknown";
|
|
93
|
+
const source = `skill-suggest:${sid}`;
|
|
94
|
+
if (skillDraftExists(source))
|
|
95
|
+
return null;
|
|
96
|
+
const opportunity = detectSkillOpportunity(input.notes);
|
|
97
|
+
if (!opportunity)
|
|
98
|
+
return null;
|
|
99
|
+
return insertSkillDraft({
|
|
100
|
+
repoId: input.repo.id,
|
|
101
|
+
title: opportunity.title,
|
|
102
|
+
summary: opportunity.summary,
|
|
103
|
+
kind: "suggestion",
|
|
104
|
+
source,
|
|
105
|
+
sessionId: input.sessionId,
|
|
106
|
+
reasons: opportunity.reasons,
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* A skill was followed this session and things still went wrong — that is the signal the
|
|
111
|
+
* skill itself needs work. Mirrors the existing miss→learn loop, pointed at procedures.
|
|
112
|
+
*/
|
|
113
|
+
export function captureSkillRevision(input) {
|
|
114
|
+
if (!loadPolicy().policy.skill_capture)
|
|
115
|
+
return null;
|
|
116
|
+
const sid = input.sessionId || "";
|
|
117
|
+
// Prefer the exact session; fall back to recent use in this memory, because MCP
|
|
118
|
+
// clients do not reliably pass a session id through to skill views.
|
|
119
|
+
const used = sid ? listSkillsUsedInSession(sid, 3) : [];
|
|
120
|
+
const candidates = used.length > 0 ? used : listRecentSkillUses(input.repo.id, 120, 3);
|
|
121
|
+
if (candidates.length === 0)
|
|
122
|
+
return null;
|
|
123
|
+
const assistant = assistantText(input.notes);
|
|
124
|
+
const user = userText(input.notes);
|
|
125
|
+
const struggled = (ERROR_RE.test(assistant) && RESOLUTION_RE.test(assistant)) || CORRECTION_RE.test(user);
|
|
126
|
+
if (!struggled)
|
|
127
|
+
return null;
|
|
128
|
+
const target = candidates[0];
|
|
129
|
+
const source = `skill-revise:${sid || input.repo.id}:${target}`;
|
|
130
|
+
if (skillDraftExists(source))
|
|
131
|
+
return null;
|
|
132
|
+
const reasons = [
|
|
133
|
+
`followed ${target} but still hit trouble`,
|
|
134
|
+
CORRECTION_RE.test(user) ? "user corrected the approach" : "error then recovery in the session",
|
|
135
|
+
];
|
|
136
|
+
return insertSkillDraft({
|
|
137
|
+
repoId: input.repo.id,
|
|
138
|
+
title: `Revise ${target}`,
|
|
139
|
+
summary: `The ${target} skill was loaded this session but the work still went sideways. Check whether the procedure is missing a step or a pitfall.`,
|
|
140
|
+
kind: "revision",
|
|
141
|
+
targetSkill: target,
|
|
142
|
+
source,
|
|
143
|
+
sessionId: input.sessionId,
|
|
144
|
+
reasons,
|
|
145
|
+
});
|
|
146
|
+
}
|