@iamem/amem 0.1.2 → 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 +64 -1
- package/dist/api/routes.js +331 -1
- package/dist/attest.d.ts +13 -0
- package/dist/attest.js +44 -0
- package/dist/cli.js +241 -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/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 +1 -1
- 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/skills.js
ADDED
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Procedural memory. Claims are small facts that ride in every packet; a skill is a
|
|
3
|
+
* longer procedure that should only load when it is relevant.
|
|
4
|
+
*
|
|
5
|
+
* Files on disk are the source of truth (`~/.amem/skills/<name>/SKILL.md`), because every
|
|
6
|
+
* other agent tool in this ecosystem — Cursor, Claude, Hermes — discovers skills by
|
|
7
|
+
* scanning folders, and a skill's `references/` and `scripts/` do not fit in a column.
|
|
8
|
+
* SQLite only indexes them for ranking and usage stats; see `src/db.ts`.
|
|
9
|
+
*/
|
|
10
|
+
import { createHash } from "node:crypto";
|
|
11
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync, } from "node:fs";
|
|
12
|
+
import { join, resolve, sep } from "node:path";
|
|
13
|
+
import { pruneSkillRows, upsertSkillRow } from "./db.js";
|
|
14
|
+
import { amemHome } from "./paths.js";
|
|
15
|
+
import { compiledDenyPatterns } from "./policy.js";
|
|
16
|
+
import { tokenize } from "./search.js";
|
|
17
|
+
export const SKILL_FILE = "SKILL.md";
|
|
18
|
+
/** Subdirectories a skill may carry, matching the agentskills.io layout. */
|
|
19
|
+
export const SKILL_ASSET_DIRS = ["references", "templates", "scripts", "examples", "assets"];
|
|
20
|
+
export function skillsDir() {
|
|
21
|
+
return join(amemHome(), "skills");
|
|
22
|
+
}
|
|
23
|
+
export function ensureSkillsDir() {
|
|
24
|
+
const dir = skillsDir();
|
|
25
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
26
|
+
return dir;
|
|
27
|
+
}
|
|
28
|
+
export function hashSkillContent(content) {
|
|
29
|
+
return createHash("sha256").update(content, "utf8").digest("hex").slice(0, 16);
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Skill names become directory names and slash commands, so keep them to the identifier
|
|
33
|
+
* shape the ecosystem uses and never let one escape the skills directory.
|
|
34
|
+
*/
|
|
35
|
+
export function slugifySkillName(raw) {
|
|
36
|
+
const slug = String(raw || "")
|
|
37
|
+
.trim()
|
|
38
|
+
.toLowerCase()
|
|
39
|
+
.replace(/[^a-z0-9_-]+/g, "-")
|
|
40
|
+
.replace(/^-+|-+$/g, "")
|
|
41
|
+
.slice(0, 64);
|
|
42
|
+
return slug;
|
|
43
|
+
}
|
|
44
|
+
export function isValidSkillName(raw) {
|
|
45
|
+
return /^[a-z][a-z0-9_-]*$/.test(String(raw || "")) && String(raw).length <= 64;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Minimal YAML-frontmatter reader — enough for the scalar and inline-list keys skills
|
|
49
|
+
* actually use. Nested keys are flattened to their leaf (`metadata.hermes.tags` -> `tags`)
|
|
50
|
+
* so a Hermes-authored skill and a Cursor-authored one both parse.
|
|
51
|
+
*/
|
|
52
|
+
export function parseFrontmatter(raw) {
|
|
53
|
+
const text = String(raw ?? "").replace(/^\uFEFF/, "");
|
|
54
|
+
const match = /^---[ \t]*\r?\n([\s\S]*?)\r?\n---[ \t]*(?:\r?\n|$)/.exec(text);
|
|
55
|
+
if (!match)
|
|
56
|
+
return { meta: {}, body: text.trim() };
|
|
57
|
+
const meta = {};
|
|
58
|
+
for (const line of match[1].split(/\r?\n/)) {
|
|
59
|
+
if (!line.trim() || line.trim().startsWith("#"))
|
|
60
|
+
continue;
|
|
61
|
+
const kv = /^\s*([A-Za-z0-9_.-]+)\s*:\s*(.*)$/.exec(line);
|
|
62
|
+
if (!kv)
|
|
63
|
+
continue;
|
|
64
|
+
const key = kv[1].split(".").pop().toLowerCase();
|
|
65
|
+
const value = kv[2].trim();
|
|
66
|
+
if (!value)
|
|
67
|
+
continue; // a bare `metadata:` parent carries nothing itself
|
|
68
|
+
if (value.startsWith("[") && value.endsWith("]")) {
|
|
69
|
+
meta[key] = value
|
|
70
|
+
.slice(1, -1)
|
|
71
|
+
.split(",")
|
|
72
|
+
.map((v) => stripQuotes(v.trim()))
|
|
73
|
+
.filter(Boolean);
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
meta[key] = stripQuotes(value);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return { meta, body: text.slice(match[0].length).trim() };
|
|
80
|
+
}
|
|
81
|
+
function stripQuotes(value) {
|
|
82
|
+
const v = value.trim();
|
|
83
|
+
if (v.length >= 2 && ((v.startsWith('"') && v.endsWith('"')) || (v.startsWith("'") && v.endsWith("'")))) {
|
|
84
|
+
return v.slice(1, -1);
|
|
85
|
+
}
|
|
86
|
+
return v;
|
|
87
|
+
}
|
|
88
|
+
function metaString(meta, key) {
|
|
89
|
+
const v = meta[key];
|
|
90
|
+
if (typeof v === "string" && v.trim())
|
|
91
|
+
return v.trim();
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
function metaList(meta, key) {
|
|
95
|
+
const v = meta[key];
|
|
96
|
+
if (Array.isArray(v))
|
|
97
|
+
return v.filter((s) => typeof s === "string" && s.trim()).map((s) => s.trim());
|
|
98
|
+
if (typeof v === "string" && v.trim())
|
|
99
|
+
return v.split(/[,\s]+/).filter(Boolean);
|
|
100
|
+
return [];
|
|
101
|
+
}
|
|
102
|
+
/** First markdown heading, used when a skill has no `description:` to show. */
|
|
103
|
+
function firstHeading(body) {
|
|
104
|
+
for (const line of body.split(/\r?\n/)) {
|
|
105
|
+
const h = /^#{1,3}\s+(.+?)\s*$/.exec(line);
|
|
106
|
+
if (h)
|
|
107
|
+
return h[1].trim();
|
|
108
|
+
}
|
|
109
|
+
return "";
|
|
110
|
+
}
|
|
111
|
+
export function readSkillMeta(dir, source = "local") {
|
|
112
|
+
const file = join(dir, SKILL_FILE);
|
|
113
|
+
if (!existsSync(file) || !statSync(file).isFile())
|
|
114
|
+
return null;
|
|
115
|
+
const raw = readFileSync(file, "utf8");
|
|
116
|
+
const { meta, body } = parseFrontmatter(raw);
|
|
117
|
+
// Name resolution mirrors the ecosystem: frontmatter first, then the folder name.
|
|
118
|
+
const declared = metaString(meta, "name");
|
|
119
|
+
const name = isValidSkillName(declared || "") ? declared : slugifySkillName(dir.split(sep).pop() || "");
|
|
120
|
+
if (!name)
|
|
121
|
+
return null;
|
|
122
|
+
return {
|
|
123
|
+
name,
|
|
124
|
+
description: metaString(meta, "description") || firstHeading(body),
|
|
125
|
+
version: metaString(meta, "version"),
|
|
126
|
+
tags: metaList(meta, "tags"),
|
|
127
|
+
path: file,
|
|
128
|
+
dir,
|
|
129
|
+
hash: hashSkillContent(raw),
|
|
130
|
+
source,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
/** Every skill on disk, sorted by name. Skips dot/underscore dirs like the hub state. */
|
|
134
|
+
export function scanSkills(root = skillsDir()) {
|
|
135
|
+
if (!existsSync(root))
|
|
136
|
+
return [];
|
|
137
|
+
const out = [];
|
|
138
|
+
for (const entry of readdirSync(root, { withFileTypes: true })) {
|
|
139
|
+
if (!entry.isDirectory())
|
|
140
|
+
continue;
|
|
141
|
+
if (entry.name.startsWith(".") || entry.name.startsWith("_"))
|
|
142
|
+
continue;
|
|
143
|
+
const dir = join(root, entry.name);
|
|
144
|
+
const meta = readSkillMeta(dir);
|
|
145
|
+
if (meta) {
|
|
146
|
+
out.push(meta);
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
// One level of category nesting, the way Hermes groups skills (mlops/axolotl).
|
|
150
|
+
for (const child of readdirSync(dir, { withFileTypes: true })) {
|
|
151
|
+
if (!child.isDirectory() || child.name.startsWith("."))
|
|
152
|
+
continue;
|
|
153
|
+
const nested = readSkillMeta(join(dir, child.name));
|
|
154
|
+
if (nested)
|
|
155
|
+
out.push(nested);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return out.sort((a, b) => a.name.localeCompare(b.name));
|
|
159
|
+
}
|
|
160
|
+
export function findSkillOnDisk(name, root = skillsDir()) {
|
|
161
|
+
const slug = slugifySkillName(name);
|
|
162
|
+
if (!slug)
|
|
163
|
+
return null;
|
|
164
|
+
return scanSkills(root).find((s) => s.name === slug) ?? null;
|
|
165
|
+
}
|
|
166
|
+
export function skillDirFor(name, root = skillsDir()) {
|
|
167
|
+
const slug = slugifySkillName(name);
|
|
168
|
+
if (!isValidSkillName(slug))
|
|
169
|
+
throw new Error(`Invalid skill name: ${name}`);
|
|
170
|
+
return join(root, slug);
|
|
171
|
+
}
|
|
172
|
+
export function readSkillBody(name, root = skillsDir()) {
|
|
173
|
+
const meta = findSkillOnDisk(name, root);
|
|
174
|
+
return meta ? readFileSync(meta.path, "utf8") : null;
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Read a supporting file (`references/foo.md`). Skills come from other people, so the
|
|
178
|
+
* path is resolved and re-checked rather than trusted.
|
|
179
|
+
*/
|
|
180
|
+
export function readSkillAsset(name, relPath, root = skillsDir()) {
|
|
181
|
+
const meta = findSkillOnDisk(name, root);
|
|
182
|
+
if (!meta)
|
|
183
|
+
return null;
|
|
184
|
+
const base = resolve(meta.dir);
|
|
185
|
+
const target = resolve(base, relPath);
|
|
186
|
+
// resolve() collapses "..", and an absolute relPath lands outside base — both caught here.
|
|
187
|
+
if (target !== base && !target.startsWith(base + sep))
|
|
188
|
+
return null;
|
|
189
|
+
if (!existsSync(target) || !statSync(target).isFile())
|
|
190
|
+
return null;
|
|
191
|
+
return readFileSync(target, "utf8");
|
|
192
|
+
}
|
|
193
|
+
export function listSkillAssets(name, root = skillsDir()) {
|
|
194
|
+
const meta = findSkillOnDisk(name, root);
|
|
195
|
+
if (!meta)
|
|
196
|
+
return [];
|
|
197
|
+
const out = [];
|
|
198
|
+
for (const sub of SKILL_ASSET_DIRS) {
|
|
199
|
+
const dir = join(meta.dir, sub);
|
|
200
|
+
if (!existsSync(dir) || !statSync(dir).isDirectory())
|
|
201
|
+
continue;
|
|
202
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
203
|
+
if (entry.isFile())
|
|
204
|
+
out.push(`${sub}/${entry.name}`);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
return out.sort();
|
|
208
|
+
}
|
|
209
|
+
/** Render a SKILL.md from parts, for `amem skills new` and agent-authored saves. */
|
|
210
|
+
export function renderSkillMarkdown(input) {
|
|
211
|
+
const lines = ["---", `name: ${input.name}`, `description: ${input.description}`];
|
|
212
|
+
if (input.version)
|
|
213
|
+
lines.push(`version: ${input.version}`);
|
|
214
|
+
if (input.tags?.length)
|
|
215
|
+
lines.push(`tags: [${input.tags.join(", ")}]`);
|
|
216
|
+
lines.push("---", "");
|
|
217
|
+
const body = (input.body || "").trim();
|
|
218
|
+
lines.push(body || defaultSkillBody(input.name));
|
|
219
|
+
return `${lines.join("\n")}\n`;
|
|
220
|
+
}
|
|
221
|
+
function defaultSkillBody(name) {
|
|
222
|
+
const title = name.replace(/[-_]+/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
|
223
|
+
return [
|
|
224
|
+
`# ${title}`,
|
|
225
|
+
"",
|
|
226
|
+
"## When to use",
|
|
227
|
+
"",
|
|
228
|
+
"Describe the trigger — the situation where this procedure applies.",
|
|
229
|
+
"",
|
|
230
|
+
"## Procedure",
|
|
231
|
+
"",
|
|
232
|
+
"1. First step.",
|
|
233
|
+
"2. Second step.",
|
|
234
|
+
"",
|
|
235
|
+
"## Pitfalls",
|
|
236
|
+
"",
|
|
237
|
+
"- Known failure modes and how to get past them.",
|
|
238
|
+
"",
|
|
239
|
+
"## Verification",
|
|
240
|
+
"",
|
|
241
|
+
"How to confirm it worked.",
|
|
242
|
+
].join("\n");
|
|
243
|
+
}
|
|
244
|
+
export function writeSkill(name, content, root = skillsDir()) {
|
|
245
|
+
const slug = slugifySkillName(name);
|
|
246
|
+
if (!isValidSkillName(slug))
|
|
247
|
+
throw new Error(`Invalid skill name: ${name}`);
|
|
248
|
+
const dir = join(root, slug);
|
|
249
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
250
|
+
const path = join(dir, SKILL_FILE);
|
|
251
|
+
const body = content.endsWith("\n") ? content : `${content}\n`;
|
|
252
|
+
writeFileSync(path, body, { mode: 0o600 });
|
|
253
|
+
return { name: slug, path, hash: hashSkillContent(body) };
|
|
254
|
+
}
|
|
255
|
+
export function deleteSkill(name, root = skillsDir()) {
|
|
256
|
+
const meta = findSkillOnDisk(name, root);
|
|
257
|
+
if (!meta)
|
|
258
|
+
return false;
|
|
259
|
+
rmSync(meta.dir, { recursive: true, force: true });
|
|
260
|
+
return true;
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* Prompt-injection shapes that have no business in a stored procedure. A skill is riskier
|
|
264
|
+
* than a claim: a claim is a fact the agent reads, a skill is an instruction it follows.
|
|
265
|
+
*/
|
|
266
|
+
const SKILL_INJECTION_PATTERNS = [
|
|
267
|
+
{
|
|
268
|
+
re: /ignore\s+(?:all\s+)?(?:previous|prior|above)\s+instructions/i,
|
|
269
|
+
reason: "prompt-injection directive",
|
|
270
|
+
},
|
|
271
|
+
{ re: /disregard\s+(?:your\s+)?(?:system\s+prompt|safety|guidelines)/i, reason: "safety override" },
|
|
272
|
+
{ re: /\bcurl\b[^\n]*\|\s*(?:ba)?sh\b/i, reason: "pipes a remote script into a shell" },
|
|
273
|
+
{ re: /rm\s+-rf\s+[~/]\s*(?:$|[^\w/])/im, reason: "destructive filesystem command" },
|
|
274
|
+
{
|
|
275
|
+
re: /(?:curl|wget|fetch)[^\n]*(?:AWS_|API_KEY|SECRET|TOKEN|\.env\b|id_rsa)/i,
|
|
276
|
+
reason: "looks like credential exfiltration",
|
|
277
|
+
},
|
|
278
|
+
];
|
|
279
|
+
/**
|
|
280
|
+
* Gate content before it lands in the library. Deny patterns come from policy so an IT
|
|
281
|
+
* operator's additions apply to skills too, not just claims.
|
|
282
|
+
*/
|
|
283
|
+
export function scanSkillContent(content, denyPatterns) {
|
|
284
|
+
const text = String(content ?? "");
|
|
285
|
+
if (!text.trim())
|
|
286
|
+
return { ok: false, reason: "empty skill content" };
|
|
287
|
+
for (const { re, reason } of SKILL_INJECTION_PATTERNS) {
|
|
288
|
+
if (re.test(text))
|
|
289
|
+
return { ok: false, reason };
|
|
290
|
+
}
|
|
291
|
+
const deny = denyPatterns ?? compiledDenyPatterns();
|
|
292
|
+
for (const re of deny) {
|
|
293
|
+
if (re.test(text))
|
|
294
|
+
return { ok: false, reason: `matches deny pattern ${re.source}` };
|
|
295
|
+
}
|
|
296
|
+
return { ok: true };
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* Import a skill from a local directory. Local paths only — no registries and no network,
|
|
300
|
+
* which keeps this on the right side of the "no cloud, no hosted anything" line.
|
|
301
|
+
* Supporting files come along, but only from the allowlisted asset directories.
|
|
302
|
+
*/
|
|
303
|
+
export function importSkillFromPath(sourcePath, overrideName, root = skillsDir()) {
|
|
304
|
+
const src = resolve(sourcePath);
|
|
305
|
+
if (!existsSync(src))
|
|
306
|
+
throw new Error(`No such path: ${sourcePath}`);
|
|
307
|
+
const srcDir = statSync(src).isDirectory() ? src : join(src, "..");
|
|
308
|
+
const skillFile = statSync(src).isDirectory() ? join(src, SKILL_FILE) : src;
|
|
309
|
+
if (!existsSync(skillFile))
|
|
310
|
+
throw new Error(`No ${SKILL_FILE} found at ${sourcePath}`);
|
|
311
|
+
const raw = readFileSync(skillFile, "utf8");
|
|
312
|
+
const scan = scanSkillContent(raw);
|
|
313
|
+
if (!scan.ok)
|
|
314
|
+
throw new Error(`Refusing to import: ${scan.reason}`);
|
|
315
|
+
const { meta } = parseFrontmatter(raw);
|
|
316
|
+
const declared = typeof meta.name === "string" ? meta.name : "";
|
|
317
|
+
const candidate = overrideName || declared || srcDir.split(sep).pop() || "";
|
|
318
|
+
const slug = slugifySkillName(candidate);
|
|
319
|
+
if (!isValidSkillName(slug)) {
|
|
320
|
+
throw new Error(`Could not derive a skill name from ${sourcePath} — pass --name`);
|
|
321
|
+
}
|
|
322
|
+
const written = writeSkill(slug, raw, root);
|
|
323
|
+
const destDir = join(root, slug);
|
|
324
|
+
const files = [];
|
|
325
|
+
for (const sub of SKILL_ASSET_DIRS) {
|
|
326
|
+
const from = join(srcDir, sub);
|
|
327
|
+
if (!existsSync(from) || !statSync(from).isDirectory())
|
|
328
|
+
continue;
|
|
329
|
+
for (const entry of readdirSync(from, { withFileTypes: true })) {
|
|
330
|
+
if (!entry.isFile())
|
|
331
|
+
continue;
|
|
332
|
+
const target = join(destDir, sub, entry.name);
|
|
333
|
+
mkdirSync(join(destDir, sub), { recursive: true, mode: 0o700 });
|
|
334
|
+
writeFileSync(target, readFileSync(join(from, entry.name)), { mode: 0o600 });
|
|
335
|
+
files.push(`${sub}/${entry.name}`);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
return { name: written.name, path: written.path, files };
|
|
339
|
+
}
|
|
340
|
+
/**
|
|
341
|
+
* Reconcile the index with disk. Cheap enough to run before any read, which keeps the
|
|
342
|
+
* index honest when a user or agent edits a SKILL.md with ordinary file tools.
|
|
343
|
+
*/
|
|
344
|
+
export function syncSkillIndex(root = skillsDir()) {
|
|
345
|
+
const found = scanSkills(root);
|
|
346
|
+
const out = [];
|
|
347
|
+
for (const meta of found) {
|
|
348
|
+
const row = upsertSkillRow({
|
|
349
|
+
name: meta.name,
|
|
350
|
+
path: meta.path,
|
|
351
|
+
description: meta.description,
|
|
352
|
+
version: meta.version,
|
|
353
|
+
tags: meta.tags,
|
|
354
|
+
contentHash: meta.hash,
|
|
355
|
+
source: meta.source,
|
|
356
|
+
});
|
|
357
|
+
out.push({
|
|
358
|
+
...meta,
|
|
359
|
+
repoId: row.repo_id,
|
|
360
|
+
uses: row.uses,
|
|
361
|
+
lastUsedAt: row.last_used_at,
|
|
362
|
+
modified: Boolean(row.origin_hash) && row.origin_hash !== meta.hash,
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
pruneSkillRows(found.map((s) => s.name));
|
|
366
|
+
return out;
|
|
367
|
+
}
|
|
368
|
+
export function listIndexedSkills(root = skillsDir()) {
|
|
369
|
+
return syncSkillIndex(root);
|
|
370
|
+
}
|
|
371
|
+
/**
|
|
372
|
+
* Rank skills for a query. Deliberately matches on the index fields only — name,
|
|
373
|
+
* description, tags — because the whole point is to decide what is worth loading
|
|
374
|
+
* without paying for the bodies.
|
|
375
|
+
*/
|
|
376
|
+
export function skillSummary(s) {
|
|
377
|
+
return {
|
|
378
|
+
name: s.name,
|
|
379
|
+
description: s.description,
|
|
380
|
+
version: s.version,
|
|
381
|
+
tags: s.tags,
|
|
382
|
+
path: s.path,
|
|
383
|
+
dir: s.dir,
|
|
384
|
+
hash: s.hash,
|
|
385
|
+
source: s.source,
|
|
386
|
+
...("repoId" in s ? { repoId: s.repoId, uses: s.uses, lastUsedAt: s.lastUsedAt, modified: s.modified } : {}),
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
export function rankSkills(skills, query, limit = 3) {
|
|
390
|
+
const tokens = tokenize(query);
|
|
391
|
+
if (tokens.length === 0)
|
|
392
|
+
return [];
|
|
393
|
+
const ranked = [];
|
|
394
|
+
for (const skill of skills) {
|
|
395
|
+
const name = skill.name.toLowerCase();
|
|
396
|
+
const haystack = `${name} ${skill.description} ${skill.tags.join(" ")}`.toLowerCase();
|
|
397
|
+
let score = 0;
|
|
398
|
+
const reasons = [];
|
|
399
|
+
let hits = 0;
|
|
400
|
+
for (const token of tokens) {
|
|
401
|
+
if (name.includes(token)) {
|
|
402
|
+
score += 6;
|
|
403
|
+
hits += 1;
|
|
404
|
+
}
|
|
405
|
+
else if (haystack.includes(token)) {
|
|
406
|
+
score += token.length > 4 ? 3 : 2;
|
|
407
|
+
hits += 1;
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
if (hits === 0)
|
|
411
|
+
continue;
|
|
412
|
+
reasons.push(`match+${score}`);
|
|
413
|
+
// A skill that keeps getting used is a better bet than one nobody has opened.
|
|
414
|
+
if (skill.uses > 0) {
|
|
415
|
+
const useBoost = Math.min(4, Math.log2(skill.uses + 1) * 2);
|
|
416
|
+
score += useBoost;
|
|
417
|
+
reasons.push(`used×${skill.uses}`);
|
|
418
|
+
}
|
|
419
|
+
ranked.push({ ...skill, score, reasons });
|
|
420
|
+
}
|
|
421
|
+
return ranked.sort((a, b) => b.score - a.score || a.name.localeCompare(b.name)).slice(0, limit);
|
|
422
|
+
}
|
package/docs/backlog.md
CHANGED
|
@@ -4,6 +4,7 @@ Updated after completing the Feature Map **Later** phase (local embedding model
|
|
|
4
4
|
|
|
5
5
|
## Shipped recently
|
|
6
6
|
|
|
7
|
+
- **Agent Tasks Kanban** — per-project board + MCP `amem_task_*` + open tasks in context packets (complement to Memory facts)
|
|
7
8
|
- FTS retrieval, claim staleness, supersede/conflict
|
|
8
9
|
- Session-end **draft capture** + Memory approve/dismiss
|
|
9
10
|
- Memory **edit / delete / pin / search**
|
|
@@ -37,6 +38,14 @@ Updated after completing the Feature Map **Later** phase (local embedding model
|
|
|
37
38
|
|
|
38
39
|
## Open
|
|
39
40
|
|
|
41
|
+
- **Skills in backup/restore** — `createBackup` copies only the DB file, so `~/.amem/skills/`
|
|
42
|
+
is lost on restore. Now sharper than before: the `skills` index, `skill_drafts`, and
|
|
43
|
+
`skill_uses` tables all survive a restore while the SKILL.md bodies they point at do not,
|
|
44
|
+
so a restored machine gets an index of skills that no longer exist. Needs a decision:
|
|
45
|
+
make backups an archive (DB + skills dir), or keep backups DB-only, prune the index on
|
|
46
|
+
restore, and document skills as separately versioned.
|
|
47
|
+
- **Skill drafts have no retention policy** — dismissed and applied rows accumulate. Memory
|
|
48
|
+
drafts have hygiene sweeps; skill drafts do not yet.
|
|
40
49
|
- Prompt-pack before/after Stats benchmark; restore wizard polish; IT seat pack.
|
|
41
50
|
- Decide one-time vs subscription (offline files cannot revoke on cancel unless you add `expires_at` and re-issue).
|
|
42
51
|
- Optional vendored ONNX/MiniLM weights in a paid pack (external command is the local hook today).
|
package/package.json
CHANGED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Manage deferred tasks and Kanban lifecycle in local personal amem memory so work is preserved across sessions and available for context retrieval.
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# amem-tasks
|
|
6
|
+
|
|
7
|
+
Track deferred work, multi-step progress, and backlog items in local amem memory so tasks never get lost across chat sessions.
|
|
8
|
+
|
|
9
|
+
## Privacy
|
|
10
|
+
|
|
11
|
+
Memory is personal and stored under `~/.amem` on this machine. Tasks stay local and are never committed to remote repositories.
|
|
12
|
+
|
|
13
|
+
## When to Use
|
|
14
|
+
|
|
15
|
+
1. **Multi-step work**: When working on complex tasks or features with 3+ steps, create and manage tasks to track progress.
|
|
16
|
+
2. **Deferred work**: When user or agent identifies follow-up work ("do X later", "verify Y after deploy", "refactor Z next week"), add it to the project backlog.
|
|
17
|
+
3. **Session continuity & handoff**: Before ending a session or moving to a new topic, park unfinished items on the board.
|
|
18
|
+
4. **Context retrieval**: Check `amem_task_list` or context packet `## Open tasks` / `## Tasks` to review current and completed work.
|
|
19
|
+
|
|
20
|
+
## Task Status Lifecycle
|
|
21
|
+
|
|
22
|
+
- `backlog` — Queued items, follow-ups, and ideas.
|
|
23
|
+
- `next` — Prioritized tasks ready to start soon.
|
|
24
|
+
- `doing` — Currently in progress (keep 1 active task at a time).
|
|
25
|
+
- `blocked` — Stalled on external feedback, bug, or dependency.
|
|
26
|
+
- `done` — Finished work (retained for context retrieval and history).
|
|
27
|
+
|
|
28
|
+
## Available Tools
|
|
29
|
+
|
|
30
|
+
### 1. MCP Tools (Cursor, Claude Code, Windsurf, Continue, Zed)
|
|
31
|
+
|
|
32
|
+
- **`amem_task_add`**
|
|
33
|
+
```json
|
|
34
|
+
{
|
|
35
|
+
"title": "Add rate limiter middleware to /api/auth",
|
|
36
|
+
"body": "Protect login endpoints against brute-force attempts",
|
|
37
|
+
"status": "backlog",
|
|
38
|
+
"anchors": ["src/api/auth.ts"]
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
- **`amem_task_list`**
|
|
43
|
+
```json
|
|
44
|
+
{
|
|
45
|
+
"status": "doing",
|
|
46
|
+
"include_done": true
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
- **`amem_task_update`**
|
|
51
|
+
```json
|
|
52
|
+
{
|
|
53
|
+
"id": "task_1234abcd",
|
|
54
|
+
"status": "doing",
|
|
55
|
+
"body": "Updated progress notes..."
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
- **`amem_task_complete`**
|
|
60
|
+
```json
|
|
61
|
+
{
|
|
62
|
+
"id": "task_1234abcd"
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### 2. CLI Commands (Terminal, Aider, Bash scripts)
|
|
67
|
+
|
|
68
|
+
- **List tasks**:
|
|
69
|
+
```bash
|
|
70
|
+
amem task list
|
|
71
|
+
amem task list --status doing
|
|
72
|
+
amem task list --include-done --all
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
- **Add task**:
|
|
76
|
+
```bash
|
|
77
|
+
amem task add "Add rate limiter middleware" --body "Protect /api/auth" --status backlog --anchor src/api/auth.ts
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
- **Update task**:
|
|
81
|
+
```bash
|
|
82
|
+
amem task update <id> --status doing
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
- **Complete task**:
|
|
86
|
+
```bash
|
|
87
|
+
amem task complete <id>
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
- **Delete task**:
|
|
91
|
+
```bash
|
|
92
|
+
amem task delete <id>
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Instructions for Agents
|
|
96
|
+
|
|
97
|
+
1. **Deconstruct**: When starting a non-trivial user request, add the plan items to `amem_task_add`.
|
|
98
|
+
2. **Track**: Mark the current working task as `doing` with `amem_task_update`.
|
|
99
|
+
3. **Complete**: When a task is finished, call `amem_task_complete` immediately. Completed tasks stay stored in local memory and are searchable in context retrieval.
|
|
100
|
+
4. **Learn**: If completing the task revealed durable repository facts (constraints, gotchas, ownership), save them with `amem_remember` or `amem-update-working-memory`.
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: amem-write-skill
|
|
3
|
+
description: Write a durable procedure from this session into local amem skills.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# amem-write-skill
|
|
7
|
+
|
|
8
|
+
Turn a non-trivial workflow you just worked out into a reusable skill stored in local amem.
|
|
9
|
+
|
|
10
|
+
Use this when an amem context packet shows a **Worth saving as a skill** or **Skill worth
|
|
11
|
+
revising** nudge, or when you notice on your own that you solved something worth repeating.
|
|
12
|
+
|
|
13
|
+
## Privacy
|
|
14
|
+
|
|
15
|
+
- Skills are stored locally under `~/.amem/skills` and never leave this machine.
|
|
16
|
+
- Do not store secrets, tokens, credentials, connection strings, or private personal data.
|
|
17
|
+
amem scans content and will refuse writes that look like credentials.
|
|
18
|
+
- Do not paste proprietary company LLM instructions — write the procedure, not the prompt.
|
|
19
|
+
|
|
20
|
+
## When to write one
|
|
21
|
+
|
|
22
|
+
Write a skill when at least one of these is true:
|
|
23
|
+
|
|
24
|
+
- You worked out a multi-step workflow that will come up again.
|
|
25
|
+
- You hit errors or dead ends and found the path that actually works.
|
|
26
|
+
- The user corrected your approach and the correction generalizes.
|
|
27
|
+
|
|
28
|
+
Do **not** write a skill for a one-off fix, a single command, or anything already obvious
|
|
29
|
+
from the repo's README. A small durable fact belongs in memory (`amem_remember`), not here.
|
|
30
|
+
The split: memory holds facts that should always be in context; skills hold procedures that
|
|
31
|
+
should load only when relevant.
|
|
32
|
+
|
|
33
|
+
## Steps
|
|
34
|
+
|
|
35
|
+
1. Check what already exists so you update instead of duplicating:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
amem skills list
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
If a related skill exists, load it and revise it rather than writing a second one:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
amem skills show <name>
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
2. Draft the SKILL.md. Keep the description under about 80 characters — it is the only
|
|
48
|
+
thing agents see until they load the body, so it must say *when to use this*, not just
|
|
49
|
+
what it is.
|
|
50
|
+
|
|
51
|
+
3. Save it with the `amem_skill_save` MCP tool, passing `name`, `description`, and
|
|
52
|
+
`content`. Prefer that over writing files directly so the content scan runs.
|
|
53
|
+
|
|
54
|
+
## Format
|
|
55
|
+
|
|
56
|
+
```markdown
|
|
57
|
+
---
|
|
58
|
+
name: deploy-staging
|
|
59
|
+
description: Deploy the staging server and verify the health endpoint
|
|
60
|
+
tags: [deploy, staging]
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
# Deploy Staging
|
|
64
|
+
|
|
65
|
+
## When to use
|
|
66
|
+
Trigger conditions — the situation where this procedure applies.
|
|
67
|
+
|
|
68
|
+
## Procedure
|
|
69
|
+
1. Concrete step with the real command.
|
|
70
|
+
2. Next step.
|
|
71
|
+
|
|
72
|
+
## Pitfalls
|
|
73
|
+
- The dead end you hit, and what got you past it.
|
|
74
|
+
|
|
75
|
+
## Verification
|
|
76
|
+
How to confirm it actually worked.
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Pitfalls
|
|
80
|
+
|
|
81
|
+
- **Vague descriptions.** "Helps with deploys" is useless for ranking. Name the trigger.
|
|
82
|
+
- **Transcribing the chat.** Write the procedure that worked, not the exploration that led
|
|
83
|
+
to it. Skip the wrong turns except as entries under Pitfalls.
|
|
84
|
+
- **Inventing steps.** Only include commands you actually ran and saw work.
|
|
85
|
+
- **Duplicates.** Revising an existing skill beats adding a near-copy.
|
|
86
|
+
|
|
87
|
+
## Verification
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
amem skills list # your skill appears with its description
|
|
91
|
+
amem skills show <name> # body reads as a procedure someone else could follow
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
If a write is staged instead of saved, an approval gate is on. Review it with:
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
amem skills drafts
|
|
98
|
+
amem skills approve <draft-id>
|
|
99
|
+
```
|
|
@@ -1,23 +1,33 @@
|
|
|
1
1
|
---
|
|
2
|
-
description: Use local personal amem memory before broad codebase exploration
|
|
2
|
+
description: Use local personal amem memory and tasks before broad codebase exploration
|
|
3
3
|
globs:
|
|
4
4
|
alwaysApply: true
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
<!-- Generated by amem. Safe to commit: contains no memory contents. -->
|
|
8
8
|
|
|
9
|
-
# amem local memory
|
|
9
|
+
# amem local memory & tasks
|
|
10
10
|
|
|
11
|
-
amem injects matching local memory into Cursor automatically (session start + each prompt). Treat that packet as the first map of this repo.
|
|
11
|
+
amem injects matching local memory and open tasks into Cursor automatically (session start + each prompt). Treat that packet as the first map of this repo.
|
|
12
12
|
|
|
13
13
|
1. Prefer amem file anchors over broad greps and multi-folder reads.
|
|
14
14
|
2. Still verify current code before editing — memory can be stale. Trust **fresh** claims more; re-check anything marked **stale**.
|
|
15
15
|
3. Use the **Why:** line as ranking explainability, not as proof.
|
|
16
|
-
4.
|
|
17
|
-
|
|
16
|
+
4. **Agent Tasks & Kanban:**
|
|
17
|
+
- Use `amem_task_add` to record deferred work, follow-ups, or multi-step tasks so they don't get lost across chat sessions.
|
|
18
|
+
- Update tasks to `doing` with `amem_task_update` when starting work.
|
|
19
|
+
- Mark completed tasks with `amem_task_complete` as soon as finished so completed tasks are preserved in history and context retrieval.
|
|
20
|
+
- Query `amem_task_list` (or check `## Open tasks` in context) to see pending and completed tasks.
|
|
21
|
+
5. **Skills (procedural memory):**
|
|
22
|
+
- A `## Relevant skills` section lists names and descriptions only. If one looks like it applies, call `amem_skill_view` to load the procedure **before** working it out yourself.
|
|
23
|
+
- When you solve something worth repeating — a multi-step workflow, a dead end you found the way past, or a correction the user gave you — save it with `amem_skill_save`. The `amem-write-skill` skill has the format.
|
|
24
|
+
- A `## Worth saving as a skill` or `## Skill worth revising` note in the packet means amem already spotted one. Write it up if you agree; it is a suggestion, not an order.
|
|
25
|
+
- Skills are procedures. Small durable facts still belong in memory via `amem_remember`.
|
|
26
|
+
6. After durable learnings, the stop hook queues a compact **session draft** (and may queue **miss→learn** drafts). Approve in `amem ui` → Memory, or run `amem-update-working-memory` for higher-quality facts.
|
|
27
|
+
7. Cross-repo personal prefs may appear with Why reason `personal` — they are local “how I work” notes, not org wiki.
|
|
18
28
|
|
|
19
29
|
Do not re-run `amem context` unless the injected packet is empty or clearly wrong.
|
|
20
30
|
|
|
21
31
|
Memory is personal and stored under `~/.amem` on this machine. Do not commit exports, backups, or database copies to shared remotes.
|
|
22
32
|
|
|
23
|
-
Open the local UI anytime with `amem ui` (Setup, Memory drafts, Stats).
|
|
33
|
+
Open the local UI anytime with `amem ui` (Setup, Memory drafts, Tasks, Skills, Stats).
|