@iamem/amem 0.1.1 → 0.1.3
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 +98 -4
- package/desktop/icons/icon-1024.png +0 -0
- package/desktop/icons/icon.icns +0 -0
- package/desktop/icons/icon.png +0 -0
- package/desktop/main.js +100 -0
- package/desktop/package.json +16 -0
- package/desktop/preload.js +4 -0
- package/desktop/scripts/ensure-electron.mjs +122 -0
- package/dist/api/routes.js +331 -1
- package/dist/app-shell.d.ts +13 -0
- package/dist/app-shell.js +126 -0
- package/dist/attest.d.ts +13 -0
- package/dist/attest.js +44 -0
- package/dist/cli.js +258 -2
- 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/hook.js +8 -1
- package/dist/hygiene.d.ts +1 -2
- package/dist/hygiene.js +1 -2
- package/dist/install/hosts.js +8 -0
- package/dist/install/skills.js +10 -5
- package/dist/license.d.ts +1 -0
- package/dist/license.js +21 -19
- package/dist/mcp.js +221 -0
- package/dist/policy.d.ts +6 -0
- package/dist/policy.js +16 -1
- package/dist/publish.d.ts +1 -1
- package/dist/publish.js +1 -0
- 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/docs/npm-release.md +1 -1
- package/package.json +7 -2
- package/scripts/postinstall.js +62 -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 +435 -227
- package/ui-static/index.html +11 -34
- package/ui-static/styles.css +299 -0
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/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;
|
package/dist/publish.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** Paths that must be inside the published tarball for `npx @iamem/amem setup`. */
|
|
2
|
-
export declare const REQUIRED_PACK_PATHS: readonly ["dist/cli.js", "dist/mcp.js", "dist/remember-contract.js", "ui-static/index.html", "ui-static/app.js", "ui-static/styles.css", "docs/remember-contract.md", "docs/agent-install-prompt.md", "docs/npm-release.md", "skills/amem-update-working-memory/SKILL.md", "scripts/mdm-offboard.sh"];
|
|
2
|
+
export declare const REQUIRED_PACK_PATHS: readonly ["dist/cli.js", "dist/mcp.js", "dist/remember-contract.js", "ui-static/index.html", "ui-static/app.js", "ui-static/styles.css", "docs/remember-contract.md", "docs/agent-install-prompt.md", "docs/npm-release.md", "skills/amem-update-working-memory/SKILL.md", "scripts/mdm-offboard.sh", "scripts/postinstall.js"];
|
|
3
3
|
export declare const PUBLISH_PACKAGE_NAME = "@iamem/amem";
|
|
4
4
|
export type PublishReady = {
|
|
5
5
|
root: string;
|
package/dist/publish.js
CHANGED
|
@@ -14,6 +14,7 @@ export const REQUIRED_PACK_PATHS = [
|
|
|
14
14
|
"docs/npm-release.md",
|
|
15
15
|
"skills/amem-update-working-memory/SKILL.md",
|
|
16
16
|
"scripts/mdm-offboard.sh",
|
|
17
|
+
"scripts/postinstall.js",
|
|
17
18
|
];
|
|
18
19
|
export const PUBLISH_PACKAGE_NAME = "@iamem/amem";
|
|
19
20
|
export function packageRoot() {
|
|
@@ -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
|
+
}
|
package/dist/skills.d.ts
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
export declare const SKILL_FILE = "SKILL.md";
|
|
2
|
+
/** Subdirectories a skill may carry, matching the agentskills.io layout. */
|
|
3
|
+
export declare const SKILL_ASSET_DIRS: string[];
|
|
4
|
+
export type SkillSource = "local" | "bundled" | "import";
|
|
5
|
+
export type SkillMeta = {
|
|
6
|
+
name: string;
|
|
7
|
+
description: string;
|
|
8
|
+
version: string | null;
|
|
9
|
+
tags: string[];
|
|
10
|
+
/** Absolute path to the skill's SKILL.md. */
|
|
11
|
+
path: string;
|
|
12
|
+
/** Absolute path to the skill directory. */
|
|
13
|
+
dir: string;
|
|
14
|
+
hash: string;
|
|
15
|
+
source: SkillSource;
|
|
16
|
+
};
|
|
17
|
+
export declare function skillsDir(): string;
|
|
18
|
+
export declare function ensureSkillsDir(): string;
|
|
19
|
+
export declare function hashSkillContent(content: string): string;
|
|
20
|
+
/**
|
|
21
|
+
* Skill names become directory names and slash commands, so keep them to the identifier
|
|
22
|
+
* shape the ecosystem uses and never let one escape the skills directory.
|
|
23
|
+
*/
|
|
24
|
+
export declare function slugifySkillName(raw: string): string;
|
|
25
|
+
export declare function isValidSkillName(raw: string): boolean;
|
|
26
|
+
export type Frontmatter = {
|
|
27
|
+
meta: Record<string, string | string[]>;
|
|
28
|
+
body: string;
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* Minimal YAML-frontmatter reader — enough for the scalar and inline-list keys skills
|
|
32
|
+
* actually use. Nested keys are flattened to their leaf (`metadata.hermes.tags` -> `tags`)
|
|
33
|
+
* so a Hermes-authored skill and a Cursor-authored one both parse.
|
|
34
|
+
*/
|
|
35
|
+
export declare function parseFrontmatter(raw: string): Frontmatter;
|
|
36
|
+
export declare function readSkillMeta(dir: string, source?: SkillSource): SkillMeta | null;
|
|
37
|
+
/** Every skill on disk, sorted by name. Skips dot/underscore dirs like the hub state. */
|
|
38
|
+
export declare function scanSkills(root?: string): SkillMeta[];
|
|
39
|
+
export declare function findSkillOnDisk(name: string, root?: string): SkillMeta | null;
|
|
40
|
+
export declare function skillDirFor(name: string, root?: string): string;
|
|
41
|
+
export declare function readSkillBody(name: string, root?: string): string | null;
|
|
42
|
+
/**
|
|
43
|
+
* Read a supporting file (`references/foo.md`). Skills come from other people, so the
|
|
44
|
+
* path is resolved and re-checked rather than trusted.
|
|
45
|
+
*/
|
|
46
|
+
export declare function readSkillAsset(name: string, relPath: string, root?: string): string | null;
|
|
47
|
+
export declare function listSkillAssets(name: string, root?: string): string[];
|
|
48
|
+
/** Render a SKILL.md from parts, for `amem skills new` and agent-authored saves. */
|
|
49
|
+
export declare function renderSkillMarkdown(input: {
|
|
50
|
+
name: string;
|
|
51
|
+
description: string;
|
|
52
|
+
body?: string;
|
|
53
|
+
version?: string;
|
|
54
|
+
tags?: string[];
|
|
55
|
+
}): string;
|
|
56
|
+
export declare function writeSkill(name: string, content: string, root?: string): {
|
|
57
|
+
name: string;
|
|
58
|
+
path: string;
|
|
59
|
+
hash: string;
|
|
60
|
+
};
|
|
61
|
+
export declare function deleteSkill(name: string, root?: string): boolean;
|
|
62
|
+
export type SkillScan = {
|
|
63
|
+
ok: true;
|
|
64
|
+
} | {
|
|
65
|
+
ok: false;
|
|
66
|
+
reason: string;
|
|
67
|
+
};
|
|
68
|
+
/**
|
|
69
|
+
* Gate content before it lands in the library. Deny patterns come from policy so an IT
|
|
70
|
+
* operator's additions apply to skills too, not just claims.
|
|
71
|
+
*/
|
|
72
|
+
export declare function scanSkillContent(content: string, denyPatterns?: RegExp[]): SkillScan;
|
|
73
|
+
/**
|
|
74
|
+
* Import a skill from a local directory. Local paths only — no registries and no network,
|
|
75
|
+
* which keeps this on the right side of the "no cloud, no hosted anything" line.
|
|
76
|
+
* Supporting files come along, but only from the allowlisted asset directories.
|
|
77
|
+
*/
|
|
78
|
+
export declare function importSkillFromPath(sourcePath: string, overrideName?: string, root?: string): {
|
|
79
|
+
name: string;
|
|
80
|
+
path: string;
|
|
81
|
+
files: string[];
|
|
82
|
+
};
|
|
83
|
+
export type IndexedSkill = SkillMeta & {
|
|
84
|
+
repoId: string | null;
|
|
85
|
+
uses: number;
|
|
86
|
+
lastUsedAt: string | null;
|
|
87
|
+
/** True when the file changed since it was installed — do not overwrite these. */
|
|
88
|
+
modified: boolean;
|
|
89
|
+
};
|
|
90
|
+
/**
|
|
91
|
+
* Reconcile the index with disk. Cheap enough to run before any read, which keeps the
|
|
92
|
+
* index honest when a user or agent edits a SKILL.md with ordinary file tools.
|
|
93
|
+
*/
|
|
94
|
+
export declare function syncSkillIndex(root?: string): IndexedSkill[];
|
|
95
|
+
export declare function listIndexedSkills(root?: string): IndexedSkill[];
|
|
96
|
+
export type RankedSkill = IndexedSkill & {
|
|
97
|
+
score: number;
|
|
98
|
+
reasons: string[];
|
|
99
|
+
};
|
|
100
|
+
/**
|
|
101
|
+
* Rank skills for a query. Deliberately matches on the index fields only — name,
|
|
102
|
+
* description, tags — because the whole point is to decide what is worth loading
|
|
103
|
+
* without paying for the bodies.
|
|
104
|
+
*/
|
|
105
|
+
export declare function skillSummary(s: SkillMeta | IndexedSkill): Record<string, unknown>;
|
|
106
|
+
export declare function rankSkills(skills: IndexedSkill[], query: string, limit?: number): RankedSkill[];
|