zam-core 0.3.6 → 0.3.11
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 +177 -9
- package/.agents/skills/zam/SKILL.md +178 -9
- package/.claude/skills/zam/SKILL.md +177 -9
- package/README.de.md +3 -3
- package/README.md +3 -3
- package/dist/cli/index.js +3371 -1616
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +532 -146
- package/dist/index.js +2033 -898
- package/dist/index.js.map +1 -1
- package/package.json +21 -3
- package/templates/personal/README.md +31 -0
- package/templates/personal/beliefs/README.md +19 -0
- package/templates/personal/goals/README.md +19 -0
- package/templates/personal/package.json +9 -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) {
|
|
@@ -219,9 +249,258 @@ async function fetchActiveWorkItems(config) {
|
|
|
219
249
|
|
|
220
250
|
// src/kernel/db/connection.ts
|
|
221
251
|
import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, rmSync } from "fs";
|
|
252
|
+
import { createRequire } from "module";
|
|
222
253
|
import { homedir as homedir2 } from "os";
|
|
223
254
|
import { dirname as dirname2, join as join2 } from "path";
|
|
224
|
-
|
|
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
|
+
}
|
|
225
504
|
|
|
226
505
|
// src/kernel/db/schema.ts
|
|
227
506
|
var SCHEMA = `
|
|
@@ -302,6 +581,22 @@ CREATE TABLE IF NOT EXISTS session_steps (
|
|
|
302
581
|
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
303
582
|
);
|
|
304
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
|
+
|
|
305
600
|
-- User configuration
|
|
306
601
|
CREATE TABLE IF NOT EXISTS user_config (
|
|
307
602
|
key TEXT PRIMARY KEY,
|
|
@@ -334,13 +629,84 @@ CREATE INDEX IF NOT EXISTS idx_review_logs_user ON review_logs(user_id, reviewed
|
|
|
334
629
|
CREATE INDEX IF NOT EXISTS idx_session_steps_session ON session_steps(session_id);
|
|
335
630
|
`;
|
|
336
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
|
+
|
|
337
683
|
// src/kernel/db/connection.ts
|
|
338
684
|
var DEFAULT_DB_DIR = join2(homedir2(), ".zam");
|
|
339
685
|
var DEFAULT_DB_PATH = join2(DEFAULT_DB_DIR, "zam.db");
|
|
686
|
+
var require2 = createRequire(import.meta.url);
|
|
340
687
|
function isRemoteDatabasePath(dbPath) {
|
|
341
|
-
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";
|
|
692
|
+
}
|
|
693
|
+
function openLocalSqlite(dbPath) {
|
|
694
|
+
const mod = require2("better-sqlite3");
|
|
695
|
+
const BetterSqlite3 = "default" in mod ? mod.default : mod;
|
|
696
|
+
return new BetterSqlite3(dbPath);
|
|
342
697
|
}
|
|
343
|
-
function
|
|
698
|
+
function loadLibsql() {
|
|
699
|
+
try {
|
|
700
|
+
const module = require2("libsql");
|
|
701
|
+
return "default" in module ? module.default : module;
|
|
702
|
+
} catch (err) {
|
|
703
|
+
const detail = err instanceof Error ? ` ${err.message}` : "";
|
|
704
|
+
throw new Error(
|
|
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}`
|
|
706
|
+
);
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
async function openDatabase(options = {}) {
|
|
344
710
|
const configuredCloud = options.useConfiguredCloud !== false && !options.dbPath && !options.syncUrl ? getTursoCredentials() : null;
|
|
345
711
|
let requiresTurso = false;
|
|
346
712
|
try {
|
|
@@ -361,7 +727,26 @@ function openDatabase(options = {}) {
|
|
|
361
727
|
const dbPath = configuredCloud?.url ?? options.dbPath ?? DEFAULT_DB_PATH;
|
|
362
728
|
const isRemote = isRemoteDatabasePath(dbPath);
|
|
363
729
|
const isEmbeddedReplica = Boolean(options.syncUrl);
|
|
364
|
-
|
|
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) {
|
|
365
750
|
const dir = dirname2(dbPath);
|
|
366
751
|
if (!existsSync2(dir)) {
|
|
367
752
|
mkdirSync2(dir, { recursive: true });
|
|
@@ -386,61 +771,93 @@ function openDatabase(options = {}) {
|
|
|
386
771
|
if (authToken) {
|
|
387
772
|
dbOpts.authToken = authToken;
|
|
388
773
|
}
|
|
389
|
-
let
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
774
|
+
let driver;
|
|
775
|
+
if (isRemote || isEmbeddedReplica) {
|
|
776
|
+
try {
|
|
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;
|
|
804
|
+
}
|
|
805
|
+
throw nativeErr;
|
|
402
806
|
}
|
|
807
|
+
} else {
|
|
808
|
+
driver = openLocalSqlite(dbPath);
|
|
403
809
|
}
|
|
404
810
|
if (!isRemote && !isEmbeddedReplica) {
|
|
405
|
-
|
|
811
|
+
driver.pragma("journal_mode = WAL");
|
|
406
812
|
}
|
|
407
|
-
|
|
813
|
+
driver.pragma("foreign_keys = ON");
|
|
408
814
|
if (!isRemote) {
|
|
409
|
-
|
|
815
|
+
driver.pragma("busy_timeout = 5000");
|
|
410
816
|
}
|
|
817
|
+
const db = wrapSyncDatabase(driver);
|
|
411
818
|
if (isEmbeddedReplica) {
|
|
412
|
-
db.sync();
|
|
819
|
+
await db.sync?.();
|
|
413
820
|
}
|
|
414
|
-
if (
|
|
415
|
-
db.exec(SCHEMA);
|
|
821
|
+
if (shouldInitialize) {
|
|
822
|
+
await db.exec(SCHEMA);
|
|
416
823
|
}
|
|
417
|
-
runMigrations(db);
|
|
824
|
+
await runMigrations(db);
|
|
418
825
|
return db;
|
|
419
826
|
}
|
|
420
|
-
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 = {}) {
|
|
421
838
|
return openDatabase(options);
|
|
422
839
|
}
|
|
423
840
|
function getDefaultDbPath() {
|
|
424
841
|
return DEFAULT_DB_PATH;
|
|
425
842
|
}
|
|
426
|
-
function runMigrations(db) {
|
|
427
|
-
const sessionCols = db.pragma("table_info(sessions)");
|
|
843
|
+
async function runMigrations(db) {
|
|
844
|
+
const sessionCols = await db.pragma("table_info(sessions)");
|
|
428
845
|
if (sessionCols.length > 0 && !sessionCols.some((c) => c.name === "execution_context")) {
|
|
429
|
-
db.exec(
|
|
846
|
+
await db.exec(
|
|
430
847
|
`ALTER TABLE sessions ADD COLUMN execution_context TEXT NOT NULL DEFAULT 'shell'`
|
|
431
848
|
);
|
|
432
849
|
}
|
|
433
|
-
const tokenCols = db.pragma("table_info(tokens)");
|
|
850
|
+
const tokenCols = await db.pragma("table_info(tokens)");
|
|
434
851
|
if (tokenCols.length > 0 && !tokenCols.some((c) => c.name === "deprecated_at")) {
|
|
435
|
-
db.exec(`ALTER TABLE tokens ADD COLUMN deprecated_at TEXT`);
|
|
852
|
+
await db.exec(`ALTER TABLE tokens ADD COLUMN deprecated_at TEXT`);
|
|
436
853
|
}
|
|
437
854
|
if (tokenCols.length > 0 && !tokenCols.some((c) => c.name === "source_link")) {
|
|
438
|
-
db.exec(`ALTER TABLE tokens ADD COLUMN source_link TEXT`);
|
|
855
|
+
await db.exec(`ALTER TABLE tokens ADD COLUMN source_link TEXT`);
|
|
439
856
|
}
|
|
440
857
|
if (tokenCols.length > 0 && !tokenCols.some((c) => c.name === "question")) {
|
|
441
|
-
db.exec(`ALTER TABLE tokens ADD COLUMN question TEXT`);
|
|
858
|
+
await db.exec(`ALTER TABLE tokens ADD COLUMN question TEXT`);
|
|
442
859
|
}
|
|
443
|
-
db.exec(`
|
|
860
|
+
await db.exec(`
|
|
444
861
|
CREATE TABLE IF NOT EXISTS agent_skills (
|
|
445
862
|
id TEXT PRIMARY KEY,
|
|
446
863
|
slug TEXT NOT NULL UNIQUE,
|
|
@@ -452,6 +869,165 @@ function runMigrations(db) {
|
|
|
452
869
|
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
453
870
|
)
|
|
454
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
|
+
});
|
|
455
1031
|
}
|
|
456
1032
|
|
|
457
1033
|
// src/kernel/goals/engine.ts
|
|
@@ -649,14 +1225,14 @@ function parseRow(row) {
|
|
|
649
1225
|
token_slugs: JSON.parse(row.token_slugs)
|
|
650
1226
|
};
|
|
651
1227
|
}
|
|
652
|
-
function createAgentSkill(db, input) {
|
|
653
|
-
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);
|
|
654
1230
|
if (existing) {
|
|
655
1231
|
throw new Error(`Agent skill already exists: ${input.slug}`);
|
|
656
1232
|
}
|
|
657
1233
|
const id = ulid();
|
|
658
1234
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
659
|
-
db.prepare(
|
|
1235
|
+
await db.prepare(
|
|
660
1236
|
`INSERT INTO agent_skills (id, slug, description, steps, token_slugs, source, created_at, updated_at)
|
|
661
1237
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
|
|
662
1238
|
).run(
|
|
@@ -670,38 +1246,38 @@ function createAgentSkill(db, input) {
|
|
|
670
1246
|
now
|
|
671
1247
|
);
|
|
672
1248
|
return parseRow(
|
|
673
|
-
db.prepare("SELECT * FROM agent_skills WHERE id = ?").get(id)
|
|
1249
|
+
await db.prepare("SELECT * FROM agent_skills WHERE id = ?").get(id)
|
|
674
1250
|
);
|
|
675
1251
|
}
|
|
676
|
-
function getAgentSkill(db, slug) {
|
|
677
|
-
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);
|
|
678
1254
|
return row ? parseRow(row) : void 0;
|
|
679
1255
|
}
|
|
680
|
-
function listAgentSkills(db) {
|
|
681
|
-
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();
|
|
682
1258
|
return rows.map(parseRow);
|
|
683
1259
|
}
|
|
684
1260
|
|
|
685
1261
|
// src/kernel/models/card.ts
|
|
686
1262
|
import { ulid as ulid2 } from "ulid";
|
|
687
|
-
function ensureCard(db, tokenId, userId) {
|
|
688
|
-
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);
|
|
689
1265
|
if (existing) return existing;
|
|
690
1266
|
const id = ulid2();
|
|
691
1267
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
692
|
-
db.prepare(
|
|
1268
|
+
await db.prepare(
|
|
693
1269
|
`INSERT INTO cards (id, token_id, user_id, due_at)
|
|
694
1270
|
VALUES (?, ?, ?, ?)`
|
|
695
1271
|
).run(id, tokenId, userId, now);
|
|
696
|
-
return db.prepare("SELECT * FROM cards WHERE id = ?").get(id);
|
|
1272
|
+
return await db.prepare("SELECT * FROM cards WHERE id = ?").get(id);
|
|
697
1273
|
}
|
|
698
|
-
function getCard(db, tokenId, userId) {
|
|
699
|
-
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);
|
|
700
1276
|
}
|
|
701
|
-
function getCardById(db, cardId) {
|
|
702
|
-
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);
|
|
703
1279
|
}
|
|
704
|
-
function updateCard(db, cardId, updates) {
|
|
1280
|
+
async function updateCard(db, cardId, updates) {
|
|
705
1281
|
const fields = [];
|
|
706
1282
|
const values = [];
|
|
707
1283
|
if (updates.stability !== void 0) {
|
|
@@ -748,32 +1324,32 @@ function updateCard(db, cardId, updates) {
|
|
|
748
1324
|
throw new Error("updateCard called with no fields to update");
|
|
749
1325
|
}
|
|
750
1326
|
values.push(cardId);
|
|
751
|
-
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);
|
|
752
1328
|
if (result.changes === 0) {
|
|
753
1329
|
throw new Error(`Card not found: ${cardId}`);
|
|
754
1330
|
}
|
|
755
|
-
return db.prepare("SELECT * FROM cards WHERE id = ?").get(cardId);
|
|
1331
|
+
return await db.prepare("SELECT * FROM cards WHERE id = ?").get(cardId);
|
|
756
1332
|
}
|
|
757
|
-
function getCardDeletionImpact(db, tokenId, userId) {
|
|
758
|
-
const card = getCard(db, tokenId, userId);
|
|
1333
|
+
async function getCardDeletionImpact(db, tokenId, userId) {
|
|
1334
|
+
const card = await getCard(db, tokenId, userId);
|
|
759
1335
|
if (!card) {
|
|
760
1336
|
throw new Error(`Card not found for token ${tokenId} and user ${userId}`);
|
|
761
1337
|
}
|
|
762
|
-
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);
|
|
763
1339
|
return { review_logs: reviewLogs.n };
|
|
764
1340
|
}
|
|
765
|
-
function deleteCardForUser(db, tokenId, userId) {
|
|
766
|
-
const card = getCard(db, tokenId, userId);
|
|
1341
|
+
async function deleteCardForUser(db, tokenId, userId) {
|
|
1342
|
+
const card = await getCard(db, tokenId, userId);
|
|
767
1343
|
if (!card) {
|
|
768
1344
|
throw new Error(`Card not found for token ${tokenId} and user ${userId}`);
|
|
769
1345
|
}
|
|
770
|
-
const impact = getCardDeletionImpact(db, tokenId, userId);
|
|
771
|
-
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);
|
|
772
1348
|
return { card, impact };
|
|
773
1349
|
}
|
|
774
|
-
function getDueCards(db, userId, now) {
|
|
1350
|
+
async function getDueCards(db, userId, now) {
|
|
775
1351
|
const cutoff = now ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
776
|
-
return db.prepare(
|
|
1352
|
+
return await db.prepare(
|
|
777
1353
|
`SELECT c.*, t.slug, t.concept, t.domain, t.bloom_level
|
|
778
1354
|
FROM cards c
|
|
779
1355
|
JOIN tokens t ON t.id = c.token_id
|
|
@@ -781,8 +1357,8 @@ function getDueCards(db, userId, now) {
|
|
|
781
1357
|
ORDER BY t.bloom_level ASC, c.due_at ASC`
|
|
782
1358
|
).all(userId, cutoff);
|
|
783
1359
|
}
|
|
784
|
-
function getBlockedCards(db, userId) {
|
|
785
|
-
return db.prepare(
|
|
1360
|
+
async function getBlockedCards(db, userId) {
|
|
1361
|
+
return await db.prepare(
|
|
786
1362
|
`SELECT c.*, t.slug, t.concept, t.domain, t.bloom_level
|
|
787
1363
|
FROM cards c
|
|
788
1364
|
JOIN tokens t ON t.id = c.token_id
|
|
@@ -791,226 +1367,53 @@ function getBlockedCards(db, userId) {
|
|
|
791
1367
|
).all(userId);
|
|
792
1368
|
}
|
|
793
1369
|
|
|
794
|
-
// src/kernel/models/
|
|
795
|
-
function addPrerequisite(db, tokenId, requiresId) {
|
|
796
|
-
if (tokenId === requiresId) {
|
|
797
|
-
throw new Error("A token cannot be a prerequisite of itself");
|
|
798
|
-
}
|
|
799
|
-
db.prepare(
|
|
800
|
-
"INSERT OR IGNORE INTO prerequisites (token_id, requires_id) VALUES (?, ?)"
|
|
801
|
-
).run(tokenId, requiresId);
|
|
802
|
-
}
|
|
803
|
-
function getPrerequisites(db, tokenId) {
|
|
804
|
-
return db.prepare(
|
|
805
|
-
`SELECT p.token_id, p.requires_id, t.slug, t.concept, t.domain, t.bloom_level
|
|
806
|
-
FROM prerequisites p
|
|
807
|
-
JOIN tokens t ON t.id = p.requires_id
|
|
808
|
-
WHERE p.token_id = ?`
|
|
809
|
-
).all(tokenId);
|
|
810
|
-
}
|
|
811
|
-
function getDependents(db, tokenId) {
|
|
812
|
-
return db.prepare(
|
|
813
|
-
`SELECT p.token_id, p.requires_id, t.slug, t.concept, t.domain, t.bloom_level
|
|
814
|
-
FROM prerequisites p
|
|
815
|
-
JOIN tokens t ON t.id = p.token_id
|
|
816
|
-
WHERE p.requires_id = ?`
|
|
817
|
-
).all(tokenId);
|
|
818
|
-
}
|
|
819
|
-
|
|
820
|
-
// src/kernel/models/review.ts
|
|
1370
|
+
// src/kernel/models/token.ts
|
|
821
1371
|
import { ulid as ulid3 } from "ulid";
|
|
822
|
-
function
|
|
823
|
-
if (input.rating < 1 || input.rating > 4) {
|
|
824
|
-
throw new Error(`Rating must be between 1 and 4, got ${input.rating}`);
|
|
825
|
-
}
|
|
1372
|
+
async function createToken(db, input) {
|
|
826
1373
|
const id = ulid3();
|
|
827
1374
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
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(
|
|
832
1383
|
id,
|
|
833
|
-
input.
|
|
834
|
-
input.
|
|
835
|
-
input.
|
|
836
|
-
|
|
837
|
-
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,
|
|
838
1392
|
now,
|
|
839
|
-
|
|
840
|
-
input.session_id ?? null
|
|
1393
|
+
now
|
|
841
1394
|
);
|
|
842
|
-
return db
|
|
1395
|
+
return await getTokenById(db, id);
|
|
843
1396
|
}
|
|
844
|
-
function
|
|
845
|
-
return db.prepare(
|
|
846
|
-
"SELECT * FROM review_logs WHERE card_id = ? ORDER BY reviewed_at ASC"
|
|
847
|
-
).all(cardId);
|
|
1397
|
+
async function getTokenBySlug(db, slug) {
|
|
1398
|
+
return await db.prepare("SELECT * FROM tokens WHERE slug = ?").get(slug);
|
|
848
1399
|
}
|
|
849
|
-
function
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
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}`);
|
|
855
1407
|
}
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
1408
|
+
const fields = [];
|
|
1409
|
+
const values = [];
|
|
1410
|
+
if (updates.concept !== void 0) {
|
|
1411
|
+
fields.push("concept = ?");
|
|
1412
|
+
values.push(updates.concept);
|
|
859
1413
|
}
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
params.push(options.limit);
|
|
864
|
-
}
|
|
865
|
-
return db.prepare(sql).all(...params);
|
|
866
|
-
}
|
|
867
|
-
|
|
868
|
-
// src/kernel/models/session.ts
|
|
869
|
-
import { ulid as ulid4 } from "ulid";
|
|
870
|
-
function startSession(db, input) {
|
|
871
|
-
const id = ulid4();
|
|
872
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
873
|
-
const ctx = input.execution_context ?? "shell";
|
|
874
|
-
db.prepare(
|
|
875
|
-
`INSERT INTO sessions (id, user_id, task, execution_context, started_at)
|
|
876
|
-
VALUES (?, ?, ?, ?, ?)`
|
|
877
|
-
).run(id, input.user_id, input.task, ctx, now);
|
|
878
|
-
return db.prepare("SELECT * FROM sessions WHERE id = ?").get(id);
|
|
879
|
-
}
|
|
880
|
-
function endSession(db, sessionId) {
|
|
881
|
-
const session = db.prepare("SELECT * FROM sessions WHERE id = ?").get(sessionId);
|
|
882
|
-
if (!session) {
|
|
883
|
-
throw new Error(`Session not found: ${sessionId}`);
|
|
884
|
-
}
|
|
885
|
-
if (session.completed_at) {
|
|
886
|
-
throw new Error(`Session already completed: ${sessionId}`);
|
|
887
|
-
}
|
|
888
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
889
|
-
db.prepare("UPDATE sessions SET completed_at = ? WHERE id = ?").run(
|
|
890
|
-
now,
|
|
891
|
-
sessionId
|
|
892
|
-
);
|
|
893
|
-
return db.prepare("SELECT * FROM sessions WHERE id = ?").get(sessionId);
|
|
894
|
-
}
|
|
895
|
-
function logStep(db, input) {
|
|
896
|
-
if (input.done_by !== "user" && input.done_by !== "agent") {
|
|
897
|
-
throw new Error(
|
|
898
|
-
`done_by must be 'user' or 'agent', got '${input.done_by}'`
|
|
899
|
-
);
|
|
900
|
-
}
|
|
901
|
-
if (input.rating != null && (input.rating < 1 || input.rating > 4)) {
|
|
902
|
-
throw new Error(`Rating must be between 1 and 4, got ${input.rating}`);
|
|
903
|
-
}
|
|
904
|
-
const session = db.prepare("SELECT id FROM sessions WHERE id = ?").get(input.session_id);
|
|
905
|
-
if (!session) {
|
|
906
|
-
throw new Error(`Session not found: ${input.session_id}`);
|
|
907
|
-
}
|
|
908
|
-
const id = ulid4();
|
|
909
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
910
|
-
db.prepare(
|
|
911
|
-
`INSERT INTO session_steps (id, session_id, token_id, done_by, rating, notes, created_at)
|
|
912
|
-
VALUES (?, ?, ?, ?, ?, ?, ?)`
|
|
913
|
-
).run(
|
|
914
|
-
id,
|
|
915
|
-
input.session_id,
|
|
916
|
-
input.token_id,
|
|
917
|
-
input.done_by,
|
|
918
|
-
input.rating ?? null,
|
|
919
|
-
input.notes ?? null,
|
|
920
|
-
now
|
|
921
|
-
);
|
|
922
|
-
return db.prepare("SELECT * FROM session_steps WHERE id = ?").get(id);
|
|
923
|
-
}
|
|
924
|
-
function getSessionSummary(db, sessionId) {
|
|
925
|
-
const session = db.prepare("SELECT * FROM sessions WHERE id = ?").get(sessionId);
|
|
926
|
-
if (!session) {
|
|
927
|
-
throw new Error(`Session not found: ${sessionId}`);
|
|
928
|
-
}
|
|
929
|
-
const steps = db.prepare(
|
|
930
|
-
`SELECT ss.*, t.slug, t.concept, t.domain, t.bloom_level
|
|
931
|
-
FROM session_steps ss
|
|
932
|
-
JOIN tokens t ON t.id = ss.token_id
|
|
933
|
-
WHERE ss.session_id = ?
|
|
934
|
-
ORDER BY ss.created_at ASC`
|
|
935
|
-
).all(sessionId);
|
|
936
|
-
return { session, steps };
|
|
937
|
-
}
|
|
938
|
-
|
|
939
|
-
// src/kernel/models/settings.ts
|
|
940
|
-
function getSetting(db, key) {
|
|
941
|
-
const row = db.prepare("SELECT value FROM user_config WHERE key = ?").get(key);
|
|
942
|
-
return row?.value;
|
|
943
|
-
}
|
|
944
|
-
function getAllSettings(db) {
|
|
945
|
-
const rows = db.prepare("SELECT key, value FROM user_config ORDER BY key").all();
|
|
946
|
-
const map = {};
|
|
947
|
-
for (const row of rows) {
|
|
948
|
-
map[row.key] = row.value;
|
|
949
|
-
}
|
|
950
|
-
return map;
|
|
951
|
-
}
|
|
952
|
-
function getAllSettingsDetailed(db) {
|
|
953
|
-
return db.prepare("SELECT key, value, updated_at FROM user_config ORDER BY key").all();
|
|
954
|
-
}
|
|
955
|
-
function setSetting(db, key, value) {
|
|
956
|
-
db.prepare(
|
|
957
|
-
`INSERT INTO user_config (key, value, updated_at)
|
|
958
|
-
VALUES (?, ?, datetime('now'))
|
|
959
|
-
ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at`
|
|
960
|
-
).run(key, value);
|
|
961
|
-
}
|
|
962
|
-
function deleteSetting(db, key) {
|
|
963
|
-
const result = db.prepare("DELETE FROM user_config WHERE key = ?").run(key);
|
|
964
|
-
return result.changes > 0;
|
|
965
|
-
}
|
|
966
|
-
|
|
967
|
-
// src/kernel/models/token.ts
|
|
968
|
-
import { ulid as ulid5 } from "ulid";
|
|
969
|
-
function createToken(db, input) {
|
|
970
|
-
const id = ulid5();
|
|
971
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
972
|
-
const bloom = input.bloom_level ?? 1;
|
|
973
|
-
if (bloom < 1 || bloom > 5) {
|
|
974
|
-
throw new Error(`bloom_level must be between 1 and 5, got ${bloom}`);
|
|
975
|
-
}
|
|
976
|
-
db.prepare(`
|
|
977
|
-
INSERT INTO tokens (id, slug, concept, domain, bloom_level, context, symbiosis_mode, source_link, question, created_at, updated_at)
|
|
978
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
979
|
-
`).run(
|
|
980
|
-
id,
|
|
981
|
-
input.slug,
|
|
982
|
-
input.concept,
|
|
983
|
-
input.domain ?? "",
|
|
984
|
-
bloom,
|
|
985
|
-
input.context ?? "",
|
|
986
|
-
input.symbiosis_mode ?? null,
|
|
987
|
-
input.source_link ?? null,
|
|
988
|
-
input.question ?? null,
|
|
989
|
-
now,
|
|
990
|
-
now
|
|
991
|
-
);
|
|
992
|
-
return getTokenById(db, id);
|
|
993
|
-
}
|
|
994
|
-
function getTokenBySlug(db, slug) {
|
|
995
|
-
return db.prepare("SELECT * FROM tokens WHERE slug = ?").get(slug);
|
|
996
|
-
}
|
|
997
|
-
function getTokenById(db, id) {
|
|
998
|
-
return db.prepare("SELECT * FROM tokens WHERE id = ?").get(id);
|
|
999
|
-
}
|
|
1000
|
-
function updateToken(db, slug, updates) {
|
|
1001
|
-
const token = getTokenBySlug(db, slug);
|
|
1002
|
-
if (!token) {
|
|
1003
|
-
throw new Error(`Token not found: ${slug}`);
|
|
1004
|
-
}
|
|
1005
|
-
const fields = [];
|
|
1006
|
-
const values = [];
|
|
1007
|
-
if (updates.concept !== void 0) {
|
|
1008
|
-
fields.push("concept = ?");
|
|
1009
|
-
values.push(updates.concept);
|
|
1010
|
-
}
|
|
1011
|
-
if (updates.domain !== void 0) {
|
|
1012
|
-
fields.push("domain = ?");
|
|
1013
|
-
values.push(updates.domain);
|
|
1414
|
+
if (updates.domain !== void 0) {
|
|
1415
|
+
fields.push("domain = ?");
|
|
1416
|
+
values.push(updates.domain);
|
|
1014
1417
|
}
|
|
1015
1418
|
if (updates.bloom_level !== void 0) {
|
|
1016
1419
|
if (updates.bloom_level < 1 || updates.bloom_level > 5) {
|
|
@@ -1047,13 +1450,11 @@ function updateToken(db, slug, updates) {
|
|
|
1047
1450
|
fields.push("updated_at = ?");
|
|
1048
1451
|
values.push((/* @__PURE__ */ new Date()).toISOString());
|
|
1049
1452
|
values.push(slug);
|
|
1050
|
-
db.prepare(`UPDATE tokens SET ${fields.join(", ")} WHERE slug = ?`).run(
|
|
1051
|
-
|
|
1052
|
-
);
|
|
1053
|
-
return getTokenBySlug(db, slug);
|
|
1453
|
+
await db.prepare(`UPDATE tokens SET ${fields.join(", ")} WHERE slug = ?`).run(...values);
|
|
1454
|
+
return await getTokenBySlug(db, slug);
|
|
1054
1455
|
}
|
|
1055
|
-
function deprecateToken(db, slug) {
|
|
1056
|
-
const token = getTokenBySlug(db, slug);
|
|
1456
|
+
async function deprecateToken(db, slug) {
|
|
1457
|
+
const token = await getTokenBySlug(db, slug);
|
|
1057
1458
|
if (!token) {
|
|
1058
1459
|
throw new Error(`Token not found: ${slug}`);
|
|
1059
1460
|
}
|
|
@@ -1061,25 +1462,25 @@ function deprecateToken(db, slug) {
|
|
|
1061
1462
|
throw new Error(`Token already deprecated: ${slug}`);
|
|
1062
1463
|
}
|
|
1063
1464
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1064
|
-
db.prepare(
|
|
1465
|
+
await db.prepare(
|
|
1065
1466
|
"UPDATE tokens SET deprecated_at = ?, updated_at = ? WHERE slug = ?"
|
|
1066
1467
|
).run(now, now, slug);
|
|
1067
|
-
return getTokenBySlug(db, slug);
|
|
1468
|
+
return await getTokenBySlug(db, slug);
|
|
1068
1469
|
}
|
|
1069
|
-
function getTokenDeleteImpact(db, slug) {
|
|
1070
|
-
const token = getTokenBySlug(db, slug);
|
|
1470
|
+
async function getTokenDeleteImpact(db, slug) {
|
|
1471
|
+
const token = await getTokenBySlug(db, slug);
|
|
1071
1472
|
if (!token) {
|
|
1072
1473
|
throw new Error(`Token not found: ${slug}`);
|
|
1073
1474
|
}
|
|
1074
|
-
const cards = db.prepare("SELECT COUNT(*) AS n FROM cards WHERE token_id = ?").get(token.id);
|
|
1075
|
-
const reviewLogs = db.prepare("SELECT COUNT(*) AS n FROM review_logs WHERE token_id = ?").get(token.id);
|
|
1076
|
-
const prereqsFrom = db.prepare("SELECT COUNT(*) AS n FROM prerequisites WHERE token_id = ?").get(token.id);
|
|
1077
|
-
const prereqsTo = db.prepare("SELECT COUNT(*) AS n FROM prerequisites WHERE requires_id = ?").get(token.id);
|
|
1078
|
-
const sessionSteps = db.prepare("SELECT COUNT(*) AS n FROM session_steps WHERE token_id = ?").get(token.id);
|
|
1079
|
-
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(
|
|
1080
1481
|
"SELECT COUNT(DISTINCT session_id) AS n FROM session_steps WHERE token_id = ?"
|
|
1081
1482
|
).get(token.id);
|
|
1082
|
-
const skillRows = db.prepare("SELECT token_slugs FROM agent_skills").all();
|
|
1483
|
+
const skillRows = await db.prepare("SELECT token_slugs FROM agent_skills").all();
|
|
1083
1484
|
const agentSkills = skillRows.filter((row) => {
|
|
1084
1485
|
const tokenSlugs = JSON.parse(row.token_slugs);
|
|
1085
1486
|
return tokenSlugs.includes(slug);
|
|
@@ -1094,158 +1495,453 @@ function getTokenDeleteImpact(db, slug) {
|
|
|
1094
1495
|
agent_skills: agentSkills
|
|
1095
1496
|
};
|
|
1096
1497
|
}
|
|
1097
|
-
function deleteToken(db, slug) {
|
|
1098
|
-
const token = getTokenBySlug(db, slug);
|
|
1498
|
+
async function deleteToken(db, slug) {
|
|
1499
|
+
const token = await getTokenBySlug(db, slug);
|
|
1099
1500
|
if (!token) {
|
|
1100
1501
|
throw new Error(`Token not found: ${slug}`);
|
|
1101
1502
|
}
|
|
1102
|
-
const impact = getTokenDeleteImpact(db, slug);
|
|
1103
|
-
db.
|
|
1104
|
-
try {
|
|
1503
|
+
const impact = await getTokenDeleteImpact(db, slug);
|
|
1504
|
+
await db.transaction(async (tx) => {
|
|
1105
1505
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1106
|
-
const skillRows =
|
|
1506
|
+
const skillRows = await tx.prepare("SELECT id, token_slugs FROM agent_skills").all();
|
|
1107
1507
|
for (const row of skillRows) {
|
|
1108
1508
|
const tokenSlugs = JSON.parse(row.token_slugs);
|
|
1109
1509
|
const filtered = tokenSlugs.filter((tokenSlug) => tokenSlug !== slug);
|
|
1110
1510
|
if (filtered.length !== tokenSlugs.length) {
|
|
1111
|
-
|
|
1511
|
+
await tx.prepare(
|
|
1112
1512
|
"UPDATE agent_skills SET token_slugs = ?, updated_at = ? WHERE id = ?"
|
|
1113
1513
|
).run(JSON.stringify(filtered), now, row.id);
|
|
1114
1514
|
}
|
|
1115
1515
|
}
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
} catch (err) {
|
|
1119
|
-
db.exec("ROLLBACK");
|
|
1120
|
-
throw err;
|
|
1121
|
-
}
|
|
1516
|
+
await tx.prepare("DELETE FROM tokens WHERE id = ?").run(token.id);
|
|
1517
|
+
});
|
|
1122
1518
|
return { token, impact };
|
|
1123
1519
|
}
|
|
1124
|
-
function findTokens(db, query) {
|
|
1520
|
+
async function findTokens(db, query) {
|
|
1125
1521
|
const normalised = query.toLowerCase();
|
|
1126
|
-
const
|
|
1127
|
-
|
|
1128
|
-
);
|
|
1129
|
-
const
|
|
1130
|
-
const
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1522
|
+
const searchTokens = normalised.split(/[\s,.\-_/\\:;!?()[\]{}]+/).filter((t2) => t2.length > 0);
|
|
1523
|
+
if (searchTokens.length === 0) return [];
|
|
1524
|
+
const shortTerms = searchTokens.filter((t2) => t2.length <= 2);
|
|
1525
|
+
const longTerms = searchTokens.filter((t2) => t2.length > 2);
|
|
1526
|
+
const scoreMap = /* @__PURE__ */ new Map();
|
|
1527
|
+
const likeSQL = `SELECT * FROM tokens WHERE deprecated_at IS NULL AND (lower(slug) LIKE ? OR lower(concept) LIKE ? OR lower(domain) LIKE ?)`;
|
|
1528
|
+
for (const term of longTerms) {
|
|
1529
|
+
const pattern = `%${term}%`;
|
|
1530
|
+
const rows = await db.prepare(likeSQL).all(pattern, pattern, pattern);
|
|
1531
|
+
for (const row of rows) {
|
|
1532
|
+
const entry = scoreMap.get(row.id);
|
|
1533
|
+
if (entry) {
|
|
1534
|
+
entry.score++;
|
|
1535
|
+
} else {
|
|
1536
|
+
scoreMap.set(row.id, { token: row, score: 1 });
|
|
1537
|
+
}
|
|
1136
1538
|
}
|
|
1137
|
-
|
|
1138
|
-
|
|
1539
|
+
}
|
|
1540
|
+
if (shortTerms.length > 0 || longTerms.length === 0) {
|
|
1541
|
+
const allTokens = await db.prepare("SELECT * FROM tokens WHERE deprecated_at IS NULL").all();
|
|
1542
|
+
for (const token of allTokens) {
|
|
1543
|
+
const words = `${token.slug} ${token.concept} ${token.domain}`.toLowerCase().split(/[\s,.\-_/\\:;!?()[\]{}]+/).filter(Boolean);
|
|
1544
|
+
let matchCount = 0;
|
|
1545
|
+
for (const term of shortTerms.length > 0 ? shortTerms : searchTokens) {
|
|
1546
|
+
for (const w of words) {
|
|
1547
|
+
if (w === term) matchCount++;
|
|
1548
|
+
}
|
|
1549
|
+
}
|
|
1550
|
+
if (matchCount > 0) {
|
|
1551
|
+
const entry = scoreMap.get(token.id);
|
|
1552
|
+
if (entry) {
|
|
1553
|
+
entry.score += matchCount;
|
|
1554
|
+
} else {
|
|
1555
|
+
scoreMap.set(token.id, { token, score: matchCount });
|
|
1556
|
+
}
|
|
1557
|
+
}
|
|
1139
1558
|
}
|
|
1140
|
-
|
|
1141
|
-
|
|
1559
|
+
}
|
|
1560
|
+
const scored = [];
|
|
1561
|
+
for (const { token, score } of scoreMap.values()) {
|
|
1562
|
+
let finalScore = score;
|
|
1563
|
+
if (token.concept.toLowerCase().includes(normalised.slice(0, 25))) {
|
|
1564
|
+
finalScore += 3;
|
|
1142
1565
|
}
|
|
1566
|
+
scored.push({ score: finalScore, ...token });
|
|
1143
1567
|
}
|
|
1144
1568
|
scored.sort((a, b) => b.score - a.score);
|
|
1145
1569
|
return scored;
|
|
1146
1570
|
}
|
|
1147
|
-
function listTokens(db, options) {
|
|
1571
|
+
async function listTokens(db, options) {
|
|
1148
1572
|
if (options?.domain) {
|
|
1149
|
-
return db.prepare(
|
|
1573
|
+
return await db.prepare(
|
|
1150
1574
|
"SELECT * FROM tokens WHERE domain = ? AND deprecated_at IS NULL ORDER BY bloom_level, slug"
|
|
1151
1575
|
).all(options.domain);
|
|
1152
1576
|
}
|
|
1153
|
-
return db.prepare(
|
|
1577
|
+
return await db.prepare(
|
|
1154
1578
|
"SELECT * FROM tokens WHERE deprecated_at IS NULL ORDER BY bloom_level, domain, slug"
|
|
1155
1579
|
).all();
|
|
1156
1580
|
}
|
|
1157
1581
|
|
|
1158
|
-
// src/kernel/
|
|
1159
|
-
function
|
|
1160
|
-
const
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
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);
|
|
1167
1591
|
}
|
|
1592
|
+
ancestors.add(row.requires_id);
|
|
1168
1593
|
}
|
|
1169
|
-
return
|
|
1594
|
+
return map;
|
|
1170
1595
|
}
|
|
1171
|
-
function
|
|
1172
|
-
|
|
1173
|
-
const
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
seq: e.seq,
|
|
1186
|
-
pid: e.pid ?? 0,
|
|
1187
|
-
command: start.command ?? "",
|
|
1188
|
-
cwd: start.cwd ?? "",
|
|
1189
|
-
startedAt: start.ts,
|
|
1190
|
-
endedAt: e.ts,
|
|
1191
|
-
durationMs: endMs - startMs,
|
|
1192
|
-
exitCode: e.exit_code ?? null
|
|
1193
|
-
});
|
|
1194
|
-
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);
|
|
1195
1610
|
}
|
|
1196
1611
|
}
|
|
1197
1612
|
}
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
cwd: start.cwd ?? "",
|
|
1204
|
-
startedAt: start.ts,
|
|
1205
|
-
endedAt: null,
|
|
1206
|
-
durationMs: null,
|
|
1207
|
-
exitCode: null
|
|
1208
|
-
});
|
|
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");
|
|
1209
1618
|
}
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
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);
|
|
1214
1627
|
}
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
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);
|
|
1220
1635
|
}
|
|
1221
|
-
function
|
|
1222
|
-
|
|
1223
|
-
|
|
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);
|
|
1224
1643
|
}
|
|
1225
|
-
function
|
|
1226
|
-
|
|
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 };
|
|
1227
1708
|
}
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
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);
|
|
1233
1733
|
}
|
|
1234
|
-
function
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
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 = [];
|
|
1933
|
+
for (const tp of tokenPatterns) {
|
|
1934
|
+
const matchIndices = [];
|
|
1935
|
+
const matchedTexts = [];
|
|
1936
|
+
for (let i = 0; i < commands.length; i++) {
|
|
1937
|
+
if (matchesToken(commands[i].command, tp.patterns)) {
|
|
1938
|
+
matchIndices.push(i);
|
|
1939
|
+
matchedTexts.push(commands[i].command);
|
|
1940
|
+
matchedSet.add(i);
|
|
1941
|
+
}
|
|
1942
|
+
}
|
|
1943
|
+
if (matchIndices.length === 0) {
|
|
1944
|
+
ratings.push({
|
|
1249
1945
|
tokenSlug: tp.slug,
|
|
1250
1946
|
rating: null,
|
|
1251
1947
|
confidence: "low",
|
|
@@ -1288,8 +1984,8 @@ function analyzeObservation(commands, tokenPatterns) {
|
|
|
1288
1984
|
const prefix = commandPrefix(commands[mi].command);
|
|
1289
1985
|
prefixGroups.set(prefix, (prefixGroups.get(prefix) ?? 0) + 1);
|
|
1290
1986
|
}
|
|
1291
|
-
for (const
|
|
1292
|
-
if (
|
|
1987
|
+
for (const count2 of prefixGroups.values()) {
|
|
1988
|
+
if (count2 > 1) selfCorrections += count2 - 1;
|
|
1293
1989
|
}
|
|
1294
1990
|
const gaps = [];
|
|
1295
1991
|
for (let k = 1; k < matchIndices.length; k++) {
|
|
@@ -1408,21 +2104,471 @@ function readMonitorLog(sessionId) {
|
|
|
1408
2104
|
if (!existsSync4(path)) {
|
|
1409
2105
|
return [];
|
|
1410
2106
|
}
|
|
1411
|
-
const content = readFileSync4(path, "utf-8");
|
|
1412
|
-
return parseMonitorLog(content);
|
|
2107
|
+
const content = readFileSync4(path, "utf-8");
|
|
2108
|
+
return parseMonitorLog(content);
|
|
2109
|
+
}
|
|
2110
|
+
function monitorLogExists(sessionId) {
|
|
2111
|
+
return existsSync4(getMonitorPath(sessionId));
|
|
2112
|
+
}
|
|
2113
|
+
function getMonitorLogStats(sessionId) {
|
|
2114
|
+
const path = getMonitorPath(sessionId);
|
|
2115
|
+
if (!existsSync4(path)) {
|
|
2116
|
+
return { exists: false, sizeBytes: 0, lineCount: 0 };
|
|
2117
|
+
}
|
|
2118
|
+
const stat = statSync(path);
|
|
2119
|
+
const content = readFileSync4(path, "utf-8");
|
|
2120
|
+
const lineCount = content.split("\n").filter((l) => l.trim()).length;
|
|
2121
|
+
return { exists: true, sizeBytes: stat.size, lineCount };
|
|
2122
|
+
}
|
|
2123
|
+
|
|
2124
|
+
// src/kernel/observation/session-synthesis.ts
|
|
2125
|
+
import { ulid as ulid7 } from "ulid";
|
|
2126
|
+
|
|
2127
|
+
// src/kernel/recall/evaluator.ts
|
|
2128
|
+
import { ulid as ulid6 } from "ulid";
|
|
2129
|
+
|
|
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));
|
|
2160
|
+
}
|
|
2161
|
+
function daysBetween(a, b) {
|
|
2162
|
+
return (b.getTime() - a.getTime()) / (1e3 * 60 * 60 * 24);
|
|
2163
|
+
}
|
|
2164
|
+
function initialStability(w, rating) {
|
|
2165
|
+
return w[rating - 1];
|
|
2166
|
+
}
|
|
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/session-synthesis.ts
|
|
2395
|
+
function parseSynthesisRow(row) {
|
|
2396
|
+
return {
|
|
2397
|
+
...row,
|
|
2398
|
+
evidence: JSON.parse(row.evidence)
|
|
2399
|
+
};
|
|
2400
|
+
}
|
|
2401
|
+
function confidenceRank(confidence) {
|
|
2402
|
+
return confidence === "high" ? 2 : confidence === "medium" ? 1 : 0;
|
|
2403
|
+
}
|
|
2404
|
+
function normalizeSkillStep(step) {
|
|
2405
|
+
const codeSpans = [...step.matchAll(/`([^`]+)`/g)].map((match) => match[1].trim()).filter(Boolean);
|
|
2406
|
+
if (codeSpans.length > 0) return codeSpans;
|
|
2407
|
+
const normalized = step.trim().replace(/^(?:[-*]|\d+[.)])\s+/, "").replace(/^(?:run|execute)\s+/i, "").replace(/^`|`$/g, "").trim();
|
|
2408
|
+
return normalized ? [normalized] : [];
|
|
2409
|
+
}
|
|
2410
|
+
async function buildSkillPatterns(db) {
|
|
2411
|
+
const byToken = /* @__PURE__ */ new Map();
|
|
2412
|
+
for (const skill of await listAgentSkills(db)) {
|
|
2413
|
+
if (skill.token_slugs.length !== 1) continue;
|
|
2414
|
+
const patterns = skill.steps.flatMap(normalizeSkillStep);
|
|
2415
|
+
for (const slug of skill.token_slugs) {
|
|
2416
|
+
const tokenPatterns = byToken.get(slug) ?? /* @__PURE__ */ new Set();
|
|
2417
|
+
for (const pattern of patterns) tokenPatterns.add(pattern);
|
|
2418
|
+
byToken.set(slug, tokenPatterns);
|
|
2419
|
+
}
|
|
2420
|
+
}
|
|
2421
|
+
return [...byToken.entries()].map(([slug, patterns]) => ({
|
|
2422
|
+
slug,
|
|
2423
|
+
patterns: [...patterns]
|
|
2424
|
+
}));
|
|
2425
|
+
}
|
|
2426
|
+
function mergePatterns(automatic, explicit) {
|
|
2427
|
+
const merged = /* @__PURE__ */ new Map();
|
|
2428
|
+
for (const entry of [...automatic, ...explicit]) {
|
|
2429
|
+
const patterns = merged.get(entry.slug) ?? /* @__PURE__ */ new Set();
|
|
2430
|
+
for (const pattern of entry.patterns) {
|
|
2431
|
+
const trimmed = pattern.trim();
|
|
2432
|
+
if (trimmed) patterns.add(trimmed);
|
|
2433
|
+
}
|
|
2434
|
+
merged.set(entry.slug, patterns);
|
|
2435
|
+
}
|
|
2436
|
+
return [...merged.entries()].filter(([, patterns]) => patterns.size > 0).map(([slug, patterns]) => ({ slug, patterns: [...patterns] }));
|
|
1413
2437
|
}
|
|
1414
|
-
function
|
|
1415
|
-
|
|
2438
|
+
async function getSession(db, sessionId) {
|
|
2439
|
+
const session = await db.prepare("SELECT * FROM sessions WHERE id = ?").get(sessionId);
|
|
2440
|
+
if (!session) throw new Error(`Session not found: ${sessionId}`);
|
|
2441
|
+
return session;
|
|
1416
2442
|
}
|
|
1417
|
-
function
|
|
1418
|
-
const
|
|
1419
|
-
|
|
1420
|
-
|
|
2443
|
+
async function getSessionSynthesisRecords(db, sessionId) {
|
|
2444
|
+
const rows = await db.prepare(
|
|
2445
|
+
"SELECT * FROM session_syntheses WHERE session_id = ? ORDER BY created_at"
|
|
2446
|
+
).all(sessionId);
|
|
2447
|
+
return rows.map(parseSynthesisRow);
|
|
2448
|
+
}
|
|
2449
|
+
async function prepareSessionSynthesis(db, input) {
|
|
2450
|
+
const session = await getSession(db, input.sessionId);
|
|
2451
|
+
const patterns = mergePatterns(
|
|
2452
|
+
await buildSkillPatterns(db),
|
|
2453
|
+
input.explicitPatterns ?? []
|
|
2454
|
+
);
|
|
2455
|
+
const validPatterns = [];
|
|
2456
|
+
const tokens = /* @__PURE__ */ new Map();
|
|
2457
|
+
for (const pattern of patterns) {
|
|
2458
|
+
const token = await getTokenBySlug(db, pattern.slug);
|
|
2459
|
+
if (!token || token.deprecated_at) continue;
|
|
2460
|
+
validPatterns.push(pattern);
|
|
2461
|
+
tokens.set(pattern.slug, token);
|
|
2462
|
+
}
|
|
2463
|
+
const commands = input.commands ?? pairCommands(readMonitorLog(input.sessionId));
|
|
2464
|
+
const analysis = analyzeObservation(commands, validPatterns);
|
|
2465
|
+
const applied = new Set(
|
|
2466
|
+
(await getSessionSynthesisRecords(db, input.sessionId)).map(
|
|
2467
|
+
(record) => record.token_id
|
|
2468
|
+
)
|
|
2469
|
+
);
|
|
2470
|
+
const minRank = confidenceRank(input.minConfidence ?? "medium");
|
|
2471
|
+
let skippedLowConfidence = 0;
|
|
2472
|
+
const candidates = [];
|
|
2473
|
+
for (const rating of analysis.ratings) {
|
|
2474
|
+
const token = tokens.get(rating.tokenSlug);
|
|
2475
|
+
if (!token || rating.rating == null || applied.has(token.id)) continue;
|
|
2476
|
+
if (confidenceRank(rating.confidence) < minRank) {
|
|
2477
|
+
skippedLowConfidence++;
|
|
2478
|
+
continue;
|
|
2479
|
+
}
|
|
2480
|
+
candidates.push({
|
|
2481
|
+
tokenId: token.id,
|
|
2482
|
+
tokenSlug: token.slug,
|
|
2483
|
+
concept: token.concept,
|
|
2484
|
+
domain: token.domain,
|
|
2485
|
+
inferredRating: rating.rating,
|
|
2486
|
+
confidence: rating.confidence,
|
|
2487
|
+
evidence: rating.evidence,
|
|
2488
|
+
matchedCommandTexts: rating.matchedCommandTexts
|
|
2489
|
+
});
|
|
1421
2490
|
}
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
2491
|
+
return {
|
|
2492
|
+
sessionId: session.id,
|
|
2493
|
+
userId: session.user_id,
|
|
2494
|
+
patternCount: validPatterns.length,
|
|
2495
|
+
commandCount: commands.length,
|
|
2496
|
+
alreadyApplied: applied.size,
|
|
2497
|
+
skippedLowConfidence,
|
|
2498
|
+
candidates,
|
|
2499
|
+
unmatchedCommands: analysis.unmatchedCommands,
|
|
2500
|
+
timeSpan: analysis.timeSpan
|
|
2501
|
+
};
|
|
2502
|
+
}
|
|
2503
|
+
async function applySessionSynthesis(db, input) {
|
|
2504
|
+
return db.transaction(async (tx) => {
|
|
2505
|
+
const session = await getSession(tx, input.sessionId);
|
|
2506
|
+
const token = await getTokenBySlug(tx, input.tokenSlug);
|
|
2507
|
+
if (!token || token.deprecated_at) {
|
|
2508
|
+
throw new Error(`Active token not found: ${input.tokenSlug}`);
|
|
2509
|
+
}
|
|
2510
|
+
const existing = await tx.prepare(
|
|
2511
|
+
"SELECT * FROM session_syntheses WHERE session_id = ? AND token_id = ?"
|
|
2512
|
+
).get(session.id, token.id);
|
|
2513
|
+
if (existing) {
|
|
2514
|
+
return { applied: false, record: parseSynthesisRow(existing) };
|
|
2515
|
+
}
|
|
2516
|
+
const card = await ensureCard(tx, token.id, session.user_id);
|
|
2517
|
+
const reviewLogId = ulid7();
|
|
2518
|
+
await evaluateRatingWithinTransaction(tx, {
|
|
2519
|
+
cardId: card.id,
|
|
2520
|
+
tokenId: token.id,
|
|
2521
|
+
userId: session.user_id,
|
|
2522
|
+
rating: input.confirmedRating,
|
|
2523
|
+
sessionId: session.id,
|
|
2524
|
+
reviewLogId
|
|
2525
|
+
});
|
|
2526
|
+
let blocked;
|
|
2527
|
+
if (input.confirmedRating === 1) {
|
|
2528
|
+
const prerequisites = await getPrerequisites(tx, token.id);
|
|
2529
|
+
if (prerequisites.length > 0) {
|
|
2530
|
+
blocked = await cascadeBlock(tx, session.user_id, token.slug);
|
|
2531
|
+
}
|
|
2532
|
+
}
|
|
2533
|
+
const notes = `Observation synthesis (${input.confidence}, inferred ${input.inferredRating}): ${input.matchedCommandTexts.slice(0, 3).join(" | ")}`;
|
|
2534
|
+
const step = await logStep(tx, {
|
|
2535
|
+
session_id: session.id,
|
|
2536
|
+
token_id: token.id,
|
|
2537
|
+
done_by: "user",
|
|
2538
|
+
rating: input.confirmedRating,
|
|
2539
|
+
notes
|
|
2540
|
+
});
|
|
2541
|
+
const evidence = {
|
|
2542
|
+
signals: input.evidence,
|
|
2543
|
+
matchedCommandTexts: input.matchedCommandTexts
|
|
2544
|
+
};
|
|
2545
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2546
|
+
await tx.prepare(
|
|
2547
|
+
`INSERT INTO session_syntheses (
|
|
2548
|
+
session_id, token_id, card_id, inferred_rating, confirmed_rating,
|
|
2549
|
+
confidence, evidence, review_log_id, session_step_id, created_at
|
|
2550
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
2551
|
+
).run(
|
|
2552
|
+
session.id,
|
|
2553
|
+
token.id,
|
|
2554
|
+
card.id,
|
|
2555
|
+
input.inferredRating,
|
|
2556
|
+
input.confirmedRating,
|
|
2557
|
+
input.confidence,
|
|
2558
|
+
JSON.stringify(evidence),
|
|
2559
|
+
reviewLogId,
|
|
2560
|
+
step.id,
|
|
2561
|
+
now
|
|
2562
|
+
);
|
|
2563
|
+
const record = await tx.prepare(
|
|
2564
|
+
"SELECT * FROM session_syntheses WHERE session_id = ? AND token_id = ?"
|
|
2565
|
+
).get(session.id, token.id);
|
|
2566
|
+
return {
|
|
2567
|
+
applied: true,
|
|
2568
|
+
record: parseSynthesisRow(record),
|
|
2569
|
+
blocked
|
|
2570
|
+
};
|
|
2571
|
+
});
|
|
1426
2572
|
}
|
|
1427
2573
|
|
|
1428
2574
|
// src/kernel/observation/shell-hooks.ts
|
|
@@ -1734,369 +2880,111 @@ function extractSequences(commands, minLen, maxLen) {
|
|
|
1734
2880
|
for (let len = minLen; len <= maxLen; len++) {
|
|
1735
2881
|
for (let i = 0; i <= normalized.length - len; i++) {
|
|
1736
2882
|
const seq = normalized.slice(i, i + len);
|
|
1737
|
-
if (new Set(seq).size >= 2) {
|
|
1738
|
-
sequences.push(seq);
|
|
1739
|
-
}
|
|
1740
|
-
}
|
|
1741
|
-
}
|
|
1742
|
-
return sequences;
|
|
1743
|
-
}
|
|
1744
|
-
function findExamplesForSequence(commands, steps) {
|
|
1745
|
-
const normalized = commands.map((c) => ({
|
|
1746
|
-
norm: normalizeCommand(c.command),
|
|
1747
|
-
full: c.command
|
|
1748
|
-
}));
|
|
1749
|
-
for (let i = 0; i <= normalized.length - steps.length; i++) {
|
|
1750
|
-
let match = true;
|
|
1751
|
-
for (let j = 0; j < steps.length; j++) {
|
|
1752
|
-
if (normalized[i + j].norm !== steps[j]) {
|
|
1753
|
-
match = false;
|
|
1754
|
-
break;
|
|
1755
|
-
}
|
|
1756
|
-
}
|
|
1757
|
-
if (match) {
|
|
1758
|
-
return normalized.slice(i, i + steps.length).map((n) => n.full);
|
|
1759
|
-
}
|
|
1760
|
-
}
|
|
1761
|
-
return [];
|
|
1762
|
-
}
|
|
1763
|
-
function removeSubsequences(candidates) {
|
|
1764
|
-
const sorted = [...candidates].sort(
|
|
1765
|
-
(a, b) => b.steps.length - a.steps.length
|
|
1766
|
-
);
|
|
1767
|
-
const result = [];
|
|
1768
|
-
for (const candidate of sorted) {
|
|
1769
|
-
const key = candidate.steps.join(" \u2192 ");
|
|
1770
|
-
const isSubsequence = result.some((longer) => {
|
|
1771
|
-
const longerKey = longer.steps.join(" \u2192 ");
|
|
1772
|
-
return longerKey.includes(key) && longerKey !== key;
|
|
1773
|
-
});
|
|
1774
|
-
if (!isSubsequence) {
|
|
1775
|
-
result.push(candidate);
|
|
1776
|
-
}
|
|
1777
|
-
}
|
|
1778
|
-
return result;
|
|
1779
|
-
}
|
|
1780
|
-
function generateSlug(steps) {
|
|
1781
|
-
return steps.map((s) => {
|
|
1782
|
-
const parts = s.split(/\s+/);
|
|
1783
|
-
return parts[parts.length - 1];
|
|
1784
|
-
}).join("-");
|
|
1785
|
-
}
|
|
1786
|
-
function describeSequence(steps) {
|
|
1787
|
-
return `Recurring pattern: ${steps.join(" \u2192 ")}`;
|
|
1788
|
-
}
|
|
1789
|
-
|
|
1790
|
-
// src/kernel/scheduler/blocker.ts
|
|
1791
|
-
function cascadeBlock(db, userId, tokenSlug) {
|
|
1792
|
-
const token = getTokenBySlug(db, tokenSlug);
|
|
1793
|
-
if (!token) {
|
|
1794
|
-
throw new Error(`Unknown token slug: ${tokenSlug}`);
|
|
1795
|
-
}
|
|
1796
|
-
ensureCard(db, token.id, userId);
|
|
1797
|
-
db.prepare(
|
|
1798
|
-
"UPDATE cards SET blocked = 1 WHERE token_id = ? AND user_id = ?"
|
|
1799
|
-
).run(token.id, userId);
|
|
1800
|
-
const prereqs = getPrerequisites(db, token.id);
|
|
1801
|
-
const surfaced = [];
|
|
1802
|
-
for (const prereq of prereqs) {
|
|
1803
|
-
const card = ensureCard(db, prereq.requires_id, userId);
|
|
1804
|
-
if (card.blocked === 1) {
|
|
1805
|
-
const prereqOfPrereq = db.prepare("SELECT COUNT(*) as n FROM prerequisites WHERE token_id = ?").get(prereq.requires_id);
|
|
1806
|
-
if (prereqOfPrereq.n === 0) {
|
|
1807
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1808
|
-
db.prepare(
|
|
1809
|
-
"UPDATE cards SET blocked = 0, due_at = ? WHERE token_id = ? AND user_id = ?"
|
|
1810
|
-
).run(now, prereq.requires_id, userId);
|
|
1811
|
-
}
|
|
1812
|
-
}
|
|
1813
|
-
surfaced.push({
|
|
1814
|
-
slug: prereq.slug,
|
|
1815
|
-
concept: prereq.concept,
|
|
1816
|
-
bloomLevel: prereq.bloom_level
|
|
1817
|
-
});
|
|
1818
|
-
}
|
|
1819
|
-
return {
|
|
1820
|
-
blockedSlug: tokenSlug,
|
|
1821
|
-
prerequisites: surfaced
|
|
1822
|
-
};
|
|
1823
|
-
}
|
|
1824
|
-
function unblockReady(db, userId) {
|
|
1825
|
-
const blockedCards = db.prepare(
|
|
1826
|
-
`SELECT c.token_id, t.slug, t.concept
|
|
1827
|
-
FROM cards c
|
|
1828
|
-
JOIN tokens t ON t.id = c.token_id
|
|
1829
|
-
WHERE c.user_id = ? AND c.blocked = 1`
|
|
1830
|
-
).all(userId);
|
|
1831
|
-
const unblocked = [];
|
|
1832
|
-
for (const card of blockedCards) {
|
|
1833
|
-
const totalPrereqs = db.prepare("SELECT COUNT(*) as n FROM prerequisites WHERE token_id = ?").get(card.token_id);
|
|
1834
|
-
const metPrereqs = db.prepare(
|
|
1835
|
-
`SELECT COUNT(*) as n FROM cards c
|
|
1836
|
-
JOIN prerequisites p ON p.requires_id = c.token_id
|
|
1837
|
-
WHERE p.token_id = ? AND c.user_id = ? AND c.reps >= 1 AND c.blocked = 0`
|
|
1838
|
-
).get(card.token_id, userId);
|
|
1839
|
-
if (totalPrereqs.n === 0 || metPrereqs.n === totalPrereqs.n) {
|
|
1840
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1841
|
-
db.prepare(
|
|
1842
|
-
"UPDATE cards SET blocked = 0, due_at = ? WHERE token_id = ? AND user_id = ?"
|
|
1843
|
-
).run(now, card.token_id, userId);
|
|
1844
|
-
unblocked.push({ slug: card.slug, concept: card.concept });
|
|
1845
|
-
}
|
|
1846
|
-
}
|
|
1847
|
-
return { unblocked };
|
|
1848
|
-
}
|
|
1849
|
-
|
|
1850
|
-
// src/kernel/recall/evaluator.ts
|
|
1851
|
-
import { ulid as ulid6 } from "ulid";
|
|
1852
|
-
|
|
1853
|
-
// src/kernel/scheduler/fsrs.ts
|
|
1854
|
-
var DEFAULT_W = [
|
|
1855
|
-
0.4072,
|
|
1856
|
-
1.1829,
|
|
1857
|
-
3.1262,
|
|
1858
|
-
15.4722,
|
|
1859
|
-
// w0–w3: initial stability per rating
|
|
1860
|
-
7.2102,
|
|
1861
|
-
0.5316,
|
|
1862
|
-
1.0651,
|
|
1863
|
-
// w4–w6: difficulty
|
|
1864
|
-
92e-4,
|
|
1865
|
-
1.5988,
|
|
1866
|
-
0.1176,
|
|
1867
|
-
1.0014,
|
|
1868
|
-
// w7–w10: stability after forgetting
|
|
1869
|
-
2.0032,
|
|
1870
|
-
0.0266,
|
|
1871
|
-
0.3077,
|
|
1872
|
-
0.15,
|
|
1873
|
-
// w11–w14: stability increase
|
|
1874
|
-
0,
|
|
1875
|
-
2.7849,
|
|
1876
|
-
0.3477,
|
|
1877
|
-
0.6831
|
|
1878
|
-
// w15–w18: additional parameters
|
|
1879
|
-
];
|
|
1880
|
-
var DEFAULT_REQUEST_RETENTION = 0.9;
|
|
1881
|
-
function clamp(value, lo, hi) {
|
|
1882
|
-
return Math.min(hi, Math.max(lo, value));
|
|
1883
|
-
}
|
|
1884
|
-
function daysBetween(a, b) {
|
|
1885
|
-
return (b.getTime() - a.getTime()) / (1e3 * 60 * 60 * 24);
|
|
1886
|
-
}
|
|
1887
|
-
function initialStability(w, rating) {
|
|
1888
|
-
return w[rating - 1];
|
|
1889
|
-
}
|
|
1890
|
-
function initialDifficulty(w, rating) {
|
|
1891
|
-
return clamp(w[4] - Math.exp(w[5] * (rating - 1)) + 1, 1, 10);
|
|
1892
|
-
}
|
|
1893
|
-
function nextDifficulty(w, d, rating) {
|
|
1894
|
-
const d0ForGood = initialDifficulty(w, 3);
|
|
1895
|
-
const updated = w[7] * d0ForGood + (1 - w[7]) * (d - w[6] * (rating - 3));
|
|
1896
|
-
return clamp(updated, 1, 10);
|
|
1897
|
-
}
|
|
1898
|
-
function retrievability(elapsed, stability) {
|
|
1899
|
-
if (stability <= 0) return 0;
|
|
1900
|
-
return (1 + elapsed / (9 * stability)) ** -1;
|
|
1901
|
-
}
|
|
1902
|
-
function stabilityAfterSuccess(w, s, d, r, rating) {
|
|
1903
|
-
const hardPenalty = rating === 2 ? w[15] : 1;
|
|
1904
|
-
const easyBonus = rating === 4 ? w[16] : 1;
|
|
1905
|
-
const inner = Math.exp(w[8]) * (11 - d) * s ** -w[9] * (Math.exp(w[10] * (1 - r)) - 1) * hardPenalty * easyBonus;
|
|
1906
|
-
return s * (inner + 1);
|
|
1907
|
-
}
|
|
1908
|
-
function stabilityAfterForgetting(w, s, d, r) {
|
|
1909
|
-
return w[11] * d ** -w[12] * ((s + 1) ** w[13] - 1) * Math.exp(w[14] * (1 - r));
|
|
1910
|
-
}
|
|
1911
|
-
function nextInterval(stability, requestRetention) {
|
|
1912
|
-
const interval = 9 * stability * (1 / requestRetention - 1);
|
|
1913
|
-
return Math.max(1, Math.round(interval));
|
|
1914
|
-
}
|
|
1915
|
-
function createFSRS(params) {
|
|
1916
|
-
const resolvedParams = {
|
|
1917
|
-
w: params?.w ?? [...DEFAULT_W],
|
|
1918
|
-
requestRetention: params?.requestRetention ?? DEFAULT_REQUEST_RETENTION
|
|
1919
|
-
};
|
|
1920
|
-
function schedule(card, rating, now) {
|
|
1921
|
-
const reviewTime = now ?? /* @__PURE__ */ new Date();
|
|
1922
|
-
const w = resolvedParams.w;
|
|
1923
|
-
const elapsed = card.lastReviewAt !== null ? Math.max(0, daysBetween(card.lastReviewAt, reviewTime)) : 0;
|
|
1924
|
-
if (card.state === "new") {
|
|
1925
|
-
const s = initialStability(w, rating);
|
|
1926
|
-
const d = initialDifficulty(w, rating);
|
|
1927
|
-
const interval2 = nextInterval(s, resolvedParams.requestRetention);
|
|
1928
|
-
const dueAt2 = new Date(reviewTime);
|
|
1929
|
-
dueAt2.setDate(dueAt2.getDate() + interval2);
|
|
1930
|
-
return {
|
|
1931
|
-
stability: s,
|
|
1932
|
-
difficulty: d,
|
|
1933
|
-
elapsedDays: 0,
|
|
1934
|
-
scheduledDays: interval2,
|
|
1935
|
-
reps: rating >= 2 ? 1 : 0,
|
|
1936
|
-
lapses: rating === 1 ? 1 : 0,
|
|
1937
|
-
state: "learning",
|
|
1938
|
-
dueAt: dueAt2,
|
|
1939
|
-
lastReviewAt: reviewTime
|
|
1940
|
-
};
|
|
1941
|
-
}
|
|
1942
|
-
const r = retrievability(elapsed, card.stability);
|
|
1943
|
-
let newStability;
|
|
1944
|
-
let newDifficulty;
|
|
1945
|
-
let newReps;
|
|
1946
|
-
let newLapses;
|
|
1947
|
-
let newState;
|
|
1948
|
-
if (rating === 1) {
|
|
1949
|
-
newStability = stabilityAfterForgetting(
|
|
1950
|
-
w,
|
|
1951
|
-
card.stability,
|
|
1952
|
-
card.difficulty,
|
|
1953
|
-
r
|
|
1954
|
-
);
|
|
1955
|
-
newDifficulty = nextDifficulty(w, card.difficulty, rating);
|
|
1956
|
-
newReps = 0;
|
|
1957
|
-
newLapses = card.lapses + 1;
|
|
1958
|
-
newState = "relearning";
|
|
1959
|
-
} else {
|
|
1960
|
-
newStability = stabilityAfterSuccess(
|
|
1961
|
-
w,
|
|
1962
|
-
card.stability,
|
|
1963
|
-
card.difficulty,
|
|
1964
|
-
r,
|
|
1965
|
-
rating
|
|
1966
|
-
);
|
|
1967
|
-
newDifficulty = nextDifficulty(w, card.difficulty, rating);
|
|
1968
|
-
newReps = card.reps + 1;
|
|
1969
|
-
newLapses = card.lapses;
|
|
1970
|
-
newState = "review";
|
|
2883
|
+
if (new Set(seq).size >= 2) {
|
|
2884
|
+
sequences.push(seq);
|
|
2885
|
+
}
|
|
1971
2886
|
}
|
|
1972
|
-
const interval = nextInterval(
|
|
1973
|
-
newStability,
|
|
1974
|
-
resolvedParams.requestRetention
|
|
1975
|
-
);
|
|
1976
|
-
const dueAt = new Date(reviewTime);
|
|
1977
|
-
dueAt.setDate(dueAt.getDate() + interval);
|
|
1978
|
-
return {
|
|
1979
|
-
stability: newStability,
|
|
1980
|
-
difficulty: newDifficulty,
|
|
1981
|
-
elapsedDays: elapsed,
|
|
1982
|
-
scheduledDays: interval,
|
|
1983
|
-
reps: newReps,
|
|
1984
|
-
lapses: newLapses,
|
|
1985
|
-
state: newState,
|
|
1986
|
-
dueAt,
|
|
1987
|
-
lastReviewAt: reviewTime
|
|
1988
|
-
};
|
|
1989
2887
|
}
|
|
1990
|
-
return
|
|
1991
|
-
schedule,
|
|
1992
|
-
params: Object.freeze(resolvedParams)
|
|
1993
|
-
};
|
|
2888
|
+
return sequences;
|
|
1994
2889
|
}
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2890
|
+
function findExamplesForSequence(commands, steps) {
|
|
2891
|
+
const normalized = commands.map((c) => ({
|
|
2892
|
+
norm: normalizeCommand(c.command),
|
|
2893
|
+
full: c.command
|
|
2894
|
+
}));
|
|
2895
|
+
for (let i = 0; i <= normalized.length - steps.length; i++) {
|
|
2896
|
+
let match = true;
|
|
2897
|
+
for (let j = 0; j < steps.length; j++) {
|
|
2898
|
+
if (normalized[i + j].norm !== steps[j]) {
|
|
2899
|
+
match = false;
|
|
2900
|
+
break;
|
|
2901
|
+
}
|
|
2902
|
+
}
|
|
2903
|
+
if (match) {
|
|
2904
|
+
return normalized.slice(i, i + steps.length).map((n) => n.full);
|
|
2905
|
+
}
|
|
2001
2906
|
}
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
elapsedDays: card.elapsed_days,
|
|
2008
|
-
scheduledDays: card.scheduled_days,
|
|
2009
|
-
reps: card.reps,
|
|
2010
|
-
lapses: card.lapses,
|
|
2011
|
-
state: card.state,
|
|
2012
|
-
dueAt: new Date(card.due_at),
|
|
2013
|
-
lastReviewAt: card.last_review_at ? new Date(card.last_review_at) : null
|
|
2014
|
-
};
|
|
2015
|
-
const updated = fsrs.schedule(schedulingCard, input.rating, now);
|
|
2016
|
-
updateCard(db, input.cardId, {
|
|
2017
|
-
stability: updated.stability,
|
|
2018
|
-
difficulty: updated.difficulty,
|
|
2019
|
-
elapsed_days: updated.elapsedDays,
|
|
2020
|
-
scheduled_days: updated.scheduledDays,
|
|
2021
|
-
reps: updated.reps,
|
|
2022
|
-
lapses: updated.lapses,
|
|
2023
|
-
state: updated.state,
|
|
2024
|
-
due_at: updated.dueAt.toISOString(),
|
|
2025
|
-
last_review_at: now.toISOString()
|
|
2026
|
-
});
|
|
2027
|
-
db.prepare(
|
|
2028
|
-
`INSERT INTO review_logs (id, card_id, token_id, user_id, rating, response_time_ms, reviewed_at, scheduled_at, session_id)
|
|
2029
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
2030
|
-
).run(
|
|
2031
|
-
ulid6(),
|
|
2032
|
-
input.cardId,
|
|
2033
|
-
input.tokenId,
|
|
2034
|
-
input.userId,
|
|
2035
|
-
input.rating,
|
|
2036
|
-
input.responseTimeMs ?? null,
|
|
2037
|
-
now.toISOString(),
|
|
2038
|
-
card.due_at,
|
|
2039
|
-
input.sessionId ?? null
|
|
2907
|
+
return [];
|
|
2908
|
+
}
|
|
2909
|
+
function removeSubsequences(candidates) {
|
|
2910
|
+
const sorted = [...candidates].sort(
|
|
2911
|
+
(a, b) => b.steps.length - a.steps.length
|
|
2040
2912
|
);
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2913
|
+
const result = [];
|
|
2914
|
+
for (const candidate of sorted) {
|
|
2915
|
+
const key = candidate.steps.join(" \u2192 ");
|
|
2916
|
+
const isSubsequence = result.some((longer) => {
|
|
2917
|
+
const longerKey = longer.steps.join(" \u2192 ");
|
|
2918
|
+
return longerKey.includes(key) && longerKey !== key;
|
|
2919
|
+
});
|
|
2920
|
+
if (!isSubsequence) {
|
|
2921
|
+
result.push(candidate);
|
|
2922
|
+
}
|
|
2923
|
+
}
|
|
2924
|
+
return result;
|
|
2925
|
+
}
|
|
2926
|
+
function generateSlug(steps) {
|
|
2927
|
+
return steps.map((s) => {
|
|
2928
|
+
const parts = s.split(/\s+/);
|
|
2929
|
+
return parts[parts.length - 1];
|
|
2930
|
+
}).join("-");
|
|
2931
|
+
}
|
|
2932
|
+
function describeSequence(steps) {
|
|
2933
|
+
return `Recurring pattern: ${steps.join(" \u2192 ")}`;
|
|
2050
2934
|
}
|
|
2051
2935
|
|
|
2052
2936
|
// src/kernel/recall/actions.ts
|
|
2053
|
-
function getReviewTarget(db, cardId, userId) {
|
|
2054
|
-
const card = getCardById(db, cardId);
|
|
2937
|
+
async function getReviewTarget(db, cardId, userId) {
|
|
2938
|
+
const card = await getCardById(db, cardId);
|
|
2055
2939
|
if (!card) {
|
|
2056
2940
|
throw new Error(`Card not found: ${cardId}`);
|
|
2057
2941
|
}
|
|
2058
2942
|
if (card.user_id !== userId) {
|
|
2059
2943
|
throw new Error(`Card ${cardId} does not belong to user ${userId}`);
|
|
2060
2944
|
}
|
|
2061
|
-
const token = getTokenById(db, card.token_id);
|
|
2945
|
+
const token = await getTokenById(db, card.token_id);
|
|
2062
2946
|
if (!token) {
|
|
2063
2947
|
throw new Error(`Token not found for card ${cardId}`);
|
|
2064
2948
|
}
|
|
2065
2949
|
return { cardId: card.id, token };
|
|
2066
2950
|
}
|
|
2067
|
-
function executeReviewAction(db, input) {
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
const
|
|
2075
|
-
|
|
2076
|
-
|
|
2951
|
+
async function executeReviewAction(db, input) {
|
|
2952
|
+
if (input.action === "rate") {
|
|
2953
|
+
if (input.rating == null) {
|
|
2954
|
+
throw new Error("rating is required for action=rate");
|
|
2955
|
+
}
|
|
2956
|
+
const rating = input.rating;
|
|
2957
|
+
return db.transaction(async (tx) => {
|
|
2958
|
+
const target2 = await getReviewTarget(tx, input.cardId, input.userId);
|
|
2959
|
+
const evaluation = await evaluateRatingWithinTransaction(tx, {
|
|
2960
|
+
cardId: target2.cardId,
|
|
2961
|
+
tokenId: target2.token.id,
|
|
2077
2962
|
userId: input.userId,
|
|
2078
|
-
rating
|
|
2963
|
+
rating
|
|
2079
2964
|
});
|
|
2080
2965
|
let blocked;
|
|
2081
|
-
if (
|
|
2082
|
-
const prereqs = getPrerequisites(
|
|
2966
|
+
if (rating === 1) {
|
|
2967
|
+
const prereqs = await getPrerequisites(tx, target2.token.id);
|
|
2083
2968
|
if (prereqs.length > 0) {
|
|
2084
|
-
blocked = cascadeBlock(
|
|
2969
|
+
blocked = await cascadeBlock(tx, input.userId, target2.token.slug);
|
|
2085
2970
|
}
|
|
2086
2971
|
}
|
|
2087
2972
|
return {
|
|
2088
2973
|
action: input.action,
|
|
2089
|
-
token:
|
|
2974
|
+
token: target2.token,
|
|
2090
2975
|
evaluation,
|
|
2091
2976
|
blocked
|
|
2092
2977
|
};
|
|
2093
|
-
}
|
|
2978
|
+
});
|
|
2979
|
+
}
|
|
2980
|
+
const target = await getReviewTarget(db, input.cardId, input.userId);
|
|
2981
|
+
switch (input.action) {
|
|
2094
2982
|
case "skip":
|
|
2095
2983
|
return { action: input.action, token: target.token, skipped: true };
|
|
2096
2984
|
case "stop":
|
|
2097
2985
|
return { action: input.action, token: target.token, stopped: true };
|
|
2098
2986
|
case "edit-token": {
|
|
2099
|
-
const updatedToken = updateToken(
|
|
2987
|
+
const updatedToken = await updateToken(
|
|
2100
2988
|
db,
|
|
2101
2989
|
target.token.slug,
|
|
2102
2990
|
input.tokenUpdates ?? {}
|
|
@@ -2108,7 +2996,7 @@ function executeReviewAction(db, input) {
|
|
|
2108
2996
|
};
|
|
2109
2997
|
}
|
|
2110
2998
|
case "deprecate-token": {
|
|
2111
|
-
const updatedToken = deprecateToken(db, target.token.slug);
|
|
2999
|
+
const updatedToken = await deprecateToken(db, target.token.slug);
|
|
2112
3000
|
return {
|
|
2113
3001
|
action: input.action,
|
|
2114
3002
|
token: target.token,
|
|
@@ -2116,7 +3004,7 @@ function executeReviewAction(db, input) {
|
|
|
2116
3004
|
};
|
|
2117
3005
|
}
|
|
2118
3006
|
case "delete-token": {
|
|
2119
|
-
const deletedToken = deleteToken(db, target.token.slug);
|
|
3007
|
+
const deletedToken = await deleteToken(db, target.token.slug);
|
|
2120
3008
|
return {
|
|
2121
3009
|
action: input.action,
|
|
2122
3010
|
token: target.token,
|
|
@@ -2124,7 +3012,11 @@ function executeReviewAction(db, input) {
|
|
|
2124
3012
|
};
|
|
2125
3013
|
}
|
|
2126
3014
|
case "delete-card": {
|
|
2127
|
-
const deletedCard = deleteCardForUser(
|
|
3015
|
+
const deletedCard = await deleteCardForUser(
|
|
3016
|
+
db,
|
|
3017
|
+
target.token.id,
|
|
3018
|
+
input.userId
|
|
3019
|
+
);
|
|
2128
3020
|
return {
|
|
2129
3021
|
action: input.action,
|
|
2130
3022
|
token: target.token,
|
|
@@ -2428,12 +3320,12 @@ function interleave(items, maxConsecutive = 2) {
|
|
|
2428
3320
|
}
|
|
2429
3321
|
|
|
2430
3322
|
// src/kernel/scheduler/queue.ts
|
|
2431
|
-
function buildReviewQueue(db, options) {
|
|
3323
|
+
async function buildReviewQueue(db, options) {
|
|
2432
3324
|
const maxNew = options.maxNew ?? 10;
|
|
2433
3325
|
const maxReviews = options.maxReviews ?? 50;
|
|
2434
3326
|
const now = options.now ?? /* @__PURE__ */ new Date();
|
|
2435
3327
|
const nowISO = now.toISOString();
|
|
2436
|
-
const dueRows = db.prepare(
|
|
3328
|
+
const dueRows = await db.prepare(
|
|
2437
3329
|
`SELECT
|
|
2438
3330
|
c.id AS card_id,
|
|
2439
3331
|
c.token_id AS token_id,
|
|
@@ -2454,7 +3346,7 @@ function buildReviewQueue(db, options) {
|
|
|
2454
3346
|
AND t.deprecated_at IS NULL
|
|
2455
3347
|
ORDER BY c.due_at ASC`
|
|
2456
3348
|
).all(options.userId, nowISO);
|
|
2457
|
-
const newRows = db.prepare(
|
|
3349
|
+
const newRows = await db.prepare(
|
|
2458
3350
|
`SELECT
|
|
2459
3351
|
c.id AS card_id,
|
|
2460
3352
|
c.token_id AS token_id,
|
|
@@ -2556,7 +3448,8 @@ import {
|
|
|
2556
3448
|
copyFileSync,
|
|
2557
3449
|
existsSync as existsSync6,
|
|
2558
3450
|
mkdirSync as mkdirSync4,
|
|
2559
|
-
readFileSync as readFileSync6
|
|
3451
|
+
readFileSync as readFileSync6,
|
|
3452
|
+
writeFileSync as writeFileSync3
|
|
2560
3453
|
} from "fs";
|
|
2561
3454
|
import { homedir as homedir4 } from "os";
|
|
2562
3455
|
import { join as join6 } from "path";
|
|
@@ -2672,102 +3565,82 @@ function distributeGlobalSkills(home = HOME) {
|
|
|
2672
3565
|
}
|
|
2673
3566
|
return results;
|
|
2674
3567
|
}
|
|
2675
|
-
|
|
2676
|
-
const results = [];
|
|
2677
|
-
const hookLine = `
|
|
3568
|
+
var POSIX_OLD_HOOK = `
|
|
2678
3569
|
# ZAM Shell Observation Hooks
|
|
2679
3570
|
if (command -v zam >/dev/null 2>&1); then eval "$(zam monitor start --quiet)"; fi
|
|
2680
3571
|
`;
|
|
2681
|
-
|
|
3572
|
+
var POWERSHELL_OLD_HOOK = `
|
|
2682
3573
|
# ZAM Shell Observation Hooks
|
|
2683
3574
|
if (Get-Command zam -ErrorAction SilentlyContinue) { Invoke-Expression (& zam monitor start --quiet pwsh) }
|
|
2684
3575
|
`;
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
|
|
2693
|
-
|
|
2694
|
-
|
|
2695
|
-
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
}
|
|
3576
|
+
var HOOK_MARKER = "# ZAM Monitor Session Helper";
|
|
3577
|
+
function posixHook(shell) {
|
|
3578
|
+
return `
|
|
3579
|
+
${HOOK_MARKER}
|
|
3580
|
+
zam-monitor-session() {
|
|
3581
|
+
local session_id="\${1:-}"
|
|
3582
|
+
if [ -z "$session_id" ]; then
|
|
3583
|
+
printf 'Usage: zam-monitor-session <session-id>
|
|
3584
|
+
' >&2
|
|
3585
|
+
return 2
|
|
3586
|
+
fi
|
|
3587
|
+
eval "$(command zam monitor start --session "$session_id" --shell ${shell})"
|
|
3588
|
+
}
|
|
3589
|
+
`;
|
|
3590
|
+
}
|
|
3591
|
+
var POWERSHELL_HOOK = `
|
|
3592
|
+
${HOOK_MARKER}
|
|
3593
|
+
function Start-ZamMonitor {
|
|
3594
|
+
param([Parameter(Mandatory = $true)][string]$Session)
|
|
3595
|
+
Invoke-Expression (& zam monitor start --session $Session --shell pwsh)
|
|
3596
|
+
}
|
|
3597
|
+
`;
|
|
3598
|
+
function installHook(file, hook, oldHook) {
|
|
3599
|
+
try {
|
|
3600
|
+
const content = existsSync6(file) ? readFileSync6(file, "utf8") : "";
|
|
3601
|
+
if (content.includes(HOOK_MARKER)) {
|
|
3602
|
+
return { success: true, alreadyHooked: true };
|
|
3603
|
+
}
|
|
3604
|
+
if (content.includes(oldHook.trim())) {
|
|
3605
|
+
writeFileSync3(file, content.replace(oldHook.trim(), hook.trim()), "utf8");
|
|
3606
|
+
} else {
|
|
3607
|
+
appendFileSync2(file, hook);
|
|
2712
3608
|
}
|
|
3609
|
+
return { success: true, alreadyHooked: false };
|
|
3610
|
+
} catch {
|
|
3611
|
+
return { success: false, alreadyHooked: false };
|
|
3612
|
+
}
|
|
3613
|
+
}
|
|
3614
|
+
function injectShellHooks(home = HOME) {
|
|
3615
|
+
const results = [];
|
|
3616
|
+
const zshrc = join6(home, ".zshrc");
|
|
3617
|
+
if (existsSync6(zshrc)) {
|
|
3618
|
+
const status = installHook(zshrc, posixHook("zsh"), POSIX_OLD_HOOK);
|
|
3619
|
+
results.push({ shell: "zsh", file: zshrc, ...status });
|
|
2713
3620
|
}
|
|
2714
|
-
const bashrc = join6(
|
|
3621
|
+
const bashrc = join6(home, ".bashrc");
|
|
2715
3622
|
if (existsSync6(bashrc)) {
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
if (content.includes("zam monitor start")) {
|
|
2719
|
-
results.push({
|
|
2720
|
-
shell: "bash",
|
|
2721
|
-
file: bashrc,
|
|
2722
|
-
success: true,
|
|
2723
|
-
alreadyHooked: true
|
|
2724
|
-
});
|
|
2725
|
-
} else {
|
|
2726
|
-
appendFileSync2(bashrc, hookLine);
|
|
2727
|
-
results.push({
|
|
2728
|
-
shell: "bash",
|
|
2729
|
-
file: bashrc,
|
|
2730
|
-
success: true,
|
|
2731
|
-
alreadyHooked: false
|
|
2732
|
-
});
|
|
2733
|
-
}
|
|
2734
|
-
} catch {
|
|
2735
|
-
results.push({
|
|
2736
|
-
shell: "bash",
|
|
2737
|
-
file: bashrc,
|
|
2738
|
-
success: false,
|
|
2739
|
-
alreadyHooked: false
|
|
2740
|
-
});
|
|
2741
|
-
}
|
|
3623
|
+
const status = installHook(bashrc, posixHook("bash"), POSIX_OLD_HOOK);
|
|
3624
|
+
results.push({ shell: "bash", file: bashrc, ...status });
|
|
2742
3625
|
}
|
|
2743
3626
|
const pwshDirs = [
|
|
2744
|
-
join6(
|
|
2745
|
-
join6(
|
|
3627
|
+
join6(home, "Documents", "PowerShell"),
|
|
3628
|
+
join6(home, "Documents", "WindowsPowerShell")
|
|
2746
3629
|
];
|
|
2747
3630
|
for (const dir of pwshDirs) {
|
|
2748
3631
|
const profileFile = join6(dir, "Microsoft.PowerShell_profile.ps1");
|
|
2749
3632
|
try {
|
|
2750
3633
|
mkdirSync4(dir, { recursive: true });
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
});
|
|
2762
|
-
} else {
|
|
2763
|
-
appendFileSync2(profileFile, pwshHookLine);
|
|
2764
|
-
results.push({
|
|
2765
|
-
shell: "powershell",
|
|
2766
|
-
file: profileFile,
|
|
2767
|
-
success: true,
|
|
2768
|
-
alreadyHooked: false
|
|
2769
|
-
});
|
|
2770
|
-
}
|
|
3634
|
+
const status = installHook(
|
|
3635
|
+
profileFile,
|
|
3636
|
+
POWERSHELL_HOOK,
|
|
3637
|
+
POWERSHELL_OLD_HOOK
|
|
3638
|
+
);
|
|
3639
|
+
results.push({
|
|
3640
|
+
shell: "powershell",
|
|
3641
|
+
file: profileFile,
|
|
3642
|
+
...status
|
|
3643
|
+
});
|
|
2771
3644
|
} catch {
|
|
2772
3645
|
results.push({
|
|
2773
3646
|
shell: "powershell",
|
|
@@ -2988,11 +3861,61 @@ function t(locale, key, params = {}) {
|
|
|
2988
3861
|
return str;
|
|
2989
3862
|
}
|
|
2990
3863
|
|
|
2991
|
-
// src/kernel/system/
|
|
2992
|
-
import {
|
|
2993
|
-
import { existsSync as existsSync7 } from "fs";
|
|
3864
|
+
// src/kernel/system/install-config.ts
|
|
3865
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync5, readFileSync as readFileSync7, writeFileSync as writeFileSync4 } from "fs";
|
|
2994
3866
|
import { homedir as homedir5 } from "os";
|
|
2995
|
-
import { join as join7 } from "path";
|
|
3867
|
+
import { dirname as dirname4, join as join7 } from "path";
|
|
3868
|
+
var DEFAULT_CONFIG_PATH = join7(homedir5(), ".zam", "config.json");
|
|
3869
|
+
function loadInstallConfig(path = DEFAULT_CONFIG_PATH) {
|
|
3870
|
+
if (!existsSync7(path)) return {};
|
|
3871
|
+
try {
|
|
3872
|
+
return JSON.parse(readFileSync7(path, "utf-8"));
|
|
3873
|
+
} catch {
|
|
3874
|
+
return {};
|
|
3875
|
+
}
|
|
3876
|
+
}
|
|
3877
|
+
function saveInstallConfig(config, path = DEFAULT_CONFIG_PATH) {
|
|
3878
|
+
const dir = dirname4(path);
|
|
3879
|
+
if (!existsSync7(dir)) mkdirSync5(dir, { recursive: true });
|
|
3880
|
+
writeFileSync4(path, `${JSON.stringify(config, null, 2)}
|
|
3881
|
+
`, "utf-8");
|
|
3882
|
+
}
|
|
3883
|
+
function getInstallMode(path = DEFAULT_CONFIG_PATH) {
|
|
3884
|
+
return loadInstallConfig(path).mode ?? "developer";
|
|
3885
|
+
}
|
|
3886
|
+
function setInstallMode(mode, path = DEFAULT_CONFIG_PATH) {
|
|
3887
|
+
const config = loadInstallConfig(path);
|
|
3888
|
+
config.mode = mode;
|
|
3889
|
+
saveInstallConfig(config, path);
|
|
3890
|
+
}
|
|
3891
|
+
function getInstallChannel(path = DEFAULT_CONFIG_PATH) {
|
|
3892
|
+
const config = loadInstallConfig(path);
|
|
3893
|
+
if (config.channel) return config.channel;
|
|
3894
|
+
return (config.mode ?? "developer") === "developer" ? "developer" : "direct";
|
|
3895
|
+
}
|
|
3896
|
+
function setInstallChannel(channel, path = DEFAULT_CONFIG_PATH) {
|
|
3897
|
+
const config = loadInstallConfig(path);
|
|
3898
|
+
config.channel = channel;
|
|
3899
|
+
saveInstallConfig(config, path);
|
|
3900
|
+
}
|
|
3901
|
+
function detectSyncProvider(dir) {
|
|
3902
|
+
const p = dir.toLowerCase();
|
|
3903
|
+
if (p.includes("onedrive")) return "OneDrive";
|
|
3904
|
+
if (p.includes("dropbox")) return "Dropbox";
|
|
3905
|
+
if (p.includes("google drive") || p.includes("googledrive") || p.includes("/my drive")) {
|
|
3906
|
+
return "Google Drive";
|
|
3907
|
+
}
|
|
3908
|
+
if (p.includes("icloud") || p.includes("mobile documents")) {
|
|
3909
|
+
return "iCloud Drive";
|
|
3910
|
+
}
|
|
3911
|
+
return null;
|
|
3912
|
+
}
|
|
3913
|
+
|
|
3914
|
+
// src/kernel/system/installer.ts
|
|
3915
|
+
import { execFileSync, execSync } from "child_process";
|
|
3916
|
+
import { existsSync as existsSync8 } from "fs";
|
|
3917
|
+
import { homedir as homedir6 } from "os";
|
|
3918
|
+
import { join as join8 } from "path";
|
|
2996
3919
|
function hasCommand(cmd) {
|
|
2997
3920
|
try {
|
|
2998
3921
|
const checkCmd = process.platform === "win32" ? `where ${cmd}` : `which ${cmd}`;
|
|
@@ -3009,7 +3932,7 @@ function installFastFlowLM() {
|
|
|
3009
3932
|
message: "FastFlowLM is only supported on Windows."
|
|
3010
3933
|
};
|
|
3011
3934
|
}
|
|
3012
|
-
const hasFlm = hasCommand("flm") ||
|
|
3935
|
+
const hasFlm = hasCommand("flm") || existsSync8("C:\\Program Files\\flm\\flm.exe");
|
|
3013
3936
|
if (hasFlm) {
|
|
3014
3937
|
return { success: true, message: "FastFlowLM is already installed." };
|
|
3015
3938
|
}
|
|
@@ -3036,8 +3959,8 @@ function installFastFlowLM() {
|
|
|
3036
3959
|
function installOllama() {
|
|
3037
3960
|
const isMac = process.platform === "darwin";
|
|
3038
3961
|
const isWin = process.platform === "win32";
|
|
3039
|
-
const hasOllama = hasCommand("ollama") || isMac &&
|
|
3040
|
-
|
|
3962
|
+
const hasOllama = hasCommand("ollama") || isMac && existsSync8("/Applications/Ollama.app") || isWin && existsSync8(
|
|
3963
|
+
join8(homedir6(), "AppData", "Local", "Programs", "Ollama", "ollama.exe")
|
|
3041
3964
|
);
|
|
3042
3965
|
if (hasOllama) {
|
|
3043
3966
|
return { success: true, message: "Ollama is already installed." };
|
|
@@ -3094,6 +4017,108 @@ function installOllama() {
|
|
|
3094
4017
|
}
|
|
3095
4018
|
}
|
|
3096
4019
|
}
|
|
4020
|
+
function resolveOllamaCommand() {
|
|
4021
|
+
if (hasCommand("ollama")) return "ollama";
|
|
4022
|
+
const candidates = process.platform === "win32" ? [
|
|
4023
|
+
join8(
|
|
4024
|
+
homedir6(),
|
|
4025
|
+
"AppData",
|
|
4026
|
+
"Local",
|
|
4027
|
+
"Programs",
|
|
4028
|
+
"Ollama",
|
|
4029
|
+
"ollama.exe"
|
|
4030
|
+
)
|
|
4031
|
+
] : process.platform === "darwin" ? ["/Applications/Ollama.app/Contents/Resources/ollama"] : [];
|
|
4032
|
+
return candidates.find((candidate) => existsSync8(candidate));
|
|
4033
|
+
}
|
|
4034
|
+
function prepareLocalModel(runner, model) {
|
|
4035
|
+
if (runner === "fastflowlm") {
|
|
4036
|
+
return {
|
|
4037
|
+
success: true,
|
|
4038
|
+
message: `${model} will be downloaded by FastFlowLM on first use.`
|
|
4039
|
+
};
|
|
4040
|
+
}
|
|
4041
|
+
if (runner !== "ollama") {
|
|
4042
|
+
return {
|
|
4043
|
+
success: false,
|
|
4044
|
+
message: "No supported local LLM runner was selected."
|
|
4045
|
+
};
|
|
4046
|
+
}
|
|
4047
|
+
const ollamaCommand = resolveOllamaCommand();
|
|
4048
|
+
if (!ollamaCommand) {
|
|
4049
|
+
return {
|
|
4050
|
+
success: false,
|
|
4051
|
+
message: `Ollama was installed but its command is not available yet. Restart the terminal, then run ollama pull ${model}.`
|
|
4052
|
+
};
|
|
4053
|
+
}
|
|
4054
|
+
console.log(`Downloading ${model} with Ollama...`);
|
|
4055
|
+
try {
|
|
4056
|
+
execFileSync(ollamaCommand, ["pull", model], { stdio: "inherit" });
|
|
4057
|
+
return { success: true, message: `${model} is ready in Ollama.` };
|
|
4058
|
+
} catch (err) {
|
|
4059
|
+
return {
|
|
4060
|
+
success: false,
|
|
4061
|
+
message: `Could not prepare ${model}: ${err.message}. Start Ollama and run: ollama pull ${model}`
|
|
4062
|
+
};
|
|
4063
|
+
}
|
|
4064
|
+
}
|
|
4065
|
+
function planOpenCodeInstall(env) {
|
|
4066
|
+
if (env.hasNpm) {
|
|
4067
|
+
return { method: "npm", command: "npm install -g opencode-ai" };
|
|
4068
|
+
}
|
|
4069
|
+
if (env.platform === "darwin") {
|
|
4070
|
+
if (env.hasBrew) {
|
|
4071
|
+
return {
|
|
4072
|
+
method: "homebrew",
|
|
4073
|
+
command: "brew install anomalyco/tap/opencode"
|
|
4074
|
+
};
|
|
4075
|
+
}
|
|
4076
|
+
return {
|
|
4077
|
+
method: "script",
|
|
4078
|
+
command: "curl -fsSL https://opencode.ai/install | bash"
|
|
4079
|
+
};
|
|
4080
|
+
}
|
|
4081
|
+
if (env.platform === "win32") {
|
|
4082
|
+
if (env.hasScoop)
|
|
4083
|
+
return { method: "scoop", command: "scoop install opencode" };
|
|
4084
|
+
if (env.hasChoco) {
|
|
4085
|
+
return { method: "chocolatey", command: "choco install opencode" };
|
|
4086
|
+
}
|
|
4087
|
+
return null;
|
|
4088
|
+
}
|
|
4089
|
+
return {
|
|
4090
|
+
method: "script",
|
|
4091
|
+
command: "curl -fsSL https://opencode.ai/install | bash"
|
|
4092
|
+
};
|
|
4093
|
+
}
|
|
4094
|
+
function installOpenCode() {
|
|
4095
|
+
if (hasCommand("opencode")) {
|
|
4096
|
+
return { success: true, message: "opencode is already installed." };
|
|
4097
|
+
}
|
|
4098
|
+
const plan = planOpenCodeInstall({
|
|
4099
|
+
platform: process.platform,
|
|
4100
|
+
hasNpm: hasCommand("npm"),
|
|
4101
|
+
hasBrew: hasCommand("brew"),
|
|
4102
|
+
hasScoop: hasCommand("scoop"),
|
|
4103
|
+
hasChoco: hasCommand("choco")
|
|
4104
|
+
});
|
|
4105
|
+
if (!plan) {
|
|
4106
|
+
return {
|
|
4107
|
+
success: false,
|
|
4108
|
+
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)."
|
|
4109
|
+
};
|
|
4110
|
+
}
|
|
4111
|
+
console.log(`Installing opencode via ${plan.method}...`);
|
|
4112
|
+
try {
|
|
4113
|
+
execSync(plan.command, { stdio: "inherit" });
|
|
4114
|
+
return { success: true, message: `opencode installed via ${plan.method}.` };
|
|
4115
|
+
} catch (err) {
|
|
4116
|
+
return {
|
|
4117
|
+
success: false,
|
|
4118
|
+
message: `Failed to install opencode: ${err.message}. Try manually: ${plan.command}`
|
|
4119
|
+
};
|
|
4120
|
+
}
|
|
4121
|
+
}
|
|
3097
4122
|
|
|
3098
4123
|
// src/kernel/system/locale.ts
|
|
3099
4124
|
import { execSync as execSync2 } from "child_process";
|
|
@@ -3192,72 +4217,164 @@ function getSystemProfile() {
|
|
|
3192
4217
|
}
|
|
3193
4218
|
|
|
3194
4219
|
// src/kernel/system/repos.ts
|
|
3195
|
-
import { existsSync as
|
|
4220
|
+
import { existsSync as existsSync9 } from "fs";
|
|
3196
4221
|
import { resolve as resolve2 } from "path";
|
|
3197
|
-
function getRepoPaths(db) {
|
|
3198
|
-
const personalSetting = getSetting(db, "repo.personal") || getSetting(db, "personal.workspace_dir");
|
|
3199
|
-
const teamSetting = getSetting(db, "repo.team");
|
|
3200
|
-
const orgSetting = getSetting(db, "repo.org");
|
|
4222
|
+
async function getRepoPaths(db) {
|
|
4223
|
+
const personalSetting = await getSetting(db, "repo.personal") || await getSetting(db, "personal.workspace_dir");
|
|
4224
|
+
const teamSetting = await getSetting(db, "repo.team");
|
|
4225
|
+
const orgSetting = await getSetting(db, "repo.org");
|
|
3201
4226
|
return {
|
|
3202
4227
|
personal: personalSetting ? resolve2(personalSetting) : null,
|
|
3203
4228
|
team: teamSetting ? resolve2(teamSetting) : null,
|
|
3204
4229
|
org: orgSetting ? resolve2(orgSetting) : null
|
|
3205
4230
|
};
|
|
3206
4231
|
}
|
|
3207
|
-
function resolveRepoPath(db, type) {
|
|
3208
|
-
const paths = getRepoPaths(db);
|
|
4232
|
+
async function resolveRepoPath(db, type) {
|
|
4233
|
+
const paths = await getRepoPaths(db);
|
|
3209
4234
|
return paths[type];
|
|
3210
4235
|
}
|
|
3211
|
-
function resolveAllBeliefPaths(db) {
|
|
3212
|
-
const paths = getRepoPaths(db);
|
|
4236
|
+
async function resolveAllBeliefPaths(db) {
|
|
4237
|
+
const paths = await getRepoPaths(db);
|
|
3213
4238
|
const dirs = [];
|
|
3214
4239
|
if (paths.personal) {
|
|
3215
4240
|
const personalDir = resolve2(paths.personal, "beliefs");
|
|
3216
|
-
if (
|
|
4241
|
+
if (existsSync9(personalDir)) dirs.push(personalDir);
|
|
3217
4242
|
}
|
|
3218
4243
|
if (paths.team) {
|
|
3219
4244
|
const teamDir = resolve2(paths.team, "beliefs");
|
|
3220
|
-
if (
|
|
4245
|
+
if (existsSync9(teamDir)) dirs.push(teamDir);
|
|
3221
4246
|
}
|
|
3222
4247
|
if (paths.org) {
|
|
3223
4248
|
const orgDir = resolve2(paths.org, "beliefs");
|
|
3224
|
-
if (
|
|
4249
|
+
if (existsSync9(orgDir)) dirs.push(orgDir);
|
|
3225
4250
|
}
|
|
3226
4251
|
return dirs;
|
|
3227
4252
|
}
|
|
3228
|
-
function resolveAllGoalPaths(db) {
|
|
3229
|
-
const paths = getRepoPaths(db);
|
|
4253
|
+
async function resolveAllGoalPaths(db) {
|
|
4254
|
+
const paths = await getRepoPaths(db);
|
|
3230
4255
|
const dirs = [];
|
|
3231
4256
|
if (paths.personal) {
|
|
3232
4257
|
const personalDir = resolve2(paths.personal, "goals");
|
|
3233
|
-
if (
|
|
4258
|
+
if (existsSync9(personalDir)) dirs.push(personalDir);
|
|
3234
4259
|
}
|
|
3235
4260
|
if (paths.team) {
|
|
3236
4261
|
const teamDir = resolve2(paths.team, "goals");
|
|
3237
|
-
if (
|
|
4262
|
+
if (existsSync9(teamDir)) dirs.push(teamDir);
|
|
3238
4263
|
}
|
|
3239
4264
|
if (paths.org) {
|
|
3240
4265
|
const orgDir = resolve2(paths.org, "goals");
|
|
3241
|
-
if (
|
|
4266
|
+
if (existsSync9(orgDir)) dirs.push(orgDir);
|
|
3242
4267
|
}
|
|
3243
4268
|
return dirs;
|
|
3244
4269
|
}
|
|
4270
|
+
|
|
4271
|
+
// src/kernel/system/update-check.ts
|
|
4272
|
+
var WINGET_PACKAGE_ID = "ZAM.ZAM";
|
|
4273
|
+
var HOMEBREW_CASK = "zam";
|
|
4274
|
+
function parseVersion(version) {
|
|
4275
|
+
const clean = version.trim().replace(/^v/i, "");
|
|
4276
|
+
const [main, pre = ""] = clean.split("-", 2);
|
|
4277
|
+
const core = main.split(".").map((n) => Number.parseInt(n, 10) || 0);
|
|
4278
|
+
while (core.length < 3) core.push(0);
|
|
4279
|
+
return { core, pre: pre ? pre.split(".") : [] };
|
|
4280
|
+
}
|
|
4281
|
+
function compareVersions(a, b) {
|
|
4282
|
+
const pa = parseVersion(a);
|
|
4283
|
+
const pb = parseVersion(b);
|
|
4284
|
+
for (let i = 0; i < 3; i++) {
|
|
4285
|
+
if (pa.core[i] !== pb.core[i]) return pa.core[i] > pb.core[i] ? 1 : -1;
|
|
4286
|
+
}
|
|
4287
|
+
if (pa.pre.length === 0 && pb.pre.length === 0) return 0;
|
|
4288
|
+
if (pa.pre.length === 0) return 1;
|
|
4289
|
+
if (pb.pre.length === 0) return -1;
|
|
4290
|
+
const len = Math.max(pa.pre.length, pb.pre.length);
|
|
4291
|
+
for (let i = 0; i < len; i++) {
|
|
4292
|
+
const x = pa.pre[i];
|
|
4293
|
+
const y = pb.pre[i];
|
|
4294
|
+
if (x === void 0) return -1;
|
|
4295
|
+
if (y === void 0) return 1;
|
|
4296
|
+
const xNum = /^\d+$/.test(x);
|
|
4297
|
+
const yNum = /^\d+$/.test(y);
|
|
4298
|
+
if (xNum && yNum) {
|
|
4299
|
+
const dx = Number(x);
|
|
4300
|
+
const dy = Number(y);
|
|
4301
|
+
if (dx !== dy) return dx > dy ? 1 : -1;
|
|
4302
|
+
} else if (xNum !== yNum) {
|
|
4303
|
+
return xNum ? -1 : 1;
|
|
4304
|
+
} else if (x !== y) {
|
|
4305
|
+
return x > y ? 1 : -1;
|
|
4306
|
+
}
|
|
4307
|
+
}
|
|
4308
|
+
return 0;
|
|
4309
|
+
}
|
|
4310
|
+
function decideUpdate(input) {
|
|
4311
|
+
const { currentVersion, latestVersion, channel } = input;
|
|
4312
|
+
const base = { currentVersion, latestVersion, channel };
|
|
4313
|
+
if (compareVersions(latestVersion, currentVersion) <= 0) {
|
|
4314
|
+
return {
|
|
4315
|
+
...base,
|
|
4316
|
+
updateAvailable: false,
|
|
4317
|
+
action: "none",
|
|
4318
|
+
reason: "Already on the latest version."
|
|
4319
|
+
};
|
|
4320
|
+
}
|
|
4321
|
+
switch (channel) {
|
|
4322
|
+
case "direct":
|
|
4323
|
+
return {
|
|
4324
|
+
...base,
|
|
4325
|
+
updateAvailable: true,
|
|
4326
|
+
action: "self-update",
|
|
4327
|
+
reason: "A signed update can be installed in place."
|
|
4328
|
+
};
|
|
4329
|
+
case "winget":
|
|
4330
|
+
return {
|
|
4331
|
+
...base,
|
|
4332
|
+
updateAvailable: true,
|
|
4333
|
+
action: "run-command",
|
|
4334
|
+
command: `winget upgrade --id ${WINGET_PACKAGE_ID}`,
|
|
4335
|
+
reason: "Update available through winget."
|
|
4336
|
+
};
|
|
4337
|
+
case "homebrew":
|
|
4338
|
+
return {
|
|
4339
|
+
...base,
|
|
4340
|
+
updateAvailable: true,
|
|
4341
|
+
action: "run-command",
|
|
4342
|
+
command: `brew upgrade --cask ${HOMEBREW_CASK}`,
|
|
4343
|
+
reason: "Update available through Homebrew."
|
|
4344
|
+
};
|
|
4345
|
+
default:
|
|
4346
|
+
return {
|
|
4347
|
+
...base,
|
|
4348
|
+
updateAvailable: true,
|
|
4349
|
+
action: "inform",
|
|
4350
|
+
command: "git pull && npm install && npm run build",
|
|
4351
|
+
reason: "Developer install \u2014 update from source."
|
|
4352
|
+
};
|
|
4353
|
+
}
|
|
4354
|
+
}
|
|
3245
4355
|
export {
|
|
3246
4356
|
DEFAULT_REVIEW_CONTEXT_MAX_CHARS,
|
|
4357
|
+
HOMEBREW_CASK,
|
|
4358
|
+
SNAPSHOT_VERSION,
|
|
4359
|
+
WINGET_PACKAGE_ID,
|
|
3247
4360
|
addPrerequisite,
|
|
3248
4361
|
analyzeObservation,
|
|
4362
|
+
applySessionSynthesis,
|
|
3249
4363
|
buildReviewQueue,
|
|
3250
4364
|
cascadeBlock,
|
|
3251
4365
|
clearADOCredentials,
|
|
3252
4366
|
clearTursoCredentials,
|
|
4367
|
+
compareVersions,
|
|
3253
4368
|
createAgentSkill,
|
|
3254
4369
|
createFSRS,
|
|
3255
4370
|
createGoal,
|
|
3256
4371
|
createToken,
|
|
4372
|
+
decideUpdate,
|
|
3257
4373
|
deleteCardForUser,
|
|
3258
4374
|
deleteSetting,
|
|
3259
4375
|
deleteToken,
|
|
3260
4376
|
deprecateToken,
|
|
4377
|
+
detectSyncProvider,
|
|
3261
4378
|
detectSystemLocale,
|
|
3262
4379
|
discoverSkills,
|
|
3263
4380
|
distributeGlobalSkills,
|
|
@@ -3266,6 +4383,7 @@ export {
|
|
|
3266
4383
|
ensureMonitorDir,
|
|
3267
4384
|
evaluateRating,
|
|
3268
4385
|
executeReviewAction,
|
|
4386
|
+
exportSnapshot,
|
|
3269
4387
|
extractTasks,
|
|
3270
4388
|
extractTokenRefs,
|
|
3271
4389
|
fetchActiveWorkItems,
|
|
@@ -3292,6 +4410,8 @@ export {
|
|
|
3292
4410
|
getDueCards,
|
|
3293
4411
|
getGoal,
|
|
3294
4412
|
getGoalTree,
|
|
4413
|
+
getInstallChannel,
|
|
4414
|
+
getInstallMode,
|
|
3295
4415
|
getMonitorDir,
|
|
3296
4416
|
getMonitorLogStats,
|
|
3297
4417
|
getMonitorPath,
|
|
@@ -3301,23 +4421,28 @@ export {
|
|
|
3301
4421
|
getReviewsForCard,
|
|
3302
4422
|
getReviewsForUser,
|
|
3303
4423
|
getSessionSummary,
|
|
4424
|
+
getSessionSynthesisRecords,
|
|
3304
4425
|
getSetting,
|
|
3305
4426
|
getSystemProfile,
|
|
3306
4427
|
getTokenById,
|
|
3307
4428
|
getTokenBySlug,
|
|
3308
4429
|
getTokenDeleteImpact,
|
|
4430
|
+
getTokenNeighborhood,
|
|
3309
4431
|
getTursoCredentials,
|
|
3310
4432
|
getUserStats,
|
|
3311
4433
|
hasCommand,
|
|
4434
|
+
importSnapshot,
|
|
3312
4435
|
injectShellHooks,
|
|
3313
4436
|
installFastFlowLM,
|
|
3314
4437
|
installOllama,
|
|
4438
|
+
installOpenCode,
|
|
3315
4439
|
interleave,
|
|
3316
4440
|
listAgentSkills,
|
|
3317
4441
|
listGoals,
|
|
3318
4442
|
listTokens,
|
|
3319
4443
|
loadADOConfig,
|
|
3320
4444
|
loadCredentials,
|
|
4445
|
+
loadInstallConfig,
|
|
3321
4446
|
logReview,
|
|
3322
4447
|
logStep,
|
|
3323
4448
|
matchesFilePath,
|
|
@@ -3326,9 +4451,14 @@ export {
|
|
|
3326
4451
|
normalizePath,
|
|
3327
4452
|
openDatabase,
|
|
3328
4453
|
openDatabaseWithSync,
|
|
4454
|
+
openRemoteDatabase,
|
|
3329
4455
|
pairCommands,
|
|
3330
4456
|
parseGoalFile,
|
|
3331
4457
|
parseMonitorLog,
|
|
4458
|
+
parseSnapshot,
|
|
4459
|
+
planOpenCodeInstall,
|
|
4460
|
+
prepareLocalModel,
|
|
4461
|
+
prepareSessionSynthesis,
|
|
3332
4462
|
readMonitorLog,
|
|
3333
4463
|
resolveAllBeliefPaths,
|
|
3334
4464
|
resolveAllGoalPaths,
|
|
@@ -3336,8 +4466,11 @@ export {
|
|
|
3336
4466
|
resolveRepoPath,
|
|
3337
4467
|
resolveReviewContext,
|
|
3338
4468
|
saveCredentials,
|
|
4469
|
+
saveInstallConfig,
|
|
3339
4470
|
serializeGoal,
|
|
3340
4471
|
setADOCredentials,
|
|
4472
|
+
setInstallChannel,
|
|
4473
|
+
setInstallMode,
|
|
3341
4474
|
setSetting,
|
|
3342
4475
|
setTursoCredentials,
|
|
3343
4476
|
startSession,
|
|
@@ -3346,6 +4479,8 @@ export {
|
|
|
3346
4479
|
updateCard,
|
|
3347
4480
|
updateGoalStatus,
|
|
3348
4481
|
updateToken,
|
|
4482
|
+
verifySnapshot,
|
|
4483
|
+
wouldCreateCycle,
|
|
3349
4484
|
writeMonitorEvent
|
|
3350
4485
|
};
|
|
3351
4486
|
//# sourceMappingURL=index.js.map
|