@iamem/amem 0.1.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/LICENSE +21 -0
- package/README.md +534 -0
- package/dist/activity.d.ts +27 -0
- package/dist/activity.js +202 -0
- package/dist/api/routes.d.ts +31 -0
- package/dist/api/routes.js +1345 -0
- package/dist/attest.d.ts +52 -0
- package/dist/attest.js +192 -0
- package/dist/backup-schedule.d.ts +25 -0
- package/dist/backup-schedule.js +216 -0
- package/dist/capture.d.ts +34 -0
- package/dist/capture.js +257 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +1007 -0
- package/dist/context.d.ts +46 -0
- package/dist/context.js +334 -0
- package/dist/crypto.d.ts +39 -0
- package/dist/crypto.js +166 -0
- package/dist/db.d.ts +192 -0
- package/dist/db.js +666 -0
- package/dist/draft-quality.d.ts +19 -0
- package/dist/draft-quality.js +85 -0
- package/dist/embed.d.ts +76 -0
- package/dist/embed.js +331 -0
- package/dist/estimate.d.ts +32 -0
- package/dist/estimate.js +69 -0
- package/dist/freshness.d.ts +15 -0
- package/dist/freshness.js +93 -0
- package/dist/hook.d.ts +16 -0
- package/dist/hook.js +177 -0
- package/dist/hygiene-schedule.d.ts +28 -0
- package/dist/hygiene-schedule.js +221 -0
- package/dist/hygiene.d.ts +61 -0
- package/dist/hygiene.js +196 -0
- package/dist/install/claude.d.ts +6 -0
- package/dist/install/claude.js +69 -0
- package/dist/install/cursor.d.ts +7 -0
- package/dist/install/cursor.js +80 -0
- package/dist/install/hosts.d.ts +21 -0
- package/dist/install/hosts.js +186 -0
- package/dist/install/skills.d.ts +8 -0
- package/dist/install/skills.js +67 -0
- package/dist/it-pack.d.ts +20 -0
- package/dist/it-pack.js +84 -0
- package/dist/kinds.d.ts +18 -0
- package/dist/kinds.js +106 -0
- package/dist/license.d.ts +48 -0
- package/dist/license.js +172 -0
- package/dist/mcp.d.ts +40 -0
- package/dist/mcp.js +435 -0
- package/dist/paths.d.ts +11 -0
- package/dist/paths.js +55 -0
- package/dist/personal.d.ts +7 -0
- package/dist/personal.js +44 -0
- package/dist/platforms.d.ts +11 -0
- package/dist/platforms.js +32 -0
- package/dist/policy.d.ts +46 -0
- package/dist/policy.js +254 -0
- package/dist/prefs.d.ts +6 -0
- package/dist/prefs.js +11 -0
- package/dist/proposal.d.ts +90 -0
- package/dist/proposal.js +376 -0
- package/dist/publish.d.ts +28 -0
- package/dist/publish.js +57 -0
- package/dist/remember-contract.d.ts +23 -0
- package/dist/remember-contract.js +117 -0
- package/dist/repo-identity.d.ts +15 -0
- package/dist/repo-identity.js +82 -0
- package/dist/rules-sync.d.ts +7 -0
- package/dist/rules-sync.js +47 -0
- package/dist/savings-export.d.ts +49 -0
- package/dist/savings-export.js +141 -0
- package/dist/scan.d.ts +16 -0
- package/dist/scan.js +109 -0
- package/dist/search.d.ts +25 -0
- package/dist/search.js +150 -0
- package/dist/service.d.ts +20 -0
- package/dist/service.js +254 -0
- package/dist/shop.d.ts +9 -0
- package/dist/shop.js +15 -0
- package/dist/ui/server.d.ts +21 -0
- package/dist/ui/server.js +268 -0
- package/dist/vault.d.ts +25 -0
- package/dist/vault.js +42 -0
- package/dist/workspace-setup.d.ts +8 -0
- package/dist/workspace-setup.js +55 -0
- package/docs/agent-install-prompt.md +41 -0
- package/docs/backlog.md +59 -0
- package/docs/enterprise-endpoint.md +98 -0
- package/docs/license.md +54 -0
- package/docs/npm-release.md +38 -0
- package/docs/remember-contract.md +42 -0
- package/package.json +64 -0
- package/scripts/mdm-offboard.sh +14 -0
- package/skills/amem-bootstrap/SKILL.md +83 -0
- package/skills/amem-update-working-memory/SKILL.md +54 -0
- package/templates/cursor-rule.mdc +23 -0
- package/templates/mdm/co.amem.managed.plist +33 -0
- package/templates/policy.deny-default.toml +20 -0
- package/templates/policy.example.toml +22 -0
- package/ui-static/app.js +3966 -0
- package/ui-static/index.html +178 -0
- package/ui-static/orbit.js +389 -0
- package/ui-static/styles.css +2411 -0
package/dist/db.js
ADDED
|
@@ -0,0 +1,666 @@
|
|
|
1
|
+
import Database from "better-sqlite3";
|
|
2
|
+
import { existsSync, realpathSync } from "node:fs";
|
|
3
|
+
import { resolve } from "node:path";
|
|
4
|
+
import { dbPath, ensureAmemHome } from "./paths.js";
|
|
5
|
+
import { detectRepoIdentity, newId, parseWorkspaceSlug, slugifyWorkspace } from "./repo-identity.js";
|
|
6
|
+
import { ensureClaimsFts, reindexAllClaimsFts, reindexRepoClaimsFts, removeClaimFts, upsertClaimFts } from "./search.js";
|
|
7
|
+
import { ensureClaimsEmbed, reindexRepoEmbeds, removeClaimEmbed, upsertClaimEmbed, } from "./embed.js";
|
|
8
|
+
import { isDbEncryptedAtRest, resolvePassphrase, unlockDatabase } from "./crypto.js";
|
|
9
|
+
const SCHEMA = `
|
|
10
|
+
CREATE TABLE IF NOT EXISTS repos (
|
|
11
|
+
id TEXT PRIMARY KEY,
|
|
12
|
+
repo_key TEXT NOT NULL UNIQUE,
|
|
13
|
+
remote_url TEXT,
|
|
14
|
+
root_path TEXT NOT NULL,
|
|
15
|
+
repo_name TEXT NOT NULL,
|
|
16
|
+
default_branch TEXT NOT NULL,
|
|
17
|
+
platform TEXT,
|
|
18
|
+
created_at TEXT NOT NULL,
|
|
19
|
+
updated_at TEXT NOT NULL
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
CREATE TABLE IF NOT EXISTS components (
|
|
23
|
+
repo_id TEXT NOT NULL REFERENCES repos(id) ON DELETE CASCADE,
|
|
24
|
+
id TEXT NOT NULL,
|
|
25
|
+
name TEXT NOT NULL,
|
|
26
|
+
code_anchor TEXT,
|
|
27
|
+
PRIMARY KEY(repo_id, id)
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
CREATE TABLE IF NOT EXISTS flows (
|
|
31
|
+
repo_id TEXT NOT NULL REFERENCES repos(id) ON DELETE CASCADE,
|
|
32
|
+
id TEXT NOT NULL,
|
|
33
|
+
name TEXT NOT NULL,
|
|
34
|
+
PRIMARY KEY(repo_id, id)
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
CREATE TABLE IF NOT EXISTS claims (
|
|
38
|
+
repo_id TEXT NOT NULL REFERENCES repos(id) ON DELETE CASCADE,
|
|
39
|
+
id TEXT NOT NULL,
|
|
40
|
+
kind TEXT NOT NULL,
|
|
41
|
+
text TEXT NOT NULL,
|
|
42
|
+
code_anchors TEXT NOT NULL DEFAULT '[]',
|
|
43
|
+
source_ref TEXT,
|
|
44
|
+
created_at TEXT NOT NULL,
|
|
45
|
+
updated_at TEXT NOT NULL,
|
|
46
|
+
status TEXT NOT NULL DEFAULT 'active',
|
|
47
|
+
superseded_by TEXT,
|
|
48
|
+
pinned INTEGER NOT NULL DEFAULT 0,
|
|
49
|
+
PRIMARY KEY(repo_id, id)
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
CREATE TABLE IF NOT EXISTS edges (
|
|
53
|
+
repo_id TEXT NOT NULL REFERENCES repos(id) ON DELETE CASCADE,
|
|
54
|
+
id TEXT NOT NULL,
|
|
55
|
+
from_id TEXT NOT NULL,
|
|
56
|
+
from_type TEXT NOT NULL,
|
|
57
|
+
to_id TEXT NOT NULL,
|
|
58
|
+
to_type TEXT NOT NULL,
|
|
59
|
+
kind TEXT NOT NULL,
|
|
60
|
+
PRIMARY KEY(repo_id, id)
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
CREATE TABLE IF NOT EXISTS agent_sessions (
|
|
64
|
+
platform TEXT NOT NULL,
|
|
65
|
+
session_id TEXT NOT NULL,
|
|
66
|
+
repo_id TEXT NOT NULL REFERENCES repos(id) ON DELETE CASCADE,
|
|
67
|
+
last_seen TEXT NOT NULL,
|
|
68
|
+
PRIMARY KEY(platform, session_id)
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
CREATE TABLE IF NOT EXISTS usage_events (
|
|
72
|
+
id TEXT PRIMARY KEY,
|
|
73
|
+
repo_id TEXT NOT NULL REFERENCES repos(id) ON DELETE CASCADE,
|
|
74
|
+
platform TEXT NOT NULL,
|
|
75
|
+
session_id TEXT,
|
|
76
|
+
query TEXT NOT NULL,
|
|
77
|
+
claim_ids TEXT NOT NULL,
|
|
78
|
+
anchors_count INTEGER NOT NULL,
|
|
79
|
+
claims_count INTEGER NOT NULL,
|
|
80
|
+
packet_tokens INTEGER NOT NULL,
|
|
81
|
+
estimated_tokens_saved INTEGER NOT NULL,
|
|
82
|
+
reported_tokens_saved INTEGER,
|
|
83
|
+
created_at TEXT NOT NULL,
|
|
84
|
+
local_ms INTEGER,
|
|
85
|
+
estimated_ms_saved INTEGER,
|
|
86
|
+
kind TEXT
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
CREATE TABLE IF NOT EXISTS setup_state (
|
|
90
|
+
repo_id TEXT PRIMARY KEY REFERENCES repos(id) ON DELETE CASCADE,
|
|
91
|
+
platforms TEXT NOT NULL,
|
|
92
|
+
setup_completed_at TEXT,
|
|
93
|
+
updated_at TEXT NOT NULL
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
CREATE TABLE IF NOT EXISTS conversation_notes (
|
|
97
|
+
id TEXT PRIMARY KEY,
|
|
98
|
+
repo_id TEXT NOT NULL REFERENCES repos(id) ON DELETE CASCADE,
|
|
99
|
+
platform TEXT NOT NULL,
|
|
100
|
+
session_id TEXT,
|
|
101
|
+
role TEXT NOT NULL,
|
|
102
|
+
text TEXT NOT NULL,
|
|
103
|
+
created_at TEXT NOT NULL
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
CREATE TABLE IF NOT EXISTS proposal_drafts (
|
|
107
|
+
id TEXT PRIMARY KEY,
|
|
108
|
+
repo_id TEXT NOT NULL REFERENCES repos(id) ON DELETE CASCADE,
|
|
109
|
+
platform TEXT NOT NULL,
|
|
110
|
+
session_id TEXT,
|
|
111
|
+
title TEXT NOT NULL,
|
|
112
|
+
proposal_json TEXT NOT NULL,
|
|
113
|
+
status TEXT NOT NULL DEFAULT 'pending',
|
|
114
|
+
source TEXT NOT NULL DEFAULT 'session-end',
|
|
115
|
+
created_at TEXT NOT NULL,
|
|
116
|
+
updated_at TEXT NOT NULL
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
CREATE INDEX IF NOT EXISTS claims_repo_idx ON claims(repo_id);
|
|
120
|
+
CREATE INDEX IF NOT EXISTS edges_repo_idx ON edges(repo_id);
|
|
121
|
+
CREATE INDEX IF NOT EXISTS components_repo_idx ON components(repo_id);
|
|
122
|
+
CREATE INDEX IF NOT EXISTS flows_repo_idx ON flows(repo_id);
|
|
123
|
+
CREATE INDEX IF NOT EXISTS agent_sessions_repo_idx ON agent_sessions(repo_id);
|
|
124
|
+
CREATE INDEX IF NOT EXISTS usage_events_repo_idx ON usage_events(repo_id);
|
|
125
|
+
CREATE INDEX IF NOT EXISTS usage_events_created_idx ON usage_events(created_at);
|
|
126
|
+
CREATE INDEX IF NOT EXISTS conversation_notes_repo_idx ON conversation_notes(repo_id);
|
|
127
|
+
CREATE INDEX IF NOT EXISTS conversation_notes_created_idx ON conversation_notes(created_at);
|
|
128
|
+
CREATE INDEX IF NOT EXISTS proposal_drafts_repo_idx ON proposal_drafts(repo_id);
|
|
129
|
+
CREATE INDEX IF NOT EXISTS proposal_drafts_status_idx ON proposal_drafts(status);
|
|
130
|
+
`;
|
|
131
|
+
let cached = null;
|
|
132
|
+
export function openDb() {
|
|
133
|
+
if (cached)
|
|
134
|
+
return cached;
|
|
135
|
+
ensureAmemHome();
|
|
136
|
+
if (isDbEncryptedAtRest()) {
|
|
137
|
+
try {
|
|
138
|
+
unlockDatabase(resolvePassphrase(process.env.AMEM_PASSPHRASE));
|
|
139
|
+
}
|
|
140
|
+
catch (error) {
|
|
141
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
142
|
+
throw new Error(`amem database is encrypted at rest. Unlock with \`amem unlock --passphrase …\` or set AMEM_PASSPHRASE. (${detail})`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
const db = new Database(dbPath());
|
|
146
|
+
db.pragma("journal_mode = WAL");
|
|
147
|
+
db.pragma("busy_timeout = 5000");
|
|
148
|
+
db.pragma("foreign_keys = ON");
|
|
149
|
+
db.exec(SCHEMA);
|
|
150
|
+
migrateUsageEvents(db);
|
|
151
|
+
migrateClaimsColumns(db);
|
|
152
|
+
ensureClaimsFts(db);
|
|
153
|
+
migrateClaimsFtsBootstrap(db);
|
|
154
|
+
ensureProposalDrafts(db);
|
|
155
|
+
ensureClaimsEmbed(db);
|
|
156
|
+
cached = db;
|
|
157
|
+
return db;
|
|
158
|
+
}
|
|
159
|
+
function migrateUsageEvents(db) {
|
|
160
|
+
const cols = db.prepare("PRAGMA table_info(usage_events)").all();
|
|
161
|
+
const have = new Set(cols.map((c) => c.name));
|
|
162
|
+
if (!have.has("local_ms"))
|
|
163
|
+
db.exec("ALTER TABLE usage_events ADD COLUMN local_ms INTEGER");
|
|
164
|
+
if (!have.has("estimated_ms_saved")) {
|
|
165
|
+
db.exec("ALTER TABLE usage_events ADD COLUMN estimated_ms_saved INTEGER");
|
|
166
|
+
}
|
|
167
|
+
if (!have.has("kind"))
|
|
168
|
+
db.exec("ALTER TABLE usage_events ADD COLUMN kind TEXT");
|
|
169
|
+
}
|
|
170
|
+
function migrateClaimsColumns(db) {
|
|
171
|
+
const cols = db.prepare("PRAGMA table_info(claims)").all();
|
|
172
|
+
const have = new Set(cols.map((c) => c.name));
|
|
173
|
+
if (!have.has("status")) {
|
|
174
|
+
db.exec(`ALTER TABLE claims ADD COLUMN status TEXT NOT NULL DEFAULT 'active'`);
|
|
175
|
+
}
|
|
176
|
+
if (!have.has("superseded_by")) {
|
|
177
|
+
db.exec(`ALTER TABLE claims ADD COLUMN superseded_by TEXT`);
|
|
178
|
+
}
|
|
179
|
+
if (!have.has("pinned")) {
|
|
180
|
+
db.exec(`ALTER TABLE claims ADD COLUMN pinned INTEGER NOT NULL DEFAULT 0`);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
function ensureProposalDrafts(db) {
|
|
184
|
+
db.exec(`
|
|
185
|
+
CREATE TABLE IF NOT EXISTS proposal_drafts (
|
|
186
|
+
id TEXT PRIMARY KEY,
|
|
187
|
+
repo_id TEXT NOT NULL REFERENCES repos(id) ON DELETE CASCADE,
|
|
188
|
+
platform TEXT NOT NULL,
|
|
189
|
+
session_id TEXT,
|
|
190
|
+
title TEXT NOT NULL,
|
|
191
|
+
proposal_json TEXT NOT NULL,
|
|
192
|
+
status TEXT NOT NULL DEFAULT 'pending',
|
|
193
|
+
source TEXT NOT NULL DEFAULT 'session-end',
|
|
194
|
+
created_at TEXT NOT NULL,
|
|
195
|
+
updated_at TEXT NOT NULL
|
|
196
|
+
);
|
|
197
|
+
CREATE INDEX IF NOT EXISTS proposal_drafts_repo_idx ON proposal_drafts(repo_id);
|
|
198
|
+
CREATE INDEX IF NOT EXISTS proposal_drafts_status_idx ON proposal_drafts(status);
|
|
199
|
+
`);
|
|
200
|
+
}
|
|
201
|
+
/** One-shot FTS rebuild flag so upgrades populate the index. */
|
|
202
|
+
function migrateClaimsFtsBootstrap(db) {
|
|
203
|
+
db.exec(`
|
|
204
|
+
CREATE TABLE IF NOT EXISTS amem_meta (
|
|
205
|
+
key TEXT PRIMARY KEY,
|
|
206
|
+
value TEXT NOT NULL
|
|
207
|
+
);
|
|
208
|
+
`);
|
|
209
|
+
const row = db
|
|
210
|
+
.prepare(`SELECT value FROM amem_meta WHERE key = 'claims_fts_v1'`)
|
|
211
|
+
.get();
|
|
212
|
+
if (row?.value === "1")
|
|
213
|
+
return;
|
|
214
|
+
reindexAllClaimsFts(db);
|
|
215
|
+
db.prepare(`INSERT INTO amem_meta (key, value) VALUES ('claims_fts_v1', '1')
|
|
216
|
+
ON CONFLICT(key) DO UPDATE SET value = excluded.value`).run();
|
|
217
|
+
}
|
|
218
|
+
export function closeDb() {
|
|
219
|
+
if (cached) {
|
|
220
|
+
cached.close();
|
|
221
|
+
cached = null;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
function nowIso() {
|
|
225
|
+
return new Date().toISOString();
|
|
226
|
+
}
|
|
227
|
+
export function upsertRepo(identity = detectRepoIdentity(), platform) {
|
|
228
|
+
const db = openDb();
|
|
229
|
+
const existing = db
|
|
230
|
+
.prepare("SELECT * FROM repos WHERE repo_key = ?")
|
|
231
|
+
.get(identity.repoKey);
|
|
232
|
+
const ts = nowIso();
|
|
233
|
+
if (existing) {
|
|
234
|
+
db.prepare(`UPDATE repos SET remote_url = ?, root_path = ?, repo_name = ?, default_branch = ?,
|
|
235
|
+
platform = COALESCE(?, platform), updated_at = ? WHERE id = ?`).run(identity.remoteUrl, identity.rootPath, identity.repoName, identity.defaultBranch, platform ?? null, ts, existing.id);
|
|
236
|
+
return db.prepare("SELECT * FROM repos WHERE id = ?").get(existing.id);
|
|
237
|
+
}
|
|
238
|
+
const id = newId("repo");
|
|
239
|
+
db.prepare(`INSERT INTO repos (id, repo_key, remote_url, root_path, repo_name, default_branch, platform, created_at, updated_at)
|
|
240
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(id, identity.repoKey, identity.remoteUrl, identity.rootPath, identity.repoName, identity.defaultBranch, platform ?? null, ts, ts);
|
|
241
|
+
return db.prepare("SELECT * FROM repos WHERE id = ?").get(id);
|
|
242
|
+
}
|
|
243
|
+
function normalizeFsPath(p) {
|
|
244
|
+
const resolved = resolve(p).replace(/[/\\]+$/, "");
|
|
245
|
+
try {
|
|
246
|
+
if (existsSync(resolved))
|
|
247
|
+
return realpathSync(resolved);
|
|
248
|
+
}
|
|
249
|
+
catch {
|
|
250
|
+
// ignore
|
|
251
|
+
}
|
|
252
|
+
return resolved;
|
|
253
|
+
}
|
|
254
|
+
export function getRepoByCwd(cwd = process.cwd()) {
|
|
255
|
+
const identity = detectRepoIdentity(cwd);
|
|
256
|
+
const db = openDb();
|
|
257
|
+
const byKey = db
|
|
258
|
+
.prepare("SELECT * FROM repos WHERE repo_key = ?")
|
|
259
|
+
.get(identity.repoKey);
|
|
260
|
+
if (byKey)
|
|
261
|
+
return byKey;
|
|
262
|
+
const byExact = db
|
|
263
|
+
.prepare("SELECT * FROM repos WHERE root_path = ?")
|
|
264
|
+
.get(identity.rootPath);
|
|
265
|
+
if (byExact)
|
|
266
|
+
return byExact;
|
|
267
|
+
const want = normalizeFsPath(identity.rootPath);
|
|
268
|
+
const rows = db.prepare("SELECT * FROM repos").all();
|
|
269
|
+
return rows.find((r) => normalizeFsPath(r.root_path) === want) ?? null;
|
|
270
|
+
}
|
|
271
|
+
export function requireRepo(cwd = process.cwd()) {
|
|
272
|
+
const repo = getRepoByCwd(cwd);
|
|
273
|
+
if (!repo) {
|
|
274
|
+
throw new Error("No amem binding here. Run `amem init --platform cursor|claude` in a git repo, or `amem init --workspace <name>` for an app.");
|
|
275
|
+
}
|
|
276
|
+
return repo;
|
|
277
|
+
}
|
|
278
|
+
export function getMeta(key) {
|
|
279
|
+
const row = openDb()
|
|
280
|
+
.prepare(`SELECT value FROM amem_meta WHERE key = ?`)
|
|
281
|
+
.get(key);
|
|
282
|
+
return row?.value ?? null;
|
|
283
|
+
}
|
|
284
|
+
export function setMeta(key, value) {
|
|
285
|
+
openDb()
|
|
286
|
+
.prepare(`INSERT INTO amem_meta (key, value) VALUES (?, ?)
|
|
287
|
+
ON CONFLICT(key) DO UPDATE SET value = excluded.value`)
|
|
288
|
+
.run(key, value);
|
|
289
|
+
}
|
|
290
|
+
export function listClaims(repoId, opts = {}) {
|
|
291
|
+
if (opts.includeSuperseded) {
|
|
292
|
+
return openDb()
|
|
293
|
+
.prepare(`SELECT * FROM claims WHERE repo_id = ?
|
|
294
|
+
ORDER BY COALESCE(pinned, 0) DESC, updated_at DESC`)
|
|
295
|
+
.all(repoId);
|
|
296
|
+
}
|
|
297
|
+
return openDb()
|
|
298
|
+
.prepare(`SELECT * FROM claims WHERE repo_id = ?
|
|
299
|
+
AND COALESCE(status, 'active') = 'active'
|
|
300
|
+
ORDER BY COALESCE(pinned, 0) DESC, updated_at DESC`)
|
|
301
|
+
.all(repoId);
|
|
302
|
+
}
|
|
303
|
+
export function listClaimsAll(opts = {}) {
|
|
304
|
+
if (opts.includeSuperseded) {
|
|
305
|
+
return openDb()
|
|
306
|
+
.prepare(`SELECT * FROM claims
|
|
307
|
+
ORDER BY COALESCE(pinned, 0) DESC, updated_at DESC`)
|
|
308
|
+
.all();
|
|
309
|
+
}
|
|
310
|
+
return openDb()
|
|
311
|
+
.prepare(`SELECT * FROM claims
|
|
312
|
+
WHERE COALESCE(status, 'active') = 'active'
|
|
313
|
+
ORDER BY COALESCE(pinned, 0) DESC, updated_at DESC`)
|
|
314
|
+
.all();
|
|
315
|
+
}
|
|
316
|
+
export function getClaim(repoId, claimId) {
|
|
317
|
+
return (openDb()
|
|
318
|
+
.prepare("SELECT * FROM claims WHERE repo_id = ? AND id = ?")
|
|
319
|
+
.get(repoId, claimId) ?? null);
|
|
320
|
+
}
|
|
321
|
+
export function updateClaim(repoId, claimId, patch) {
|
|
322
|
+
const existing = getClaim(repoId, claimId);
|
|
323
|
+
if (!existing)
|
|
324
|
+
return null;
|
|
325
|
+
const ts = nowIso();
|
|
326
|
+
const text = patch.text !== undefined ? patch.text.trim() : existing.text;
|
|
327
|
+
const kind = patch.kind !== undefined ? patch.kind.trim() : existing.kind;
|
|
328
|
+
const anchors = patch.code_anchors !== undefined
|
|
329
|
+
? JSON.stringify(patch.code_anchors)
|
|
330
|
+
: existing.code_anchors;
|
|
331
|
+
const pinned = patch.pinned !== undefined ? (patch.pinned ? 1 : 0) : (existing.pinned ?? 0);
|
|
332
|
+
if (!text || !kind)
|
|
333
|
+
throw new Error("claim text and kind are required");
|
|
334
|
+
openDb()
|
|
335
|
+
.prepare(`UPDATE claims SET text = ?, kind = ?, code_anchors = ?, pinned = ?, updated_at = ?
|
|
336
|
+
WHERE repo_id = ? AND id = ?`)
|
|
337
|
+
.run(text, kind, anchors, pinned, ts, repoId, claimId);
|
|
338
|
+
const updated = getClaim(repoId, claimId);
|
|
339
|
+
if (updated) {
|
|
340
|
+
upsertClaimFts(openDb(), updated);
|
|
341
|
+
upsertClaimEmbed(openDb(), updated);
|
|
342
|
+
}
|
|
343
|
+
return updated;
|
|
344
|
+
}
|
|
345
|
+
export function setClaimPinned(repoId, claimId, pinned) {
|
|
346
|
+
return updateClaim(repoId, claimId, { pinned });
|
|
347
|
+
}
|
|
348
|
+
export function setClaimStatus(repoId, claimId, status) {
|
|
349
|
+
const existing = getClaim(repoId, claimId);
|
|
350
|
+
if (!existing)
|
|
351
|
+
return null;
|
|
352
|
+
const ts = nowIso();
|
|
353
|
+
openDb()
|
|
354
|
+
.prepare(`UPDATE claims SET status = ?, updated_at = ? WHERE repo_id = ? AND id = ?`)
|
|
355
|
+
.run(status, ts, repoId, claimId);
|
|
356
|
+
const updated = getClaim(repoId, claimId);
|
|
357
|
+
if (updated) {
|
|
358
|
+
upsertClaimFts(openDb(), updated);
|
|
359
|
+
if ((updated.status ?? "active") !== "active")
|
|
360
|
+
removeClaimEmbed(openDb(), repoId, claimId);
|
|
361
|
+
else
|
|
362
|
+
upsertClaimEmbed(openDb(), updated);
|
|
363
|
+
}
|
|
364
|
+
return updated;
|
|
365
|
+
}
|
|
366
|
+
export function deleteClaim(repoId, claimId) {
|
|
367
|
+
const db = openDb();
|
|
368
|
+
removeClaimFts(db, repoId, claimId);
|
|
369
|
+
removeClaimEmbed(db, repoId, claimId);
|
|
370
|
+
const result = db.prepare(`DELETE FROM claims WHERE repo_id = ? AND id = ?`).run(repoId, claimId);
|
|
371
|
+
return Number(result.changes ?? 0) > 0;
|
|
372
|
+
}
|
|
373
|
+
/** Reindex FTS + local embeddings for one repo (after propose apply / wipe helpers). */
|
|
374
|
+
export function reindexClaimsSearch(repoId) {
|
|
375
|
+
const db = openDb();
|
|
376
|
+
reindexRepoClaimsFts(db, repoId);
|
|
377
|
+
reindexRepoEmbeds(db, repoId);
|
|
378
|
+
}
|
|
379
|
+
export function listComponents(repoId) {
|
|
380
|
+
return openDb()
|
|
381
|
+
.prepare("SELECT * FROM components WHERE repo_id = ? ORDER BY name")
|
|
382
|
+
.all(repoId);
|
|
383
|
+
}
|
|
384
|
+
export function listComponentsAll() {
|
|
385
|
+
return openDb().prepare("SELECT * FROM components ORDER BY name").all();
|
|
386
|
+
}
|
|
387
|
+
export function listFlows(repoId) {
|
|
388
|
+
return openDb()
|
|
389
|
+
.prepare("SELECT * FROM flows WHERE repo_id = ? ORDER BY name")
|
|
390
|
+
.all(repoId);
|
|
391
|
+
}
|
|
392
|
+
export function listFlowsAll() {
|
|
393
|
+
return openDb().prepare("SELECT * FROM flows ORDER BY name").all();
|
|
394
|
+
}
|
|
395
|
+
export function listEdges(repoId) {
|
|
396
|
+
return openDb()
|
|
397
|
+
.prepare("SELECT * FROM edges WHERE repo_id = ?")
|
|
398
|
+
.all(repoId);
|
|
399
|
+
}
|
|
400
|
+
export function listEdgesAll() {
|
|
401
|
+
return openDb().prepare("SELECT * FROM edges").all();
|
|
402
|
+
}
|
|
403
|
+
export function wipeRepo(repoId) {
|
|
404
|
+
const db = openDb();
|
|
405
|
+
const claimIds = db
|
|
406
|
+
.prepare("SELECT id FROM claims WHERE repo_id = ?")
|
|
407
|
+
.all(repoId);
|
|
408
|
+
for (const c of claimIds) {
|
|
409
|
+
removeClaimFts(db, repoId, c.id);
|
|
410
|
+
removeClaimEmbed(db, repoId, c.id);
|
|
411
|
+
}
|
|
412
|
+
db.prepare("DELETE FROM repos WHERE id = ?").run(repoId);
|
|
413
|
+
}
|
|
414
|
+
/** Delete every bound repo (cascade clears claims/flows/usage). Returns count wiped. */
|
|
415
|
+
export function wipeAllRepos() {
|
|
416
|
+
const db = openDb();
|
|
417
|
+
const row = db.prepare("SELECT COUNT(*) AS c FROM repos").get();
|
|
418
|
+
const repos = db.prepare("SELECT id FROM repos").all();
|
|
419
|
+
for (const r of repos) {
|
|
420
|
+
const claimIds = db
|
|
421
|
+
.prepare("SELECT id FROM claims WHERE repo_id = ?")
|
|
422
|
+
.all(r.id);
|
|
423
|
+
for (const c of claimIds) {
|
|
424
|
+
removeClaimFts(db, r.id, c.id);
|
|
425
|
+
removeClaimEmbed(db, r.id, c.id);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
db.exec("DELETE FROM repos");
|
|
429
|
+
return row.c;
|
|
430
|
+
}
|
|
431
|
+
export function touchSession(platform, sessionId, repoId) {
|
|
432
|
+
openDb()
|
|
433
|
+
.prepare(`INSERT INTO agent_sessions (platform, session_id, repo_id, last_seen)
|
|
434
|
+
VALUES (?, ?, ?, ?)
|
|
435
|
+
ON CONFLICT(platform, session_id) DO UPDATE SET
|
|
436
|
+
repo_id = excluded.repo_id,
|
|
437
|
+
last_seen = excluded.last_seen`)
|
|
438
|
+
.run(platform, sessionId, repoId, nowIso());
|
|
439
|
+
}
|
|
440
|
+
export function insertUsageEvent(input) {
|
|
441
|
+
const id = newId("usage");
|
|
442
|
+
const ts = nowIso();
|
|
443
|
+
if (input.sessionId) {
|
|
444
|
+
touchSession(input.platform, input.sessionId, input.repoId);
|
|
445
|
+
}
|
|
446
|
+
openDb()
|
|
447
|
+
.prepare(`INSERT INTO usage_events (
|
|
448
|
+
id, repo_id, platform, session_id, query, claim_ids,
|
|
449
|
+
anchors_count, claims_count, packet_tokens, estimated_tokens_saved,
|
|
450
|
+
reported_tokens_saved, created_at, local_ms, estimated_ms_saved, kind
|
|
451
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, ?, ?, ?, ?)`)
|
|
452
|
+
.run(id, input.repoId, input.platform, input.sessionId ?? null, input.query, JSON.stringify(input.claimIds), input.anchorsCount, input.claimsCount, input.packetTokens, input.estimatedTokensSaved, ts, input.localMs ?? null, input.estimatedMsSaved ?? null, input.kind ?? null);
|
|
453
|
+
return openDb().prepare("SELECT * FROM usage_events WHERE id = ?").get(id);
|
|
454
|
+
}
|
|
455
|
+
export function listSessions(repoId) {
|
|
456
|
+
return openDb()
|
|
457
|
+
.prepare(`SELECT * FROM agent_sessions WHERE repo_id = ? ORDER BY last_seen DESC`)
|
|
458
|
+
.all(repoId);
|
|
459
|
+
}
|
|
460
|
+
export function listSessionsAll() {
|
|
461
|
+
return openDb()
|
|
462
|
+
.prepare(`SELECT * FROM agent_sessions ORDER BY last_seen DESC`)
|
|
463
|
+
.all();
|
|
464
|
+
}
|
|
465
|
+
export function setReportedTokensSaved(eventId, saved) {
|
|
466
|
+
const row = openDb()
|
|
467
|
+
.prepare("SELECT * FROM usage_events WHERE id = ?")
|
|
468
|
+
.get(eventId);
|
|
469
|
+
if (!row)
|
|
470
|
+
throw new Error(`Unknown usage event: ${eventId}`);
|
|
471
|
+
openDb()
|
|
472
|
+
.prepare("UPDATE usage_events SET reported_tokens_saved = ? WHERE id = ?")
|
|
473
|
+
.run(saved, eventId);
|
|
474
|
+
return openDb().prepare("SELECT * FROM usage_events WHERE id = ?").get(eventId);
|
|
475
|
+
}
|
|
476
|
+
export function setReportedOnLatest(repoId, platform, saved) {
|
|
477
|
+
const row = openDb()
|
|
478
|
+
.prepare(`SELECT * FROM usage_events WHERE repo_id = ? AND platform = ?
|
|
479
|
+
ORDER BY created_at DESC LIMIT 1`)
|
|
480
|
+
.get(repoId, platform);
|
|
481
|
+
if (!row)
|
|
482
|
+
throw new Error("No usage events to update for this platform");
|
|
483
|
+
return setReportedTokensSaved(row.id, saved);
|
|
484
|
+
}
|
|
485
|
+
export function listUsageEvents(opts) {
|
|
486
|
+
const days = opts.days ?? 30;
|
|
487
|
+
const since = new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString();
|
|
488
|
+
if (opts.repoId) {
|
|
489
|
+
return openDb()
|
|
490
|
+
.prepare(`SELECT * FROM usage_events WHERE repo_id = ? AND created_at >= ?
|
|
491
|
+
ORDER BY created_at DESC`)
|
|
492
|
+
.all(opts.repoId, since);
|
|
493
|
+
}
|
|
494
|
+
return openDb()
|
|
495
|
+
.prepare(`SELECT * FROM usage_events WHERE created_at >= ? ORDER BY created_at DESC`)
|
|
496
|
+
.all(since);
|
|
497
|
+
}
|
|
498
|
+
export function listRepos() {
|
|
499
|
+
return openDb()
|
|
500
|
+
.prepare("SELECT * FROM repos ORDER BY updated_at DESC")
|
|
501
|
+
.all();
|
|
502
|
+
}
|
|
503
|
+
export function getSetupState(repoId) {
|
|
504
|
+
return (openDb()
|
|
505
|
+
.prepare("SELECT * FROM setup_state WHERE repo_id = ?")
|
|
506
|
+
.get(repoId) ?? null);
|
|
507
|
+
}
|
|
508
|
+
export function upsertSetupState(repoId, platforms, completed) {
|
|
509
|
+
const ts = nowIso();
|
|
510
|
+
const existing = getSetupState(repoId);
|
|
511
|
+
const completedAt = completed
|
|
512
|
+
? (existing?.setup_completed_at ?? ts)
|
|
513
|
+
: existing?.setup_completed_at ?? null;
|
|
514
|
+
openDb()
|
|
515
|
+
.prepare(`INSERT INTO setup_state (repo_id, platforms, setup_completed_at, updated_at)
|
|
516
|
+
VALUES (?, ?, ?, ?)
|
|
517
|
+
ON CONFLICT(repo_id) DO UPDATE SET
|
|
518
|
+
platforms = excluded.platforms,
|
|
519
|
+
setup_completed_at = COALESCE(excluded.setup_completed_at, setup_state.setup_completed_at),
|
|
520
|
+
updated_at = excluded.updated_at`)
|
|
521
|
+
.run(repoId, JSON.stringify(platforms), completed ? completedAt ?? ts : completedAt, ts);
|
|
522
|
+
if (completed && !existing?.setup_completed_at) {
|
|
523
|
+
openDb()
|
|
524
|
+
.prepare("UPDATE setup_state SET setup_completed_at = ? WHERE repo_id = ?")
|
|
525
|
+
.run(ts, repoId);
|
|
526
|
+
}
|
|
527
|
+
return getSetupState(repoId);
|
|
528
|
+
}
|
|
529
|
+
export function getRepoById(id) {
|
|
530
|
+
return (openDb().prepare("SELECT * FROM repos WHERE id = ?").get(id) ??
|
|
531
|
+
null);
|
|
532
|
+
}
|
|
533
|
+
export function getRepoByName(name) {
|
|
534
|
+
const byId = getRepoById(name);
|
|
535
|
+
if (byId)
|
|
536
|
+
return byId;
|
|
537
|
+
const raw = name.trim();
|
|
538
|
+
if (!raw)
|
|
539
|
+
return null;
|
|
540
|
+
let slug;
|
|
541
|
+
try {
|
|
542
|
+
slug = slugifyWorkspace(raw);
|
|
543
|
+
}
|
|
544
|
+
catch {
|
|
545
|
+
return null;
|
|
546
|
+
}
|
|
547
|
+
const rows = listRepos();
|
|
548
|
+
const lower = raw.toLowerCase();
|
|
549
|
+
return (rows.find((r) => parseWorkspaceSlug(r.remote_url) === slug) ??
|
|
550
|
+
rows.find((r) => r.repo_name.toLowerCase() === lower) ??
|
|
551
|
+
rows.find((r) => {
|
|
552
|
+
try {
|
|
553
|
+
return slugifyWorkspace(r.repo_name) === slug;
|
|
554
|
+
}
|
|
555
|
+
catch {
|
|
556
|
+
return false;
|
|
557
|
+
}
|
|
558
|
+
}) ??
|
|
559
|
+
null);
|
|
560
|
+
}
|
|
561
|
+
/** Change a workspace display name. Keeps id, repo_key, and amem:// slug so memory stays bound. */
|
|
562
|
+
export function renameWorkspace(repoId, displayName) {
|
|
563
|
+
const repo = getRepoById(repoId);
|
|
564
|
+
if (!repo)
|
|
565
|
+
throw new Error("Workspace not found");
|
|
566
|
+
if (!parseWorkspaceSlug(repo.remote_url)) {
|
|
567
|
+
throw new Error("Only named workspaces can be renamed (git repos follow the folder / remote name)");
|
|
568
|
+
}
|
|
569
|
+
const label = displayName.trim().replace(/\s+/g, " ").slice(0, 80);
|
|
570
|
+
if (!label)
|
|
571
|
+
throw new Error("Name required");
|
|
572
|
+
slugifyWorkspace(label);
|
|
573
|
+
const ts = nowIso();
|
|
574
|
+
openDb()
|
|
575
|
+
.prepare("UPDATE repos SET repo_name = ?, updated_at = ? WHERE id = ?")
|
|
576
|
+
.run(label, ts, repoId);
|
|
577
|
+
return getRepoById(repoId);
|
|
578
|
+
}
|
|
579
|
+
export function insertConversationNote(input) {
|
|
580
|
+
const id = newId("note");
|
|
581
|
+
const ts = nowIso();
|
|
582
|
+
const text = input.text.trim().slice(0, 2000);
|
|
583
|
+
openDb()
|
|
584
|
+
.prepare(`INSERT INTO conversation_notes (id, repo_id, platform, session_id, role, text, created_at)
|
|
585
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`)
|
|
586
|
+
.run(id, input.repoId, input.platform, input.sessionId ?? null, input.role, text, ts);
|
|
587
|
+
pruneConversationNotes(input.repoId);
|
|
588
|
+
return openDb()
|
|
589
|
+
.prepare("SELECT * FROM conversation_notes WHERE id = ?")
|
|
590
|
+
.get(id);
|
|
591
|
+
}
|
|
592
|
+
function pruneConversationNotes(repoId) {
|
|
593
|
+
const cutoff = openDb()
|
|
594
|
+
.prepare(`SELECT created_at FROM conversation_notes WHERE repo_id = ?
|
|
595
|
+
ORDER BY created_at DESC LIMIT 1 OFFSET 100`)
|
|
596
|
+
.get(repoId);
|
|
597
|
+
if (!cutoff)
|
|
598
|
+
return;
|
|
599
|
+
openDb()
|
|
600
|
+
.prepare("DELETE FROM conversation_notes WHERE repo_id = ? AND created_at < ?")
|
|
601
|
+
.run(repoId, cutoff.created_at);
|
|
602
|
+
}
|
|
603
|
+
export function listConversationNotes(repoId, limit = 40) {
|
|
604
|
+
return openDb()
|
|
605
|
+
.prepare(`SELECT * FROM conversation_notes WHERE repo_id = ?
|
|
606
|
+
ORDER BY created_at DESC LIMIT ?`)
|
|
607
|
+
.all(repoId, limit);
|
|
608
|
+
}
|
|
609
|
+
export function insertProposalDraft(input) {
|
|
610
|
+
const id = newId("draft");
|
|
611
|
+
const ts = nowIso();
|
|
612
|
+
openDb()
|
|
613
|
+
.prepare(`INSERT INTO proposal_drafts (
|
|
614
|
+
id, repo_id, platform, session_id, title, proposal_json, status, source, created_at, updated_at
|
|
615
|
+
) VALUES (?, ?, ?, ?, ?, ?, 'pending', ?, ?, ?)`)
|
|
616
|
+
.run(id, input.repoId, input.platform, input.sessionId ?? null, input.title.slice(0, 200), JSON.stringify(input.proposal), input.source ?? "session-end", ts, ts);
|
|
617
|
+
return getProposalDraft(id);
|
|
618
|
+
}
|
|
619
|
+
export function getProposalDraft(id) {
|
|
620
|
+
return (openDb().prepare(`SELECT * FROM proposal_drafts WHERE id = ?`).get(id) ?? null);
|
|
621
|
+
}
|
|
622
|
+
export function listProposalDrafts(repoId, opts = {}) {
|
|
623
|
+
const limit = opts.limit ?? 40;
|
|
624
|
+
if (opts.status) {
|
|
625
|
+
return openDb()
|
|
626
|
+
.prepare(`SELECT * FROM proposal_drafts WHERE repo_id = ? AND status = ?
|
|
627
|
+
ORDER BY created_at DESC LIMIT ?`)
|
|
628
|
+
.all(repoId, opts.status, limit);
|
|
629
|
+
}
|
|
630
|
+
return openDb()
|
|
631
|
+
.prepare(`SELECT * FROM proposal_drafts WHERE repo_id = ?
|
|
632
|
+
ORDER BY created_at DESC LIMIT ?`)
|
|
633
|
+
.all(repoId, limit);
|
|
634
|
+
}
|
|
635
|
+
export function listProposalDraftsAll(opts = {}) {
|
|
636
|
+
const limit = opts.limit ?? 80;
|
|
637
|
+
if (opts.status) {
|
|
638
|
+
return openDb()
|
|
639
|
+
.prepare(`SELECT * FROM proposal_drafts WHERE status = ?
|
|
640
|
+
ORDER BY created_at DESC LIMIT ?`)
|
|
641
|
+
.all(opts.status, limit);
|
|
642
|
+
}
|
|
643
|
+
return openDb()
|
|
644
|
+
.prepare(`SELECT * FROM proposal_drafts ORDER BY created_at DESC LIMIT ?`)
|
|
645
|
+
.all(limit);
|
|
646
|
+
}
|
|
647
|
+
export function countProposalDrafts(repoId, status = "pending") {
|
|
648
|
+
const row = openDb()
|
|
649
|
+
.prepare(`SELECT COUNT(*) AS n FROM proposal_drafts WHERE repo_id = ? AND status = ?`)
|
|
650
|
+
.get(repoId, status);
|
|
651
|
+
return Number(row?.n || 0);
|
|
652
|
+
}
|
|
653
|
+
export function countProposalDraftsAll(status = "pending") {
|
|
654
|
+
const row = openDb()
|
|
655
|
+
.prepare(`SELECT COUNT(*) AS n FROM proposal_drafts WHERE status = ?`)
|
|
656
|
+
.get(status);
|
|
657
|
+
return Number(row?.n || 0);
|
|
658
|
+
}
|
|
659
|
+
export function setProposalDraftStatus(id, status) {
|
|
660
|
+
const ts = nowIso();
|
|
661
|
+
openDb()
|
|
662
|
+
.prepare(`UPDATE proposal_drafts SET status = ?, updated_at = ? WHERE id = ?`)
|
|
663
|
+
.run(status, ts, id);
|
|
664
|
+
return getProposalDraft(id);
|
|
665
|
+
}
|
|
666
|
+
export { nowIso };
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { type ProposalDraftRow } from "./db.js";
|
|
2
|
+
import { type ClaimConflict, type Proposal } from "./proposal.js";
|
|
3
|
+
export declare const REJECT_SCORE = 25;
|
|
4
|
+
export type QualityLabel = "high" | "medium" | "low" | "reject";
|
|
5
|
+
export type DraftQuality = {
|
|
6
|
+
score: number;
|
|
7
|
+
label: QualityLabel;
|
|
8
|
+
reasons: string[];
|
|
9
|
+
reject: boolean;
|
|
10
|
+
};
|
|
11
|
+
export type DecoratedDraft = ProposalDraftRow & {
|
|
12
|
+
quality: DraftQuality;
|
|
13
|
+
conflicts: ClaimConflict[];
|
|
14
|
+
};
|
|
15
|
+
/** Confidence 0–100 for a session-end / miss→learn proposal. */
|
|
16
|
+
export declare function scoreProposal(proposal: Proposal): DraftQuality;
|
|
17
|
+
export declare function parseDraftProposal(draft: ProposalDraftRow): Proposal;
|
|
18
|
+
export declare function decorateDraft(draft: ProposalDraftRow, existingActive?: import("./db.js").ClaimRow[]): DecoratedDraft;
|
|
19
|
+
export declare function decorateDrafts(drafts: ProposalDraftRow[]): DecoratedDraft[];
|