zam-core 0.3.7 → 0.3.12
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/.agent/skills/zam/SKILL.md +170 -7
- package/.agents/skills/zam/SKILL.md +170 -7
- package/.claude/skills/zam/SKILL.md +170 -7
- package/README.de.md +3 -3
- package/README.md +3 -3
- package/dist/cli/index.js +3918 -1502
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +572 -151
- package/dist/index.js +2372 -1076
- package/dist/index.js.map +1 -1
- package/package.json +9 -2
- package/templates/personal/beliefs/README.md +1 -1
- package/templates/personal/foundations/README.md +19 -0
package/dist/index.js
CHANGED
|
@@ -1,76 +1,84 @@
|
|
|
1
1
|
// src/kernel/analytics/stats.ts
|
|
2
|
-
function q(db, sql, ...params) {
|
|
3
|
-
return db.prepare(sql).get(...params);
|
|
2
|
+
async function q(db, sql, ...params) {
|
|
3
|
+
return await db.prepare(sql).get(...params);
|
|
4
4
|
}
|
|
5
|
-
function
|
|
5
|
+
async function count(db, sql, ...params) {
|
|
6
|
+
return (await q(db, sql, ...params)).n;
|
|
7
|
+
}
|
|
8
|
+
async function getUserStats(db, userId) {
|
|
9
|
+
const avgRow = await q(
|
|
10
|
+
db,
|
|
11
|
+
"SELECT AVG(stability) as v FROM cards WHERE user_id = ? AND reps > 0",
|
|
12
|
+
userId
|
|
13
|
+
);
|
|
14
|
+
const lastSessionRow = await db.prepare(
|
|
15
|
+
"SELECT started_at FROM sessions WHERE user_id = ? ORDER BY started_at DESC LIMIT 1"
|
|
16
|
+
).get(userId);
|
|
6
17
|
return {
|
|
7
18
|
userId,
|
|
8
|
-
totalTokens:
|
|
9
|
-
cardsInDeck:
|
|
10
|
-
|
|
19
|
+
totalTokens: await count(db, "SELECT COUNT(*) as n FROM tokens"),
|
|
20
|
+
cardsInDeck: await count(
|
|
21
|
+
db,
|
|
22
|
+
"SELECT COUNT(*) as n FROM cards WHERE user_id = ?",
|
|
23
|
+
userId
|
|
24
|
+
),
|
|
25
|
+
dueToday: await count(
|
|
11
26
|
db,
|
|
12
27
|
"SELECT COUNT(*) as n FROM cards WHERE user_id = ? AND blocked = 0 AND due_at <= datetime('now')",
|
|
13
28
|
userId
|
|
14
|
-
)
|
|
15
|
-
blocked:
|
|
29
|
+
),
|
|
30
|
+
blocked: await count(
|
|
16
31
|
db,
|
|
17
32
|
"SELECT COUNT(*) as n FROM cards WHERE user_id = ? AND blocked = 1",
|
|
18
33
|
userId
|
|
19
|
-
)
|
|
20
|
-
mature:
|
|
34
|
+
),
|
|
35
|
+
mature: await count(
|
|
21
36
|
db,
|
|
22
37
|
"SELECT COUNT(*) as n FROM cards WHERE user_id = ? AND reps >= 3 AND stability >= 21",
|
|
23
38
|
userId
|
|
24
|
-
)
|
|
25
|
-
avgStability: (
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
})(),
|
|
33
|
-
totalSessions: q(db, "SELECT COUNT(*) as n FROM sessions WHERE user_id = ?", userId).n,
|
|
34
|
-
lastSession: (() => {
|
|
35
|
-
const r = db.prepare(
|
|
36
|
-
"SELECT started_at FROM sessions WHERE user_id = ? ORDER BY started_at DESC LIMIT 1"
|
|
37
|
-
).get(userId);
|
|
38
|
-
return r?.started_at ?? null;
|
|
39
|
-
})()
|
|
39
|
+
),
|
|
40
|
+
avgStability: avgRow.v ? Math.round(avgRow.v * 100) / 100 : null,
|
|
41
|
+
totalSessions: await count(
|
|
42
|
+
db,
|
|
43
|
+
"SELECT COUNT(*) as n FROM sessions WHERE user_id = ?",
|
|
44
|
+
userId
|
|
45
|
+
),
|
|
46
|
+
lastSession: lastSessionRow?.started_at ?? null
|
|
40
47
|
};
|
|
41
48
|
}
|
|
42
|
-
function getDomainCompetence(db, userId) {
|
|
43
|
-
const domains = db.prepare(
|
|
49
|
+
async function getDomainCompetence(db, userId) {
|
|
50
|
+
const domains = await db.prepare(
|
|
44
51
|
`SELECT DISTINCT t.domain FROM cards c
|
|
45
52
|
JOIN tokens t ON t.id = c.token_id
|
|
46
53
|
WHERE c.user_id = ? AND t.domain != ''`
|
|
47
54
|
).all(userId);
|
|
48
|
-
|
|
49
|
-
|
|
55
|
+
const competences = [];
|
|
56
|
+
for (const d of domains) {
|
|
57
|
+
const total = await count(
|
|
50
58
|
db,
|
|
51
59
|
`SELECT COUNT(*) as n FROM cards c
|
|
52
60
|
JOIN tokens t ON t.id = c.token_id
|
|
53
61
|
WHERE c.user_id = ? AND t.domain = ?`,
|
|
54
62
|
userId,
|
|
55
63
|
d.domain
|
|
56
|
-
)
|
|
57
|
-
const mature =
|
|
64
|
+
);
|
|
65
|
+
const mature = await count(
|
|
58
66
|
db,
|
|
59
67
|
`SELECT COUNT(*) as n FROM cards c
|
|
60
68
|
JOIN tokens t ON t.id = c.token_id
|
|
61
69
|
WHERE c.user_id = ? AND t.domain = ? AND c.reps >= 3 AND c.stability >= 21`,
|
|
62
70
|
userId,
|
|
63
71
|
d.domain
|
|
64
|
-
)
|
|
65
|
-
const avgStab = q(
|
|
72
|
+
);
|
|
73
|
+
const avgStab = (await q(
|
|
66
74
|
db,
|
|
67
75
|
`SELECT AVG(c.stability) as v FROM cards c
|
|
68
76
|
JOIN tokens t ON t.id = c.token_id
|
|
69
77
|
WHERE c.user_id = ? AND t.domain = ? AND c.reps > 0`,
|
|
70
78
|
userId,
|
|
71
79
|
d.domain
|
|
72
|
-
).v ?? 0;
|
|
73
|
-
const reviews = q(
|
|
80
|
+
)).v ?? 0;
|
|
81
|
+
const reviews = await q(
|
|
74
82
|
db,
|
|
75
83
|
`SELECT COUNT(*) as total,
|
|
76
84
|
SUM(CASE WHEN rating >= 2 THEN 1 ELSE 0 END) as passed
|
|
@@ -88,19 +96,26 @@ function getDomainCompetence(db, userId) {
|
|
|
88
96
|
} else {
|
|
89
97
|
suggestedMode = "shadowing";
|
|
90
98
|
}
|
|
91
|
-
|
|
99
|
+
competences.push({
|
|
92
100
|
domain: d.domain,
|
|
93
101
|
totalCards: total,
|
|
94
102
|
matureCards: mature,
|
|
95
103
|
avgStability: Math.round(avgStab * 100) / 100,
|
|
96
104
|
retentionRate: Math.round(retentionRate * 1e3) / 1e3,
|
|
97
105
|
suggestedMode
|
|
98
|
-
};
|
|
99
|
-
}
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
return competences;
|
|
100
109
|
}
|
|
101
110
|
|
|
102
111
|
// src/kernel/credentials.ts
|
|
103
|
-
import {
|
|
112
|
+
import {
|
|
113
|
+
chmodSync,
|
|
114
|
+
existsSync,
|
|
115
|
+
mkdirSync,
|
|
116
|
+
readFileSync,
|
|
117
|
+
writeFileSync
|
|
118
|
+
} from "fs";
|
|
104
119
|
import { homedir } from "os";
|
|
105
120
|
import { dirname, join } from "path";
|
|
106
121
|
var DEFAULT_CREDENTIALS_PATH = join(homedir(), ".zam", "credentials.json");
|
|
@@ -116,22 +131,37 @@ function loadCredentials(path) {
|
|
|
116
131
|
function saveCredentials(creds, path) {
|
|
117
132
|
const p = path ?? DEFAULT_CREDENTIALS_PATH;
|
|
118
133
|
const dir = dirname(p);
|
|
134
|
+
let createdDirectory = false;
|
|
119
135
|
if (!existsSync(dir)) {
|
|
120
|
-
mkdirSync(dir, { recursive: true });
|
|
136
|
+
mkdirSync(dir, { recursive: true, mode: 448 });
|
|
137
|
+
createdDirectory = true;
|
|
138
|
+
}
|
|
139
|
+
if (process.platform !== "win32" && (path === void 0 || createdDirectory)) {
|
|
140
|
+
chmodSync(dir, 448);
|
|
121
141
|
}
|
|
122
142
|
writeFileSync(p, `${JSON.stringify(creds, null, 2)}
|
|
123
|
-
`,
|
|
143
|
+
`, {
|
|
144
|
+
encoding: "utf-8",
|
|
145
|
+
mode: 384
|
|
146
|
+
});
|
|
147
|
+
if (process.platform !== "win32") {
|
|
148
|
+
chmodSync(p, 384);
|
|
149
|
+
}
|
|
124
150
|
}
|
|
125
151
|
function getTursoCredentials(path) {
|
|
126
152
|
const creds = loadCredentials(path);
|
|
127
153
|
if (creds.turso?.url && creds.turso?.token) {
|
|
128
|
-
return {
|
|
154
|
+
return {
|
|
155
|
+
url: creds.turso.url,
|
|
156
|
+
token: creds.turso.token,
|
|
157
|
+
...creds.turso.mode ? { mode: creds.turso.mode } : {}
|
|
158
|
+
};
|
|
129
159
|
}
|
|
130
160
|
return null;
|
|
131
161
|
}
|
|
132
|
-
function setTursoCredentials(url, token, path) {
|
|
162
|
+
function setTursoCredentials(url, token, path, mode) {
|
|
133
163
|
const creds = loadCredentials(path);
|
|
134
|
-
creds.turso = { url, token };
|
|
164
|
+
creds.turso = { url, token, ...mode ? { mode } : {} };
|
|
135
165
|
saveCredentials(creds, path);
|
|
136
166
|
}
|
|
137
167
|
function clearTursoCredentials(path) {
|
|
@@ -222,7 +252,255 @@ import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as rea
|
|
|
222
252
|
import { createRequire } from "module";
|
|
223
253
|
import { homedir as homedir2 } from "os";
|
|
224
254
|
import { dirname as dirname2, join as join2 } from "path";
|
|
225
|
-
|
|
255
|
+
|
|
256
|
+
// src/kernel/db/remote/hrana.ts
|
|
257
|
+
var DEFAULT_TIMEOUT_MS = 15e3;
|
|
258
|
+
var DEFAULT_MAX_ATTEMPTS = 2;
|
|
259
|
+
function toHttpUrl(url) {
|
|
260
|
+
return url.replace(/^libsql:\/\//i, "https://").replace(/^wss:\/\//i, "https://").replace(/^ws:\/\//i, "http://").replace(/\/+$/, "");
|
|
261
|
+
}
|
|
262
|
+
function encodeValue(param) {
|
|
263
|
+
if (param === null) return { type: "null" };
|
|
264
|
+
if (typeof param === "string") return { type: "text", value: param };
|
|
265
|
+
if (typeof param === "bigint") {
|
|
266
|
+
return { type: "integer", value: param.toString() };
|
|
267
|
+
}
|
|
268
|
+
if (typeof param === "number") {
|
|
269
|
+
if (Number.isSafeInteger(param)) {
|
|
270
|
+
return { type: "integer", value: param.toString() };
|
|
271
|
+
}
|
|
272
|
+
return { type: "float", value: param };
|
|
273
|
+
}
|
|
274
|
+
if (param instanceof Uint8Array) {
|
|
275
|
+
return { type: "blob", base64: Buffer.from(param).toString("base64") };
|
|
276
|
+
}
|
|
277
|
+
throw new TypeError(
|
|
278
|
+
`Cannot bind a value of type ${typeof param} to a SQL parameter`
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
function decodeValue(value) {
|
|
282
|
+
switch (value.type) {
|
|
283
|
+
case "null":
|
|
284
|
+
return null;
|
|
285
|
+
case "integer":
|
|
286
|
+
return Number(value.value);
|
|
287
|
+
case "float":
|
|
288
|
+
return value.value;
|
|
289
|
+
case "text":
|
|
290
|
+
return value.value;
|
|
291
|
+
case "blob":
|
|
292
|
+
return new Uint8Array(Buffer.from(value.base64, "base64"));
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
function rowsToObjects(result) {
|
|
296
|
+
return result.rows.map((row) => {
|
|
297
|
+
const obj = {};
|
|
298
|
+
row.forEach((value, i) => {
|
|
299
|
+
obj[result.cols[i]?.name ?? `col${i}`] = decodeValue(value);
|
|
300
|
+
});
|
|
301
|
+
return obj;
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
function isRetryableTransportError(err) {
|
|
305
|
+
if (!(err instanceof Error) || err.name === "HranaResponseError") {
|
|
306
|
+
return false;
|
|
307
|
+
}
|
|
308
|
+
const code = err.cause?.code;
|
|
309
|
+
return code === "ECONNREFUSED" || code === "ENOTFOUND" || code === "EAI_AGAIN";
|
|
310
|
+
}
|
|
311
|
+
var HranaResponseError = class extends Error {
|
|
312
|
+
name = "HranaResponseError";
|
|
313
|
+
};
|
|
314
|
+
var HranaTransport = class {
|
|
315
|
+
pipelineUrl;
|
|
316
|
+
authToken;
|
|
317
|
+
timeoutMs;
|
|
318
|
+
maxAttempts;
|
|
319
|
+
constructor(options) {
|
|
320
|
+
this.pipelineUrl = `${toHttpUrl(options.url)}/v3/pipeline`;
|
|
321
|
+
this.authToken = options.authToken;
|
|
322
|
+
this.timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
323
|
+
this.maxAttempts = options.maxAttempts ?? DEFAULT_MAX_ATTEMPTS;
|
|
324
|
+
}
|
|
325
|
+
/**
|
|
326
|
+
* POST one pipeline of requests. Returns the server results plus the baton
|
|
327
|
+
* for continuing an open stream. Retries transport-level failures only for
|
|
328
|
+
* stateless pipelines (no baton involved on either side).
|
|
329
|
+
*/
|
|
330
|
+
async pipeline(requests, baton, baseUrl) {
|
|
331
|
+
const url = baseUrl ? `${toHttpUrl(baseUrl)}/v3/pipeline` : this.pipelineUrl;
|
|
332
|
+
const keepsState = baton != null || !requests.some((r) => r.type === "close");
|
|
333
|
+
const attempts = keepsState ? 1 : this.maxAttempts;
|
|
334
|
+
let lastError;
|
|
335
|
+
for (let attempt = 1; attempt <= attempts; attempt++) {
|
|
336
|
+
try {
|
|
337
|
+
return await this.post(url, { baton: baton ?? null, requests });
|
|
338
|
+
} catch (err) {
|
|
339
|
+
lastError = err;
|
|
340
|
+
if (!isRetryableTransportError(err) || attempt === attempts) {
|
|
341
|
+
throw this.offline(err);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
throw this.offline(lastError);
|
|
346
|
+
}
|
|
347
|
+
async post(url, body) {
|
|
348
|
+
const controller = new AbortController();
|
|
349
|
+
const timer = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
350
|
+
let response;
|
|
351
|
+
try {
|
|
352
|
+
response = await fetch(url, {
|
|
353
|
+
method: "POST",
|
|
354
|
+
headers: {
|
|
355
|
+
"content-type": "application/json",
|
|
356
|
+
...this.authToken ? { authorization: `Bearer ${this.authToken}` } : {}
|
|
357
|
+
},
|
|
358
|
+
body: JSON.stringify(body),
|
|
359
|
+
signal: controller.signal
|
|
360
|
+
});
|
|
361
|
+
} finally {
|
|
362
|
+
clearTimeout(timer);
|
|
363
|
+
}
|
|
364
|
+
if (response.status === 401 || response.status === 403) {
|
|
365
|
+
throw new HranaResponseError(
|
|
366
|
+
`Turso rejected the configured credentials (HTTP ${response.status}). Refresh the token with: zam connector setup turso`
|
|
367
|
+
);
|
|
368
|
+
}
|
|
369
|
+
if (!response.ok) {
|
|
370
|
+
const detail = await response.text().catch(() => "");
|
|
371
|
+
throw new HranaResponseError(
|
|
372
|
+
`Turso request failed with HTTP ${response.status}${detail ? `: ${detail.slice(0, 200)}` : ""}`
|
|
373
|
+
);
|
|
374
|
+
}
|
|
375
|
+
return await response.json();
|
|
376
|
+
}
|
|
377
|
+
offline(err) {
|
|
378
|
+
if (err instanceof HranaResponseError) return err;
|
|
379
|
+
const cause = err instanceof Error ? err.cause?.message ?? err.message : String(err);
|
|
380
|
+
return new HranaResponseError(
|
|
381
|
+
`Cannot reach the Turso database at ${this.pipelineUrl}: ${cause}. Check your network connection, or switch to the local provider (ZAM_DB_PROVIDER=local).`
|
|
382
|
+
);
|
|
383
|
+
}
|
|
384
|
+
};
|
|
385
|
+
function unwrapResult(response, index) {
|
|
386
|
+
const entry = response.results[index];
|
|
387
|
+
if (!entry) {
|
|
388
|
+
throw new HranaResponseError(`Turso response is missing result #${index}`);
|
|
389
|
+
}
|
|
390
|
+
if (entry.type === "error") {
|
|
391
|
+
throw new Error(entry.error.message);
|
|
392
|
+
}
|
|
393
|
+
return entry.response.result;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// src/kernel/db/remote/provider.ts
|
|
397
|
+
var TABLE_INFO_PRAGMA = /^\s*table_info\s*\(\s*['"]?(\w+)['"]?\s*\)\s*$/i;
|
|
398
|
+
function toRunResult(result) {
|
|
399
|
+
return {
|
|
400
|
+
changes: result?.affected_row_count ?? 0,
|
|
401
|
+
lastInsertRowid: result?.last_insert_rowid != null ? Number(result.last_insert_rowid) : 0
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
function makeStatement(sql, run) {
|
|
405
|
+
const execute = async (params, wantRows) => {
|
|
406
|
+
const { results } = await run([
|
|
407
|
+
{
|
|
408
|
+
type: "execute",
|
|
409
|
+
stmt: { sql, args: params.map(encodeValue), want_rows: wantRows }
|
|
410
|
+
}
|
|
411
|
+
]);
|
|
412
|
+
return results[0];
|
|
413
|
+
};
|
|
414
|
+
return {
|
|
415
|
+
async run(...params) {
|
|
416
|
+
return toRunResult(await execute(params, false));
|
|
417
|
+
},
|
|
418
|
+
async get(...params) {
|
|
419
|
+
const result = await execute(params, true);
|
|
420
|
+
return result ? rowsToObjects(result)[0] : void 0;
|
|
421
|
+
},
|
|
422
|
+
async all(...params) {
|
|
423
|
+
const result = await execute(params, true);
|
|
424
|
+
return result ? rowsToObjects(result) : [];
|
|
425
|
+
}
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
function makeDatabase(run, transport) {
|
|
429
|
+
let txTail = Promise.resolve();
|
|
430
|
+
const db = {
|
|
431
|
+
prepare(sql) {
|
|
432
|
+
return makeStatement(sql, run);
|
|
433
|
+
},
|
|
434
|
+
async exec(sql) {
|
|
435
|
+
await run([{ type: "sequence", sql }]);
|
|
436
|
+
},
|
|
437
|
+
async pragma(source) {
|
|
438
|
+
const tableInfo = TABLE_INFO_PRAGMA.exec(source);
|
|
439
|
+
const sql = tableInfo ? `SELECT * FROM pragma_table_info('${tableInfo[1]}')` : `PRAGMA ${source}`;
|
|
440
|
+
return db.prepare(sql).all();
|
|
441
|
+
},
|
|
442
|
+
transaction(fn) {
|
|
443
|
+
const next = txTail.then(async () => {
|
|
444
|
+
const stream = openStream(transport);
|
|
445
|
+
try {
|
|
446
|
+
await stream.run([
|
|
447
|
+
{
|
|
448
|
+
type: "execute",
|
|
449
|
+
stmt: { sql: "BEGIN IMMEDIATE", want_rows: false }
|
|
450
|
+
}
|
|
451
|
+
]);
|
|
452
|
+
const result = await fn(makeDatabase(stream.run, transport));
|
|
453
|
+
await stream.run(
|
|
454
|
+
[{ type: "execute", stmt: { sql: "COMMIT", want_rows: false } }],
|
|
455
|
+
true
|
|
456
|
+
);
|
|
457
|
+
return result;
|
|
458
|
+
} catch (err) {
|
|
459
|
+
await stream.run(
|
|
460
|
+
[
|
|
461
|
+
{
|
|
462
|
+
type: "execute",
|
|
463
|
+
stmt: { sql: "ROLLBACK", want_rows: false }
|
|
464
|
+
}
|
|
465
|
+
],
|
|
466
|
+
true
|
|
467
|
+
).catch(() => {
|
|
468
|
+
});
|
|
469
|
+
throw err;
|
|
470
|
+
}
|
|
471
|
+
});
|
|
472
|
+
txTail = next.catch(() => {
|
|
473
|
+
});
|
|
474
|
+
return next;
|
|
475
|
+
},
|
|
476
|
+
async close() {
|
|
477
|
+
}
|
|
478
|
+
};
|
|
479
|
+
return db;
|
|
480
|
+
}
|
|
481
|
+
function openStream(transport) {
|
|
482
|
+
let baton = null;
|
|
483
|
+
let baseUrl = null;
|
|
484
|
+
return {
|
|
485
|
+
async run(requests, close = false) {
|
|
486
|
+
const sent = close ? [...requests, { type: "close" }] : requests;
|
|
487
|
+
const response = await transport.pipeline(sent, baton, baseUrl);
|
|
488
|
+
baton = response.baton;
|
|
489
|
+
baseUrl = response.base_url ?? baseUrl;
|
|
490
|
+
return {
|
|
491
|
+
results: requests.map((_, i) => unwrapResult(response, i))
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
function openRemoteDatabase(options) {
|
|
497
|
+
const transport = new HranaTransport(options);
|
|
498
|
+
const statelessRun = async (requests) => {
|
|
499
|
+
const response = await transport.pipeline([...requests, { type: "close" }]);
|
|
500
|
+
return { results: requests.map((_, i) => unwrapResult(response, i)) };
|
|
501
|
+
};
|
|
502
|
+
return makeDatabase(statelessRun, transport);
|
|
503
|
+
}
|
|
226
504
|
|
|
227
505
|
// src/kernel/db/schema.ts
|
|
228
506
|
var SCHEMA = `
|
|
@@ -303,6 +581,22 @@ CREATE TABLE IF NOT EXISTS session_steps (
|
|
|
303
581
|
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
304
582
|
);
|
|
305
583
|
|
|
584
|
+
-- Confirmed ratings synthesized from monitor evidence.
|
|
585
|
+
-- The composite primary key makes repeated synthesis idempotent per token.
|
|
586
|
+
CREATE TABLE IF NOT EXISTS session_syntheses (
|
|
587
|
+
session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
|
|
588
|
+
token_id TEXT NOT NULL REFERENCES tokens(id) ON DELETE CASCADE,
|
|
589
|
+
card_id TEXT NOT NULL REFERENCES cards(id) ON DELETE CASCADE,
|
|
590
|
+
inferred_rating INTEGER NOT NULL CHECK (inferred_rating BETWEEN 1 AND 4),
|
|
591
|
+
confirmed_rating INTEGER NOT NULL CHECK (confirmed_rating BETWEEN 1 AND 4),
|
|
592
|
+
confidence TEXT NOT NULL CHECK (confidence IN ('medium', 'high')),
|
|
593
|
+
evidence TEXT NOT NULL DEFAULT '{}',
|
|
594
|
+
review_log_id TEXT NOT NULL REFERENCES review_logs(id) ON DELETE CASCADE,
|
|
595
|
+
session_step_id TEXT NOT NULL REFERENCES session_steps(id) ON DELETE CASCADE,
|
|
596
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
597
|
+
PRIMARY KEY (session_id, token_id)
|
|
598
|
+
);
|
|
599
|
+
|
|
306
600
|
-- User configuration
|
|
307
601
|
CREATE TABLE IF NOT EXISTS user_config (
|
|
308
602
|
key TEXT PRIMARY KEY,
|
|
@@ -335,14 +629,70 @@ CREATE INDEX IF NOT EXISTS idx_review_logs_user ON review_logs(user_id, reviewed
|
|
|
335
629
|
CREATE INDEX IF NOT EXISTS idx_session_steps_session ON session_steps(session_id);
|
|
336
630
|
`;
|
|
337
631
|
|
|
632
|
+
// src/kernel/db/sync-adapter.ts
|
|
633
|
+
function wrapSyncDatabase(driver) {
|
|
634
|
+
let txTail = Promise.resolve();
|
|
635
|
+
const db = {
|
|
636
|
+
prepare(sql) {
|
|
637
|
+
return {
|
|
638
|
+
async run(...params) {
|
|
639
|
+
return driver.prepare(sql).run(...params);
|
|
640
|
+
},
|
|
641
|
+
async get(...params) {
|
|
642
|
+
return driver.prepare(sql).get(...params);
|
|
643
|
+
},
|
|
644
|
+
async all(...params) {
|
|
645
|
+
return driver.prepare(sql).all(...params);
|
|
646
|
+
}
|
|
647
|
+
};
|
|
648
|
+
},
|
|
649
|
+
async exec(sql) {
|
|
650
|
+
driver.exec(sql);
|
|
651
|
+
},
|
|
652
|
+
async pragma(source) {
|
|
653
|
+
return driver.pragma(source);
|
|
654
|
+
},
|
|
655
|
+
transaction(fn) {
|
|
656
|
+
const run = txTail.then(async () => {
|
|
657
|
+
driver.exec("BEGIN IMMEDIATE");
|
|
658
|
+
try {
|
|
659
|
+
const result = await fn(db);
|
|
660
|
+
driver.exec("COMMIT");
|
|
661
|
+
return result;
|
|
662
|
+
} catch (err) {
|
|
663
|
+
driver.exec("ROLLBACK");
|
|
664
|
+
throw err;
|
|
665
|
+
}
|
|
666
|
+
});
|
|
667
|
+
txTail = run.catch(() => {
|
|
668
|
+
});
|
|
669
|
+
return run;
|
|
670
|
+
},
|
|
671
|
+
...driver.sync ? {
|
|
672
|
+
async sync() {
|
|
673
|
+
driver.sync?.();
|
|
674
|
+
}
|
|
675
|
+
} : {},
|
|
676
|
+
async close() {
|
|
677
|
+
driver.close();
|
|
678
|
+
}
|
|
679
|
+
};
|
|
680
|
+
return db;
|
|
681
|
+
}
|
|
682
|
+
|
|
338
683
|
// src/kernel/db/connection.ts
|
|
339
684
|
var DEFAULT_DB_DIR = join2(homedir2(), ".zam");
|
|
340
685
|
var DEFAULT_DB_PATH = join2(DEFAULT_DB_DIR, "zam.db");
|
|
341
686
|
var require2 = createRequire(import.meta.url);
|
|
342
687
|
function isRemoteDatabasePath(dbPath) {
|
|
343
|
-
return /^(libsql|https?):\/\//i.test(dbPath);
|
|
688
|
+
return /^(libsql|https?|wss?):\/\//i.test(dbPath);
|
|
689
|
+
}
|
|
690
|
+
function isDatabaseProvider(value) {
|
|
691
|
+
return value === "local" || value === "native" || value === "remote";
|
|
344
692
|
}
|
|
345
693
|
function openLocalSqlite(dbPath) {
|
|
694
|
+
const mod = require2("better-sqlite3");
|
|
695
|
+
const BetterSqlite3 = "default" in mod ? mod.default : mod;
|
|
346
696
|
return new BetterSqlite3(dbPath);
|
|
347
697
|
}
|
|
348
698
|
function loadLibsql() {
|
|
@@ -352,11 +702,11 @@ function loadLibsql() {
|
|
|
352
702
|
} catch (err) {
|
|
353
703
|
const detail = err instanceof Error ? ` ${err.message}` : "";
|
|
354
704
|
throw new Error(
|
|
355
|
-
`Turso sync requires the optional native libsql backend, which is not available for ${process.platform}/${process.arch}.${detail}`
|
|
705
|
+
`Turso sync requires the optional native libsql backend, which is not available for ${process.platform}/${process.arch}. Switch to the HTTP provider instead: zam connector setup turso --mode remote (or set ZAM_DB_PROVIDER=remote).${detail}`
|
|
356
706
|
);
|
|
357
707
|
}
|
|
358
708
|
}
|
|
359
|
-
function openDatabase(options = {}) {
|
|
709
|
+
async function openDatabase(options = {}) {
|
|
360
710
|
const configuredCloud = options.useConfiguredCloud !== false && !options.dbPath && !options.syncUrl ? getTursoCredentials() : null;
|
|
361
711
|
let requiresTurso = false;
|
|
362
712
|
try {
|
|
@@ -377,7 +727,26 @@ function openDatabase(options = {}) {
|
|
|
377
727
|
const dbPath = configuredCloud?.url ?? options.dbPath ?? DEFAULT_DB_PATH;
|
|
378
728
|
const isRemote = isRemoteDatabasePath(dbPath);
|
|
379
729
|
const isEmbeddedReplica = Boolean(options.syncUrl);
|
|
380
|
-
|
|
730
|
+
const provider = resolveProvider(options, configuredCloud?.mode, isRemote);
|
|
731
|
+
const shouldInitialize = options.initialize === true || !isRemote && !isEmbeddedReplica && !existsSync2(dbPath);
|
|
732
|
+
if (provider === "remote") {
|
|
733
|
+
const url = isRemote ? dbPath : options.syncUrl;
|
|
734
|
+
if (!url) {
|
|
735
|
+
throw new Error(
|
|
736
|
+
"The remote database provider is selected but no Turso URL is configured. Run: zam connector setup turso"
|
|
737
|
+
);
|
|
738
|
+
}
|
|
739
|
+
const db2 = openRemoteDatabase({
|
|
740
|
+
url,
|
|
741
|
+
authToken: configuredCloud?.token ?? options.authToken
|
|
742
|
+
});
|
|
743
|
+
if (options.initialize) {
|
|
744
|
+
await db2.exec(SCHEMA);
|
|
745
|
+
}
|
|
746
|
+
await runMigrations(db2);
|
|
747
|
+
return db2;
|
|
748
|
+
}
|
|
749
|
+
if (shouldInitialize && !isRemote) {
|
|
381
750
|
const dir = dirname2(dbPath);
|
|
382
751
|
if (!existsSync2(dir)) {
|
|
383
752
|
mkdirSync2(dir, { recursive: true });
|
|
@@ -402,66 +771,93 @@ function openDatabase(options = {}) {
|
|
|
402
771
|
if (authToken) {
|
|
403
772
|
dbOpts.authToken = authToken;
|
|
404
773
|
}
|
|
405
|
-
let
|
|
774
|
+
let driver;
|
|
406
775
|
if (isRemote || isEmbeddedReplica) {
|
|
407
|
-
const LibsqlDatabase = loadLibsql();
|
|
408
776
|
try {
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
const
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
777
|
+
const LibsqlDatabase = loadLibsql();
|
|
778
|
+
try {
|
|
779
|
+
driver = new LibsqlDatabase(dbPath, dbOpts);
|
|
780
|
+
} catch (err) {
|
|
781
|
+
const msg = err.message;
|
|
782
|
+
if (msg.includes("InvalidLocalState") && options.syncUrl) {
|
|
783
|
+
const metaPath = `${dbPath}.meta`;
|
|
784
|
+
const infoPath = `${dbPath}-info`;
|
|
785
|
+
if (existsSync2(metaPath)) rmSync(metaPath);
|
|
786
|
+
if (existsSync2(infoPath)) rmSync(infoPath);
|
|
787
|
+
driver = new LibsqlDatabase(dbPath, dbOpts);
|
|
788
|
+
} else {
|
|
789
|
+
throw err;
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
} catch (nativeErr) {
|
|
793
|
+
const fallbackUrl = isRemote ? dbPath : options.syncUrl;
|
|
794
|
+
if (isRemote && !isEmbeddedReplica && fallbackUrl) {
|
|
795
|
+
const db2 = openRemoteDatabase({
|
|
796
|
+
url: fallbackUrl,
|
|
797
|
+
authToken: configuredCloud?.token ?? options.authToken
|
|
798
|
+
});
|
|
799
|
+
if (options.initialize) {
|
|
800
|
+
await db2.exec(SCHEMA);
|
|
801
|
+
}
|
|
802
|
+
await runMigrations(db2);
|
|
803
|
+
return db2;
|
|
420
804
|
}
|
|
805
|
+
throw nativeErr;
|
|
421
806
|
}
|
|
422
807
|
} else {
|
|
423
|
-
|
|
808
|
+
driver = openLocalSqlite(dbPath);
|
|
424
809
|
}
|
|
425
810
|
if (!isRemote && !isEmbeddedReplica) {
|
|
426
|
-
|
|
811
|
+
driver.pragma("journal_mode = WAL");
|
|
427
812
|
}
|
|
428
|
-
|
|
813
|
+
driver.pragma("foreign_keys = ON");
|
|
429
814
|
if (!isRemote) {
|
|
430
|
-
|
|
815
|
+
driver.pragma("busy_timeout = 5000");
|
|
431
816
|
}
|
|
817
|
+
const db = wrapSyncDatabase(driver);
|
|
432
818
|
if (isEmbeddedReplica) {
|
|
433
|
-
db.sync?.();
|
|
819
|
+
await db.sync?.();
|
|
434
820
|
}
|
|
435
|
-
if (
|
|
436
|
-
db.exec(SCHEMA);
|
|
821
|
+
if (shouldInitialize) {
|
|
822
|
+
await db.exec(SCHEMA);
|
|
437
823
|
}
|
|
438
|
-
runMigrations(db);
|
|
824
|
+
await runMigrations(db);
|
|
439
825
|
return db;
|
|
440
826
|
}
|
|
441
|
-
function
|
|
827
|
+
function resolveProvider(options, credentialsMode, isRemote) {
|
|
828
|
+
if (options.provider) return options.provider;
|
|
829
|
+
const env = process.env.ZAM_DB_PROVIDER;
|
|
830
|
+
if (isDatabaseProvider(env)) return env;
|
|
831
|
+
if (isDatabaseProvider(credentialsMode) && (isRemote || options.syncUrl)) {
|
|
832
|
+
return credentialsMode;
|
|
833
|
+
}
|
|
834
|
+
if (isRemote || options.syncUrl) return "native";
|
|
835
|
+
return "local";
|
|
836
|
+
}
|
|
837
|
+
async function openDatabaseWithSync(options = {}) {
|
|
442
838
|
return openDatabase(options);
|
|
443
839
|
}
|
|
444
840
|
function getDefaultDbPath() {
|
|
445
841
|
return DEFAULT_DB_PATH;
|
|
446
842
|
}
|
|
447
|
-
function runMigrations(db) {
|
|
448
|
-
const sessionCols = db.pragma("table_info(sessions)");
|
|
843
|
+
async function runMigrations(db) {
|
|
844
|
+
const sessionCols = await db.pragma("table_info(sessions)");
|
|
449
845
|
if (sessionCols.length > 0 && !sessionCols.some((c) => c.name === "execution_context")) {
|
|
450
|
-
db.exec(
|
|
846
|
+
await db.exec(
|
|
451
847
|
`ALTER TABLE sessions ADD COLUMN execution_context TEXT NOT NULL DEFAULT 'shell'`
|
|
452
848
|
);
|
|
453
849
|
}
|
|
454
|
-
const tokenCols = db.pragma("table_info(tokens)");
|
|
850
|
+
const tokenCols = await db.pragma("table_info(tokens)");
|
|
455
851
|
if (tokenCols.length > 0 && !tokenCols.some((c) => c.name === "deprecated_at")) {
|
|
456
|
-
db.exec(`ALTER TABLE tokens ADD COLUMN deprecated_at TEXT`);
|
|
852
|
+
await db.exec(`ALTER TABLE tokens ADD COLUMN deprecated_at TEXT`);
|
|
457
853
|
}
|
|
458
854
|
if (tokenCols.length > 0 && !tokenCols.some((c) => c.name === "source_link")) {
|
|
459
|
-
db.exec(`ALTER TABLE tokens ADD COLUMN source_link TEXT`);
|
|
855
|
+
await db.exec(`ALTER TABLE tokens ADD COLUMN source_link TEXT`);
|
|
460
856
|
}
|
|
461
857
|
if (tokenCols.length > 0 && !tokenCols.some((c) => c.name === "question")) {
|
|
462
|
-
db.exec(`ALTER TABLE tokens ADD COLUMN question TEXT`);
|
|
858
|
+
await db.exec(`ALTER TABLE tokens ADD COLUMN question TEXT`);
|
|
463
859
|
}
|
|
464
|
-
db.exec(`
|
|
860
|
+
await db.exec(`
|
|
465
861
|
CREATE TABLE IF NOT EXISTS agent_skills (
|
|
466
862
|
id TEXT PRIMARY KEY,
|
|
467
863
|
slug TEXT NOT NULL UNIQUE,
|
|
@@ -473,6 +869,165 @@ function runMigrations(db) {
|
|
|
473
869
|
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
474
870
|
)
|
|
475
871
|
`);
|
|
872
|
+
await db.exec(`
|
|
873
|
+
CREATE TABLE IF NOT EXISTS session_syntheses (
|
|
874
|
+
session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
|
|
875
|
+
token_id TEXT NOT NULL REFERENCES tokens(id) ON DELETE CASCADE,
|
|
876
|
+
card_id TEXT NOT NULL REFERENCES cards(id) ON DELETE CASCADE,
|
|
877
|
+
inferred_rating INTEGER NOT NULL CHECK (inferred_rating BETWEEN 1 AND 4),
|
|
878
|
+
confirmed_rating INTEGER NOT NULL CHECK (confirmed_rating BETWEEN 1 AND 4),
|
|
879
|
+
confidence TEXT NOT NULL CHECK (confidence IN ('medium', 'high')),
|
|
880
|
+
evidence TEXT NOT NULL DEFAULT '{}',
|
|
881
|
+
review_log_id TEXT NOT NULL REFERENCES review_logs(id) ON DELETE CASCADE,
|
|
882
|
+
session_step_id TEXT NOT NULL REFERENCES session_steps(id) ON DELETE CASCADE,
|
|
883
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
884
|
+
PRIMARY KEY (session_id, token_id)
|
|
885
|
+
)
|
|
886
|
+
`);
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
// src/kernel/db/snapshot.ts
|
|
890
|
+
import { createHash } from "crypto";
|
|
891
|
+
var SNAPSHOT_FORMAT = "zam-snapshot";
|
|
892
|
+
var SNAPSHOT_VERSION = 1;
|
|
893
|
+
var MANIFEST_PREFIX = "-- zam-snapshot: ";
|
|
894
|
+
var SNAPSHOT_TABLES = [
|
|
895
|
+
"tokens",
|
|
896
|
+
"sessions",
|
|
897
|
+
"cards",
|
|
898
|
+
"prerequisites",
|
|
899
|
+
"session_steps",
|
|
900
|
+
"review_logs",
|
|
901
|
+
"session_syntheses",
|
|
902
|
+
"user_config",
|
|
903
|
+
"agent_skills"
|
|
904
|
+
];
|
|
905
|
+
function quoteValue(value) {
|
|
906
|
+
if (value === null || value === void 0) return "NULL";
|
|
907
|
+
if (typeof value === "number") {
|
|
908
|
+
return Number.isFinite(value) ? String(value) : "NULL";
|
|
909
|
+
}
|
|
910
|
+
if (typeof value === "bigint") return value.toString();
|
|
911
|
+
if (typeof value === "string") return `'${value.replace(/'/g, "''")}'`;
|
|
912
|
+
if (value instanceof Uint8Array) {
|
|
913
|
+
let hex = "";
|
|
914
|
+
for (const byte of value) hex += byte.toString(16).padStart(2, "0");
|
|
915
|
+
return `X'${hex}'`;
|
|
916
|
+
}
|
|
917
|
+
throw new Error(`Cannot serialize value of type ${typeof value} to SQL`);
|
|
918
|
+
}
|
|
919
|
+
async function getColumns(db, table) {
|
|
920
|
+
const cols = await db.pragma(`table_info(${table})`);
|
|
921
|
+
return cols.map((c) => c.name);
|
|
922
|
+
}
|
|
923
|
+
async function countRows(db, table) {
|
|
924
|
+
const row = await db.prepare(`SELECT COUNT(*) AS n FROM ${table}`).get();
|
|
925
|
+
return Number(row.n);
|
|
926
|
+
}
|
|
927
|
+
async function exportSnapshot(db, options = {}) {
|
|
928
|
+
const createdAt = options.createdAt ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
929
|
+
const tables = {};
|
|
930
|
+
const sections = [];
|
|
931
|
+
for (const table of SNAPSHOT_TABLES) {
|
|
932
|
+
const columns = await getColumns(db, table);
|
|
933
|
+
if (columns.length === 0) {
|
|
934
|
+
tables[table] = 0;
|
|
935
|
+
continue;
|
|
936
|
+
}
|
|
937
|
+
const colList = columns.join(", ");
|
|
938
|
+
const rows = await db.prepare(`SELECT ${colList} FROM ${table}`).all();
|
|
939
|
+
tables[table] = rows.length;
|
|
940
|
+
if (rows.length === 0) continue;
|
|
941
|
+
const lines = [`-- ${table} (${rows.length})`];
|
|
942
|
+
for (const row of rows) {
|
|
943
|
+
const values = columns.map((c) => quoteValue(row[c])).join(", ");
|
|
944
|
+
lines.push(`INSERT INTO ${table} (${colList}) VALUES (${values});`);
|
|
945
|
+
}
|
|
946
|
+
sections.push(lines.join("\n"));
|
|
947
|
+
}
|
|
948
|
+
const body = sections.length > 0 ? `${sections.join("\n\n")}
|
|
949
|
+
` : "";
|
|
950
|
+
const checksum = createHash("sha256").update(body).digest("hex");
|
|
951
|
+
const manifest = {
|
|
952
|
+
format: SNAPSHOT_FORMAT,
|
|
953
|
+
version: SNAPSHOT_VERSION,
|
|
954
|
+
createdAt,
|
|
955
|
+
tables,
|
|
956
|
+
checksum
|
|
957
|
+
};
|
|
958
|
+
return `${MANIFEST_PREFIX}${JSON.stringify(manifest)}
|
|
959
|
+
${body}`;
|
|
960
|
+
}
|
|
961
|
+
function parseSnapshot(snapshot) {
|
|
962
|
+
const newline = snapshot.indexOf("\n");
|
|
963
|
+
const header = (newline === -1 ? snapshot : snapshot.slice(0, newline)).trim();
|
|
964
|
+
const body = newline === -1 ? "" : snapshot.slice(newline + 1);
|
|
965
|
+
if (!header.startsWith(MANIFEST_PREFIX)) {
|
|
966
|
+
throw new Error("Not a ZAM snapshot: missing manifest header.");
|
|
967
|
+
}
|
|
968
|
+
let manifest;
|
|
969
|
+
try {
|
|
970
|
+
manifest = JSON.parse(header.slice(MANIFEST_PREFIX.length));
|
|
971
|
+
} catch {
|
|
972
|
+
throw new Error("Snapshot manifest is not valid JSON.");
|
|
973
|
+
}
|
|
974
|
+
if (manifest.format !== SNAPSHOT_FORMAT) {
|
|
975
|
+
throw new Error(`Unsupported snapshot format: ${manifest.format}`);
|
|
976
|
+
}
|
|
977
|
+
if (manifest.version > SNAPSHOT_VERSION) {
|
|
978
|
+
throw new Error(
|
|
979
|
+
`Snapshot version ${manifest.version} is newer than supported (${SNAPSHOT_VERSION}). Upgrade ZAM to import it.`
|
|
980
|
+
);
|
|
981
|
+
}
|
|
982
|
+
return { manifest, body };
|
|
983
|
+
}
|
|
984
|
+
function verifySnapshot(snapshot) {
|
|
985
|
+
const { manifest, body } = parseSnapshot(snapshot);
|
|
986
|
+
const actual = createHash("sha256").update(body).digest("hex");
|
|
987
|
+
if (actual !== manifest.checksum) {
|
|
988
|
+
throw new Error("Snapshot is corrupted: checksum mismatch.");
|
|
989
|
+
}
|
|
990
|
+
return manifest;
|
|
991
|
+
}
|
|
992
|
+
async function importSnapshot(db, snapshot, options = {}) {
|
|
993
|
+
const { manifest, body } = parseSnapshot(snapshot);
|
|
994
|
+
const actual = createHash("sha256").update(body).digest("hex");
|
|
995
|
+
if (actual !== manifest.checksum) {
|
|
996
|
+
throw new Error("Snapshot is corrupted: checksum mismatch.");
|
|
997
|
+
}
|
|
998
|
+
return db.transaction(async (tx) => {
|
|
999
|
+
let existing = 0;
|
|
1000
|
+
for (const table of SNAPSHOT_TABLES) {
|
|
1001
|
+
existing += await countRows(tx, table);
|
|
1002
|
+
}
|
|
1003
|
+
if (existing > 0 && !options.force) {
|
|
1004
|
+
throw new Error(
|
|
1005
|
+
`Target database already holds ${existing} row(s). Pass force to overwrite it.`
|
|
1006
|
+
);
|
|
1007
|
+
}
|
|
1008
|
+
if (options.force) {
|
|
1009
|
+
for (const table of [...SNAPSHOT_TABLES].reverse()) {
|
|
1010
|
+
await tx.exec(`DELETE FROM ${table};`);
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
if (body.trim().length > 0) {
|
|
1014
|
+
await tx.exec(body);
|
|
1015
|
+
}
|
|
1016
|
+
const tables = {};
|
|
1017
|
+
let total = 0;
|
|
1018
|
+
for (const table of SNAPSHOT_TABLES) {
|
|
1019
|
+
const count2 = await countRows(tx, table);
|
|
1020
|
+
tables[table] = count2;
|
|
1021
|
+
total += count2;
|
|
1022
|
+
const expected = manifest.tables[table] ?? 0;
|
|
1023
|
+
if (count2 !== expected) {
|
|
1024
|
+
throw new Error(
|
|
1025
|
+
`Restore verification failed for ${table}: expected ${expected} row(s), found ${count2}.`
|
|
1026
|
+
);
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
return { tables, total };
|
|
1030
|
+
});
|
|
476
1031
|
}
|
|
477
1032
|
|
|
478
1033
|
// src/kernel/goals/engine.ts
|
|
@@ -670,14 +1225,14 @@ function parseRow(row) {
|
|
|
670
1225
|
token_slugs: JSON.parse(row.token_slugs)
|
|
671
1226
|
};
|
|
672
1227
|
}
|
|
673
|
-
function createAgentSkill(db, input) {
|
|
674
|
-
const existing = db.prepare("SELECT * FROM agent_skills WHERE slug = ?").get(input.slug);
|
|
1228
|
+
async function createAgentSkill(db, input) {
|
|
1229
|
+
const existing = await db.prepare("SELECT * FROM agent_skills WHERE slug = ?").get(input.slug);
|
|
675
1230
|
if (existing) {
|
|
676
1231
|
throw new Error(`Agent skill already exists: ${input.slug}`);
|
|
677
1232
|
}
|
|
678
1233
|
const id = ulid();
|
|
679
1234
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
680
|
-
db.prepare(
|
|
1235
|
+
await db.prepare(
|
|
681
1236
|
`INSERT INTO agent_skills (id, slug, description, steps, token_slugs, source, created_at, updated_at)
|
|
682
1237
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
|
|
683
1238
|
).run(
|
|
@@ -691,38 +1246,38 @@ function createAgentSkill(db, input) {
|
|
|
691
1246
|
now
|
|
692
1247
|
);
|
|
693
1248
|
return parseRow(
|
|
694
|
-
db.prepare("SELECT * FROM agent_skills WHERE id = ?").get(id)
|
|
1249
|
+
await db.prepare("SELECT * FROM agent_skills WHERE id = ?").get(id)
|
|
695
1250
|
);
|
|
696
1251
|
}
|
|
697
|
-
function getAgentSkill(db, slug) {
|
|
698
|
-
const row = db.prepare("SELECT * FROM agent_skills WHERE slug = ?").get(slug);
|
|
1252
|
+
async function getAgentSkill(db, slug) {
|
|
1253
|
+
const row = await db.prepare("SELECT * FROM agent_skills WHERE slug = ?").get(slug);
|
|
699
1254
|
return row ? parseRow(row) : void 0;
|
|
700
1255
|
}
|
|
701
|
-
function listAgentSkills(db) {
|
|
702
|
-
const rows = db.prepare("SELECT * FROM agent_skills ORDER BY created_at ASC").all();
|
|
1256
|
+
async function listAgentSkills(db) {
|
|
1257
|
+
const rows = await db.prepare("SELECT * FROM agent_skills ORDER BY created_at ASC").all();
|
|
703
1258
|
return rows.map(parseRow);
|
|
704
1259
|
}
|
|
705
1260
|
|
|
706
1261
|
// src/kernel/models/card.ts
|
|
707
1262
|
import { ulid as ulid2 } from "ulid";
|
|
708
|
-
function ensureCard(db, tokenId, userId) {
|
|
709
|
-
const existing = db.prepare("SELECT * FROM cards WHERE token_id = ? AND user_id = ?").get(tokenId, userId);
|
|
1263
|
+
async function ensureCard(db, tokenId, userId) {
|
|
1264
|
+
const existing = await db.prepare("SELECT * FROM cards WHERE token_id = ? AND user_id = ?").get(tokenId, userId);
|
|
710
1265
|
if (existing) return existing;
|
|
711
1266
|
const id = ulid2();
|
|
712
1267
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
713
|
-
db.prepare(
|
|
1268
|
+
await db.prepare(
|
|
714
1269
|
`INSERT INTO cards (id, token_id, user_id, due_at)
|
|
715
1270
|
VALUES (?, ?, ?, ?)`
|
|
716
1271
|
).run(id, tokenId, userId, now);
|
|
717
|
-
return db.prepare("SELECT * FROM cards WHERE id = ?").get(id);
|
|
1272
|
+
return await db.prepare("SELECT * FROM cards WHERE id = ?").get(id);
|
|
718
1273
|
}
|
|
719
|
-
function getCard(db, tokenId, userId) {
|
|
720
|
-
return db.prepare("SELECT * FROM cards WHERE token_id = ? AND user_id = ?").get(tokenId, userId);
|
|
1274
|
+
async function getCard(db, tokenId, userId) {
|
|
1275
|
+
return await db.prepare("SELECT * FROM cards WHERE token_id = ? AND user_id = ?").get(tokenId, userId);
|
|
721
1276
|
}
|
|
722
|
-
function getCardById(db, cardId) {
|
|
723
|
-
return db.prepare("SELECT * FROM cards WHERE id = ?").get(cardId);
|
|
1277
|
+
async function getCardById(db, cardId) {
|
|
1278
|
+
return await db.prepare("SELECT * FROM cards WHERE id = ?").get(cardId);
|
|
724
1279
|
}
|
|
725
|
-
function updateCard(db, cardId, updates) {
|
|
1280
|
+
async function updateCard(db, cardId, updates) {
|
|
726
1281
|
const fields = [];
|
|
727
1282
|
const values = [];
|
|
728
1283
|
if (updates.stability !== void 0) {
|
|
@@ -769,32 +1324,32 @@ function updateCard(db, cardId, updates) {
|
|
|
769
1324
|
throw new Error("updateCard called with no fields to update");
|
|
770
1325
|
}
|
|
771
1326
|
values.push(cardId);
|
|
772
|
-
const result = db.prepare(`UPDATE cards SET ${fields.join(", ")} WHERE id = ?`).run(...values);
|
|
1327
|
+
const result = await db.prepare(`UPDATE cards SET ${fields.join(", ")} WHERE id = ?`).run(...values);
|
|
773
1328
|
if (result.changes === 0) {
|
|
774
1329
|
throw new Error(`Card not found: ${cardId}`);
|
|
775
1330
|
}
|
|
776
|
-
return db.prepare("SELECT * FROM cards WHERE id = ?").get(cardId);
|
|
1331
|
+
return await db.prepare("SELECT * FROM cards WHERE id = ?").get(cardId);
|
|
777
1332
|
}
|
|
778
|
-
function getCardDeletionImpact(db, tokenId, userId) {
|
|
779
|
-
const card = getCard(db, tokenId, userId);
|
|
1333
|
+
async function getCardDeletionImpact(db, tokenId, userId) {
|
|
1334
|
+
const card = await getCard(db, tokenId, userId);
|
|
780
1335
|
if (!card) {
|
|
781
1336
|
throw new Error(`Card not found for token ${tokenId} and user ${userId}`);
|
|
782
1337
|
}
|
|
783
|
-
const reviewLogs = db.prepare("SELECT COUNT(*) AS n FROM review_logs WHERE card_id = ?").get(card.id);
|
|
1338
|
+
const reviewLogs = await db.prepare("SELECT COUNT(*) AS n FROM review_logs WHERE card_id = ?").get(card.id);
|
|
784
1339
|
return { review_logs: reviewLogs.n };
|
|
785
1340
|
}
|
|
786
|
-
function deleteCardForUser(db, tokenId, userId) {
|
|
787
|
-
const card = getCard(db, tokenId, userId);
|
|
1341
|
+
async function deleteCardForUser(db, tokenId, userId) {
|
|
1342
|
+
const card = await getCard(db, tokenId, userId);
|
|
788
1343
|
if (!card) {
|
|
789
1344
|
throw new Error(`Card not found for token ${tokenId} and user ${userId}`);
|
|
790
1345
|
}
|
|
791
|
-
const impact = getCardDeletionImpact(db, tokenId, userId);
|
|
792
|
-
db.prepare("DELETE FROM cards WHERE id = ?").run(card.id);
|
|
1346
|
+
const impact = await getCardDeletionImpact(db, tokenId, userId);
|
|
1347
|
+
await db.prepare("DELETE FROM cards WHERE id = ?").run(card.id);
|
|
793
1348
|
return { card, impact };
|
|
794
1349
|
}
|
|
795
|
-
function getDueCards(db, userId, now) {
|
|
1350
|
+
async function getDueCards(db, userId, now) {
|
|
796
1351
|
const cutoff = now ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
797
|
-
return db.prepare(
|
|
1352
|
+
return await db.prepare(
|
|
798
1353
|
`SELECT c.*, t.slug, t.concept, t.domain, t.bloom_level
|
|
799
1354
|
FROM cards c
|
|
800
1355
|
JOIN tokens t ON t.id = c.token_id
|
|
@@ -802,8 +1357,8 @@ function getDueCards(db, userId, now) {
|
|
|
802
1357
|
ORDER BY t.bloom_level ASC, c.due_at ASC`
|
|
803
1358
|
).all(userId, cutoff);
|
|
804
1359
|
}
|
|
805
|
-
function getBlockedCards(db, userId) {
|
|
806
|
-
return db.prepare(
|
|
1360
|
+
async function getBlockedCards(db, userId) {
|
|
1361
|
+
return await db.prepare(
|
|
807
1362
|
`SELECT c.*, t.slug, t.concept, t.domain, t.bloom_level
|
|
808
1363
|
FROM cards c
|
|
809
1364
|
JOIN tokens t ON t.id = c.token_id
|
|
@@ -812,253 +1367,43 @@ function getBlockedCards(db, userId) {
|
|
|
812
1367
|
).all(userId);
|
|
813
1368
|
}
|
|
814
1369
|
|
|
815
|
-
// src/kernel/models/
|
|
816
|
-
function buildAncestorMap(db) {
|
|
817
|
-
const rows = db.prepare("SELECT token_id, requires_id FROM prerequisites").all();
|
|
818
|
-
const map = /* @__PURE__ */ new Map();
|
|
819
|
-
for (const row of rows) {
|
|
820
|
-
let ancestors = map.get(row.token_id);
|
|
821
|
-
if (!ancestors) {
|
|
822
|
-
ancestors = /* @__PURE__ */ new Set();
|
|
823
|
-
map.set(row.token_id, ancestors);
|
|
824
|
-
}
|
|
825
|
-
ancestors.add(row.requires_id);
|
|
826
|
-
}
|
|
827
|
-
return map;
|
|
828
|
-
}
|
|
829
|
-
function wouldCreateCycle(db, tokenId, requiresId) {
|
|
830
|
-
if (tokenId === requiresId) return true;
|
|
831
|
-
const ancestors = buildAncestorMap(db);
|
|
832
|
-
const visited = /* @__PURE__ */ new Set();
|
|
833
|
-
const queue = [requiresId];
|
|
834
|
-
while (queue.length > 0) {
|
|
835
|
-
const current = queue.shift();
|
|
836
|
-
if (current === tokenId) return true;
|
|
837
|
-
if (visited.has(current)) continue;
|
|
838
|
-
visited.add(current);
|
|
839
|
-
const parents = ancestors.get(current);
|
|
840
|
-
if (parents) {
|
|
841
|
-
for (const parent of parents) {
|
|
842
|
-
if (!visited.has(parent)) queue.push(parent);
|
|
843
|
-
}
|
|
844
|
-
}
|
|
845
|
-
}
|
|
846
|
-
return false;
|
|
847
|
-
}
|
|
848
|
-
function addPrerequisite(db, tokenId, requiresId) {
|
|
849
|
-
if (tokenId === requiresId) {
|
|
850
|
-
throw new Error("A token cannot be a prerequisite of itself");
|
|
851
|
-
}
|
|
852
|
-
if (wouldCreateCycle(db, tokenId, requiresId)) {
|
|
853
|
-
throw new Error(
|
|
854
|
-
`Cannot add prerequisite: would create a cycle. ${requiresId} already depends on ${tokenId} (directly or transitively).`
|
|
855
|
-
);
|
|
856
|
-
}
|
|
857
|
-
db.prepare(
|
|
858
|
-
"INSERT OR IGNORE INTO prerequisites (token_id, requires_id) VALUES (?, ?)"
|
|
859
|
-
).run(tokenId, requiresId);
|
|
860
|
-
}
|
|
861
|
-
function getPrerequisites(db, tokenId) {
|
|
862
|
-
return db.prepare(
|
|
863
|
-
`SELECT p.token_id, p.requires_id, t.slug, t.concept, t.domain, t.bloom_level
|
|
864
|
-
FROM prerequisites p
|
|
865
|
-
JOIN tokens t ON t.id = p.requires_id
|
|
866
|
-
WHERE p.token_id = ?`
|
|
867
|
-
).all(tokenId);
|
|
868
|
-
}
|
|
869
|
-
function getDependents(db, tokenId) {
|
|
870
|
-
return db.prepare(
|
|
871
|
-
`SELECT p.token_id, p.requires_id, t.slug, t.concept, t.domain, t.bloom_level
|
|
872
|
-
FROM prerequisites p
|
|
873
|
-
JOIN tokens t ON t.id = p.token_id
|
|
874
|
-
WHERE p.requires_id = ?`
|
|
875
|
-
).all(tokenId);
|
|
876
|
-
}
|
|
877
|
-
|
|
878
|
-
// src/kernel/models/review.ts
|
|
1370
|
+
// src/kernel/models/token.ts
|
|
879
1371
|
import { ulid as ulid3 } from "ulid";
|
|
880
|
-
function
|
|
881
|
-
if (input.rating < 1 || input.rating > 4) {
|
|
882
|
-
throw new Error(`Rating must be between 1 and 4, got ${input.rating}`);
|
|
883
|
-
}
|
|
1372
|
+
async function createToken(db, input) {
|
|
884
1373
|
const id = ulid3();
|
|
885
1374
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
1375
|
+
const bloom = input.bloom_level ?? 1;
|
|
1376
|
+
if (bloom < 1 || bloom > 5) {
|
|
1377
|
+
throw new Error(`bloom_level must be between 1 and 5, got ${bloom}`);
|
|
1378
|
+
}
|
|
1379
|
+
await db.prepare(`
|
|
1380
|
+
INSERT INTO tokens (id, slug, concept, domain, bloom_level, context, symbiosis_mode, source_link, question, created_at, updated_at)
|
|
1381
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1382
|
+
`).run(
|
|
890
1383
|
id,
|
|
891
|
-
input.
|
|
892
|
-
input.
|
|
893
|
-
input.
|
|
894
|
-
|
|
895
|
-
input.
|
|
1384
|
+
input.slug,
|
|
1385
|
+
input.concept,
|
|
1386
|
+
input.domain ?? "",
|
|
1387
|
+
bloom,
|
|
1388
|
+
input.context ?? "",
|
|
1389
|
+
input.symbiosis_mode ?? null,
|
|
1390
|
+
input.source_link ?? null,
|
|
1391
|
+
input.question ?? null,
|
|
896
1392
|
now,
|
|
897
|
-
|
|
898
|
-
input.session_id ?? null
|
|
1393
|
+
now
|
|
899
1394
|
);
|
|
900
|
-
return db
|
|
1395
|
+
return await getTokenById(db, id);
|
|
901
1396
|
}
|
|
902
|
-
function
|
|
903
|
-
return db.prepare(
|
|
904
|
-
"SELECT * FROM review_logs WHERE card_id = ? ORDER BY reviewed_at ASC"
|
|
905
|
-
).all(cardId);
|
|
1397
|
+
async function getTokenBySlug(db, slug) {
|
|
1398
|
+
return await db.prepare("SELECT * FROM tokens WHERE slug = ?").get(slug);
|
|
906
1399
|
}
|
|
907
|
-
function
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
if (options?.before) {
|
|
915
|
-
conditions.push("reviewed_at < ?");
|
|
916
|
-
params.push(options.before);
|
|
917
|
-
}
|
|
918
|
-
let sql = `SELECT * FROM review_logs WHERE ${conditions.join(" AND ")} ORDER BY reviewed_at DESC`;
|
|
919
|
-
if (options?.limit) {
|
|
920
|
-
sql += " LIMIT ?";
|
|
921
|
-
params.push(options.limit);
|
|
922
|
-
}
|
|
923
|
-
return db.prepare(sql).all(...params);
|
|
924
|
-
}
|
|
925
|
-
|
|
926
|
-
// src/kernel/models/session.ts
|
|
927
|
-
import { ulid as ulid4 } from "ulid";
|
|
928
|
-
function startSession(db, input) {
|
|
929
|
-
const id = ulid4();
|
|
930
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
931
|
-
const ctx = input.execution_context ?? "shell";
|
|
932
|
-
db.prepare(
|
|
933
|
-
`INSERT INTO sessions (id, user_id, task, execution_context, started_at)
|
|
934
|
-
VALUES (?, ?, ?, ?, ?)`
|
|
935
|
-
).run(id, input.user_id, input.task, ctx, now);
|
|
936
|
-
return db.prepare("SELECT * FROM sessions WHERE id = ?").get(id);
|
|
937
|
-
}
|
|
938
|
-
function endSession(db, sessionId) {
|
|
939
|
-
const session = db.prepare("SELECT * FROM sessions WHERE id = ?").get(sessionId);
|
|
940
|
-
if (!session) {
|
|
941
|
-
throw new Error(`Session not found: ${sessionId}`);
|
|
942
|
-
}
|
|
943
|
-
if (session.completed_at) {
|
|
944
|
-
throw new Error(`Session already completed: ${sessionId}`);
|
|
945
|
-
}
|
|
946
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
947
|
-
db.prepare("UPDATE sessions SET completed_at = ? WHERE id = ?").run(
|
|
948
|
-
now,
|
|
949
|
-
sessionId
|
|
950
|
-
);
|
|
951
|
-
return db.prepare("SELECT * FROM sessions WHERE id = ?").get(sessionId);
|
|
952
|
-
}
|
|
953
|
-
function logStep(db, input) {
|
|
954
|
-
if (input.done_by !== "user" && input.done_by !== "agent") {
|
|
955
|
-
throw new Error(
|
|
956
|
-
`done_by must be 'user' or 'agent', got '${input.done_by}'`
|
|
957
|
-
);
|
|
958
|
-
}
|
|
959
|
-
if (input.rating != null && (input.rating < 1 || input.rating > 4)) {
|
|
960
|
-
throw new Error(`Rating must be between 1 and 4, got ${input.rating}`);
|
|
961
|
-
}
|
|
962
|
-
const session = db.prepare("SELECT id FROM sessions WHERE id = ?").get(input.session_id);
|
|
963
|
-
if (!session) {
|
|
964
|
-
throw new Error(`Session not found: ${input.session_id}`);
|
|
965
|
-
}
|
|
966
|
-
const id = ulid4();
|
|
967
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
968
|
-
db.prepare(
|
|
969
|
-
`INSERT INTO session_steps (id, session_id, token_id, done_by, rating, notes, created_at)
|
|
970
|
-
VALUES (?, ?, ?, ?, ?, ?, ?)`
|
|
971
|
-
).run(
|
|
972
|
-
id,
|
|
973
|
-
input.session_id,
|
|
974
|
-
input.token_id,
|
|
975
|
-
input.done_by,
|
|
976
|
-
input.rating ?? null,
|
|
977
|
-
input.notes ?? null,
|
|
978
|
-
now
|
|
979
|
-
);
|
|
980
|
-
return db.prepare("SELECT * FROM session_steps WHERE id = ?").get(id);
|
|
981
|
-
}
|
|
982
|
-
function getSessionSummary(db, sessionId) {
|
|
983
|
-
const session = db.prepare("SELECT * FROM sessions WHERE id = ?").get(sessionId);
|
|
984
|
-
if (!session) {
|
|
985
|
-
throw new Error(`Session not found: ${sessionId}`);
|
|
986
|
-
}
|
|
987
|
-
const steps = db.prepare(
|
|
988
|
-
`SELECT ss.*, t.slug, t.concept, t.domain, t.bloom_level
|
|
989
|
-
FROM session_steps ss
|
|
990
|
-
JOIN tokens t ON t.id = ss.token_id
|
|
991
|
-
WHERE ss.session_id = ?
|
|
992
|
-
ORDER BY ss.created_at ASC`
|
|
993
|
-
).all(sessionId);
|
|
994
|
-
return { session, steps };
|
|
995
|
-
}
|
|
996
|
-
|
|
997
|
-
// src/kernel/models/settings.ts
|
|
998
|
-
function getSetting(db, key) {
|
|
999
|
-
const row = db.prepare("SELECT value FROM user_config WHERE key = ?").get(key);
|
|
1000
|
-
return row?.value;
|
|
1001
|
-
}
|
|
1002
|
-
function getAllSettings(db) {
|
|
1003
|
-
const rows = db.prepare("SELECT key, value FROM user_config ORDER BY key").all();
|
|
1004
|
-
const map = {};
|
|
1005
|
-
for (const row of rows) {
|
|
1006
|
-
map[row.key] = row.value;
|
|
1007
|
-
}
|
|
1008
|
-
return map;
|
|
1009
|
-
}
|
|
1010
|
-
function getAllSettingsDetailed(db) {
|
|
1011
|
-
return db.prepare("SELECT key, value, updated_at FROM user_config ORDER BY key").all();
|
|
1012
|
-
}
|
|
1013
|
-
function setSetting(db, key, value) {
|
|
1014
|
-
db.prepare(
|
|
1015
|
-
`INSERT INTO user_config (key, value, updated_at)
|
|
1016
|
-
VALUES (?, ?, datetime('now'))
|
|
1017
|
-
ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at`
|
|
1018
|
-
).run(key, value);
|
|
1019
|
-
}
|
|
1020
|
-
function deleteSetting(db, key) {
|
|
1021
|
-
const result = db.prepare("DELETE FROM user_config WHERE key = ?").run(key);
|
|
1022
|
-
return result.changes > 0;
|
|
1023
|
-
}
|
|
1024
|
-
|
|
1025
|
-
// src/kernel/models/token.ts
|
|
1026
|
-
import { ulid as ulid5 } from "ulid";
|
|
1027
|
-
function createToken(db, input) {
|
|
1028
|
-
const id = ulid5();
|
|
1029
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1030
|
-
const bloom = input.bloom_level ?? 1;
|
|
1031
|
-
if (bloom < 1 || bloom > 5) {
|
|
1032
|
-
throw new Error(`bloom_level must be between 1 and 5, got ${bloom}`);
|
|
1033
|
-
}
|
|
1034
|
-
db.prepare(`
|
|
1035
|
-
INSERT INTO tokens (id, slug, concept, domain, bloom_level, context, symbiosis_mode, source_link, question, created_at, updated_at)
|
|
1036
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1037
|
-
`).run(
|
|
1038
|
-
id,
|
|
1039
|
-
input.slug,
|
|
1040
|
-
input.concept,
|
|
1041
|
-
input.domain ?? "",
|
|
1042
|
-
bloom,
|
|
1043
|
-
input.context ?? "",
|
|
1044
|
-
input.symbiosis_mode ?? null,
|
|
1045
|
-
input.source_link ?? null,
|
|
1046
|
-
input.question ?? null,
|
|
1047
|
-
now,
|
|
1048
|
-
now
|
|
1049
|
-
);
|
|
1050
|
-
return getTokenById(db, id);
|
|
1051
|
-
}
|
|
1052
|
-
function getTokenBySlug(db, slug) {
|
|
1053
|
-
return db.prepare("SELECT * FROM tokens WHERE slug = ?").get(slug);
|
|
1054
|
-
}
|
|
1055
|
-
function getTokenById(db, id) {
|
|
1056
|
-
return db.prepare("SELECT * FROM tokens WHERE id = ?").get(id);
|
|
1057
|
-
}
|
|
1058
|
-
function updateToken(db, slug, updates) {
|
|
1059
|
-
const token = getTokenBySlug(db, slug);
|
|
1060
|
-
if (!token) {
|
|
1061
|
-
throw new Error(`Token not found: ${slug}`);
|
|
1400
|
+
async function getTokenById(db, id) {
|
|
1401
|
+
return await db.prepare("SELECT * FROM tokens WHERE id = ?").get(id);
|
|
1402
|
+
}
|
|
1403
|
+
async function updateToken(db, slug, updates) {
|
|
1404
|
+
const token = await getTokenBySlug(db, slug);
|
|
1405
|
+
if (!token) {
|
|
1406
|
+
throw new Error(`Token not found: ${slug}`);
|
|
1062
1407
|
}
|
|
1063
1408
|
const fields = [];
|
|
1064
1409
|
const values = [];
|
|
@@ -1105,13 +1450,11 @@ function updateToken(db, slug, updates) {
|
|
|
1105
1450
|
fields.push("updated_at = ?");
|
|
1106
1451
|
values.push((/* @__PURE__ */ new Date()).toISOString());
|
|
1107
1452
|
values.push(slug);
|
|
1108
|
-
db.prepare(`UPDATE tokens SET ${fields.join(", ")} WHERE slug = ?`).run(
|
|
1109
|
-
|
|
1110
|
-
);
|
|
1111
|
-
return getTokenBySlug(db, slug);
|
|
1453
|
+
await db.prepare(`UPDATE tokens SET ${fields.join(", ")} WHERE slug = ?`).run(...values);
|
|
1454
|
+
return await getTokenBySlug(db, slug);
|
|
1112
1455
|
}
|
|
1113
|
-
function deprecateToken(db, slug) {
|
|
1114
|
-
const token = getTokenBySlug(db, slug);
|
|
1456
|
+
async function deprecateToken(db, slug) {
|
|
1457
|
+
const token = await getTokenBySlug(db, slug);
|
|
1115
1458
|
if (!token) {
|
|
1116
1459
|
throw new Error(`Token not found: ${slug}`);
|
|
1117
1460
|
}
|
|
@@ -1119,25 +1462,25 @@ function deprecateToken(db, slug) {
|
|
|
1119
1462
|
throw new Error(`Token already deprecated: ${slug}`);
|
|
1120
1463
|
}
|
|
1121
1464
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1122
|
-
db.prepare(
|
|
1465
|
+
await db.prepare(
|
|
1123
1466
|
"UPDATE tokens SET deprecated_at = ?, updated_at = ? WHERE slug = ?"
|
|
1124
1467
|
).run(now, now, slug);
|
|
1125
|
-
return getTokenBySlug(db, slug);
|
|
1468
|
+
return await getTokenBySlug(db, slug);
|
|
1126
1469
|
}
|
|
1127
|
-
function getTokenDeleteImpact(db, slug) {
|
|
1128
|
-
const token = getTokenBySlug(db, slug);
|
|
1470
|
+
async function getTokenDeleteImpact(db, slug) {
|
|
1471
|
+
const token = await getTokenBySlug(db, slug);
|
|
1129
1472
|
if (!token) {
|
|
1130
1473
|
throw new Error(`Token not found: ${slug}`);
|
|
1131
1474
|
}
|
|
1132
|
-
const cards = db.prepare("SELECT COUNT(*) AS n FROM cards WHERE token_id = ?").get(token.id);
|
|
1133
|
-
const reviewLogs = db.prepare("SELECT COUNT(*) AS n FROM review_logs WHERE token_id = ?").get(token.id);
|
|
1134
|
-
const prereqsFrom = db.prepare("SELECT COUNT(*) AS n FROM prerequisites WHERE token_id = ?").get(token.id);
|
|
1135
|
-
const prereqsTo = db.prepare("SELECT COUNT(*) AS n FROM prerequisites WHERE requires_id = ?").get(token.id);
|
|
1136
|
-
const sessionSteps = db.prepare("SELECT COUNT(*) AS n FROM session_steps WHERE token_id = ?").get(token.id);
|
|
1137
|
-
const sessionsTouched = db.prepare(
|
|
1475
|
+
const cards = await db.prepare("SELECT COUNT(*) AS n FROM cards WHERE token_id = ?").get(token.id);
|
|
1476
|
+
const reviewLogs = await db.prepare("SELECT COUNT(*) AS n FROM review_logs WHERE token_id = ?").get(token.id);
|
|
1477
|
+
const prereqsFrom = await db.prepare("SELECT COUNT(*) AS n FROM prerequisites WHERE token_id = ?").get(token.id);
|
|
1478
|
+
const prereqsTo = await db.prepare("SELECT COUNT(*) AS n FROM prerequisites WHERE requires_id = ?").get(token.id);
|
|
1479
|
+
const sessionSteps = await db.prepare("SELECT COUNT(*) AS n FROM session_steps WHERE token_id = ?").get(token.id);
|
|
1480
|
+
const sessionsTouched = await db.prepare(
|
|
1138
1481
|
"SELECT COUNT(DISTINCT session_id) AS n FROM session_steps WHERE token_id = ?"
|
|
1139
1482
|
).get(token.id);
|
|
1140
|
-
const skillRows = db.prepare("SELECT token_slugs FROM agent_skills").all();
|
|
1483
|
+
const skillRows = await db.prepare("SELECT token_slugs FROM agent_skills").all();
|
|
1141
1484
|
const agentSkills = skillRows.filter((row) => {
|
|
1142
1485
|
const tokenSlugs = JSON.parse(row.token_slugs);
|
|
1143
1486
|
return tokenSlugs.includes(slug);
|
|
@@ -1152,34 +1495,29 @@ function getTokenDeleteImpact(db, slug) {
|
|
|
1152
1495
|
agent_skills: agentSkills
|
|
1153
1496
|
};
|
|
1154
1497
|
}
|
|
1155
|
-
function deleteToken(db, slug) {
|
|
1156
|
-
const token = getTokenBySlug(db, slug);
|
|
1498
|
+
async function deleteToken(db, slug) {
|
|
1499
|
+
const token = await getTokenBySlug(db, slug);
|
|
1157
1500
|
if (!token) {
|
|
1158
1501
|
throw new Error(`Token not found: ${slug}`);
|
|
1159
1502
|
}
|
|
1160
|
-
const impact = getTokenDeleteImpact(db, slug);
|
|
1161
|
-
db.
|
|
1162
|
-
try {
|
|
1503
|
+
const impact = await getTokenDeleteImpact(db, slug);
|
|
1504
|
+
await db.transaction(async (tx) => {
|
|
1163
1505
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1164
|
-
const skillRows =
|
|
1506
|
+
const skillRows = await tx.prepare("SELECT id, token_slugs FROM agent_skills").all();
|
|
1165
1507
|
for (const row of skillRows) {
|
|
1166
1508
|
const tokenSlugs = JSON.parse(row.token_slugs);
|
|
1167
1509
|
const filtered = tokenSlugs.filter((tokenSlug) => tokenSlug !== slug);
|
|
1168
1510
|
if (filtered.length !== tokenSlugs.length) {
|
|
1169
|
-
|
|
1511
|
+
await tx.prepare(
|
|
1170
1512
|
"UPDATE agent_skills SET token_slugs = ?, updated_at = ? WHERE id = ?"
|
|
1171
1513
|
).run(JSON.stringify(filtered), now, row.id);
|
|
1172
1514
|
}
|
|
1173
1515
|
}
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
} catch (err) {
|
|
1177
|
-
db.exec("ROLLBACK");
|
|
1178
|
-
throw err;
|
|
1179
|
-
}
|
|
1516
|
+
await tx.prepare("DELETE FROM tokens WHERE id = ?").run(token.id);
|
|
1517
|
+
});
|
|
1180
1518
|
return { token, impact };
|
|
1181
1519
|
}
|
|
1182
|
-
function findTokens(db, query) {
|
|
1520
|
+
async function findTokens(db, query) {
|
|
1183
1521
|
const normalised = query.toLowerCase();
|
|
1184
1522
|
const searchTokens = normalised.split(/[\s,.\-_/\\:;!?()[\]{}]+/).filter((t2) => t2.length > 0);
|
|
1185
1523
|
if (searchTokens.length === 0) return [];
|
|
@@ -1189,7 +1527,7 @@ function findTokens(db, query) {
|
|
|
1189
1527
|
const likeSQL = `SELECT * FROM tokens WHERE deprecated_at IS NULL AND (lower(slug) LIKE ? OR lower(concept) LIKE ? OR lower(domain) LIKE ?)`;
|
|
1190
1528
|
for (const term of longTerms) {
|
|
1191
1529
|
const pattern = `%${term}%`;
|
|
1192
|
-
const rows = db.prepare(likeSQL).all(pattern, pattern, pattern);
|
|
1530
|
+
const rows = await db.prepare(likeSQL).all(pattern, pattern, pattern);
|
|
1193
1531
|
for (const row of rows) {
|
|
1194
1532
|
const entry = scoreMap.get(row.id);
|
|
1195
1533
|
if (entry) {
|
|
@@ -1200,7 +1538,7 @@ function findTokens(db, query) {
|
|
|
1200
1538
|
}
|
|
1201
1539
|
}
|
|
1202
1540
|
if (shortTerms.length > 0 || longTerms.length === 0) {
|
|
1203
|
-
const allTokens = db.prepare("SELECT * FROM tokens WHERE deprecated_at IS NULL").all();
|
|
1541
|
+
const allTokens = await db.prepare("SELECT * FROM tokens WHERE deprecated_at IS NULL").all();
|
|
1204
1542
|
for (const token of allTokens) {
|
|
1205
1543
|
const words = `${token.slug} ${token.concept} ${token.domain}`.toLowerCase().split(/[\s,.\-_/\\:;!?()[\]{}]+/).filter(Boolean);
|
|
1206
1544
|
let matchCount = 0;
|
|
@@ -1230,103 +1568,375 @@ function findTokens(db, query) {
|
|
|
1230
1568
|
scored.sort((a, b) => b.score - a.score);
|
|
1231
1569
|
return scored;
|
|
1232
1570
|
}
|
|
1233
|
-
function listTokens(db, options) {
|
|
1571
|
+
async function listTokens(db, options) {
|
|
1234
1572
|
if (options?.domain) {
|
|
1235
|
-
return db.prepare(
|
|
1573
|
+
return await db.prepare(
|
|
1236
1574
|
"SELECT * FROM tokens WHERE domain = ? AND deprecated_at IS NULL ORDER BY bloom_level, slug"
|
|
1237
1575
|
).all(options.domain);
|
|
1238
1576
|
}
|
|
1239
|
-
return db.prepare(
|
|
1577
|
+
return await db.prepare(
|
|
1240
1578
|
"SELECT * FROM tokens WHERE deprecated_at IS NULL ORDER BY bloom_level, domain, slug"
|
|
1241
1579
|
).all();
|
|
1242
1580
|
}
|
|
1243
1581
|
|
|
1244
|
-
// src/kernel/
|
|
1245
|
-
function
|
|
1246
|
-
const
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1582
|
+
// src/kernel/models/prerequisite.ts
|
|
1583
|
+
async function buildAncestorMap(db) {
|
|
1584
|
+
const rows = await db.prepare("SELECT token_id, requires_id FROM prerequisites").all();
|
|
1585
|
+
const map = /* @__PURE__ */ new Map();
|
|
1586
|
+
for (const row of rows) {
|
|
1587
|
+
let ancestors = map.get(row.token_id);
|
|
1588
|
+
if (!ancestors) {
|
|
1589
|
+
ancestors = /* @__PURE__ */ new Set();
|
|
1590
|
+
map.set(row.token_id, ancestors);
|
|
1253
1591
|
}
|
|
1592
|
+
ancestors.add(row.requires_id);
|
|
1254
1593
|
}
|
|
1255
|
-
return
|
|
1594
|
+
return map;
|
|
1256
1595
|
}
|
|
1257
|
-
function
|
|
1258
|
-
|
|
1259
|
-
const
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
seq: e.seq,
|
|
1272
|
-
pid: e.pid ?? 0,
|
|
1273
|
-
command: start.command ?? "",
|
|
1274
|
-
cwd: start.cwd ?? "",
|
|
1275
|
-
startedAt: start.ts,
|
|
1276
|
-
endedAt: e.ts,
|
|
1277
|
-
durationMs: endMs - startMs,
|
|
1278
|
-
exitCode: e.exit_code ?? null
|
|
1279
|
-
});
|
|
1280
|
-
starts.delete(key);
|
|
1596
|
+
async function wouldCreateCycle(db, tokenId, requiresId) {
|
|
1597
|
+
if (tokenId === requiresId) return true;
|
|
1598
|
+
const ancestors = await buildAncestorMap(db);
|
|
1599
|
+
const visited = /* @__PURE__ */ new Set();
|
|
1600
|
+
const queue = [requiresId];
|
|
1601
|
+
while (queue.length > 0) {
|
|
1602
|
+
const current = queue.shift();
|
|
1603
|
+
if (current === tokenId) return true;
|
|
1604
|
+
if (visited.has(current)) continue;
|
|
1605
|
+
visited.add(current);
|
|
1606
|
+
const parents = ancestors.get(current);
|
|
1607
|
+
if (parents) {
|
|
1608
|
+
for (const parent of parents) {
|
|
1609
|
+
if (!visited.has(parent)) queue.push(parent);
|
|
1281
1610
|
}
|
|
1282
1611
|
}
|
|
1283
1612
|
}
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
cwd: start.cwd ?? "",
|
|
1290
|
-
startedAt: start.ts,
|
|
1291
|
-
endedAt: null,
|
|
1292
|
-
durationMs: null,
|
|
1293
|
-
exitCode: null
|
|
1294
|
-
});
|
|
1613
|
+
return false;
|
|
1614
|
+
}
|
|
1615
|
+
async function addPrerequisite(db, tokenId, requiresId) {
|
|
1616
|
+
if (tokenId === requiresId) {
|
|
1617
|
+
throw new Error("A token cannot be a prerequisite of itself");
|
|
1295
1618
|
}
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1619
|
+
if (await wouldCreateCycle(db, tokenId, requiresId)) {
|
|
1620
|
+
throw new Error(
|
|
1621
|
+
`Cannot add prerequisite: would create a cycle. ${requiresId} already depends on ${tokenId} (directly or transitively).`
|
|
1622
|
+
);
|
|
1623
|
+
}
|
|
1624
|
+
await db.prepare(
|
|
1625
|
+
"INSERT OR IGNORE INTO prerequisites (token_id, requires_id) VALUES (?, ?)"
|
|
1626
|
+
).run(tokenId, requiresId);
|
|
1300
1627
|
}
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1628
|
+
async function getPrerequisites(db, tokenId) {
|
|
1629
|
+
return await db.prepare(
|
|
1630
|
+
`SELECT p.token_id, p.requires_id, t.slug, t.concept, t.domain, t.bloom_level
|
|
1631
|
+
FROM prerequisites p
|
|
1632
|
+
JOIN tokens t ON t.id = p.requires_id
|
|
1633
|
+
WHERE p.token_id = ?`
|
|
1634
|
+
).all(tokenId);
|
|
1306
1635
|
}
|
|
1307
|
-
function
|
|
1308
|
-
|
|
1309
|
-
|
|
1636
|
+
async function getDependents(db, tokenId) {
|
|
1637
|
+
return await db.prepare(
|
|
1638
|
+
`SELECT p.token_id, p.requires_id, t.slug, t.concept, t.domain, t.bloom_level
|
|
1639
|
+
FROM prerequisites p
|
|
1640
|
+
JOIN tokens t ON t.id = p.token_id
|
|
1641
|
+
WHERE p.requires_id = ?`
|
|
1642
|
+
).all(tokenId);
|
|
1310
1643
|
}
|
|
1311
|
-
function
|
|
1312
|
-
|
|
1644
|
+
async function getTokenNeighborhood(db, tokenId, userId) {
|
|
1645
|
+
const token = await getTokenById(db, tokenId);
|
|
1646
|
+
if (!token) {
|
|
1647
|
+
throw new Error(`Token not found: ${tokenId}`);
|
|
1648
|
+
}
|
|
1649
|
+
const centerCard = userId ? await getCard(db, tokenId, userId) : void 0;
|
|
1650
|
+
const prereqRows = await getPrerequisites(db, tokenId);
|
|
1651
|
+
const depRows = await getDependents(db, tokenId);
|
|
1652
|
+
const relatedTokenIds = /* @__PURE__ */ new Set();
|
|
1653
|
+
for (const p of prereqRows) relatedTokenIds.add(p.requires_id);
|
|
1654
|
+
for (const d of depRows) relatedTokenIds.add(d.token_id);
|
|
1655
|
+
const cardMap = /* @__PURE__ */ new Map();
|
|
1656
|
+
if (userId && relatedTokenIds.size > 0) {
|
|
1657
|
+
const ids = Array.from(relatedTokenIds);
|
|
1658
|
+
const placeholders = ids.map(() => "?").join(",");
|
|
1659
|
+
const rows = await db.prepare(
|
|
1660
|
+
`SELECT * FROM cards WHERE token_id IN (${placeholders}) AND user_id = ?`
|
|
1661
|
+
).all(...ids, userId);
|
|
1662
|
+
for (const row of rows) {
|
|
1663
|
+
cardMap.set(row.token_id, row);
|
|
1664
|
+
}
|
|
1665
|
+
}
|
|
1666
|
+
const toNode = (t2, card) => ({
|
|
1667
|
+
id: t2.id,
|
|
1668
|
+
slug: t2.slug,
|
|
1669
|
+
concept: t2.concept,
|
|
1670
|
+
domain: t2.domain,
|
|
1671
|
+
bloom_level: t2.bloom_level,
|
|
1672
|
+
card: card ? {
|
|
1673
|
+
state: card.state,
|
|
1674
|
+
reps: card.reps,
|
|
1675
|
+
stability: card.stability,
|
|
1676
|
+
difficulty: card.difficulty,
|
|
1677
|
+
blocked: card.blocked === 1,
|
|
1678
|
+
due_at: card.due_at,
|
|
1679
|
+
last_review_at: card.last_review_at
|
|
1680
|
+
} : null
|
|
1681
|
+
});
|
|
1682
|
+
const center = toNode(token, centerCard);
|
|
1683
|
+
const prerequisites = prereqRows.map(
|
|
1684
|
+
(p) => toNode(
|
|
1685
|
+
{
|
|
1686
|
+
id: p.requires_id,
|
|
1687
|
+
slug: p.slug,
|
|
1688
|
+
concept: p.concept,
|
|
1689
|
+
domain: p.domain,
|
|
1690
|
+
bloom_level: p.bloom_level
|
|
1691
|
+
},
|
|
1692
|
+
cardMap.get(p.requires_id)
|
|
1693
|
+
)
|
|
1694
|
+
);
|
|
1695
|
+
const dependents = depRows.map(
|
|
1696
|
+
(d) => toNode(
|
|
1697
|
+
{
|
|
1698
|
+
id: d.token_id,
|
|
1699
|
+
slug: d.slug,
|
|
1700
|
+
concept: d.concept,
|
|
1701
|
+
domain: d.domain,
|
|
1702
|
+
bloom_level: d.bloom_level
|
|
1703
|
+
},
|
|
1704
|
+
cardMap.get(d.token_id)
|
|
1705
|
+
)
|
|
1706
|
+
);
|
|
1707
|
+
return { center, prerequisites, dependents };
|
|
1313
1708
|
}
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1709
|
+
|
|
1710
|
+
// src/kernel/models/review.ts
|
|
1711
|
+
import { ulid as ulid4 } from "ulid";
|
|
1712
|
+
async function logReview(db, input) {
|
|
1713
|
+
if (input.rating < 1 || input.rating > 4) {
|
|
1714
|
+
throw new Error(`Rating must be between 1 and 4, got ${input.rating}`);
|
|
1715
|
+
}
|
|
1716
|
+
const id = ulid4();
|
|
1717
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1718
|
+
await db.prepare(
|
|
1719
|
+
`INSERT INTO review_logs (id, card_id, token_id, user_id, rating, response_time_ms, reviewed_at, scheduled_at, session_id)
|
|
1720
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
1721
|
+
).run(
|
|
1722
|
+
id,
|
|
1723
|
+
input.card_id,
|
|
1724
|
+
input.token_id,
|
|
1725
|
+
input.user_id,
|
|
1726
|
+
input.rating,
|
|
1727
|
+
input.response_time_ms ?? null,
|
|
1728
|
+
now,
|
|
1729
|
+
input.scheduled_at,
|
|
1730
|
+
input.session_id ?? null
|
|
1731
|
+
);
|
|
1732
|
+
return await db.prepare("SELECT * FROM review_logs WHERE id = ?").get(id);
|
|
1319
1733
|
}
|
|
1320
|
-
function
|
|
1321
|
-
|
|
1322
|
-
|
|
1734
|
+
async function getReviewsForCard(db, cardId) {
|
|
1735
|
+
return await db.prepare(
|
|
1736
|
+
"SELECT * FROM review_logs WHERE card_id = ? ORDER BY reviewed_at ASC"
|
|
1737
|
+
).all(cardId);
|
|
1738
|
+
}
|
|
1739
|
+
async function getReviewsForUser(db, userId, options) {
|
|
1740
|
+
const conditions = ["user_id = ?"];
|
|
1741
|
+
const params = [userId];
|
|
1742
|
+
if (options?.after) {
|
|
1743
|
+
conditions.push("reviewed_at > ?");
|
|
1744
|
+
params.push(options.after);
|
|
1745
|
+
}
|
|
1746
|
+
if (options?.before) {
|
|
1747
|
+
conditions.push("reviewed_at < ?");
|
|
1748
|
+
params.push(options.before);
|
|
1749
|
+
}
|
|
1750
|
+
let sql = `SELECT * FROM review_logs WHERE ${conditions.join(" AND ")} ORDER BY reviewed_at DESC`;
|
|
1751
|
+
if (options?.limit) {
|
|
1752
|
+
sql += " LIMIT ?";
|
|
1753
|
+
params.push(options.limit);
|
|
1754
|
+
}
|
|
1755
|
+
return await db.prepare(sql).all(...params);
|
|
1756
|
+
}
|
|
1757
|
+
|
|
1758
|
+
// src/kernel/models/session.ts
|
|
1759
|
+
import { ulid as ulid5 } from "ulid";
|
|
1760
|
+
async function startSession(db, input) {
|
|
1761
|
+
const id = ulid5();
|
|
1762
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1763
|
+
const ctx = input.execution_context ?? "shell";
|
|
1764
|
+
await db.prepare(
|
|
1765
|
+
`INSERT INTO sessions (id, user_id, task, execution_context, started_at)
|
|
1766
|
+
VALUES (?, ?, ?, ?, ?)`
|
|
1767
|
+
).run(id, input.user_id, input.task, ctx, now);
|
|
1768
|
+
return await db.prepare("SELECT * FROM sessions WHERE id = ?").get(id);
|
|
1769
|
+
}
|
|
1770
|
+
async function endSession(db, sessionId) {
|
|
1771
|
+
const session = await db.prepare("SELECT * FROM sessions WHERE id = ?").get(sessionId);
|
|
1772
|
+
if (!session) {
|
|
1773
|
+
throw new Error(`Session not found: ${sessionId}`);
|
|
1774
|
+
}
|
|
1775
|
+
if (session.completed_at) {
|
|
1776
|
+
throw new Error(`Session already completed: ${sessionId}`);
|
|
1777
|
+
}
|
|
1778
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1779
|
+
await db.prepare("UPDATE sessions SET completed_at = ? WHERE id = ?").run(now, sessionId);
|
|
1780
|
+
return await db.prepare("SELECT * FROM sessions WHERE id = ?").get(sessionId);
|
|
1781
|
+
}
|
|
1782
|
+
async function logStep(db, input) {
|
|
1783
|
+
if (input.done_by !== "user" && input.done_by !== "agent") {
|
|
1784
|
+
throw new Error(
|
|
1785
|
+
`done_by must be 'user' or 'agent', got '${input.done_by}'`
|
|
1786
|
+
);
|
|
1787
|
+
}
|
|
1788
|
+
if (input.rating != null && (input.rating < 1 || input.rating > 4)) {
|
|
1789
|
+
throw new Error(`Rating must be between 1 and 4, got ${input.rating}`);
|
|
1790
|
+
}
|
|
1791
|
+
const session = await db.prepare("SELECT id FROM sessions WHERE id = ?").get(input.session_id);
|
|
1792
|
+
if (!session) {
|
|
1793
|
+
throw new Error(`Session not found: ${input.session_id}`);
|
|
1794
|
+
}
|
|
1795
|
+
const id = ulid5();
|
|
1796
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1797
|
+
await db.prepare(
|
|
1798
|
+
`INSERT INTO session_steps (id, session_id, token_id, done_by, rating, notes, created_at)
|
|
1799
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`
|
|
1800
|
+
).run(
|
|
1801
|
+
id,
|
|
1802
|
+
input.session_id,
|
|
1803
|
+
input.token_id,
|
|
1804
|
+
input.done_by,
|
|
1805
|
+
input.rating ?? null,
|
|
1806
|
+
input.notes ?? null,
|
|
1807
|
+
now
|
|
1808
|
+
);
|
|
1809
|
+
return await db.prepare("SELECT * FROM session_steps WHERE id = ?").get(id);
|
|
1810
|
+
}
|
|
1811
|
+
async function getSessionSummary(db, sessionId) {
|
|
1812
|
+
const session = await db.prepare("SELECT * FROM sessions WHERE id = ?").get(sessionId);
|
|
1813
|
+
if (!session) {
|
|
1814
|
+
throw new Error(`Session not found: ${sessionId}`);
|
|
1815
|
+
}
|
|
1816
|
+
const steps = await db.prepare(
|
|
1817
|
+
`SELECT ss.*, t.slug, t.concept, t.domain, t.bloom_level
|
|
1818
|
+
FROM session_steps ss
|
|
1819
|
+
JOIN tokens t ON t.id = ss.token_id
|
|
1820
|
+
WHERE ss.session_id = ?
|
|
1821
|
+
ORDER BY ss.created_at ASC`
|
|
1822
|
+
).all(sessionId);
|
|
1823
|
+
return { session, steps };
|
|
1824
|
+
}
|
|
1825
|
+
|
|
1826
|
+
// src/kernel/models/settings.ts
|
|
1827
|
+
async function getSetting(db, key) {
|
|
1828
|
+
const row = await db.prepare("SELECT value FROM user_config WHERE key = ?").get(key);
|
|
1829
|
+
return row?.value;
|
|
1830
|
+
}
|
|
1831
|
+
async function getAllSettings(db) {
|
|
1832
|
+
const rows = await db.prepare("SELECT key, value FROM user_config ORDER BY key").all();
|
|
1833
|
+
const map = {};
|
|
1834
|
+
for (const row of rows) {
|
|
1835
|
+
map[row.key] = row.value;
|
|
1836
|
+
}
|
|
1837
|
+
return map;
|
|
1838
|
+
}
|
|
1839
|
+
async function getAllSettingsDetailed(db) {
|
|
1840
|
+
return await db.prepare("SELECT key, value, updated_at FROM user_config ORDER BY key").all();
|
|
1841
|
+
}
|
|
1842
|
+
async function setSetting(db, key, value) {
|
|
1843
|
+
await db.prepare(
|
|
1844
|
+
`INSERT INTO user_config (key, value, updated_at)
|
|
1845
|
+
VALUES (?, ?, datetime('now'))
|
|
1846
|
+
ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at`
|
|
1847
|
+
).run(key, value);
|
|
1848
|
+
}
|
|
1849
|
+
async function deleteSetting(db, key) {
|
|
1850
|
+
const result = await db.prepare("DELETE FROM user_config WHERE key = ?").run(key);
|
|
1851
|
+
return result.changes > 0;
|
|
1852
|
+
}
|
|
1853
|
+
|
|
1854
|
+
// src/kernel/observation/analyzer.ts
|
|
1855
|
+
function parseMonitorLog(jsonl) {
|
|
1856
|
+
const events = [];
|
|
1857
|
+
for (const line of jsonl.split("\n")) {
|
|
1858
|
+
const trimmed = line.trim();
|
|
1859
|
+
if (!trimmed) continue;
|
|
1860
|
+
try {
|
|
1861
|
+
events.push(JSON.parse(trimmed));
|
|
1862
|
+
} catch {
|
|
1863
|
+
}
|
|
1864
|
+
}
|
|
1865
|
+
return events;
|
|
1866
|
+
}
|
|
1867
|
+
function pairCommands(events) {
|
|
1868
|
+
const starts = /* @__PURE__ */ new Map();
|
|
1869
|
+
const records = [];
|
|
1870
|
+
for (const e of events) {
|
|
1871
|
+
if (e.type === "command_start" && e.seq != null) {
|
|
1872
|
+
const key = `${e.pid ?? 0}:${e.seq}`;
|
|
1873
|
+
starts.set(key, e);
|
|
1874
|
+
} else if (e.type === "command_end" && e.seq != null) {
|
|
1875
|
+
const key = `${e.pid ?? 0}:${e.seq}`;
|
|
1876
|
+
const start = starts.get(key);
|
|
1877
|
+
if (start) {
|
|
1878
|
+
const startMs = new Date(start.ts).getTime();
|
|
1879
|
+
const endMs = new Date(e.ts).getTime();
|
|
1880
|
+
records.push({
|
|
1881
|
+
seq: e.seq,
|
|
1882
|
+
pid: e.pid ?? 0,
|
|
1883
|
+
command: start.command ?? "",
|
|
1884
|
+
cwd: start.cwd ?? "",
|
|
1885
|
+
startedAt: start.ts,
|
|
1886
|
+
endedAt: e.ts,
|
|
1887
|
+
durationMs: endMs - startMs,
|
|
1888
|
+
exitCode: e.exit_code ?? null
|
|
1889
|
+
});
|
|
1890
|
+
starts.delete(key);
|
|
1891
|
+
}
|
|
1892
|
+
}
|
|
1893
|
+
}
|
|
1894
|
+
for (const [, start] of starts) {
|
|
1895
|
+
records.push({
|
|
1896
|
+
seq: start.seq ?? 0,
|
|
1897
|
+
pid: start.pid ?? 0,
|
|
1898
|
+
command: start.command ?? "",
|
|
1899
|
+
cwd: start.cwd ?? "",
|
|
1900
|
+
startedAt: start.ts,
|
|
1901
|
+
endedAt: null,
|
|
1902
|
+
durationMs: null,
|
|
1903
|
+
exitCode: null
|
|
1904
|
+
});
|
|
1905
|
+
}
|
|
1906
|
+
records.sort(
|
|
1907
|
+
(a, b) => new Date(a.startedAt).getTime() - new Date(b.startedAt).getTime()
|
|
1908
|
+
);
|
|
1909
|
+
return records;
|
|
1910
|
+
}
|
|
1911
|
+
var HELP_PATTERNS = ["--help", "man ", "tldr ", "help "];
|
|
1912
|
+
var HELP_WINDOW_MS = 6e4;
|
|
1913
|
+
function matchesToken(command, patterns) {
|
|
1914
|
+
const lower = command.toLowerCase();
|
|
1915
|
+
return patterns.some((p) => lower.includes(p.toLowerCase()));
|
|
1916
|
+
}
|
|
1917
|
+
function isHelpCommand(command) {
|
|
1918
|
+
const lower = command.toLowerCase();
|
|
1919
|
+
return HELP_PATTERNS.some((p) => lower.includes(p));
|
|
1920
|
+
}
|
|
1921
|
+
function commandPrefix(command) {
|
|
1922
|
+
return command.split(/\s+/).slice(0, 2).join(" ").toLowerCase();
|
|
1923
|
+
}
|
|
1924
|
+
function computeMedian(values) {
|
|
1925
|
+
if (values.length === 0) return null;
|
|
1926
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
1927
|
+
const mid = Math.floor(sorted.length / 2);
|
|
1928
|
+
return sorted.length % 2 !== 0 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2;
|
|
1929
|
+
}
|
|
1930
|
+
function analyzeObservation(commands, tokenPatterns) {
|
|
1931
|
+
const matchedSet = /* @__PURE__ */ new Set();
|
|
1932
|
+
const ratings = [];
|
|
1323
1933
|
for (const tp of tokenPatterns) {
|
|
1324
1934
|
const matchIndices = [];
|
|
1325
|
-
const
|
|
1935
|
+
const matchedTexts2 = [];
|
|
1326
1936
|
for (let i = 0; i < commands.length; i++) {
|
|
1327
1937
|
if (matchesToken(commands[i].command, tp.patterns)) {
|
|
1328
1938
|
matchIndices.push(i);
|
|
1329
|
-
|
|
1939
|
+
matchedTexts2.push(commands[i].command);
|
|
1330
1940
|
matchedSet.add(i);
|
|
1331
1941
|
}
|
|
1332
1942
|
}
|
|
@@ -1374,8 +1984,8 @@ function analyzeObservation(commands, tokenPatterns) {
|
|
|
1374
1984
|
const prefix = commandPrefix(commands[mi].command);
|
|
1375
1985
|
prefixGroups.set(prefix, (prefixGroups.get(prefix) ?? 0) + 1);
|
|
1376
1986
|
}
|
|
1377
|
-
for (const
|
|
1378
|
-
if (
|
|
1987
|
+
for (const count2 of prefixGroups.values()) {
|
|
1988
|
+
if (count2 > 1) selfCorrections += count2 - 1;
|
|
1379
1989
|
}
|
|
1380
1990
|
const gaps = [];
|
|
1381
1991
|
for (let k = 1; k < matchIndices.length; k++) {
|
|
@@ -1416,7 +2026,7 @@ function analyzeObservation(commands, tokenPatterns) {
|
|
|
1416
2026
|
medianGapMs,
|
|
1417
2027
|
thinkingGapMs
|
|
1418
2028
|
},
|
|
1419
|
-
matchedCommandTexts:
|
|
2029
|
+
matchedCommandTexts: matchedTexts2
|
|
1420
2030
|
});
|
|
1421
2031
|
}
|
|
1422
2032
|
const unmatchedCommands = [];
|
|
@@ -1511,60 +2121,774 @@ function getMonitorLogStats(sessionId) {
|
|
|
1511
2121
|
return { exists: true, sizeBytes: stat.size, lineCount };
|
|
1512
2122
|
}
|
|
1513
2123
|
|
|
1514
|
-
// src/kernel/observation/
|
|
1515
|
-
|
|
1516
|
-
return `'${value.replace(/'/g, "''")}'`;
|
|
1517
|
-
}
|
|
1518
|
-
function generateZshHooks(monitorFile, sessionId) {
|
|
1519
|
-
return `
|
|
1520
|
-
# ZAM monitor hooks for session ${sessionId}
|
|
1521
|
-
export __ZAM_MONITOR_FILE="${monitorFile}"
|
|
1522
|
-
export __ZAM_MONITOR_SEQ=0
|
|
1523
|
-
export __ZAM_MONITOR_SESSION="${sessionId}"
|
|
2124
|
+
// src/kernel/observation/session-synthesis.ts
|
|
2125
|
+
import { ulid as ulid7 } from "ulid";
|
|
1524
2126
|
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
local sec="\${EPOCHREALTIME%%.*}"
|
|
1528
|
-
local frac="\${EPOCHREALTIME##*.}"
|
|
1529
|
-
frac="\${frac:0:3}"
|
|
1530
|
-
printf '%s.%sZ' "$(date -u -r "$sec" '+%Y-%m-%dT%H:%M:%S' 2>/dev/null || date -u '+%Y-%m-%dT%H:%M:%S')" "$frac"
|
|
1531
|
-
else
|
|
1532
|
-
date -u '+%Y-%m-%dT%H:%M:%SZ'
|
|
1533
|
-
fi
|
|
1534
|
-
}
|
|
2127
|
+
// src/kernel/recall/evaluator.ts
|
|
2128
|
+
import { ulid as ulid6 } from "ulid";
|
|
1535
2129
|
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
2130
|
+
// src/kernel/scheduler/fsrs.ts
|
|
2131
|
+
var DEFAULT_W = [
|
|
2132
|
+
0.4072,
|
|
2133
|
+
1.1829,
|
|
2134
|
+
3.1262,
|
|
2135
|
+
15.4722,
|
|
2136
|
+
// w0–w3: initial stability per rating
|
|
2137
|
+
7.2102,
|
|
2138
|
+
0.5316,
|
|
2139
|
+
1.0651,
|
|
2140
|
+
// w4–w6: difficulty
|
|
2141
|
+
92e-4,
|
|
2142
|
+
1.5988,
|
|
2143
|
+
0.1176,
|
|
2144
|
+
1.0014,
|
|
2145
|
+
// w7–w10: stability after forgetting
|
|
2146
|
+
2.0032,
|
|
2147
|
+
0.0266,
|
|
2148
|
+
0.3077,
|
|
2149
|
+
0.15,
|
|
2150
|
+
// w11–w14: stability increase
|
|
2151
|
+
0,
|
|
2152
|
+
2.7849,
|
|
2153
|
+
0.3477,
|
|
2154
|
+
0.6831
|
|
2155
|
+
// w15–w18: additional parameters
|
|
2156
|
+
];
|
|
2157
|
+
var DEFAULT_REQUEST_RETENTION = 0.9;
|
|
2158
|
+
function clamp(value, lo, hi) {
|
|
2159
|
+
return Math.min(hi, Math.max(lo, value));
|
|
1544
2160
|
}
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
local exit_code=$?
|
|
1548
|
-
[[ $__ZAM_MONITOR_SEQ -eq 0 ]] && return
|
|
1549
|
-
local ts="$(__zam_ts)"
|
|
1550
|
-
printf '{"type":"command_end","ts":"%s","exit_code":%d,"seq":%d,"pid":%d}\\n' \\
|
|
1551
|
-
"$ts" "$exit_code" "$__ZAM_MONITOR_SEQ" "$$" \\
|
|
1552
|
-
>> "$__ZAM_MONITOR_FILE"
|
|
2161
|
+
function daysBetween(a, b) {
|
|
2162
|
+
return (b.getTime() - a.getTime()) / (1e3 * 60 * 60 * 24);
|
|
1553
2163
|
}
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
add-zsh-hook preexec __zam_preexec
|
|
1557
|
-
add-zsh-hook precmd __zam_precmd
|
|
1558
|
-
|
|
1559
|
-
echo "ZAM monitor active for session $__ZAM_MONITOR_SESSION"
|
|
1560
|
-
`.trim();
|
|
2164
|
+
function initialStability(w, rating) {
|
|
2165
|
+
return w[rating - 1];
|
|
1561
2166
|
}
|
|
1562
|
-
function
|
|
1563
|
-
return
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
2167
|
+
function initialDifficulty(w, rating) {
|
|
2168
|
+
return clamp(w[4] - Math.exp(w[5] * (rating - 1)) + 1, 1, 10);
|
|
2169
|
+
}
|
|
2170
|
+
function nextDifficulty(w, d, rating) {
|
|
2171
|
+
const d0ForGood = initialDifficulty(w, 3);
|
|
2172
|
+
const updated = w[7] * d0ForGood + (1 - w[7]) * (d - w[6] * (rating - 3));
|
|
2173
|
+
return clamp(updated, 1, 10);
|
|
2174
|
+
}
|
|
2175
|
+
function retrievability(elapsed, stability) {
|
|
2176
|
+
if (stability <= 0) return 0;
|
|
2177
|
+
return (1 + elapsed / (9 * stability)) ** -1;
|
|
2178
|
+
}
|
|
2179
|
+
function stabilityAfterSuccess(w, s, d, r, rating) {
|
|
2180
|
+
const hardPenalty = rating === 2 ? w[15] : 1;
|
|
2181
|
+
const easyBonus = rating === 4 ? w[16] : 1;
|
|
2182
|
+
const inner = Math.exp(w[8]) * (11 - d) * s ** -w[9] * (Math.exp(w[10] * (1 - r)) - 1) * hardPenalty * easyBonus;
|
|
2183
|
+
return s * (inner + 1);
|
|
2184
|
+
}
|
|
2185
|
+
function stabilityAfterForgetting(w, s, d, r) {
|
|
2186
|
+
return w[11] * d ** -w[12] * ((s + 1) ** w[13] - 1) * Math.exp(w[14] * (1 - r));
|
|
2187
|
+
}
|
|
2188
|
+
function nextInterval(stability, requestRetention) {
|
|
2189
|
+
const interval = 9 * stability * (1 / requestRetention - 1);
|
|
2190
|
+
return Math.max(1, Math.round(interval));
|
|
2191
|
+
}
|
|
2192
|
+
function createFSRS(params) {
|
|
2193
|
+
const resolvedParams = {
|
|
2194
|
+
w: params?.w ?? [...DEFAULT_W],
|
|
2195
|
+
requestRetention: params?.requestRetention ?? DEFAULT_REQUEST_RETENTION
|
|
2196
|
+
};
|
|
2197
|
+
function schedule(card, rating, now) {
|
|
2198
|
+
const reviewTime = now ?? /* @__PURE__ */ new Date();
|
|
2199
|
+
const w = resolvedParams.w;
|
|
2200
|
+
const elapsed = card.lastReviewAt !== null ? Math.max(0, daysBetween(card.lastReviewAt, reviewTime)) : 0;
|
|
2201
|
+
if (card.state === "new") {
|
|
2202
|
+
const s = initialStability(w, rating);
|
|
2203
|
+
const d = initialDifficulty(w, rating);
|
|
2204
|
+
const interval2 = nextInterval(s, resolvedParams.requestRetention);
|
|
2205
|
+
const dueAt2 = new Date(reviewTime);
|
|
2206
|
+
dueAt2.setDate(dueAt2.getDate() + interval2);
|
|
2207
|
+
return {
|
|
2208
|
+
stability: s,
|
|
2209
|
+
difficulty: d,
|
|
2210
|
+
elapsedDays: 0,
|
|
2211
|
+
scheduledDays: interval2,
|
|
2212
|
+
reps: rating >= 2 ? 1 : 0,
|
|
2213
|
+
lapses: rating === 1 ? 1 : 0,
|
|
2214
|
+
state: "learning",
|
|
2215
|
+
dueAt: dueAt2,
|
|
2216
|
+
lastReviewAt: reviewTime
|
|
2217
|
+
};
|
|
2218
|
+
}
|
|
2219
|
+
const r = retrievability(elapsed, card.stability);
|
|
2220
|
+
let newStability;
|
|
2221
|
+
let newDifficulty;
|
|
2222
|
+
let newReps;
|
|
2223
|
+
let newLapses;
|
|
2224
|
+
let newState;
|
|
2225
|
+
if (rating === 1) {
|
|
2226
|
+
newStability = stabilityAfterForgetting(
|
|
2227
|
+
w,
|
|
2228
|
+
card.stability,
|
|
2229
|
+
card.difficulty,
|
|
2230
|
+
r
|
|
2231
|
+
);
|
|
2232
|
+
newDifficulty = nextDifficulty(w, card.difficulty, rating);
|
|
2233
|
+
newReps = 0;
|
|
2234
|
+
newLapses = card.lapses + 1;
|
|
2235
|
+
newState = "relearning";
|
|
2236
|
+
} else {
|
|
2237
|
+
newStability = stabilityAfterSuccess(
|
|
2238
|
+
w,
|
|
2239
|
+
card.stability,
|
|
2240
|
+
card.difficulty,
|
|
2241
|
+
r,
|
|
2242
|
+
rating
|
|
2243
|
+
);
|
|
2244
|
+
newDifficulty = nextDifficulty(w, card.difficulty, rating);
|
|
2245
|
+
newReps = card.reps + 1;
|
|
2246
|
+
newLapses = card.lapses;
|
|
2247
|
+
newState = "review";
|
|
2248
|
+
}
|
|
2249
|
+
const interval = nextInterval(
|
|
2250
|
+
newStability,
|
|
2251
|
+
resolvedParams.requestRetention
|
|
2252
|
+
);
|
|
2253
|
+
const dueAt = new Date(reviewTime);
|
|
2254
|
+
dueAt.setDate(dueAt.getDate() + interval);
|
|
2255
|
+
return {
|
|
2256
|
+
stability: newStability,
|
|
2257
|
+
difficulty: newDifficulty,
|
|
2258
|
+
elapsedDays: elapsed,
|
|
2259
|
+
scheduledDays: interval,
|
|
2260
|
+
reps: newReps,
|
|
2261
|
+
lapses: newLapses,
|
|
2262
|
+
state: newState,
|
|
2263
|
+
dueAt,
|
|
2264
|
+
lastReviewAt: reviewTime
|
|
2265
|
+
};
|
|
2266
|
+
}
|
|
2267
|
+
return {
|
|
2268
|
+
schedule,
|
|
2269
|
+
params: Object.freeze(resolvedParams)
|
|
2270
|
+
};
|
|
2271
|
+
}
|
|
2272
|
+
|
|
2273
|
+
// src/kernel/recall/evaluator.ts
|
|
2274
|
+
async function evaluateRating(db, input) {
|
|
2275
|
+
return db.transaction((tx) => evaluateRatingWithinTransaction(tx, input));
|
|
2276
|
+
}
|
|
2277
|
+
async function evaluateRatingWithinTransaction(db, input) {
|
|
2278
|
+
const card = await db.prepare("SELECT * FROM cards WHERE id = ?").get(input.cardId);
|
|
2279
|
+
if (!card) {
|
|
2280
|
+
throw new Error(`Card not found: ${input.cardId}`);
|
|
2281
|
+
}
|
|
2282
|
+
const now = /* @__PURE__ */ new Date();
|
|
2283
|
+
const fsrs = createFSRS();
|
|
2284
|
+
const schedulingCard = {
|
|
2285
|
+
stability: card.stability,
|
|
2286
|
+
difficulty: card.difficulty,
|
|
2287
|
+
elapsedDays: card.elapsed_days,
|
|
2288
|
+
scheduledDays: card.scheduled_days,
|
|
2289
|
+
reps: card.reps,
|
|
2290
|
+
lapses: card.lapses,
|
|
2291
|
+
state: card.state,
|
|
2292
|
+
dueAt: new Date(card.due_at),
|
|
2293
|
+
lastReviewAt: card.last_review_at ? new Date(card.last_review_at) : null
|
|
2294
|
+
};
|
|
2295
|
+
const updated = fsrs.schedule(schedulingCard, input.rating, now);
|
|
2296
|
+
await updateCard(db, input.cardId, {
|
|
2297
|
+
stability: updated.stability,
|
|
2298
|
+
difficulty: updated.difficulty,
|
|
2299
|
+
elapsed_days: updated.elapsedDays,
|
|
2300
|
+
scheduled_days: updated.scheduledDays,
|
|
2301
|
+
reps: updated.reps,
|
|
2302
|
+
lapses: updated.lapses,
|
|
2303
|
+
state: updated.state,
|
|
2304
|
+
due_at: updated.dueAt.toISOString(),
|
|
2305
|
+
last_review_at: now.toISOString()
|
|
2306
|
+
});
|
|
2307
|
+
const reviewLogId = input.reviewLogId ?? ulid6();
|
|
2308
|
+
await db.prepare(
|
|
2309
|
+
`INSERT INTO review_logs (id, card_id, token_id, user_id, rating, response_time_ms, reviewed_at, scheduled_at, session_id)
|
|
2310
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
2311
|
+
).run(
|
|
2312
|
+
reviewLogId,
|
|
2313
|
+
input.cardId,
|
|
2314
|
+
input.tokenId,
|
|
2315
|
+
input.userId,
|
|
2316
|
+
input.rating,
|
|
2317
|
+
input.responseTimeMs ?? null,
|
|
2318
|
+
now.toISOString(),
|
|
2319
|
+
card.due_at,
|
|
2320
|
+
input.sessionId ?? null
|
|
2321
|
+
);
|
|
2322
|
+
return {
|
|
2323
|
+
nextDueAt: updated.dueAt.toISOString(),
|
|
2324
|
+
stability: updated.stability,
|
|
2325
|
+
difficulty: updated.difficulty,
|
|
2326
|
+
state: updated.state,
|
|
2327
|
+
scheduledDays: updated.scheduledDays,
|
|
2328
|
+
reps: updated.reps,
|
|
2329
|
+
lapses: updated.lapses
|
|
2330
|
+
};
|
|
2331
|
+
}
|
|
2332
|
+
|
|
2333
|
+
// src/kernel/scheduler/blocker.ts
|
|
2334
|
+
async function cascadeBlock(db, userId, tokenSlug) {
|
|
2335
|
+
const token = await getTokenBySlug(db, tokenSlug);
|
|
2336
|
+
if (!token) {
|
|
2337
|
+
throw new Error(`Unknown token slug: ${tokenSlug}`);
|
|
2338
|
+
}
|
|
2339
|
+
const prereqs = await getPrerequisites(db, token.id);
|
|
2340
|
+
if (prereqs.length === 0) {
|
|
2341
|
+
throw new Error(`Cannot block ${tokenSlug}: token has no prerequisites`);
|
|
2342
|
+
}
|
|
2343
|
+
await ensureCard(db, token.id, userId);
|
|
2344
|
+
await db.prepare("UPDATE cards SET blocked = 1 WHERE token_id = ? AND user_id = ?").run(token.id, userId);
|
|
2345
|
+
const surfaced = [];
|
|
2346
|
+
for (const prereq of prereqs) {
|
|
2347
|
+
const card = await ensureCard(db, prereq.requires_id, userId);
|
|
2348
|
+
if (card.blocked === 1) {
|
|
2349
|
+
const prereqOfPrereq = await db.prepare("SELECT COUNT(*) as n FROM prerequisites WHERE token_id = ?").get(prereq.requires_id);
|
|
2350
|
+
if (prereqOfPrereq.n === 0) {
|
|
2351
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2352
|
+
await db.prepare(
|
|
2353
|
+
"UPDATE cards SET blocked = 0, due_at = ? WHERE token_id = ? AND user_id = ?"
|
|
2354
|
+
).run(now, prereq.requires_id, userId);
|
|
2355
|
+
}
|
|
2356
|
+
}
|
|
2357
|
+
surfaced.push({
|
|
2358
|
+
slug: prereq.slug,
|
|
2359
|
+
concept: prereq.concept,
|
|
2360
|
+
bloomLevel: prereq.bloom_level
|
|
2361
|
+
});
|
|
2362
|
+
}
|
|
2363
|
+
return {
|
|
2364
|
+
blockedSlug: tokenSlug,
|
|
2365
|
+
prerequisites: surfaced
|
|
2366
|
+
};
|
|
2367
|
+
}
|
|
2368
|
+
async function unblockReady(db, userId) {
|
|
2369
|
+
const blockedCards = await db.prepare(
|
|
2370
|
+
`SELECT c.token_id, t.slug, t.concept
|
|
2371
|
+
FROM cards c
|
|
2372
|
+
JOIN tokens t ON t.id = c.token_id
|
|
2373
|
+
WHERE c.user_id = ? AND c.blocked = 1`
|
|
2374
|
+
).all(userId);
|
|
2375
|
+
const unblocked = [];
|
|
2376
|
+
for (const card of blockedCards) {
|
|
2377
|
+
const totalPrereqs = await db.prepare("SELECT COUNT(*) as n FROM prerequisites WHERE token_id = ?").get(card.token_id);
|
|
2378
|
+
const metPrereqs = await db.prepare(
|
|
2379
|
+
`SELECT COUNT(*) as n FROM cards c
|
|
2380
|
+
JOIN prerequisites p ON p.requires_id = c.token_id
|
|
2381
|
+
WHERE p.token_id = ? AND c.user_id = ? AND c.reps >= 1 AND c.blocked = 0`
|
|
2382
|
+
).get(card.token_id, userId);
|
|
2383
|
+
if (totalPrereqs.n === 0 || metPrereqs.n === totalPrereqs.n) {
|
|
2384
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2385
|
+
await db.prepare(
|
|
2386
|
+
"UPDATE cards SET blocked = 0, due_at = ? WHERE token_id = ? AND user_id = ?"
|
|
2387
|
+
).run(now, card.token_id, userId);
|
|
2388
|
+
unblocked.push({ slug: card.slug, concept: card.concept });
|
|
2389
|
+
}
|
|
2390
|
+
}
|
|
2391
|
+
return { unblocked };
|
|
2392
|
+
}
|
|
2393
|
+
|
|
2394
|
+
// src/kernel/observation/ui-observer-io.ts
|
|
2395
|
+
import { appendFileSync as appendFileSync2, existsSync as existsSync5, mkdirSync as mkdirSync4, readFileSync as readFileSync5 } from "fs";
|
|
2396
|
+
import { homedir as homedir4 } from "os";
|
|
2397
|
+
import { join as join5 } from "path";
|
|
2398
|
+
|
|
2399
|
+
// src/kernel/observation/ui-observer.ts
|
|
2400
|
+
var UI_OBSERVATION_PROTOCOL_VERSION = 1;
|
|
2401
|
+
var OBSERVATION_KINDS = /* @__PURE__ */ new Set([
|
|
2402
|
+
"progress",
|
|
2403
|
+
"step-completed",
|
|
2404
|
+
"error",
|
|
2405
|
+
"help-seeking",
|
|
2406
|
+
"uncertain",
|
|
2407
|
+
"privacy-pause",
|
|
2408
|
+
"heartbeat"
|
|
2409
|
+
]);
|
|
2410
|
+
var ACTION_TYPES = /* @__PURE__ */ new Set([
|
|
2411
|
+
"click",
|
|
2412
|
+
"shortcut",
|
|
2413
|
+
"typing",
|
|
2414
|
+
"scroll",
|
|
2415
|
+
"window-change"
|
|
2416
|
+
]);
|
|
2417
|
+
var EVIDENCE_TYPES = /* @__PURE__ */ new Set([
|
|
2418
|
+
"uia",
|
|
2419
|
+
"keyframe",
|
|
2420
|
+
"clip",
|
|
2421
|
+
"window"
|
|
2422
|
+
]);
|
|
2423
|
+
function isUiObservationReport(value) {
|
|
2424
|
+
if (!isRecord(value)) return false;
|
|
2425
|
+
if (value.version !== UI_OBSERVATION_PROTOCOL_VERSION) return false;
|
|
2426
|
+
if (!isNonEmptyString(value.sessionId)) return false;
|
|
2427
|
+
if (!isNonNegativeInteger(value.sequence)) return false;
|
|
2428
|
+
if (!isNonEmptyString(value.observedFrom)) return false;
|
|
2429
|
+
if (!isNonEmptyString(value.observedTo)) return false;
|
|
2430
|
+
if (typeof value.kind !== "string" || !OBSERVATION_KINDS.has(value.kind)) {
|
|
2431
|
+
return false;
|
|
2432
|
+
}
|
|
2433
|
+
if (!isApplication(value.application)) return false;
|
|
2434
|
+
if (!isNonEmptyString(value.summary)) return false;
|
|
2435
|
+
if (!Array.isArray(value.actions) || !value.actions.every(isObservedAction)) {
|
|
2436
|
+
return false;
|
|
2437
|
+
}
|
|
2438
|
+
if (!Array.isArray(value.evidence) || !value.evidence.every(isEvidenceRef)) {
|
|
2439
|
+
return false;
|
|
2440
|
+
}
|
|
2441
|
+
if (!Array.isArray(value.candidateTokens) || !value.candidateTokens.every(isCandidateToken)) {
|
|
2442
|
+
return false;
|
|
2443
|
+
}
|
|
2444
|
+
return isConfidence(value.confidence);
|
|
2445
|
+
}
|
|
2446
|
+
function parseUiObservationLog(jsonl) {
|
|
2447
|
+
const reports = [];
|
|
2448
|
+
for (const line of jsonl.split("\n")) {
|
|
2449
|
+
const trimmed = line.trim();
|
|
2450
|
+
if (!trimmed) continue;
|
|
2451
|
+
try {
|
|
2452
|
+
const value = JSON.parse(trimmed);
|
|
2453
|
+
if (isUiObservationReport(value)) {
|
|
2454
|
+
reports.push(value);
|
|
2455
|
+
}
|
|
2456
|
+
} catch {
|
|
2457
|
+
}
|
|
2458
|
+
}
|
|
2459
|
+
return reports.sort((left, right) => left.sequence - right.sequence);
|
|
2460
|
+
}
|
|
2461
|
+
function isApplication(value) {
|
|
2462
|
+
if (!isRecord(value) || !isNonEmptyString(value.processName)) return false;
|
|
2463
|
+
if (value.processId !== void 0 && !isNonNegativeInteger(value.processId)) {
|
|
2464
|
+
return false;
|
|
2465
|
+
}
|
|
2466
|
+
return value.windowTitle === void 0 || typeof value.windowTitle === "string";
|
|
2467
|
+
}
|
|
2468
|
+
function isObservedAction(value) {
|
|
2469
|
+
if (!isRecord(value) || typeof value.type !== "string") return false;
|
|
2470
|
+
if (!ACTION_TYPES.has(value.type)) return false;
|
|
2471
|
+
if (value.target !== void 0 && typeof value.target !== "string") {
|
|
2472
|
+
return false;
|
|
2473
|
+
}
|
|
2474
|
+
return value.result === void 0 || typeof value.result === "string";
|
|
2475
|
+
}
|
|
2476
|
+
function isEvidenceRef(value) {
|
|
2477
|
+
if (!isRecord(value) || typeof value.type !== "string") return false;
|
|
2478
|
+
return EVIDENCE_TYPES.has(value.type) && isNonEmptyString(value.ref) && typeof value.redacted === "boolean";
|
|
2479
|
+
}
|
|
2480
|
+
function isCandidateToken(value) {
|
|
2481
|
+
return isRecord(value) && isNonEmptyString(value.slug) && isConfidence(value.confidence) && isNonEmptyString(value.rationale);
|
|
2482
|
+
}
|
|
2483
|
+
function isRecord(value) {
|
|
2484
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2485
|
+
}
|
|
2486
|
+
function isNonEmptyString(value) {
|
|
2487
|
+
return typeof value === "string" && value.trim().length > 0;
|
|
2488
|
+
}
|
|
2489
|
+
function isNonNegativeInteger(value) {
|
|
2490
|
+
return Number.isInteger(value) && Number(value) >= 0;
|
|
2491
|
+
}
|
|
2492
|
+
function isConfidence(value) {
|
|
2493
|
+
return typeof value === "number" && value >= 0 && value <= 1;
|
|
2494
|
+
}
|
|
2495
|
+
|
|
2496
|
+
// src/kernel/observation/ui-observer-io.ts
|
|
2497
|
+
var DEFAULT_UI_OBSERVER_DIR = join5(homedir4(), ".zam", "observer");
|
|
2498
|
+
function getUiObserverDir() {
|
|
2499
|
+
return process.env.ZAM_OBSERVER_DIR || DEFAULT_UI_OBSERVER_DIR;
|
|
2500
|
+
}
|
|
2501
|
+
function getUiObservationPath(sessionId) {
|
|
2502
|
+
if (!/^[A-Za-z0-9_-]+$/.test(sessionId)) {
|
|
2503
|
+
throw new Error(`Invalid observer session ID: ${sessionId}`);
|
|
2504
|
+
}
|
|
2505
|
+
return join5(getUiObserverDir(), `${sessionId}.reports.jsonl`);
|
|
2506
|
+
}
|
|
2507
|
+
function ensureUiObserverDir() {
|
|
2508
|
+
const dir = getUiObserverDir();
|
|
2509
|
+
if (!existsSync5(dir)) {
|
|
2510
|
+
mkdirSync4(dir, { recursive: true, mode: 448 });
|
|
2511
|
+
}
|
|
2512
|
+
}
|
|
2513
|
+
function uiObservationLogExists(sessionId) {
|
|
2514
|
+
return existsSync5(getUiObservationPath(sessionId));
|
|
2515
|
+
}
|
|
2516
|
+
function readUiObservationLog(sessionId) {
|
|
2517
|
+
const path = getUiObservationPath(sessionId);
|
|
2518
|
+
if (!existsSync5(path)) return [];
|
|
2519
|
+
return parseUiObservationLog(readFileSync5(path, "utf8"));
|
|
2520
|
+
}
|
|
2521
|
+
function appendUiObservationReport(report) {
|
|
2522
|
+
if (!isUiObservationReport(report)) {
|
|
2523
|
+
throw new Error("Invalid UI observation report");
|
|
2524
|
+
}
|
|
2525
|
+
ensureUiObserverDir();
|
|
2526
|
+
appendFileSync2(
|
|
2527
|
+
getUiObservationPath(report.sessionId),
|
|
2528
|
+
`${JSON.stringify(report)}
|
|
2529
|
+
`,
|
|
2530
|
+
{ encoding: "utf8", mode: 384 }
|
|
2531
|
+
);
|
|
2532
|
+
}
|
|
2533
|
+
|
|
2534
|
+
// src/kernel/observation/ui-observer-synthesis.ts
|
|
2535
|
+
var MIN_UI_CONFIDENCE = 0.6;
|
|
2536
|
+
function kindToRating(kind) {
|
|
2537
|
+
switch (kind) {
|
|
2538
|
+
case "step-completed":
|
|
2539
|
+
return 4;
|
|
2540
|
+
case "progress":
|
|
2541
|
+
return 3;
|
|
2542
|
+
case "error":
|
|
2543
|
+
case "help-seeking":
|
|
2544
|
+
return 2;
|
|
2545
|
+
default:
|
|
2546
|
+
return null;
|
|
2547
|
+
}
|
|
2548
|
+
}
|
|
2549
|
+
function toSynthesisConfidence(confidence) {
|
|
2550
|
+
return confidence >= 0.85 ? "high" : "medium";
|
|
2551
|
+
}
|
|
2552
|
+
function confidenceRank(confidence) {
|
|
2553
|
+
return confidence === "high" ? 2 : 1;
|
|
2554
|
+
}
|
|
2555
|
+
function buildEvidence(report) {
|
|
2556
|
+
return {
|
|
2557
|
+
matchedCommands: report.actions.length,
|
|
2558
|
+
helpSeeking: report.kind === "help-seeking",
|
|
2559
|
+
errorCount: report.kind === "error" ? 1 : 0,
|
|
2560
|
+
selfCorrections: 0,
|
|
2561
|
+
medianGapMs: null,
|
|
2562
|
+
thinkingGapMs: null
|
|
2563
|
+
};
|
|
2564
|
+
}
|
|
2565
|
+
function matchedTexts(report) {
|
|
2566
|
+
const texts = [report.summary];
|
|
2567
|
+
for (const action of report.actions) {
|
|
2568
|
+
const label = [action.type, action.target, action.result].filter(Boolean).join(" ");
|
|
2569
|
+
if (label) texts.push(label);
|
|
2570
|
+
}
|
|
2571
|
+
return texts;
|
|
2572
|
+
}
|
|
2573
|
+
function buildUiSynthesisCandidates(reports, tokens, applied, minConfidence) {
|
|
2574
|
+
const minRank = confidenceRank(minConfidence);
|
|
2575
|
+
const bestBySlug = /* @__PURE__ */ new Map();
|
|
2576
|
+
let skippedLowConfidence = 0;
|
|
2577
|
+
for (const report of reports) {
|
|
2578
|
+
if (report.confidence < MIN_UI_CONFIDENCE) continue;
|
|
2579
|
+
const fallbackRating = kindToRating(report.kind);
|
|
2580
|
+
const candidates2 = report.candidateTokens.length > 0 ? report.candidateTokens.map((candidate) => ({
|
|
2581
|
+
slug: candidate.slug,
|
|
2582
|
+
confidence: candidate.confidence,
|
|
2583
|
+
rating: fallbackRating
|
|
2584
|
+
})) : [];
|
|
2585
|
+
for (const candidate of candidates2) {
|
|
2586
|
+
const token = tokens.get(candidate.slug);
|
|
2587
|
+
if (!token || applied.has(token.id)) continue;
|
|
2588
|
+
const synthesisConfidence = toSynthesisConfidence(candidate.confidence);
|
|
2589
|
+
if (confidenceRank(synthesisConfidence) < minRank) {
|
|
2590
|
+
skippedLowConfidence++;
|
|
2591
|
+
continue;
|
|
2592
|
+
}
|
|
2593
|
+
const inferredRating = candidate.rating ?? 3;
|
|
2594
|
+
const existing = bestBySlug.get(candidate.slug);
|
|
2595
|
+
if (existing && existing.reportConfidence >= candidate.confidence) {
|
|
2596
|
+
continue;
|
|
2597
|
+
}
|
|
2598
|
+
bestBySlug.set(candidate.slug, {
|
|
2599
|
+
token,
|
|
2600
|
+
inferredRating,
|
|
2601
|
+
confidence: synthesisConfidence,
|
|
2602
|
+
evidence: buildEvidence(report),
|
|
2603
|
+
matchedCommandTexts: matchedTexts(report),
|
|
2604
|
+
reportConfidence: candidate.confidence
|
|
2605
|
+
});
|
|
2606
|
+
}
|
|
2607
|
+
}
|
|
2608
|
+
const candidates = [...bestBySlug.values()].sort((left, right) => right.reportConfidence - left.reportConfidence).map((entry) => ({
|
|
2609
|
+
tokenId: entry.token.id,
|
|
2610
|
+
tokenSlug: entry.token.slug,
|
|
2611
|
+
concept: entry.token.concept,
|
|
2612
|
+
domain: entry.token.domain,
|
|
2613
|
+
inferredRating: entry.inferredRating,
|
|
2614
|
+
confidence: entry.confidence,
|
|
2615
|
+
evidence: entry.evidence,
|
|
2616
|
+
matchedCommandTexts: entry.matchedCommandTexts
|
|
2617
|
+
}));
|
|
2618
|
+
return { candidates, skippedLowConfidence };
|
|
2619
|
+
}
|
|
2620
|
+
function uiObservationTimeSpan(reports) {
|
|
2621
|
+
if (reports.length === 0) return null;
|
|
2622
|
+
const start = reports[0].observedFrom;
|
|
2623
|
+
const end = reports[reports.length - 1].observedTo;
|
|
2624
|
+
const durationMs = Math.max(0, Date.parse(end) - Date.parse(start));
|
|
2625
|
+
return { start, end, durationMs };
|
|
2626
|
+
}
|
|
2627
|
+
|
|
2628
|
+
// src/kernel/observation/session-synthesis.ts
|
|
2629
|
+
function parseSynthesisRow(row) {
|
|
2630
|
+
return {
|
|
2631
|
+
...row,
|
|
2632
|
+
evidence: JSON.parse(row.evidence)
|
|
2633
|
+
};
|
|
2634
|
+
}
|
|
2635
|
+
function confidenceRank2(confidence) {
|
|
2636
|
+
return confidence === "high" ? 2 : confidence === "medium" ? 1 : 0;
|
|
2637
|
+
}
|
|
2638
|
+
function normalizeSkillStep(step) {
|
|
2639
|
+
const codeSpans = [...step.matchAll(/`([^`]+)`/g)].map((match) => match[1].trim()).filter(Boolean);
|
|
2640
|
+
if (codeSpans.length > 0) return codeSpans;
|
|
2641
|
+
const normalized = step.trim().replace(/^(?:[-*]|\d+[.)])\s+/, "").replace(/^(?:run|execute)\s+/i, "").replace(/^`|`$/g, "").trim();
|
|
2642
|
+
return normalized ? [normalized] : [];
|
|
2643
|
+
}
|
|
2644
|
+
async function buildSkillPatterns(db) {
|
|
2645
|
+
const byToken = /* @__PURE__ */ new Map();
|
|
2646
|
+
for (const skill of await listAgentSkills(db)) {
|
|
2647
|
+
if (skill.token_slugs.length !== 1) continue;
|
|
2648
|
+
const patterns = skill.steps.flatMap(normalizeSkillStep);
|
|
2649
|
+
for (const slug of skill.token_slugs) {
|
|
2650
|
+
const tokenPatterns = byToken.get(slug) ?? /* @__PURE__ */ new Set();
|
|
2651
|
+
for (const pattern of patterns) tokenPatterns.add(pattern);
|
|
2652
|
+
byToken.set(slug, tokenPatterns);
|
|
2653
|
+
}
|
|
2654
|
+
}
|
|
2655
|
+
return [...byToken.entries()].map(([slug, patterns]) => ({
|
|
2656
|
+
slug,
|
|
2657
|
+
patterns: [...patterns]
|
|
2658
|
+
}));
|
|
2659
|
+
}
|
|
2660
|
+
function mergePatterns(automatic, explicit) {
|
|
2661
|
+
const merged = /* @__PURE__ */ new Map();
|
|
2662
|
+
for (const entry of [...automatic, ...explicit]) {
|
|
2663
|
+
const patterns = merged.get(entry.slug) ?? /* @__PURE__ */ new Set();
|
|
2664
|
+
for (const pattern of entry.patterns) {
|
|
2665
|
+
const trimmed = pattern.trim();
|
|
2666
|
+
if (trimmed) patterns.add(trimmed);
|
|
2667
|
+
}
|
|
2668
|
+
merged.set(entry.slug, patterns);
|
|
2669
|
+
}
|
|
2670
|
+
return [...merged.entries()].filter(([, patterns]) => patterns.size > 0).map(([slug, patterns]) => ({ slug, patterns: [...patterns] }));
|
|
2671
|
+
}
|
|
2672
|
+
async function getSession(db, sessionId) {
|
|
2673
|
+
const session = await db.prepare("SELECT * FROM sessions WHERE id = ?").get(sessionId);
|
|
2674
|
+
if (!session) throw new Error(`Session not found: ${sessionId}`);
|
|
2675
|
+
return session;
|
|
2676
|
+
}
|
|
2677
|
+
async function getSessionSynthesisRecords(db, sessionId) {
|
|
2678
|
+
const rows = await db.prepare(
|
|
2679
|
+
"SELECT * FROM session_syntheses WHERE session_id = ? ORDER BY created_at"
|
|
2680
|
+
).all(sessionId);
|
|
2681
|
+
return rows.map(parseSynthesisRow);
|
|
2682
|
+
}
|
|
2683
|
+
async function prepareSessionSynthesis(db, input) {
|
|
2684
|
+
const session = await getSession(db, input.sessionId);
|
|
2685
|
+
const patterns = mergePatterns(
|
|
2686
|
+
await buildSkillPatterns(db),
|
|
2687
|
+
input.explicitPatterns ?? []
|
|
2688
|
+
);
|
|
2689
|
+
const validPatterns = [];
|
|
2690
|
+
const tokens = /* @__PURE__ */ new Map();
|
|
2691
|
+
for (const pattern of patterns) {
|
|
2692
|
+
const token = await getTokenBySlug(db, pattern.slug);
|
|
2693
|
+
if (!token || token.deprecated_at) continue;
|
|
2694
|
+
validPatterns.push(pattern);
|
|
2695
|
+
tokens.set(pattern.slug, token);
|
|
2696
|
+
}
|
|
2697
|
+
const applied = new Set(
|
|
2698
|
+
(await getSessionSynthesisRecords(db, input.sessionId)).map(
|
|
2699
|
+
(record) => record.token_id
|
|
2700
|
+
)
|
|
2701
|
+
);
|
|
2702
|
+
const minConfidence = input.minConfidence ?? "medium";
|
|
2703
|
+
if (session.execution_context === "ui") {
|
|
2704
|
+
const reports = readUiObservationLog(input.sessionId);
|
|
2705
|
+
for (const report of reports) {
|
|
2706
|
+
for (const candidate of report.candidateTokens) {
|
|
2707
|
+
if (tokens.has(candidate.slug)) continue;
|
|
2708
|
+
const token = await getTokenBySlug(db, candidate.slug);
|
|
2709
|
+
if (token && !token.deprecated_at) {
|
|
2710
|
+
tokens.set(candidate.slug, token);
|
|
2711
|
+
}
|
|
2712
|
+
}
|
|
2713
|
+
}
|
|
2714
|
+
const { candidates: candidates2, skippedLowConfidence: skippedLowConfidence2 } = buildUiSynthesisCandidates(
|
|
2715
|
+
reports,
|
|
2716
|
+
tokens,
|
|
2717
|
+
applied,
|
|
2718
|
+
minConfidence
|
|
2719
|
+
);
|
|
2720
|
+
return {
|
|
2721
|
+
sessionId: session.id,
|
|
2722
|
+
userId: session.user_id,
|
|
2723
|
+
patternCount: validPatterns.length,
|
|
2724
|
+
commandCount: reports.length,
|
|
2725
|
+
alreadyApplied: applied.size,
|
|
2726
|
+
skippedLowConfidence: skippedLowConfidence2,
|
|
2727
|
+
candidates: candidates2,
|
|
2728
|
+
unmatchedCommands: [],
|
|
2729
|
+
timeSpan: uiObservationTimeSpan(reports)
|
|
2730
|
+
};
|
|
2731
|
+
}
|
|
2732
|
+
const commands = input.commands ?? pairCommands(readMonitorLog(input.sessionId));
|
|
2733
|
+
const analysis = analyzeObservation(commands, validPatterns);
|
|
2734
|
+
const minRank = confidenceRank2(minConfidence);
|
|
2735
|
+
let skippedLowConfidence = 0;
|
|
2736
|
+
const candidates = [];
|
|
2737
|
+
for (const rating of analysis.ratings) {
|
|
2738
|
+
const token = tokens.get(rating.tokenSlug);
|
|
2739
|
+
if (!token || rating.rating == null || applied.has(token.id)) continue;
|
|
2740
|
+
if (confidenceRank2(rating.confidence) < minRank) {
|
|
2741
|
+
skippedLowConfidence++;
|
|
2742
|
+
continue;
|
|
2743
|
+
}
|
|
2744
|
+
candidates.push({
|
|
2745
|
+
tokenId: token.id,
|
|
2746
|
+
tokenSlug: token.slug,
|
|
2747
|
+
concept: token.concept,
|
|
2748
|
+
domain: token.domain,
|
|
2749
|
+
inferredRating: rating.rating,
|
|
2750
|
+
confidence: rating.confidence,
|
|
2751
|
+
evidence: rating.evidence,
|
|
2752
|
+
matchedCommandTexts: rating.matchedCommandTexts
|
|
2753
|
+
});
|
|
2754
|
+
}
|
|
2755
|
+
return {
|
|
2756
|
+
sessionId: session.id,
|
|
2757
|
+
userId: session.user_id,
|
|
2758
|
+
patternCount: validPatterns.length,
|
|
2759
|
+
commandCount: commands.length,
|
|
2760
|
+
alreadyApplied: applied.size,
|
|
2761
|
+
skippedLowConfidence,
|
|
2762
|
+
candidates,
|
|
2763
|
+
unmatchedCommands: analysis.unmatchedCommands,
|
|
2764
|
+
timeSpan: analysis.timeSpan
|
|
2765
|
+
};
|
|
2766
|
+
}
|
|
2767
|
+
async function applySessionSynthesis(db, input) {
|
|
2768
|
+
return db.transaction(async (tx) => {
|
|
2769
|
+
const session = await getSession(tx, input.sessionId);
|
|
2770
|
+
const token = await getTokenBySlug(tx, input.tokenSlug);
|
|
2771
|
+
if (!token || token.deprecated_at) {
|
|
2772
|
+
throw new Error(`Active token not found: ${input.tokenSlug}`);
|
|
2773
|
+
}
|
|
2774
|
+
const existing = await tx.prepare(
|
|
2775
|
+
"SELECT * FROM session_syntheses WHERE session_id = ? AND token_id = ?"
|
|
2776
|
+
).get(session.id, token.id);
|
|
2777
|
+
if (existing) {
|
|
2778
|
+
return { applied: false, record: parseSynthesisRow(existing) };
|
|
2779
|
+
}
|
|
2780
|
+
const card = await ensureCard(tx, token.id, session.user_id);
|
|
2781
|
+
const reviewLogId = ulid7();
|
|
2782
|
+
await evaluateRatingWithinTransaction(tx, {
|
|
2783
|
+
cardId: card.id,
|
|
2784
|
+
tokenId: token.id,
|
|
2785
|
+
userId: session.user_id,
|
|
2786
|
+
rating: input.confirmedRating,
|
|
2787
|
+
sessionId: session.id,
|
|
2788
|
+
reviewLogId
|
|
2789
|
+
});
|
|
2790
|
+
let blocked;
|
|
2791
|
+
if (input.confirmedRating === 1) {
|
|
2792
|
+
const prerequisites = await getPrerequisites(tx, token.id);
|
|
2793
|
+
if (prerequisites.length > 0) {
|
|
2794
|
+
blocked = await cascadeBlock(tx, session.user_id, token.slug);
|
|
2795
|
+
}
|
|
2796
|
+
}
|
|
2797
|
+
const notes = `Observation synthesis (${input.confidence}, inferred ${input.inferredRating}): ${input.matchedCommandTexts.slice(0, 3).join(" | ")}`;
|
|
2798
|
+
const step = await logStep(tx, {
|
|
2799
|
+
session_id: session.id,
|
|
2800
|
+
token_id: token.id,
|
|
2801
|
+
done_by: "user",
|
|
2802
|
+
rating: input.confirmedRating,
|
|
2803
|
+
notes
|
|
2804
|
+
});
|
|
2805
|
+
const evidence = {
|
|
2806
|
+
signals: input.evidence,
|
|
2807
|
+
matchedCommandTexts: input.matchedCommandTexts
|
|
2808
|
+
};
|
|
2809
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2810
|
+
await tx.prepare(
|
|
2811
|
+
`INSERT INTO session_syntheses (
|
|
2812
|
+
session_id, token_id, card_id, inferred_rating, confirmed_rating,
|
|
2813
|
+
confidence, evidence, review_log_id, session_step_id, created_at
|
|
2814
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
2815
|
+
).run(
|
|
2816
|
+
session.id,
|
|
2817
|
+
token.id,
|
|
2818
|
+
card.id,
|
|
2819
|
+
input.inferredRating,
|
|
2820
|
+
input.confirmedRating,
|
|
2821
|
+
input.confidence,
|
|
2822
|
+
JSON.stringify(evidence),
|
|
2823
|
+
reviewLogId,
|
|
2824
|
+
step.id,
|
|
2825
|
+
now
|
|
2826
|
+
);
|
|
2827
|
+
const record = await tx.prepare(
|
|
2828
|
+
"SELECT * FROM session_syntheses WHERE session_id = ? AND token_id = ?"
|
|
2829
|
+
).get(session.id, token.id);
|
|
2830
|
+
return {
|
|
2831
|
+
applied: true,
|
|
2832
|
+
record: parseSynthesisRow(record),
|
|
2833
|
+
blocked
|
|
2834
|
+
};
|
|
2835
|
+
});
|
|
2836
|
+
}
|
|
2837
|
+
|
|
2838
|
+
// src/kernel/observation/shell-hooks.ts
|
|
2839
|
+
function psSingleQuoted(value) {
|
|
2840
|
+
return `'${value.replace(/'/g, "''")}'`;
|
|
2841
|
+
}
|
|
2842
|
+
function generateZshHooks(monitorFile, sessionId) {
|
|
2843
|
+
return `
|
|
2844
|
+
# ZAM monitor hooks for session ${sessionId}
|
|
2845
|
+
export __ZAM_MONITOR_FILE="${monitorFile}"
|
|
2846
|
+
export __ZAM_MONITOR_SEQ=0
|
|
2847
|
+
export __ZAM_MONITOR_SESSION="${sessionId}"
|
|
2848
|
+
|
|
2849
|
+
__zam_ts() {
|
|
2850
|
+
if [[ -n "\${EPOCHREALTIME:-}" ]]; then
|
|
2851
|
+
local sec="\${EPOCHREALTIME%%.*}"
|
|
2852
|
+
local frac="\${EPOCHREALTIME##*.}"
|
|
2853
|
+
frac="\${frac:0:3}"
|
|
2854
|
+
printf '%s.%sZ' "$(date -u -r "$sec" '+%Y-%m-%dT%H:%M:%S' 2>/dev/null || date -u '+%Y-%m-%dT%H:%M:%S')" "$frac"
|
|
2855
|
+
else
|
|
2856
|
+
date -u '+%Y-%m-%dT%H:%M:%SZ'
|
|
2857
|
+
fi
|
|
2858
|
+
}
|
|
2859
|
+
|
|
2860
|
+
__zam_preexec() {
|
|
2861
|
+
(( __ZAM_MONITOR_SEQ++ ))
|
|
2862
|
+
local cmd="\${1//\\"/\\\\\\"}"
|
|
2863
|
+
local cwd="\${PWD//\\"/\\\\\\"}"
|
|
2864
|
+
local ts="$(__zam_ts)"
|
|
2865
|
+
printf '{"type":"command_start","ts":"%s","command":"%s","cwd":"%s","seq":%d,"pid":%d}\\n' \\
|
|
2866
|
+
"$ts" "$cmd" "$cwd" "$__ZAM_MONITOR_SEQ" "$$" \\
|
|
2867
|
+
>> "$__ZAM_MONITOR_FILE"
|
|
2868
|
+
}
|
|
2869
|
+
|
|
2870
|
+
__zam_precmd() {
|
|
2871
|
+
local exit_code=$?
|
|
2872
|
+
[[ $__ZAM_MONITOR_SEQ -eq 0 ]] && return
|
|
2873
|
+
local ts="$(__zam_ts)"
|
|
2874
|
+
printf '{"type":"command_end","ts":"%s","exit_code":%d,"seq":%d,"pid":%d}\\n' \\
|
|
2875
|
+
"$ts" "$exit_code" "$__ZAM_MONITOR_SEQ" "$$" \\
|
|
2876
|
+
>> "$__ZAM_MONITOR_FILE"
|
|
2877
|
+
}
|
|
2878
|
+
|
|
2879
|
+
autoload -Uz add-zsh-hook
|
|
2880
|
+
add-zsh-hook preexec __zam_preexec
|
|
2881
|
+
add-zsh-hook precmd __zam_precmd
|
|
2882
|
+
|
|
2883
|
+
echo "ZAM monitor active for session $__ZAM_MONITOR_SESSION"
|
|
2884
|
+
`.trim();
|
|
2885
|
+
}
|
|
2886
|
+
function generateBashHooks(monitorFile, sessionId) {
|
|
2887
|
+
return `
|
|
2888
|
+
# ZAM monitor hooks for session ${sessionId}
|
|
2889
|
+
export __ZAM_MONITOR_FILE="${monitorFile}"
|
|
2890
|
+
export __ZAM_MONITOR_SEQ=0
|
|
2891
|
+
export __ZAM_MONITOR_SESSION="${sessionId}"
|
|
1568
2892
|
export __ZAM_MONITOR_CMD_ACTIVE=0
|
|
1569
2893
|
|
|
1570
2894
|
__zam_ts() {
|
|
@@ -1643,546 +2967,288 @@ function global:__zam_record_last_history {
|
|
|
1643
2967
|
if ($history.Id -le $global:__ZAM_MONITOR_LAST_HISTORY_ID) { return }
|
|
1644
2968
|
|
|
1645
2969
|
$global:__ZAM_MONITOR_LAST_HISTORY_ID = $history.Id
|
|
1646
|
-
$global:__ZAM_MONITOR_SEQ += 1
|
|
1647
|
-
|
|
1648
|
-
$exitCode = 0
|
|
1649
|
-
if (-not $Success) {
|
|
1650
|
-
if ($NativeExitCode -is [int] -and $NativeExitCode -ne 0) {
|
|
1651
|
-
$exitCode = $NativeExitCode
|
|
1652
|
-
} else {
|
|
1653
|
-
$exitCode = 1
|
|
1654
|
-
}
|
|
1655
|
-
}
|
|
1656
|
-
|
|
1657
|
-
$cwd = (Get-Location).Path
|
|
1658
|
-
__zam_write_monitor_event @{
|
|
1659
|
-
type = "command_start"
|
|
1660
|
-
ts = (__zam_iso_utc $history.StartExecutionTime)
|
|
1661
|
-
command = $history.CommandLine
|
|
1662
|
-
cwd = $cwd
|
|
1663
|
-
seq = $global:__ZAM_MONITOR_SEQ
|
|
1664
|
-
pid = $PID
|
|
1665
|
-
}
|
|
1666
|
-
__zam_write_monitor_event @{
|
|
1667
|
-
type = "command_end"
|
|
1668
|
-
ts = (__zam_iso_utc $history.EndExecutionTime)
|
|
1669
|
-
exit_code = $exitCode
|
|
1670
|
-
seq = $global:__ZAM_MONITOR_SEQ
|
|
1671
|
-
pid = $PID
|
|
1672
|
-
}
|
|
1673
|
-
}
|
|
1674
|
-
|
|
1675
|
-
if (-not (Test-Path function:\\__zam_previous_prompt) -and (Test-Path function:\\prompt)) {
|
|
1676
|
-
Set-Item -Path function:\\__zam_previous_prompt -Value (Get-Item function:\\prompt).ScriptBlock
|
|
1677
|
-
}
|
|
1678
|
-
__zam_update_last_history_id
|
|
1679
|
-
|
|
1680
|
-
function global:prompt {
|
|
1681
|
-
$zamSuccess = $?
|
|
1682
|
-
$zamNativeExitCode = $global:LASTEXITCODE
|
|
1683
|
-
|
|
1684
|
-
if ($global:__ZAM_MONITOR_SKIP_NEXT_PROMPT) {
|
|
1685
|
-
__zam_update_last_history_id
|
|
1686
|
-
$global:__ZAM_MONITOR_SKIP_NEXT_PROMPT = $false
|
|
1687
|
-
} else {
|
|
1688
|
-
__zam_record_last_history -Success $zamSuccess -NativeExitCode $zamNativeExitCode
|
|
1689
|
-
}
|
|
1690
|
-
|
|
1691
|
-
if (Test-Path function:\\__zam_previous_prompt) {
|
|
1692
|
-
& (Get-Item function:\\__zam_previous_prompt).ScriptBlock
|
|
1693
|
-
} else {
|
|
1694
|
-
"PS $($executionContext.SessionState.Path.CurrentLocation)$('>' * ($nestedPromptLevel + 1)) "
|
|
1695
|
-
}
|
|
1696
|
-
}
|
|
1697
|
-
|
|
1698
|
-
Write-Host "ZAM monitor active for session $global:__ZAM_MONITOR_SESSION"
|
|
1699
|
-
`.trim();
|
|
1700
|
-
}
|
|
1701
|
-
function generateZshUnhooks() {
|
|
1702
|
-
return `
|
|
1703
|
-
# Remove ZAM monitor hooks
|
|
1704
|
-
add-zsh-hook -d preexec __zam_preexec 2>/dev/null
|
|
1705
|
-
add-zsh-hook -d precmd __zam_precmd 2>/dev/null
|
|
1706
|
-
unset -f __zam_preexec __zam_precmd __zam_ts 2>/dev/null
|
|
1707
|
-
unset __ZAM_MONITOR_FILE __ZAM_MONITOR_SEQ __ZAM_MONITOR_SESSION 2>/dev/null
|
|
1708
|
-
echo "ZAM monitor stopped."
|
|
1709
|
-
`.trim();
|
|
1710
|
-
}
|
|
1711
|
-
function generateBashUnhooks() {
|
|
1712
|
-
return `
|
|
1713
|
-
# Remove ZAM monitor hooks
|
|
1714
|
-
trap - DEBUG
|
|
1715
|
-
PROMPT_COMMAND="\${PROMPT_COMMAND/__zam_prompt_cmd;/}"
|
|
1716
|
-
unset -f __zam_debug_trap __zam_prompt_cmd __zam_ts 2>/dev/null
|
|
1717
|
-
unset __ZAM_MONITOR_FILE __ZAM_MONITOR_SEQ __ZAM_MONITOR_SESSION __ZAM_MONITOR_CMD_ACTIVE 2>/dev/null
|
|
1718
|
-
echo "ZAM monitor stopped."
|
|
1719
|
-
`.trim();
|
|
1720
|
-
}
|
|
1721
|
-
function generatePowerShellUnhooks() {
|
|
1722
|
-
return `
|
|
1723
|
-
# Remove ZAM monitor hooks
|
|
1724
|
-
if (Test-Path function:\\__zam_previous_prompt) {
|
|
1725
|
-
Set-Item -Path function:\\prompt -Value (Get-Item function:\\__zam_previous_prompt).ScriptBlock
|
|
1726
|
-
Remove-Item function:\\__zam_previous_prompt -Force -ErrorAction SilentlyContinue
|
|
1727
|
-
}
|
|
1728
|
-
Remove-Item function:\\__zam_write_monitor_event,function:\\__zam_iso_utc,function:\\__zam_update_last_history_id,function:\\__zam_record_last_history -ErrorAction SilentlyContinue
|
|
1729
|
-
Remove-Variable -Name __ZAM_MONITOR_FILE,__ZAM_MONITOR_SEQ,__ZAM_MONITOR_SESSION,__ZAM_MONITOR_LAST_HISTORY_ID,__ZAM_MONITOR_SKIP_NEXT_PROMPT -Scope Global -ErrorAction SilentlyContinue
|
|
1730
|
-
Write-Host "ZAM monitor stopped."
|
|
1731
|
-
`.trim();
|
|
1732
|
-
}
|
|
1733
|
-
|
|
1734
|
-
// src/kernel/observation/skill-discovery.ts
|
|
1735
|
-
function discoverSkills(sessionCommands, options = {}) {
|
|
1736
|
-
const minSessions = options.minSessions ?? 2;
|
|
1737
|
-
const minLen = options.minSequenceLength ?? 2;
|
|
1738
|
-
const maxLen = options.maxSequenceLength ?? 5;
|
|
1739
|
-
const existing = new Set(options.existingSkillSlugs ?? []);
|
|
1740
|
-
const sessionSequences = /* @__PURE__ */ new Map();
|
|
1741
|
-
for (const [sessionId, commands] of sessionCommands) {
|
|
1742
|
-
const sequences = extractSequences(commands, minLen, maxLen);
|
|
1743
|
-
if (sequences.length > 0) {
|
|
1744
|
-
sessionSequences.set(sessionId, sequences);
|
|
1745
|
-
}
|
|
1746
|
-
}
|
|
1747
|
-
const sequenceIndex = /* @__PURE__ */ new Map();
|
|
1748
|
-
for (const [, sequences] of sessionSequences) {
|
|
1749
|
-
const seen = /* @__PURE__ */ new Set();
|
|
1750
|
-
for (const seq of sequences) {
|
|
1751
|
-
const key = seq.join(" \u2192 ");
|
|
1752
|
-
if (seen.has(key)) continue;
|
|
1753
|
-
seen.add(key);
|
|
1754
|
-
const entry = sequenceIndex.get(key);
|
|
1755
|
-
if (entry) {
|
|
1756
|
-
entry.sessionCount++;
|
|
1757
|
-
entry.totalOccurrences++;
|
|
1758
|
-
} else {
|
|
1759
|
-
sequenceIndex.set(key, {
|
|
1760
|
-
steps: seq,
|
|
1761
|
-
sessionCount: 1,
|
|
1762
|
-
totalOccurrences: 1,
|
|
1763
|
-
examples: []
|
|
1764
|
-
});
|
|
1765
|
-
}
|
|
1766
|
-
}
|
|
1767
|
-
}
|
|
1768
|
-
const lastSessionId = [...sessionCommands.keys()].pop();
|
|
1769
|
-
if (lastSessionId) {
|
|
1770
|
-
const lastCommands = sessionCommands.get(lastSessionId);
|
|
1771
|
-
for (const [_key, entry] of sequenceIndex) {
|
|
1772
|
-
if (entry.sessionCount >= minSessions) {
|
|
1773
|
-
entry.examples = findExamplesForSequence(lastCommands, entry.steps);
|
|
1774
|
-
}
|
|
1775
|
-
}
|
|
1776
|
-
}
|
|
1777
|
-
const candidates = [...sequenceIndex.values()].filter(
|
|
1778
|
-
(s) => s.sessionCount >= minSessions
|
|
1779
|
-
);
|
|
1780
|
-
const pruned = removeSubsequences(candidates);
|
|
1781
|
-
const proposals = [];
|
|
1782
|
-
for (const seq of pruned) {
|
|
1783
|
-
const slug = generateSlug(seq.steps);
|
|
1784
|
-
if (existing.has(slug)) continue;
|
|
1785
|
-
proposals.push({
|
|
1786
|
-
slug,
|
|
1787
|
-
description: describeSequence(seq.steps),
|
|
1788
|
-
steps: seq.steps,
|
|
1789
|
-
sessionCount: seq.sessionCount,
|
|
1790
|
-
confidence: seq.sessionCount >= 4 ? "high" : seq.sessionCount >= 3 ? "medium" : "low",
|
|
1791
|
-
examples: seq.examples
|
|
1792
|
-
});
|
|
1793
|
-
}
|
|
1794
|
-
const confidenceOrder = { high: 0, medium: 1, low: 2 };
|
|
1795
|
-
proposals.sort((a, b) => {
|
|
1796
|
-
const confDiff = confidenceOrder[a.confidence] - confidenceOrder[b.confidence];
|
|
1797
|
-
if (confDiff !== 0) return confDiff;
|
|
1798
|
-
return b.sessionCount - a.sessionCount;
|
|
1799
|
-
});
|
|
1800
|
-
return proposals;
|
|
1801
|
-
}
|
|
1802
|
-
function normalizeCommand(command) {
|
|
1803
|
-
const parts = command.trim().split(/\s+/);
|
|
1804
|
-
const multiWord = ["docker compose", "npm run", "npx", "git"];
|
|
1805
|
-
const lower = command.toLowerCase();
|
|
1806
|
-
for (const mw of multiWord) {
|
|
1807
|
-
if (lower.startsWith(mw) && parts.length >= mw.split(" ").length + 1) {
|
|
1808
|
-
return parts.slice(0, mw.split(" ").length + 1).join(" ").toLowerCase();
|
|
1809
|
-
}
|
|
1810
|
-
}
|
|
1811
|
-
return parts.slice(0, Math.min(2, parts.length)).join(" ").toLowerCase();
|
|
1812
|
-
}
|
|
1813
|
-
function extractSequences(commands, minLen, maxLen) {
|
|
1814
|
-
const filtered = commands.filter((c) => {
|
|
1815
|
-
const lower = c.command.toLowerCase().trim();
|
|
1816
|
-
return lower.length > 0 && !lower.startsWith("cd ") && lower !== "cd" && lower !== "ls" && lower !== "pwd" && lower !== "clear" && lower !== "exit" && !lower.startsWith("echo ");
|
|
1817
|
-
});
|
|
1818
|
-
const normalized = filtered.map((c) => normalizeCommand(c.command));
|
|
1819
|
-
const sequences = [];
|
|
1820
|
-
for (let len = minLen; len <= maxLen; len++) {
|
|
1821
|
-
for (let i = 0; i <= normalized.length - len; i++) {
|
|
1822
|
-
const seq = normalized.slice(i, i + len);
|
|
1823
|
-
if (new Set(seq).size >= 2) {
|
|
1824
|
-
sequences.push(seq);
|
|
1825
|
-
}
|
|
1826
|
-
}
|
|
1827
|
-
}
|
|
1828
|
-
return sequences;
|
|
1829
|
-
}
|
|
1830
|
-
function findExamplesForSequence(commands, steps) {
|
|
1831
|
-
const normalized = commands.map((c) => ({
|
|
1832
|
-
norm: normalizeCommand(c.command),
|
|
1833
|
-
full: c.command
|
|
1834
|
-
}));
|
|
1835
|
-
for (let i = 0; i <= normalized.length - steps.length; i++) {
|
|
1836
|
-
let match = true;
|
|
1837
|
-
for (let j = 0; j < steps.length; j++) {
|
|
1838
|
-
if (normalized[i + j].norm !== steps[j]) {
|
|
1839
|
-
match = false;
|
|
1840
|
-
break;
|
|
1841
|
-
}
|
|
1842
|
-
}
|
|
1843
|
-
if (match) {
|
|
1844
|
-
return normalized.slice(i, i + steps.length).map((n) => n.full);
|
|
1845
|
-
}
|
|
1846
|
-
}
|
|
1847
|
-
return [];
|
|
1848
|
-
}
|
|
1849
|
-
function removeSubsequences(candidates) {
|
|
1850
|
-
const sorted = [...candidates].sort(
|
|
1851
|
-
(a, b) => b.steps.length - a.steps.length
|
|
1852
|
-
);
|
|
1853
|
-
const result = [];
|
|
1854
|
-
for (const candidate of sorted) {
|
|
1855
|
-
const key = candidate.steps.join(" \u2192 ");
|
|
1856
|
-
const isSubsequence = result.some((longer) => {
|
|
1857
|
-
const longerKey = longer.steps.join(" \u2192 ");
|
|
1858
|
-
return longerKey.includes(key) && longerKey !== key;
|
|
1859
|
-
});
|
|
1860
|
-
if (!isSubsequence) {
|
|
1861
|
-
result.push(candidate);
|
|
2970
|
+
$global:__ZAM_MONITOR_SEQ += 1
|
|
2971
|
+
|
|
2972
|
+
$exitCode = 0
|
|
2973
|
+
if (-not $Success) {
|
|
2974
|
+
if ($NativeExitCode -is [int] -and $NativeExitCode -ne 0) {
|
|
2975
|
+
$exitCode = $NativeExitCode
|
|
2976
|
+
} else {
|
|
2977
|
+
$exitCode = 1
|
|
1862
2978
|
}
|
|
1863
2979
|
}
|
|
1864
|
-
return result;
|
|
1865
|
-
}
|
|
1866
|
-
function generateSlug(steps) {
|
|
1867
|
-
return steps.map((s) => {
|
|
1868
|
-
const parts = s.split(/\s+/);
|
|
1869
|
-
return parts[parts.length - 1];
|
|
1870
|
-
}).join("-");
|
|
1871
|
-
}
|
|
1872
|
-
function describeSequence(steps) {
|
|
1873
|
-
return `Recurring pattern: ${steps.join(" \u2192 ")}`;
|
|
1874
|
-
}
|
|
1875
2980
|
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
2981
|
+
$cwd = (Get-Location).Path
|
|
2982
|
+
__zam_write_monitor_event @{
|
|
2983
|
+
type = "command_start"
|
|
2984
|
+
ts = (__zam_iso_utc $history.StartExecutionTime)
|
|
2985
|
+
command = $history.CommandLine
|
|
2986
|
+
cwd = $cwd
|
|
2987
|
+
seq = $global:__ZAM_MONITOR_SEQ
|
|
2988
|
+
pid = $PID
|
|
1881
2989
|
}
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
for (const prereq of prereqs) {
|
|
1889
|
-
const card = ensureCard(db, prereq.requires_id, userId);
|
|
1890
|
-
if (card.blocked === 1) {
|
|
1891
|
-
const prereqOfPrereq = db.prepare("SELECT COUNT(*) as n FROM prerequisites WHERE token_id = ?").get(prereq.requires_id);
|
|
1892
|
-
if (prereqOfPrereq.n === 0) {
|
|
1893
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1894
|
-
db.prepare(
|
|
1895
|
-
"UPDATE cards SET blocked = 0, due_at = ? WHERE token_id = ? AND user_id = ?"
|
|
1896
|
-
).run(now, prereq.requires_id, userId);
|
|
1897
|
-
}
|
|
1898
|
-
}
|
|
1899
|
-
surfaced.push({
|
|
1900
|
-
slug: prereq.slug,
|
|
1901
|
-
concept: prereq.concept,
|
|
1902
|
-
bloomLevel: prereq.bloom_level
|
|
1903
|
-
});
|
|
2990
|
+
__zam_write_monitor_event @{
|
|
2991
|
+
type = "command_end"
|
|
2992
|
+
ts = (__zam_iso_utc $history.EndExecutionTime)
|
|
2993
|
+
exit_code = $exitCode
|
|
2994
|
+
seq = $global:__ZAM_MONITOR_SEQ
|
|
2995
|
+
pid = $PID
|
|
1904
2996
|
}
|
|
1905
|
-
return {
|
|
1906
|
-
blockedSlug: tokenSlug,
|
|
1907
|
-
prerequisites: surfaced
|
|
1908
|
-
};
|
|
1909
2997
|
}
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
FROM cards c
|
|
1914
|
-
JOIN tokens t ON t.id = c.token_id
|
|
1915
|
-
WHERE c.user_id = ? AND c.blocked = 1`
|
|
1916
|
-
).all(userId);
|
|
1917
|
-
const unblocked = [];
|
|
1918
|
-
for (const card of blockedCards) {
|
|
1919
|
-
const totalPrereqs = db.prepare("SELECT COUNT(*) as n FROM prerequisites WHERE token_id = ?").get(card.token_id);
|
|
1920
|
-
const metPrereqs = db.prepare(
|
|
1921
|
-
`SELECT COUNT(*) as n FROM cards c
|
|
1922
|
-
JOIN prerequisites p ON p.requires_id = c.token_id
|
|
1923
|
-
WHERE p.token_id = ? AND c.user_id = ? AND c.reps >= 1 AND c.blocked = 0`
|
|
1924
|
-
).get(card.token_id, userId);
|
|
1925
|
-
if (totalPrereqs.n === 0 || metPrereqs.n === totalPrereqs.n) {
|
|
1926
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1927
|
-
db.prepare(
|
|
1928
|
-
"UPDATE cards SET blocked = 0, due_at = ? WHERE token_id = ? AND user_id = ?"
|
|
1929
|
-
).run(now, card.token_id, userId);
|
|
1930
|
-
unblocked.push({ slug: card.slug, concept: card.concept });
|
|
1931
|
-
}
|
|
1932
|
-
}
|
|
1933
|
-
return { unblocked };
|
|
2998
|
+
|
|
2999
|
+
if (-not (Test-Path function:\\__zam_previous_prompt) -and (Test-Path function:\\prompt)) {
|
|
3000
|
+
Set-Item -Path function:\\__zam_previous_prompt -Value (Get-Item function:\\prompt).ScriptBlock
|
|
1934
3001
|
}
|
|
3002
|
+
__zam_update_last_history_id
|
|
1935
3003
|
|
|
1936
|
-
|
|
1937
|
-
|
|
3004
|
+
function global:prompt {
|
|
3005
|
+
$zamSuccess = $?
|
|
3006
|
+
$zamNativeExitCode = $global:LASTEXITCODE
|
|
1938
3007
|
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
1.5988,
|
|
1952
|
-
0.1176,
|
|
1953
|
-
1.0014,
|
|
1954
|
-
// w7–w10: stability after forgetting
|
|
1955
|
-
2.0032,
|
|
1956
|
-
0.0266,
|
|
1957
|
-
0.3077,
|
|
1958
|
-
0.15,
|
|
1959
|
-
// w11–w14: stability increase
|
|
1960
|
-
0,
|
|
1961
|
-
2.7849,
|
|
1962
|
-
0.3477,
|
|
1963
|
-
0.6831
|
|
1964
|
-
// w15–w18: additional parameters
|
|
1965
|
-
];
|
|
1966
|
-
var DEFAULT_REQUEST_RETENTION = 0.9;
|
|
1967
|
-
function clamp(value, lo, hi) {
|
|
1968
|
-
return Math.min(hi, Math.max(lo, value));
|
|
1969
|
-
}
|
|
1970
|
-
function daysBetween(a, b) {
|
|
1971
|
-
return (b.getTime() - a.getTime()) / (1e3 * 60 * 60 * 24);
|
|
1972
|
-
}
|
|
1973
|
-
function initialStability(w, rating) {
|
|
1974
|
-
return w[rating - 1];
|
|
1975
|
-
}
|
|
1976
|
-
function initialDifficulty(w, rating) {
|
|
1977
|
-
return clamp(w[4] - Math.exp(w[5] * (rating - 1)) + 1, 1, 10);
|
|
3008
|
+
if ($global:__ZAM_MONITOR_SKIP_NEXT_PROMPT) {
|
|
3009
|
+
__zam_update_last_history_id
|
|
3010
|
+
$global:__ZAM_MONITOR_SKIP_NEXT_PROMPT = $false
|
|
3011
|
+
} else {
|
|
3012
|
+
__zam_record_last_history -Success $zamSuccess -NativeExitCode $zamNativeExitCode
|
|
3013
|
+
}
|
|
3014
|
+
|
|
3015
|
+
if (Test-Path function:\\__zam_previous_prompt) {
|
|
3016
|
+
& (Get-Item function:\\__zam_previous_prompt).ScriptBlock
|
|
3017
|
+
} else {
|
|
3018
|
+
"PS $($executionContext.SessionState.Path.CurrentLocation)$('>' * ($nestedPromptLevel + 1)) "
|
|
3019
|
+
}
|
|
1978
3020
|
}
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
return clamp(updated, 1, 10);
|
|
3021
|
+
|
|
3022
|
+
Write-Host "ZAM monitor active for session $global:__ZAM_MONITOR_SESSION"
|
|
3023
|
+
`.trim();
|
|
1983
3024
|
}
|
|
1984
|
-
function
|
|
1985
|
-
|
|
1986
|
-
|
|
3025
|
+
function generateZshUnhooks() {
|
|
3026
|
+
return `
|
|
3027
|
+
# Remove ZAM monitor hooks
|
|
3028
|
+
add-zsh-hook -d preexec __zam_preexec 2>/dev/null
|
|
3029
|
+
add-zsh-hook -d precmd __zam_precmd 2>/dev/null
|
|
3030
|
+
unset -f __zam_preexec __zam_precmd __zam_ts 2>/dev/null
|
|
3031
|
+
unset __ZAM_MONITOR_FILE __ZAM_MONITOR_SEQ __ZAM_MONITOR_SESSION 2>/dev/null
|
|
3032
|
+
echo "ZAM monitor stopped."
|
|
3033
|
+
`.trim();
|
|
1987
3034
|
}
|
|
1988
|
-
function
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
3035
|
+
function generateBashUnhooks() {
|
|
3036
|
+
return `
|
|
3037
|
+
# Remove ZAM monitor hooks
|
|
3038
|
+
trap - DEBUG
|
|
3039
|
+
PROMPT_COMMAND="\${PROMPT_COMMAND/__zam_prompt_cmd;/}"
|
|
3040
|
+
unset -f __zam_debug_trap __zam_prompt_cmd __zam_ts 2>/dev/null
|
|
3041
|
+
unset __ZAM_MONITOR_FILE __ZAM_MONITOR_SEQ __ZAM_MONITOR_SESSION __ZAM_MONITOR_CMD_ACTIVE 2>/dev/null
|
|
3042
|
+
echo "ZAM monitor stopped."
|
|
3043
|
+
`.trim();
|
|
1993
3044
|
}
|
|
1994
|
-
function
|
|
1995
|
-
return
|
|
3045
|
+
function generatePowerShellUnhooks() {
|
|
3046
|
+
return `
|
|
3047
|
+
# Remove ZAM monitor hooks
|
|
3048
|
+
if (Test-Path function:\\__zam_previous_prompt) {
|
|
3049
|
+
Set-Item -Path function:\\prompt -Value (Get-Item function:\\__zam_previous_prompt).ScriptBlock
|
|
3050
|
+
Remove-Item function:\\__zam_previous_prompt -Force -ErrorAction SilentlyContinue
|
|
1996
3051
|
}
|
|
1997
|
-
function
|
|
1998
|
-
|
|
1999
|
-
|
|
3052
|
+
Remove-Item function:\\__zam_write_monitor_event,function:\\__zam_iso_utc,function:\\__zam_update_last_history_id,function:\\__zam_record_last_history -ErrorAction SilentlyContinue
|
|
3053
|
+
Remove-Variable -Name __ZAM_MONITOR_FILE,__ZAM_MONITOR_SEQ,__ZAM_MONITOR_SESSION,__ZAM_MONITOR_LAST_HISTORY_ID,__ZAM_MONITOR_SKIP_NEXT_PROMPT -Scope Global -ErrorAction SilentlyContinue
|
|
3054
|
+
Write-Host "ZAM monitor stopped."
|
|
3055
|
+
`.trim();
|
|
2000
3056
|
}
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
const interval2 = nextInterval(s, resolvedParams.requestRetention);
|
|
2014
|
-
const dueAt2 = new Date(reviewTime);
|
|
2015
|
-
dueAt2.setDate(dueAt2.getDate() + interval2);
|
|
2016
|
-
return {
|
|
2017
|
-
stability: s,
|
|
2018
|
-
difficulty: d,
|
|
2019
|
-
elapsedDays: 0,
|
|
2020
|
-
scheduledDays: interval2,
|
|
2021
|
-
reps: rating >= 2 ? 1 : 0,
|
|
2022
|
-
lapses: rating === 1 ? 1 : 0,
|
|
2023
|
-
state: "learning",
|
|
2024
|
-
dueAt: dueAt2,
|
|
2025
|
-
lastReviewAt: reviewTime
|
|
2026
|
-
};
|
|
3057
|
+
|
|
3058
|
+
// src/kernel/observation/skill-discovery.ts
|
|
3059
|
+
function discoverSkills(sessionCommands, options = {}) {
|
|
3060
|
+
const minSessions = options.minSessions ?? 2;
|
|
3061
|
+
const minLen = options.minSequenceLength ?? 2;
|
|
3062
|
+
const maxLen = options.maxSequenceLength ?? 5;
|
|
3063
|
+
const existing = new Set(options.existingSkillSlugs ?? []);
|
|
3064
|
+
const sessionSequences = /* @__PURE__ */ new Map();
|
|
3065
|
+
for (const [sessionId, commands] of sessionCommands) {
|
|
3066
|
+
const sequences = extractSequences(commands, minLen, maxLen);
|
|
3067
|
+
if (sequences.length > 0) {
|
|
3068
|
+
sessionSequences.set(sessionId, sequences);
|
|
2027
3069
|
}
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
card.stability,
|
|
2049
|
-
card.difficulty,
|
|
2050
|
-
r,
|
|
2051
|
-
rating
|
|
2052
|
-
);
|
|
2053
|
-
newDifficulty = nextDifficulty(w, card.difficulty, rating);
|
|
2054
|
-
newReps = card.reps + 1;
|
|
2055
|
-
newLapses = card.lapses;
|
|
2056
|
-
newState = "review";
|
|
3070
|
+
}
|
|
3071
|
+
const sequenceIndex = /* @__PURE__ */ new Map();
|
|
3072
|
+
for (const [, sequences] of sessionSequences) {
|
|
3073
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3074
|
+
for (const seq of sequences) {
|
|
3075
|
+
const key = seq.join(" \u2192 ");
|
|
3076
|
+
if (seen.has(key)) continue;
|
|
3077
|
+
seen.add(key);
|
|
3078
|
+
const entry = sequenceIndex.get(key);
|
|
3079
|
+
if (entry) {
|
|
3080
|
+
entry.sessionCount++;
|
|
3081
|
+
entry.totalOccurrences++;
|
|
3082
|
+
} else {
|
|
3083
|
+
sequenceIndex.set(key, {
|
|
3084
|
+
steps: seq,
|
|
3085
|
+
sessionCount: 1,
|
|
3086
|
+
totalOccurrences: 1,
|
|
3087
|
+
examples: []
|
|
3088
|
+
});
|
|
3089
|
+
}
|
|
2057
3090
|
}
|
|
2058
|
-
const interval = nextInterval(
|
|
2059
|
-
newStability,
|
|
2060
|
-
resolvedParams.requestRetention
|
|
2061
|
-
);
|
|
2062
|
-
const dueAt = new Date(reviewTime);
|
|
2063
|
-
dueAt.setDate(dueAt.getDate() + interval);
|
|
2064
|
-
return {
|
|
2065
|
-
stability: newStability,
|
|
2066
|
-
difficulty: newDifficulty,
|
|
2067
|
-
elapsedDays: elapsed,
|
|
2068
|
-
scheduledDays: interval,
|
|
2069
|
-
reps: newReps,
|
|
2070
|
-
lapses: newLapses,
|
|
2071
|
-
state: newState,
|
|
2072
|
-
dueAt,
|
|
2073
|
-
lastReviewAt: reviewTime
|
|
2074
|
-
};
|
|
2075
3091
|
}
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
3092
|
+
const lastSessionId = [...sessionCommands.keys()].pop();
|
|
3093
|
+
if (lastSessionId) {
|
|
3094
|
+
const lastCommands = sessionCommands.get(lastSessionId);
|
|
3095
|
+
for (const [_key, entry] of sequenceIndex) {
|
|
3096
|
+
if (entry.sessionCount >= minSessions) {
|
|
3097
|
+
entry.examples = findExamplesForSequence(lastCommands, entry.steps);
|
|
3098
|
+
}
|
|
3099
|
+
}
|
|
3100
|
+
}
|
|
3101
|
+
const candidates = [...sequenceIndex.values()].filter(
|
|
3102
|
+
(s) => s.sessionCount >= minSessions
|
|
3103
|
+
);
|
|
3104
|
+
const pruned = removeSubsequences(candidates);
|
|
3105
|
+
const proposals = [];
|
|
3106
|
+
for (const seq of pruned) {
|
|
3107
|
+
const slug = generateSlug(seq.steps);
|
|
3108
|
+
if (existing.has(slug)) continue;
|
|
3109
|
+
proposals.push({
|
|
3110
|
+
slug,
|
|
3111
|
+
description: describeSequence(seq.steps),
|
|
3112
|
+
steps: seq.steps,
|
|
3113
|
+
sessionCount: seq.sessionCount,
|
|
3114
|
+
confidence: seq.sessionCount >= 4 ? "high" : seq.sessionCount >= 3 ? "medium" : "low",
|
|
3115
|
+
examples: seq.examples
|
|
3116
|
+
});
|
|
3117
|
+
}
|
|
3118
|
+
const confidenceOrder = { high: 0, medium: 1, low: 2 };
|
|
3119
|
+
proposals.sort((a, b) => {
|
|
3120
|
+
const confDiff = confidenceOrder[a.confidence] - confidenceOrder[b.confidence];
|
|
3121
|
+
if (confDiff !== 0) return confDiff;
|
|
3122
|
+
return b.sessionCount - a.sessionCount;
|
|
3123
|
+
});
|
|
3124
|
+
return proposals;
|
|
2080
3125
|
}
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
const
|
|
2085
|
-
|
|
2086
|
-
|
|
3126
|
+
function normalizeCommand(command) {
|
|
3127
|
+
const parts = command.trim().split(/\s+/);
|
|
3128
|
+
const multiWord = ["docker compose", "npm run", "npx", "git"];
|
|
3129
|
+
const lower = command.toLowerCase();
|
|
3130
|
+
for (const mw of multiWord) {
|
|
3131
|
+
if (lower.startsWith(mw) && parts.length >= mw.split(" ").length + 1) {
|
|
3132
|
+
return parts.slice(0, mw.split(" ").length + 1).join(" ").toLowerCase();
|
|
3133
|
+
}
|
|
2087
3134
|
}
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
scheduledDays: card.scheduled_days,
|
|
2095
|
-
reps: card.reps,
|
|
2096
|
-
lapses: card.lapses,
|
|
2097
|
-
state: card.state,
|
|
2098
|
-
dueAt: new Date(card.due_at),
|
|
2099
|
-
lastReviewAt: card.last_review_at ? new Date(card.last_review_at) : null
|
|
2100
|
-
};
|
|
2101
|
-
const updated = fsrs.schedule(schedulingCard, input.rating, now);
|
|
2102
|
-
updateCard(db, input.cardId, {
|
|
2103
|
-
stability: updated.stability,
|
|
2104
|
-
difficulty: updated.difficulty,
|
|
2105
|
-
elapsed_days: updated.elapsedDays,
|
|
2106
|
-
scheduled_days: updated.scheduledDays,
|
|
2107
|
-
reps: updated.reps,
|
|
2108
|
-
lapses: updated.lapses,
|
|
2109
|
-
state: updated.state,
|
|
2110
|
-
due_at: updated.dueAt.toISOString(),
|
|
2111
|
-
last_review_at: now.toISOString()
|
|
3135
|
+
return parts.slice(0, Math.min(2, parts.length)).join(" ").toLowerCase();
|
|
3136
|
+
}
|
|
3137
|
+
function extractSequences(commands, minLen, maxLen) {
|
|
3138
|
+
const filtered = commands.filter((c) => {
|
|
3139
|
+
const lower = c.command.toLowerCase().trim();
|
|
3140
|
+
return lower.length > 0 && !lower.startsWith("cd ") && lower !== "cd" && lower !== "ls" && lower !== "pwd" && lower !== "clear" && lower !== "exit" && !lower.startsWith("echo ");
|
|
2112
3141
|
});
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
3142
|
+
const normalized = filtered.map((c) => normalizeCommand(c.command));
|
|
3143
|
+
const sequences = [];
|
|
3144
|
+
for (let len = minLen; len <= maxLen; len++) {
|
|
3145
|
+
for (let i = 0; i <= normalized.length - len; i++) {
|
|
3146
|
+
const seq = normalized.slice(i, i + len);
|
|
3147
|
+
if (new Set(seq).size >= 2) {
|
|
3148
|
+
sequences.push(seq);
|
|
3149
|
+
}
|
|
3150
|
+
}
|
|
3151
|
+
}
|
|
3152
|
+
return sequences;
|
|
3153
|
+
}
|
|
3154
|
+
function findExamplesForSequence(commands, steps) {
|
|
3155
|
+
const normalized = commands.map((c) => ({
|
|
3156
|
+
norm: normalizeCommand(c.command),
|
|
3157
|
+
full: c.command
|
|
3158
|
+
}));
|
|
3159
|
+
for (let i = 0; i <= normalized.length - steps.length; i++) {
|
|
3160
|
+
let match = true;
|
|
3161
|
+
for (let j = 0; j < steps.length; j++) {
|
|
3162
|
+
if (normalized[i + j].norm !== steps[j]) {
|
|
3163
|
+
match = false;
|
|
3164
|
+
break;
|
|
3165
|
+
}
|
|
3166
|
+
}
|
|
3167
|
+
if (match) {
|
|
3168
|
+
return normalized.slice(i, i + steps.length).map((n) => n.full);
|
|
3169
|
+
}
|
|
3170
|
+
}
|
|
3171
|
+
return [];
|
|
3172
|
+
}
|
|
3173
|
+
function removeSubsequences(candidates) {
|
|
3174
|
+
const sorted = [...candidates].sort(
|
|
3175
|
+
(a, b) => b.steps.length - a.steps.length
|
|
2126
3176
|
);
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
3177
|
+
const result = [];
|
|
3178
|
+
for (const candidate of sorted) {
|
|
3179
|
+
const key = candidate.steps.join(" \u2192 ");
|
|
3180
|
+
const isSubsequence = result.some((longer) => {
|
|
3181
|
+
const longerKey = longer.steps.join(" \u2192 ");
|
|
3182
|
+
return longerKey.includes(key) && longerKey !== key;
|
|
3183
|
+
});
|
|
3184
|
+
if (!isSubsequence) {
|
|
3185
|
+
result.push(candidate);
|
|
3186
|
+
}
|
|
3187
|
+
}
|
|
3188
|
+
return result;
|
|
3189
|
+
}
|
|
3190
|
+
function generateSlug(steps) {
|
|
3191
|
+
return steps.map((s) => {
|
|
3192
|
+
const parts = s.split(/\s+/);
|
|
3193
|
+
return parts[parts.length - 1];
|
|
3194
|
+
}).join("-");
|
|
3195
|
+
}
|
|
3196
|
+
function describeSequence(steps) {
|
|
3197
|
+
return `Recurring pattern: ${steps.join(" \u2192 ")}`;
|
|
2136
3198
|
}
|
|
2137
3199
|
|
|
2138
3200
|
// src/kernel/recall/actions.ts
|
|
2139
|
-
function getReviewTarget(db, cardId, userId) {
|
|
2140
|
-
const card = getCardById(db, cardId);
|
|
3201
|
+
async function getReviewTarget(db, cardId, userId) {
|
|
3202
|
+
const card = await getCardById(db, cardId);
|
|
2141
3203
|
if (!card) {
|
|
2142
3204
|
throw new Error(`Card not found: ${cardId}`);
|
|
2143
3205
|
}
|
|
2144
3206
|
if (card.user_id !== userId) {
|
|
2145
3207
|
throw new Error(`Card ${cardId} does not belong to user ${userId}`);
|
|
2146
3208
|
}
|
|
2147
|
-
const token = getTokenById(db, card.token_id);
|
|
3209
|
+
const token = await getTokenById(db, card.token_id);
|
|
2148
3210
|
if (!token) {
|
|
2149
3211
|
throw new Error(`Token not found for card ${cardId}`);
|
|
2150
3212
|
}
|
|
2151
3213
|
return { cardId: card.id, token };
|
|
2152
3214
|
}
|
|
2153
|
-
function executeReviewAction(db, input) {
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
const
|
|
2161
|
-
|
|
2162
|
-
|
|
3215
|
+
async function executeReviewAction(db, input) {
|
|
3216
|
+
if (input.action === "rate") {
|
|
3217
|
+
if (input.rating == null) {
|
|
3218
|
+
throw new Error("rating is required for action=rate");
|
|
3219
|
+
}
|
|
3220
|
+
const rating = input.rating;
|
|
3221
|
+
return db.transaction(async (tx) => {
|
|
3222
|
+
const target2 = await getReviewTarget(tx, input.cardId, input.userId);
|
|
3223
|
+
const evaluation = await evaluateRatingWithinTransaction(tx, {
|
|
3224
|
+
cardId: target2.cardId,
|
|
3225
|
+
tokenId: target2.token.id,
|
|
2163
3226
|
userId: input.userId,
|
|
2164
|
-
rating
|
|
3227
|
+
rating
|
|
2165
3228
|
});
|
|
2166
3229
|
let blocked;
|
|
2167
|
-
if (
|
|
2168
|
-
const prereqs = getPrerequisites(
|
|
3230
|
+
if (rating === 1) {
|
|
3231
|
+
const prereqs = await getPrerequisites(tx, target2.token.id);
|
|
2169
3232
|
if (prereqs.length > 0) {
|
|
2170
|
-
blocked = cascadeBlock(
|
|
3233
|
+
blocked = await cascadeBlock(tx, input.userId, target2.token.slug);
|
|
2171
3234
|
}
|
|
2172
3235
|
}
|
|
2173
3236
|
return {
|
|
2174
3237
|
action: input.action,
|
|
2175
|
-
token:
|
|
3238
|
+
token: target2.token,
|
|
2176
3239
|
evaluation,
|
|
2177
3240
|
blocked
|
|
2178
3241
|
};
|
|
2179
|
-
}
|
|
3242
|
+
});
|
|
3243
|
+
}
|
|
3244
|
+
const target = await getReviewTarget(db, input.cardId, input.userId);
|
|
3245
|
+
switch (input.action) {
|
|
2180
3246
|
case "skip":
|
|
2181
3247
|
return { action: input.action, token: target.token, skipped: true };
|
|
2182
3248
|
case "stop":
|
|
2183
3249
|
return { action: input.action, token: target.token, stopped: true };
|
|
2184
3250
|
case "edit-token": {
|
|
2185
|
-
const updatedToken = updateToken(
|
|
3251
|
+
const updatedToken = await updateToken(
|
|
2186
3252
|
db,
|
|
2187
3253
|
target.token.slug,
|
|
2188
3254
|
input.tokenUpdates ?? {}
|
|
@@ -2194,7 +3260,7 @@ function executeReviewAction(db, input) {
|
|
|
2194
3260
|
};
|
|
2195
3261
|
}
|
|
2196
3262
|
case "deprecate-token": {
|
|
2197
|
-
const updatedToken = deprecateToken(db, target.token.slug);
|
|
3263
|
+
const updatedToken = await deprecateToken(db, target.token.slug);
|
|
2198
3264
|
return {
|
|
2199
3265
|
action: input.action,
|
|
2200
3266
|
token: target.token,
|
|
@@ -2202,7 +3268,7 @@ function executeReviewAction(db, input) {
|
|
|
2202
3268
|
};
|
|
2203
3269
|
}
|
|
2204
3270
|
case "delete-token": {
|
|
2205
|
-
const deletedToken = deleteToken(db, target.token.slug);
|
|
3271
|
+
const deletedToken = await deleteToken(db, target.token.slug);
|
|
2206
3272
|
return {
|
|
2207
3273
|
action: input.action,
|
|
2208
3274
|
token: target.token,
|
|
@@ -2210,7 +3276,11 @@ function executeReviewAction(db, input) {
|
|
|
2210
3276
|
};
|
|
2211
3277
|
}
|
|
2212
3278
|
case "delete-card": {
|
|
2213
|
-
const deletedCard = deleteCardForUser(
|
|
3279
|
+
const deletedCard = await deleteCardForUser(
|
|
3280
|
+
db,
|
|
3281
|
+
target.token.id,
|
|
3282
|
+
input.userId
|
|
3283
|
+
);
|
|
2214
3284
|
return {
|
|
2215
3285
|
action: input.action,
|
|
2216
3286
|
token: target.token,
|
|
@@ -2264,8 +3334,8 @@ function generatePrompt(input) {
|
|
|
2264
3334
|
}
|
|
2265
3335
|
|
|
2266
3336
|
// src/kernel/recall/reference-resolver.ts
|
|
2267
|
-
import { existsSync as
|
|
2268
|
-
import { dirname as dirname3, join as
|
|
3337
|
+
import { existsSync as existsSync6, readFileSync as readFileSync6 } from "fs";
|
|
3338
|
+
import { dirname as dirname3, join as join6, resolve } from "path";
|
|
2269
3339
|
var DEFAULT_REVIEW_CONTEXT_MAX_CHARS = 6e3;
|
|
2270
3340
|
function htmlToText(html) {
|
|
2271
3341
|
let content = html;
|
|
@@ -2326,11 +3396,11 @@ async function resolveReference(sourceLink) {
|
|
|
2326
3396
|
const filePath = anchorIndex2 !== -1 ? fullPathWithAnchor.slice(0, anchorIndex2) : fullPathWithAnchor;
|
|
2327
3397
|
const anchor2 = anchorIndex2 !== -1 ? fullPathWithAnchor.slice(anchorIndex2) : "";
|
|
2328
3398
|
const parentDir = dirname3(process.cwd());
|
|
2329
|
-
const localRepoPath =
|
|
2330
|
-
const localFilePath =
|
|
2331
|
-
if (
|
|
3399
|
+
const localRepoPath = join6(parentDir, repo);
|
|
3400
|
+
const localFilePath = join6(localRepoPath, filePath);
|
|
3401
|
+
if (existsSync6(localFilePath)) {
|
|
2332
3402
|
try {
|
|
2333
|
-
let fileContent =
|
|
3403
|
+
let fileContent = readFileSync6(localFilePath, "utf-8");
|
|
2334
3404
|
if (anchor2) {
|
|
2335
3405
|
fileContent = extractLines(fileContent, anchor2);
|
|
2336
3406
|
}
|
|
@@ -2384,9 +3454,9 @@ Link: ${cleaned}`,
|
|
|
2384
3454
|
const relativePath = anchorIndex !== -1 ? cleaned.slice(0, anchorIndex) : cleaned;
|
|
2385
3455
|
const anchor = anchorIndex !== -1 ? cleaned.slice(anchorIndex) : "";
|
|
2386
3456
|
const absolutePath = resolve(process.cwd(), relativePath);
|
|
2387
|
-
if (
|
|
3457
|
+
if (existsSync6(absolutePath)) {
|
|
2388
3458
|
try {
|
|
2389
|
-
let fileContent =
|
|
3459
|
+
let fileContent = readFileSync6(absolutePath, "utf-8");
|
|
2390
3460
|
if (anchor) {
|
|
2391
3461
|
fileContent = extractLines(fileContent, anchor);
|
|
2392
3462
|
}
|
|
@@ -2514,12 +3584,12 @@ function interleave(items, maxConsecutive = 2) {
|
|
|
2514
3584
|
}
|
|
2515
3585
|
|
|
2516
3586
|
// src/kernel/scheduler/queue.ts
|
|
2517
|
-
function buildReviewQueue(db, options) {
|
|
3587
|
+
async function buildReviewQueue(db, options) {
|
|
2518
3588
|
const maxNew = options.maxNew ?? 10;
|
|
2519
3589
|
const maxReviews = options.maxReviews ?? 50;
|
|
2520
3590
|
const now = options.now ?? /* @__PURE__ */ new Date();
|
|
2521
3591
|
const nowISO = now.toISOString();
|
|
2522
|
-
const dueRows = db.prepare(
|
|
3592
|
+
const dueRows = await db.prepare(
|
|
2523
3593
|
`SELECT
|
|
2524
3594
|
c.id AS card_id,
|
|
2525
3595
|
c.token_id AS token_id,
|
|
@@ -2540,7 +3610,7 @@ function buildReviewQueue(db, options) {
|
|
|
2540
3610
|
AND t.deprecated_at IS NULL
|
|
2541
3611
|
ORDER BY c.due_at ASC`
|
|
2542
3612
|
).all(options.userId, nowISO);
|
|
2543
|
-
const newRows = db.prepare(
|
|
3613
|
+
const newRows = await db.prepare(
|
|
2544
3614
|
`SELECT
|
|
2545
3615
|
c.id AS card_id,
|
|
2546
3616
|
c.token_id AS token_id,
|
|
@@ -2638,44 +3708,44 @@ function intersperseNew(reviews, newCards, interval) {
|
|
|
2638
3708
|
|
|
2639
3709
|
// src/kernel/system/hooks.ts
|
|
2640
3710
|
import {
|
|
2641
|
-
appendFileSync as
|
|
3711
|
+
appendFileSync as appendFileSync3,
|
|
2642
3712
|
copyFileSync,
|
|
2643
|
-
existsSync as
|
|
2644
|
-
mkdirSync as
|
|
2645
|
-
readFileSync as
|
|
3713
|
+
existsSync as existsSync7,
|
|
3714
|
+
mkdirSync as mkdirSync5,
|
|
3715
|
+
readFileSync as readFileSync7,
|
|
2646
3716
|
writeFileSync as writeFileSync3
|
|
2647
3717
|
} from "fs";
|
|
2648
|
-
import { homedir as
|
|
2649
|
-
import { join as
|
|
3718
|
+
import { homedir as homedir5 } from "os";
|
|
3719
|
+
import { join as join7 } from "path";
|
|
2650
3720
|
import { fileURLToPath } from "url";
|
|
2651
|
-
var HOME =
|
|
3721
|
+
var HOME = homedir5();
|
|
2652
3722
|
function getPackageSkillPath(agent = "default") {
|
|
2653
3723
|
const packageRoot = [
|
|
2654
3724
|
fileURLToPath(new URL("../../..", import.meta.url)),
|
|
2655
3725
|
fileURLToPath(new URL("../..", import.meta.url)),
|
|
2656
3726
|
fileURLToPath(new URL("..", import.meta.url))
|
|
2657
|
-
].find((candidate) =>
|
|
3727
|
+
].find((candidate) => existsSync7(join7(candidate, "package.json"))) ?? "";
|
|
2658
3728
|
if (!packageRoot) return "";
|
|
2659
3729
|
if (agent === "codex") {
|
|
2660
|
-
const codexPath =
|
|
2661
|
-
if (
|
|
3730
|
+
const codexPath = join7(packageRoot, ".agents", "skills", "zam", "SKILL.md");
|
|
3731
|
+
if (existsSync7(codexPath)) return codexPath;
|
|
2662
3732
|
return "";
|
|
2663
3733
|
}
|
|
2664
3734
|
if (agent === "claude") {
|
|
2665
|
-
const claudePath =
|
|
3735
|
+
const claudePath = join7(
|
|
2666
3736
|
packageRoot,
|
|
2667
3737
|
".claude",
|
|
2668
3738
|
"skills",
|
|
2669
3739
|
"zam",
|
|
2670
3740
|
"SKILL.md"
|
|
2671
3741
|
);
|
|
2672
|
-
if (
|
|
3742
|
+
if (existsSync7(claudePath)) return claudePath;
|
|
2673
3743
|
return "";
|
|
2674
3744
|
}
|
|
2675
|
-
let path =
|
|
2676
|
-
if (
|
|
2677
|
-
path =
|
|
2678
|
-
if (
|
|
3745
|
+
let path = join7(packageRoot, ".agent", "skills", "zam", "SKILL.md");
|
|
3746
|
+
if (existsSync7(path)) return path;
|
|
3747
|
+
path = join7(packageRoot, ".claude", "skills", "zam", "SKILL.md");
|
|
3748
|
+
if (existsSync7(path)) return path;
|
|
2679
3749
|
return "";
|
|
2680
3750
|
}
|
|
2681
3751
|
function distributeGlobalSkills(home = HOME) {
|
|
@@ -2687,16 +3757,16 @@ function distributeGlobalSkills(home = HOME) {
|
|
|
2687
3757
|
console.warn("Could not find ZAM source SKILL.md in the package folder.");
|
|
2688
3758
|
return results;
|
|
2689
3759
|
}
|
|
2690
|
-
const claudeSkillsDir =
|
|
3760
|
+
const claudeSkillsDir = join7(home, ".claude", "skills", "zam");
|
|
2691
3761
|
try {
|
|
2692
3762
|
if (!claudeSourceSkill) {
|
|
2693
3763
|
throw new Error("Claude skill source not found");
|
|
2694
3764
|
}
|
|
2695
|
-
|
|
2696
|
-
copyFileSync(claudeSourceSkill,
|
|
3765
|
+
mkdirSync5(claudeSkillsDir, { recursive: true });
|
|
3766
|
+
copyFileSync(claudeSourceSkill, join7(claudeSkillsDir, "SKILL.md"));
|
|
2697
3767
|
results.push({
|
|
2698
3768
|
name: "Claude Code Global",
|
|
2699
|
-
path:
|
|
3769
|
+
path: join7(claudeSkillsDir, "SKILL.md"),
|
|
2700
3770
|
success: true
|
|
2701
3771
|
});
|
|
2702
3772
|
} catch (_err) {
|
|
@@ -2706,13 +3776,13 @@ function distributeGlobalSkills(home = HOME) {
|
|
|
2706
3776
|
success: false
|
|
2707
3777
|
});
|
|
2708
3778
|
}
|
|
2709
|
-
const geminiSkillsDir =
|
|
3779
|
+
const geminiSkillsDir = join7(home, ".gemini", "skills", "zam");
|
|
2710
3780
|
try {
|
|
2711
|
-
|
|
2712
|
-
copyFileSync(sourceSkill,
|
|
3781
|
+
mkdirSync5(geminiSkillsDir, { recursive: true });
|
|
3782
|
+
copyFileSync(sourceSkill, join7(geminiSkillsDir, "SKILL.md"));
|
|
2713
3783
|
results.push({
|
|
2714
3784
|
name: "Gemini CLI Global",
|
|
2715
|
-
path:
|
|
3785
|
+
path: join7(geminiSkillsDir, "SKILL.md"),
|
|
2716
3786
|
success: true
|
|
2717
3787
|
});
|
|
2718
3788
|
} catch (_err) {
|
|
@@ -2722,16 +3792,16 @@ function distributeGlobalSkills(home = HOME) {
|
|
|
2722
3792
|
success: false
|
|
2723
3793
|
});
|
|
2724
3794
|
}
|
|
2725
|
-
const codexSkillsDir =
|
|
3795
|
+
const codexSkillsDir = join7(home, ".agents", "skills", "zam");
|
|
2726
3796
|
try {
|
|
2727
3797
|
if (!codexSourceSkill) {
|
|
2728
3798
|
throw new Error("Codex skill source not found");
|
|
2729
3799
|
}
|
|
2730
|
-
|
|
2731
|
-
copyFileSync(codexSourceSkill,
|
|
3800
|
+
mkdirSync5(codexSkillsDir, { recursive: true });
|
|
3801
|
+
copyFileSync(codexSourceSkill, join7(codexSkillsDir, "SKILL.md"));
|
|
2732
3802
|
results.push({
|
|
2733
3803
|
name: "Codex Global",
|
|
2734
|
-
path:
|
|
3804
|
+
path: join7(codexSkillsDir, "SKILL.md"),
|
|
2735
3805
|
success: true
|
|
2736
3806
|
});
|
|
2737
3807
|
} catch (_err) {
|
|
@@ -2741,13 +3811,13 @@ function distributeGlobalSkills(home = HOME) {
|
|
|
2741
3811
|
success: false
|
|
2742
3812
|
});
|
|
2743
3813
|
}
|
|
2744
|
-
const gooseSkillsDir =
|
|
3814
|
+
const gooseSkillsDir = join7(home, ".goose", "skills", "zam");
|
|
2745
3815
|
try {
|
|
2746
|
-
|
|
2747
|
-
copyFileSync(sourceSkill,
|
|
3816
|
+
mkdirSync5(gooseSkillsDir, { recursive: true });
|
|
3817
|
+
copyFileSync(sourceSkill, join7(gooseSkillsDir, "SKILL.md"));
|
|
2748
3818
|
results.push({
|
|
2749
3819
|
name: "Goose Global",
|
|
2750
|
-
path:
|
|
3820
|
+
path: join7(gooseSkillsDir, "SKILL.md"),
|
|
2751
3821
|
success: true
|
|
2752
3822
|
});
|
|
2753
3823
|
} catch (_err) {
|
|
@@ -2791,14 +3861,14 @@ function Start-ZamMonitor {
|
|
|
2791
3861
|
`;
|
|
2792
3862
|
function installHook(file, hook, oldHook) {
|
|
2793
3863
|
try {
|
|
2794
|
-
const content =
|
|
3864
|
+
const content = existsSync7(file) ? readFileSync7(file, "utf8") : "";
|
|
2795
3865
|
if (content.includes(HOOK_MARKER)) {
|
|
2796
3866
|
return { success: true, alreadyHooked: true };
|
|
2797
3867
|
}
|
|
2798
3868
|
if (content.includes(oldHook.trim())) {
|
|
2799
3869
|
writeFileSync3(file, content.replace(oldHook.trim(), hook.trim()), "utf8");
|
|
2800
3870
|
} else {
|
|
2801
|
-
|
|
3871
|
+
appendFileSync3(file, hook);
|
|
2802
3872
|
}
|
|
2803
3873
|
return { success: true, alreadyHooked: false };
|
|
2804
3874
|
} catch {
|
|
@@ -2807,24 +3877,24 @@ function installHook(file, hook, oldHook) {
|
|
|
2807
3877
|
}
|
|
2808
3878
|
function injectShellHooks(home = HOME) {
|
|
2809
3879
|
const results = [];
|
|
2810
|
-
const zshrc =
|
|
2811
|
-
if (
|
|
3880
|
+
const zshrc = join7(home, ".zshrc");
|
|
3881
|
+
if (existsSync7(zshrc)) {
|
|
2812
3882
|
const status = installHook(zshrc, posixHook("zsh"), POSIX_OLD_HOOK);
|
|
2813
3883
|
results.push({ shell: "zsh", file: zshrc, ...status });
|
|
2814
3884
|
}
|
|
2815
|
-
const bashrc =
|
|
2816
|
-
if (
|
|
3885
|
+
const bashrc = join7(home, ".bashrc");
|
|
3886
|
+
if (existsSync7(bashrc)) {
|
|
2817
3887
|
const status = installHook(bashrc, posixHook("bash"), POSIX_OLD_HOOK);
|
|
2818
3888
|
results.push({ shell: "bash", file: bashrc, ...status });
|
|
2819
3889
|
}
|
|
2820
3890
|
const pwshDirs = [
|
|
2821
|
-
|
|
2822
|
-
|
|
3891
|
+
join7(home, "Documents", "PowerShell"),
|
|
3892
|
+
join7(home, "Documents", "WindowsPowerShell")
|
|
2823
3893
|
];
|
|
2824
3894
|
for (const dir of pwshDirs) {
|
|
2825
|
-
const profileFile =
|
|
3895
|
+
const profileFile = join7(dir, "Microsoft.PowerShell_profile.ps1");
|
|
2826
3896
|
try {
|
|
2827
|
-
|
|
3897
|
+
mkdirSync5(dir, { recursive: true });
|
|
2828
3898
|
const status = installHook(
|
|
2829
3899
|
profileFile,
|
|
2830
3900
|
POWERSHELL_HOOK,
|
|
@@ -3055,11 +4125,61 @@ function t(locale, key, params = {}) {
|
|
|
3055
4125
|
return str;
|
|
3056
4126
|
}
|
|
3057
4127
|
|
|
4128
|
+
// src/kernel/system/install-config.ts
|
|
4129
|
+
import { existsSync as existsSync8, mkdirSync as mkdirSync6, readFileSync as readFileSync8, writeFileSync as writeFileSync4 } from "fs";
|
|
4130
|
+
import { homedir as homedir6 } from "os";
|
|
4131
|
+
import { dirname as dirname4, join as join8 } from "path";
|
|
4132
|
+
var DEFAULT_CONFIG_PATH = join8(homedir6(), ".zam", "config.json");
|
|
4133
|
+
function loadInstallConfig(path = DEFAULT_CONFIG_PATH) {
|
|
4134
|
+
if (!existsSync8(path)) return {};
|
|
4135
|
+
try {
|
|
4136
|
+
return JSON.parse(readFileSync8(path, "utf-8"));
|
|
4137
|
+
} catch {
|
|
4138
|
+
return {};
|
|
4139
|
+
}
|
|
4140
|
+
}
|
|
4141
|
+
function saveInstallConfig(config, path = DEFAULT_CONFIG_PATH) {
|
|
4142
|
+
const dir = dirname4(path);
|
|
4143
|
+
if (!existsSync8(dir)) mkdirSync6(dir, { recursive: true });
|
|
4144
|
+
writeFileSync4(path, `${JSON.stringify(config, null, 2)}
|
|
4145
|
+
`, "utf-8");
|
|
4146
|
+
}
|
|
4147
|
+
function getInstallMode(path = DEFAULT_CONFIG_PATH) {
|
|
4148
|
+
return loadInstallConfig(path).mode ?? "developer";
|
|
4149
|
+
}
|
|
4150
|
+
function setInstallMode(mode, path = DEFAULT_CONFIG_PATH) {
|
|
4151
|
+
const config = loadInstallConfig(path);
|
|
4152
|
+
config.mode = mode;
|
|
4153
|
+
saveInstallConfig(config, path);
|
|
4154
|
+
}
|
|
4155
|
+
function getInstallChannel(path = DEFAULT_CONFIG_PATH) {
|
|
4156
|
+
const config = loadInstallConfig(path);
|
|
4157
|
+
if (config.channel) return config.channel;
|
|
4158
|
+
return (config.mode ?? "developer") === "developer" ? "developer" : "direct";
|
|
4159
|
+
}
|
|
4160
|
+
function setInstallChannel(channel, path = DEFAULT_CONFIG_PATH) {
|
|
4161
|
+
const config = loadInstallConfig(path);
|
|
4162
|
+
config.channel = channel;
|
|
4163
|
+
saveInstallConfig(config, path);
|
|
4164
|
+
}
|
|
4165
|
+
function detectSyncProvider(dir) {
|
|
4166
|
+
const p = dir.toLowerCase();
|
|
4167
|
+
if (p.includes("onedrive")) return "OneDrive";
|
|
4168
|
+
if (p.includes("dropbox")) return "Dropbox";
|
|
4169
|
+
if (p.includes("google drive") || p.includes("googledrive") || p.includes("/my drive")) {
|
|
4170
|
+
return "Google Drive";
|
|
4171
|
+
}
|
|
4172
|
+
if (p.includes("icloud") || p.includes("mobile documents")) {
|
|
4173
|
+
return "iCloud Drive";
|
|
4174
|
+
}
|
|
4175
|
+
return null;
|
|
4176
|
+
}
|
|
4177
|
+
|
|
3058
4178
|
// src/kernel/system/installer.ts
|
|
3059
4179
|
import { execFileSync, execSync } from "child_process";
|
|
3060
|
-
import { existsSync as
|
|
3061
|
-
import { homedir as
|
|
3062
|
-
import { join as
|
|
4180
|
+
import { existsSync as existsSync9 } from "fs";
|
|
4181
|
+
import { homedir as homedir7 } from "os";
|
|
4182
|
+
import { join as join9 } from "path";
|
|
3063
4183
|
function hasCommand(cmd) {
|
|
3064
4184
|
try {
|
|
3065
4185
|
const checkCmd = process.platform === "win32" ? `where ${cmd}` : `which ${cmd}`;
|
|
@@ -3076,7 +4196,7 @@ function installFastFlowLM() {
|
|
|
3076
4196
|
message: "FastFlowLM is only supported on Windows."
|
|
3077
4197
|
};
|
|
3078
4198
|
}
|
|
3079
|
-
const hasFlm = hasCommand("flm") ||
|
|
4199
|
+
const hasFlm = hasCommand("flm") || existsSync9("C:\\Program Files\\flm\\flm.exe");
|
|
3080
4200
|
if (hasFlm) {
|
|
3081
4201
|
return { success: true, message: "FastFlowLM is already installed." };
|
|
3082
4202
|
}
|
|
@@ -3103,8 +4223,8 @@ function installFastFlowLM() {
|
|
|
3103
4223
|
function installOllama() {
|
|
3104
4224
|
const isMac = process.platform === "darwin";
|
|
3105
4225
|
const isWin = process.platform === "win32";
|
|
3106
|
-
const hasOllama = hasCommand("ollama") || isMac &&
|
|
3107
|
-
|
|
4226
|
+
const hasOllama = hasCommand("ollama") || isMac && existsSync9("/Applications/Ollama.app") || isWin && existsSync9(
|
|
4227
|
+
join9(homedir7(), "AppData", "Local", "Programs", "Ollama", "ollama.exe")
|
|
3108
4228
|
);
|
|
3109
4229
|
if (hasOllama) {
|
|
3110
4230
|
return { success: true, message: "Ollama is already installed." };
|
|
@@ -3164,8 +4284,8 @@ function installOllama() {
|
|
|
3164
4284
|
function resolveOllamaCommand() {
|
|
3165
4285
|
if (hasCommand("ollama")) return "ollama";
|
|
3166
4286
|
const candidates = process.platform === "win32" ? [
|
|
3167
|
-
|
|
3168
|
-
|
|
4287
|
+
join9(
|
|
4288
|
+
homedir7(),
|
|
3169
4289
|
"AppData",
|
|
3170
4290
|
"Local",
|
|
3171
4291
|
"Programs",
|
|
@@ -3173,7 +4293,7 @@ function resolveOllamaCommand() {
|
|
|
3173
4293
|
"ollama.exe"
|
|
3174
4294
|
)
|
|
3175
4295
|
] : process.platform === "darwin" ? ["/Applications/Ollama.app/Contents/Resources/ollama"] : [];
|
|
3176
|
-
return candidates.find((candidate) =>
|
|
4296
|
+
return candidates.find((candidate) => existsSync9(candidate));
|
|
3177
4297
|
}
|
|
3178
4298
|
function prepareLocalModel(runner, model) {
|
|
3179
4299
|
if (runner === "fastflowlm") {
|
|
@@ -3206,6 +4326,63 @@ function prepareLocalModel(runner, model) {
|
|
|
3206
4326
|
};
|
|
3207
4327
|
}
|
|
3208
4328
|
}
|
|
4329
|
+
function planOpenCodeInstall(env) {
|
|
4330
|
+
if (env.hasNpm) {
|
|
4331
|
+
return { method: "npm", command: "npm install -g opencode-ai" };
|
|
4332
|
+
}
|
|
4333
|
+
if (env.platform === "darwin") {
|
|
4334
|
+
if (env.hasBrew) {
|
|
4335
|
+
return {
|
|
4336
|
+
method: "homebrew",
|
|
4337
|
+
command: "brew install anomalyco/tap/opencode"
|
|
4338
|
+
};
|
|
4339
|
+
}
|
|
4340
|
+
return {
|
|
4341
|
+
method: "script",
|
|
4342
|
+
command: "curl -fsSL https://opencode.ai/install | bash"
|
|
4343
|
+
};
|
|
4344
|
+
}
|
|
4345
|
+
if (env.platform === "win32") {
|
|
4346
|
+
if (env.hasScoop)
|
|
4347
|
+
return { method: "scoop", command: "scoop install opencode" };
|
|
4348
|
+
if (env.hasChoco) {
|
|
4349
|
+
return { method: "chocolatey", command: "choco install opencode" };
|
|
4350
|
+
}
|
|
4351
|
+
return null;
|
|
4352
|
+
}
|
|
4353
|
+
return {
|
|
4354
|
+
method: "script",
|
|
4355
|
+
command: "curl -fsSL https://opencode.ai/install | bash"
|
|
4356
|
+
};
|
|
4357
|
+
}
|
|
4358
|
+
function installOpenCode() {
|
|
4359
|
+
if (hasCommand("opencode")) {
|
|
4360
|
+
return { success: true, message: "opencode is already installed." };
|
|
4361
|
+
}
|
|
4362
|
+
const plan = planOpenCodeInstall({
|
|
4363
|
+
platform: process.platform,
|
|
4364
|
+
hasNpm: hasCommand("npm"),
|
|
4365
|
+
hasBrew: hasCommand("brew"),
|
|
4366
|
+
hasScoop: hasCommand("scoop"),
|
|
4367
|
+
hasChoco: hasCommand("choco")
|
|
4368
|
+
});
|
|
4369
|
+
if (!plan) {
|
|
4370
|
+
return {
|
|
4371
|
+
success: false,
|
|
4372
|
+
message: "Could not find a way to install opencode automatically. Install npm, Scoop, or Chocolatey, or follow https://opencode.ai/docs (native Apple Silicon and Windows on ARM builds are available)."
|
|
4373
|
+
};
|
|
4374
|
+
}
|
|
4375
|
+
console.log(`Installing opencode via ${plan.method}...`);
|
|
4376
|
+
try {
|
|
4377
|
+
execSync(plan.command, { stdio: "inherit" });
|
|
4378
|
+
return { success: true, message: `opencode installed via ${plan.method}.` };
|
|
4379
|
+
} catch (err) {
|
|
4380
|
+
return {
|
|
4381
|
+
success: false,
|
|
4382
|
+
message: `Failed to install opencode: ${err.message}. Try manually: ${plan.command}`
|
|
4383
|
+
};
|
|
4384
|
+
}
|
|
4385
|
+
}
|
|
3209
4386
|
|
|
3210
4387
|
// src/kernel/system/locale.ts
|
|
3211
4388
|
import { execSync as execSync2 } from "child_process";
|
|
@@ -3304,80 +4481,177 @@ function getSystemProfile() {
|
|
|
3304
4481
|
}
|
|
3305
4482
|
|
|
3306
4483
|
// src/kernel/system/repos.ts
|
|
3307
|
-
import { existsSync as
|
|
4484
|
+
import { existsSync as existsSync10 } from "fs";
|
|
3308
4485
|
import { resolve as resolve2 } from "path";
|
|
3309
|
-
function getRepoPaths(db) {
|
|
3310
|
-
const personalSetting = getSetting(db, "repo.personal") || getSetting(db, "personal.workspace_dir");
|
|
3311
|
-
const teamSetting = getSetting(db, "repo.team");
|
|
3312
|
-
const orgSetting = getSetting(db, "repo.org");
|
|
4486
|
+
async function getRepoPaths(db) {
|
|
4487
|
+
const personalSetting = await getSetting(db, "repo.personal") || await getSetting(db, "personal.workspace_dir");
|
|
4488
|
+
const teamSetting = await getSetting(db, "repo.team");
|
|
4489
|
+
const orgSetting = await getSetting(db, "repo.org");
|
|
3313
4490
|
return {
|
|
3314
4491
|
personal: personalSetting ? resolve2(personalSetting) : null,
|
|
3315
4492
|
team: teamSetting ? resolve2(teamSetting) : null,
|
|
3316
4493
|
org: orgSetting ? resolve2(orgSetting) : null
|
|
3317
4494
|
};
|
|
3318
4495
|
}
|
|
3319
|
-
function resolveRepoPath(db, type) {
|
|
3320
|
-
const paths = getRepoPaths(db);
|
|
4496
|
+
async function resolveRepoPath(db, type) {
|
|
4497
|
+
const paths = await getRepoPaths(db);
|
|
3321
4498
|
return paths[type];
|
|
3322
4499
|
}
|
|
3323
|
-
function resolveAllBeliefPaths(db) {
|
|
3324
|
-
const paths = getRepoPaths(db);
|
|
4500
|
+
async function resolveAllBeliefPaths(db) {
|
|
4501
|
+
const paths = await getRepoPaths(db);
|
|
3325
4502
|
const dirs = [];
|
|
3326
4503
|
if (paths.personal) {
|
|
3327
4504
|
const personalDir = resolve2(paths.personal, "beliefs");
|
|
3328
|
-
if (
|
|
4505
|
+
if (existsSync10(personalDir)) dirs.push(personalDir);
|
|
3329
4506
|
}
|
|
3330
4507
|
if (paths.team) {
|
|
3331
4508
|
const teamDir = resolve2(paths.team, "beliefs");
|
|
3332
|
-
if (
|
|
4509
|
+
if (existsSync10(teamDir)) dirs.push(teamDir);
|
|
3333
4510
|
}
|
|
3334
4511
|
if (paths.org) {
|
|
3335
4512
|
const orgDir = resolve2(paths.org, "beliefs");
|
|
3336
|
-
if (
|
|
4513
|
+
if (existsSync10(orgDir)) dirs.push(orgDir);
|
|
3337
4514
|
}
|
|
3338
4515
|
return dirs;
|
|
3339
4516
|
}
|
|
3340
|
-
function resolveAllGoalPaths(db) {
|
|
3341
|
-
const paths = getRepoPaths(db);
|
|
4517
|
+
async function resolveAllGoalPaths(db) {
|
|
4518
|
+
const paths = await getRepoPaths(db);
|
|
3342
4519
|
const dirs = [];
|
|
3343
4520
|
if (paths.personal) {
|
|
3344
4521
|
const personalDir = resolve2(paths.personal, "goals");
|
|
3345
|
-
if (
|
|
4522
|
+
if (existsSync10(personalDir)) dirs.push(personalDir);
|
|
3346
4523
|
}
|
|
3347
4524
|
if (paths.team) {
|
|
3348
4525
|
const teamDir = resolve2(paths.team, "goals");
|
|
3349
|
-
if (
|
|
4526
|
+
if (existsSync10(teamDir)) dirs.push(teamDir);
|
|
3350
4527
|
}
|
|
3351
4528
|
if (paths.org) {
|
|
3352
4529
|
const orgDir = resolve2(paths.org, "goals");
|
|
3353
|
-
if (
|
|
4530
|
+
if (existsSync10(orgDir)) dirs.push(orgDir);
|
|
3354
4531
|
}
|
|
3355
4532
|
return dirs;
|
|
3356
4533
|
}
|
|
4534
|
+
|
|
4535
|
+
// src/kernel/system/update-check.ts
|
|
4536
|
+
var WINGET_PACKAGE_ID = "ZAM.ZAM";
|
|
4537
|
+
var HOMEBREW_CASK = "zam";
|
|
4538
|
+
function parseVersion(version) {
|
|
4539
|
+
const clean = version.trim().replace(/^v/i, "");
|
|
4540
|
+
const [main, pre = ""] = clean.split("-", 2);
|
|
4541
|
+
const core = main.split(".").map((n) => Number.parseInt(n, 10) || 0);
|
|
4542
|
+
while (core.length < 3) core.push(0);
|
|
4543
|
+
return { core, pre: pre ? pre.split(".") : [] };
|
|
4544
|
+
}
|
|
4545
|
+
function compareVersions(a, b) {
|
|
4546
|
+
const pa = parseVersion(a);
|
|
4547
|
+
const pb = parseVersion(b);
|
|
4548
|
+
for (let i = 0; i < 3; i++) {
|
|
4549
|
+
if (pa.core[i] !== pb.core[i]) return pa.core[i] > pb.core[i] ? 1 : -1;
|
|
4550
|
+
}
|
|
4551
|
+
if (pa.pre.length === 0 && pb.pre.length === 0) return 0;
|
|
4552
|
+
if (pa.pre.length === 0) return 1;
|
|
4553
|
+
if (pb.pre.length === 0) return -1;
|
|
4554
|
+
const len = Math.max(pa.pre.length, pb.pre.length);
|
|
4555
|
+
for (let i = 0; i < len; i++) {
|
|
4556
|
+
const x = pa.pre[i];
|
|
4557
|
+
const y = pb.pre[i];
|
|
4558
|
+
if (x === void 0) return -1;
|
|
4559
|
+
if (y === void 0) return 1;
|
|
4560
|
+
const xNum = /^\d+$/.test(x);
|
|
4561
|
+
const yNum = /^\d+$/.test(y);
|
|
4562
|
+
if (xNum && yNum) {
|
|
4563
|
+
const dx = Number(x);
|
|
4564
|
+
const dy = Number(y);
|
|
4565
|
+
if (dx !== dy) return dx > dy ? 1 : -1;
|
|
4566
|
+
} else if (xNum !== yNum) {
|
|
4567
|
+
return xNum ? -1 : 1;
|
|
4568
|
+
} else if (x !== y) {
|
|
4569
|
+
return x > y ? 1 : -1;
|
|
4570
|
+
}
|
|
4571
|
+
}
|
|
4572
|
+
return 0;
|
|
4573
|
+
}
|
|
4574
|
+
function decideUpdate(input) {
|
|
4575
|
+
const { currentVersion, latestVersion, channel } = input;
|
|
4576
|
+
const base = { currentVersion, latestVersion, channel };
|
|
4577
|
+
if (compareVersions(latestVersion, currentVersion) <= 0) {
|
|
4578
|
+
return {
|
|
4579
|
+
...base,
|
|
4580
|
+
updateAvailable: false,
|
|
4581
|
+
action: "none",
|
|
4582
|
+
reason: "Already on the latest version."
|
|
4583
|
+
};
|
|
4584
|
+
}
|
|
4585
|
+
switch (channel) {
|
|
4586
|
+
case "direct":
|
|
4587
|
+
return {
|
|
4588
|
+
...base,
|
|
4589
|
+
updateAvailable: true,
|
|
4590
|
+
action: "self-update",
|
|
4591
|
+
reason: "A signed update can be installed in place."
|
|
4592
|
+
};
|
|
4593
|
+
case "winget":
|
|
4594
|
+
return {
|
|
4595
|
+
...base,
|
|
4596
|
+
updateAvailable: true,
|
|
4597
|
+
action: "run-command",
|
|
4598
|
+
command: `winget upgrade --id ${WINGET_PACKAGE_ID}`,
|
|
4599
|
+
reason: "Update available through winget."
|
|
4600
|
+
};
|
|
4601
|
+
case "homebrew":
|
|
4602
|
+
return {
|
|
4603
|
+
...base,
|
|
4604
|
+
updateAvailable: true,
|
|
4605
|
+
action: "run-command",
|
|
4606
|
+
command: `brew upgrade --cask ${HOMEBREW_CASK}`,
|
|
4607
|
+
reason: "Update available through Homebrew."
|
|
4608
|
+
};
|
|
4609
|
+
default:
|
|
4610
|
+
return {
|
|
4611
|
+
...base,
|
|
4612
|
+
updateAvailable: true,
|
|
4613
|
+
action: "inform",
|
|
4614
|
+
command: "git pull && npm install && npm run build",
|
|
4615
|
+
reason: "Developer install \u2014 update from source."
|
|
4616
|
+
};
|
|
4617
|
+
}
|
|
4618
|
+
}
|
|
3357
4619
|
export {
|
|
3358
4620
|
DEFAULT_REVIEW_CONTEXT_MAX_CHARS,
|
|
4621
|
+
HOMEBREW_CASK,
|
|
4622
|
+
SNAPSHOT_VERSION,
|
|
4623
|
+
UI_OBSERVATION_PROTOCOL_VERSION,
|
|
4624
|
+
WINGET_PACKAGE_ID,
|
|
3359
4625
|
addPrerequisite,
|
|
3360
4626
|
analyzeObservation,
|
|
4627
|
+
appendUiObservationReport,
|
|
4628
|
+
applySessionSynthesis,
|
|
3361
4629
|
buildReviewQueue,
|
|
4630
|
+
buildUiSynthesisCandidates,
|
|
3362
4631
|
cascadeBlock,
|
|
3363
4632
|
clearADOCredentials,
|
|
3364
4633
|
clearTursoCredentials,
|
|
4634
|
+
compareVersions,
|
|
3365
4635
|
createAgentSkill,
|
|
3366
4636
|
createFSRS,
|
|
3367
4637
|
createGoal,
|
|
3368
4638
|
createToken,
|
|
4639
|
+
decideUpdate,
|
|
3369
4640
|
deleteCardForUser,
|
|
3370
4641
|
deleteSetting,
|
|
3371
4642
|
deleteToken,
|
|
3372
4643
|
deprecateToken,
|
|
4644
|
+
detectSyncProvider,
|
|
3373
4645
|
detectSystemLocale,
|
|
3374
4646
|
discoverSkills,
|
|
3375
4647
|
distributeGlobalSkills,
|
|
3376
4648
|
endSession,
|
|
3377
4649
|
ensureCard,
|
|
3378
4650
|
ensureMonitorDir,
|
|
4651
|
+
ensureUiObserverDir,
|
|
3379
4652
|
evaluateRating,
|
|
3380
4653
|
executeReviewAction,
|
|
4654
|
+
exportSnapshot,
|
|
3381
4655
|
extractTasks,
|
|
3382
4656
|
extractTokenRefs,
|
|
3383
4657
|
fetchActiveWorkItems,
|
|
@@ -3404,6 +4678,8 @@ export {
|
|
|
3404
4678
|
getDueCards,
|
|
3405
4679
|
getGoal,
|
|
3406
4680
|
getGoalTree,
|
|
4681
|
+
getInstallChannel,
|
|
4682
|
+
getInstallMode,
|
|
3407
4683
|
getMonitorDir,
|
|
3408
4684
|
getMonitorLogStats,
|
|
3409
4685
|
getMonitorPath,
|
|
@@ -3413,23 +4689,31 @@ export {
|
|
|
3413
4689
|
getReviewsForCard,
|
|
3414
4690
|
getReviewsForUser,
|
|
3415
4691
|
getSessionSummary,
|
|
4692
|
+
getSessionSynthesisRecords,
|
|
3416
4693
|
getSetting,
|
|
3417
4694
|
getSystemProfile,
|
|
3418
4695
|
getTokenById,
|
|
3419
4696
|
getTokenBySlug,
|
|
3420
4697
|
getTokenDeleteImpact,
|
|
4698
|
+
getTokenNeighborhood,
|
|
3421
4699
|
getTursoCredentials,
|
|
4700
|
+
getUiObservationPath,
|
|
4701
|
+
getUiObserverDir,
|
|
3422
4702
|
getUserStats,
|
|
3423
4703
|
hasCommand,
|
|
4704
|
+
importSnapshot,
|
|
3424
4705
|
injectShellHooks,
|
|
3425
4706
|
installFastFlowLM,
|
|
3426
4707
|
installOllama,
|
|
4708
|
+
installOpenCode,
|
|
3427
4709
|
interleave,
|
|
4710
|
+
isUiObservationReport,
|
|
3428
4711
|
listAgentSkills,
|
|
3429
4712
|
listGoals,
|
|
3430
4713
|
listTokens,
|
|
3431
4714
|
loadADOConfig,
|
|
3432
4715
|
loadCredentials,
|
|
4716
|
+
loadInstallConfig,
|
|
3433
4717
|
logReview,
|
|
3434
4718
|
logStep,
|
|
3435
4719
|
matchesFilePath,
|
|
@@ -3438,27 +4722,39 @@ export {
|
|
|
3438
4722
|
normalizePath,
|
|
3439
4723
|
openDatabase,
|
|
3440
4724
|
openDatabaseWithSync,
|
|
4725
|
+
openRemoteDatabase,
|
|
3441
4726
|
pairCommands,
|
|
3442
4727
|
parseGoalFile,
|
|
3443
4728
|
parseMonitorLog,
|
|
4729
|
+
parseSnapshot,
|
|
4730
|
+
parseUiObservationLog,
|
|
4731
|
+
planOpenCodeInstall,
|
|
3444
4732
|
prepareLocalModel,
|
|
4733
|
+
prepareSessionSynthesis,
|
|
3445
4734
|
readMonitorLog,
|
|
4735
|
+
readUiObservationLog,
|
|
3446
4736
|
resolveAllBeliefPaths,
|
|
3447
4737
|
resolveAllGoalPaths,
|
|
3448
4738
|
resolveReference,
|
|
3449
4739
|
resolveRepoPath,
|
|
3450
4740
|
resolveReviewContext,
|
|
3451
4741
|
saveCredentials,
|
|
4742
|
+
saveInstallConfig,
|
|
3452
4743
|
serializeGoal,
|
|
3453
4744
|
setADOCredentials,
|
|
4745
|
+
setInstallChannel,
|
|
4746
|
+
setInstallMode,
|
|
3454
4747
|
setSetting,
|
|
3455
4748
|
setTursoCredentials,
|
|
3456
4749
|
startSession,
|
|
3457
4750
|
t,
|
|
4751
|
+
uiObservationLogExists,
|
|
4752
|
+
uiObservationTimeSpan,
|
|
3458
4753
|
unblockReady,
|
|
3459
4754
|
updateCard,
|
|
3460
4755
|
updateGoalStatus,
|
|
3461
4756
|
updateToken,
|
|
4757
|
+
verifySnapshot,
|
|
3462
4758
|
wouldCreateCycle,
|
|
3463
4759
|
writeMonitorEvent
|
|
3464
4760
|
};
|