zam-core 0.3.2 → 0.3.5
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/{.gemini → .agent}/skills/zam/SKILL.md +361 -335
- package/.agents/skills/zam/SKILL.md +364 -0
- package/.claude/skills/zam/SKILL.md +331 -331
- package/LICENSE +199 -199
- package/README.de.md +127 -86
- package/README.md +147 -86
- package/dist/cli/index.js +6371 -2465
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +823 -547
- package/dist/index.js +2748 -1326
- package/dist/index.js.map +1 -1
- package/package.json +57 -56
package/dist/index.js
CHANGED
|
@@ -1,14 +1,232 @@
|
|
|
1
|
-
// src/kernel/
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
// src/kernel/analytics/stats.ts
|
|
2
|
+
function q(db, sql, ...params) {
|
|
3
|
+
return db.prepare(sql).get(...params);
|
|
4
|
+
}
|
|
5
|
+
function getUserStats(db, userId) {
|
|
6
|
+
return {
|
|
7
|
+
userId,
|
|
8
|
+
totalTokens: q(db, "SELECT COUNT(*) as n FROM tokens").n,
|
|
9
|
+
cardsInDeck: q(db, "SELECT COUNT(*) as n FROM cards WHERE user_id = ?", userId).n,
|
|
10
|
+
dueToday: q(
|
|
11
|
+
db,
|
|
12
|
+
"SELECT COUNT(*) as n FROM cards WHERE user_id = ? AND blocked = 0 AND due_at <= datetime('now')",
|
|
13
|
+
userId
|
|
14
|
+
).n,
|
|
15
|
+
blocked: q(
|
|
16
|
+
db,
|
|
17
|
+
"SELECT COUNT(*) as n FROM cards WHERE user_id = ? AND blocked = 1",
|
|
18
|
+
userId
|
|
19
|
+
).n,
|
|
20
|
+
mature: q(
|
|
21
|
+
db,
|
|
22
|
+
"SELECT COUNT(*) as n FROM cards WHERE user_id = ? AND reps >= 3 AND stability >= 21",
|
|
23
|
+
userId
|
|
24
|
+
).n,
|
|
25
|
+
avgStability: (() => {
|
|
26
|
+
const v = q(
|
|
27
|
+
db,
|
|
28
|
+
"SELECT AVG(stability) as v FROM cards WHERE user_id = ? AND reps > 0",
|
|
29
|
+
userId
|
|
30
|
+
);
|
|
31
|
+
return v.v ? Math.round(v.v * 100) / 100 : null;
|
|
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
|
+
})()
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
function getDomainCompetence(db, userId) {
|
|
43
|
+
const domains = db.prepare(
|
|
44
|
+
`SELECT DISTINCT t.domain FROM cards c
|
|
45
|
+
JOIN tokens t ON t.id = c.token_id
|
|
46
|
+
WHERE c.user_id = ? AND t.domain != ''`
|
|
47
|
+
).all(userId);
|
|
48
|
+
return domains.map((d) => {
|
|
49
|
+
const total = q(
|
|
50
|
+
db,
|
|
51
|
+
`SELECT COUNT(*) as n FROM cards c
|
|
52
|
+
JOIN tokens t ON t.id = c.token_id
|
|
53
|
+
WHERE c.user_id = ? AND t.domain = ?`,
|
|
54
|
+
userId,
|
|
55
|
+
d.domain
|
|
56
|
+
).n;
|
|
57
|
+
const mature = q(
|
|
58
|
+
db,
|
|
59
|
+
`SELECT COUNT(*) as n FROM cards c
|
|
60
|
+
JOIN tokens t ON t.id = c.token_id
|
|
61
|
+
WHERE c.user_id = ? AND t.domain = ? AND c.reps >= 3 AND c.stability >= 21`,
|
|
62
|
+
userId,
|
|
63
|
+
d.domain
|
|
64
|
+
).n;
|
|
65
|
+
const avgStab = q(
|
|
66
|
+
db,
|
|
67
|
+
`SELECT AVG(c.stability) as v FROM cards c
|
|
68
|
+
JOIN tokens t ON t.id = c.token_id
|
|
69
|
+
WHERE c.user_id = ? AND t.domain = ? AND c.reps > 0`,
|
|
70
|
+
userId,
|
|
71
|
+
d.domain
|
|
72
|
+
).v ?? 0;
|
|
73
|
+
const reviews = q(
|
|
74
|
+
db,
|
|
75
|
+
`SELECT COUNT(*) as total,
|
|
76
|
+
SUM(CASE WHEN rating >= 2 THEN 1 ELSE 0 END) as passed
|
|
77
|
+
FROM review_logs
|
|
78
|
+
WHERE user_id = ? AND token_id IN (SELECT id FROM tokens WHERE domain = ?)`,
|
|
79
|
+
userId,
|
|
80
|
+
d.domain
|
|
81
|
+
);
|
|
82
|
+
const retentionRate = reviews.total > 0 ? reviews.passed / reviews.total : 0;
|
|
83
|
+
let suggestedMode;
|
|
84
|
+
if (retentionRate > 0.9 && avgStab > 30) {
|
|
85
|
+
suggestedMode = "autonomy";
|
|
86
|
+
} else if (retentionRate > 0.7 && avgStab > 7) {
|
|
87
|
+
suggestedMode = "copilot";
|
|
88
|
+
} else {
|
|
89
|
+
suggestedMode = "shadowing";
|
|
90
|
+
}
|
|
91
|
+
return {
|
|
92
|
+
domain: d.domain,
|
|
93
|
+
totalCards: total,
|
|
94
|
+
matureCards: mature,
|
|
95
|
+
avgStability: Math.round(avgStab * 100) / 100,
|
|
96
|
+
retentionRate: Math.round(retentionRate * 1e3) / 1e3,
|
|
97
|
+
suggestedMode
|
|
98
|
+
};
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// src/kernel/credentials.ts
|
|
103
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
4
104
|
import { homedir } from "os";
|
|
5
105
|
import { dirname, join } from "path";
|
|
106
|
+
var DEFAULT_CREDENTIALS_PATH = join(homedir(), ".zam", "credentials.json");
|
|
107
|
+
function loadCredentials(path) {
|
|
108
|
+
const p = path ?? DEFAULT_CREDENTIALS_PATH;
|
|
109
|
+
if (!existsSync(p)) return {};
|
|
110
|
+
try {
|
|
111
|
+
return JSON.parse(readFileSync(p, "utf-8"));
|
|
112
|
+
} catch {
|
|
113
|
+
return {};
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
function saveCredentials(creds, path) {
|
|
117
|
+
const p = path ?? DEFAULT_CREDENTIALS_PATH;
|
|
118
|
+
const dir = dirname(p);
|
|
119
|
+
if (!existsSync(dir)) {
|
|
120
|
+
mkdirSync(dir, { recursive: true });
|
|
121
|
+
}
|
|
122
|
+
writeFileSync(p, `${JSON.stringify(creds, null, 2)}
|
|
123
|
+
`, "utf-8");
|
|
124
|
+
}
|
|
125
|
+
function getTursoCredentials(path) {
|
|
126
|
+
const creds = loadCredentials(path);
|
|
127
|
+
if (creds.turso?.url && creds.turso?.token) {
|
|
128
|
+
return { url: creds.turso.url, token: creds.turso.token };
|
|
129
|
+
}
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
function setTursoCredentials(url, token, path) {
|
|
133
|
+
const creds = loadCredentials(path);
|
|
134
|
+
creds.turso = { url, token };
|
|
135
|
+
saveCredentials(creds, path);
|
|
136
|
+
}
|
|
137
|
+
function clearTursoCredentials(path) {
|
|
138
|
+
const creds = loadCredentials(path);
|
|
139
|
+
delete creds.turso;
|
|
140
|
+
saveCredentials(creds, path);
|
|
141
|
+
}
|
|
142
|
+
function getADOCredentials(path) {
|
|
143
|
+
const creds = loadCredentials(path);
|
|
144
|
+
if (creds.ado?.org_url && creds.ado?.project && creds.ado?.pat) {
|
|
145
|
+
return {
|
|
146
|
+
org_url: creds.ado.org_url,
|
|
147
|
+
project: creds.ado.project,
|
|
148
|
+
pat: creds.ado.pat
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
function setADOCredentials(orgUrl, project, pat, path) {
|
|
154
|
+
const creds = loadCredentials(path);
|
|
155
|
+
creds.ado = { org_url: orgUrl, project, pat };
|
|
156
|
+
saveCredentials(creds, path);
|
|
157
|
+
}
|
|
158
|
+
function clearADOCredentials(path) {
|
|
159
|
+
const creds = loadCredentials(path);
|
|
160
|
+
delete creds.ado;
|
|
161
|
+
saveCredentials(creds, path);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// src/kernel/connectors/azure-devops.ts
|
|
165
|
+
function loadADOConfig() {
|
|
166
|
+
const creds = getADOCredentials();
|
|
167
|
+
if (!creds) return null;
|
|
168
|
+
return {
|
|
169
|
+
orgUrl: creds.org_url.replace(/\/+$/, ""),
|
|
170
|
+
project: creds.project,
|
|
171
|
+
pat: creds.pat
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
function authHeader(pat) {
|
|
175
|
+
return `Basic ${Buffer.from(`:${pat}`).toString("base64")}`;
|
|
176
|
+
}
|
|
177
|
+
async function fetchActiveWorkItems(config) {
|
|
178
|
+
const { orgUrl, project, pat } = config;
|
|
179
|
+
const wiqlUrl = `${orgUrl}/${project}/_apis/wit/wiql?api-version=7.1`;
|
|
180
|
+
const wiqlBody = {
|
|
181
|
+
query: `SELECT [System.Id] FROM WorkItems WHERE [System.AssignedTo] = @me AND [System.State] NOT IN ('Closed', 'Completed', 'Done', 'Removed') ORDER BY [Microsoft.VSTS.Common.Priority] ASC, [System.ChangedDate] DESC`
|
|
182
|
+
};
|
|
183
|
+
const wiqlRes = await fetch(wiqlUrl, {
|
|
184
|
+
method: "POST",
|
|
185
|
+
headers: {
|
|
186
|
+
Authorization: authHeader(pat),
|
|
187
|
+
"Content-Type": "application/json"
|
|
188
|
+
},
|
|
189
|
+
body: JSON.stringify(wiqlBody)
|
|
190
|
+
});
|
|
191
|
+
if (!wiqlRes.ok) {
|
|
192
|
+
const text = await wiqlRes.text();
|
|
193
|
+
throw new Error(`ADO WIQL query failed (${wiqlRes.status}): ${text}`);
|
|
194
|
+
}
|
|
195
|
+
const wiqlData = await wiqlRes.json();
|
|
196
|
+
const ids = wiqlData.workItems.map((wi) => wi.id);
|
|
197
|
+
if (ids.length === 0) return [];
|
|
198
|
+
const batchIds = ids.slice(0, 200);
|
|
199
|
+
const fields = "System.Id,System.Title,System.State,System.WorkItemType,System.AssignedTo";
|
|
200
|
+
const detailUrl = `${orgUrl}/${project}/_apis/wit/workitems?ids=${batchIds.join(",")}&fields=${fields}&api-version=7.1`;
|
|
201
|
+
const detailRes = await fetch(detailUrl, {
|
|
202
|
+
headers: { Authorization: authHeader(pat) }
|
|
203
|
+
});
|
|
204
|
+
if (!detailRes.ok) {
|
|
205
|
+
const text = await detailRes.text();
|
|
206
|
+
throw new Error(
|
|
207
|
+
`ADO work items fetch failed (${detailRes.status}): ${text}`
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
const detailData = await detailRes.json();
|
|
211
|
+
return detailData.value.map((wi) => ({
|
|
212
|
+
id: wi.id,
|
|
213
|
+
title: wi.fields["System.Title"],
|
|
214
|
+
state: wi.fields["System.State"],
|
|
215
|
+
type: wi.fields["System.WorkItemType"],
|
|
216
|
+
assignedTo: wi.fields["System.AssignedTo"]?.displayName ?? ""
|
|
217
|
+
}));
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// src/kernel/db/connection.ts
|
|
221
|
+
import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, rmSync } from "fs";
|
|
222
|
+
import { homedir as homedir2 } from "os";
|
|
223
|
+
import { dirname as dirname2, join as join2 } from "path";
|
|
224
|
+
import Database from "libsql";
|
|
6
225
|
|
|
7
226
|
// src/kernel/db/schema.ts
|
|
8
227
|
var SCHEMA = `
|
|
9
|
-
--
|
|
10
|
-
|
|
11
|
-
PRAGMA foreign_keys = ON;
|
|
228
|
+
-- PRAGMAs (WAL, foreign_keys) are set programmatically in connection.ts,
|
|
229
|
+
-- not here, because libsql embedded replicas manage their own WAL.
|
|
12
230
|
|
|
13
231
|
-- Knowledge tokens: atomic concepts/facts with Bloom levels
|
|
14
232
|
CREATE TABLE IF NOT EXISTS tokens (
|
|
@@ -19,9 +237,11 @@ CREATE TABLE IF NOT EXISTS tokens (
|
|
|
19
237
|
bloom_level INTEGER NOT NULL DEFAULT 1 CHECK (bloom_level BETWEEN 1 AND 5),
|
|
20
238
|
context TEXT NOT NULL DEFAULT '',
|
|
21
239
|
symbiosis_mode TEXT CHECK (symbiosis_mode IN ('shadowing', 'copilot', 'autonomy')),
|
|
240
|
+
source_link TEXT,
|
|
22
241
|
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
23
242
|
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
24
|
-
deprecated_at TEXT
|
|
243
|
+
deprecated_at TEXT,
|
|
244
|
+
question TEXT
|
|
25
245
|
);
|
|
26
246
|
|
|
27
247
|
-- Prerequisite dependency graph: "to learn A, first know B"
|
|
@@ -115,52 +335,90 @@ CREATE INDEX IF NOT EXISTS idx_session_steps_session ON session_steps(session_id
|
|
|
115
335
|
`;
|
|
116
336
|
|
|
117
337
|
// src/kernel/db/connection.ts
|
|
118
|
-
var DEFAULT_DB_DIR =
|
|
119
|
-
var DEFAULT_DB_PATH =
|
|
338
|
+
var DEFAULT_DB_DIR = join2(homedir2(), ".zam");
|
|
339
|
+
var DEFAULT_DB_PATH = join2(DEFAULT_DB_DIR, "zam.db");
|
|
340
|
+
function isRemoteDatabasePath(dbPath) {
|
|
341
|
+
return /^(libsql|https?):\/\//i.test(dbPath);
|
|
342
|
+
}
|
|
120
343
|
function openDatabase(options = {}) {
|
|
121
|
-
const
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
344
|
+
const configuredCloud = options.useConfiguredCloud !== false && !options.dbPath && !options.syncUrl ? getTursoCredentials() : null;
|
|
345
|
+
let requiresTurso = false;
|
|
346
|
+
try {
|
|
347
|
+
const configPath = join2(process.cwd(), ".zam", "config.yaml");
|
|
348
|
+
if (existsSync2(configPath)) {
|
|
349
|
+
const configText = readFileSync2(configPath, "utf-8");
|
|
350
|
+
if (/[\s\S]*turso:[\s\S]*url:/m.test(configText)) {
|
|
351
|
+
requiresTurso = true;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
} catch (_e) {
|
|
355
|
+
}
|
|
356
|
+
if (requiresTurso && !configuredCloud && options.useConfiguredCloud !== false && !options.dbPath && !options.syncUrl) {
|
|
357
|
+
throw new Error(
|
|
358
|
+
"Turso cloud database is configured in .zam/config.yaml but missing local credentials. Run: zam connector setup turso"
|
|
359
|
+
);
|
|
360
|
+
}
|
|
361
|
+
const dbPath = configuredCloud?.url ?? options.dbPath ?? DEFAULT_DB_PATH;
|
|
362
|
+
const isRemote = isRemoteDatabasePath(dbPath);
|
|
363
|
+
const isEmbeddedReplica = Boolean(options.syncUrl);
|
|
364
|
+
if (options.initialize && !isRemote) {
|
|
365
|
+
const dir = dirname2(dbPath);
|
|
366
|
+
if (!existsSync2(dir)) {
|
|
367
|
+
mkdirSync2(dir, { recursive: true });
|
|
126
368
|
}
|
|
127
369
|
}
|
|
128
370
|
const dbOpts = {};
|
|
129
371
|
if (options.syncUrl) {
|
|
130
372
|
dbOpts.syncUrl = options.syncUrl;
|
|
131
373
|
const metaPath = `${dbPath}.meta`;
|
|
132
|
-
|
|
374
|
+
const infoPath = `${dbPath}-info`;
|
|
375
|
+
if (existsSync2(dbPath) && !existsSync2(metaPath) && !existsSync2(infoPath)) {
|
|
133
376
|
for (const suffix of ["", "-wal", "-shm"]) {
|
|
134
377
|
const f = `${dbPath}${suffix}`;
|
|
135
|
-
if (
|
|
378
|
+
if (existsSync2(f)) rmSync(f, { force: true });
|
|
136
379
|
}
|
|
380
|
+
} else if (!existsSync2(dbPath) && (existsSync2(metaPath) || existsSync2(infoPath))) {
|
|
381
|
+
if (existsSync2(metaPath)) rmSync(metaPath);
|
|
382
|
+
if (existsSync2(infoPath)) rmSync(infoPath);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
const authToken = configuredCloud?.token ?? options.authToken;
|
|
386
|
+
if (authToken) {
|
|
387
|
+
dbOpts.authToken = authToken;
|
|
388
|
+
}
|
|
389
|
+
let db;
|
|
390
|
+
try {
|
|
391
|
+
db = new Database(dbPath, dbOpts);
|
|
392
|
+
} catch (err) {
|
|
393
|
+
const msg = err.message;
|
|
394
|
+
if (msg.includes("InvalidLocalState") && options.syncUrl) {
|
|
395
|
+
const metaPath = `${dbPath}.meta`;
|
|
396
|
+
const infoPath = `${dbPath}-info`;
|
|
397
|
+
if (existsSync2(metaPath)) rmSync(metaPath);
|
|
398
|
+
if (existsSync2(infoPath)) rmSync(infoPath);
|
|
399
|
+
db = new Database(dbPath, dbOpts);
|
|
400
|
+
} else {
|
|
401
|
+
throw err;
|
|
137
402
|
}
|
|
138
403
|
}
|
|
139
|
-
if (
|
|
140
|
-
|
|
404
|
+
if (!isRemote && !isEmbeddedReplica) {
|
|
405
|
+
db.pragma("journal_mode = WAL");
|
|
141
406
|
}
|
|
142
|
-
const db = new Database(dbPath, dbOpts);
|
|
143
|
-
db.pragma("journal_mode = WAL");
|
|
144
407
|
db.pragma("foreign_keys = ON");
|
|
145
|
-
|
|
408
|
+
if (!isRemote) {
|
|
409
|
+
db.pragma("busy_timeout = 5000");
|
|
410
|
+
}
|
|
411
|
+
if (isEmbeddedReplica) {
|
|
412
|
+
db.sync();
|
|
413
|
+
}
|
|
146
414
|
if (options.initialize) {
|
|
147
415
|
db.exec(SCHEMA);
|
|
148
416
|
}
|
|
149
417
|
runMigrations(db);
|
|
150
|
-
if (options.syncUrl) {
|
|
151
|
-
db.sync();
|
|
152
|
-
}
|
|
153
418
|
return db;
|
|
154
419
|
}
|
|
155
420
|
function openDatabaseWithSync(options = {}) {
|
|
156
|
-
|
|
157
|
-
const syncUrl = db.prepare("SELECT value FROM user_config WHERE key = ?").get("turso.url");
|
|
158
|
-
const authToken = db.prepare("SELECT value FROM user_config WHERE key = ?").get("turso.token");
|
|
159
|
-
if (!syncUrl || !authToken) return db;
|
|
160
|
-
db.pragma("wal_checkpoint(TRUNCATE)");
|
|
161
|
-
db.pragma("journal_mode = DELETE");
|
|
162
|
-
db.close();
|
|
163
|
-
return openDatabase({ ...options, syncUrl: syncUrl.value, authToken: authToken.value });
|
|
421
|
+
return openDatabase(options);
|
|
164
422
|
}
|
|
165
423
|
function getDefaultDbPath() {
|
|
166
424
|
return DEFAULT_DB_PATH;
|
|
@@ -176,6 +434,12 @@ function runMigrations(db) {
|
|
|
176
434
|
if (tokenCols.length > 0 && !tokenCols.some((c) => c.name === "deprecated_at")) {
|
|
177
435
|
db.exec(`ALTER TABLE tokens ADD COLUMN deprecated_at TEXT`);
|
|
178
436
|
}
|
|
437
|
+
if (tokenCols.length > 0 && !tokenCols.some((c) => c.name === "source_link")) {
|
|
438
|
+
db.exec(`ALTER TABLE tokens ADD COLUMN source_link TEXT`);
|
|
439
|
+
}
|
|
440
|
+
if (tokenCols.length > 0 && !tokenCols.some((c) => c.name === "question")) {
|
|
441
|
+
db.exec(`ALTER TABLE tokens ADD COLUMN question TEXT`);
|
|
442
|
+
}
|
|
179
443
|
db.exec(`
|
|
180
444
|
CREATE TABLE IF NOT EXISTS agent_skills (
|
|
181
445
|
id TEXT PRIMARY KEY,
|
|
@@ -190,111 +454,232 @@ function runMigrations(db) {
|
|
|
190
454
|
`);
|
|
191
455
|
}
|
|
192
456
|
|
|
193
|
-
// src/kernel/
|
|
194
|
-
import {
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
now,
|
|
214
|
-
now
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
}
|
|
218
|
-
function getTokenBySlug(db, slug) {
|
|
219
|
-
return db.prepare("SELECT * FROM tokens WHERE slug = ?").get(slug);
|
|
220
|
-
}
|
|
221
|
-
function getTokenById(db, id) {
|
|
222
|
-
return db.prepare("SELECT * FROM tokens WHERE id = ?").get(id);
|
|
457
|
+
// src/kernel/goals/engine.ts
|
|
458
|
+
import { existsSync as existsSync3, readdirSync, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
|
|
459
|
+
import { basename, join as join3 } from "path";
|
|
460
|
+
|
|
461
|
+
// src/kernel/goals/parser.ts
|
|
462
|
+
function parseGoalFile(content, slug, filePath) {
|
|
463
|
+
const { frontmatter, body } = splitFrontmatter(content);
|
|
464
|
+
const validStatuses = [
|
|
465
|
+
"active",
|
|
466
|
+
"completed",
|
|
467
|
+
"paused",
|
|
468
|
+
"abandoned"
|
|
469
|
+
];
|
|
470
|
+
const status = validStatuses.includes(frontmatter.status) ? frontmatter.status : "active";
|
|
471
|
+
const now = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
472
|
+
return {
|
|
473
|
+
slug,
|
|
474
|
+
title: frontmatter.title || slug,
|
|
475
|
+
status,
|
|
476
|
+
parent: frontmatter.parent || null,
|
|
477
|
+
created: frontmatter.created || now,
|
|
478
|
+
updated: frontmatter.updated || now,
|
|
479
|
+
body,
|
|
480
|
+
filePath
|
|
481
|
+
};
|
|
223
482
|
}
|
|
224
|
-
function
|
|
225
|
-
const
|
|
226
|
-
if (
|
|
227
|
-
|
|
483
|
+
function serializeGoal(goal) {
|
|
484
|
+
const lines = ["---", `title: ${goal.title}`, `status: ${goal.status}`];
|
|
485
|
+
if (goal.parent) {
|
|
486
|
+
lines.push(`parent: ${goal.parent}`);
|
|
228
487
|
}
|
|
229
|
-
|
|
230
|
-
|
|
488
|
+
lines.push(`created: ${goal.created}`);
|
|
489
|
+
lines.push(`updated: ${goal.updated}`);
|
|
490
|
+
lines.push("---");
|
|
491
|
+
lines.push("");
|
|
492
|
+
if (goal.body.trim()) {
|
|
493
|
+
lines.push(goal.body.trim());
|
|
494
|
+
lines.push("");
|
|
231
495
|
}
|
|
232
|
-
|
|
233
|
-
db.prepare("UPDATE tokens SET deprecated_at = ?, updated_at = ? WHERE slug = ?").run(
|
|
234
|
-
now,
|
|
235
|
-
now,
|
|
236
|
-
slug
|
|
237
|
-
);
|
|
238
|
-
return getTokenBySlug(db, slug);
|
|
496
|
+
return lines.join("\n");
|
|
239
497
|
}
|
|
240
|
-
function
|
|
241
|
-
const
|
|
242
|
-
const
|
|
243
|
-
|
|
244
|
-
)
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
}
|
|
253
|
-
if (t.concept.toLowerCase().includes(normalised.slice(0, 25))) {
|
|
254
|
-
score += 3;
|
|
255
|
-
}
|
|
256
|
-
if (score > 0) {
|
|
257
|
-
scored.push({ score, ...t });
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
scored.sort((a, b) => b.score - a.score);
|
|
261
|
-
return scored;
|
|
498
|
+
function extractTasks(body) {
|
|
499
|
+
const tasks = [];
|
|
500
|
+
const taskRegex = /^[-*]\s+\[([ xX])\]\s+(.+)$/gm;
|
|
501
|
+
let match = taskRegex.exec(body);
|
|
502
|
+
while (match !== null) {
|
|
503
|
+
tasks.push({
|
|
504
|
+
done: match[1] !== " ",
|
|
505
|
+
text: match[2].trim()
|
|
506
|
+
});
|
|
507
|
+
match = taskRegex.exec(body);
|
|
508
|
+
}
|
|
509
|
+
return tasks;
|
|
262
510
|
}
|
|
263
|
-
function
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
511
|
+
function extractTokenRefs(body) {
|
|
512
|
+
const tokensSection = body.match(/## Tokens\n([\s\S]*?)(?=\n## |\n*$)/);
|
|
513
|
+
if (!tokensSection) return [];
|
|
514
|
+
const refs = [];
|
|
515
|
+
const lines = tokensSection[1].split("\n");
|
|
516
|
+
for (const line of lines) {
|
|
517
|
+
const match = line.match(/^[-*]\s+(\S+)/);
|
|
518
|
+
if (match) {
|
|
519
|
+
refs.push(match[1]);
|
|
520
|
+
}
|
|
268
521
|
}
|
|
269
|
-
return
|
|
270
|
-
|
|
271
|
-
|
|
522
|
+
return refs;
|
|
523
|
+
}
|
|
524
|
+
function splitFrontmatter(content) {
|
|
525
|
+
const trimmed = content.trim();
|
|
526
|
+
if (!trimmed.startsWith("---")) {
|
|
527
|
+
return { frontmatter: {}, body: trimmed };
|
|
528
|
+
}
|
|
529
|
+
const endIndex = trimmed.indexOf("---", 3);
|
|
530
|
+
if (endIndex === -1) {
|
|
531
|
+
return { frontmatter: {}, body: trimmed };
|
|
532
|
+
}
|
|
533
|
+
const fmBlock = trimmed.slice(3, endIndex).trim();
|
|
534
|
+
const body = trimmed.slice(endIndex + 3).trim();
|
|
535
|
+
const frontmatter = {};
|
|
536
|
+
for (const line of fmBlock.split("\n")) {
|
|
537
|
+
const colonIndex = line.indexOf(":");
|
|
538
|
+
if (colonIndex === -1) continue;
|
|
539
|
+
const key = line.slice(0, colonIndex).trim();
|
|
540
|
+
const value = line.slice(colonIndex + 1).trim();
|
|
541
|
+
if (key && value) {
|
|
542
|
+
frontmatter[key] = value;
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
return { frontmatter, body };
|
|
272
546
|
}
|
|
273
547
|
|
|
274
|
-
// src/kernel/
|
|
275
|
-
function
|
|
276
|
-
if (
|
|
277
|
-
|
|
548
|
+
// src/kernel/goals/engine.ts
|
|
549
|
+
function listGoals(goalsDir) {
|
|
550
|
+
if (!existsSync3(goalsDir)) return [];
|
|
551
|
+
const files = readdirSync(goalsDir).filter(
|
|
552
|
+
(f) => f.endsWith(".md") && f !== "README.md"
|
|
553
|
+
);
|
|
554
|
+
const summaries = [];
|
|
555
|
+
for (const file of files) {
|
|
556
|
+
const filePath = join3(goalsDir, file);
|
|
557
|
+
const content = readFileSync3(filePath, "utf-8");
|
|
558
|
+
const slug = basename(file, ".md");
|
|
559
|
+
const goal = parseGoalFile(content, slug, filePath);
|
|
560
|
+
const tasks = extractTasks(goal.body);
|
|
561
|
+
const tokens = extractTokenRefs(goal.body);
|
|
562
|
+
summaries.push({
|
|
563
|
+
slug: goal.slug,
|
|
564
|
+
title: goal.title,
|
|
565
|
+
status: goal.status,
|
|
566
|
+
parent: goal.parent,
|
|
567
|
+
taskCount: tasks.length,
|
|
568
|
+
tasksDone: tasks.filter((t2) => t2.done).length,
|
|
569
|
+
tokenCount: tokens.length
|
|
570
|
+
});
|
|
571
|
+
}
|
|
572
|
+
const statusOrder = {
|
|
573
|
+
active: 0,
|
|
574
|
+
paused: 1,
|
|
575
|
+
completed: 2,
|
|
576
|
+
abandoned: 3
|
|
577
|
+
};
|
|
578
|
+
summaries.sort((a, b) => {
|
|
579
|
+
const statusDiff = statusOrder[a.status] - statusOrder[b.status];
|
|
580
|
+
if (statusDiff !== 0) return statusDiff;
|
|
581
|
+
return a.title.localeCompare(b.title);
|
|
582
|
+
});
|
|
583
|
+
return summaries;
|
|
584
|
+
}
|
|
585
|
+
function getGoal(goalsDir, slug) {
|
|
586
|
+
const filePath = join3(goalsDir, `${slug}.md`);
|
|
587
|
+
if (!existsSync3(filePath)) return void 0;
|
|
588
|
+
const content = readFileSync3(filePath, "utf-8");
|
|
589
|
+
return parseGoalFile(content, slug, filePath);
|
|
590
|
+
}
|
|
591
|
+
function createGoal(goalsDir, input) {
|
|
592
|
+
const filePath = join3(goalsDir, `${input.slug}.md`);
|
|
593
|
+
if (existsSync3(filePath)) {
|
|
594
|
+
throw new Error(`Goal already exists: ${input.slug}`);
|
|
595
|
+
}
|
|
596
|
+
const now = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
597
|
+
const goal = {
|
|
598
|
+
slug: input.slug,
|
|
599
|
+
title: input.title,
|
|
600
|
+
status: input.status ?? "active",
|
|
601
|
+
parent: input.parent ?? null,
|
|
602
|
+
created: now,
|
|
603
|
+
updated: now,
|
|
604
|
+
body: input.description ? `## Description
|
|
605
|
+
${input.description}
|
|
606
|
+
|
|
607
|
+
## Tasks
|
|
608
|
+
|
|
609
|
+
## Tokens` : "## Description\n\n## Tasks\n\n## Tokens",
|
|
610
|
+
filePath
|
|
611
|
+
};
|
|
612
|
+
writeFileSync2(filePath, serializeGoal(goal), "utf-8");
|
|
613
|
+
return goal;
|
|
614
|
+
}
|
|
615
|
+
function updateGoalStatus(goalsDir, slug, status) {
|
|
616
|
+
const goal = getGoal(goalsDir, slug);
|
|
617
|
+
if (!goal) throw new Error(`Goal not found: ${slug}`);
|
|
618
|
+
goal.status = status;
|
|
619
|
+
goal.updated = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
620
|
+
writeFileSync2(goal.filePath, serializeGoal(goal), "utf-8");
|
|
621
|
+
return goal;
|
|
622
|
+
}
|
|
623
|
+
function getGoalTree(goalsDir) {
|
|
624
|
+
const all = listGoals(goalsDir);
|
|
625
|
+
const bySlug = new Map(all.map((g) => [g.slug, g]));
|
|
626
|
+
const roots = [];
|
|
627
|
+
const children = /* @__PURE__ */ new Map();
|
|
628
|
+
for (const g of all) {
|
|
629
|
+
if (g.parent && bySlug.has(g.parent)) {
|
|
630
|
+
const list = children.get(g.parent) ?? [];
|
|
631
|
+
list.push(g);
|
|
632
|
+
children.set(g.parent, list);
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
for (const g of all) {
|
|
636
|
+
if (!g.parent || !bySlug.has(g.parent)) {
|
|
637
|
+
roots.push({ ...g, children: children.get(g.slug) ?? [] });
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
return roots;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
// src/kernel/models/agent-skill.ts
|
|
644
|
+
import { ulid } from "ulid";
|
|
645
|
+
function parseRow(row) {
|
|
646
|
+
return {
|
|
647
|
+
...row,
|
|
648
|
+
steps: JSON.parse(row.steps),
|
|
649
|
+
token_slugs: JSON.parse(row.token_slugs)
|
|
650
|
+
};
|
|
651
|
+
}
|
|
652
|
+
function createAgentSkill(db, input) {
|
|
653
|
+
const existing = db.prepare("SELECT * FROM agent_skills WHERE slug = ?").get(input.slug);
|
|
654
|
+
if (existing) {
|
|
655
|
+
throw new Error(`Agent skill already exists: ${input.slug}`);
|
|
278
656
|
}
|
|
657
|
+
const id = ulid();
|
|
658
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
279
659
|
db.prepare(
|
|
280
|
-
|
|
281
|
-
|
|
660
|
+
`INSERT INTO agent_skills (id, slug, description, steps, token_slugs, source, created_at, updated_at)
|
|
661
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
|
|
662
|
+
).run(
|
|
663
|
+
id,
|
|
664
|
+
input.slug,
|
|
665
|
+
input.description,
|
|
666
|
+
JSON.stringify(input.steps),
|
|
667
|
+
JSON.stringify(input.token_slugs ?? []),
|
|
668
|
+
input.source ?? "learned",
|
|
669
|
+
now,
|
|
670
|
+
now
|
|
671
|
+
);
|
|
672
|
+
return parseRow(
|
|
673
|
+
db.prepare("SELECT * FROM agent_skills WHERE id = ?").get(id)
|
|
674
|
+
);
|
|
282
675
|
}
|
|
283
|
-
function
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
FROM prerequisites p
|
|
287
|
-
JOIN tokens t ON t.id = p.requires_id
|
|
288
|
-
WHERE p.token_id = ?`
|
|
289
|
-
).all(tokenId);
|
|
676
|
+
function getAgentSkill(db, slug) {
|
|
677
|
+
const row = db.prepare("SELECT * FROM agent_skills WHERE slug = ?").get(slug);
|
|
678
|
+
return row ? parseRow(row) : void 0;
|
|
290
679
|
}
|
|
291
|
-
function
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
FROM prerequisites p
|
|
295
|
-
JOIN tokens t ON t.id = p.token_id
|
|
296
|
-
WHERE p.requires_id = ?`
|
|
297
|
-
).all(tokenId);
|
|
680
|
+
function listAgentSkills(db) {
|
|
681
|
+
const rows = db.prepare("SELECT * FROM agent_skills ORDER BY created_at ASC").all();
|
|
682
|
+
return rows.map(parseRow);
|
|
298
683
|
}
|
|
299
684
|
|
|
300
685
|
// src/kernel/models/card.ts
|
|
@@ -313,6 +698,9 @@ function ensureCard(db, tokenId, userId) {
|
|
|
313
698
|
function getCard(db, tokenId, userId) {
|
|
314
699
|
return db.prepare("SELECT * FROM cards WHERE token_id = ? AND user_id = ?").get(tokenId, userId);
|
|
315
700
|
}
|
|
701
|
+
function getCardById(db, cardId) {
|
|
702
|
+
return db.prepare("SELECT * FROM cards WHERE id = ?").get(cardId);
|
|
703
|
+
}
|
|
316
704
|
function updateCard(db, cardId, updates) {
|
|
317
705
|
const fields = [];
|
|
318
706
|
const values = [];
|
|
@@ -366,6 +754,23 @@ function updateCard(db, cardId, updates) {
|
|
|
366
754
|
}
|
|
367
755
|
return db.prepare("SELECT * FROM cards WHERE id = ?").get(cardId);
|
|
368
756
|
}
|
|
757
|
+
function getCardDeletionImpact(db, tokenId, userId) {
|
|
758
|
+
const card = getCard(db, tokenId, userId);
|
|
759
|
+
if (!card) {
|
|
760
|
+
throw new Error(`Card not found for token ${tokenId} and user ${userId}`);
|
|
761
|
+
}
|
|
762
|
+
const reviewLogs = db.prepare("SELECT COUNT(*) AS n FROM review_logs WHERE card_id = ?").get(card.id);
|
|
763
|
+
return { review_logs: reviewLogs.n };
|
|
764
|
+
}
|
|
765
|
+
function deleteCardForUser(db, tokenId, userId) {
|
|
766
|
+
const card = getCard(db, tokenId, userId);
|
|
767
|
+
if (!card) {
|
|
768
|
+
throw new Error(`Card not found for token ${tokenId} and user ${userId}`);
|
|
769
|
+
}
|
|
770
|
+
const impact = getCardDeletionImpact(db, tokenId, userId);
|
|
771
|
+
db.prepare("DELETE FROM cards WHERE id = ?").run(card.id);
|
|
772
|
+
return { card, impact };
|
|
773
|
+
}
|
|
369
774
|
function getDueCards(db, userId, now) {
|
|
370
775
|
const cutoff = now ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
371
776
|
return db.prepare(
|
|
@@ -386,6 +791,32 @@ function getBlockedCards(db, userId) {
|
|
|
386
791
|
).all(userId);
|
|
387
792
|
}
|
|
388
793
|
|
|
794
|
+
// src/kernel/models/prerequisite.ts
|
|
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
|
+
|
|
389
820
|
// src/kernel/models/review.ts
|
|
390
821
|
import { ulid as ulid3 } from "ulid";
|
|
391
822
|
function logReview(db, input) {
|
|
@@ -455,12 +886,17 @@ function endSession(db, sessionId) {
|
|
|
455
886
|
throw new Error(`Session already completed: ${sessionId}`);
|
|
456
887
|
}
|
|
457
888
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
458
|
-
db.prepare("UPDATE sessions SET completed_at = ? WHERE id = ?").run(
|
|
889
|
+
db.prepare("UPDATE sessions SET completed_at = ? WHERE id = ?").run(
|
|
890
|
+
now,
|
|
891
|
+
sessionId
|
|
892
|
+
);
|
|
459
893
|
return db.prepare("SELECT * FROM sessions WHERE id = ?").get(sessionId);
|
|
460
894
|
}
|
|
461
895
|
function logStep(db, input) {
|
|
462
896
|
if (input.done_by !== "user" && input.done_by !== "agent") {
|
|
463
|
-
throw new Error(
|
|
897
|
+
throw new Error(
|
|
898
|
+
`done_by must be 'user' or 'agent', got '${input.done_by}'`
|
|
899
|
+
);
|
|
464
900
|
}
|
|
465
901
|
if (input.rating != null && (input.rating < 1 || input.rating > 4)) {
|
|
466
902
|
throw new Error(`Rating must be between 1 and 4, got ${input.rating}`);
|
|
@@ -500,58 +936,16 @@ function getSessionSummary(db, sessionId) {
|
|
|
500
936
|
return { session, steps };
|
|
501
937
|
}
|
|
502
938
|
|
|
503
|
-
// src/kernel/models/
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
return
|
|
507
|
-
...row,
|
|
508
|
-
steps: JSON.parse(row.steps),
|
|
509
|
-
token_slugs: JSON.parse(row.token_slugs)
|
|
510
|
-
};
|
|
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;
|
|
511
943
|
}
|
|
512
|
-
function
|
|
513
|
-
const
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
const id = ulid5();
|
|
518
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
519
|
-
db.prepare(
|
|
520
|
-
`INSERT INTO agent_skills (id, slug, description, steps, token_slugs, source, created_at, updated_at)
|
|
521
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
|
|
522
|
-
).run(
|
|
523
|
-
id,
|
|
524
|
-
input.slug,
|
|
525
|
-
input.description,
|
|
526
|
-
JSON.stringify(input.steps),
|
|
527
|
-
JSON.stringify(input.token_slugs ?? []),
|
|
528
|
-
input.source ?? "learned",
|
|
529
|
-
now,
|
|
530
|
-
now
|
|
531
|
-
);
|
|
532
|
-
return parseRow(
|
|
533
|
-
db.prepare("SELECT * FROM agent_skills WHERE id = ?").get(id)
|
|
534
|
-
);
|
|
535
|
-
}
|
|
536
|
-
function getAgentSkill(db, slug) {
|
|
537
|
-
const row = db.prepare("SELECT * FROM agent_skills WHERE slug = ?").get(slug);
|
|
538
|
-
return row ? parseRow(row) : void 0;
|
|
539
|
-
}
|
|
540
|
-
function listAgentSkills(db) {
|
|
541
|
-
const rows = db.prepare("SELECT * FROM agent_skills ORDER BY created_at ASC").all();
|
|
542
|
-
return rows.map(parseRow);
|
|
543
|
-
}
|
|
544
|
-
|
|
545
|
-
// src/kernel/models/settings.ts
|
|
546
|
-
function getSetting(db, key) {
|
|
547
|
-
const row = db.prepare("SELECT value FROM user_config WHERE key = ?").get(key);
|
|
548
|
-
return row?.value;
|
|
549
|
-
}
|
|
550
|
-
function getAllSettings(db) {
|
|
551
|
-
const rows = db.prepare("SELECT key, value FROM user_config ORDER BY key").all();
|
|
552
|
-
const map = {};
|
|
553
|
-
for (const row of rows) {
|
|
554
|
-
map[row.key] = row.value;
|
|
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;
|
|
555
949
|
}
|
|
556
950
|
return map;
|
|
557
951
|
}
|
|
@@ -570,1323 +964,2328 @@ function deleteSetting(db, key) {
|
|
|
570
964
|
return result.changes > 0;
|
|
571
965
|
}
|
|
572
966
|
|
|
573
|
-
// src/kernel/
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
];
|
|
600
|
-
var DEFAULT_REQUEST_RETENTION = 0.9;
|
|
601
|
-
function clamp(value, lo, hi) {
|
|
602
|
-
return Math.min(hi, Math.max(lo, value));
|
|
603
|
-
}
|
|
604
|
-
function daysBetween(a, b) {
|
|
605
|
-
return (b.getTime() - a.getTime()) / (1e3 * 60 * 60 * 24);
|
|
606
|
-
}
|
|
607
|
-
function initialStability(w, rating) {
|
|
608
|
-
return w[rating - 1];
|
|
609
|
-
}
|
|
610
|
-
function initialDifficulty(w, rating) {
|
|
611
|
-
return clamp(w[4] - Math.exp(w[5] * (rating - 1)) + 1, 1, 10);
|
|
612
|
-
}
|
|
613
|
-
function nextDifficulty(w, d, rating) {
|
|
614
|
-
const d0ForGood = initialDifficulty(w, 3);
|
|
615
|
-
const updated = w[7] * d0ForGood + (1 - w[7]) * (d - w[6] * (rating - 3));
|
|
616
|
-
return clamp(updated, 1, 10);
|
|
617
|
-
}
|
|
618
|
-
function retrievability(elapsed, stability) {
|
|
619
|
-
if (stability <= 0) return 0;
|
|
620
|
-
return Math.pow(1 + elapsed / (9 * stability), -1);
|
|
621
|
-
}
|
|
622
|
-
function stabilityAfterSuccess(w, s, d, r, rating) {
|
|
623
|
-
const hardPenalty = rating === 2 ? w[15] : 1;
|
|
624
|
-
const easyBonus = rating === 4 ? w[16] : 1;
|
|
625
|
-
const inner = Math.exp(w[8]) * (11 - d) * Math.pow(s, -w[9]) * (Math.exp(w[10] * (1 - r)) - 1) * hardPenalty * easyBonus;
|
|
626
|
-
return s * (inner + 1);
|
|
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);
|
|
627
993
|
}
|
|
628
|
-
function
|
|
629
|
-
return
|
|
994
|
+
function getTokenBySlug(db, slug) {
|
|
995
|
+
return db.prepare("SELECT * FROM tokens WHERE slug = ?").get(slug);
|
|
630
996
|
}
|
|
631
|
-
function
|
|
632
|
-
|
|
633
|
-
return Math.max(1, Math.round(interval));
|
|
997
|
+
function getTokenById(db, id) {
|
|
998
|
+
return db.prepare("SELECT * FROM tokens WHERE id = ?").get(id);
|
|
634
999
|
}
|
|
635
|
-
function
|
|
636
|
-
const
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
}
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
reps: rating >= 2 ? 1 : 0,
|
|
656
|
-
lapses: rating === 1 ? 1 : 0,
|
|
657
|
-
state: "learning",
|
|
658
|
-
dueAt: dueAt2,
|
|
659
|
-
lastReviewAt: reviewTime
|
|
660
|
-
};
|
|
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);
|
|
1014
|
+
}
|
|
1015
|
+
if (updates.bloom_level !== void 0) {
|
|
1016
|
+
if (updates.bloom_level < 1 || updates.bloom_level > 5) {
|
|
1017
|
+
throw new Error(
|
|
1018
|
+
`bloom_level must be between 1 and 5, got ${updates.bloom_level}`
|
|
1019
|
+
);
|
|
661
1020
|
}
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
newState = "relearning";
|
|
674
|
-
} else {
|
|
675
|
-
newStability = stabilityAfterSuccess(w, card.stability, card.difficulty, r, rating);
|
|
676
|
-
newDifficulty = nextDifficulty(w, card.difficulty, rating);
|
|
677
|
-
newReps = card.reps + 1;
|
|
678
|
-
newLapses = card.lapses;
|
|
679
|
-
newState = "review";
|
|
1021
|
+
fields.push("bloom_level = ?");
|
|
1022
|
+
values.push(updates.bloom_level);
|
|
1023
|
+
}
|
|
1024
|
+
if (updates.context !== void 0) {
|
|
1025
|
+
fields.push("context = ?");
|
|
1026
|
+
values.push(updates.context);
|
|
1027
|
+
}
|
|
1028
|
+
if (updates.symbiosis_mode !== void 0) {
|
|
1029
|
+
const validModes = ["shadowing", "copilot", "autonomy"];
|
|
1030
|
+
if (updates.symbiosis_mode !== null && !validModes.includes(updates.symbiosis_mode)) {
|
|
1031
|
+
throw new Error(`Invalid symbiosis_mode: ${updates.symbiosis_mode}`);
|
|
680
1032
|
}
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
1033
|
+
fields.push("symbiosis_mode = ?");
|
|
1034
|
+
values.push(updates.symbiosis_mode);
|
|
1035
|
+
}
|
|
1036
|
+
if (updates.source_link !== void 0) {
|
|
1037
|
+
fields.push("source_link = ?");
|
|
1038
|
+
values.push(updates.source_link);
|
|
1039
|
+
}
|
|
1040
|
+
if (updates.question !== void 0) {
|
|
1041
|
+
fields.push("question = ?");
|
|
1042
|
+
values.push(updates.question);
|
|
1043
|
+
}
|
|
1044
|
+
if (fields.length === 0) {
|
|
1045
|
+
throw new Error("updateToken called with no fields to update");
|
|
1046
|
+
}
|
|
1047
|
+
fields.push("updated_at = ?");
|
|
1048
|
+
values.push((/* @__PURE__ */ new Date()).toISOString());
|
|
1049
|
+
values.push(slug);
|
|
1050
|
+
db.prepare(`UPDATE tokens SET ${fields.join(", ")} WHERE slug = ?`).run(
|
|
1051
|
+
...values
|
|
1052
|
+
);
|
|
1053
|
+
return getTokenBySlug(db, slug);
|
|
1054
|
+
}
|
|
1055
|
+
function deprecateToken(db, slug) {
|
|
1056
|
+
const token = getTokenBySlug(db, slug);
|
|
1057
|
+
if (!token) {
|
|
1058
|
+
throw new Error(`Token not found: ${slug}`);
|
|
1059
|
+
}
|
|
1060
|
+
if (token.deprecated_at) {
|
|
1061
|
+
throw new Error(`Token already deprecated: ${slug}`);
|
|
1062
|
+
}
|
|
1063
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1064
|
+
db.prepare(
|
|
1065
|
+
"UPDATE tokens SET deprecated_at = ?, updated_at = ? WHERE slug = ?"
|
|
1066
|
+
).run(now, now, slug);
|
|
1067
|
+
return getTokenBySlug(db, slug);
|
|
1068
|
+
}
|
|
1069
|
+
function getTokenDeleteImpact(db, slug) {
|
|
1070
|
+
const token = getTokenBySlug(db, slug);
|
|
1071
|
+
if (!token) {
|
|
1072
|
+
throw new Error(`Token not found: ${slug}`);
|
|
695
1073
|
}
|
|
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(
|
|
1080
|
+
"SELECT COUNT(DISTINCT session_id) AS n FROM session_steps WHERE token_id = ?"
|
|
1081
|
+
).get(token.id);
|
|
1082
|
+
const skillRows = db.prepare("SELECT token_slugs FROM agent_skills").all();
|
|
1083
|
+
const agentSkills = skillRows.filter((row) => {
|
|
1084
|
+
const tokenSlugs = JSON.parse(row.token_slugs);
|
|
1085
|
+
return tokenSlugs.includes(slug);
|
|
1086
|
+
}).length;
|
|
696
1087
|
return {
|
|
697
|
-
|
|
698
|
-
|
|
1088
|
+
cards: cards.n,
|
|
1089
|
+
review_logs: reviewLogs.n,
|
|
1090
|
+
prerequisite_edges_from_token: prereqsFrom.n,
|
|
1091
|
+
prerequisite_edges_to_token: prereqsTo.n,
|
|
1092
|
+
session_steps: sessionSteps.n,
|
|
1093
|
+
sessions_touched: sessionsTouched.n,
|
|
1094
|
+
agent_skills: agentSkills
|
|
699
1095
|
};
|
|
700
1096
|
}
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
function cascadeBlock(db, userId, tokenSlug) {
|
|
704
|
-
const token = getTokenBySlug(db, tokenSlug);
|
|
1097
|
+
function deleteToken(db, slug) {
|
|
1098
|
+
const token = getTokenBySlug(db, slug);
|
|
705
1099
|
if (!token) {
|
|
706
|
-
throw new Error(`
|
|
1100
|
+
throw new Error(`Token not found: ${slug}`);
|
|
707
1101
|
}
|
|
708
|
-
|
|
709
|
-
db.
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
const prereqOfPrereq = db.prepare("SELECT COUNT(*) as n FROM prerequisites WHERE token_id = ?").get(prereq.requires_id);
|
|
718
|
-
if (prereqOfPrereq.n === 0) {
|
|
719
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1102
|
+
const impact = getTokenDeleteImpact(db, slug);
|
|
1103
|
+
db.exec("BEGIN");
|
|
1104
|
+
try {
|
|
1105
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1106
|
+
const skillRows = db.prepare("SELECT id, token_slugs FROM agent_skills").all();
|
|
1107
|
+
for (const row of skillRows) {
|
|
1108
|
+
const tokenSlugs = JSON.parse(row.token_slugs);
|
|
1109
|
+
const filtered = tokenSlugs.filter((tokenSlug) => tokenSlug !== slug);
|
|
1110
|
+
if (filtered.length !== tokenSlugs.length) {
|
|
720
1111
|
db.prepare(
|
|
721
|
-
"UPDATE
|
|
722
|
-
).run(now,
|
|
1112
|
+
"UPDATE agent_skills SET token_slugs = ?, updated_at = ? WHERE id = ?"
|
|
1113
|
+
).run(JSON.stringify(filtered), now, row.id);
|
|
723
1114
|
}
|
|
724
1115
|
}
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
1116
|
+
db.prepare("DELETE FROM tokens WHERE id = ?").run(token.id);
|
|
1117
|
+
db.exec("COMMIT");
|
|
1118
|
+
} catch (err) {
|
|
1119
|
+
db.exec("ROLLBACK");
|
|
1120
|
+
throw err;
|
|
730
1121
|
}
|
|
731
|
-
return {
|
|
732
|
-
blockedSlug: tokenSlug,
|
|
733
|
-
prerequisites: surfaced
|
|
734
|
-
};
|
|
1122
|
+
return { token, impact };
|
|
735
1123
|
}
|
|
736
|
-
function
|
|
737
|
-
const
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
const
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
const
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
"UPDATE cards SET blocked = 0, due_at = ? WHERE token_id = ? AND user_id = ?"
|
|
755
|
-
).run(now, card.token_id, userId);
|
|
756
|
-
unblocked.push({ slug: card.slug, concept: card.concept });
|
|
1124
|
+
function findTokens(db, query) {
|
|
1125
|
+
const normalised = query.toLowerCase();
|
|
1126
|
+
const qTokens = new Set(
|
|
1127
|
+
normalised.split(/[\s,.\-_/\\:;!?()[\]{}]+/).filter((t2) => t2.length > 2)
|
|
1128
|
+
);
|
|
1129
|
+
const tokens = db.prepare("SELECT * FROM tokens WHERE deprecated_at IS NULL").all();
|
|
1130
|
+
const scored = [];
|
|
1131
|
+
for (const t2 of tokens) {
|
|
1132
|
+
const words = `${t2.slug} ${t2.concept} ${t2.domain}`.toLowerCase().split(/[\s,.\-_/\\:;!?()[\]{}]+/).filter(Boolean);
|
|
1133
|
+
let score = 0;
|
|
1134
|
+
for (const w of words) {
|
|
1135
|
+
if (qTokens.has(w)) score++;
|
|
1136
|
+
}
|
|
1137
|
+
if (t2.concept.toLowerCase().includes(normalised.slice(0, 25))) {
|
|
1138
|
+
score += 3;
|
|
1139
|
+
}
|
|
1140
|
+
if (score > 0) {
|
|
1141
|
+
scored.push({ score, ...t2 });
|
|
757
1142
|
}
|
|
758
1143
|
}
|
|
759
|
-
|
|
1144
|
+
scored.sort((a, b) => b.score - a.score);
|
|
1145
|
+
return scored;
|
|
1146
|
+
}
|
|
1147
|
+
function listTokens(db, options) {
|
|
1148
|
+
if (options?.domain) {
|
|
1149
|
+
return db.prepare(
|
|
1150
|
+
"SELECT * FROM tokens WHERE domain = ? AND deprecated_at IS NULL ORDER BY bloom_level, slug"
|
|
1151
|
+
).all(options.domain);
|
|
1152
|
+
}
|
|
1153
|
+
return db.prepare(
|
|
1154
|
+
"SELECT * FROM tokens WHERE deprecated_at IS NULL ORDER BY bloom_level, domain, slug"
|
|
1155
|
+
).all();
|
|
760
1156
|
}
|
|
761
1157
|
|
|
762
|
-
// src/kernel/
|
|
763
|
-
function
|
|
764
|
-
|
|
765
|
-
const
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
}
|
|
771
|
-
byDomain.set(item.domain, [item]);
|
|
1158
|
+
// src/kernel/observation/analyzer.ts
|
|
1159
|
+
function parseMonitorLog(jsonl) {
|
|
1160
|
+
const events = [];
|
|
1161
|
+
for (const line of jsonl.split("\n")) {
|
|
1162
|
+
const trimmed = line.trim();
|
|
1163
|
+
if (!trimmed) continue;
|
|
1164
|
+
try {
|
|
1165
|
+
events.push(JSON.parse(trimmed));
|
|
1166
|
+
} catch {
|
|
772
1167
|
}
|
|
773
1168
|
}
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
const
|
|
779
|
-
for (const
|
|
780
|
-
|
|
1169
|
+
return events;
|
|
1170
|
+
}
|
|
1171
|
+
function pairCommands(events) {
|
|
1172
|
+
const starts = /* @__PURE__ */ new Map();
|
|
1173
|
+
const records = [];
|
|
1174
|
+
for (const e of events) {
|
|
1175
|
+
if (e.type === "command_start" && e.seq != null) {
|
|
1176
|
+
const key = `${e.pid ?? 0}:${e.seq}`;
|
|
1177
|
+
starts.set(key, e);
|
|
1178
|
+
} else if (e.type === "command_end" && e.seq != null) {
|
|
1179
|
+
const key = `${e.pid ?? 0}:${e.seq}`;
|
|
1180
|
+
const start = starts.get(key);
|
|
1181
|
+
if (start) {
|
|
1182
|
+
const startMs = new Date(start.ts).getTime();
|
|
1183
|
+
const endMs = new Date(e.ts).getTime();
|
|
1184
|
+
records.push({
|
|
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);
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
781
1197
|
}
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
1198
|
+
for (const [, start] of starts) {
|
|
1199
|
+
records.push({
|
|
1200
|
+
seq: start.seq ?? 0,
|
|
1201
|
+
pid: start.pid ?? 0,
|
|
1202
|
+
command: start.command ?? "",
|
|
1203
|
+
cwd: start.cwd ?? "",
|
|
1204
|
+
startedAt: start.ts,
|
|
1205
|
+
endedAt: null,
|
|
1206
|
+
durationMs: null,
|
|
1207
|
+
exitCode: null
|
|
787
1208
|
});
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
1209
|
+
}
|
|
1210
|
+
records.sort(
|
|
1211
|
+
(a, b) => new Date(a.startedAt).getTime() - new Date(b.startedAt).getTime()
|
|
1212
|
+
);
|
|
1213
|
+
return records;
|
|
1214
|
+
}
|
|
1215
|
+
var HELP_PATTERNS = ["--help", "man ", "tldr ", "help "];
|
|
1216
|
+
var HELP_WINDOW_MS = 6e4;
|
|
1217
|
+
function matchesToken(command, patterns) {
|
|
1218
|
+
const lower = command.toLowerCase();
|
|
1219
|
+
return patterns.some((p) => lower.includes(p.toLowerCase()));
|
|
1220
|
+
}
|
|
1221
|
+
function isHelpCommand(command) {
|
|
1222
|
+
const lower = command.toLowerCase();
|
|
1223
|
+
return HELP_PATTERNS.some((p) => lower.includes(p));
|
|
1224
|
+
}
|
|
1225
|
+
function commandPrefix(command) {
|
|
1226
|
+
return command.split(/\s+/).slice(0, 2).join(" ").toLowerCase();
|
|
1227
|
+
}
|
|
1228
|
+
function computeMedian(values) {
|
|
1229
|
+
if (values.length === 0) return null;
|
|
1230
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
1231
|
+
const mid = Math.floor(sorted.length / 2);
|
|
1232
|
+
return sorted.length % 2 !== 0 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2;
|
|
1233
|
+
}
|
|
1234
|
+
function analyzeObservation(commands, tokenPatterns) {
|
|
1235
|
+
const matchedSet = /* @__PURE__ */ new Set();
|
|
1236
|
+
const ratings = [];
|
|
1237
|
+
for (const tp of tokenPatterns) {
|
|
1238
|
+
const matchIndices = [];
|
|
1239
|
+
const matchedTexts = [];
|
|
1240
|
+
for (let i = 0; i < commands.length; i++) {
|
|
1241
|
+
if (matchesToken(commands[i].command, tp.patterns)) {
|
|
1242
|
+
matchIndices.push(i);
|
|
1243
|
+
matchedTexts.push(commands[i].command);
|
|
1244
|
+
matchedSet.add(i);
|
|
804
1245
|
}
|
|
805
|
-
break;
|
|
806
1246
|
}
|
|
807
|
-
if (
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
1247
|
+
if (matchIndices.length === 0) {
|
|
1248
|
+
ratings.push({
|
|
1249
|
+
tokenSlug: tp.slug,
|
|
1250
|
+
rating: null,
|
|
1251
|
+
confidence: "low",
|
|
1252
|
+
evidence: {
|
|
1253
|
+
matchedCommands: 0,
|
|
1254
|
+
helpSeeking: false,
|
|
1255
|
+
errorCount: 0,
|
|
1256
|
+
selfCorrections: 0,
|
|
1257
|
+
medianGapMs: null,
|
|
1258
|
+
thinkingGapMs: null
|
|
1259
|
+
},
|
|
1260
|
+
matchedCommandTexts: []
|
|
1261
|
+
});
|
|
1262
|
+
continue;
|
|
1263
|
+
}
|
|
1264
|
+
let helpSeeking = false;
|
|
1265
|
+
for (const mi of matchIndices) {
|
|
1266
|
+
const matchTime = new Date(commands[mi].startedAt).getTime();
|
|
1267
|
+
for (let j = 0; j < commands.length; j++) {
|
|
1268
|
+
if (j === mi) continue;
|
|
1269
|
+
const cmdTime = new Date(commands[j].startedAt).getTime();
|
|
1270
|
+
if (cmdTime >= matchTime - HELP_WINDOW_MS && cmdTime < matchTime) {
|
|
1271
|
+
if (isHelpCommand(commands[j].command)) {
|
|
1272
|
+
helpSeeking = true;
|
|
1273
|
+
break;
|
|
1274
|
+
}
|
|
818
1275
|
}
|
|
819
|
-
|
|
1276
|
+
}
|
|
1277
|
+
if (helpSeeking) break;
|
|
1278
|
+
}
|
|
1279
|
+
let errorCount = 0;
|
|
1280
|
+
for (const mi of matchIndices) {
|
|
1281
|
+
if (commands[mi].exitCode != null && commands[mi].exitCode !== 0) {
|
|
1282
|
+
errorCount++;
|
|
820
1283
|
}
|
|
821
1284
|
}
|
|
1285
|
+
let selfCorrections = 0;
|
|
1286
|
+
const prefixGroups = /* @__PURE__ */ new Map();
|
|
1287
|
+
for (const mi of matchIndices) {
|
|
1288
|
+
const prefix = commandPrefix(commands[mi].command);
|
|
1289
|
+
prefixGroups.set(prefix, (prefixGroups.get(prefix) ?? 0) + 1);
|
|
1290
|
+
}
|
|
1291
|
+
for (const count of prefixGroups.values()) {
|
|
1292
|
+
if (count > 1) selfCorrections += count - 1;
|
|
1293
|
+
}
|
|
1294
|
+
const gaps = [];
|
|
1295
|
+
for (let k = 1; k < matchIndices.length; k++) {
|
|
1296
|
+
const prev = commands[matchIndices[k - 1]];
|
|
1297
|
+
const curr = commands[matchIndices[k]];
|
|
1298
|
+
if (prev.endedAt) {
|
|
1299
|
+
const gap = new Date(curr.startedAt).getTime() - new Date(prev.endedAt).getTime();
|
|
1300
|
+
if (gap >= 0) gaps.push(gap);
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
let thinkingGapMs = null;
|
|
1304
|
+
const firstMatchIdx = matchIndices[0];
|
|
1305
|
+
if (firstMatchIdx > 0) {
|
|
1306
|
+
const prev = commands[firstMatchIdx - 1];
|
|
1307
|
+
if (prev.endedAt) {
|
|
1308
|
+
thinkingGapMs = new Date(commands[firstMatchIdx].startedAt).getTime() - new Date(prev.endedAt).getTime();
|
|
1309
|
+
}
|
|
1310
|
+
}
|
|
1311
|
+
const medianGapMs = computeMedian(gaps);
|
|
1312
|
+
const rating = inferRating({
|
|
1313
|
+
helpSeeking,
|
|
1314
|
+
errorCount,
|
|
1315
|
+
selfCorrections,
|
|
1316
|
+
medianGapMs,
|
|
1317
|
+
thinkingGapMs,
|
|
1318
|
+
matchedCommands: matchIndices.length
|
|
1319
|
+
});
|
|
1320
|
+
const confidence = matchIndices.length >= 3 ? "high" : matchIndices.length >= 2 ? "medium" : "low";
|
|
1321
|
+
ratings.push({
|
|
1322
|
+
tokenSlug: tp.slug,
|
|
1323
|
+
rating,
|
|
1324
|
+
confidence,
|
|
1325
|
+
evidence: {
|
|
1326
|
+
matchedCommands: matchIndices.length,
|
|
1327
|
+
helpSeeking,
|
|
1328
|
+
errorCount,
|
|
1329
|
+
selfCorrections,
|
|
1330
|
+
medianGapMs,
|
|
1331
|
+
thinkingGapMs
|
|
1332
|
+
},
|
|
1333
|
+
matchedCommandTexts: matchedTexts
|
|
1334
|
+
});
|
|
822
1335
|
}
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
function buildReviewQueue(db, options) {
|
|
828
|
-
const maxNew = options.maxNew ?? 10;
|
|
829
|
-
const maxReviews = options.maxReviews ?? 50;
|
|
830
|
-
const now = options.now ?? /* @__PURE__ */ new Date();
|
|
831
|
-
const nowISO = now.toISOString();
|
|
832
|
-
const dueRows = db.prepare(
|
|
833
|
-
`SELECT
|
|
834
|
-
c.id AS card_id,
|
|
835
|
-
c.token_id AS token_id,
|
|
836
|
-
t.slug AS slug,
|
|
837
|
-
t.concept AS concept,
|
|
838
|
-
t.domain AS domain,
|
|
839
|
-
t.bloom_level AS bloom_level,
|
|
840
|
-
c.state AS state,
|
|
841
|
-
c.due_at AS due_at
|
|
842
|
-
FROM cards c
|
|
843
|
-
JOIN tokens t ON t.id = c.token_id
|
|
844
|
-
WHERE c.user_id = ?
|
|
845
|
-
AND c.blocked = 0
|
|
846
|
-
AND c.due_at <= ?
|
|
847
|
-
AND c.state IN ('review', 'relearning', 'learning')
|
|
848
|
-
AND t.deprecated_at IS NULL
|
|
849
|
-
ORDER BY c.due_at ASC`
|
|
850
|
-
).all(options.userId, nowISO);
|
|
851
|
-
const newRows = db.prepare(
|
|
852
|
-
`SELECT
|
|
853
|
-
c.id AS card_id,
|
|
854
|
-
c.token_id AS token_id,
|
|
855
|
-
t.slug AS slug,
|
|
856
|
-
t.concept AS concept,
|
|
857
|
-
t.domain AS domain,
|
|
858
|
-
t.bloom_level AS bloom_level,
|
|
859
|
-
c.state AS state,
|
|
860
|
-
c.due_at AS due_at
|
|
861
|
-
FROM cards c
|
|
862
|
-
JOIN tokens t ON t.id = c.token_id
|
|
863
|
-
WHERE c.user_id = ?
|
|
864
|
-
AND c.blocked = 0
|
|
865
|
-
AND c.state = 'new'
|
|
866
|
-
AND t.deprecated_at IS NULL
|
|
867
|
-
ORDER BY t.bloom_level ASC, t.slug ASC
|
|
868
|
-
LIMIT ?`
|
|
869
|
-
).all(options.userId, maxNew);
|
|
870
|
-
const nowMs = now.getTime();
|
|
871
|
-
const sortedDue = [...dueRows].sort((a, b) => {
|
|
872
|
-
const overdueA = nowMs - new Date(a.due_at).getTime();
|
|
873
|
-
const overdueB = nowMs - new Date(b.due_at).getTime();
|
|
874
|
-
return overdueB - overdueA;
|
|
875
|
-
});
|
|
876
|
-
const interleavedDue = interleave(
|
|
877
|
-
sortedDue.map((row) => ({ ...rowToItem(row), domain: row.domain }))
|
|
878
|
-
);
|
|
879
|
-
const newItems = newRows.map(rowToItem);
|
|
880
|
-
const merged = intersperseNew(interleavedDue, newItems, 5);
|
|
881
|
-
const capped = merged.slice(0, maxReviews);
|
|
882
|
-
let newCount = 0;
|
|
883
|
-
let reviewCount = 0;
|
|
884
|
-
let relearnCount = 0;
|
|
885
|
-
const domainSet = /* @__PURE__ */ new Set();
|
|
886
|
-
for (const item of capped) {
|
|
887
|
-
domainSet.add(item.domain);
|
|
888
|
-
switch (item.state) {
|
|
889
|
-
case "new":
|
|
890
|
-
newCount++;
|
|
891
|
-
break;
|
|
892
|
-
case "relearning":
|
|
893
|
-
relearnCount++;
|
|
894
|
-
break;
|
|
895
|
-
default:
|
|
896
|
-
reviewCount++;
|
|
897
|
-
break;
|
|
1336
|
+
const unmatchedCommands = [];
|
|
1337
|
+
for (let i = 0; i < commands.length; i++) {
|
|
1338
|
+
if (!matchedSet.has(i) && !isHelpCommand(commands[i].command)) {
|
|
1339
|
+
unmatchedCommands.push(commands[i].command);
|
|
898
1340
|
}
|
|
899
1341
|
}
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
1342
|
+
let timeSpan = null;
|
|
1343
|
+
if (commands.length > 0) {
|
|
1344
|
+
const first = commands[0];
|
|
1345
|
+
const last = commands[commands.length - 1];
|
|
1346
|
+
const endTs = last.endedAt ?? last.startedAt;
|
|
1347
|
+
timeSpan = {
|
|
1348
|
+
start: first.startedAt,
|
|
1349
|
+
end: endTs,
|
|
1350
|
+
durationMs: new Date(endTs).getTime() - new Date(first.startedAt).getTime()
|
|
1351
|
+
};
|
|
1352
|
+
}
|
|
1353
|
+
return { ratings, unmatchedCommands, timeSpan };
|
|
907
1354
|
}
|
|
908
|
-
function
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
1355
|
+
function inferRating(signals) {
|
|
1356
|
+
const {
|
|
1357
|
+
helpSeeking,
|
|
1358
|
+
errorCount,
|
|
1359
|
+
selfCorrections,
|
|
1360
|
+
medianGapMs,
|
|
1361
|
+
thinkingGapMs
|
|
1362
|
+
} = signals;
|
|
1363
|
+
let negatives = 0;
|
|
1364
|
+
if (helpSeeking) negatives += 2;
|
|
1365
|
+
if (errorCount >= 3) negatives += 3;
|
|
1366
|
+
else if (errorCount >= 1) negatives += 1;
|
|
1367
|
+
if (selfCorrections >= 2) negatives += 2;
|
|
1368
|
+
else if (selfCorrections >= 1) negatives += 1;
|
|
1369
|
+
if (medianGapMs != null && medianGapMs > 3e4) negatives += 2;
|
|
1370
|
+
else if (medianGapMs != null && medianGapMs > 1e4) negatives += 1;
|
|
1371
|
+
if (thinkingGapMs != null && thinkingGapMs > 3e4) negatives += 1;
|
|
1372
|
+
if (negatives >= 5) return 1;
|
|
1373
|
+
if (negatives >= 3) return 2;
|
|
1374
|
+
if (negatives >= 1) return 3;
|
|
1375
|
+
return 4;
|
|
919
1376
|
}
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
1377
|
+
|
|
1378
|
+
// src/kernel/observation/monitor-io.ts
|
|
1379
|
+
import {
|
|
1380
|
+
appendFileSync,
|
|
1381
|
+
existsSync as existsSync4,
|
|
1382
|
+
mkdirSync as mkdirSync3,
|
|
1383
|
+
readFileSync as readFileSync4,
|
|
1384
|
+
statSync
|
|
1385
|
+
} from "fs";
|
|
1386
|
+
import { homedir as homedir3 } from "os";
|
|
1387
|
+
import { join as join4 } from "path";
|
|
1388
|
+
var MONITOR_DIR = join4(homedir3(), ".zam", "monitor");
|
|
1389
|
+
function getMonitorDir() {
|
|
1390
|
+
return MONITOR_DIR;
|
|
1391
|
+
}
|
|
1392
|
+
function getMonitorPath(sessionId) {
|
|
1393
|
+
return join4(MONITOR_DIR, `${sessionId}.jsonl`);
|
|
1394
|
+
}
|
|
1395
|
+
function ensureMonitorDir() {
|
|
1396
|
+
if (!existsSync4(MONITOR_DIR)) {
|
|
1397
|
+
mkdirSync3(MONITOR_DIR, { recursive: true, mode: 448 });
|
|
939
1398
|
}
|
|
940
|
-
|
|
1399
|
+
}
|
|
1400
|
+
function writeMonitorEvent(sessionId, event) {
|
|
1401
|
+
ensureMonitorDir();
|
|
1402
|
+
const path = getMonitorPath(sessionId);
|
|
1403
|
+
appendFileSync(path, `${JSON.stringify(event)}
|
|
1404
|
+
`);
|
|
1405
|
+
}
|
|
1406
|
+
function readMonitorLog(sessionId) {
|
|
1407
|
+
const path = getMonitorPath(sessionId);
|
|
1408
|
+
if (!existsSync4(path)) {
|
|
1409
|
+
return [];
|
|
1410
|
+
}
|
|
1411
|
+
const content = readFileSync4(path, "utf-8");
|
|
1412
|
+
return parseMonitorLog(content);
|
|
1413
|
+
}
|
|
1414
|
+
function monitorLogExists(sessionId) {
|
|
1415
|
+
return existsSync4(getMonitorPath(sessionId));
|
|
1416
|
+
}
|
|
1417
|
+
function getMonitorLogStats(sessionId) {
|
|
1418
|
+
const path = getMonitorPath(sessionId);
|
|
1419
|
+
if (!existsSync4(path)) {
|
|
1420
|
+
return { exists: false, sizeBytes: 0, lineCount: 0 };
|
|
1421
|
+
}
|
|
1422
|
+
const stat = statSync(path);
|
|
1423
|
+
const content = readFileSync4(path, "utf-8");
|
|
1424
|
+
const lineCount = content.split("\n").filter((l) => l.trim()).length;
|
|
1425
|
+
return { exists: true, sizeBytes: stat.size, lineCount };
|
|
941
1426
|
}
|
|
942
1427
|
|
|
943
|
-
// src/kernel/
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
2: "Understand",
|
|
947
|
-
3: "Apply",
|
|
948
|
-
4: "Analyze",
|
|
949
|
-
5: "Synthesize"
|
|
950
|
-
};
|
|
951
|
-
var BLOOM_PROMPTS = {
|
|
952
|
-
1: (c) => `What is: ${c}?`,
|
|
953
|
-
2: (c) => `Explain how this works: ${c}`,
|
|
954
|
-
3: (c) => `Apply this concept: ${c}`,
|
|
955
|
-
4: (c) => `Analyze the trade-offs: ${c}`,
|
|
956
|
-
5: (c) => `Design a solution using: ${c}`
|
|
957
|
-
};
|
|
958
|
-
function generatePrompt(input) {
|
|
959
|
-
const bloom = input.bloomLevel >= 1 && input.bloomLevel <= 5 ? input.bloomLevel : 1;
|
|
960
|
-
return {
|
|
961
|
-
cardId: input.cardId,
|
|
962
|
-
tokenId: input.tokenId,
|
|
963
|
-
slug: input.slug,
|
|
964
|
-
question: BLOOM_PROMPTS[bloom](input.concept),
|
|
965
|
-
concept: input.concept,
|
|
966
|
-
domain: input.domain,
|
|
967
|
-
bloomLevel: bloom,
|
|
968
|
-
bloomVerb: BLOOM_VERBS[bloom],
|
|
969
|
-
hints: []
|
|
970
|
-
};
|
|
1428
|
+
// src/kernel/observation/shell-hooks.ts
|
|
1429
|
+
function psSingleQuoted(value) {
|
|
1430
|
+
return `'${value.replace(/'/g, "''")}'`;
|
|
971
1431
|
}
|
|
1432
|
+
function generateZshHooks(monitorFile, sessionId) {
|
|
1433
|
+
return `
|
|
1434
|
+
# ZAM monitor hooks for session ${sessionId}
|
|
1435
|
+
export __ZAM_MONITOR_FILE="${monitorFile}"
|
|
1436
|
+
export __ZAM_MONITOR_SEQ=0
|
|
1437
|
+
export __ZAM_MONITOR_SESSION="${sessionId}"
|
|
972
1438
|
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
const schedulingCard = {
|
|
983
|
-
stability: card.stability,
|
|
984
|
-
difficulty: card.difficulty,
|
|
985
|
-
elapsedDays: card.elapsed_days,
|
|
986
|
-
scheduledDays: card.scheduled_days,
|
|
987
|
-
reps: card.reps,
|
|
988
|
-
lapses: card.lapses,
|
|
989
|
-
state: card.state,
|
|
990
|
-
dueAt: new Date(card.due_at),
|
|
991
|
-
lastReviewAt: card.last_review_at ? new Date(card.last_review_at) : null
|
|
992
|
-
};
|
|
993
|
-
const updated = fsrs.schedule(schedulingCard, input.rating, now);
|
|
994
|
-
updateCard(db, input.cardId, {
|
|
995
|
-
stability: updated.stability,
|
|
996
|
-
difficulty: updated.difficulty,
|
|
997
|
-
elapsed_days: updated.elapsedDays,
|
|
998
|
-
scheduled_days: updated.scheduledDays,
|
|
999
|
-
reps: updated.reps,
|
|
1000
|
-
lapses: updated.lapses,
|
|
1001
|
-
state: updated.state,
|
|
1002
|
-
due_at: updated.dueAt.toISOString(),
|
|
1003
|
-
last_review_at: now.toISOString()
|
|
1004
|
-
});
|
|
1005
|
-
db.prepare(
|
|
1006
|
-
`INSERT INTO review_logs (id, card_id, token_id, user_id, rating, response_time_ms, reviewed_at, scheduled_at, session_id)
|
|
1007
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
1008
|
-
).run(
|
|
1009
|
-
ulid6(),
|
|
1010
|
-
input.cardId,
|
|
1011
|
-
input.tokenId,
|
|
1012
|
-
input.userId,
|
|
1013
|
-
input.rating,
|
|
1014
|
-
input.responseTimeMs ?? null,
|
|
1015
|
-
now.toISOString(),
|
|
1016
|
-
card.due_at,
|
|
1017
|
-
input.sessionId ?? null
|
|
1018
|
-
);
|
|
1019
|
-
return {
|
|
1020
|
-
nextDueAt: updated.dueAt.toISOString(),
|
|
1021
|
-
stability: updated.stability,
|
|
1022
|
-
difficulty: updated.difficulty,
|
|
1023
|
-
state: updated.state,
|
|
1024
|
-
scheduledDays: updated.scheduledDays,
|
|
1025
|
-
reps: updated.reps,
|
|
1026
|
-
lapses: updated.lapses
|
|
1027
|
-
};
|
|
1439
|
+
__zam_ts() {
|
|
1440
|
+
if [[ -n "\${EPOCHREALTIME:-}" ]]; then
|
|
1441
|
+
local sec="\${EPOCHREALTIME%%.*}"
|
|
1442
|
+
local frac="\${EPOCHREALTIME##*.}"
|
|
1443
|
+
frac="\${frac:0:3}"
|
|
1444
|
+
printf '%s.%sZ' "$(date -u -r "$sec" '+%Y-%m-%dT%H:%M:%S' 2>/dev/null || date -u '+%Y-%m-%dT%H:%M:%S')" "$frac"
|
|
1445
|
+
else
|
|
1446
|
+
date -u '+%Y-%m-%dT%H:%M:%SZ'
|
|
1447
|
+
fi
|
|
1028
1448
|
}
|
|
1029
1449
|
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1450
|
+
__zam_preexec() {
|
|
1451
|
+
(( __ZAM_MONITOR_SEQ++ ))
|
|
1452
|
+
local cmd="\${1//\\"/\\\\\\"}"
|
|
1453
|
+
local cwd="\${PWD//\\"/\\\\\\"}"
|
|
1454
|
+
local ts="$(__zam_ts)"
|
|
1455
|
+
printf '{"type":"command_start","ts":"%s","command":"%s","cwd":"%s","seq":%d,"pid":%d}\\n' \\
|
|
1456
|
+
"$ts" "$cmd" "$cwd" "$__ZAM_MONITOR_SEQ" "$$" \\
|
|
1457
|
+
>> "$__ZAM_MONITOR_FILE"
|
|
1033
1458
|
}
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
userId
|
|
1043
|
-
).n,
|
|
1044
|
-
blocked: q(db, "SELECT COUNT(*) as n FROM cards WHERE user_id = ? AND blocked = 1", userId).n,
|
|
1045
|
-
mature: q(
|
|
1046
|
-
db,
|
|
1047
|
-
"SELECT COUNT(*) as n FROM cards WHERE user_id = ? AND reps >= 3 AND stability >= 21",
|
|
1048
|
-
userId
|
|
1049
|
-
).n,
|
|
1050
|
-
avgStability: (() => {
|
|
1051
|
-
const v = q(db, "SELECT AVG(stability) as v FROM cards WHERE user_id = ? AND reps > 0", userId);
|
|
1052
|
-
return v.v ? Math.round(v.v * 100) / 100 : null;
|
|
1053
|
-
})(),
|
|
1054
|
-
totalSessions: q(db, "SELECT COUNT(*) as n FROM sessions WHERE user_id = ?", userId).n,
|
|
1055
|
-
lastSession: (() => {
|
|
1056
|
-
const r = db.prepare(
|
|
1057
|
-
"SELECT started_at FROM sessions WHERE user_id = ? ORDER BY started_at DESC LIMIT 1"
|
|
1058
|
-
).get(userId);
|
|
1059
|
-
return r?.started_at ?? null;
|
|
1060
|
-
})()
|
|
1061
|
-
};
|
|
1459
|
+
|
|
1460
|
+
__zam_precmd() {
|
|
1461
|
+
local exit_code=$?
|
|
1462
|
+
[[ $__ZAM_MONITOR_SEQ -eq 0 ]] && return
|
|
1463
|
+
local ts="$(__zam_ts)"
|
|
1464
|
+
printf '{"type":"command_end","ts":"%s","exit_code":%d,"seq":%d,"pid":%d}\\n' \\
|
|
1465
|
+
"$ts" "$exit_code" "$__ZAM_MONITOR_SEQ" "$$" \\
|
|
1466
|
+
>> "$__ZAM_MONITOR_FILE"
|
|
1062
1467
|
}
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
const total = q(
|
|
1071
|
-
db,
|
|
1072
|
-
`SELECT COUNT(*) as n FROM cards c
|
|
1073
|
-
JOIN tokens t ON t.id = c.token_id
|
|
1074
|
-
WHERE c.user_id = ? AND t.domain = ?`,
|
|
1075
|
-
userId,
|
|
1076
|
-
d.domain
|
|
1077
|
-
).n;
|
|
1078
|
-
const mature = q(
|
|
1079
|
-
db,
|
|
1080
|
-
`SELECT COUNT(*) as n FROM cards c
|
|
1081
|
-
JOIN tokens t ON t.id = c.token_id
|
|
1082
|
-
WHERE c.user_id = ? AND t.domain = ? AND c.reps >= 3 AND c.stability >= 21`,
|
|
1083
|
-
userId,
|
|
1084
|
-
d.domain
|
|
1085
|
-
).n;
|
|
1086
|
-
const avgStab = q(
|
|
1087
|
-
db,
|
|
1088
|
-
`SELECT AVG(c.stability) as v FROM cards c
|
|
1089
|
-
JOIN tokens t ON t.id = c.token_id
|
|
1090
|
-
WHERE c.user_id = ? AND t.domain = ? AND c.reps > 0`,
|
|
1091
|
-
userId,
|
|
1092
|
-
d.domain
|
|
1093
|
-
).v ?? 0;
|
|
1094
|
-
const reviews = q(
|
|
1095
|
-
db,
|
|
1096
|
-
`SELECT COUNT(*) as total,
|
|
1097
|
-
SUM(CASE WHEN rating >= 2 THEN 1 ELSE 0 END) as passed
|
|
1098
|
-
FROM review_logs
|
|
1099
|
-
WHERE user_id = ? AND token_id IN (SELECT id FROM tokens WHERE domain = ?)`,
|
|
1100
|
-
userId,
|
|
1101
|
-
d.domain
|
|
1102
|
-
);
|
|
1103
|
-
const retentionRate = reviews.total > 0 ? reviews.passed / reviews.total : 0;
|
|
1104
|
-
let suggestedMode;
|
|
1105
|
-
if (retentionRate > 0.9 && avgStab > 30) {
|
|
1106
|
-
suggestedMode = "autonomy";
|
|
1107
|
-
} else if (retentionRate > 0.7 && avgStab > 7) {
|
|
1108
|
-
suggestedMode = "copilot";
|
|
1109
|
-
} else {
|
|
1110
|
-
suggestedMode = "shadowing";
|
|
1111
|
-
}
|
|
1112
|
-
return {
|
|
1113
|
-
domain: d.domain,
|
|
1114
|
-
totalCards: total,
|
|
1115
|
-
matureCards: mature,
|
|
1116
|
-
avgStability: Math.round(avgStab * 100) / 100,
|
|
1117
|
-
retentionRate: Math.round(retentionRate * 1e3) / 1e3,
|
|
1118
|
-
suggestedMode
|
|
1119
|
-
};
|
|
1120
|
-
});
|
|
1468
|
+
|
|
1469
|
+
autoload -Uz add-zsh-hook
|
|
1470
|
+
add-zsh-hook preexec __zam_preexec
|
|
1471
|
+
add-zsh-hook precmd __zam_precmd
|
|
1472
|
+
|
|
1473
|
+
echo "ZAM monitor active for session $__ZAM_MONITOR_SESSION"
|
|
1474
|
+
`.trim();
|
|
1121
1475
|
}
|
|
1476
|
+
function generateBashHooks(monitorFile, sessionId) {
|
|
1477
|
+
return `
|
|
1478
|
+
# ZAM monitor hooks for session ${sessionId}
|
|
1479
|
+
export __ZAM_MONITOR_FILE="${monitorFile}"
|
|
1480
|
+
export __ZAM_MONITOR_SEQ=0
|
|
1481
|
+
export __ZAM_MONITOR_SESSION="${sessionId}"
|
|
1482
|
+
export __ZAM_MONITOR_CMD_ACTIVE=0
|
|
1122
1483
|
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1484
|
+
__zam_ts() {
|
|
1485
|
+
date -u '+%Y-%m-%dT%H:%M:%SZ'
|
|
1486
|
+
}
|
|
1487
|
+
|
|
1488
|
+
__zam_debug_trap() {
|
|
1489
|
+
[[ "$__ZAM_MONITOR_CMD_ACTIVE" -eq 1 ]] && return
|
|
1490
|
+
__ZAM_MONITOR_CMD_ACTIVE=1
|
|
1491
|
+
(( __ZAM_MONITOR_SEQ++ ))
|
|
1492
|
+
local cmd="\${BASH_COMMAND//\\"/\\\\\\"}"
|
|
1493
|
+
local cwd="\${PWD//\\"/\\\\\\"}"
|
|
1494
|
+
local ts="$(__zam_ts)"
|
|
1495
|
+
printf '{"type":"command_start","ts":"%s","command":"%s","cwd":"%s","seq":%d,"pid":%d}\\n' \\
|
|
1496
|
+
"$ts" "$cmd" "$cwd" "$__ZAM_MONITOR_SEQ" "$$" \\
|
|
1497
|
+
>> "$__ZAM_MONITOR_FILE"
|
|
1498
|
+
}
|
|
1499
|
+
|
|
1500
|
+
__zam_prompt_cmd() {
|
|
1501
|
+
local exit_code=$?
|
|
1502
|
+
if [[ "$__ZAM_MONITOR_CMD_ACTIVE" -eq 1 ]]; then
|
|
1503
|
+
__ZAM_MONITOR_CMD_ACTIVE=0
|
|
1504
|
+
local ts="$(__zam_ts)"
|
|
1505
|
+
printf '{"type":"command_end","ts":"%s","exit_code":%d,"seq":%d,"pid":%d}\\n' \\
|
|
1506
|
+
"$ts" "$exit_code" "$__ZAM_MONITOR_SEQ" "$$" \\
|
|
1507
|
+
>> "$__ZAM_MONITOR_FILE"
|
|
1508
|
+
fi
|
|
1509
|
+
}
|
|
1510
|
+
|
|
1511
|
+
trap '__zam_debug_trap' DEBUG
|
|
1512
|
+
PROMPT_COMMAND="__zam_prompt_cmd;\${PROMPT_COMMAND:-}"
|
|
1513
|
+
|
|
1514
|
+
echo "ZAM monitor active for session $__ZAM_MONITOR_SESSION"
|
|
1515
|
+
`.trim();
|
|
1516
|
+
}
|
|
1517
|
+
function generatePowerShellHooks(monitorFile, sessionId) {
|
|
1518
|
+
return `
|
|
1519
|
+
# ZAM monitor hooks for session ${sessionId}
|
|
1520
|
+
$global:__ZAM_MONITOR_FILE = ${psSingleQuoted(monitorFile)}
|
|
1521
|
+
$global:__ZAM_MONITOR_SEQ = 0
|
|
1522
|
+
$global:__ZAM_MONITOR_SESSION = ${psSingleQuoted(sessionId)}
|
|
1523
|
+
$global:__ZAM_MONITOR_SKIP_NEXT_PROMPT = $true
|
|
1524
|
+
|
|
1525
|
+
function global:__zam_write_monitor_event {
|
|
1526
|
+
param([hashtable]$Event)
|
|
1527
|
+
$json = $Event | ConvertTo-Json -Compress -Depth 4
|
|
1528
|
+
$utf8NoBom = New-Object System.Text.UTF8Encoding $false
|
|
1529
|
+
[System.IO.File]::AppendAllText($global:__ZAM_MONITOR_FILE, $json + [Environment]::NewLine, $utf8NoBom)
|
|
1530
|
+
}
|
|
1531
|
+
|
|
1532
|
+
function global:__zam_iso_utc {
|
|
1533
|
+
param([datetime]$Date)
|
|
1534
|
+
if ($Date -eq [datetime]::MinValue) {
|
|
1535
|
+
return (Get-Date).ToUniversalTime().ToString("o")
|
|
1133
1536
|
}
|
|
1134
|
-
return
|
|
1537
|
+
return $Date.ToUniversalTime().ToString("o")
|
|
1135
1538
|
}
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1539
|
+
|
|
1540
|
+
function global:__zam_update_last_history_id {
|
|
1541
|
+
$history = Get-History -Count 1
|
|
1542
|
+
if ($null -ne $history) {
|
|
1543
|
+
$global:__ZAM_MONITOR_LAST_HISTORY_ID = $history.Id
|
|
1544
|
+
} elseif ($null -eq $global:__ZAM_MONITOR_LAST_HISTORY_ID) {
|
|
1545
|
+
$global:__ZAM_MONITOR_LAST_HISTORY_ID = 0
|
|
1546
|
+
}
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1549
|
+
function global:__zam_record_last_history {
|
|
1550
|
+
param(
|
|
1551
|
+
[bool]$Success,
|
|
1552
|
+
[object]$NativeExitCode
|
|
1553
|
+
)
|
|
1554
|
+
|
|
1555
|
+
$history = Get-History -Count 1
|
|
1556
|
+
if ($null -eq $history) { return }
|
|
1557
|
+
if ($history.Id -le $global:__ZAM_MONITOR_LAST_HISTORY_ID) { return }
|
|
1558
|
+
|
|
1559
|
+
$global:__ZAM_MONITOR_LAST_HISTORY_ID = $history.Id
|
|
1560
|
+
$global:__ZAM_MONITOR_SEQ += 1
|
|
1561
|
+
|
|
1562
|
+
$exitCode = 0
|
|
1563
|
+
if (-not $Success) {
|
|
1564
|
+
if ($NativeExitCode -is [int] -and $NativeExitCode -ne 0) {
|
|
1565
|
+
$exitCode = $NativeExitCode
|
|
1566
|
+
} else {
|
|
1567
|
+
$exitCode = 1
|
|
1161
1568
|
}
|
|
1162
1569
|
}
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1570
|
+
|
|
1571
|
+
$cwd = (Get-Location).Path
|
|
1572
|
+
__zam_write_monitor_event @{
|
|
1573
|
+
type = "command_start"
|
|
1574
|
+
ts = (__zam_iso_utc $history.StartExecutionTime)
|
|
1575
|
+
command = $history.CommandLine
|
|
1576
|
+
cwd = $cwd
|
|
1577
|
+
seq = $global:__ZAM_MONITOR_SEQ
|
|
1578
|
+
pid = $PID
|
|
1579
|
+
}
|
|
1580
|
+
__zam_write_monitor_event @{
|
|
1581
|
+
type = "command_end"
|
|
1582
|
+
ts = (__zam_iso_utc $history.EndExecutionTime)
|
|
1583
|
+
exit_code = $exitCode
|
|
1584
|
+
seq = $global:__ZAM_MONITOR_SEQ
|
|
1585
|
+
pid = $PID
|
|
1174
1586
|
}
|
|
1175
|
-
records.sort((a, b) => new Date(a.startedAt).getTime() - new Date(b.startedAt).getTime());
|
|
1176
|
-
return records;
|
|
1177
1587
|
}
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
function
|
|
1181
|
-
const lower = command.toLowerCase();
|
|
1182
|
-
return patterns.some((p) => lower.includes(p.toLowerCase()));
|
|
1588
|
+
|
|
1589
|
+
if (-not (Test-Path function:\\__zam_previous_prompt) -and (Test-Path function:\\prompt)) {
|
|
1590
|
+
Set-Item -Path function:\\__zam_previous_prompt -Value (Get-Item function:\\prompt).ScriptBlock
|
|
1183
1591
|
}
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1592
|
+
__zam_update_last_history_id
|
|
1593
|
+
|
|
1594
|
+
function global:prompt {
|
|
1595
|
+
$zamSuccess = $?
|
|
1596
|
+
$zamNativeExitCode = $global:LASTEXITCODE
|
|
1597
|
+
|
|
1598
|
+
if ($global:__ZAM_MONITOR_SKIP_NEXT_PROMPT) {
|
|
1599
|
+
__zam_update_last_history_id
|
|
1600
|
+
$global:__ZAM_MONITOR_SKIP_NEXT_PROMPT = $false
|
|
1601
|
+
} else {
|
|
1602
|
+
__zam_record_last_history -Success $zamSuccess -NativeExitCode $zamNativeExitCode
|
|
1603
|
+
}
|
|
1604
|
+
|
|
1605
|
+
if (Test-Path function:\\__zam_previous_prompt) {
|
|
1606
|
+
& (Get-Item function:\\__zam_previous_prompt).ScriptBlock
|
|
1607
|
+
} else {
|
|
1608
|
+
"PS $($executionContext.SessionState.Path.CurrentLocation)$('>' * ($nestedPromptLevel + 1)) "
|
|
1609
|
+
}
|
|
1187
1610
|
}
|
|
1188
|
-
|
|
1189
|
-
|
|
1611
|
+
|
|
1612
|
+
Write-Host "ZAM monitor active for session $global:__ZAM_MONITOR_SESSION"
|
|
1613
|
+
`.trim();
|
|
1190
1614
|
}
|
|
1191
|
-
function
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1615
|
+
function generateZshUnhooks() {
|
|
1616
|
+
return `
|
|
1617
|
+
# Remove ZAM monitor hooks
|
|
1618
|
+
add-zsh-hook -d preexec __zam_preexec 2>/dev/null
|
|
1619
|
+
add-zsh-hook -d precmd __zam_precmd 2>/dev/null
|
|
1620
|
+
unset -f __zam_preexec __zam_precmd __zam_ts 2>/dev/null
|
|
1621
|
+
unset __ZAM_MONITOR_FILE __ZAM_MONITOR_SEQ __ZAM_MONITOR_SESSION 2>/dev/null
|
|
1622
|
+
echo "ZAM monitor stopped."
|
|
1623
|
+
`.trim();
|
|
1196
1624
|
}
|
|
1197
|
-
function
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1625
|
+
function generateBashUnhooks() {
|
|
1626
|
+
return `
|
|
1627
|
+
# Remove ZAM monitor hooks
|
|
1628
|
+
trap - DEBUG
|
|
1629
|
+
PROMPT_COMMAND="\${PROMPT_COMMAND/__zam_prompt_cmd;/}"
|
|
1630
|
+
unset -f __zam_debug_trap __zam_prompt_cmd __zam_ts 2>/dev/null
|
|
1631
|
+
unset __ZAM_MONITOR_FILE __ZAM_MONITOR_SEQ __ZAM_MONITOR_SESSION __ZAM_MONITOR_CMD_ACTIVE 2>/dev/null
|
|
1632
|
+
echo "ZAM monitor stopped."
|
|
1633
|
+
`.trim();
|
|
1634
|
+
}
|
|
1635
|
+
function generatePowerShellUnhooks() {
|
|
1636
|
+
return `
|
|
1637
|
+
# Remove ZAM monitor hooks
|
|
1638
|
+
if (Test-Path function:\\__zam_previous_prompt) {
|
|
1639
|
+
Set-Item -Path function:\\prompt -Value (Get-Item function:\\__zam_previous_prompt).ScriptBlock
|
|
1640
|
+
Remove-Item function:\\__zam_previous_prompt -Force -ErrorAction SilentlyContinue
|
|
1641
|
+
}
|
|
1642
|
+
Remove-Item function:\\__zam_write_monitor_event,function:\\__zam_iso_utc,function:\\__zam_update_last_history_id,function:\\__zam_record_last_history -ErrorAction SilentlyContinue
|
|
1643
|
+
Remove-Variable -Name __ZAM_MONITOR_FILE,__ZAM_MONITOR_SEQ,__ZAM_MONITOR_SESSION,__ZAM_MONITOR_LAST_HISTORY_ID,__ZAM_MONITOR_SKIP_NEXT_PROMPT -Scope Global -ErrorAction SilentlyContinue
|
|
1644
|
+
Write-Host "ZAM monitor stopped."
|
|
1645
|
+
`.trim();
|
|
1646
|
+
}
|
|
1647
|
+
|
|
1648
|
+
// src/kernel/observation/skill-discovery.ts
|
|
1649
|
+
function discoverSkills(sessionCommands, options = {}) {
|
|
1650
|
+
const minSessions = options.minSessions ?? 2;
|
|
1651
|
+
const minLen = options.minSequenceLength ?? 2;
|
|
1652
|
+
const maxLen = options.maxSequenceLength ?? 5;
|
|
1653
|
+
const existing = new Set(options.existingSkillSlugs ?? []);
|
|
1654
|
+
const sessionSequences = /* @__PURE__ */ new Map();
|
|
1655
|
+
for (const [sessionId, commands] of sessionCommands) {
|
|
1656
|
+
const sequences = extractSequences(commands, minLen, maxLen);
|
|
1657
|
+
if (sequences.length > 0) {
|
|
1658
|
+
sessionSequences.set(sessionId, sequences);
|
|
1226
1659
|
}
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1660
|
+
}
|
|
1661
|
+
const sequenceIndex = /* @__PURE__ */ new Map();
|
|
1662
|
+
for (const [, sequences] of sessionSequences) {
|
|
1663
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1664
|
+
for (const seq of sequences) {
|
|
1665
|
+
const key = seq.join(" \u2192 ");
|
|
1666
|
+
if (seen.has(key)) continue;
|
|
1667
|
+
seen.add(key);
|
|
1668
|
+
const entry = sequenceIndex.get(key);
|
|
1669
|
+
if (entry) {
|
|
1670
|
+
entry.sessionCount++;
|
|
1671
|
+
entry.totalOccurrences++;
|
|
1672
|
+
} else {
|
|
1673
|
+
sequenceIndex.set(key, {
|
|
1674
|
+
steps: seq,
|
|
1675
|
+
sessionCount: 1,
|
|
1676
|
+
totalOccurrences: 1,
|
|
1677
|
+
examples: []
|
|
1678
|
+
});
|
|
1239
1679
|
}
|
|
1240
|
-
if (helpSeeking) break;
|
|
1241
1680
|
}
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1681
|
+
}
|
|
1682
|
+
const lastSessionId = [...sessionCommands.keys()].pop();
|
|
1683
|
+
if (lastSessionId) {
|
|
1684
|
+
const lastCommands = sessionCommands.get(lastSessionId);
|
|
1685
|
+
for (const [_key, entry] of sequenceIndex) {
|
|
1686
|
+
if (entry.sessionCount >= minSessions) {
|
|
1687
|
+
entry.examples = findExamplesForSequence(lastCommands, entry.steps);
|
|
1246
1688
|
}
|
|
1247
1689
|
}
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1690
|
+
}
|
|
1691
|
+
const candidates = [...sequenceIndex.values()].filter(
|
|
1692
|
+
(s) => s.sessionCount >= minSessions
|
|
1693
|
+
);
|
|
1694
|
+
const pruned = removeSubsequences(candidates);
|
|
1695
|
+
const proposals = [];
|
|
1696
|
+
for (const seq of pruned) {
|
|
1697
|
+
const slug = generateSlug(seq.steps);
|
|
1698
|
+
if (existing.has(slug)) continue;
|
|
1699
|
+
proposals.push({
|
|
1700
|
+
slug,
|
|
1701
|
+
description: describeSequence(seq.steps),
|
|
1702
|
+
steps: seq.steps,
|
|
1703
|
+
sessionCount: seq.sessionCount,
|
|
1704
|
+
confidence: seq.sessionCount >= 4 ? "high" : seq.sessionCount >= 3 ? "medium" : "low",
|
|
1705
|
+
examples: seq.examples
|
|
1706
|
+
});
|
|
1707
|
+
}
|
|
1708
|
+
const confidenceOrder = { high: 0, medium: 1, low: 2 };
|
|
1709
|
+
proposals.sort((a, b) => {
|
|
1710
|
+
const confDiff = confidenceOrder[a.confidence] - confidenceOrder[b.confidence];
|
|
1711
|
+
if (confDiff !== 0) return confDiff;
|
|
1712
|
+
return b.sessionCount - a.sessionCount;
|
|
1713
|
+
});
|
|
1714
|
+
return proposals;
|
|
1715
|
+
}
|
|
1716
|
+
function normalizeCommand(command) {
|
|
1717
|
+
const parts = command.trim().split(/\s+/);
|
|
1718
|
+
const multiWord = ["docker compose", "npm run", "npx", "git"];
|
|
1719
|
+
const lower = command.toLowerCase();
|
|
1720
|
+
for (const mw of multiWord) {
|
|
1721
|
+
if (lower.startsWith(mw) && parts.length >= mw.split(" ").length + 1) {
|
|
1722
|
+
return parts.slice(0, mw.split(" ").length + 1).join(" ").toLowerCase();
|
|
1256
1723
|
}
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1724
|
+
}
|
|
1725
|
+
return parts.slice(0, Math.min(2, parts.length)).join(" ").toLowerCase();
|
|
1726
|
+
}
|
|
1727
|
+
function extractSequences(commands, minLen, maxLen) {
|
|
1728
|
+
const filtered = commands.filter((c) => {
|
|
1729
|
+
const lower = c.command.toLowerCase().trim();
|
|
1730
|
+
return lower.length > 0 && !lower.startsWith("cd ") && lower !== "cd" && lower !== "ls" && lower !== "pwd" && lower !== "clear" && lower !== "exit" && !lower.startsWith("echo ");
|
|
1731
|
+
});
|
|
1732
|
+
const normalized = filtered.map((c) => normalizeCommand(c.command));
|
|
1733
|
+
const sequences = [];
|
|
1734
|
+
for (let len = minLen; len <= maxLen; len++) {
|
|
1735
|
+
for (let i = 0; i <= normalized.length - len; i++) {
|
|
1736
|
+
const seq = normalized.slice(i, i + len);
|
|
1737
|
+
if (new Set(seq).size >= 2) {
|
|
1738
|
+
sequences.push(seq);
|
|
1264
1739
|
}
|
|
1265
1740
|
}
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
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;
|
|
1272
1755
|
}
|
|
1273
1756
|
}
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
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;
|
|
1282
1773
|
});
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
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
|
|
1297
1817
|
});
|
|
1298
1818
|
}
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
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 });
|
|
1303
1845
|
}
|
|
1304
1846
|
}
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
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";
|
|
1971
|
+
}
|
|
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
|
|
1314
1988
|
};
|
|
1315
1989
|
}
|
|
1316
|
-
return {
|
|
1990
|
+
return {
|
|
1991
|
+
schedule,
|
|
1992
|
+
params: Object.freeze(resolvedParams)
|
|
1993
|
+
};
|
|
1317
1994
|
}
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
if (
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1995
|
+
|
|
1996
|
+
// src/kernel/recall/evaluator.ts
|
|
1997
|
+
function evaluateRating(db, input) {
|
|
1998
|
+
const card = db.prepare("SELECT * FROM cards WHERE id = ?").get(input.cardId);
|
|
1999
|
+
if (!card) {
|
|
2000
|
+
throw new Error(`Card not found: ${input.cardId}`);
|
|
2001
|
+
}
|
|
2002
|
+
const now = /* @__PURE__ */ new Date();
|
|
2003
|
+
const fsrs = createFSRS();
|
|
2004
|
+
const schedulingCard = {
|
|
2005
|
+
stability: card.stability,
|
|
2006
|
+
difficulty: card.difficulty,
|
|
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
|
|
2040
|
+
);
|
|
2041
|
+
return {
|
|
2042
|
+
nextDueAt: updated.dueAt.toISOString(),
|
|
2043
|
+
stability: updated.stability,
|
|
2044
|
+
difficulty: updated.difficulty,
|
|
2045
|
+
state: updated.state,
|
|
2046
|
+
scheduledDays: updated.scheduledDays,
|
|
2047
|
+
reps: updated.reps,
|
|
2048
|
+
lapses: updated.lapses
|
|
2049
|
+
};
|
|
1333
2050
|
}
|
|
1334
2051
|
|
|
1335
|
-
// src/kernel/
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
function getMonitorDir() {
|
|
1341
|
-
return MONITOR_DIR;
|
|
1342
|
-
}
|
|
1343
|
-
function getMonitorPath(sessionId) {
|
|
1344
|
-
return join2(MONITOR_DIR, `${sessionId}.jsonl`);
|
|
1345
|
-
}
|
|
1346
|
-
function ensureMonitorDir() {
|
|
1347
|
-
if (!existsSync2(MONITOR_DIR)) {
|
|
1348
|
-
mkdirSync2(MONITOR_DIR, { recursive: true, mode: 448 });
|
|
2052
|
+
// src/kernel/recall/actions.ts
|
|
2053
|
+
function getReviewTarget(db, cardId, userId) {
|
|
2054
|
+
const card = getCardById(db, cardId);
|
|
2055
|
+
if (!card) {
|
|
2056
|
+
throw new Error(`Card not found: ${cardId}`);
|
|
1349
2057
|
}
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
ensureMonitorDir();
|
|
1353
|
-
const path = getMonitorPath(sessionId);
|
|
1354
|
-
appendFileSync(path, JSON.stringify(event) + "\n");
|
|
1355
|
-
}
|
|
1356
|
-
function readMonitorLog(sessionId) {
|
|
1357
|
-
const path = getMonitorPath(sessionId);
|
|
1358
|
-
if (!existsSync2(path)) {
|
|
1359
|
-
return [];
|
|
2058
|
+
if (card.user_id !== userId) {
|
|
2059
|
+
throw new Error(`Card ${cardId} does not belong to user ${userId}`);
|
|
1360
2060
|
}
|
|
1361
|
-
const
|
|
1362
|
-
|
|
1363
|
-
}
|
|
1364
|
-
function monitorLogExists(sessionId) {
|
|
1365
|
-
return existsSync2(getMonitorPath(sessionId));
|
|
1366
|
-
}
|
|
1367
|
-
function getMonitorLogStats(sessionId) {
|
|
1368
|
-
const path = getMonitorPath(sessionId);
|
|
1369
|
-
if (!existsSync2(path)) {
|
|
1370
|
-
return { exists: false, sizeBytes: 0, lineCount: 0 };
|
|
2061
|
+
const token = getTokenById(db, card.token_id);
|
|
2062
|
+
if (!token) {
|
|
2063
|
+
throw new Error(`Token not found for card ${cardId}`);
|
|
1371
2064
|
}
|
|
1372
|
-
|
|
1373
|
-
const content = readFileSync(path, "utf-8");
|
|
1374
|
-
const lineCount = content.split("\n").filter((l) => l.trim()).length;
|
|
1375
|
-
return { exists: true, sizeBytes: stat.size, lineCount };
|
|
2065
|
+
return { cardId: card.id, token };
|
|
1376
2066
|
}
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
2067
|
+
function executeReviewAction(db, input) {
|
|
2068
|
+
const target = getReviewTarget(db, input.cardId, input.userId);
|
|
2069
|
+
switch (input.action) {
|
|
2070
|
+
case "rate": {
|
|
2071
|
+
if (input.rating == null) {
|
|
2072
|
+
throw new Error("rating is required for action=rate");
|
|
2073
|
+
}
|
|
2074
|
+
const evaluation = evaluateRating(db, {
|
|
2075
|
+
cardId: target.cardId,
|
|
2076
|
+
tokenId: target.token.id,
|
|
2077
|
+
userId: input.userId,
|
|
2078
|
+
rating: input.rating
|
|
2079
|
+
});
|
|
2080
|
+
let blocked;
|
|
2081
|
+
if (input.rating === 1) {
|
|
2082
|
+
const prereqs = getPrerequisites(db, target.token.id);
|
|
2083
|
+
if (prereqs.length > 0) {
|
|
2084
|
+
blocked = cascadeBlock(db, input.userId, target.token.slug);
|
|
2085
|
+
}
|
|
2086
|
+
}
|
|
2087
|
+
return {
|
|
2088
|
+
action: input.action,
|
|
2089
|
+
token: target.token,
|
|
2090
|
+
evaluation,
|
|
2091
|
+
blocked
|
|
2092
|
+
};
|
|
2093
|
+
}
|
|
2094
|
+
case "skip":
|
|
2095
|
+
return { action: input.action, token: target.token, skipped: true };
|
|
2096
|
+
case "stop":
|
|
2097
|
+
return { action: input.action, token: target.token, stopped: true };
|
|
2098
|
+
case "edit-token": {
|
|
2099
|
+
const updatedToken = updateToken(
|
|
2100
|
+
db,
|
|
2101
|
+
target.token.slug,
|
|
2102
|
+
input.tokenUpdates ?? {}
|
|
2103
|
+
);
|
|
2104
|
+
return {
|
|
2105
|
+
action: input.action,
|
|
2106
|
+
token: target.token,
|
|
2107
|
+
updatedToken
|
|
2108
|
+
};
|
|
2109
|
+
}
|
|
2110
|
+
case "deprecate-token": {
|
|
2111
|
+
const updatedToken = deprecateToken(db, target.token.slug);
|
|
2112
|
+
return {
|
|
2113
|
+
action: input.action,
|
|
2114
|
+
token: target.token,
|
|
2115
|
+
updatedToken
|
|
2116
|
+
};
|
|
2117
|
+
}
|
|
2118
|
+
case "delete-token": {
|
|
2119
|
+
const deletedToken = deleteToken(db, target.token.slug);
|
|
2120
|
+
return {
|
|
2121
|
+
action: input.action,
|
|
2122
|
+
token: target.token,
|
|
2123
|
+
deletedToken
|
|
2124
|
+
};
|
|
2125
|
+
}
|
|
2126
|
+
case "delete-card": {
|
|
2127
|
+
const deletedCard = deleteCardForUser(db, target.token.id, input.userId);
|
|
2128
|
+
return {
|
|
2129
|
+
action: input.action,
|
|
2130
|
+
token: target.token,
|
|
2131
|
+
deletedCard
|
|
2132
|
+
};
|
|
2133
|
+
}
|
|
2134
|
+
default: {
|
|
2135
|
+
const exhaustive = input.action;
|
|
2136
|
+
throw new Error(`Unsupported review action: ${exhaustive}`);
|
|
2137
|
+
}
|
|
2138
|
+
}
|
|
1395
2139
|
}
|
|
1396
2140
|
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
2141
|
+
// src/kernel/recall/prompter.ts
|
|
2142
|
+
var BLOOM_VERBS = {
|
|
2143
|
+
1: "Remember",
|
|
2144
|
+
2: "Understand",
|
|
2145
|
+
3: "Apply",
|
|
2146
|
+
4: "Analyze",
|
|
2147
|
+
5: "Synthesize"
|
|
2148
|
+
};
|
|
2149
|
+
function formatSlugForCue(slug) {
|
|
2150
|
+
return slug.replace(/[-_]/g, " ");
|
|
2151
|
+
}
|
|
2152
|
+
var BLOOM_CUES = {
|
|
2153
|
+
1: (slug) => `Recall the definition and core concept of: ${formatSlugForCue(slug)}`,
|
|
2154
|
+
2: (slug) => `Explain the concept and how ${formatSlugForCue(slug)} works.`,
|
|
2155
|
+
3: (slug) => `Describe how or where you would apply the concept of ${formatSlugForCue(slug)}.`,
|
|
2156
|
+
4: (slug) => `Analyze the trade-offs, advantages, or alternatives of ${formatSlugForCue(slug)}.`,
|
|
2157
|
+
5: (slug) => `How would you design a solution using the concept of ${formatSlugForCue(slug)}?`
|
|
2158
|
+
};
|
|
2159
|
+
function generateConceptFreeCue(bloomLevel, slug, _domain) {
|
|
2160
|
+
const bloom = bloomLevel >= 1 && bloomLevel <= 5 ? bloomLevel : 1;
|
|
2161
|
+
return BLOOM_CUES[bloom](slug);
|
|
1405
2162
|
}
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
2163
|
+
function generatePrompt(input) {
|
|
2164
|
+
const bloom = input.bloomLevel >= 1 && input.bloomLevel <= 5 ? input.bloomLevel : 1;
|
|
2165
|
+
const question = input.question?.trim() ? input.question.trim() : generateConceptFreeCue(bloom, input.slug, input.domain);
|
|
2166
|
+
return {
|
|
2167
|
+
cardId: input.cardId,
|
|
2168
|
+
tokenId: input.tokenId,
|
|
2169
|
+
slug: input.slug,
|
|
2170
|
+
question,
|
|
2171
|
+
concept: input.concept,
|
|
2172
|
+
domain: input.domain,
|
|
2173
|
+
bloomLevel: bloom,
|
|
2174
|
+
bloomVerb: BLOOM_VERBS[bloom],
|
|
2175
|
+
hints: [],
|
|
2176
|
+
sourceLink: input.sourceLink ?? null
|
|
2177
|
+
};
|
|
1414
2178
|
}
|
|
1415
2179
|
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
2180
|
+
// src/kernel/recall/reference-resolver.ts
|
|
2181
|
+
import { existsSync as existsSync5, readFileSync as readFileSync5 } from "fs";
|
|
2182
|
+
import { dirname as dirname3, join as join5, resolve } from "path";
|
|
2183
|
+
var DEFAULT_REVIEW_CONTEXT_MAX_CHARS = 6e3;
|
|
2184
|
+
function htmlToText(html) {
|
|
2185
|
+
let content = html;
|
|
2186
|
+
const bodyMatch = /<body[^>]*>([\s\S]*?)<\/body>/i.exec(html);
|
|
2187
|
+
if (bodyMatch) {
|
|
2188
|
+
content = bodyMatch[1];
|
|
2189
|
+
}
|
|
2190
|
+
content = content.replace(/<(script|style|head)[^>]*>([\s\S]*?)<\/\1>/gi, "");
|
|
2191
|
+
content = content.replace(
|
|
2192
|
+
/<h[1-6][^>]*>([\s\S]*?)<\/h[1-6]>/gi,
|
|
2193
|
+
"\n\n# $1\n"
|
|
2194
|
+
);
|
|
2195
|
+
content = content.replace(/<(p|div|li)[^>]*>/gi, "\n");
|
|
2196
|
+
content = content.replace(/<\/(p|div|li)>/gi, "\n");
|
|
2197
|
+
content = content.replace(/<br\s*\/?>/gi, "\n");
|
|
2198
|
+
content = content.replace(/<[^>]+>/g, "");
|
|
2199
|
+
content = content.replace(/ /g, " ").replace(/</g, "<").replace(/>/g, ">").replace(/&/g, "&").replace(/"/g, '"').replace(/'/g, "'");
|
|
2200
|
+
content = content.replace(/\n{3,}/g, "\n\n").trim();
|
|
2201
|
+
return content;
|
|
2202
|
+
}
|
|
2203
|
+
function extractLines(content, anchor) {
|
|
2204
|
+
const lines = content.split(/\r?\n/);
|
|
2205
|
+
const match = /#L(\d+)(?:-L(\d+))?$/i.exec(anchor);
|
|
2206
|
+
if (!match) return content;
|
|
2207
|
+
const start = Number.parseInt(match[1], 10) - 1;
|
|
2208
|
+
const end = match[2] ? Number.parseInt(match[2], 10) - 1 : start;
|
|
2209
|
+
if (start < 0 || start >= lines.length) return content;
|
|
2210
|
+
const slice = lines.slice(start, Math.min(end + 1, lines.length));
|
|
2211
|
+
return slice.join("\n");
|
|
2212
|
+
}
|
|
2213
|
+
async function resolveReference(sourceLink) {
|
|
2214
|
+
const cleaned = sourceLink.trim();
|
|
2215
|
+
if (cleaned.startsWith("search://")) {
|
|
2216
|
+
try {
|
|
2217
|
+
const url = new URL(cleaned);
|
|
2218
|
+
const query = url.searchParams.get("q") || "";
|
|
2219
|
+
return {
|
|
2220
|
+
sourceType: "dynamic_search",
|
|
2221
|
+
content: `QUERY_DIRECTIVE: Run web search for "${query}"`,
|
|
2222
|
+
url: cleaned
|
|
2223
|
+
};
|
|
2224
|
+
} catch {
|
|
2225
|
+
const query = cleaned.replace(/^search:\/\/(\??q=)?/, "");
|
|
2226
|
+
return {
|
|
2227
|
+
sourceType: "dynamic_search",
|
|
2228
|
+
content: `QUERY_DIRECTIVE: Run web search for "${decodeURIComponent(query)}"`,
|
|
2229
|
+
url: cleaned
|
|
2230
|
+
};
|
|
2231
|
+
}
|
|
2232
|
+
}
|
|
2233
|
+
if (cleaned.startsWith("http://") || cleaned.startsWith("https://")) {
|
|
2234
|
+
const gitHubMatch = /^https?:\/\/(?:www\.)?github\.com\/([^/]+)\/([^/]+)\/blob\/([^/]+)\/(.+)$/i.exec(
|
|
2235
|
+
cleaned
|
|
2236
|
+
);
|
|
2237
|
+
if (gitHubMatch) {
|
|
2238
|
+
const [_, owner, repo, branch, fullPathWithAnchor] = gitHubMatch;
|
|
2239
|
+
const anchorIndex2 = fullPathWithAnchor.indexOf("#");
|
|
2240
|
+
const filePath = anchorIndex2 !== -1 ? fullPathWithAnchor.slice(0, anchorIndex2) : fullPathWithAnchor;
|
|
2241
|
+
const anchor2 = anchorIndex2 !== -1 ? fullPathWithAnchor.slice(anchorIndex2) : "";
|
|
2242
|
+
const parentDir = dirname3(process.cwd());
|
|
2243
|
+
const localRepoPath = join5(parentDir, repo);
|
|
2244
|
+
const localFilePath = join5(localRepoPath, filePath);
|
|
2245
|
+
if (existsSync5(localFilePath)) {
|
|
2246
|
+
try {
|
|
2247
|
+
let fileContent = readFileSync5(localFilePath, "utf-8");
|
|
2248
|
+
if (anchor2) {
|
|
2249
|
+
fileContent = extractLines(fileContent, anchor2);
|
|
2250
|
+
}
|
|
2251
|
+
return {
|
|
2252
|
+
sourceType: "local",
|
|
2253
|
+
content: fileContent,
|
|
2254
|
+
filePath: localFilePath
|
|
2255
|
+
};
|
|
2256
|
+
} catch (_e) {
|
|
2257
|
+
}
|
|
2258
|
+
}
|
|
2259
|
+
const rawUrl = `https://raw.githubusercontent.com/${owner}/${repo}/${branch}/${filePath}`;
|
|
2260
|
+
try {
|
|
2261
|
+
const response = await fetch(rawUrl);
|
|
2262
|
+
if (response.ok) {
|
|
2263
|
+
let rawText = await response.text();
|
|
2264
|
+
if (anchor2) {
|
|
2265
|
+
rawText = extractLines(rawText, anchor2);
|
|
2266
|
+
}
|
|
2267
|
+
return {
|
|
2268
|
+
sourceType: "remote_web",
|
|
2269
|
+
content: rawText,
|
|
2270
|
+
url: cleaned
|
|
2271
|
+
};
|
|
2272
|
+
}
|
|
2273
|
+
} catch (_e) {
|
|
2274
|
+
}
|
|
2275
|
+
}
|
|
2276
|
+
try {
|
|
2277
|
+
const response = await fetch(cleaned);
|
|
2278
|
+
if (response.ok) {
|
|
2279
|
+
const text = await response.text();
|
|
2280
|
+
const cleanText = htmlToText(text);
|
|
2281
|
+
return {
|
|
2282
|
+
sourceType: "remote_web",
|
|
2283
|
+
content: cleanText,
|
|
2284
|
+
url: cleaned
|
|
2285
|
+
};
|
|
2286
|
+
}
|
|
2287
|
+
throw new Error(`HTTP error ${response.status}: ${response.statusText}`);
|
|
2288
|
+
} catch (err) {
|
|
2289
|
+
return {
|
|
2290
|
+
sourceType: "remote_web",
|
|
2291
|
+
content: `Error fetching URL reference: ${err.message}
|
|
2292
|
+
Link: ${cleaned}`,
|
|
2293
|
+
url: cleaned
|
|
2294
|
+
};
|
|
2295
|
+
}
|
|
2296
|
+
}
|
|
2297
|
+
const anchorIndex = cleaned.indexOf("#");
|
|
2298
|
+
const relativePath = anchorIndex !== -1 ? cleaned.slice(0, anchorIndex) : cleaned;
|
|
2299
|
+
const anchor = anchorIndex !== -1 ? cleaned.slice(anchorIndex) : "";
|
|
2300
|
+
const absolutePath = resolve(process.cwd(), relativePath);
|
|
2301
|
+
if (existsSync5(absolutePath)) {
|
|
2302
|
+
try {
|
|
2303
|
+
let fileContent = readFileSync5(absolutePath, "utf-8");
|
|
2304
|
+
if (anchor) {
|
|
2305
|
+
fileContent = extractLines(fileContent, anchor);
|
|
2306
|
+
}
|
|
2307
|
+
return {
|
|
2308
|
+
sourceType: "local",
|
|
2309
|
+
content: fileContent,
|
|
2310
|
+
filePath: absolutePath
|
|
2311
|
+
};
|
|
2312
|
+
} catch (_e) {
|
|
2313
|
+
}
|
|
2314
|
+
}
|
|
2315
|
+
return {
|
|
2316
|
+
sourceType: "local",
|
|
2317
|
+
content: `Local reference file not found or unreadable.
|
|
2318
|
+
Reference: ${cleaned}`,
|
|
2319
|
+
filePath: absolutePath
|
|
2320
|
+
};
|
|
1422
2321
|
}
|
|
1423
|
-
function
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
2322
|
+
async function resolveReviewContext(sourceLink, opts = {}) {
|
|
2323
|
+
const cleaned = sourceLink?.trim();
|
|
2324
|
+
if (!cleaned) return null;
|
|
2325
|
+
const maxChars = opts.maxChars ?? DEFAULT_REVIEW_CONTEXT_MAX_CHARS;
|
|
2326
|
+
const resolved = await resolveReference(cleaned);
|
|
2327
|
+
let content = resolved.content;
|
|
2328
|
+
let truncated = false;
|
|
2329
|
+
if (content.length > maxChars) {
|
|
2330
|
+
content = content.slice(0, maxChars);
|
|
2331
|
+
truncated = true;
|
|
2332
|
+
}
|
|
2333
|
+
return {
|
|
2334
|
+
sourceLink: cleaned,
|
|
2335
|
+
sourceType: resolved.sourceType,
|
|
2336
|
+
content,
|
|
2337
|
+
filePath: resolved.filePath,
|
|
2338
|
+
url: resolved.url,
|
|
2339
|
+
truncated
|
|
2340
|
+
};
|
|
1433
2341
|
}
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
2342
|
+
function normalizePath(p) {
|
|
2343
|
+
const base = p.split("#")[0].trim();
|
|
2344
|
+
return base.replace(/\\/g, "/").toLowerCase();
|
|
2345
|
+
}
|
|
2346
|
+
function matchesFilePath(sourceLink, changedFile) {
|
|
2347
|
+
if (!sourceLink) return false;
|
|
2348
|
+
const normSource = normalizePath(sourceLink);
|
|
2349
|
+
const normChanged = normalizePath(changedFile);
|
|
2350
|
+
if (!normSource || !normChanged) return false;
|
|
2351
|
+
const gitHubMatch = /^https?:\/\/(?:www\.)?github\.com\/([^/]+)\/([^/]+)\/blob\/([^/]+)\/(.+)$/i.exec(
|
|
2352
|
+
normSource
|
|
2353
|
+
);
|
|
2354
|
+
if (gitHubMatch) {
|
|
2355
|
+
const filePath = gitHubMatch[4];
|
|
2356
|
+
return filePath === normChanged;
|
|
2357
|
+
}
|
|
2358
|
+
if (normSource.startsWith("http://") || normSource.startsWith("https://")) {
|
|
2359
|
+
return false;
|
|
2360
|
+
}
|
|
2361
|
+
return normSource.endsWith(normChanged) || normChanged.endsWith(normSource);
|
|
1445
2362
|
}
|
|
1446
2363
|
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
if
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
2364
|
+
// src/kernel/scheduler/interleaver.ts
|
|
2365
|
+
function interleave(items, maxConsecutive = 2) {
|
|
2366
|
+
if (items.length <= 1) return [...items];
|
|
2367
|
+
const byDomain = /* @__PURE__ */ new Map();
|
|
2368
|
+
for (const item of items) {
|
|
2369
|
+
const group = byDomain.get(item.domain);
|
|
2370
|
+
if (group) {
|
|
2371
|
+
group.push(item);
|
|
2372
|
+
} else {
|
|
2373
|
+
byDomain.set(item.domain, [item]);
|
|
2374
|
+
}
|
|
2375
|
+
}
|
|
2376
|
+
if (byDomain.size === 1) return [...items];
|
|
2377
|
+
const result = [];
|
|
2378
|
+
let consecutiveCount = 0;
|
|
2379
|
+
let lastDomain = null;
|
|
2380
|
+
const cursors = /* @__PURE__ */ new Map();
|
|
2381
|
+
for (const domain of byDomain.keys()) {
|
|
2382
|
+
cursors.set(domain, 0);
|
|
2383
|
+
}
|
|
2384
|
+
while (result.length < items.length) {
|
|
2385
|
+
const activeDomains = [...byDomain.entries()].filter(
|
|
2386
|
+
([domain]) => (cursors.get(domain) ?? 0) < (byDomain.get(domain)?.length ?? 0)
|
|
2387
|
+
).sort((a, b) => {
|
|
2388
|
+
const remainA = a[1].length - (cursors.get(a[0]) ?? 0);
|
|
2389
|
+
const remainB = b[1].length - (cursors.get(b[0]) ?? 0);
|
|
2390
|
+
return remainB - remainA;
|
|
2391
|
+
});
|
|
2392
|
+
if (activeDomains.length === 0) break;
|
|
2393
|
+
let pickedThisRound = false;
|
|
2394
|
+
for (const [domain, group] of activeDomains) {
|
|
2395
|
+
const cursor = cursors.get(domain) ?? 0;
|
|
2396
|
+
if (cursor >= group.length) continue;
|
|
2397
|
+
if (domain === lastDomain && consecutiveCount >= maxConsecutive) {
|
|
2398
|
+
continue;
|
|
2399
|
+
}
|
|
2400
|
+
result.push(group[cursor]);
|
|
2401
|
+
cursors.set(domain, cursor + 1);
|
|
2402
|
+
pickedThisRound = true;
|
|
2403
|
+
if (domain === lastDomain) {
|
|
2404
|
+
consecutiveCount++;
|
|
2405
|
+
} else {
|
|
2406
|
+
lastDomain = domain;
|
|
2407
|
+
consecutiveCount = 1;
|
|
2408
|
+
}
|
|
2409
|
+
break;
|
|
2410
|
+
}
|
|
2411
|
+
if (!pickedThisRound) {
|
|
2412
|
+
for (const [domain, group] of activeDomains) {
|
|
2413
|
+
const cursor = cursors.get(domain) ?? 0;
|
|
2414
|
+
if (cursor >= group.length) continue;
|
|
2415
|
+
result.push(group[cursor]);
|
|
2416
|
+
cursors.set(domain, cursor + 1);
|
|
2417
|
+
if (domain === lastDomain) {
|
|
2418
|
+
consecutiveCount++;
|
|
2419
|
+
} else {
|
|
2420
|
+
lastDomain = domain;
|
|
2421
|
+
consecutiveCount = 1;
|
|
2422
|
+
}
|
|
2423
|
+
break;
|
|
2424
|
+
}
|
|
2425
|
+
}
|
|
2426
|
+
}
|
|
2427
|
+
return result;
|
|
1456
2428
|
}
|
|
1457
2429
|
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
2430
|
+
// src/kernel/scheduler/queue.ts
|
|
2431
|
+
function buildReviewQueue(db, options) {
|
|
2432
|
+
const maxNew = options.maxNew ?? 10;
|
|
2433
|
+
const maxReviews = options.maxReviews ?? 50;
|
|
2434
|
+
const now = options.now ?? /* @__PURE__ */ new Date();
|
|
2435
|
+
const nowISO = now.toISOString();
|
|
2436
|
+
const dueRows = db.prepare(
|
|
2437
|
+
`SELECT
|
|
2438
|
+
c.id AS card_id,
|
|
2439
|
+
c.token_id AS token_id,
|
|
2440
|
+
t.slug AS slug,
|
|
2441
|
+
t.concept AS concept,
|
|
2442
|
+
t.domain AS domain,
|
|
2443
|
+
t.bloom_level AS bloom_level,
|
|
2444
|
+
c.state AS state,
|
|
2445
|
+
c.due_at AS due_at,
|
|
2446
|
+
t.source_link AS source_link,
|
|
2447
|
+
t.question AS question
|
|
2448
|
+
FROM cards c
|
|
2449
|
+
JOIN tokens t ON t.id = c.token_id
|
|
2450
|
+
WHERE c.user_id = ?
|
|
2451
|
+
AND c.blocked = 0
|
|
2452
|
+
AND c.due_at <= ?
|
|
2453
|
+
AND c.state IN ('review', 'relearning', 'learning')
|
|
2454
|
+
AND t.deprecated_at IS NULL
|
|
2455
|
+
ORDER BY c.due_at ASC`
|
|
2456
|
+
).all(options.userId, nowISO);
|
|
2457
|
+
const newRows = db.prepare(
|
|
2458
|
+
`SELECT
|
|
2459
|
+
c.id AS card_id,
|
|
2460
|
+
c.token_id AS token_id,
|
|
2461
|
+
t.slug AS slug,
|
|
2462
|
+
t.concept AS concept,
|
|
2463
|
+
t.domain AS domain,
|
|
2464
|
+
t.bloom_level AS bloom_level,
|
|
2465
|
+
c.state AS state,
|
|
2466
|
+
c.due_at AS due_at,
|
|
2467
|
+
t.source_link AS source_link,
|
|
2468
|
+
t.question AS question
|
|
2469
|
+
FROM cards c
|
|
2470
|
+
JOIN tokens t ON t.id = c.token_id
|
|
2471
|
+
WHERE c.user_id = ?
|
|
2472
|
+
AND c.blocked = 0
|
|
2473
|
+
AND c.state = 'new'
|
|
2474
|
+
AND t.deprecated_at IS NULL
|
|
2475
|
+
ORDER BY t.bloom_level ASC, t.slug ASC
|
|
2476
|
+
LIMIT ?`
|
|
2477
|
+
).all(options.userId, maxNew);
|
|
2478
|
+
const nowMs = now.getTime();
|
|
2479
|
+
const sortedDue = [...dueRows].sort((a, b) => {
|
|
2480
|
+
const overdueA = nowMs - new Date(a.due_at).getTime();
|
|
2481
|
+
const overdueB = nowMs - new Date(b.due_at).getTime();
|
|
2482
|
+
return overdueB - overdueA;
|
|
2483
|
+
});
|
|
2484
|
+
const interleavedDue = interleave(
|
|
2485
|
+
sortedDue.map((row) => ({ ...rowToItem(row), domain: row.domain }))
|
|
2486
|
+
);
|
|
2487
|
+
const newItems = newRows.map(rowToItem);
|
|
2488
|
+
const merged = intersperseNew(interleavedDue, newItems, 5);
|
|
2489
|
+
const capped = merged.slice(0, maxReviews);
|
|
2490
|
+
let newCount = 0;
|
|
2491
|
+
let reviewCount = 0;
|
|
2492
|
+
let relearnCount = 0;
|
|
2493
|
+
const domainSet = /* @__PURE__ */ new Set();
|
|
2494
|
+
for (const item of capped) {
|
|
2495
|
+
domainSet.add(item.domain);
|
|
2496
|
+
switch (item.state) {
|
|
2497
|
+
case "new":
|
|
2498
|
+
newCount++;
|
|
2499
|
+
break;
|
|
2500
|
+
case "relearning":
|
|
2501
|
+
relearnCount++;
|
|
2502
|
+
break;
|
|
2503
|
+
default:
|
|
2504
|
+
reviewCount++;
|
|
2505
|
+
break;
|
|
2506
|
+
}
|
|
2507
|
+
}
|
|
2508
|
+
return {
|
|
2509
|
+
items: capped,
|
|
2510
|
+
newCount,
|
|
2511
|
+
reviewCount,
|
|
2512
|
+
relearnCount,
|
|
2513
|
+
totalDomains: [...domainSet].sort()
|
|
2514
|
+
};
|
|
1463
2515
|
}
|
|
1464
|
-
function
|
|
1465
|
-
return
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
2516
|
+
function rowToItem(row) {
|
|
2517
|
+
return {
|
|
2518
|
+
cardId: row.card_id,
|
|
2519
|
+
tokenId: row.token_id,
|
|
2520
|
+
slug: row.slug,
|
|
2521
|
+
concept: row.concept,
|
|
2522
|
+
domain: row.domain,
|
|
2523
|
+
bloomLevel: row.bloom_level,
|
|
2524
|
+
state: row.state,
|
|
2525
|
+
dueAt: row.due_at,
|
|
2526
|
+
sourceLink: row.source_link,
|
|
2527
|
+
question: row.question
|
|
2528
|
+
};
|
|
1473
2529
|
}
|
|
1474
|
-
function
|
|
1475
|
-
return
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
2530
|
+
function intersperseNew(reviews, newCards, interval) {
|
|
2531
|
+
if (newCards.length === 0) return [...reviews];
|
|
2532
|
+
if (reviews.length === 0) return [...newCards];
|
|
2533
|
+
const result = [];
|
|
2534
|
+
let reviewIdx = 0;
|
|
2535
|
+
let newIdx = 0;
|
|
2536
|
+
let position = 0;
|
|
2537
|
+
while (reviewIdx < reviews.length || newIdx < newCards.length) {
|
|
2538
|
+
if (newIdx < newCards.length && position > 0 && position % interval === interval - 1) {
|
|
2539
|
+
result.push(newCards[newIdx]);
|
|
2540
|
+
newIdx++;
|
|
2541
|
+
} else if (reviewIdx < reviews.length) {
|
|
2542
|
+
result.push(reviews[reviewIdx]);
|
|
2543
|
+
reviewIdx++;
|
|
2544
|
+
} else if (newIdx < newCards.length) {
|
|
2545
|
+
result.push(newCards[newIdx]);
|
|
2546
|
+
newIdx++;
|
|
2547
|
+
}
|
|
2548
|
+
position++;
|
|
2549
|
+
}
|
|
2550
|
+
return result;
|
|
1483
2551
|
}
|
|
1484
2552
|
|
|
1485
|
-
// src/kernel/
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
2553
|
+
// src/kernel/system/hooks.ts
|
|
2554
|
+
import {
|
|
2555
|
+
appendFileSync as appendFileSync2,
|
|
2556
|
+
copyFileSync,
|
|
2557
|
+
existsSync as existsSync6,
|
|
2558
|
+
mkdirSync as mkdirSync4,
|
|
2559
|
+
readFileSync as readFileSync6
|
|
2560
|
+
} from "fs";
|
|
2561
|
+
import { homedir as homedir4 } from "os";
|
|
2562
|
+
import { join as join6 } from "path";
|
|
2563
|
+
import { fileURLToPath } from "url";
|
|
2564
|
+
var HOME = homedir4();
|
|
2565
|
+
function getPackageSkillPath(agent = "default") {
|
|
2566
|
+
const packageRoot = [
|
|
2567
|
+
fileURLToPath(new URL("../../..", import.meta.url)),
|
|
2568
|
+
fileURLToPath(new URL("../..", import.meta.url)),
|
|
2569
|
+
fileURLToPath(new URL("..", import.meta.url))
|
|
2570
|
+
].find((candidate) => existsSync6(join6(candidate, "package.json"))) ?? "";
|
|
2571
|
+
if (!packageRoot) return "";
|
|
2572
|
+
if (agent === "codex") {
|
|
2573
|
+
const codexPath = join6(packageRoot, ".agents", "skills", "zam", "SKILL.md");
|
|
2574
|
+
if (existsSync6(codexPath)) return codexPath;
|
|
2575
|
+
return "";
|
|
2576
|
+
}
|
|
2577
|
+
if (agent === "claude") {
|
|
2578
|
+
const claudePath = join6(
|
|
2579
|
+
packageRoot,
|
|
2580
|
+
".claude",
|
|
2581
|
+
"skills",
|
|
2582
|
+
"zam",
|
|
2583
|
+
"SKILL.md"
|
|
2584
|
+
);
|
|
2585
|
+
if (existsSync6(claudePath)) return claudePath;
|
|
2586
|
+
return "";
|
|
2587
|
+
}
|
|
2588
|
+
let path = join6(packageRoot, ".agent", "skills", "zam", "SKILL.md");
|
|
2589
|
+
if (existsSync6(path)) return path;
|
|
2590
|
+
path = join6(packageRoot, ".claude", "skills", "zam", "SKILL.md");
|
|
2591
|
+
if (existsSync6(path)) return path;
|
|
2592
|
+
return "";
|
|
2593
|
+
}
|
|
2594
|
+
function distributeGlobalSkills(home = HOME) {
|
|
2595
|
+
const sourceSkill = getPackageSkillPath();
|
|
2596
|
+
const claudeSourceSkill = getPackageSkillPath("claude");
|
|
2597
|
+
const codexSourceSkill = getPackageSkillPath("codex");
|
|
2598
|
+
const results = [];
|
|
2599
|
+
if (!sourceSkill) {
|
|
2600
|
+
console.warn("Could not find ZAM source SKILL.md in the package folder.");
|
|
2601
|
+
return results;
|
|
2602
|
+
}
|
|
2603
|
+
const claudeSkillsDir = join6(home, ".claude", "skills", "zam");
|
|
2604
|
+
try {
|
|
2605
|
+
if (!claudeSourceSkill) {
|
|
2606
|
+
throw new Error("Claude skill source not found");
|
|
1496
2607
|
}
|
|
2608
|
+
mkdirSync4(claudeSkillsDir, { recursive: true });
|
|
2609
|
+
copyFileSync(claudeSourceSkill, join6(claudeSkillsDir, "SKILL.md"));
|
|
2610
|
+
results.push({
|
|
2611
|
+
name: "Claude Code Global",
|
|
2612
|
+
path: join6(claudeSkillsDir, "SKILL.md"),
|
|
2613
|
+
success: true
|
|
2614
|
+
});
|
|
2615
|
+
} catch (_err) {
|
|
2616
|
+
results.push({
|
|
2617
|
+
name: "Claude Code Global",
|
|
2618
|
+
path: claudeSkillsDir,
|
|
2619
|
+
success: false
|
|
2620
|
+
});
|
|
1497
2621
|
}
|
|
1498
|
-
const
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
totalOccurrences: 1,
|
|
1514
|
-
examples: []
|
|
1515
|
-
});
|
|
1516
|
-
}
|
|
1517
|
-
}
|
|
2622
|
+
const geminiSkillsDir = join6(home, ".gemini", "skills", "zam");
|
|
2623
|
+
try {
|
|
2624
|
+
mkdirSync4(geminiSkillsDir, { recursive: true });
|
|
2625
|
+
copyFileSync(sourceSkill, join6(geminiSkillsDir, "SKILL.md"));
|
|
2626
|
+
results.push({
|
|
2627
|
+
name: "Gemini CLI Global",
|
|
2628
|
+
path: join6(geminiSkillsDir, "SKILL.md"),
|
|
2629
|
+
success: true
|
|
2630
|
+
});
|
|
2631
|
+
} catch (_err) {
|
|
2632
|
+
results.push({
|
|
2633
|
+
name: "Gemini CLI Global",
|
|
2634
|
+
path: geminiSkillsDir,
|
|
2635
|
+
success: false
|
|
2636
|
+
});
|
|
1518
2637
|
}
|
|
1519
|
-
const
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
if (entry.sessionCount >= minSessions) {
|
|
1524
|
-
entry.examples = findExamplesForSequence(lastCommands, entry.steps);
|
|
1525
|
-
}
|
|
2638
|
+
const codexSkillsDir = join6(home, ".agents", "skills", "zam");
|
|
2639
|
+
try {
|
|
2640
|
+
if (!codexSourceSkill) {
|
|
2641
|
+
throw new Error("Codex skill source not found");
|
|
1526
2642
|
}
|
|
2643
|
+
mkdirSync4(codexSkillsDir, { recursive: true });
|
|
2644
|
+
copyFileSync(codexSourceSkill, join6(codexSkillsDir, "SKILL.md"));
|
|
2645
|
+
results.push({
|
|
2646
|
+
name: "Codex Global",
|
|
2647
|
+
path: join6(codexSkillsDir, "SKILL.md"),
|
|
2648
|
+
success: true
|
|
2649
|
+
});
|
|
2650
|
+
} catch (_err) {
|
|
2651
|
+
results.push({
|
|
2652
|
+
name: "Codex Global",
|
|
2653
|
+
path: codexSkillsDir,
|
|
2654
|
+
success: false
|
|
2655
|
+
});
|
|
1527
2656
|
}
|
|
1528
|
-
const
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
examples: seq.examples
|
|
2657
|
+
const gooseSkillsDir = join6(home, ".goose", "skills", "zam");
|
|
2658
|
+
try {
|
|
2659
|
+
mkdirSync4(gooseSkillsDir, { recursive: true });
|
|
2660
|
+
copyFileSync(sourceSkill, join6(gooseSkillsDir, "SKILL.md"));
|
|
2661
|
+
results.push({
|
|
2662
|
+
name: "Goose Global",
|
|
2663
|
+
path: join6(gooseSkillsDir, "SKILL.md"),
|
|
2664
|
+
success: true
|
|
2665
|
+
});
|
|
2666
|
+
} catch (_err) {
|
|
2667
|
+
results.push({
|
|
2668
|
+
name: "Goose Global",
|
|
2669
|
+
path: gooseSkillsDir,
|
|
2670
|
+
success: false
|
|
1543
2671
|
});
|
|
1544
2672
|
}
|
|
1545
|
-
|
|
1546
|
-
proposals.sort((a, b) => {
|
|
1547
|
-
const confDiff = confidenceOrder[a.confidence] - confidenceOrder[b.confidence];
|
|
1548
|
-
if (confDiff !== 0) return confDiff;
|
|
1549
|
-
return b.sessionCount - a.sessionCount;
|
|
1550
|
-
});
|
|
1551
|
-
return proposals;
|
|
2673
|
+
return results;
|
|
1552
2674
|
}
|
|
1553
|
-
function
|
|
1554
|
-
const
|
|
1555
|
-
const
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
2675
|
+
function injectShellHooks() {
|
|
2676
|
+
const results = [];
|
|
2677
|
+
const hookLine = `
|
|
2678
|
+
# ZAM Shell Observation Hooks
|
|
2679
|
+
if (command -v zam >/dev/null 2>&1); then eval "$(zam monitor start --quiet)"; fi
|
|
2680
|
+
`;
|
|
2681
|
+
const pwshHookLine = `
|
|
2682
|
+
# ZAM Shell Observation Hooks
|
|
2683
|
+
if (Get-Command zam -ErrorAction SilentlyContinue) { Invoke-Expression (& zam monitor start --quiet pwsh) }
|
|
2684
|
+
`;
|
|
2685
|
+
const zshrc = join6(HOME, ".zshrc");
|
|
2686
|
+
if (existsSync6(zshrc)) {
|
|
2687
|
+
try {
|
|
2688
|
+
const content = readFileSync6(zshrc, "utf8");
|
|
2689
|
+
if (content.includes("zam monitor start")) {
|
|
2690
|
+
results.push({
|
|
2691
|
+
shell: "zsh",
|
|
2692
|
+
file: zshrc,
|
|
2693
|
+
success: true,
|
|
2694
|
+
alreadyHooked: true
|
|
2695
|
+
});
|
|
2696
|
+
} else {
|
|
2697
|
+
appendFileSync2(zshrc, hookLine);
|
|
2698
|
+
results.push({
|
|
2699
|
+
shell: "zsh",
|
|
2700
|
+
file: zshrc,
|
|
2701
|
+
success: true,
|
|
2702
|
+
alreadyHooked: false
|
|
2703
|
+
});
|
|
2704
|
+
}
|
|
2705
|
+
} catch {
|
|
2706
|
+
results.push({
|
|
2707
|
+
shell: "zsh",
|
|
2708
|
+
file: zshrc,
|
|
2709
|
+
success: false,
|
|
2710
|
+
alreadyHooked: false
|
|
2711
|
+
});
|
|
1560
2712
|
}
|
|
1561
2713
|
}
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
2714
|
+
const bashrc = join6(HOME, ".bashrc");
|
|
2715
|
+
if (existsSync6(bashrc)) {
|
|
2716
|
+
try {
|
|
2717
|
+
const content = readFileSync6(bashrc, "utf8");
|
|
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
|
+
});
|
|
1576
2733
|
}
|
|
2734
|
+
} catch {
|
|
2735
|
+
results.push({
|
|
2736
|
+
shell: "bash",
|
|
2737
|
+
file: bashrc,
|
|
2738
|
+
success: false,
|
|
2739
|
+
alreadyHooked: false
|
|
2740
|
+
});
|
|
1577
2741
|
}
|
|
1578
2742
|
}
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
2743
|
+
const pwshDirs = [
|
|
2744
|
+
join6(HOME, "Documents", "PowerShell"),
|
|
2745
|
+
join6(HOME, "Documents", "WindowsPowerShell")
|
|
2746
|
+
];
|
|
2747
|
+
for (const dir of pwshDirs) {
|
|
2748
|
+
const profileFile = join6(dir, "Microsoft.PowerShell_profile.ps1");
|
|
2749
|
+
try {
|
|
2750
|
+
mkdirSync4(dir, { recursive: true });
|
|
2751
|
+
let content = "";
|
|
2752
|
+
if (existsSync6(profileFile)) {
|
|
2753
|
+
content = readFileSync6(profileFile, "utf8");
|
|
2754
|
+
}
|
|
2755
|
+
if (content.includes("zam monitor start")) {
|
|
2756
|
+
results.push({
|
|
2757
|
+
shell: "powershell",
|
|
2758
|
+
file: profileFile,
|
|
2759
|
+
success: true,
|
|
2760
|
+
alreadyHooked: true
|
|
2761
|
+
});
|
|
2762
|
+
} else {
|
|
2763
|
+
appendFileSync2(profileFile, pwshHookLine);
|
|
2764
|
+
results.push({
|
|
2765
|
+
shell: "powershell",
|
|
2766
|
+
file: profileFile,
|
|
2767
|
+
success: true,
|
|
2768
|
+
alreadyHooked: false
|
|
2769
|
+
});
|
|
1592
2770
|
}
|
|
1593
|
-
}
|
|
1594
|
-
|
|
1595
|
-
|
|
2771
|
+
} catch {
|
|
2772
|
+
results.push({
|
|
2773
|
+
shell: "powershell",
|
|
2774
|
+
file: profileFile,
|
|
2775
|
+
success: false,
|
|
2776
|
+
alreadyHooked: false
|
|
2777
|
+
});
|
|
1596
2778
|
}
|
|
1597
2779
|
}
|
|
1598
|
-
return
|
|
2780
|
+
return results;
|
|
1599
2781
|
}
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
2782
|
+
|
|
2783
|
+
// src/kernel/system/i18n.ts
|
|
2784
|
+
var TRANSLATIONS = {
|
|
2785
|
+
en: {
|
|
2786
|
+
welcome: "Learning session: {count} card(s)",
|
|
2787
|
+
new_review_relearn: " New: {newC} Review: {reviewC} Relearn: {relearnC}",
|
|
2788
|
+
domains: " Domains: {domains}",
|
|
2789
|
+
instruction: "\nRecall each answer first, reveal it, then rate yourself honestly.",
|
|
2790
|
+
quit_hint: "Type 'q' at the answer prompt (or press Ctrl+C) to stop anytime.",
|
|
2791
|
+
offline_warning: "\n\x1B[33m\u26A0 LLM-Feedback & automatic translation are disabled.\x1B[0m",
|
|
2792
|
+
offline_instruction: " Enable with: \x1B[36mnpm run dev -- settings llm on\x1B[0m\n",
|
|
2793
|
+
nothing_due: "Nothing due to learn. You're all caught up!",
|
|
2794
|
+
evaluating: "Evaluating answer via local AI...",
|
|
2795
|
+
generating_question: "Generating dynamic question...",
|
|
2796
|
+
translating: "Translating question dynamically...",
|
|
2797
|
+
prompt_answer: "Your answer (Enter to reveal \xB7 'q' to stop):",
|
|
2798
|
+
session_ended: "Learning session ended.",
|
|
2799
|
+
session_complete: "Learning session complete!",
|
|
2800
|
+
cards_rated: " Cards rated: {count}",
|
|
2801
|
+
avg_rating: " Average rating: {avg}",
|
|
2802
|
+
forgot: " Forgot: {count} card(s)",
|
|
2803
|
+
feedback_title: "\u2500\u2500 ZAM Feedback {line}",
|
|
2804
|
+
answer_title: "\u2500\u2500 Answer {line}",
|
|
2805
|
+
keep_waiting: "Would you like to keep waiting?",
|
|
2806
|
+
local_ai_working: "The local AI is still generating the response.",
|
|
2807
|
+
wait_warning: "\u26A0 The LLM server is taking a while to load the model.",
|
|
2808
|
+
wait_info: "(This is expected when transitioning between models or starting up from cold.)",
|
|
2809
|
+
keep_waiting_llm: "Would you like to keep waiting for the model?",
|
|
2810
|
+
proceeding_offline: "\u26A0 Proceeding in offline-mode (without active LLM evaluations for this session).",
|
|
2811
|
+
eval_skipped: " [LLM Evaluation skipped: {reason}]"
|
|
2812
|
+
},
|
|
2813
|
+
de: {
|
|
2814
|
+
welcome: "Lern-Session: {count} Karte(n)",
|
|
2815
|
+
new_review_relearn: " Neu: {newC} Wiederholen: {reviewC} Lernen: {relearnC}",
|
|
2816
|
+
domains: " Dom\xE4nen: {domains}",
|
|
2817
|
+
instruction: "\nRufe jede Antwort zuerst ab, decke sie auf und bewerte dich dann ehrlich selbst.",
|
|
2818
|
+
quit_hint: "Gib 'q' bei der Antwortaufforderung ein (oder dr\xFCcke Strg+C), um jederzeit zu beenden.",
|
|
2819
|
+
offline_warning: "\n\x1B[33m\u26A0 LLM-Feedback & automatische \xDCbersetzung sind deaktiviert.\x1B[0m",
|
|
2820
|
+
offline_instruction: " Aktivieren mit: \x1B[36mnpm run dev -- settings llm on\x1B[0m\n",
|
|
2821
|
+
nothing_due: "Nichts f\xE4llig zu lernen. Du bist komplett auf dem Laufenden!",
|
|
2822
|
+
evaluating: "Bewerte Antwort via lokaler KI...",
|
|
2823
|
+
generating_question: "Generiere dynamische Frage...",
|
|
2824
|
+
translating: "\xDCbersetze Frage dynamisch...",
|
|
2825
|
+
prompt_answer: "Deine Antwort (Eingabe zum Aufdecken \xB7 'q' zum Beenden):",
|
|
2826
|
+
session_ended: "Lern-Session beendet.",
|
|
2827
|
+
session_complete: "Lern-Session abgeschlossen!",
|
|
2828
|
+
cards_rated: " Bewertete Karten: {count}",
|
|
2829
|
+
avg_rating: " Durchschnittliche Bewertung: {avg}",
|
|
2830
|
+
forgot: " Vergessen: {count} Karte(n)",
|
|
2831
|
+
feedback_title: "\u2500\u2500 ZAM Feedback {line}",
|
|
2832
|
+
answer_title: "\u2500\u2500 Antwort {line}",
|
|
2833
|
+
keep_waiting: "M\xF6chtest du weiter auf die Bewertung warten?",
|
|
2834
|
+
local_ai_working: "Die lokale KI arbeitet noch an der Antwort.",
|
|
2835
|
+
wait_warning: "\u26A0 Der LLM-Server braucht ungew\xF6hnlich lange, um das Modell zu laden.",
|
|
2836
|
+
wait_info: "(Das ist normal, wenn das Modell gewechselt wird oder kalt startet.)",
|
|
2837
|
+
keep_waiting_llm: "M\xF6chtest du weiter auf das Modell warten?",
|
|
2838
|
+
proceeding_offline: "\u26A0 Fahre im Offline-Modus fort (ohne aktive LLM-Bewertungen in dieser Runde).",
|
|
2839
|
+
eval_skipped: " [LLM-Bewertung \xFCbersprungen: {reason}]"
|
|
2840
|
+
},
|
|
2841
|
+
es: {
|
|
2842
|
+
welcome: "Sesi\xF3n de aprendizaje: {count} tarjeta(s)",
|
|
2843
|
+
new_review_relearn: " Nuevas: {newC} Repasar: {reviewC} Reaprender: {relearnC}",
|
|
2844
|
+
domains: " Dominios: {domains}",
|
|
2845
|
+
instruction: "\nRecuerda cada respuesta primero, rev\xE9lala y calif\xEDcate honestamente.",
|
|
2846
|
+
quit_hint: "Escribe 'q' en la respuesta (o presiona Ctrl+C) para salir en cualquier momento.",
|
|
2847
|
+
offline_warning: "\n\x1B[33m\u26A0 Los comentarios de LLM y la traducci\xF3n autom\xE1tica est\xE1n desactivados.\x1B[0m",
|
|
2848
|
+
offline_instruction: " Activar con: \x1B[36mnpm run dev -- settings llm on\x1B[0m\n",
|
|
2849
|
+
nothing_due: "No hay nada pendiente para aprender. \xA1Est\xE1s al d\xEDa!",
|
|
2850
|
+
evaluating: "Evaluando respuesta con IA local...",
|
|
2851
|
+
generating_question: "Generando pregunta din\xE1mica...",
|
|
2852
|
+
translating: "Traduciendo pregunta din\xE1micamente...",
|
|
2853
|
+
prompt_answer: "Tu respuesta (Intro para revelar \xB7 'q' para salir):",
|
|
2854
|
+
session_ended: "Sesi\xF3n de aprendizaje finalizada.",
|
|
2855
|
+
session_complete: "\xA1Sesi\xF3n de aprendizaje completada!",
|
|
2856
|
+
cards_rated: " Tarjetas calificadas: {count}",
|
|
2857
|
+
avg_rating: " Calificaci\xF3n promedio: {avg}",
|
|
2858
|
+
forgot: " Olvidadas: {count} tarjeta(s)",
|
|
2859
|
+
feedback_title: "\u2500\u2500 Comentarios de ZAM {line}",
|
|
2860
|
+
answer_title: "\u2500\u2500 Respuesta {line}",
|
|
2861
|
+
keep_waiting: "\xBFDeseas seguir esperando la evaluaci\xF3n?",
|
|
2862
|
+
local_ai_working: "La IA local todav\xEDa est\xE1 generando la respuesta.",
|
|
2863
|
+
wait_warning: "\u26A0 El servidor LLM est\xE1 tardando en cargar el modelo.",
|
|
2864
|
+
wait_info: "(Esto es normal al cambiar de modelo o iniciar en fr\xEDo.)",
|
|
2865
|
+
keep_waiting_llm: "\xBFDeseas seguir esperando el modelo?",
|
|
2866
|
+
proceeding_offline: "\u26A0 Continuando en modo fuera de l\xEDnea (sin evaluaciones de LLM en esta sesi\xF3n).",
|
|
2867
|
+
eval_skipped: " [Evaluaci\xF3n de LLM omitida: {reason}]"
|
|
2868
|
+
},
|
|
2869
|
+
fr: {
|
|
2870
|
+
welcome: "Session d'apprentissage : {count} carte(s)",
|
|
2871
|
+
new_review_relearn: " Nouveau: {newC} R\xE9vision: {reviewC} Relever: {relearnC}",
|
|
2872
|
+
domains: " Domaines: {domains}",
|
|
2873
|
+
instruction: "\nRappelez-vous chaque r\xE9ponse d'abord, r\xE9v\xE9lez-la, puis \xE9valuez-vous honn\xEAtement.",
|
|
2874
|
+
quit_hint: "Tapez 'q' (ou Ctrl+C) pour quitter \xE0 tout moment.",
|
|
2875
|
+
offline_warning: "\n\x1B[33m\u26A0 Les commentaires LLM et la traduction automatique sont d\xE9sactiv\xE9s.\x1B[0m",
|
|
2876
|
+
offline_instruction: " Activer avec : \x1B[36mnpm run dev -- settings llm on\x1B[0m\n",
|
|
2877
|
+
nothing_due: "Rien \xE0 apprendre. Vous \xEAtes \xE0 jour !",
|
|
2878
|
+
evaluating: "\xC9valuation de la r\xE9ponse via l'IA locale...",
|
|
2879
|
+
generating_question: "G\xE9n\xE9ration d'une question dynamique...",
|
|
2880
|
+
translating: "Traduction dynamique de la question...",
|
|
2881
|
+
prompt_answer: "Votre r\xE9ponse (Entr\xE9e pour r\xE9v\xE9ler \xB7 'q' pour quitter) :",
|
|
2882
|
+
session_ended: "Session d'apprentissage arr\xEAt\xE9e.",
|
|
2883
|
+
session_complete: "Session d'apprentissage termin\xE9e !",
|
|
2884
|
+
cards_rated: " Cartes \xE9valu\xE9es: {count}",
|
|
2885
|
+
avg_rating: " Note moyenne: {avg}",
|
|
2886
|
+
forgot: " Oubli\xE9es: {count} carte(s)",
|
|
2887
|
+
feedback_title: "\u2500\u2500 Commentaires ZAM {line}",
|
|
2888
|
+
answer_title: "\u2500\u2500 R\xE9ponse {line}",
|
|
2889
|
+
keep_waiting: "Voulez-vous continuer \xE0 attendre l'\xE9valuation ?",
|
|
2890
|
+
local_ai_working: "L'IA locale est toujours en train de g\xE9n\xE9rer la r\xE9ponse.",
|
|
2891
|
+
wait_warning: "\u26A0 Le serveur LLM prend du temps pour charger le mod\xE8le.",
|
|
2892
|
+
wait_info: "(Ceci est normal lors de la transition entre mod\xE8les ou du d\xE9marrage \xE0 froid.)",
|
|
2893
|
+
keep_waiting_llm: "Voulez-vous continuer \xE0 attendre le mod\xE8le ?",
|
|
2894
|
+
proceeding_offline: "\u26A0 Poursuite en mode hors ligne (sans \xE9valuation active de l'IA pour cette session).",
|
|
2895
|
+
eval_skipped: " [\xC9valuation LLM ignor\xE9e : {reason}]"
|
|
2896
|
+
},
|
|
2897
|
+
pt: {
|
|
2898
|
+
welcome: "Sess\xE3o de aprendizado: {count} cart\xE3o(\xF5es)",
|
|
2899
|
+
new_review_relearn: " Novos: {newC} Revisar: {reviewC} Reaprender: {relearnC}",
|
|
2900
|
+
domains: " Dom\xEDnios: {domains}",
|
|
2901
|
+
instruction: "\nLembre-se de cada resposta primeiro, revele-a e avalie-se honestamente.",
|
|
2902
|
+
quit_hint: "Digite 'q' (ou Ctrl+C) para parar a qualquer momento.",
|
|
2903
|
+
offline_warning: "\n\x1B[33m\u26A0 O feedback do LLM e a tradu\xE7\xE3o autom\xE1tica est\xE3o desativados.\x1B[0m",
|
|
2904
|
+
offline_instruction: " Ativar com: \x1B[36mnpm run dev -- settings llm on\x1B[0m\n",
|
|
2905
|
+
nothing_due: "Nada faturado para aprender. Voc\xEA est\xE1 atualizado!",
|
|
2906
|
+
evaluating: "Avaliando a resposta via IA local...",
|
|
2907
|
+
generating_question: "Gerando pergunta din\xE2mica...",
|
|
2908
|
+
translating: "Traduzindo pergunta dinamicamente...",
|
|
2909
|
+
prompt_answer: "Sua resposta (Enter para revelar \xB7 'q' para parar):",
|
|
2910
|
+
session_ended: "Sess\xE3o de aprendizado encerrada.",
|
|
2911
|
+
session_complete: "Sess\xE3o de aprendizado conclu\xEDda!",
|
|
2912
|
+
cards_rated: " Cart\xF5es avaliados: {count}",
|
|
2913
|
+
avg_rating: " Nota m\xE9dia: {avg}",
|
|
2914
|
+
forgot: " Esquecidos: {count} cart\xE3o(\xF5es)",
|
|
2915
|
+
feedback_title: "\u2500\u2500 Feedback ZAM {line}",
|
|
2916
|
+
answer_title: "\u2500\u2500 Resposta {line}",
|
|
2917
|
+
keep_waiting: "Deseja continuar esperando pela avalia\xE7\xE3o?",
|
|
2918
|
+
local_ai_working: "A IA local ainda est\xE1 gerando a resposta.",
|
|
2919
|
+
wait_warning: "\u26A0 O servidor LLM est\xE1 demorando para carregar o modelo.",
|
|
2920
|
+
wait_info: "(Isso \xE9 esperado ao alternar modelos ou iniciar do zero.)",
|
|
2921
|
+
keep_waiting_llm: "Deseja continuar esperando o modelo?",
|
|
2922
|
+
proceeding_offline: "\u26A0 Continuando no modo offline (sem avalia\xE7\xF5es de LLM ativas nesta sess\xE3o).",
|
|
2923
|
+
eval_skipped: " [Avalia\xE7\xE3o LLM omitida: {reason}]"
|
|
2924
|
+
},
|
|
2925
|
+
zh: {
|
|
2926
|
+
welcome: "\u5B66\u4E60\u8BFE: {count} \u5F20\u5361\u7247",
|
|
2927
|
+
new_review_relearn: " \u65B0\u5361: {newC} \u590D\u4E60: {reviewC} \u91CD\u5B66: {relearnC}",
|
|
2928
|
+
domains: " \u77E5\u8BC6\u9886\u57DF: {domains}",
|
|
2929
|
+
instruction: "\n\u9996\u5148\u5728\u8111\u4E2D\u56DE\u5FC6\u7B54\u6848\uFF0C\u7136\u540E\u63ED\u6653\u5E76\u8BDA\u5B9E\u81EA\u6211\u8BC4\u5206\u3002",
|
|
2930
|
+
quit_hint: "\u5728\u56DE\u7B54\u63D0\u793A\u5904\u8F93\u5165 'q' (\u6216\u6309 Ctrl+C) \u53EF\u968F\u65F6\u9000\u51FA\u3002",
|
|
2931
|
+
offline_warning: "\n\x1B[33m\u26A0 LLM \u53CD\u9988\u4E0E\u81EA\u52A8\u7FFB\u8BD1\u5DF2\u7981\u7528\u3002\x1B[0m",
|
|
2932
|
+
offline_instruction: " \u5F00\u542F\u547D\u4EE4: \x1B[36mnpm run dev -- settings llm on\x1B[0m\n",
|
|
2933
|
+
nothing_due: "\u76EE\u524D\u6CA1\u6709\u9700\u8981\u5B66\u4E60\u7684\u5185\u5BB9\u3002\u60A8\u5DF2\u5168\u90E8\u638C\u63E1\uFF01",
|
|
2934
|
+
evaluating: "\u6B63\u5728\u901A\u8FC7\u672C\u5730 AI \u8BC4\u4F30\u56DE\u7B54...",
|
|
2935
|
+
generating_question: "\u6B63\u5728\u52A8\u6001\u751F\u6210\u95EE\u9898...",
|
|
2936
|
+
translating: "\u6B63\u5728\u52A8\u6001\u7FFB\u8BD1\u95EE\u9898...",
|
|
2937
|
+
prompt_answer: "\u60A8\u7684\u56DE\u7B54 (\u6309\u56DE\u8F66\u63ED\u6653 \xB7 \u8F93\u5165 'q' \u9000\u51FA):",
|
|
2938
|
+
session_ended: "\u5B66\u4E60\u8BFE\u5DF2\u7ED3\u675F\u3002",
|
|
2939
|
+
session_complete: "\u5B66\u4E60\u8BFE\u5DF2\u5B8C\u6210\uFF01",
|
|
2940
|
+
cards_rated: " \u5DF2\u8BC4\u5206\u5361\u7247: {count}",
|
|
2941
|
+
avg_rating: " \u5E73\u5747\u5206: {avg}",
|
|
2942
|
+
forgot: " \u9057\u5FD8: {count} \u5F20\u5361\u7247",
|
|
2943
|
+
feedback_title: "\u2500\u2500 ZAM \u53CD\u9988 {line}",
|
|
2944
|
+
answer_title: "\u2500\u2500 \u53C2\u8003\u7B54\u6848 {line}",
|
|
2945
|
+
keep_waiting: "\u662F\u5426\u7EE7\u7EED\u7B49\u5F85\u8BC4\u5206\uFF1F",
|
|
2946
|
+
local_ai_working: "\u672C\u5730 AI \u4ECD\u5728\u751F\u6210\u56DE\u7B54\u3002",
|
|
2947
|
+
wait_warning: "\u26A0 LLM \u670D\u52A1\u5668\u6B63\u5728\u52A0\u8F7D\u6A21\u578B\uFF0C\u8FD9\u53EF\u80FD\u9700\u8981\u4E00\u4E9B\u65F6\u95F4\u3002",
|
|
2948
|
+
wait_info: "(\u8FD9\u5728\u5207\u6362\u6A21\u578B\u6216\u51B7\u542F\u52A8\u65F6\u662F\u6B63\u5E38\u73B0\u8C61\u3002)",
|
|
2949
|
+
keep_waiting_llm: "\u662F\u5426\u7EE7\u7EED\u7B49\u5F85\u6A21\u578B\u52A0\u8F7D\uFF1F",
|
|
2950
|
+
proceeding_offline: "\u26A0 \u6B63\u5728\u4EE5\u79BB\u7EBF\u6A21\u5F0F\u7EE7\u7EED\uFF08\u672C\u6B21\u5B66\u4E60\u8BFE\u5C06\u4E0D\u5305\u542B\u6D3B\u8DC3\u7684 AI \u8BC4\u4F30\uFF09\u3002",
|
|
2951
|
+
eval_skipped: " [\u5DF2\u8DF3\u8FC7 LLM \u8BC4\u4F30: {reason}]"
|
|
2952
|
+
},
|
|
2953
|
+
ja: {
|
|
2954
|
+
welcome: "\u5B66\u7FD2\u30BB\u30C3\u30B7\u30E7\u30F3: {count} \u679A\u306E\u30AB\u30FC\u30C9",
|
|
2955
|
+
new_review_relearn: " \u65B0\u898F: {newC} \u5FA9\u7FD2: {reviewC} \u518D\u5B66\u7FD2: {relearnC}",
|
|
2956
|
+
domains: " \u30C9\u30E1\u30A4\u30F3: {domains}",
|
|
2957
|
+
instruction: "\n\u6700\u521D\u306B\u56DE\u7B54\u3092\u601D\u3044\u51FA\u3057\u3001\u6B21\u306B\u56DE\u7B54\u3092\u8868\u793A\u3057\u3066\u3001\u6B63\u76F4\u306B\u81EA\u5DF1\u8A55\u4FA1\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
|
|
2958
|
+
quit_hint: "\u56DE\u7B54\u30D7\u30ED\u30F3\u30D7\u30C8\u3067\u300Cq\u300D\u3092\u5165\u529B\u3059\u308B\uFF08\u307E\u305F\u306F Ctrl+C \u3092\u62BC\u3059\uFF09\u3068\u3001\u3044\u3064\u3067\u3082\u7D42\u4E86\u3067\u304D\u307E\u3059\u3002",
|
|
2959
|
+
offline_warning: "\n\x1B[33m\u26A0 LLM \u30D5\u30A3\u30FC\u30C9\u30D0\u30C3\u30AF\u3068\u81EA\u52D5\u7FFB\u8A33\u306F\u7121\u52B9\u3067\u3059\u3002\x1B[0m",
|
|
2960
|
+
offline_instruction: " \u6709\u52B9\u5316\u3059\u308B\u306B\u306F: \x1B[36mnpm run dev -- settings llm on\x1B[0m\n",
|
|
2961
|
+
nothing_due: "\u5B66\u7FD2\u4E88\u5B9A\u306E\u30AB\u30FC\u30C9\u306F\u3042\u308A\u307E\u305B\u3093\u3002\u3059\u3079\u3066\u5B8C\u4E86\u3057\u3066\u3044\u307E\u3059\uFF01",
|
|
2962
|
+
evaluating: "\u30ED\u30FC\u30AB\u30EBAI\u306B\u3088\u308B\u56DE\u7B54\u306E\u8A55\u4FA1\u4E2D...",
|
|
2963
|
+
generating_question: "\u8CEA\u554F\u3092\u52D5\u7684\u306B\u751F\u6210\u4E2D...",
|
|
2964
|
+
translating: "\u8CEA\u554F\u3092\u52D5\u7684\u306B\u7FFB\u8A33\u4E2D...",
|
|
2965
|
+
prompt_answer: "\u3042\u306A\u305F\u306E\u56DE\u7B54 (Enter\u3067\u8868\u793A \xB7 'q'\u3067\u7D42\u4E86):",
|
|
2966
|
+
session_ended: "\u5B66\u7FD2\u30BB\u30C3\u30B7\u30E7\u30F3\u304C\u7D42\u4E86\u3057\u307E\u3057\u305F\u3002",
|
|
2967
|
+
session_complete: "\u5B66\u7FD2\u30BB\u30C3\u30B7\u30E7\u30F3\u304C\u5B8C\u4E86\u3057\u307E\u3057\u305F\uFF01",
|
|
2968
|
+
cards_rated: " \u8A55\u4FA1\u6E08\u307F\u30AB\u30FC\u30C9\u6570: {count}",
|
|
2969
|
+
avg_rating: " \u5E73\u5747\u8A55\u4FA1: {avg}",
|
|
2970
|
+
forgot: " \u5FD8\u308C\u305F\u30AB\u30FC\u30C9\u6570: {count} \u679A",
|
|
2971
|
+
feedback_title: "\u2500\u2500 ZAM \u30D5\u30A3\u30FC\u30C9\u30D0\u30C3\u30AF {line}",
|
|
2972
|
+
answer_title: "\u2500\u2500 \u89E3\u7B54 {line}",
|
|
2973
|
+
keep_waiting: "\u8A55\u4FA1\u306E\u751F\u6210\u3092\u5F85\u3061\u307E\u3059\u304B\uFF1F",
|
|
2974
|
+
local_ai_working: "\u30ED\u30FC\u30AB\u30EBAI\u304C\u56DE\u7B54\u3092\u751F\u6210\u3057\u3066\u3044\u307E\u3059\u3002",
|
|
2975
|
+
wait_warning: "\u26A0 LLM \u30B5\u30FC\u30D0\u30FC\u304C\u30E2\u30C7\u30EB\u3092\u30ED\u30FC\u30C9\u3059\u308B\u306E\u306B\u6642\u9593\u304C\u304B\u304B\u3063\u3066\u3044\u307E\u3059\u3002",
|
|
2976
|
+
wait_info: "(\u30E2\u30C7\u30EB\u306E\u79FB\u884C\u4E2D\u3084\u30B3\u30FC\u30EB\u30C9\u30B9\u30BF\u30FC\u30C8\u6642\u306B\u306F\u3001\u3053\u308C\u304C\u4E88\u60F3\u3055\u308C\u307E\u3059\u3002)",
|
|
2977
|
+
keep_waiting_llm: "\u30E2\u30C7\u30EB\u306E\u30ED\u30FC\u30C9\u3092\u5F85\u3061\u7D9A\u3051\u307E\u3059\u304B\uFF1F",
|
|
2978
|
+
proceeding_offline: "\u26A0 \u30AA\u30D5\u30E9\u30A4\u30F3\u30E2\u30FC\u30C9\u3067\u7D9A\u884C\u3057\u307E\u3059\uFF08\u3053\u306E\u30BB\u30C3\u30B7\u30E7\u30F3\u3067\u306F\u30A2\u30AF\u30C6\u30A3\u30D6\u306A AI \u8A55\u4FA1\u306F\u884C\u308F\u308C\u307E\u305B\u3093\uFF09\u3002",
|
|
2979
|
+
eval_skipped: " [LLM \u8A55\u4FA1\u304C\u30B9\u30AD\u30C3\u30D7\u3055\u308C\u307E\u3057\u305F: {reason}]"
|
|
1612
2980
|
}
|
|
1613
|
-
|
|
1614
|
-
}
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
}
|
|
1620
|
-
|
|
1621
|
-
function describeSequence(steps) {
|
|
1622
|
-
return `Recurring pattern: ${steps.join(" \u2192 ")}`;
|
|
2981
|
+
};
|
|
2982
|
+
function t(locale, key, params = {}) {
|
|
2983
|
+
const dict = TRANSLATIONS[locale] || TRANSLATIONS.en;
|
|
2984
|
+
let str = dict[key] || TRANSLATIONS.en[key] || "";
|
|
2985
|
+
for (const [k, v] of Object.entries(params)) {
|
|
2986
|
+
str = str.replace(new RegExp(`{${k}}`, "g"), String(v));
|
|
2987
|
+
}
|
|
2988
|
+
return str;
|
|
1623
2989
|
}
|
|
1624
2990
|
|
|
1625
|
-
// src/kernel/
|
|
1626
|
-
import {
|
|
1627
|
-
import {
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
function
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
}
|
|
1646
|
-
function serializeGoal(goal) {
|
|
1647
|
-
const lines = [
|
|
1648
|
-
"---",
|
|
1649
|
-
`title: ${goal.title}`,
|
|
1650
|
-
`status: ${goal.status}`
|
|
1651
|
-
];
|
|
1652
|
-
if (goal.parent) {
|
|
1653
|
-
lines.push(`parent: ${goal.parent}`);
|
|
2991
|
+
// src/kernel/system/installer.ts
|
|
2992
|
+
import { execSync } from "child_process";
|
|
2993
|
+
import { existsSync as existsSync7 } from "fs";
|
|
2994
|
+
import { homedir as homedir5 } from "os";
|
|
2995
|
+
import { join as join7 } from "path";
|
|
2996
|
+
function hasCommand(cmd) {
|
|
2997
|
+
try {
|
|
2998
|
+
const checkCmd = process.platform === "win32" ? `where ${cmd}` : `which ${cmd}`;
|
|
2999
|
+
execSync(checkCmd, { stdio: "ignore" });
|
|
3000
|
+
return true;
|
|
3001
|
+
} catch {
|
|
3002
|
+
return false;
|
|
3003
|
+
}
|
|
3004
|
+
}
|
|
3005
|
+
function installFastFlowLM() {
|
|
3006
|
+
if (process.platform !== "win32") {
|
|
3007
|
+
return {
|
|
3008
|
+
success: false,
|
|
3009
|
+
message: "FastFlowLM is only supported on Windows."
|
|
3010
|
+
};
|
|
1654
3011
|
}
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
lines.push("");
|
|
1659
|
-
if (goal.body.trim()) {
|
|
1660
|
-
lines.push(goal.body.trim());
|
|
1661
|
-
lines.push("");
|
|
3012
|
+
const hasFlm = hasCommand("flm") || existsSync7("C:\\Program Files\\flm\\flm.exe");
|
|
3013
|
+
if (hasFlm) {
|
|
3014
|
+
return { success: true, message: "FastFlowLM is already installed." };
|
|
1662
3015
|
}
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
let match;
|
|
1669
|
-
while ((match = taskRegex.exec(body)) !== null) {
|
|
1670
|
-
tasks.push({
|
|
1671
|
-
done: match[1] !== " ",
|
|
1672
|
-
text: match[2].trim()
|
|
1673
|
-
});
|
|
3016
|
+
if (!hasCommand("winget")) {
|
|
3017
|
+
return {
|
|
3018
|
+
success: false,
|
|
3019
|
+
message: "winget package manager was not found on this system."
|
|
3020
|
+
};
|
|
1674
3021
|
}
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
}
|
|
3022
|
+
console.log("Installing FastFlowLM via winget...");
|
|
3023
|
+
try {
|
|
3024
|
+
execSync(
|
|
3025
|
+
"winget install -e --id FastFlowLM --accept-source-agreements --accept-package-agreements",
|
|
3026
|
+
{ stdio: "inherit" }
|
|
3027
|
+
);
|
|
3028
|
+
return { success: true, message: "FastFlowLM installed successfully." };
|
|
3029
|
+
} catch (err) {
|
|
3030
|
+
return {
|
|
3031
|
+
success: false,
|
|
3032
|
+
message: `Failed to install FastFlowLM: ${err.message}`
|
|
3033
|
+
};
|
|
1687
3034
|
}
|
|
1688
|
-
return refs;
|
|
1689
3035
|
}
|
|
1690
|
-
function
|
|
1691
|
-
const
|
|
1692
|
-
|
|
1693
|
-
|
|
3036
|
+
function installOllama() {
|
|
3037
|
+
const isMac = process.platform === "darwin";
|
|
3038
|
+
const isWin = process.platform === "win32";
|
|
3039
|
+
const hasOllama = hasCommand("ollama") || isMac && existsSync7("/Applications/Ollama.app") || isWin && existsSync7(
|
|
3040
|
+
join7(homedir5(), "AppData", "Local", "Programs", "Ollama", "ollama.exe")
|
|
3041
|
+
);
|
|
3042
|
+
if (hasOllama) {
|
|
3043
|
+
return { success: true, message: "Ollama is already installed." };
|
|
1694
3044
|
}
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
3045
|
+
if (process.platform === "darwin") {
|
|
3046
|
+
if (!hasCommand("brew")) {
|
|
3047
|
+
return {
|
|
3048
|
+
success: false,
|
|
3049
|
+
message: "Homebrew was not found. Please install Homebrew from brew.sh first."
|
|
3050
|
+
};
|
|
3051
|
+
}
|
|
3052
|
+
console.log("Installing Ollama via Homebrew Cask...");
|
|
3053
|
+
try {
|
|
3054
|
+
execSync("brew install --cask ollama", { stdio: "inherit" });
|
|
3055
|
+
return { success: true, message: "Ollama installed successfully." };
|
|
3056
|
+
} catch (err) {
|
|
3057
|
+
return {
|
|
3058
|
+
success: false,
|
|
3059
|
+
message: `Failed to install Ollama: ${err.message}`
|
|
3060
|
+
};
|
|
3061
|
+
}
|
|
3062
|
+
} else if (process.platform === "win32") {
|
|
3063
|
+
if (!hasCommand("winget")) {
|
|
3064
|
+
return {
|
|
3065
|
+
success: false,
|
|
3066
|
+
message: "winget was not found. Please install winget first."
|
|
3067
|
+
};
|
|
3068
|
+
}
|
|
3069
|
+
console.log("Installing Ollama via winget...");
|
|
3070
|
+
try {
|
|
3071
|
+
execSync(
|
|
3072
|
+
"winget install -e --id Ollama.Ollama --accept-source-agreements --accept-package-agreements",
|
|
3073
|
+
{ stdio: "inherit" }
|
|
3074
|
+
);
|
|
3075
|
+
return { success: true, message: "Ollama installed successfully." };
|
|
3076
|
+
} catch (err) {
|
|
3077
|
+
return {
|
|
3078
|
+
success: false,
|
|
3079
|
+
message: `Failed to install Ollama: ${err.message}`
|
|
3080
|
+
};
|
|
3081
|
+
}
|
|
3082
|
+
} else {
|
|
3083
|
+
console.log("Installing Ollama via official installer script...");
|
|
3084
|
+
try {
|
|
3085
|
+
execSync("curl -fsSL https://ollama.com/install.sh | sh", {
|
|
3086
|
+
stdio: "inherit"
|
|
3087
|
+
});
|
|
3088
|
+
return { success: true, message: "Ollama installed successfully." };
|
|
3089
|
+
} catch (err) {
|
|
3090
|
+
return {
|
|
3091
|
+
success: false,
|
|
3092
|
+
message: `Failed to install Ollama: ${err.message}`
|
|
3093
|
+
};
|
|
3094
|
+
}
|
|
1698
3095
|
}
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
3096
|
+
}
|
|
3097
|
+
|
|
3098
|
+
// src/kernel/system/locale.ts
|
|
3099
|
+
import { execSync as execSync2 } from "child_process";
|
|
3100
|
+
var SUPPORTED_LOCALES = /* @__PURE__ */ new Set([
|
|
3101
|
+
"en",
|
|
3102
|
+
"de",
|
|
3103
|
+
"es",
|
|
3104
|
+
"fr",
|
|
3105
|
+
"pt",
|
|
3106
|
+
"zh",
|
|
3107
|
+
"ja"
|
|
3108
|
+
]);
|
|
3109
|
+
function normalizeLocale(raw) {
|
|
3110
|
+
const clean = raw.trim().toLowerCase().split(/[_-]/)[0];
|
|
3111
|
+
if (SUPPORTED_LOCALES.has(clean)) {
|
|
3112
|
+
return clean;
|
|
3113
|
+
}
|
|
3114
|
+
return "en";
|
|
3115
|
+
}
|
|
3116
|
+
function detectSystemLocale() {
|
|
3117
|
+
try {
|
|
3118
|
+
const envVars = [
|
|
3119
|
+
process.env.LANG,
|
|
3120
|
+
process.env.LANGUAGE,
|
|
3121
|
+
process.env.LC_ALL,
|
|
3122
|
+
process.env.LC_MESSAGES
|
|
3123
|
+
];
|
|
3124
|
+
for (const val of envVars) {
|
|
3125
|
+
if (val && val.trim().length > 0) {
|
|
3126
|
+
return normalizeLocale(val);
|
|
3127
|
+
}
|
|
1709
3128
|
}
|
|
3129
|
+
if (process.platform === "win32") {
|
|
3130
|
+
const output = execSync2(
|
|
3131
|
+
'powershell -NoProfile -Command "[System.Globalization.CultureInfo]::CurrentCulture.Name"',
|
|
3132
|
+
{ stdio: "pipe", encoding: "utf8", timeout: 2e3 }
|
|
3133
|
+
).trim();
|
|
3134
|
+
if (output && output.length > 0) {
|
|
3135
|
+
return normalizeLocale(output);
|
|
3136
|
+
}
|
|
3137
|
+
}
|
|
3138
|
+
} catch {
|
|
1710
3139
|
}
|
|
1711
|
-
return
|
|
3140
|
+
return "en";
|
|
1712
3141
|
}
|
|
1713
3142
|
|
|
1714
|
-
// src/kernel/
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
(
|
|
3143
|
+
// src/kernel/system/profiler.ts
|
|
3144
|
+
import { execSync as execSync3 } from "child_process";
|
|
3145
|
+
function runCommand(cmd) {
|
|
3146
|
+
try {
|
|
3147
|
+
return execSync3(cmd, { stdio: "pipe", encoding: "utf8" }).trim();
|
|
3148
|
+
} catch {
|
|
3149
|
+
return "";
|
|
3150
|
+
}
|
|
3151
|
+
}
|
|
3152
|
+
function detectWindowsAMDIPU() {
|
|
3153
|
+
if (process.platform !== "win32") return false;
|
|
3154
|
+
const cmd = `powershell -NoProfile -Command "Get-CimInstance Win32_PnPEntity | Where-Object { $_.Name -like '*AMD IPU*' -or $_.Name -like '*AMD NPU*' -or $_.Name -like '*NPU Compute*' -or $_.Name -like '*Ryzen AI*' -or $_.HardwareID -like '*VEN_1022&DEV_1502*' -or $_.HardwareID -like '*VEN_1022&DEV_17F0*' } | Select-Object -First 1 -ExpandProperty Name"`;
|
|
3155
|
+
const output = runCommand(cmd);
|
|
3156
|
+
return Boolean(
|
|
3157
|
+
output && (output.toLowerCase().includes("amd") || output.toLowerCase().includes("ipu") || output.toLowerCase().includes("npu") || output.toLowerCase().includes("ryzen"))
|
|
1719
3158
|
);
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
3159
|
+
}
|
|
3160
|
+
function getSystemProfile() {
|
|
3161
|
+
const platform = process.platform;
|
|
3162
|
+
const archStr = process.arch;
|
|
3163
|
+
let os = "unknown";
|
|
3164
|
+
if (platform === "win32") os = "windows";
|
|
3165
|
+
else if (platform === "darwin") os = "macos";
|
|
3166
|
+
else if (platform === "linux") os = "linux";
|
|
3167
|
+
let arch = "unknown";
|
|
3168
|
+
if (archStr === "x64") arch = "x64";
|
|
3169
|
+
else if (archStr === "arm64") arch = "arm64";
|
|
3170
|
+
const hasRyzenNPU = os === "windows" && detectWindowsAMDIPU();
|
|
3171
|
+
const hasAppleSilicon = os === "macos" && arch === "arm64";
|
|
3172
|
+
let recommendedRunner = "generic";
|
|
3173
|
+
let recommendedModel = "qwen3.5:4b";
|
|
3174
|
+
if (hasRyzenNPU) {
|
|
3175
|
+
recommendedRunner = "fastflowlm";
|
|
3176
|
+
recommendedModel = "qwen3.5:4b";
|
|
3177
|
+
} else if (hasAppleSilicon) {
|
|
3178
|
+
recommendedRunner = "ollama";
|
|
3179
|
+
recommendedModel = "llama3.2:3b";
|
|
3180
|
+
} else if (os === "macos" || os === "linux" || os === "windows") {
|
|
3181
|
+
recommendedRunner = "ollama";
|
|
3182
|
+
recommendedModel = "llama3.2:3b";
|
|
1737
3183
|
}
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
3184
|
+
return {
|
|
3185
|
+
os,
|
|
3186
|
+
arch,
|
|
3187
|
+
hasRyzenNPU,
|
|
3188
|
+
hasAppleSilicon,
|
|
3189
|
+
recommendedRunner,
|
|
3190
|
+
recommendedModel
|
|
1743
3191
|
};
|
|
1744
|
-
summaries.sort((a, b) => {
|
|
1745
|
-
const statusDiff = statusOrder[a.status] - statusOrder[b.status];
|
|
1746
|
-
if (statusDiff !== 0) return statusDiff;
|
|
1747
|
-
return a.title.localeCompare(b.title);
|
|
1748
|
-
});
|
|
1749
|
-
return summaries;
|
|
1750
|
-
}
|
|
1751
|
-
function getGoal(goalsDir, slug) {
|
|
1752
|
-
const filePath = join3(goalsDir, `${slug}.md`);
|
|
1753
|
-
if (!existsSync3(filePath)) return void 0;
|
|
1754
|
-
const content = readFileSync2(filePath, "utf-8");
|
|
1755
|
-
return parseGoalFile(content, slug, filePath);
|
|
1756
3192
|
}
|
|
1757
|
-
function createGoal(goalsDir, input) {
|
|
1758
|
-
const filePath = join3(goalsDir, `${input.slug}.md`);
|
|
1759
|
-
if (existsSync3(filePath)) {
|
|
1760
|
-
throw new Error(`Goal already exists: ${input.slug}`);
|
|
1761
|
-
}
|
|
1762
|
-
const now = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
1763
|
-
const goal = {
|
|
1764
|
-
slug: input.slug,
|
|
1765
|
-
title: input.title,
|
|
1766
|
-
status: input.status ?? "active",
|
|
1767
|
-
parent: input.parent ?? null,
|
|
1768
|
-
created: now,
|
|
1769
|
-
updated: now,
|
|
1770
|
-
body: input.description ? `## Description
|
|
1771
|
-
${input.description}
|
|
1772
3193
|
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
3194
|
+
// src/kernel/system/repos.ts
|
|
3195
|
+
import { existsSync as existsSync8 } from "fs";
|
|
3196
|
+
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");
|
|
3201
|
+
return {
|
|
3202
|
+
personal: personalSetting ? resolve2(personalSetting) : null,
|
|
3203
|
+
team: teamSetting ? resolve2(teamSetting) : null,
|
|
3204
|
+
org: orgSetting ? resolve2(orgSetting) : null
|
|
1777
3205
|
};
|
|
1778
|
-
writeFileSync(filePath, serializeGoal(goal), "utf-8");
|
|
1779
|
-
return goal;
|
|
1780
3206
|
}
|
|
1781
|
-
function
|
|
1782
|
-
const
|
|
1783
|
-
|
|
1784
|
-
goal.status = status;
|
|
1785
|
-
goal.updated = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
1786
|
-
writeFileSync(goal.filePath, serializeGoal(goal), "utf-8");
|
|
1787
|
-
return goal;
|
|
3207
|
+
function resolveRepoPath(db, type) {
|
|
3208
|
+
const paths = getRepoPaths(db);
|
|
3209
|
+
return paths[type];
|
|
1788
3210
|
}
|
|
1789
|
-
function
|
|
1790
|
-
const
|
|
1791
|
-
const
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
if (g.parent && bySlug.has(g.parent)) {
|
|
1796
|
-
const list = children.get(g.parent) ?? [];
|
|
1797
|
-
list.push(g);
|
|
1798
|
-
children.set(g.parent, list);
|
|
1799
|
-
}
|
|
3211
|
+
function resolveAllBeliefPaths(db) {
|
|
3212
|
+
const paths = getRepoPaths(db);
|
|
3213
|
+
const dirs = [];
|
|
3214
|
+
if (paths.personal) {
|
|
3215
|
+
const personalDir = resolve2(paths.personal, "beliefs");
|
|
3216
|
+
if (existsSync8(personalDir)) dirs.push(personalDir);
|
|
1800
3217
|
}
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
}
|
|
3218
|
+
if (paths.team) {
|
|
3219
|
+
const teamDir = resolve2(paths.team, "beliefs");
|
|
3220
|
+
if (existsSync8(teamDir)) dirs.push(teamDir);
|
|
1805
3221
|
}
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
const orgUrl = getSetting(db, "ado.org_url");
|
|
1812
|
-
const project = getSetting(db, "ado.project");
|
|
1813
|
-
const pat = getSetting(db, "ado.pat");
|
|
1814
|
-
if (!orgUrl || !project || !pat) return null;
|
|
1815
|
-
return { orgUrl: orgUrl.replace(/\/+$/, ""), project, pat };
|
|
1816
|
-
}
|
|
1817
|
-
function authHeader(pat) {
|
|
1818
|
-
return `Basic ${Buffer.from(`:${pat}`).toString("base64")}`;
|
|
3222
|
+
if (paths.org) {
|
|
3223
|
+
const orgDir = resolve2(paths.org, "beliefs");
|
|
3224
|
+
if (existsSync8(orgDir)) dirs.push(orgDir);
|
|
3225
|
+
}
|
|
3226
|
+
return dirs;
|
|
1819
3227
|
}
|
|
1820
|
-
|
|
1821
|
-
const
|
|
1822
|
-
const
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
const wiqlRes = await fetch(wiqlUrl, {
|
|
1827
|
-
method: "POST",
|
|
1828
|
-
headers: {
|
|
1829
|
-
Authorization: authHeader(pat),
|
|
1830
|
-
"Content-Type": "application/json"
|
|
1831
|
-
},
|
|
1832
|
-
body: JSON.stringify(wiqlBody)
|
|
1833
|
-
});
|
|
1834
|
-
if (!wiqlRes.ok) {
|
|
1835
|
-
const text = await wiqlRes.text();
|
|
1836
|
-
throw new Error(`ADO WIQL query failed (${wiqlRes.status}): ${text}`);
|
|
3228
|
+
function resolveAllGoalPaths(db) {
|
|
3229
|
+
const paths = getRepoPaths(db);
|
|
3230
|
+
const dirs = [];
|
|
3231
|
+
if (paths.personal) {
|
|
3232
|
+
const personalDir = resolve2(paths.personal, "goals");
|
|
3233
|
+
if (existsSync8(personalDir)) dirs.push(personalDir);
|
|
1837
3234
|
}
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
const batchIds = ids.slice(0, 200);
|
|
1842
|
-
const fields = "System.Id,System.Title,System.State,System.WorkItemType,System.AssignedTo";
|
|
1843
|
-
const detailUrl = `${orgUrl}/${project}/_apis/wit/workitems?ids=${batchIds.join(",")}&fields=${fields}&api-version=7.1`;
|
|
1844
|
-
const detailRes = await fetch(detailUrl, {
|
|
1845
|
-
headers: { Authorization: authHeader(pat) }
|
|
1846
|
-
});
|
|
1847
|
-
if (!detailRes.ok) {
|
|
1848
|
-
const text = await detailRes.text();
|
|
1849
|
-
throw new Error(`ADO work items fetch failed (${detailRes.status}): ${text}`);
|
|
3235
|
+
if (paths.team) {
|
|
3236
|
+
const teamDir = resolve2(paths.team, "goals");
|
|
3237
|
+
if (existsSync8(teamDir)) dirs.push(teamDir);
|
|
1850
3238
|
}
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
type: wi.fields["System.WorkItemType"],
|
|
1857
|
-
assignedTo: wi.fields["System.AssignedTo"]?.displayName ?? ""
|
|
1858
|
-
}));
|
|
3239
|
+
if (paths.org) {
|
|
3240
|
+
const orgDir = resolve2(paths.org, "goals");
|
|
3241
|
+
if (existsSync8(orgDir)) dirs.push(orgDir);
|
|
3242
|
+
}
|
|
3243
|
+
return dirs;
|
|
1859
3244
|
}
|
|
1860
3245
|
export {
|
|
3246
|
+
DEFAULT_REVIEW_CONTEXT_MAX_CHARS,
|
|
1861
3247
|
addPrerequisite,
|
|
1862
3248
|
analyzeObservation,
|
|
1863
3249
|
buildReviewQueue,
|
|
1864
3250
|
cascadeBlock,
|
|
3251
|
+
clearADOCredentials,
|
|
3252
|
+
clearTursoCredentials,
|
|
1865
3253
|
createAgentSkill,
|
|
1866
3254
|
createFSRS,
|
|
1867
3255
|
createGoal,
|
|
1868
3256
|
createToken,
|
|
3257
|
+
deleteCardForUser,
|
|
1869
3258
|
deleteSetting,
|
|
3259
|
+
deleteToken,
|
|
1870
3260
|
deprecateToken,
|
|
3261
|
+
detectSystemLocale,
|
|
1871
3262
|
discoverSkills,
|
|
3263
|
+
distributeGlobalSkills,
|
|
1872
3264
|
endSession,
|
|
1873
3265
|
ensureCard,
|
|
1874
3266
|
ensureMonitorDir,
|
|
1875
3267
|
evaluateRating,
|
|
3268
|
+
executeReviewAction,
|
|
1876
3269
|
extractTasks,
|
|
1877
3270
|
extractTokenRefs,
|
|
1878
3271
|
fetchActiveWorkItems,
|
|
1879
3272
|
findTokens,
|
|
1880
3273
|
generateBashHooks,
|
|
1881
3274
|
generateBashUnhooks,
|
|
3275
|
+
generateConceptFreeCue,
|
|
3276
|
+
generatePowerShellHooks,
|
|
3277
|
+
generatePowerShellUnhooks,
|
|
1882
3278
|
generatePrompt,
|
|
1883
3279
|
generateZshHooks,
|
|
1884
3280
|
generateZshUnhooks,
|
|
3281
|
+
getADOCredentials,
|
|
1885
3282
|
getAgentSkill,
|
|
1886
3283
|
getAllSettings,
|
|
1887
3284
|
getAllSettingsDetailed,
|
|
1888
3285
|
getBlockedCards,
|
|
1889
3286
|
getCard,
|
|
3287
|
+
getCardById,
|
|
3288
|
+
getCardDeletionImpact,
|
|
1890
3289
|
getDefaultDbPath,
|
|
1891
3290
|
getDependents,
|
|
1892
3291
|
getDomainCompetence,
|
|
@@ -1896,34 +3295,57 @@ export {
|
|
|
1896
3295
|
getMonitorDir,
|
|
1897
3296
|
getMonitorLogStats,
|
|
1898
3297
|
getMonitorPath,
|
|
3298
|
+
getPackageSkillPath,
|
|
1899
3299
|
getPrerequisites,
|
|
3300
|
+
getRepoPaths,
|
|
1900
3301
|
getReviewsForCard,
|
|
1901
3302
|
getReviewsForUser,
|
|
1902
3303
|
getSessionSummary,
|
|
1903
3304
|
getSetting,
|
|
3305
|
+
getSystemProfile,
|
|
1904
3306
|
getTokenById,
|
|
1905
3307
|
getTokenBySlug,
|
|
3308
|
+
getTokenDeleteImpact,
|
|
3309
|
+
getTursoCredentials,
|
|
1906
3310
|
getUserStats,
|
|
3311
|
+
hasCommand,
|
|
3312
|
+
injectShellHooks,
|
|
3313
|
+
installFastFlowLM,
|
|
3314
|
+
installOllama,
|
|
1907
3315
|
interleave,
|
|
1908
3316
|
listAgentSkills,
|
|
1909
3317
|
listGoals,
|
|
1910
3318
|
listTokens,
|
|
1911
3319
|
loadADOConfig,
|
|
3320
|
+
loadCredentials,
|
|
1912
3321
|
logReview,
|
|
1913
3322
|
logStep,
|
|
3323
|
+
matchesFilePath,
|
|
1914
3324
|
monitorLogExists,
|
|
3325
|
+
normalizeLocale,
|
|
3326
|
+
normalizePath,
|
|
1915
3327
|
openDatabase,
|
|
1916
3328
|
openDatabaseWithSync,
|
|
1917
3329
|
pairCommands,
|
|
1918
3330
|
parseGoalFile,
|
|
1919
3331
|
parseMonitorLog,
|
|
1920
3332
|
readMonitorLog,
|
|
3333
|
+
resolveAllBeliefPaths,
|
|
3334
|
+
resolveAllGoalPaths,
|
|
3335
|
+
resolveReference,
|
|
3336
|
+
resolveRepoPath,
|
|
3337
|
+
resolveReviewContext,
|
|
3338
|
+
saveCredentials,
|
|
1921
3339
|
serializeGoal,
|
|
3340
|
+
setADOCredentials,
|
|
1922
3341
|
setSetting,
|
|
3342
|
+
setTursoCredentials,
|
|
1923
3343
|
startSession,
|
|
3344
|
+
t,
|
|
1924
3345
|
unblockReady,
|
|
1925
3346
|
updateCard,
|
|
1926
3347
|
updateGoalStatus,
|
|
3348
|
+
updateToken,
|
|
1927
3349
|
writeMonitorEvent
|
|
1928
3350
|
};
|
|
1929
3351
|
//# sourceMappingURL=index.js.map
|