@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/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 feature hashing (no download).
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 "hash";
34
+ return "ngram";
37
35
  }
38
36
  export function activeEmbedBackend() {
39
- const requested = requestedEmbedBackend();
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: hasFeature(FEATURE_LOCAL_EMBED),
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
- * Apply/schedule is Pro/IT only. Preview counts are free (soft paywall).
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
- * Apply/schedule is Pro/IT only. Preview counts are free (soft paywall).
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";
@@ -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
@@ -17,14 +17,19 @@ export function copyBundledSkills(targetSkillsDir) {
17
17
  mkdirSync(targetSkillsDir, { recursive: true });
18
18
  const source = skillsSourceDir();
19
19
  const installed = [];
20
- for (const name of ["amem-bootstrap", "amem-update-working-memory"]) {
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 (!existsSync(from)) {
24
- throw new Error(`Missing bundled skill: ${from}`);
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 = "Buy at https://getamem.com then: amem license apply --file ~/Downloads/amem-license.json";
21
+ const BUY_HINT = "amem is completely free and open with all features included.";
16
22
  const TIER_FEATURES = {
17
- free: [],
18
- pro: [FEATURE_LOCAL_EMBED, FEATURE_HYGIENE, FEATURE_RULES_SYNC],
19
- it: [FEATURE_LOCAL_EMBED, FEATURE_HYGIENE, FEATURE_RULES_SYNC, FEATURE_ATTEST_SKU],
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 [...TIER_FEATURES[tier]];
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 = issues.length ? "free" : file.payload.tier;
116
- const features = [...new Set([...(file.payload.features ?? []), ...featuresForTier(tier)])];
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: issues.length === 0,
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: false,
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
- const status = licenseStatus();
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
- if (hasFeature(feature))
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
  }