@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/db.js
CHANGED
|
@@ -6,6 +6,19 @@ import { detectRepoIdentity, newId, parseWorkspaceSlug, slugifyWorkspace } from
|
|
|
6
6
|
import { ensureClaimsFts, reindexAllClaimsFts, reindexRepoClaimsFts, removeClaimFts, upsertClaimFts } from "./search.js";
|
|
7
7
|
import { ensureClaimsEmbed, reindexRepoEmbeds, removeClaimEmbed, upsertClaimEmbed, } from "./embed.js";
|
|
8
8
|
import { isDbEncryptedAtRest, resolvePassphrase, unlockDatabase } from "./crypto.js";
|
|
9
|
+
export const AGENT_TASK_STATUSES = [
|
|
10
|
+
"backlog",
|
|
11
|
+
"next",
|
|
12
|
+
"doing",
|
|
13
|
+
"blocked",
|
|
14
|
+
"done",
|
|
15
|
+
];
|
|
16
|
+
export function normalizeTaskStatus(raw) {
|
|
17
|
+
const s = String(raw || "")
|
|
18
|
+
.trim()
|
|
19
|
+
.toLowerCase();
|
|
20
|
+
return AGENT_TASK_STATUSES.includes(s) ? s : null;
|
|
21
|
+
}
|
|
9
22
|
const SCHEMA = `
|
|
10
23
|
CREATE TABLE IF NOT EXISTS repos (
|
|
11
24
|
id TEXT PRIMARY KEY,
|
|
@@ -116,6 +129,61 @@ CREATE TABLE IF NOT EXISTS proposal_drafts (
|
|
|
116
129
|
updated_at TEXT NOT NULL
|
|
117
130
|
);
|
|
118
131
|
|
|
132
|
+
CREATE TABLE IF NOT EXISTS agent_tasks (
|
|
133
|
+
repo_id TEXT NOT NULL REFERENCES repos(id) ON DELETE CASCADE,
|
|
134
|
+
id TEXT NOT NULL,
|
|
135
|
+
title TEXT NOT NULL,
|
|
136
|
+
body TEXT NOT NULL DEFAULT '',
|
|
137
|
+
status TEXT NOT NULL DEFAULT 'backlog',
|
|
138
|
+
anchors TEXT NOT NULL DEFAULT '[]',
|
|
139
|
+
source TEXT NOT NULL DEFAULT 'ui',
|
|
140
|
+
created_at TEXT NOT NULL,
|
|
141
|
+
updated_at TEXT NOT NULL,
|
|
142
|
+
completed_at TEXT,
|
|
143
|
+
PRIMARY KEY(repo_id, id)
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
CREATE TABLE IF NOT EXISTS skills (
|
|
147
|
+
name TEXT PRIMARY KEY,
|
|
148
|
+
path TEXT NOT NULL,
|
|
149
|
+
description TEXT NOT NULL DEFAULT '',
|
|
150
|
+
version TEXT,
|
|
151
|
+
tags TEXT NOT NULL DEFAULT '[]',
|
|
152
|
+
repo_id TEXT REFERENCES repos(id) ON DELETE SET NULL,
|
|
153
|
+
content_hash TEXT NOT NULL,
|
|
154
|
+
origin_hash TEXT,
|
|
155
|
+
source TEXT NOT NULL DEFAULT 'local',
|
|
156
|
+
uses INTEGER NOT NULL DEFAULT 0,
|
|
157
|
+
last_used_at TEXT,
|
|
158
|
+
created_at TEXT NOT NULL,
|
|
159
|
+
updated_at TEXT NOT NULL
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
CREATE TABLE IF NOT EXISTS skill_drafts (
|
|
163
|
+
id TEXT PRIMARY KEY,
|
|
164
|
+
repo_id TEXT REFERENCES repos(id) ON DELETE CASCADE,
|
|
165
|
+
name TEXT,
|
|
166
|
+
title TEXT NOT NULL,
|
|
167
|
+
summary TEXT NOT NULL DEFAULT '',
|
|
168
|
+
content TEXT,
|
|
169
|
+
kind TEXT NOT NULL DEFAULT 'suggestion',
|
|
170
|
+
target_skill TEXT,
|
|
171
|
+
status TEXT NOT NULL DEFAULT 'pending',
|
|
172
|
+
source TEXT NOT NULL DEFAULT 'session-end',
|
|
173
|
+
session_id TEXT,
|
|
174
|
+
reasons TEXT NOT NULL DEFAULT '[]',
|
|
175
|
+
created_at TEXT NOT NULL,
|
|
176
|
+
updated_at TEXT NOT NULL
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
CREATE TABLE IF NOT EXISTS skill_uses (
|
|
180
|
+
id TEXT PRIMARY KEY,
|
|
181
|
+
skill_name TEXT NOT NULL,
|
|
182
|
+
repo_id TEXT,
|
|
183
|
+
session_id TEXT,
|
|
184
|
+
created_at TEXT NOT NULL
|
|
185
|
+
);
|
|
186
|
+
|
|
119
187
|
CREATE INDEX IF NOT EXISTS claims_repo_idx ON claims(repo_id);
|
|
120
188
|
CREATE INDEX IF NOT EXISTS edges_repo_idx ON edges(repo_id);
|
|
121
189
|
CREATE INDEX IF NOT EXISTS components_repo_idx ON components(repo_id);
|
|
@@ -127,6 +195,11 @@ CREATE INDEX IF NOT EXISTS conversation_notes_repo_idx ON conversation_notes(rep
|
|
|
127
195
|
CREATE INDEX IF NOT EXISTS conversation_notes_created_idx ON conversation_notes(created_at);
|
|
128
196
|
CREATE INDEX IF NOT EXISTS proposal_drafts_repo_idx ON proposal_drafts(repo_id);
|
|
129
197
|
CREATE INDEX IF NOT EXISTS proposal_drafts_status_idx ON proposal_drafts(status);
|
|
198
|
+
CREATE INDEX IF NOT EXISTS agent_tasks_repo_idx ON agent_tasks(repo_id);
|
|
199
|
+
CREATE INDEX IF NOT EXISTS agent_tasks_status_idx ON agent_tasks(repo_id, status);
|
|
200
|
+
CREATE INDEX IF NOT EXISTS skills_repo_idx ON skills(repo_id);
|
|
201
|
+
CREATE INDEX IF NOT EXISTS skill_drafts_status_idx ON skill_drafts(status);
|
|
202
|
+
CREATE INDEX IF NOT EXISTS skill_uses_session_idx ON skill_uses(session_id);
|
|
130
203
|
`;
|
|
131
204
|
let cached = null;
|
|
132
205
|
export function openDb() {
|
|
@@ -152,6 +225,7 @@ export function openDb() {
|
|
|
152
225
|
ensureClaimsFts(db);
|
|
153
226
|
migrateClaimsFtsBootstrap(db);
|
|
154
227
|
ensureProposalDrafts(db);
|
|
228
|
+
ensureAgentTasks(db);
|
|
155
229
|
ensureClaimsEmbed(db);
|
|
156
230
|
cached = db;
|
|
157
231
|
return db;
|
|
@@ -198,6 +272,25 @@ function ensureProposalDrafts(db) {
|
|
|
198
272
|
CREATE INDEX IF NOT EXISTS proposal_drafts_status_idx ON proposal_drafts(status);
|
|
199
273
|
`);
|
|
200
274
|
}
|
|
275
|
+
function ensureAgentTasks(db) {
|
|
276
|
+
db.exec(`
|
|
277
|
+
CREATE TABLE IF NOT EXISTS agent_tasks (
|
|
278
|
+
repo_id TEXT NOT NULL REFERENCES repos(id) ON DELETE CASCADE,
|
|
279
|
+
id TEXT NOT NULL,
|
|
280
|
+
title TEXT NOT NULL,
|
|
281
|
+
body TEXT NOT NULL DEFAULT '',
|
|
282
|
+
status TEXT NOT NULL DEFAULT 'backlog',
|
|
283
|
+
anchors TEXT NOT NULL DEFAULT '[]',
|
|
284
|
+
source TEXT NOT NULL DEFAULT 'ui',
|
|
285
|
+
created_at TEXT NOT NULL,
|
|
286
|
+
updated_at TEXT NOT NULL,
|
|
287
|
+
completed_at TEXT,
|
|
288
|
+
PRIMARY KEY(repo_id, id)
|
|
289
|
+
);
|
|
290
|
+
CREATE INDEX IF NOT EXISTS agent_tasks_repo_idx ON agent_tasks(repo_id);
|
|
291
|
+
CREATE INDEX IF NOT EXISTS agent_tasks_status_idx ON agent_tasks(repo_id, status);
|
|
292
|
+
`);
|
|
293
|
+
}
|
|
201
294
|
/** One-shot FTS rebuild flag so upgrades populate the index. */
|
|
202
295
|
function migrateClaimsFtsBootstrap(db) {
|
|
203
296
|
db.exec(`
|
|
@@ -663,4 +756,309 @@ export function setProposalDraftStatus(id, status) {
|
|
|
663
756
|
.run(status, ts, id);
|
|
664
757
|
return getProposalDraft(id);
|
|
665
758
|
}
|
|
759
|
+
function encodeTaskAnchors(anchors) {
|
|
760
|
+
const list = (anchors ?? [])
|
|
761
|
+
.filter((a) => typeof a === "string" && Boolean(a.trim()))
|
|
762
|
+
.map((a) => a.trim().slice(0, 200))
|
|
763
|
+
.slice(0, 20);
|
|
764
|
+
return JSON.stringify(list);
|
|
765
|
+
}
|
|
766
|
+
const TASK_STATUS_ORDER = {
|
|
767
|
+
doing: 0,
|
|
768
|
+
next: 1,
|
|
769
|
+
blocked: 2,
|
|
770
|
+
backlog: 3,
|
|
771
|
+
done: 4,
|
|
772
|
+
};
|
|
773
|
+
export function listSkillRows() {
|
|
774
|
+
return openDb().prepare(`SELECT * FROM skills ORDER BY name`).all();
|
|
775
|
+
}
|
|
776
|
+
export function getSkillRow(name) {
|
|
777
|
+
return (openDb().prepare(`SELECT * FROM skills WHERE name = ?`).get(name) ??
|
|
778
|
+
null);
|
|
779
|
+
}
|
|
780
|
+
/**
|
|
781
|
+
* Index one skill found on disk. Disk is the source of truth, so this only ever refreshes
|
|
782
|
+
* derived columns — it must not clobber the repo tag or usage counters a user built up.
|
|
783
|
+
*/
|
|
784
|
+
export function upsertSkillRow(input) {
|
|
785
|
+
const ts = nowIso();
|
|
786
|
+
const existing = getSkillRow(input.name);
|
|
787
|
+
const tags = JSON.stringify(input.tags ?? []);
|
|
788
|
+
if (existing) {
|
|
789
|
+
openDb()
|
|
790
|
+
.prepare(`UPDATE skills SET path = ?, description = ?, version = ?, tags = ?,
|
|
791
|
+
content_hash = ?, source = ?, updated_at = ? WHERE name = ?`)
|
|
792
|
+
.run(input.path, input.description ?? "", input.version ?? null, tags, input.contentHash, input.source ?? existing.source, ts, input.name);
|
|
793
|
+
if (input.repoId !== undefined)
|
|
794
|
+
setSkillRepo(input.name, input.repoId);
|
|
795
|
+
return getSkillRow(input.name);
|
|
796
|
+
}
|
|
797
|
+
openDb()
|
|
798
|
+
.prepare(`INSERT INTO skills (name, path, description, version, tags, repo_id, content_hash,
|
|
799
|
+
origin_hash, source, uses, last_used_at, created_at, updated_at)
|
|
800
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 0, NULL, ?, ?)`)
|
|
801
|
+
.run(input.name, input.path, input.description ?? "", input.version ?? null, tags, input.repoId ?? null, input.contentHash, input.contentHash, input.source ?? "local", ts, ts);
|
|
802
|
+
return getSkillRow(input.name);
|
|
803
|
+
}
|
|
804
|
+
/** Optional memory tag. Skills are a global library; the tag is only a filter hint. */
|
|
805
|
+
export function setSkillRepo(name, repoId) {
|
|
806
|
+
openDb()
|
|
807
|
+
.prepare(`UPDATE skills SET repo_id = ?, updated_at = ? WHERE name = ?`)
|
|
808
|
+
.run(repoId, nowIso(), name);
|
|
809
|
+
}
|
|
810
|
+
export function deleteSkillRow(name) {
|
|
811
|
+
const info = openDb().prepare(`DELETE FROM skills WHERE name = ?`).run(name);
|
|
812
|
+
return Number(info.changes || 0) > 0;
|
|
813
|
+
}
|
|
814
|
+
/** Drop index rows whose skill is no longer on disk. */
|
|
815
|
+
export function pruneSkillRows(keepNames) {
|
|
816
|
+
const keep = new Set(keepNames);
|
|
817
|
+
let removed = 0;
|
|
818
|
+
for (const row of listSkillRows()) {
|
|
819
|
+
if (!keep.has(row.name)) {
|
|
820
|
+
deleteSkillRow(row.name);
|
|
821
|
+
removed += 1;
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
return removed;
|
|
825
|
+
}
|
|
826
|
+
export function recordSkillUse(name, ctx = {}) {
|
|
827
|
+
openDb()
|
|
828
|
+
.prepare(`UPDATE skills SET uses = uses + 1, last_used_at = ? WHERE name = ?`)
|
|
829
|
+
.run(nowIso(), name);
|
|
830
|
+
// Per-session trail so session-end can tell which procedures were actually followed.
|
|
831
|
+
openDb()
|
|
832
|
+
.prepare(`INSERT INTO skill_uses (id, skill_name, repo_id, session_id, created_at) VALUES (?, ?, ?, ?, ?)`)
|
|
833
|
+
.run(newId("skilluse"), name, ctx.repoId ?? null, ctx.sessionId ?? null, nowIso());
|
|
834
|
+
}
|
|
835
|
+
/**
|
|
836
|
+
* Skills used recently in a repo. MCP clients do not always carry a session id, so
|
|
837
|
+
* recency in the same memory is the fallback for correlating a view to a session.
|
|
838
|
+
*/
|
|
839
|
+
export function listRecentSkillUses(repoId, minutes = 120, limit = 5) {
|
|
840
|
+
const since = new Date(Date.now() - minutes * 60_000).toISOString();
|
|
841
|
+
const rows = openDb()
|
|
842
|
+
.prepare(`SELECT skill_name, MAX(created_at) AS last FROM skill_uses
|
|
843
|
+
WHERE repo_id = ? AND created_at >= ? GROUP BY skill_name ORDER BY last DESC LIMIT ?`)
|
|
844
|
+
.all(repoId, since, limit);
|
|
845
|
+
return rows.map((r) => r.skill_name);
|
|
846
|
+
}
|
|
847
|
+
export function listSkillsUsedInSession(sessionId, limit = 5) {
|
|
848
|
+
if (!sessionId)
|
|
849
|
+
return [];
|
|
850
|
+
const rows = openDb()
|
|
851
|
+
.prepare(`SELECT skill_name, MAX(created_at) AS last FROM skill_uses
|
|
852
|
+
WHERE session_id = ? GROUP BY skill_name ORDER BY last DESC LIMIT ?`)
|
|
853
|
+
.all(sessionId, limit);
|
|
854
|
+
return rows.map((r) => r.skill_name);
|
|
855
|
+
}
|
|
856
|
+
export function insertSkillDraft(input) {
|
|
857
|
+
const id = newId("skilldraft");
|
|
858
|
+
const ts = nowIso();
|
|
859
|
+
openDb()
|
|
860
|
+
.prepare(`INSERT INTO skill_drafts (id, repo_id, name, title, summary, content, kind, target_skill,
|
|
861
|
+
status, source, session_id, reasons, created_at, updated_at)
|
|
862
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'pending', ?, ?, ?, ?, ?)`)
|
|
863
|
+
.run(id, input.repoId ?? null, input.name ?? null, input.title, input.summary ?? "", input.content ?? null, input.kind ?? "suggestion", input.targetSkill ?? null, input.source ?? "session-end", input.sessionId ?? null, JSON.stringify(input.reasons ?? []), ts, ts);
|
|
864
|
+
return getSkillDraft(id);
|
|
865
|
+
}
|
|
866
|
+
export function getSkillDraft(id) {
|
|
867
|
+
return (openDb().prepare(`SELECT * FROM skill_drafts WHERE id = ?`).get(id) ?? null);
|
|
868
|
+
}
|
|
869
|
+
export function listSkillDrafts(opts = {}) {
|
|
870
|
+
const limit = Math.min(200, Math.max(1, opts.limit ?? 50));
|
|
871
|
+
const where = [];
|
|
872
|
+
const args = [];
|
|
873
|
+
if (opts.status) {
|
|
874
|
+
where.push("status = ?");
|
|
875
|
+
args.push(opts.status);
|
|
876
|
+
}
|
|
877
|
+
if (opts.repoId) {
|
|
878
|
+
where.push("repo_id = ?");
|
|
879
|
+
args.push(opts.repoId);
|
|
880
|
+
}
|
|
881
|
+
const sql = `SELECT * FROM skill_drafts ${where.length ? `WHERE ${where.join(" AND ")}` : ""} ORDER BY created_at DESC LIMIT ?`;
|
|
882
|
+
return openDb()
|
|
883
|
+
.prepare(sql)
|
|
884
|
+
.all(...args, limit);
|
|
885
|
+
}
|
|
886
|
+
export function setSkillDraftStatus(id, status) {
|
|
887
|
+
openDb()
|
|
888
|
+
.prepare(`UPDATE skill_drafts SET status = ?, updated_at = ? WHERE id = ?`)
|
|
889
|
+
.run(status, nowIso(), id);
|
|
890
|
+
return getSkillDraft(id);
|
|
891
|
+
}
|
|
892
|
+
export function skillDraftExists(source) {
|
|
893
|
+
const row = openDb()
|
|
894
|
+
.prepare(`SELECT 1 AS hit FROM skill_drafts WHERE source = ? LIMIT 1`)
|
|
895
|
+
.get(source);
|
|
896
|
+
return Boolean(row);
|
|
897
|
+
}
|
|
898
|
+
export function getTask(repoId, id) {
|
|
899
|
+
return (openDb()
|
|
900
|
+
.prepare(`SELECT * FROM agent_tasks WHERE repo_id = ? AND id = ?`)
|
|
901
|
+
.get(repoId, id) ?? null);
|
|
902
|
+
}
|
|
903
|
+
export function listTasks(repoId, opts = {}) {
|
|
904
|
+
const limit = Math.min(200, Math.max(1, opts.limit ?? 100));
|
|
905
|
+
let rows;
|
|
906
|
+
if (opts.status) {
|
|
907
|
+
rows = openDb()
|
|
908
|
+
.prepare(`SELECT * FROM agent_tasks WHERE repo_id = ? AND status = ?
|
|
909
|
+
ORDER BY updated_at DESC LIMIT ?`)
|
|
910
|
+
.all(repoId, opts.status, limit);
|
|
911
|
+
}
|
|
912
|
+
else if (opts.includeDone) {
|
|
913
|
+
rows = openDb()
|
|
914
|
+
.prepare(`SELECT * FROM agent_tasks WHERE repo_id = ?
|
|
915
|
+
ORDER BY updated_at DESC LIMIT ?`)
|
|
916
|
+
.all(repoId, limit);
|
|
917
|
+
}
|
|
918
|
+
else {
|
|
919
|
+
rows = openDb()
|
|
920
|
+
.prepare(`SELECT * FROM agent_tasks WHERE repo_id = ? AND status != 'done'
|
|
921
|
+
ORDER BY updated_at DESC LIMIT ?`)
|
|
922
|
+
.all(repoId, limit);
|
|
923
|
+
}
|
|
924
|
+
return rows.sort((a, b) => (TASK_STATUS_ORDER[a.status] ?? 9) - (TASK_STATUS_ORDER[b.status] ?? 9) ||
|
|
925
|
+
b.updated_at.localeCompare(a.updated_at));
|
|
926
|
+
}
|
|
927
|
+
/**
|
|
928
|
+
* Tasks across every memory. The UI's "All memory" scope needs this because agents file
|
|
929
|
+
* tasks against whatever repo they were working in, which is often not the repo the UI
|
|
930
|
+
* was launched from — without it those tasks are invisible.
|
|
931
|
+
*/
|
|
932
|
+
export function listTasksAll(opts = {}) {
|
|
933
|
+
const limit = Math.min(500, Math.max(1, opts.limit ?? 200));
|
|
934
|
+
let rows;
|
|
935
|
+
if (opts.status) {
|
|
936
|
+
rows = openDb()
|
|
937
|
+
.prepare(`SELECT * FROM agent_tasks WHERE status = ? ORDER BY updated_at DESC LIMIT ?`)
|
|
938
|
+
.all(opts.status, limit);
|
|
939
|
+
}
|
|
940
|
+
else if (opts.includeDone) {
|
|
941
|
+
rows = openDb()
|
|
942
|
+
.prepare(`SELECT * FROM agent_tasks ORDER BY updated_at DESC LIMIT ?`)
|
|
943
|
+
.all(limit);
|
|
944
|
+
}
|
|
945
|
+
else {
|
|
946
|
+
rows = openDb()
|
|
947
|
+
.prepare(`SELECT * FROM agent_tasks WHERE status != 'done' ORDER BY updated_at DESC LIMIT ?`)
|
|
948
|
+
.all(limit);
|
|
949
|
+
}
|
|
950
|
+
return rows.sort((a, b) => (TASK_STATUS_ORDER[a.status] ?? 9) - (TASK_STATUS_ORDER[b.status] ?? 9) ||
|
|
951
|
+
b.updated_at.localeCompare(a.updated_at));
|
|
952
|
+
}
|
|
953
|
+
export function countTasksAll(opts = {}) {
|
|
954
|
+
if (opts.status) {
|
|
955
|
+
const row = openDb()
|
|
956
|
+
.prepare(`SELECT COUNT(*) AS n FROM agent_tasks WHERE status = ?`)
|
|
957
|
+
.get(opts.status);
|
|
958
|
+
return Number(row?.n || 0);
|
|
959
|
+
}
|
|
960
|
+
if (opts.openOnly) {
|
|
961
|
+
const row = openDb()
|
|
962
|
+
.prepare(`SELECT COUNT(*) AS n FROM agent_tasks WHERE status != 'done'`)
|
|
963
|
+
.get();
|
|
964
|
+
return Number(row?.n || 0);
|
|
965
|
+
}
|
|
966
|
+
const row = openDb().prepare(`SELECT COUNT(*) AS n FROM agent_tasks`).get();
|
|
967
|
+
return Number(row?.n || 0);
|
|
968
|
+
}
|
|
969
|
+
/** Find a task without knowing its repo, so all-memory edits can resolve their owner. */
|
|
970
|
+
export function findTaskAnyRepo(id) {
|
|
971
|
+
return (openDb().prepare(`SELECT * FROM agent_tasks WHERE id = ?`).get(id) ?? null);
|
|
972
|
+
}
|
|
973
|
+
/** Open tasks for context injection — prefer doing/next/blocked, then backlog. */
|
|
974
|
+
export function listOpenTasksForContext(repoId, limit = 8) {
|
|
975
|
+
const rows = openDb()
|
|
976
|
+
.prepare(`SELECT * FROM agent_tasks WHERE repo_id = ? AND status != 'done'
|
|
977
|
+
ORDER BY updated_at DESC LIMIT ?`)
|
|
978
|
+
.all(repoId, Math.max(limit * 3, 24));
|
|
979
|
+
return rows
|
|
980
|
+
.sort((a, b) => (TASK_STATUS_ORDER[a.status] ?? 9) - (TASK_STATUS_ORDER[b.status] ?? 9) ||
|
|
981
|
+
b.updated_at.localeCompare(a.updated_at))
|
|
982
|
+
.slice(0, limit);
|
|
983
|
+
}
|
|
984
|
+
export function countTasks(repoId, opts = {}) {
|
|
985
|
+
if (opts.status) {
|
|
986
|
+
const row = openDb()
|
|
987
|
+
.prepare(`SELECT COUNT(*) AS n FROM agent_tasks WHERE repo_id = ? AND status = ?`)
|
|
988
|
+
.get(repoId, opts.status);
|
|
989
|
+
return Number(row?.n || 0);
|
|
990
|
+
}
|
|
991
|
+
if (opts.openOnly) {
|
|
992
|
+
const row = openDb()
|
|
993
|
+
.prepare(`SELECT COUNT(*) AS n FROM agent_tasks WHERE repo_id = ? AND status != 'done'`)
|
|
994
|
+
.get(repoId);
|
|
995
|
+
return Number(row?.n || 0);
|
|
996
|
+
}
|
|
997
|
+
const row = openDb()
|
|
998
|
+
.prepare(`SELECT COUNT(*) AS n FROM agent_tasks WHERE repo_id = ?`)
|
|
999
|
+
.get(repoId);
|
|
1000
|
+
return Number(row?.n || 0);
|
|
1001
|
+
}
|
|
1002
|
+
export function insertTask(input) {
|
|
1003
|
+
const title = String(input.title || "")
|
|
1004
|
+
.trim()
|
|
1005
|
+
.slice(0, 200);
|
|
1006
|
+
if (!title)
|
|
1007
|
+
throw new Error("title is required");
|
|
1008
|
+
const status = normalizeTaskStatus(input.status) || "backlog";
|
|
1009
|
+
const id = newId("task");
|
|
1010
|
+
const ts = nowIso();
|
|
1011
|
+
const completed = status === "done" ? ts : null;
|
|
1012
|
+
openDb()
|
|
1013
|
+
.prepare(`INSERT INTO agent_tasks (
|
|
1014
|
+
repo_id, id, title, body, status, anchors, source, created_at, updated_at, completed_at
|
|
1015
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
1016
|
+
.run(input.repoId, id, title, String(input.body || "").slice(0, 4000), status, encodeTaskAnchors(input.anchors), String(input.source || "ui").slice(0, 80), ts, ts, completed);
|
|
1017
|
+
return getTask(input.repoId, id);
|
|
1018
|
+
}
|
|
1019
|
+
export function updateTask(repoId, id, patch) {
|
|
1020
|
+
const existing = getTask(repoId, id);
|
|
1021
|
+
if (!existing)
|
|
1022
|
+
return null;
|
|
1023
|
+
const ts = nowIso();
|
|
1024
|
+
let title = existing.title;
|
|
1025
|
+
let body = existing.body;
|
|
1026
|
+
let status = existing.status;
|
|
1027
|
+
let anchors = existing.anchors;
|
|
1028
|
+
let completedAt = existing.completed_at;
|
|
1029
|
+
if (typeof patch.title === "string") {
|
|
1030
|
+
const t = patch.title.trim().slice(0, 200);
|
|
1031
|
+
if (!t)
|
|
1032
|
+
throw new Error("title cannot be empty");
|
|
1033
|
+
title = t;
|
|
1034
|
+
}
|
|
1035
|
+
if (typeof patch.body === "string")
|
|
1036
|
+
body = patch.body.slice(0, 4000);
|
|
1037
|
+
if (patch.status !== undefined) {
|
|
1038
|
+
const next = normalizeTaskStatus(patch.status);
|
|
1039
|
+
if (!next)
|
|
1040
|
+
throw new Error("invalid status");
|
|
1041
|
+
status = next;
|
|
1042
|
+
if (status === "done")
|
|
1043
|
+
completedAt = completedAt || ts;
|
|
1044
|
+
else
|
|
1045
|
+
completedAt = null;
|
|
1046
|
+
}
|
|
1047
|
+
if (patch.anchors !== undefined)
|
|
1048
|
+
anchors = encodeTaskAnchors(patch.anchors);
|
|
1049
|
+
openDb()
|
|
1050
|
+
.prepare(`UPDATE agent_tasks SET title = ?, body = ?, status = ?, anchors = ?,
|
|
1051
|
+
updated_at = ?, completed_at = ? WHERE repo_id = ? AND id = ?`)
|
|
1052
|
+
.run(title, body, status, anchors, ts, completedAt, repoId, id);
|
|
1053
|
+
return getTask(repoId, id);
|
|
1054
|
+
}
|
|
1055
|
+
export function completeTask(repoId, id) {
|
|
1056
|
+
return updateTask(repoId, id, { status: "done" });
|
|
1057
|
+
}
|
|
1058
|
+
export function deleteTask(repoId, id) {
|
|
1059
|
+
const info = openDb()
|
|
1060
|
+
.prepare(`DELETE FROM agent_tasks WHERE repo_id = ? AND id = ?`)
|
|
1061
|
+
.run(repoId, id);
|
|
1062
|
+
return Number(info.changes || 0) > 0;
|
|
1063
|
+
}
|
|
666
1064
|
export { nowIso };
|
package/dist/embed.js
CHANGED
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* On-device embeddings. Default is
|
|
3
|
-
* Pro can switch to a local n-gram encoder (still no cloud, no model fetch).
|
|
2
|
+
* On-device embeddings. Default is local n-gram semantic encoder (Pro retrieval on by default, no download, 100% private).
|
|
4
3
|
*/
|
|
5
4
|
import { execFileSync } from "node:child_process";
|
|
6
5
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
7
6
|
import { join } from "node:path";
|
|
8
|
-
import { FEATURE_LOCAL_EMBED, hasFeature } from "./license.js";
|
|
9
7
|
import { amemHome } from "./paths.js";
|
|
10
8
|
import { tokenize } from "./search.js";
|
|
11
9
|
export const HASH_DIM = 128;
|
|
@@ -31,16 +29,12 @@ export function requestedEmbedBackend() {
|
|
|
31
29
|
if (env === "hash" || env === "ngram" || env === "external")
|
|
32
30
|
return env;
|
|
33
31
|
const raw = readEmbedSettings().backend;
|
|
34
|
-
if (raw === "ngram" || raw === "external")
|
|
32
|
+
if (raw === "hash" || raw === "ngram" || raw === "external")
|
|
35
33
|
return raw;
|
|
36
|
-
return "
|
|
34
|
+
return "ngram";
|
|
37
35
|
}
|
|
38
36
|
export function activeEmbedBackend() {
|
|
39
|
-
|
|
40
|
-
if ((requested === "ngram" || requested === "external") && hasFeature(FEATURE_LOCAL_EMBED)) {
|
|
41
|
-
return requested;
|
|
42
|
-
}
|
|
43
|
-
return "hash";
|
|
37
|
+
return requestedEmbedBackend();
|
|
44
38
|
}
|
|
45
39
|
export function embedDim(backend = activeEmbedBackend()) {
|
|
46
40
|
if (backend === "ngram")
|
|
@@ -65,16 +59,13 @@ export function embedStatus() {
|
|
|
65
59
|
backend,
|
|
66
60
|
requested,
|
|
67
61
|
dim: embedDim(backend),
|
|
68
|
-
licensed:
|
|
62
|
+
licensed: true,
|
|
69
63
|
path: embedSettingsPath(),
|
|
70
64
|
command,
|
|
71
65
|
args,
|
|
72
66
|
};
|
|
73
67
|
}
|
|
74
68
|
export function setEmbedBackend(backend, extra = {}) {
|
|
75
|
-
if ((backend === "ngram" || backend === "external") && !hasFeature(FEATURE_LOCAL_EMBED)) {
|
|
76
|
-
throw new Error("Local embeddings need an amem Pro or IT license. Buy at https://getamem.com then: amem license apply --file <amem-license.json>");
|
|
77
|
-
}
|
|
78
69
|
if (backend === "external" && !(extra.command || process.env.AMEM_EMBED_CMD || readEmbedSettings().command)) {
|
|
79
70
|
throw new Error("external embedder needs --cmd (stdin text → stdout JSON { vector: number[] })");
|
|
80
71
|
}
|
package/dist/hook.js
CHANGED
|
@@ -4,6 +4,7 @@ import { logContextUsage } from "./api/routes.js";
|
|
|
4
4
|
import { captureMissLearnDraft, captureSessionDraft, findRecentContextMisses, isUsefulCaptureText, } from "./capture.js";
|
|
5
5
|
import { getRepoByCwd, insertConversationNote, listConversationNotes, upsertRepo, } from "./db.js";
|
|
6
6
|
import { detectRepoIdentity } from "./repo-identity.js";
|
|
7
|
+
import { captureSkillRevision, captureSkillSuggestion } from "./skill-capture.js";
|
|
7
8
|
const SECRET = /password|api[_-]?key|secret|token\s*[:=]|begin (rsa |openssh )?private/i;
|
|
8
9
|
function workspaceRoot(payload) {
|
|
9
10
|
const roots = payload.workspace_roots;
|
|
@@ -63,8 +64,9 @@ function injectPacket(repo, query, session, platform) {
|
|
|
63
64
|
sessionId: session,
|
|
64
65
|
query: query || "(session start)",
|
|
65
66
|
});
|
|
66
|
-
if (packet.claims.length === 0 && packet.notes.length === 0)
|
|
67
|
+
if (packet.claims.length === 0 && packet.notes.length === 0 && (packet.tasks?.length ?? 0) === 0) {
|
|
67
68
|
return null;
|
|
69
|
+
}
|
|
68
70
|
return cap(markdown);
|
|
69
71
|
}
|
|
70
72
|
export function handleHookPayload(raw) {
|
|
@@ -171,6 +173,11 @@ function handleHookPayloadInner(raw) {
|
|
|
171
173
|
notes: recent.slice(0, 8).map((n) => ({ role: n.role, text: n.text })),
|
|
172
174
|
});
|
|
173
175
|
}
|
|
176
|
+
// Procedural memory: was this session a workflow worth writing up, or evidence that a
|
|
177
|
+
// skill we already followed is wrong? Chronological order matters to the heuristics.
|
|
178
|
+
const ordered = [...recent].reverse().map((n) => ({ role: n.role, text: n.text }));
|
|
179
|
+
captureSkillRevision({ repo, sessionId: sid, notes: ordered }) ??
|
|
180
|
+
captureSkillSuggestion({ repo, sessionId: sid, notes: ordered });
|
|
174
181
|
return {};
|
|
175
182
|
}
|
|
176
183
|
return { continue: true };
|
package/dist/hygiene.d.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Local memory hygiene: decay unused facts, find near-duplicates, review inbox.
|
|
3
|
-
*
|
|
4
|
-
* Nothing is uploaded.
|
|
3
|
+
* Completely free and open — runs on-device, nothing uploaded.
|
|
5
4
|
*/
|
|
6
5
|
import { type ClaimRow } from "./db.js";
|
|
7
6
|
export type HygieneDuplicate = {
|
package/dist/hygiene.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Local memory hygiene: decay unused facts, find near-duplicates, review inbox.
|
|
3
|
-
*
|
|
4
|
-
* Nothing is uploaded.
|
|
3
|
+
* Completely free and open — runs on-device, nothing uploaded.
|
|
5
4
|
*/
|
|
6
5
|
import { getClaim, listClaims, listProposalDrafts, listRepos, listUsageEvents, setClaimStatus, } from "./db.js";
|
|
7
6
|
import { FEATURE_HYGIENE, hasFeature, requireFeature } from "./license.js";
|
package/dist/install/hosts.js
CHANGED
|
@@ -74,6 +74,14 @@ Before large exploration, run:
|
|
|
74
74
|
amem context "\${user question}"
|
|
75
75
|
\`\`\`
|
|
76
76
|
|
|
77
|
+
Deferred tasks & Kanban:
|
|
78
|
+
|
|
79
|
+
\`\`\`bash
|
|
80
|
+
amem task list # list pending tasks
|
|
81
|
+
amem task add "..." --body "..." # add task to backlog
|
|
82
|
+
amem task complete <id> # mark finished task as done
|
|
83
|
+
\`\`\`
|
|
84
|
+
|
|
77
85
|
After durable discoveries:
|
|
78
86
|
|
|
79
87
|
\`\`\`bash
|
package/dist/install/skills.js
CHANGED
|
@@ -17,14 +17,19 @@ export function copyBundledSkills(targetSkillsDir) {
|
|
|
17
17
|
mkdirSync(targetSkillsDir, { recursive: true });
|
|
18
18
|
const source = skillsSourceDir();
|
|
19
19
|
const installed = [];
|
|
20
|
-
|
|
20
|
+
const skillNames = [
|
|
21
|
+
"amem-bootstrap",
|
|
22
|
+
"amem-update-working-memory",
|
|
23
|
+
"amem-tasks",
|
|
24
|
+
"amem-write-skill",
|
|
25
|
+
];
|
|
26
|
+
for (const name of skillNames) {
|
|
21
27
|
const from = join(source, name);
|
|
22
28
|
const to = join(targetSkillsDir, name);
|
|
23
|
-
if (
|
|
24
|
-
|
|
29
|
+
if (existsSync(from)) {
|
|
30
|
+
cpSync(from, to, { recursive: true });
|
|
31
|
+
installed.push(to);
|
|
25
32
|
}
|
|
26
|
-
cpSync(from, to, { recursive: true });
|
|
27
|
-
installed.push(to);
|
|
28
33
|
}
|
|
29
34
|
return installed;
|
|
30
35
|
}
|
package/dist/license.d.ts
CHANGED
|
@@ -27,6 +27,7 @@ export declare const FEATURE_LOCAL_EMBED = "local_embed_model";
|
|
|
27
27
|
export declare const FEATURE_ATTEST_SKU = "attest_sku";
|
|
28
28
|
export declare const FEATURE_HYGIENE = "hygiene";
|
|
29
29
|
export declare const FEATURE_RULES_SYNC = "rules_sync";
|
|
30
|
+
export declare const ALL_FEATURES: string[];
|
|
30
31
|
/** Vendor verify key (SPKI DER hex). Issue signed files with AMEM_LICENSE_PRIVKEY. */
|
|
31
32
|
export declare const DEFAULT_LICENSE_PUBKEY_HEX = "302a300506032b6570032100b1e01cdb2d1ec60b372bb4307fa48ed743caba09a67633e4689aaa22221080fa";
|
|
32
33
|
export declare function licensePath(): string;
|
package/dist/license.js
CHANGED
|
@@ -10,19 +10,25 @@ export const FEATURE_LOCAL_EMBED = "local_embed_model";
|
|
|
10
10
|
export const FEATURE_ATTEST_SKU = "attest_sku";
|
|
11
11
|
export const FEATURE_HYGIENE = "hygiene";
|
|
12
12
|
export const FEATURE_RULES_SYNC = "rules_sync";
|
|
13
|
+
export const ALL_FEATURES = [
|
|
14
|
+
FEATURE_LOCAL_EMBED,
|
|
15
|
+
FEATURE_HYGIENE,
|
|
16
|
+
FEATURE_RULES_SYNC,
|
|
17
|
+
FEATURE_ATTEST_SKU,
|
|
18
|
+
];
|
|
13
19
|
/** Vendor verify key (SPKI DER hex). Issue signed files with AMEM_LICENSE_PRIVKEY. */
|
|
14
20
|
export const DEFAULT_LICENSE_PUBKEY_HEX = "302a300506032b6570032100b1e01cdb2d1ec60b372bb4307fa48ed743caba09a67633e4689aaa22221080fa";
|
|
15
|
-
const BUY_HINT = "
|
|
21
|
+
const BUY_HINT = "amem is completely free and open with all features included.";
|
|
16
22
|
const TIER_FEATURES = {
|
|
17
|
-
free:
|
|
18
|
-
pro:
|
|
19
|
-
it:
|
|
23
|
+
free: ALL_FEATURES,
|
|
24
|
+
pro: ALL_FEATURES,
|
|
25
|
+
it: ALL_FEATURES,
|
|
20
26
|
};
|
|
21
27
|
export function licensePath() {
|
|
22
28
|
return join(amemHome(), "license.json");
|
|
23
29
|
}
|
|
24
30
|
export function featuresForTier(tier) {
|
|
25
|
-
return [...
|
|
31
|
+
return [...ALL_FEATURES];
|
|
26
32
|
}
|
|
27
33
|
export function generateLicenseKeys() {
|
|
28
34
|
const pair = generateKeyPairSync("ed25519");
|
|
@@ -102,7 +108,7 @@ export function licenseStatus(now = new Date()) {
|
|
|
102
108
|
return {
|
|
103
109
|
tier: "free",
|
|
104
110
|
kind: "none",
|
|
105
|
-
features: [],
|
|
111
|
+
features: [...ALL_FEATURES],
|
|
106
112
|
valid: true,
|
|
107
113
|
transferable: false,
|
|
108
114
|
path,
|
|
@@ -112,36 +118,34 @@ export function licenseStatus(now = new Date()) {
|
|
|
112
118
|
if (expired(file.payload, now))
|
|
113
119
|
issues.push("license expired");
|
|
114
120
|
issues.push(...verifySignedLicense(file));
|
|
115
|
-
const tier =
|
|
116
|
-
const features = [...new Set([...(file.payload.features ?? []), ...
|
|
121
|
+
const tier = file.payload.tier || "free";
|
|
122
|
+
const features = [...new Set([...(file.payload.features ?? []), ...ALL_FEATURES])];
|
|
117
123
|
return {
|
|
118
124
|
tier,
|
|
119
125
|
kind: "signed",
|
|
120
126
|
subject: file.payload.subject,
|
|
121
127
|
expires_at: file.payload.expires_at,
|
|
122
128
|
features,
|
|
123
|
-
valid:
|
|
129
|
+
valid: true,
|
|
124
130
|
transferable: true,
|
|
125
131
|
path,
|
|
126
|
-
issues,
|
|
132
|
+
issues: [],
|
|
127
133
|
};
|
|
128
134
|
}
|
|
129
135
|
catch (error) {
|
|
130
|
-
issues.push(error instanceof Error ? error.message : String(error));
|
|
131
136
|
return {
|
|
132
137
|
tier: "free",
|
|
133
138
|
kind: "none",
|
|
134
|
-
features: [],
|
|
135
|
-
valid:
|
|
139
|
+
features: [...ALL_FEATURES],
|
|
140
|
+
valid: true,
|
|
136
141
|
transferable: false,
|
|
137
142
|
path,
|
|
138
|
-
issues,
|
|
143
|
+
issues: [],
|
|
139
144
|
};
|
|
140
145
|
}
|
|
141
146
|
}
|
|
142
147
|
export function hasFeature(feature) {
|
|
143
|
-
|
|
144
|
-
return status.valid && status.features.includes(feature);
|
|
148
|
+
return true;
|
|
145
149
|
}
|
|
146
150
|
export function writeLicense(file, path = licensePath()) {
|
|
147
151
|
mkdirSync(amemHome(), { recursive: true, mode: 0o700 });
|
|
@@ -166,7 +170,5 @@ export function clearLicense() {
|
|
|
166
170
|
unlinkSync(path);
|
|
167
171
|
}
|
|
168
172
|
export function requireFeature(feature, label = feature) {
|
|
169
|
-
|
|
170
|
-
return;
|
|
171
|
-
throw new Error(`${label} needs an amem Pro or IT license. ${BUY_HINT}`);
|
|
173
|
+
// All features are included and free by default
|
|
172
174
|
}
|