@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/db.d.ts
CHANGED
|
@@ -96,6 +96,21 @@ export type SetupStateRow = {
|
|
|
96
96
|
setup_completed_at: string | null;
|
|
97
97
|
updated_at: string;
|
|
98
98
|
};
|
|
99
|
+
export type AgentTaskStatus = "backlog" | "next" | "doing" | "blocked" | "done";
|
|
100
|
+
export type AgentTaskRow = {
|
|
101
|
+
repo_id: string;
|
|
102
|
+
id: string;
|
|
103
|
+
title: string;
|
|
104
|
+
body: string;
|
|
105
|
+
status: AgentTaskStatus;
|
|
106
|
+
anchors: string;
|
|
107
|
+
source: string;
|
|
108
|
+
created_at: string;
|
|
109
|
+
updated_at: string;
|
|
110
|
+
completed_at: string | null;
|
|
111
|
+
};
|
|
112
|
+
export declare const AGENT_TASK_STATUSES: readonly AgentTaskStatus[];
|
|
113
|
+
export declare function normalizeTaskStatus(raw: unknown): AgentTaskStatus | null;
|
|
99
114
|
export declare function openDb(): Database.Database;
|
|
100
115
|
export declare function closeDb(): void;
|
|
101
116
|
declare function nowIso(): string;
|
|
@@ -189,4 +204,130 @@ export declare function listProposalDraftsAll(opts?: {
|
|
|
189
204
|
export declare function countProposalDrafts(repoId: string, status?: string): number;
|
|
190
205
|
export declare function countProposalDraftsAll(status?: string): number;
|
|
191
206
|
export declare function setProposalDraftStatus(id: string, status: "pending" | "applied" | "dismissed"): ProposalDraftRow | null;
|
|
207
|
+
export type SkillRow = {
|
|
208
|
+
name: string;
|
|
209
|
+
path: string;
|
|
210
|
+
description: string;
|
|
211
|
+
version: string | null;
|
|
212
|
+
tags: string;
|
|
213
|
+
repo_id: string | null;
|
|
214
|
+
content_hash: string;
|
|
215
|
+
origin_hash: string | null;
|
|
216
|
+
source: string;
|
|
217
|
+
uses: number;
|
|
218
|
+
last_used_at: string | null;
|
|
219
|
+
created_at: string;
|
|
220
|
+
updated_at: string;
|
|
221
|
+
};
|
|
222
|
+
export declare function listSkillRows(): SkillRow[];
|
|
223
|
+
export declare function getSkillRow(name: string): SkillRow | null;
|
|
224
|
+
/**
|
|
225
|
+
* Index one skill found on disk. Disk is the source of truth, so this only ever refreshes
|
|
226
|
+
* derived columns — it must not clobber the repo tag or usage counters a user built up.
|
|
227
|
+
*/
|
|
228
|
+
export declare function upsertSkillRow(input: {
|
|
229
|
+
name: string;
|
|
230
|
+
path: string;
|
|
231
|
+
description?: string;
|
|
232
|
+
version?: string | null;
|
|
233
|
+
tags?: string[];
|
|
234
|
+
contentHash: string;
|
|
235
|
+
source?: string;
|
|
236
|
+
repoId?: string | null;
|
|
237
|
+
}): SkillRow;
|
|
238
|
+
/** Optional memory tag. Skills are a global library; the tag is only a filter hint. */
|
|
239
|
+
export declare function setSkillRepo(name: string, repoId: string | null): void;
|
|
240
|
+
export declare function deleteSkillRow(name: string): boolean;
|
|
241
|
+
/** Drop index rows whose skill is no longer on disk. */
|
|
242
|
+
export declare function pruneSkillRows(keepNames: string[]): number;
|
|
243
|
+
export declare function recordSkillUse(name: string, ctx?: {
|
|
244
|
+
repoId?: string | null;
|
|
245
|
+
sessionId?: string | null;
|
|
246
|
+
}): void;
|
|
247
|
+
/**
|
|
248
|
+
* Skills used recently in a repo. MCP clients do not always carry a session id, so
|
|
249
|
+
* recency in the same memory is the fallback for correlating a view to a session.
|
|
250
|
+
*/
|
|
251
|
+
export declare function listRecentSkillUses(repoId: string, minutes?: number, limit?: number): string[];
|
|
252
|
+
export declare function listSkillsUsedInSession(sessionId: string, limit?: number): string[];
|
|
253
|
+
export type SkillDraftRow = {
|
|
254
|
+
id: string;
|
|
255
|
+
repo_id: string | null;
|
|
256
|
+
name: string | null;
|
|
257
|
+
title: string;
|
|
258
|
+
summary: string;
|
|
259
|
+
content: string | null;
|
|
260
|
+
kind: string;
|
|
261
|
+
target_skill: string | null;
|
|
262
|
+
status: string;
|
|
263
|
+
source: string;
|
|
264
|
+
session_id: string | null;
|
|
265
|
+
reasons: string;
|
|
266
|
+
created_at: string;
|
|
267
|
+
updated_at: string;
|
|
268
|
+
};
|
|
269
|
+
export declare function insertSkillDraft(input: {
|
|
270
|
+
repoId?: string | null;
|
|
271
|
+
name?: string | null;
|
|
272
|
+
title: string;
|
|
273
|
+
summary?: string;
|
|
274
|
+
content?: string | null;
|
|
275
|
+
kind?: "suggestion" | "create" | "revision";
|
|
276
|
+
targetSkill?: string | null;
|
|
277
|
+
source?: string;
|
|
278
|
+
sessionId?: string | null;
|
|
279
|
+
reasons?: string[];
|
|
280
|
+
}): SkillDraftRow;
|
|
281
|
+
export declare function getSkillDraft(id: string): SkillDraftRow | null;
|
|
282
|
+
export declare function listSkillDrafts(opts?: {
|
|
283
|
+
status?: string;
|
|
284
|
+
repoId?: string;
|
|
285
|
+
limit?: number;
|
|
286
|
+
}): SkillDraftRow[];
|
|
287
|
+
export declare function setSkillDraftStatus(id: string, status: string): SkillDraftRow | null;
|
|
288
|
+
export declare function skillDraftExists(source: string): boolean;
|
|
289
|
+
export declare function getTask(repoId: string, id: string): AgentTaskRow | null;
|
|
290
|
+
export declare function listTasks(repoId: string, opts?: {
|
|
291
|
+
status?: AgentTaskStatus;
|
|
292
|
+
includeDone?: boolean;
|
|
293
|
+
limit?: number;
|
|
294
|
+
}): AgentTaskRow[];
|
|
295
|
+
/**
|
|
296
|
+
* Tasks across every memory. The UI's "All memory" scope needs this because agents file
|
|
297
|
+
* tasks against whatever repo they were working in, which is often not the repo the UI
|
|
298
|
+
* was launched from — without it those tasks are invisible.
|
|
299
|
+
*/
|
|
300
|
+
export declare function listTasksAll(opts?: {
|
|
301
|
+
status?: AgentTaskStatus;
|
|
302
|
+
includeDone?: boolean;
|
|
303
|
+
limit?: number;
|
|
304
|
+
}): AgentTaskRow[];
|
|
305
|
+
export declare function countTasksAll(opts?: {
|
|
306
|
+
status?: AgentTaskStatus;
|
|
307
|
+
openOnly?: boolean;
|
|
308
|
+
}): number;
|
|
309
|
+
/** Find a task without knowing its repo, so all-memory edits can resolve their owner. */
|
|
310
|
+
export declare function findTaskAnyRepo(id: string): AgentTaskRow | null;
|
|
311
|
+
/** Open tasks for context injection — prefer doing/next/blocked, then backlog. */
|
|
312
|
+
export declare function listOpenTasksForContext(repoId: string, limit?: number): AgentTaskRow[];
|
|
313
|
+
export declare function countTasks(repoId: string, opts?: {
|
|
314
|
+
status?: AgentTaskStatus;
|
|
315
|
+
openOnly?: boolean;
|
|
316
|
+
}): number;
|
|
317
|
+
export declare function insertTask(input: {
|
|
318
|
+
repoId: string;
|
|
319
|
+
title: string;
|
|
320
|
+
body?: string;
|
|
321
|
+
status?: AgentTaskStatus | string;
|
|
322
|
+
anchors?: string[];
|
|
323
|
+
source?: string;
|
|
324
|
+
}): AgentTaskRow;
|
|
325
|
+
export declare function updateTask(repoId: string, id: string, patch: {
|
|
326
|
+
title?: string;
|
|
327
|
+
body?: string;
|
|
328
|
+
status?: AgentTaskStatus | string;
|
|
329
|
+
anchors?: string[];
|
|
330
|
+
}): AgentTaskRow | null;
|
|
331
|
+
export declare function completeTask(repoId: string, id: string): AgentTaskRow | null;
|
|
332
|
+
export declare function deleteTask(repoId: string, id: string): boolean;
|
|
192
333
|
export { nowIso };
|
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/estimate.d.ts
CHANGED
|
@@ -2,14 +2,38 @@ import type { ContextPacket } from "./context.js";
|
|
|
2
2
|
/** Rough chars→tokens. */
|
|
3
3
|
export declare function estimateTokensFromText(text: string): number;
|
|
4
4
|
/**
|
|
5
|
-
*
|
|
5
|
+
* Assumed cost of one file the agent did not have to open. This is a MODELLED
|
|
6
|
+
* constant, not a measurement: it credits a saving whether or not the agent
|
|
7
|
+
* would actually have read that file. It dominates the headline number, so
|
|
8
|
+
* treat any total built from it as an upper bound until calibrated against
|
|
9
|
+
* real reported savings (`amem usage report --saved <n>`).
|
|
10
|
+
*/
|
|
11
|
+
export declare const ASSUMED_TOKENS_PER_FILE = 4000;
|
|
12
|
+
export declare const ASSUMED_TOKENS_PER_CLAIM = 200;
|
|
13
|
+
/**
|
|
14
|
+
* Proxy for exploration avoided, net of what the packet itself cost:
|
|
6
15
|
* anchors_returned * 4000 + claims_returned * 200 - packet_tokens
|
|
16
|
+
*
|
|
17
|
+
* Deliberately NOT clamped at zero. A packet that returns little and still
|
|
18
|
+
* costs input tokens is a net loss, and the metric has to be able to say so —
|
|
19
|
+
* clamping made the dashboard structurally incapable of reporting that amem
|
|
20
|
+
* ever cost anything, which is not a property you want in your own numbers.
|
|
7
21
|
*/
|
|
8
22
|
export declare function estimateTokensSaved(input: {
|
|
9
23
|
anchorsCount: number;
|
|
10
24
|
claimsCount: number;
|
|
11
25
|
packetTokens: number;
|
|
12
26
|
}): number;
|
|
27
|
+
/**
|
|
28
|
+
* How a savings figure should be presented. "measured" only once real reported
|
|
29
|
+
* savings exist; until then the number is a model and must be labelled as one.
|
|
30
|
+
*/
|
|
31
|
+
export declare function savingsBasis(reportedTokensSaved: number): {
|
|
32
|
+
savingsBasis: "measured" | "modelled";
|
|
33
|
+
calibrated: boolean;
|
|
34
|
+
assumedTokensPerFile: number;
|
|
35
|
+
assumedTokensPerClaim: number;
|
|
36
|
+
};
|
|
13
37
|
/** Typical Cursor/Claude tool round-trip to read a file (~1.2s) plus a little per claim. */
|
|
14
38
|
export declare const MS_PER_FILE_ROUNDTRIP = 1200;
|
|
15
39
|
export declare const MS_PER_CLAIM = 80;
|