@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/cli.js
CHANGED
|
@@ -3,7 +3,7 @@ import { mkdirSync, rmSync, writeFileSync, existsSync } from "node:fs";
|
|
|
3
3
|
import { dirname, join, resolve } from "node:path";
|
|
4
4
|
import { buildAttestReport, formatAttestHuman } from "./attest.js";
|
|
5
5
|
import { handleApi, logContextUsage } from "./api/routes.js";
|
|
6
|
-
import { closeDb, getRepoByCwd, getRepoByName, renameWorkspace, listClaims, listComponents, listFlows, requireRepo, setReportedOnLatest, setReportedTokensSaved, touchSession, upsertRepo, upsertSetupState, wipeAllRepos, wipeRepo, openDb, } from "./db.js";
|
|
6
|
+
import { closeDb, getRepoByCwd, getRepoByName, renameWorkspace, listClaims, listComponents, listFlows, listRepos, listTasks, listTasksAll, insertTask, updateTask, completeTask, deleteTask, findTaskAnyRepo, normalizeTaskStatus, requireRepo, setReportedOnLatest, setReportedTokensSaved, touchSession, upsertRepo, upsertSetupState, wipeAllRepos, wipeRepo, openDb, } from "./db.js";
|
|
7
7
|
import { handleHookPayload } from "./hook.js";
|
|
8
8
|
import { installClaude, claudeInstallHealth } from "./install/claude.js";
|
|
9
9
|
import { installCursor, cursorInstallHealth } from "./install/cursor.js";
|
|
@@ -11,7 +11,7 @@ import { installHost } from "./install/hosts.js";
|
|
|
11
11
|
import { amemHome, dbPath, tryEnsureDir } from "./paths.js";
|
|
12
12
|
import { assertExportAllowed, assertPlatformAllowed, assertRemoteAllowed, assertUiAllowed, loadPolicy, } from "./policy.js";
|
|
13
13
|
import { applyProposal, diffProposal, exportRepoMemory, formatProposalDiff, loadProposalFile, validateProposal, } from "./proposal.js";
|
|
14
|
-
import { detectRepoIdentity, parseWorkspaceSlug, workspaceIdentity } from "./repo-identity.js";
|
|
14
|
+
import { detectRepoIdentity, parseWorkspaceSlug, unbindableRootReason, workspaceIdentity, } from "./repo-identity.js";
|
|
15
15
|
import { runDesktopApp } from "./app-shell.js";
|
|
16
16
|
import { startUiServer, buildUiLandingUrl, openUiInBrowser, isAddrInUse, probeUiHealth, } from "./ui/server.js";
|
|
17
17
|
import { installLoginService, isServiceInstalled, isServiceSupported, uninstallLoginService } from "./service.js";
|
|
@@ -25,16 +25,18 @@ import { rememberContract } from "./remember-contract.js";
|
|
|
25
25
|
import { vaultStatus } from "./vault.js";
|
|
26
26
|
import { applyLicenseFile, clearLicense, generateLicenseKeys, licenseStatus, signLicense, } from "./license.js";
|
|
27
27
|
import { embedIndexIssues, embedStatus, reindexAllEmbeds, setEmbedBackend, } from "./embed.js";
|
|
28
|
-
import { acceptSafeCleanups, decayStaleClaims, hygieneReport, mergeDuplicate, runScheduledHygiene } from "./hygiene.js";
|
|
28
|
+
import { acceptSafeCleanups, decayStaleClaims, hygieneReport, mergeDuplicate, purgeNonFactClaims, runScheduledHygiene, } from "./hygiene.js";
|
|
29
29
|
import { hygieneSchedulePath, installHygieneSchedule, isHygieneScheduleInstalled, uninstallHygieneSchedule, writeHygieneHelperScript, } from "./hygiene-schedule.js";
|
|
30
30
|
import { syncPinnedRules } from "./rules-sync.js";
|
|
31
|
+
import { listSkillDrafts } from "./db.js";
|
|
32
|
+
import { deleteSkill, findSkillOnDisk, importSkillFromPath, isValidSkillName, listIndexedSkills, readSkillBody, renderSkillMarkdown, skillsDir, slugifySkillName, syncSkillIndex, writeSkill, } from "./skills.js";
|
|
31
33
|
import { buildSbom, writeItPack } from "./it-pack.js";
|
|
32
34
|
function usage() {
|
|
33
35
|
console.log(`amem — local personal agent memory
|
|
34
36
|
|
|
35
37
|
Usage:
|
|
36
38
|
amem setup [--personal] [--platform <host>]
|
|
37
|
-
amem init --platform cursor|claude|windsurf|continue|aider|zed
|
|
39
|
+
amem init --platform cursor|claude|claude-desktop|windsurf|continue|aider|zed
|
|
38
40
|
amem init --workspace <name> [--path <dir>] [--platform …]
|
|
39
41
|
amem init --personal
|
|
40
42
|
amem rename "<display name>" --workspace <slug>
|
|
@@ -43,6 +45,11 @@ Usage:
|
|
|
43
45
|
amem context "<query>" [--workspace <name>] [--platform cursor|claude|luna]
|
|
44
46
|
amem remember "<text>" [--workspace <name>] [--kind session] [--anchor <path>]
|
|
45
47
|
amem recipe [--json]
|
|
48
|
+
amem task [list] [--status <backlog|next|doing|blocked|done>] [--include-done] [--all] [--json]
|
|
49
|
+
amem task add <title> [--body <notes>] [--status <status>] [--anchor <file>]
|
|
50
|
+
amem task update <id> [--title <title>] [--body <notes>] [--status <status>]
|
|
51
|
+
amem task complete <id>
|
|
52
|
+
amem task delete <id>
|
|
46
53
|
amem propose validate <file.json>
|
|
47
54
|
amem propose diff <file.json>
|
|
48
55
|
amem propose apply <file.json>
|
|
@@ -56,10 +63,13 @@ Usage:
|
|
|
56
63
|
amem backup unschedule
|
|
57
64
|
amem restore --file <backup.db|backup.db.enc> [--passphrase <secret>]
|
|
58
65
|
amem hygiene [--days 90] [--decay] [--accept-safe] [--merge <keepId> <dropId>]
|
|
66
|
+
amem hygiene --purge-nonfacts [--all] [--apply] # default is a dry run
|
|
59
67
|
amem hygiene --scheduled
|
|
60
68
|
amem hygiene schedule [--hour <0-23>]
|
|
61
69
|
amem hygiene unschedule
|
|
62
70
|
amem rules sync
|
|
71
|
+
amem skills list|show <name>|new <name> [--desc <text>]|rm <name>|sync|import <path>
|
|
72
|
+
amem skills drafts|approve <id>|dismiss <id>
|
|
63
73
|
amem it-pack [--out <dir>]
|
|
64
74
|
amem doctor [--attest] [--sbom] [--json]
|
|
65
75
|
amem session touch --platform cursor|claude [--session-id <id>]
|
|
@@ -221,11 +231,29 @@ async function main() {
|
|
|
221
231
|
}
|
|
222
232
|
const platform = flagString(flags, "platform");
|
|
223
233
|
if (!platform) {
|
|
224
|
-
throw new Error("amem init requires --platform cursor|claude|windsurf|continue|aider|zed, --workspace <name>, or --personal");
|
|
234
|
+
throw new Error("amem init requires --platform cursor|claude|claude-desktop|windsurf|continue|aider|zed, --workspace <name>, or --personal");
|
|
225
235
|
}
|
|
226
236
|
const policy = loadPolicy().policy;
|
|
227
237
|
assertPlatformAllowed(platform, policy);
|
|
238
|
+
// Claude Desktop, Windsurf, Continue and Zed read a single machine-wide
|
|
239
|
+
// MCP config. There is nothing repo-specific to bind, and binding the
|
|
240
|
+
// cwd here is exactly how running this from a home directory
|
|
241
|
+
// registered the whole home as a repo.
|
|
242
|
+
if (HOST_INSTALL_IDS.has(platform) && platform !== "aider") {
|
|
243
|
+
const hostInfo = installHost(platform, { workspace: PERSONAL_SLUG });
|
|
244
|
+
console.log(`Configured ${hostInfo.host} (machine-wide, no repo binding)`);
|
|
245
|
+
for (const cfgPath of hostInfo.paths)
|
|
246
|
+
console.log(`Config: ${cfgPath}`);
|
|
247
|
+
for (const note of hostInfo.notes)
|
|
248
|
+
console.log(`Note: ${note}`);
|
|
249
|
+
break;
|
|
250
|
+
}
|
|
228
251
|
const identity = detectRepoIdentity();
|
|
252
|
+
const unbindable = unbindableRootReason(identity.rootPath);
|
|
253
|
+
if (unbindable) {
|
|
254
|
+
throw new Error(`Refusing to bind ${unbindable} (${identity.rootPath}) as a repo. ` +
|
|
255
|
+
"cd into a project first, or use: amem init --workspace <name>");
|
|
256
|
+
}
|
|
229
257
|
assertRemoteAllowed(identity.remoteUrl, policy);
|
|
230
258
|
const repo = upsertRepo(identity, platform);
|
|
231
259
|
let installInfo = { skills: [] };
|
|
@@ -646,6 +674,33 @@ async function main() {
|
|
|
646
674
|
}
|
|
647
675
|
break;
|
|
648
676
|
}
|
|
677
|
+
if (flags.get("purge-nonfacts")) {
|
|
678
|
+
const all = Boolean(flags.get("all"));
|
|
679
|
+
const scope = all ? {} : { repoId: resolveBinding(flags).id };
|
|
680
|
+
// Dry run unless --apply is passed: deletion is not reversible.
|
|
681
|
+
const preview = purgeNonFactClaims({ ...scope, dryRun: true });
|
|
682
|
+
const byReason = new Map();
|
|
683
|
+
for (const row of preview.matched) {
|
|
684
|
+
byReason.set(row.reason, (byReason.get(row.reason) ?? 0) + 1);
|
|
685
|
+
}
|
|
686
|
+
console.log(`Scanned ${preview.scanned} claim(s)${all ? " across every repo" : ""}; ${preview.matched.length} look like clear junk.`);
|
|
687
|
+
for (const [reason, n] of [...byReason].sort((a, b) => b[1] - a[1])) {
|
|
688
|
+
console.log(` ${reason}: ${n}`);
|
|
689
|
+
}
|
|
690
|
+
for (const row of preview.matched.slice(0, 8)) {
|
|
691
|
+
console.log(` - [${row.repoName}] ${row.reason}: ${row.preview}`);
|
|
692
|
+
}
|
|
693
|
+
if (preview.matched.length > 8) {
|
|
694
|
+
console.log(` … and ${preview.matched.length - 8} more`);
|
|
695
|
+
}
|
|
696
|
+
if (!flags.get("apply")) {
|
|
697
|
+
console.log("Dry run. Re-run with --apply to delete. Back up first: amem backup");
|
|
698
|
+
break;
|
|
699
|
+
}
|
|
700
|
+
const result = purgeNonFactClaims({ ...scope, dryRun: false });
|
|
701
|
+
console.log(`Deleted ${result.deleted} claim(s).`);
|
|
702
|
+
break;
|
|
703
|
+
}
|
|
649
704
|
const repo = resolveBinding(flags);
|
|
650
705
|
const days = Number(flagString(flags, "days") ?? "90");
|
|
651
706
|
const keep = flagString(flags, "merge");
|
|
@@ -691,6 +746,235 @@ async function main() {
|
|
|
691
746
|
console.log("Keep this file out of shared git if it contains personal notes.");
|
|
692
747
|
break;
|
|
693
748
|
}
|
|
749
|
+
case "task":
|
|
750
|
+
case "tasks": {
|
|
751
|
+
const sub = positional[1] || "list";
|
|
752
|
+
if (sub === "list") {
|
|
753
|
+
const isAll = Boolean(flags.get("all"));
|
|
754
|
+
const statusRaw = flagString(flags, "status");
|
|
755
|
+
const status = statusRaw ? (normalizeTaskStatus(statusRaw) ?? undefined) : undefined;
|
|
756
|
+
const includeDone = Boolean(flags.get("include-done") || flags.get("all") || status === "done");
|
|
757
|
+
let tasks;
|
|
758
|
+
if (isAll) {
|
|
759
|
+
tasks = listTasksAll({ status, includeDone });
|
|
760
|
+
}
|
|
761
|
+
else {
|
|
762
|
+
let repo;
|
|
763
|
+
try {
|
|
764
|
+
repo = resolveBinding(flags);
|
|
765
|
+
}
|
|
766
|
+
catch {
|
|
767
|
+
repo = getRepoByCwd() || ensurePersonalWorkspace();
|
|
768
|
+
}
|
|
769
|
+
tasks = listTasks(repo.id, { status, includeDone });
|
|
770
|
+
}
|
|
771
|
+
if (flags.get("json")) {
|
|
772
|
+
console.log(JSON.stringify({ tasks }, null, 2));
|
|
773
|
+
break;
|
|
774
|
+
}
|
|
775
|
+
if (tasks.length === 0) {
|
|
776
|
+
console.log("No tasks found.");
|
|
777
|
+
break;
|
|
778
|
+
}
|
|
779
|
+
const repoMap = new Map(listRepos().map((r) => [r.id, r.repo_name]));
|
|
780
|
+
for (const t of tasks) {
|
|
781
|
+
const repoLabel = isAll ? ` [${repoMap.get(t.repo_id) || "repo"}]` : "";
|
|
782
|
+
const body = t.body ? ` — ${t.body}` : "";
|
|
783
|
+
console.log(`[${t.status}] ${t.title}${body}${repoLabel} (${t.id})`);
|
|
784
|
+
}
|
|
785
|
+
break;
|
|
786
|
+
}
|
|
787
|
+
if (sub === "add") {
|
|
788
|
+
const title = positional.slice(2).join(" ") || flagString(flags, "title");
|
|
789
|
+
if (!title || !title.trim()) {
|
|
790
|
+
throw new Error("Usage: amem task add <title> [--body <notes>] [--status <status>] [--anchor <file>]");
|
|
791
|
+
}
|
|
792
|
+
let repo;
|
|
793
|
+
try {
|
|
794
|
+
repo = resolveBinding(flags);
|
|
795
|
+
}
|
|
796
|
+
catch {
|
|
797
|
+
repo = getRepoByCwd() || ensurePersonalWorkspace();
|
|
798
|
+
}
|
|
799
|
+
const body = flagString(flags, "body") || "";
|
|
800
|
+
const status = flagString(flags, "status") || "backlog";
|
|
801
|
+
const anchor = flagString(flags, "anchor");
|
|
802
|
+
const anchors = anchor ? [anchor] : undefined;
|
|
803
|
+
const task = insertTask({
|
|
804
|
+
repoId: repo.id,
|
|
805
|
+
title: title.trim(),
|
|
806
|
+
body,
|
|
807
|
+
status,
|
|
808
|
+
anchors,
|
|
809
|
+
source: "cli",
|
|
810
|
+
});
|
|
811
|
+
console.log(`Created task ${task.id} [${task.status}]: ${task.title}`);
|
|
812
|
+
break;
|
|
813
|
+
}
|
|
814
|
+
if (sub === "update") {
|
|
815
|
+
const id = positional[2];
|
|
816
|
+
if (!id)
|
|
817
|
+
throw new Error("Usage: amem task update <id> [--title <title>] [--body <notes>] [--status <status>]");
|
|
818
|
+
const found = findTaskAnyRepo(id);
|
|
819
|
+
if (!found)
|
|
820
|
+
throw new Error(`Task not found: ${id}`);
|
|
821
|
+
const title = flagString(flags, "title");
|
|
822
|
+
const body = flagString(flags, "body");
|
|
823
|
+
const status = flagString(flags, "status");
|
|
824
|
+
const anchor = flagString(flags, "anchor");
|
|
825
|
+
const anchors = anchor ? [anchor] : undefined;
|
|
826
|
+
const updated = updateTask(found.repo_id, id, {
|
|
827
|
+
title,
|
|
828
|
+
body,
|
|
829
|
+
status,
|
|
830
|
+
anchors,
|
|
831
|
+
});
|
|
832
|
+
if (!updated)
|
|
833
|
+
throw new Error(`Task not found: ${id}`);
|
|
834
|
+
console.log(`Updated task ${updated.id} [${updated.status}]: ${updated.title}`);
|
|
835
|
+
break;
|
|
836
|
+
}
|
|
837
|
+
if (sub === "complete" || sub === "done") {
|
|
838
|
+
const id = positional[2];
|
|
839
|
+
if (!id)
|
|
840
|
+
throw new Error("Usage: amem task complete <id>");
|
|
841
|
+
const found = findTaskAnyRepo(id);
|
|
842
|
+
if (!found)
|
|
843
|
+
throw new Error(`Task not found: ${id}`);
|
|
844
|
+
const completed = completeTask(found.repo_id, id);
|
|
845
|
+
if (!completed)
|
|
846
|
+
throw new Error(`Task not found: ${id}`);
|
|
847
|
+
console.log(`Completed task ${completed.id}: ${completed.title}`);
|
|
848
|
+
break;
|
|
849
|
+
}
|
|
850
|
+
if (sub === "rm" || sub === "delete") {
|
|
851
|
+
const id = positional[2];
|
|
852
|
+
if (!id)
|
|
853
|
+
throw new Error("Usage: amem task delete <id>");
|
|
854
|
+
const found = findTaskAnyRepo(id);
|
|
855
|
+
if (!found)
|
|
856
|
+
throw new Error(`Task not found: ${id}`);
|
|
857
|
+
const ok = deleteTask(found.repo_id, id);
|
|
858
|
+
if (!ok)
|
|
859
|
+
throw new Error(`Task not found: ${id}`);
|
|
860
|
+
console.log(`Deleted task ${id}`);
|
|
861
|
+
break;
|
|
862
|
+
}
|
|
863
|
+
throw new Error("Usage: amem task list|add|update|complete|delete");
|
|
864
|
+
}
|
|
865
|
+
case "skills": {
|
|
866
|
+
const sub = positional[1] || "list";
|
|
867
|
+
if (sub === "list") {
|
|
868
|
+
const skills = listIndexedSkills();
|
|
869
|
+
if (skills.length === 0) {
|
|
870
|
+
console.log(`No skills yet. Create one with: amem skills new <name>`);
|
|
871
|
+
console.log(`Skills live in ${skillsDir()}`);
|
|
872
|
+
break;
|
|
873
|
+
}
|
|
874
|
+
for (const s of skills) {
|
|
875
|
+
const flag = s.modified ? " (edited)" : "";
|
|
876
|
+
const used = s.uses > 0 ? ` · used ${s.uses}×` : "";
|
|
877
|
+
console.log(`${s.name}${flag}${used}`);
|
|
878
|
+
if (s.description)
|
|
879
|
+
console.log(` ${s.description}`);
|
|
880
|
+
}
|
|
881
|
+
console.log(`\n${skills.length} skill(s) in ${skillsDir()}`);
|
|
882
|
+
break;
|
|
883
|
+
}
|
|
884
|
+
if (sub === "show") {
|
|
885
|
+
const name = positional[2];
|
|
886
|
+
if (!name)
|
|
887
|
+
throw new Error("Usage: amem skills show <name>");
|
|
888
|
+
const body = readSkillBody(name);
|
|
889
|
+
if (body === null)
|
|
890
|
+
throw new Error(`Skill not found: ${name}`);
|
|
891
|
+
console.log(body);
|
|
892
|
+
break;
|
|
893
|
+
}
|
|
894
|
+
if (sub === "new") {
|
|
895
|
+
const name = positional[2];
|
|
896
|
+
if (!name)
|
|
897
|
+
throw new Error("Usage: amem skills new <name> [--desc <text>]");
|
|
898
|
+
const slug = slugifySkillName(name);
|
|
899
|
+
if (!isValidSkillName(slug))
|
|
900
|
+
throw new Error(`Invalid skill name: ${name}`);
|
|
901
|
+
if (findSkillOnDisk(slug))
|
|
902
|
+
throw new Error(`Skill already exists: ${slug}`);
|
|
903
|
+
const markdown = renderSkillMarkdown({
|
|
904
|
+
name: slug,
|
|
905
|
+
description: flagString(flags, "desc") || `Procedure: ${slug.replace(/[-_]+/g, " ")}`,
|
|
906
|
+
});
|
|
907
|
+
const written = writeSkill(slug, markdown);
|
|
908
|
+
syncSkillIndex();
|
|
909
|
+
console.log(`Created ${written.path}`);
|
|
910
|
+
console.log("Edit it, then agents will see it in their skill index.");
|
|
911
|
+
break;
|
|
912
|
+
}
|
|
913
|
+
if (sub === "rm") {
|
|
914
|
+
const name = positional[2];
|
|
915
|
+
if (!name)
|
|
916
|
+
throw new Error("Usage: amem skills rm <name>");
|
|
917
|
+
if (!deleteSkill(name))
|
|
918
|
+
throw new Error(`Skill not found: ${name}`);
|
|
919
|
+
syncSkillIndex();
|
|
920
|
+
console.log(`Deleted skill ${slugifySkillName(name)}`);
|
|
921
|
+
break;
|
|
922
|
+
}
|
|
923
|
+
if (sub === "sync") {
|
|
924
|
+
const skills = syncSkillIndex();
|
|
925
|
+
console.log(`Indexed ${skills.length} skill(s) from ${skillsDir()}`);
|
|
926
|
+
const edited = skills.filter((s) => s.modified);
|
|
927
|
+
if (edited.length > 0) {
|
|
928
|
+
console.log(`${edited.length} locally edited: ${edited.map((s) => s.name).join(", ")}`);
|
|
929
|
+
}
|
|
930
|
+
break;
|
|
931
|
+
}
|
|
932
|
+
if (sub === "drafts") {
|
|
933
|
+
const drafts = listSkillDrafts({ status: "pending", limit: 50 });
|
|
934
|
+
if (drafts.length === 0) {
|
|
935
|
+
console.log("No pending skill drafts.");
|
|
936
|
+
break;
|
|
937
|
+
}
|
|
938
|
+
for (const d of drafts) {
|
|
939
|
+
const label = d.kind === "revision" ? `revise ${d.target_skill}` : d.kind === "create" ? "staged" : "suggestion";
|
|
940
|
+
console.log(`${d.id} [${label}] ${d.title}`);
|
|
941
|
+
const why = JSON.parse(d.reasons || "[]");
|
|
942
|
+
if (Array.isArray(why) && why.length)
|
|
943
|
+
console.log(` why: ${why.join(", ")}`);
|
|
944
|
+
}
|
|
945
|
+
console.log(`\n${drafts.length} pending · approve with: amem skills approve <id>`);
|
|
946
|
+
break;
|
|
947
|
+
}
|
|
948
|
+
if (sub === "approve" || sub === "dismiss") {
|
|
949
|
+
const id = positional[2];
|
|
950
|
+
if (!id)
|
|
951
|
+
throw new Error(`Usage: amem skills ${sub} <draft-id>`);
|
|
952
|
+
const path = sub === "approve" ? "/api/skills/drafts/apply" : "/api/skills/drafts/dismiss";
|
|
953
|
+
const result = handleApi({
|
|
954
|
+
method: "POST",
|
|
955
|
+
pathname: path,
|
|
956
|
+
searchParams: new URLSearchParams(),
|
|
957
|
+
body: { id },
|
|
958
|
+
cwd: process.cwd(),
|
|
959
|
+
});
|
|
960
|
+
if (result.status >= 400) {
|
|
961
|
+
throw new Error(String(result.body?.error || "Failed"));
|
|
962
|
+
}
|
|
963
|
+
console.log(sub === "approve" ? `Applied ${id}` : `Dismissed ${id}`);
|
|
964
|
+
break;
|
|
965
|
+
}
|
|
966
|
+
if (sub === "import") {
|
|
967
|
+
const src = positional[2];
|
|
968
|
+
if (!src)
|
|
969
|
+
throw new Error("Usage: amem skills import <path-to-skill-dir>");
|
|
970
|
+
const result = importSkillFromPath(src, flagString(flags, "name"));
|
|
971
|
+
syncSkillIndex();
|
|
972
|
+
console.log(`Imported ${result.name} → ${result.path}`);
|
|
973
|
+
console.log("Review it before trusting: skills are instructions agents follow.");
|
|
974
|
+
break;
|
|
975
|
+
}
|
|
976
|
+
throw new Error("Usage: amem skills list|show|new|rm|sync|import");
|
|
977
|
+
}
|
|
694
978
|
case "it-pack": {
|
|
695
979
|
const out = resolve(flagString(flags, "out") || join(amemHome(), "it-pack"));
|
|
696
980
|
const result = writeItPack(out);
|
|
@@ -919,7 +1203,8 @@ async function main() {
|
|
|
919
1203
|
break;
|
|
920
1204
|
}
|
|
921
1205
|
if (sub === "activate") {
|
|
922
|
-
|
|
1206
|
+
console.log("amem is 100% free and open — all features are already unlocked.");
|
|
1207
|
+
break;
|
|
923
1208
|
}
|
|
924
1209
|
if (sub === "clear") {
|
|
925
1210
|
clearLicense();
|
package/dist/context.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { type ClaimRow, type ComponentRow, type ConversationNoteRow, type FlowRow, type UsageEventRow } from "./db.js";
|
|
1
|
+
import { type AgentTaskRow, type ClaimRow, type ComponentRow, type ConversationNoteRow, type FlowRow, type UsageEventRow, type SkillDraftRow } from "./db.js";
|
|
2
2
|
import { type ClaimFreshness } from "./freshness.js";
|
|
3
|
+
import { type RankedSkill } from "./skills.js";
|
|
3
4
|
export type RankedClaim = ClaimRow & {
|
|
4
5
|
score: number;
|
|
5
6
|
freshness: ClaimFreshness;
|
|
@@ -14,12 +15,20 @@ export type ContextPacket = {
|
|
|
14
15
|
notes: Array<ConversationNoteRow & {
|
|
15
16
|
score: number;
|
|
16
17
|
}>;
|
|
18
|
+
/** Deferred Kanban tasks (non-done), compact for agents. */
|
|
19
|
+
tasks: AgentTaskRow[];
|
|
20
|
+
/** Index-only skill matches; bodies load on demand via `amem_skill_view`. */
|
|
21
|
+
skills: RankedSkill[];
|
|
22
|
+
/** At most one pending "worth writing up" nudge from a previous session. */
|
|
23
|
+
skillDrafts: SkillDraftRow[];
|
|
17
24
|
};
|
|
18
25
|
export type BuildContextOptions = {
|
|
19
26
|
limit?: number;
|
|
20
27
|
rootPath?: string;
|
|
21
28
|
/** Include cross-repo personal prefs (default true). */
|
|
22
29
|
includePersonal?: boolean;
|
|
30
|
+
/** Include the ranked skill index (default true). */
|
|
31
|
+
includeSkills?: boolean;
|
|
23
32
|
};
|
|
24
33
|
export declare function buildContext(repoId: string, query: string, limitOrOpts?: number | BuildContextOptions): ContextPacket;
|
|
25
34
|
export type ShowdownClaim = {
|
package/dist/context.js
CHANGED
|
@@ -1,10 +1,25 @@
|
|
|
1
|
-
import { getRepoByName, listClaims, listComponents, listConversationNotes, listEdges, listFlows, openDb, } from "./db.js";
|
|
1
|
+
import { getRepoByName, listClaims, listComponents, listConversationNotes, listEdges, listFlows, listOpenTasksForContext, listTasks, openDb, listSkillDrafts, } from "./db.js";
|
|
2
2
|
import { assessClaimFreshness, freshnessScoreMultiplier, } from "./freshness.js";
|
|
3
3
|
import { kindRankBoost } from "./kinds.js";
|
|
4
4
|
import { ftsBoostFromBm25, keywordScoreClaim, searchClaimsFts, tokenize, } from "./search.js";
|
|
5
5
|
import { embedBoostFromScore, searchClaimsEmbed, searchClaimsEmbedLive } from "./embed.js";
|
|
6
6
|
import { FEATURE_LOCAL_EMBED, hasFeature } from "./license.js";
|
|
7
7
|
import { PERSONAL_SLUG } from "./personal.js";
|
|
8
|
+
import { listIndexedSkills, rankSkills } from "./skills.js";
|
|
9
|
+
import { loadPolicy } from "./policy.js";
|
|
10
|
+
/** Reasons are stored as JSON; a malformed row must not break the whole packet. */
|
|
11
|
+
function safeReasons(raw) {
|
|
12
|
+
try {
|
|
13
|
+
const list = JSON.parse(raw);
|
|
14
|
+
if (Array.isArray(list) && list.length > 0) {
|
|
15
|
+
return ` (${list.filter((r) => typeof r === "string").join(", ")})`;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
/* fall through */
|
|
20
|
+
}
|
|
21
|
+
return "";
|
|
22
|
+
}
|
|
8
23
|
function scoreNote(note, queryTokens) {
|
|
9
24
|
if (queryTokens.length === 0)
|
|
10
25
|
return 0;
|
|
@@ -16,6 +31,17 @@ function scoreNote(note, queryTokens) {
|
|
|
16
31
|
}
|
|
17
32
|
return score;
|
|
18
33
|
}
|
|
34
|
+
function scoreTask(task, queryTokens) {
|
|
35
|
+
if (queryTokens.length === 0)
|
|
36
|
+
return 0;
|
|
37
|
+
const hay = `${task.title} ${task.body} ${task.anchors || ""}`.toLowerCase();
|
|
38
|
+
let score = 0;
|
|
39
|
+
for (const token of queryTokens) {
|
|
40
|
+
if (hay.includes(token))
|
|
41
|
+
score += token.length > 4 ? 3 : 2;
|
|
42
|
+
}
|
|
43
|
+
return score;
|
|
44
|
+
}
|
|
19
45
|
function rankClaims(claims, query, queryTokens, opts) {
|
|
20
46
|
return claims.map((c) => {
|
|
21
47
|
const freshness = assessClaimFreshness(opts.rootPath, c);
|
|
@@ -152,7 +178,44 @@ export function buildContext(repoId, query, limitOrOpts = 12) {
|
|
|
152
178
|
const notes = notesRaw.length > 0
|
|
153
179
|
? notesRaw
|
|
154
180
|
: listConversationNotes(repoId, 5).map((n) => ({ ...n, score: 0 }));
|
|
155
|
-
|
|
181
|
+
const openTasks = listOpenTasksForContext(repoId, 8);
|
|
182
|
+
const doneTasks = queryTokens.length > 0
|
|
183
|
+
? listTasks(repoId, { status: "done", includeDone: true, limit: 20 })
|
|
184
|
+
.map((t) => ({ ...t, score: scoreTask(t, queryTokens) }))
|
|
185
|
+
.filter((t) => t.score > 0)
|
|
186
|
+
.sort((a, b) => b.score - a.score || b.updated_at.localeCompare(a.updated_at))
|
|
187
|
+
.slice(0, 4)
|
|
188
|
+
: [];
|
|
189
|
+
const tasks = [...openTasks, ...doneTasks];
|
|
190
|
+
const skillsOn = opts.includeSkills !== false && loadPolicy().policy.skills_enabled;
|
|
191
|
+
const skills = skillsOn ? rankSkillsForQuery(query) : [];
|
|
192
|
+
const skillDrafts = skillsOn ? pendingSkillNudges(repoId) : [];
|
|
193
|
+
return { query, claims: selected, flows, components, notes, tasks, skills, skillDrafts };
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* amem has no model, so it cannot write a skill itself. The nudge is how a session-end
|
|
197
|
+
* detection reaches an agent that can. One at a time — this rides in every packet.
|
|
198
|
+
*/
|
|
199
|
+
function pendingSkillNudges(repoId) {
|
|
200
|
+
try {
|
|
201
|
+
return listSkillDrafts({ status: "pending", repoId, limit: 1 });
|
|
202
|
+
}
|
|
203
|
+
catch {
|
|
204
|
+
return [];
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Progressive disclosure, level 0. Only names and descriptions reach the packet; the
|
|
209
|
+
* agent pulls a body with `amem_skill_view` if it decides the procedure applies. A skills
|
|
210
|
+
* directory that cannot be read must never break context, so this stays best-effort.
|
|
211
|
+
*/
|
|
212
|
+
function rankSkillsForQuery(query) {
|
|
213
|
+
try {
|
|
214
|
+
return rankSkills(listIndexedSkills(), query, 3);
|
|
215
|
+
}
|
|
216
|
+
catch {
|
|
217
|
+
return [];
|
|
218
|
+
}
|
|
156
219
|
}
|
|
157
220
|
function toShowdownClaims(ranked) {
|
|
158
221
|
return ranked.map((c) => ({
|
|
@@ -258,7 +321,11 @@ export function decorateUsageEvents(events) {
|
|
|
258
321
|
}
|
|
259
322
|
export function renderContextMarkdown(packet) {
|
|
260
323
|
const lines = ["# Agent Memory Context", "", `Query: ${packet.query}`, ""];
|
|
261
|
-
if (packet.claims.length === 0 &&
|
|
324
|
+
if (packet.claims.length === 0 &&
|
|
325
|
+
packet.notes.length === 0 &&
|
|
326
|
+
(packet.tasks?.length ?? 0) === 0 &&
|
|
327
|
+
(packet.skills?.length ?? 0) === 0 &&
|
|
328
|
+
(packet.skillDrafts?.length ?? 0) === 0) {
|
|
262
329
|
lines.push("No claims stored for this repository yet.", "");
|
|
263
330
|
lines.push("Seed memory with the `amem-bootstrap` skill or `amem propose apply <file>`.");
|
|
264
331
|
return lines.join("\n");
|
|
@@ -266,6 +333,41 @@ export function renderContextMarkdown(packet) {
|
|
|
266
333
|
if (packet.claims.length === 0) {
|
|
267
334
|
lines.push("No durable claims yet — using recent conversation memory.", "");
|
|
268
335
|
}
|
|
336
|
+
if (packet.tasks && packet.tasks.length > 0) {
|
|
337
|
+
const hasDone = packet.tasks.some((t) => t.status === "done");
|
|
338
|
+
const hasOpen = packet.tasks.some((t) => t.status !== "done");
|
|
339
|
+
const header = hasDone && !hasOpen ? "## Completed tasks" : (hasDone ? "## Tasks (open & completed)" : "## Open tasks");
|
|
340
|
+
lines.push(header, "");
|
|
341
|
+
lines.push("_Project Kanban tasks. Use `amem_task_*` to add/update/complete._", "");
|
|
342
|
+
for (const task of packet.tasks) {
|
|
343
|
+
const body = task.body ? ` — ${task.body.replace(/\s+/g, " ").slice(0, 120)}` : "";
|
|
344
|
+
lines.push(`- **[${task.status}]** ${task.title}${body} (\`${task.id}\`)`);
|
|
345
|
+
}
|
|
346
|
+
lines.push("");
|
|
347
|
+
}
|
|
348
|
+
if (packet.skills && packet.skills.length > 0) {
|
|
349
|
+
lines.push("## Relevant skills", "");
|
|
350
|
+
lines.push("_Procedures that may apply. Only the index is shown — call `amem_skill_view` with the name to load one before following it._", "");
|
|
351
|
+
for (const skill of packet.skills) {
|
|
352
|
+
const desc = skill.description
|
|
353
|
+
? ` — ${skill.description.replace(/\s+/g, " ").slice(0, 140)}`
|
|
354
|
+
: "";
|
|
355
|
+
lines.push(`- **${skill.name}**${desc}`);
|
|
356
|
+
}
|
|
357
|
+
lines.push("");
|
|
358
|
+
}
|
|
359
|
+
const nudge = packet.skillDrafts?.[0];
|
|
360
|
+
if (nudge) {
|
|
361
|
+
const why = safeReasons(nudge.reasons);
|
|
362
|
+
if (nudge.kind === "revision" && nudge.target_skill) {
|
|
363
|
+
lines.push("## Skill worth revising", "");
|
|
364
|
+
lines.push(`_You followed **${nudge.target_skill}** in a recent session and the work still went sideways${why}. If you learned what it was missing, update it with \`amem_skill_save\`._`, "");
|
|
365
|
+
}
|
|
366
|
+
else {
|
|
367
|
+
lines.push("## Worth saving as a skill", "");
|
|
368
|
+
lines.push(`_A recent session looked like a repeatable procedure: "${nudge.title}"${why}. If you can write it up, save it with \`amem_skill_save\` so the next agent does not rediscover it._`, "");
|
|
369
|
+
}
|
|
370
|
+
}
|
|
269
371
|
const staleCount = packet.claims.filter((c) => c.freshness.status === "stale").length;
|
|
270
372
|
if (staleCount > 0) {
|
|
271
373
|
lines.push(`_${staleCount} claim(s) marked stale — anchored files changed after the claim was written. Verify before trusting._`, "");
|