zam-core 0.3.3 → 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 +6367 -2467
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +823 -547
- package/dist/index.js +2738 -1322
- 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,58 +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;
|
|
373
|
+
const metaPath = `${dbPath}.meta`;
|
|
374
|
+
const infoPath = `${dbPath}-info`;
|
|
375
|
+
if (existsSync2(dbPath) && !existsSync2(metaPath) && !existsSync2(infoPath)) {
|
|
376
|
+
for (const suffix of ["", "-wal", "-shm"]) {
|
|
377
|
+
const f = `${dbPath}${suffix}`;
|
|
378
|
+
if (existsSync2(f)) rmSync(f, { force: true });
|
|
379
|
+
}
|
|
380
|
+
} else if (!existsSync2(dbPath) && (existsSync2(metaPath) || existsSync2(infoPath))) {
|
|
381
|
+
if (existsSync2(metaPath)) rmSync(metaPath);
|
|
382
|
+
if (existsSync2(infoPath)) rmSync(infoPath);
|
|
383
|
+
}
|
|
131
384
|
}
|
|
132
|
-
|
|
133
|
-
|
|
385
|
+
const authToken = configuredCloud?.token ?? options.authToken;
|
|
386
|
+
if (authToken) {
|
|
387
|
+
dbOpts.authToken = authToken;
|
|
134
388
|
}
|
|
135
389
|
let db;
|
|
136
390
|
try {
|
|
137
391
|
db = new Database(dbPath, dbOpts);
|
|
138
392
|
} catch (err) {
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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);
|
|
144
399
|
db = new Database(dbPath, dbOpts);
|
|
145
400
|
} else {
|
|
146
401
|
throw err;
|
|
147
402
|
}
|
|
148
403
|
}
|
|
149
|
-
|
|
404
|
+
if (!isRemote && !isEmbeddedReplica) {
|
|
405
|
+
db.pragma("journal_mode = WAL");
|
|
406
|
+
}
|
|
150
407
|
db.pragma("foreign_keys = ON");
|
|
151
|
-
|
|
408
|
+
if (!isRemote) {
|
|
409
|
+
db.pragma("busy_timeout = 5000");
|
|
410
|
+
}
|
|
411
|
+
if (isEmbeddedReplica) {
|
|
412
|
+
db.sync();
|
|
413
|
+
}
|
|
152
414
|
if (options.initialize) {
|
|
153
415
|
db.exec(SCHEMA);
|
|
154
416
|
}
|
|
155
417
|
runMigrations(db);
|
|
156
|
-
if (options.syncUrl) {
|
|
157
|
-
db.sync();
|
|
158
|
-
}
|
|
159
418
|
return db;
|
|
160
419
|
}
|
|
161
420
|
function openDatabaseWithSync(options = {}) {
|
|
162
|
-
|
|
163
|
-
const syncUrl = db.prepare("SELECT value FROM user_config WHERE key = ?").get("turso.url");
|
|
164
|
-
const authToken = db.prepare("SELECT value FROM user_config WHERE key = ?").get("turso.token");
|
|
165
|
-
if (!syncUrl || !authToken) return db;
|
|
166
|
-
db.pragma("wal_checkpoint(TRUNCATE)");
|
|
167
|
-
db.pragma("journal_mode = DELETE");
|
|
168
|
-
db.close();
|
|
169
|
-
return openDatabase({ ...options, syncUrl: syncUrl.value, authToken: authToken.value });
|
|
421
|
+
return openDatabase(options);
|
|
170
422
|
}
|
|
171
423
|
function getDefaultDbPath() {
|
|
172
424
|
return DEFAULT_DB_PATH;
|
|
@@ -182,6 +434,12 @@ function runMigrations(db) {
|
|
|
182
434
|
if (tokenCols.length > 0 && !tokenCols.some((c) => c.name === "deprecated_at")) {
|
|
183
435
|
db.exec(`ALTER TABLE tokens ADD COLUMN deprecated_at TEXT`);
|
|
184
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
|
+
}
|
|
185
443
|
db.exec(`
|
|
186
444
|
CREATE TABLE IF NOT EXISTS agent_skills (
|
|
187
445
|
id TEXT PRIMARY KEY,
|
|
@@ -196,111 +454,232 @@ function runMigrations(db) {
|
|
|
196
454
|
`);
|
|
197
455
|
}
|
|
198
456
|
|
|
199
|
-
// src/kernel/
|
|
200
|
-
import {
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
now,
|
|
220
|
-
now
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
}
|
|
224
|
-
function getTokenBySlug(db, slug) {
|
|
225
|
-
return db.prepare("SELECT * FROM tokens WHERE slug = ?").get(slug);
|
|
226
|
-
}
|
|
227
|
-
function getTokenById(db, id) {
|
|
228
|
-
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
|
+
};
|
|
229
482
|
}
|
|
230
|
-
function
|
|
231
|
-
const
|
|
232
|
-
if (
|
|
233
|
-
|
|
483
|
+
function serializeGoal(goal) {
|
|
484
|
+
const lines = ["---", `title: ${goal.title}`, `status: ${goal.status}`];
|
|
485
|
+
if (goal.parent) {
|
|
486
|
+
lines.push(`parent: ${goal.parent}`);
|
|
234
487
|
}
|
|
235
|
-
|
|
236
|
-
|
|
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("");
|
|
237
495
|
}
|
|
238
|
-
|
|
239
|
-
db.prepare("UPDATE tokens SET deprecated_at = ?, updated_at = ? WHERE slug = ?").run(
|
|
240
|
-
now,
|
|
241
|
-
now,
|
|
242
|
-
slug
|
|
243
|
-
);
|
|
244
|
-
return getTokenBySlug(db, slug);
|
|
496
|
+
return lines.join("\n");
|
|
245
497
|
}
|
|
246
|
-
function
|
|
247
|
-
const
|
|
248
|
-
const
|
|
249
|
-
|
|
250
|
-
)
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
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;
|
|
510
|
+
}
|
|
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]);
|
|
261
520
|
}
|
|
262
|
-
|
|
263
|
-
|
|
521
|
+
}
|
|
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;
|
|
264
543
|
}
|
|
265
544
|
}
|
|
266
|
-
|
|
267
|
-
return scored;
|
|
545
|
+
return { frontmatter, body };
|
|
268
546
|
}
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
547
|
+
|
|
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
|
+
});
|
|
274
571
|
}
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
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);
|
|
278
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}
|
|
279
606
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
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}`);
|
|
284
656
|
}
|
|
657
|
+
const id = ulid();
|
|
658
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
285
659
|
db.prepare(
|
|
286
|
-
|
|
287
|
-
|
|
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
|
+
);
|
|
288
675
|
}
|
|
289
|
-
function
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
FROM prerequisites p
|
|
293
|
-
JOIN tokens t ON t.id = p.requires_id
|
|
294
|
-
WHERE p.token_id = ?`
|
|
295
|
-
).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;
|
|
296
679
|
}
|
|
297
|
-
function
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
FROM prerequisites p
|
|
301
|
-
JOIN tokens t ON t.id = p.token_id
|
|
302
|
-
WHERE p.requires_id = ?`
|
|
303
|
-
).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);
|
|
304
683
|
}
|
|
305
684
|
|
|
306
685
|
// src/kernel/models/card.ts
|
|
@@ -319,6 +698,9 @@ function ensureCard(db, tokenId, userId) {
|
|
|
319
698
|
function getCard(db, tokenId, userId) {
|
|
320
699
|
return db.prepare("SELECT * FROM cards WHERE token_id = ? AND user_id = ?").get(tokenId, userId);
|
|
321
700
|
}
|
|
701
|
+
function getCardById(db, cardId) {
|
|
702
|
+
return db.prepare("SELECT * FROM cards WHERE id = ?").get(cardId);
|
|
703
|
+
}
|
|
322
704
|
function updateCard(db, cardId, updates) {
|
|
323
705
|
const fields = [];
|
|
324
706
|
const values = [];
|
|
@@ -372,6 +754,23 @@ function updateCard(db, cardId, updates) {
|
|
|
372
754
|
}
|
|
373
755
|
return db.prepare("SELECT * FROM cards WHERE id = ?").get(cardId);
|
|
374
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
|
+
}
|
|
375
774
|
function getDueCards(db, userId, now) {
|
|
376
775
|
const cutoff = now ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
377
776
|
return db.prepare(
|
|
@@ -392,6 +791,32 @@ function getBlockedCards(db, userId) {
|
|
|
392
791
|
).all(userId);
|
|
393
792
|
}
|
|
394
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
|
+
|
|
395
820
|
// src/kernel/models/review.ts
|
|
396
821
|
import { ulid as ulid3 } from "ulid";
|
|
397
822
|
function logReview(db, input) {
|
|
@@ -461,12 +886,17 @@ function endSession(db, sessionId) {
|
|
|
461
886
|
throw new Error(`Session already completed: ${sessionId}`);
|
|
462
887
|
}
|
|
463
888
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
464
|
-
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
|
+
);
|
|
465
893
|
return db.prepare("SELECT * FROM sessions WHERE id = ?").get(sessionId);
|
|
466
894
|
}
|
|
467
895
|
function logStep(db, input) {
|
|
468
896
|
if (input.done_by !== "user" && input.done_by !== "agent") {
|
|
469
|
-
throw new Error(
|
|
897
|
+
throw new Error(
|
|
898
|
+
`done_by must be 'user' or 'agent', got '${input.done_by}'`
|
|
899
|
+
);
|
|
470
900
|
}
|
|
471
901
|
if (input.rating != null && (input.rating < 1 || input.rating > 4)) {
|
|
472
902
|
throw new Error(`Rating must be between 1 and 4, got ${input.rating}`);
|
|
@@ -506,52 +936,10 @@ function getSessionSummary(db, sessionId) {
|
|
|
506
936
|
return { session, steps };
|
|
507
937
|
}
|
|
508
938
|
|
|
509
|
-
// src/kernel/models/
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
return
|
|
513
|
-
...row,
|
|
514
|
-
steps: JSON.parse(row.steps),
|
|
515
|
-
token_slugs: JSON.parse(row.token_slugs)
|
|
516
|
-
};
|
|
517
|
-
}
|
|
518
|
-
function createAgentSkill(db, input) {
|
|
519
|
-
const existing = db.prepare("SELECT * FROM agent_skills WHERE slug = ?").get(input.slug);
|
|
520
|
-
if (existing) {
|
|
521
|
-
throw new Error(`Agent skill already exists: ${input.slug}`);
|
|
522
|
-
}
|
|
523
|
-
const id = ulid5();
|
|
524
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
525
|
-
db.prepare(
|
|
526
|
-
`INSERT INTO agent_skills (id, slug, description, steps, token_slugs, source, created_at, updated_at)
|
|
527
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
|
|
528
|
-
).run(
|
|
529
|
-
id,
|
|
530
|
-
input.slug,
|
|
531
|
-
input.description,
|
|
532
|
-
JSON.stringify(input.steps),
|
|
533
|
-
JSON.stringify(input.token_slugs ?? []),
|
|
534
|
-
input.source ?? "learned",
|
|
535
|
-
now,
|
|
536
|
-
now
|
|
537
|
-
);
|
|
538
|
-
return parseRow(
|
|
539
|
-
db.prepare("SELECT * FROM agent_skills WHERE id = ?").get(id)
|
|
540
|
-
);
|
|
541
|
-
}
|
|
542
|
-
function getAgentSkill(db, slug) {
|
|
543
|
-
const row = db.prepare("SELECT * FROM agent_skills WHERE slug = ?").get(slug);
|
|
544
|
-
return row ? parseRow(row) : void 0;
|
|
545
|
-
}
|
|
546
|
-
function listAgentSkills(db) {
|
|
547
|
-
const rows = db.prepare("SELECT * FROM agent_skills ORDER BY created_at ASC").all();
|
|
548
|
-
return rows.map(parseRow);
|
|
549
|
-
}
|
|
550
|
-
|
|
551
|
-
// src/kernel/models/settings.ts
|
|
552
|
-
function getSetting(db, key) {
|
|
553
|
-
const row = db.prepare("SELECT value FROM user_config WHERE key = ?").get(key);
|
|
554
|
-
return row?.value;
|
|
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;
|
|
555
943
|
}
|
|
556
944
|
function getAllSettings(db) {
|
|
557
945
|
const rows = db.prepare("SELECT key, value FROM user_config ORDER BY key").all();
|
|
@@ -576,1323 +964,2328 @@ function deleteSetting(db, key) {
|
|
|
576
964
|
return result.changes > 0;
|
|
577
965
|
}
|
|
578
966
|
|
|
579
|
-
// src/kernel/
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
];
|
|
606
|
-
var DEFAULT_REQUEST_RETENTION = 0.9;
|
|
607
|
-
function clamp(value, lo, hi) {
|
|
608
|
-
return Math.min(hi, Math.max(lo, value));
|
|
609
|
-
}
|
|
610
|
-
function daysBetween(a, b) {
|
|
611
|
-
return (b.getTime() - a.getTime()) / (1e3 * 60 * 60 * 24);
|
|
612
|
-
}
|
|
613
|
-
function initialStability(w, rating) {
|
|
614
|
-
return w[rating - 1];
|
|
615
|
-
}
|
|
616
|
-
function initialDifficulty(w, rating) {
|
|
617
|
-
return clamp(w[4] - Math.exp(w[5] * (rating - 1)) + 1, 1, 10);
|
|
618
|
-
}
|
|
619
|
-
function nextDifficulty(w, d, rating) {
|
|
620
|
-
const d0ForGood = initialDifficulty(w, 3);
|
|
621
|
-
const updated = w[7] * d0ForGood + (1 - w[7]) * (d - w[6] * (rating - 3));
|
|
622
|
-
return clamp(updated, 1, 10);
|
|
623
|
-
}
|
|
624
|
-
function retrievability(elapsed, stability) {
|
|
625
|
-
if (stability <= 0) return 0;
|
|
626
|
-
return Math.pow(1 + elapsed / (9 * stability), -1);
|
|
627
|
-
}
|
|
628
|
-
function stabilityAfterSuccess(w, s, d, r, rating) {
|
|
629
|
-
const hardPenalty = rating === 2 ? w[15] : 1;
|
|
630
|
-
const easyBonus = rating === 4 ? w[16] : 1;
|
|
631
|
-
const inner = Math.exp(w[8]) * (11 - d) * Math.pow(s, -w[9]) * (Math.exp(w[10] * (1 - r)) - 1) * hardPenalty * easyBonus;
|
|
632
|
-
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);
|
|
633
993
|
}
|
|
634
|
-
function
|
|
635
|
-
return
|
|
994
|
+
function getTokenBySlug(db, slug) {
|
|
995
|
+
return db.prepare("SELECT * FROM tokens WHERE slug = ?").get(slug);
|
|
636
996
|
}
|
|
637
|
-
function
|
|
638
|
-
|
|
639
|
-
return Math.max(1, Math.round(interval));
|
|
997
|
+
function getTokenById(db, id) {
|
|
998
|
+
return db.prepare("SELECT * FROM tokens WHERE id = ?").get(id);
|
|
640
999
|
}
|
|
641
|
-
function
|
|
642
|
-
const
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
}
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
reps: rating >= 2 ? 1 : 0,
|
|
662
|
-
lapses: rating === 1 ? 1 : 0,
|
|
663
|
-
state: "learning",
|
|
664
|
-
dueAt: dueAt2,
|
|
665
|
-
lastReviewAt: reviewTime
|
|
666
|
-
};
|
|
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
|
+
);
|
|
667
1020
|
}
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
newState = "relearning";
|
|
680
|
-
} else {
|
|
681
|
-
newStability = stabilityAfterSuccess(w, card.stability, card.difficulty, r, rating);
|
|
682
|
-
newDifficulty = nextDifficulty(w, card.difficulty, rating);
|
|
683
|
-
newReps = card.reps + 1;
|
|
684
|
-
newLapses = card.lapses;
|
|
685
|
-
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}`);
|
|
686
1032
|
}
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
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}`);
|
|
701
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;
|
|
702
1087
|
return {
|
|
703
|
-
|
|
704
|
-
|
|
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
|
|
705
1095
|
};
|
|
706
1096
|
}
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
function cascadeBlock(db, userId, tokenSlug) {
|
|
710
|
-
const token = getTokenBySlug(db, tokenSlug);
|
|
1097
|
+
function deleteToken(db, slug) {
|
|
1098
|
+
const token = getTokenBySlug(db, slug);
|
|
711
1099
|
if (!token) {
|
|
712
|
-
throw new Error(`
|
|
1100
|
+
throw new Error(`Token not found: ${slug}`);
|
|
713
1101
|
}
|
|
714
|
-
|
|
715
|
-
db.
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
const prereqOfPrereq = db.prepare("SELECT COUNT(*) as n FROM prerequisites WHERE token_id = ?").get(prereq.requires_id);
|
|
724
|
-
if (prereqOfPrereq.n === 0) {
|
|
725
|
-
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) {
|
|
726
1111
|
db.prepare(
|
|
727
|
-
"UPDATE
|
|
728
|
-
).run(now,
|
|
1112
|
+
"UPDATE agent_skills SET token_slugs = ?, updated_at = ? WHERE id = ?"
|
|
1113
|
+
).run(JSON.stringify(filtered), now, row.id);
|
|
729
1114
|
}
|
|
730
1115
|
}
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
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;
|
|
736
1121
|
}
|
|
737
|
-
return {
|
|
738
|
-
blockedSlug: tokenSlug,
|
|
739
|
-
prerequisites: surfaced
|
|
740
|
-
};
|
|
1122
|
+
return { token, impact };
|
|
741
1123
|
}
|
|
742
|
-
function
|
|
743
|
-
const
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
const
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
const
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
"UPDATE cards SET blocked = 0, due_at = ? WHERE token_id = ? AND user_id = ?"
|
|
761
|
-
).run(now, card.token_id, userId);
|
|
762
|
-
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 });
|
|
763
1142
|
}
|
|
764
1143
|
}
|
|
765
|
-
|
|
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();
|
|
766
1156
|
}
|
|
767
1157
|
|
|
768
|
-
// src/kernel/
|
|
769
|
-
function
|
|
770
|
-
|
|
771
|
-
const
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
}
|
|
777
|
-
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 {
|
|
778
1167
|
}
|
|
779
1168
|
}
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
const
|
|
785
|
-
for (const
|
|
786
|
-
|
|
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
|
+
}
|
|
787
1197
|
}
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
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
|
|
793
1208
|
});
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
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);
|
|
810
1245
|
}
|
|
811
|
-
break;
|
|
812
1246
|
}
|
|
813
|
-
if (
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
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
|
+
}
|
|
824
1275
|
}
|
|
825
|
-
|
|
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++;
|
|
1283
|
+
}
|
|
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();
|
|
826
1309
|
}
|
|
827
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
|
+
});
|
|
828
1335
|
}
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
function buildReviewQueue(db, options) {
|
|
834
|
-
const maxNew = options.maxNew ?? 10;
|
|
835
|
-
const maxReviews = options.maxReviews ?? 50;
|
|
836
|
-
const now = options.now ?? /* @__PURE__ */ new Date();
|
|
837
|
-
const nowISO = now.toISOString();
|
|
838
|
-
const dueRows = db.prepare(
|
|
839
|
-
`SELECT
|
|
840
|
-
c.id AS card_id,
|
|
841
|
-
c.token_id AS token_id,
|
|
842
|
-
t.slug AS slug,
|
|
843
|
-
t.concept AS concept,
|
|
844
|
-
t.domain AS domain,
|
|
845
|
-
t.bloom_level AS bloom_level,
|
|
846
|
-
c.state AS state,
|
|
847
|
-
c.due_at AS due_at
|
|
848
|
-
FROM cards c
|
|
849
|
-
JOIN tokens t ON t.id = c.token_id
|
|
850
|
-
WHERE c.user_id = ?
|
|
851
|
-
AND c.blocked = 0
|
|
852
|
-
AND c.due_at <= ?
|
|
853
|
-
AND c.state IN ('review', 'relearning', 'learning')
|
|
854
|
-
AND t.deprecated_at IS NULL
|
|
855
|
-
ORDER BY c.due_at ASC`
|
|
856
|
-
).all(options.userId, nowISO);
|
|
857
|
-
const newRows = db.prepare(
|
|
858
|
-
`SELECT
|
|
859
|
-
c.id AS card_id,
|
|
860
|
-
c.token_id AS token_id,
|
|
861
|
-
t.slug AS slug,
|
|
862
|
-
t.concept AS concept,
|
|
863
|
-
t.domain AS domain,
|
|
864
|
-
t.bloom_level AS bloom_level,
|
|
865
|
-
c.state AS state,
|
|
866
|
-
c.due_at AS due_at
|
|
867
|
-
FROM cards c
|
|
868
|
-
JOIN tokens t ON t.id = c.token_id
|
|
869
|
-
WHERE c.user_id = ?
|
|
870
|
-
AND c.blocked = 0
|
|
871
|
-
AND c.state = 'new'
|
|
872
|
-
AND t.deprecated_at IS NULL
|
|
873
|
-
ORDER BY t.bloom_level ASC, t.slug ASC
|
|
874
|
-
LIMIT ?`
|
|
875
|
-
).all(options.userId, maxNew);
|
|
876
|
-
const nowMs = now.getTime();
|
|
877
|
-
const sortedDue = [...dueRows].sort((a, b) => {
|
|
878
|
-
const overdueA = nowMs - new Date(a.due_at).getTime();
|
|
879
|
-
const overdueB = nowMs - new Date(b.due_at).getTime();
|
|
880
|
-
return overdueB - overdueA;
|
|
881
|
-
});
|
|
882
|
-
const interleavedDue = interleave(
|
|
883
|
-
sortedDue.map((row) => ({ ...rowToItem(row), domain: row.domain }))
|
|
884
|
-
);
|
|
885
|
-
const newItems = newRows.map(rowToItem);
|
|
886
|
-
const merged = intersperseNew(interleavedDue, newItems, 5);
|
|
887
|
-
const capped = merged.slice(0, maxReviews);
|
|
888
|
-
let newCount = 0;
|
|
889
|
-
let reviewCount = 0;
|
|
890
|
-
let relearnCount = 0;
|
|
891
|
-
const domainSet = /* @__PURE__ */ new Set();
|
|
892
|
-
for (const item of capped) {
|
|
893
|
-
domainSet.add(item.domain);
|
|
894
|
-
switch (item.state) {
|
|
895
|
-
case "new":
|
|
896
|
-
newCount++;
|
|
897
|
-
break;
|
|
898
|
-
case "relearning":
|
|
899
|
-
relearnCount++;
|
|
900
|
-
break;
|
|
901
|
-
default:
|
|
902
|
-
reviewCount++;
|
|
903
|
-
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);
|
|
904
1340
|
}
|
|
905
1341
|
}
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
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 };
|
|
913
1354
|
}
|
|
914
|
-
function
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
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;
|
|
925
1376
|
}
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
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 });
|
|
945
1398
|
}
|
|
946
|
-
return result;
|
|
947
1399
|
}
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
3: "Apply",
|
|
954
|
-
4: "Analyze",
|
|
955
|
-
5: "Synthesize"
|
|
956
|
-
};
|
|
957
|
-
var BLOOM_PROMPTS = {
|
|
958
|
-
1: (c) => `What is: ${c}?`,
|
|
959
|
-
2: (c) => `Explain how this works: ${c}`,
|
|
960
|
-
3: (c) => `Apply this concept: ${c}`,
|
|
961
|
-
4: (c) => `Analyze the trade-offs: ${c}`,
|
|
962
|
-
5: (c) => `Design a solution using: ${c}`
|
|
963
|
-
};
|
|
964
|
-
function generatePrompt(input) {
|
|
965
|
-
const bloom = input.bloomLevel >= 1 && input.bloomLevel <= 5 ? input.bloomLevel : 1;
|
|
966
|
-
return {
|
|
967
|
-
cardId: input.cardId,
|
|
968
|
-
tokenId: input.tokenId,
|
|
969
|
-
slug: input.slug,
|
|
970
|
-
question: BLOOM_PROMPTS[bloom](input.concept),
|
|
971
|
-
concept: input.concept,
|
|
972
|
-
domain: input.domain,
|
|
973
|
-
bloomLevel: bloom,
|
|
974
|
-
bloomVerb: BLOOM_VERBS[bloom],
|
|
975
|
-
hints: []
|
|
976
|
-
};
|
|
1400
|
+
function writeMonitorEvent(sessionId, event) {
|
|
1401
|
+
ensureMonitorDir();
|
|
1402
|
+
const path = getMonitorPath(sessionId);
|
|
1403
|
+
appendFileSync(path, `${JSON.stringify(event)}
|
|
1404
|
+
`);
|
|
977
1405
|
}
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
const card = db.prepare("SELECT * FROM cards WHERE id = ?").get(input.cardId);
|
|
983
|
-
if (!card) {
|
|
984
|
-
throw new Error(`Card not found: ${input.cardId}`);
|
|
1406
|
+
function readMonitorLog(sessionId) {
|
|
1407
|
+
const path = getMonitorPath(sessionId);
|
|
1408
|
+
if (!existsSync4(path)) {
|
|
1409
|
+
return [];
|
|
985
1410
|
}
|
|
986
|
-
const
|
|
987
|
-
|
|
988
|
-
const schedulingCard = {
|
|
989
|
-
stability: card.stability,
|
|
990
|
-
difficulty: card.difficulty,
|
|
991
|
-
elapsedDays: card.elapsed_days,
|
|
992
|
-
scheduledDays: card.scheduled_days,
|
|
993
|
-
reps: card.reps,
|
|
994
|
-
lapses: card.lapses,
|
|
995
|
-
state: card.state,
|
|
996
|
-
dueAt: new Date(card.due_at),
|
|
997
|
-
lastReviewAt: card.last_review_at ? new Date(card.last_review_at) : null
|
|
998
|
-
};
|
|
999
|
-
const updated = fsrs.schedule(schedulingCard, input.rating, now);
|
|
1000
|
-
updateCard(db, input.cardId, {
|
|
1001
|
-
stability: updated.stability,
|
|
1002
|
-
difficulty: updated.difficulty,
|
|
1003
|
-
elapsed_days: updated.elapsedDays,
|
|
1004
|
-
scheduled_days: updated.scheduledDays,
|
|
1005
|
-
reps: updated.reps,
|
|
1006
|
-
lapses: updated.lapses,
|
|
1007
|
-
state: updated.state,
|
|
1008
|
-
due_at: updated.dueAt.toISOString(),
|
|
1009
|
-
last_review_at: now.toISOString()
|
|
1010
|
-
});
|
|
1011
|
-
db.prepare(
|
|
1012
|
-
`INSERT INTO review_logs (id, card_id, token_id, user_id, rating, response_time_ms, reviewed_at, scheduled_at, session_id)
|
|
1013
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
1014
|
-
).run(
|
|
1015
|
-
ulid6(),
|
|
1016
|
-
input.cardId,
|
|
1017
|
-
input.tokenId,
|
|
1018
|
-
input.userId,
|
|
1019
|
-
input.rating,
|
|
1020
|
-
input.responseTimeMs ?? null,
|
|
1021
|
-
now.toISOString(),
|
|
1022
|
-
card.due_at,
|
|
1023
|
-
input.sessionId ?? null
|
|
1024
|
-
);
|
|
1025
|
-
return {
|
|
1026
|
-
nextDueAt: updated.dueAt.toISOString(),
|
|
1027
|
-
stability: updated.stability,
|
|
1028
|
-
difficulty: updated.difficulty,
|
|
1029
|
-
state: updated.state,
|
|
1030
|
-
scheduledDays: updated.scheduledDays,
|
|
1031
|
-
reps: updated.reps,
|
|
1032
|
-
lapses: updated.lapses
|
|
1033
|
-
};
|
|
1411
|
+
const content = readFileSync4(path, "utf-8");
|
|
1412
|
+
return parseMonitorLog(content);
|
|
1034
1413
|
}
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
function q(db, sql, ...params) {
|
|
1038
|
-
return db.prepare(sql).get(...params);
|
|
1414
|
+
function monitorLogExists(sessionId) {
|
|
1415
|
+
return existsSync4(getMonitorPath(sessionId));
|
|
1039
1416
|
}
|
|
1040
|
-
function
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
).n,
|
|
1050
|
-
blocked: q(db, "SELECT COUNT(*) as n FROM cards WHERE user_id = ? AND blocked = 1", userId).n,
|
|
1051
|
-
mature: q(
|
|
1052
|
-
db,
|
|
1053
|
-
"SELECT COUNT(*) as n FROM cards WHERE user_id = ? AND reps >= 3 AND stability >= 21",
|
|
1054
|
-
userId
|
|
1055
|
-
).n,
|
|
1056
|
-
avgStability: (() => {
|
|
1057
|
-
const v = q(db, "SELECT AVG(stability) as v FROM cards WHERE user_id = ? AND reps > 0", userId);
|
|
1058
|
-
return v.v ? Math.round(v.v * 100) / 100 : null;
|
|
1059
|
-
})(),
|
|
1060
|
-
totalSessions: q(db, "SELECT COUNT(*) as n FROM sessions WHERE user_id = ?", userId).n,
|
|
1061
|
-
lastSession: (() => {
|
|
1062
|
-
const r = db.prepare(
|
|
1063
|
-
"SELECT started_at FROM sessions WHERE user_id = ? ORDER BY started_at DESC LIMIT 1"
|
|
1064
|
-
).get(userId);
|
|
1065
|
-
return r?.started_at ?? null;
|
|
1066
|
-
})()
|
|
1067
|
-
};
|
|
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 };
|
|
1068
1426
|
}
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
return
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
d.domain
|
|
1091
|
-
).n;
|
|
1092
|
-
const avgStab = q(
|
|
1093
|
-
db,
|
|
1094
|
-
`SELECT AVG(c.stability) as v FROM cards c
|
|
1095
|
-
JOIN tokens t ON t.id = c.token_id
|
|
1096
|
-
WHERE c.user_id = ? AND t.domain = ? AND c.reps > 0`,
|
|
1097
|
-
userId,
|
|
1098
|
-
d.domain
|
|
1099
|
-
).v ?? 0;
|
|
1100
|
-
const reviews = q(
|
|
1101
|
-
db,
|
|
1102
|
-
`SELECT COUNT(*) as total,
|
|
1103
|
-
SUM(CASE WHEN rating >= 2 THEN 1 ELSE 0 END) as passed
|
|
1104
|
-
FROM review_logs
|
|
1105
|
-
WHERE user_id = ? AND token_id IN (SELECT id FROM tokens WHERE domain = ?)`,
|
|
1106
|
-
userId,
|
|
1107
|
-
d.domain
|
|
1108
|
-
);
|
|
1109
|
-
const retentionRate = reviews.total > 0 ? reviews.passed / reviews.total : 0;
|
|
1110
|
-
let suggestedMode;
|
|
1111
|
-
if (retentionRate > 0.9 && avgStab > 30) {
|
|
1112
|
-
suggestedMode = "autonomy";
|
|
1113
|
-
} else if (retentionRate > 0.7 && avgStab > 7) {
|
|
1114
|
-
suggestedMode = "copilot";
|
|
1115
|
-
} else {
|
|
1116
|
-
suggestedMode = "shadowing";
|
|
1117
|
-
}
|
|
1118
|
-
return {
|
|
1119
|
-
domain: d.domain,
|
|
1120
|
-
totalCards: total,
|
|
1121
|
-
matureCards: mature,
|
|
1122
|
-
avgStability: Math.round(avgStab * 100) / 100,
|
|
1123
|
-
retentionRate: Math.round(retentionRate * 1e3) / 1e3,
|
|
1124
|
-
suggestedMode
|
|
1125
|
-
};
|
|
1126
|
-
});
|
|
1427
|
+
|
|
1428
|
+
// src/kernel/observation/shell-hooks.ts
|
|
1429
|
+
function psSingleQuoted(value) {
|
|
1430
|
+
return `'${value.replace(/'/g, "''")}'`;
|
|
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}"
|
|
1438
|
+
|
|
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
|
|
1127
1448
|
}
|
|
1128
1449
|
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
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"
|
|
1458
|
+
}
|
|
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"
|
|
1467
|
+
}
|
|
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();
|
|
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
|
|
1483
|
+
|
|
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")
|
|
1139
1536
|
}
|
|
1140
|
-
return
|
|
1537
|
+
return $Date.ToUniversalTime().ToString("o")
|
|
1141
1538
|
}
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
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
|
|
1167
1568
|
}
|
|
1168
1569
|
}
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
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
|
|
1180
1586
|
}
|
|
1181
|
-
records.sort((a, b) => new Date(a.startedAt).getTime() - new Date(b.startedAt).getTime());
|
|
1182
|
-
return records;
|
|
1183
1587
|
}
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
function
|
|
1187
|
-
const lower = command.toLowerCase();
|
|
1188
|
-
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
|
|
1189
1591
|
}
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
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
|
+
}
|
|
1193
1610
|
}
|
|
1194
|
-
|
|
1195
|
-
|
|
1611
|
+
|
|
1612
|
+
Write-Host "ZAM monitor active for session $global:__ZAM_MONITOR_SESSION"
|
|
1613
|
+
`.trim();
|
|
1196
1614
|
}
|
|
1197
|
-
function
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
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();
|
|
1202
1624
|
}
|
|
1203
|
-
function
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
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);
|
|
1232
1659
|
}
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
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
|
+
});
|
|
1245
1679
|
}
|
|
1246
|
-
if (helpSeeking) break;
|
|
1247
1680
|
}
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
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);
|
|
1252
1688
|
}
|
|
1253
1689
|
}
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
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();
|
|
1262
1723
|
}
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
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);
|
|
1270
1739
|
}
|
|
1271
1740
|
}
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
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;
|
|
1278
1755
|
}
|
|
1279
1756
|
}
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
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;
|
|
1288
1773
|
});
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
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
|
|
1303
1817
|
});
|
|
1304
1818
|
}
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
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 });
|
|
1309
1845
|
}
|
|
1310
1846
|
}
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
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
|
|
1320
1988
|
};
|
|
1321
1989
|
}
|
|
1322
|
-
return {
|
|
1990
|
+
return {
|
|
1991
|
+
schedule,
|
|
1992
|
+
params: Object.freeze(resolvedParams)
|
|
1993
|
+
};
|
|
1323
1994
|
}
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
if (
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
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
|
+
};
|
|
1339
2050
|
}
|
|
1340
2051
|
|
|
1341
|
-
// src/kernel/
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
function getMonitorDir() {
|
|
1347
|
-
return MONITOR_DIR;
|
|
1348
|
-
}
|
|
1349
|
-
function getMonitorPath(sessionId) {
|
|
1350
|
-
return join2(MONITOR_DIR, `${sessionId}.jsonl`);
|
|
1351
|
-
}
|
|
1352
|
-
function ensureMonitorDir() {
|
|
1353
|
-
if (!existsSync2(MONITOR_DIR)) {
|
|
1354
|
-
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}`);
|
|
1355
2057
|
}
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
ensureMonitorDir();
|
|
1359
|
-
const path = getMonitorPath(sessionId);
|
|
1360
|
-
appendFileSync(path, JSON.stringify(event) + "\n");
|
|
1361
|
-
}
|
|
1362
|
-
function readMonitorLog(sessionId) {
|
|
1363
|
-
const path = getMonitorPath(sessionId);
|
|
1364
|
-
if (!existsSync2(path)) {
|
|
1365
|
-
return [];
|
|
2058
|
+
if (card.user_id !== userId) {
|
|
2059
|
+
throw new Error(`Card ${cardId} does not belong to user ${userId}`);
|
|
1366
2060
|
}
|
|
1367
|
-
const
|
|
1368
|
-
|
|
1369
|
-
}
|
|
1370
|
-
function monitorLogExists(sessionId) {
|
|
1371
|
-
return existsSync2(getMonitorPath(sessionId));
|
|
1372
|
-
}
|
|
1373
|
-
function getMonitorLogStats(sessionId) {
|
|
1374
|
-
const path = getMonitorPath(sessionId);
|
|
1375
|
-
if (!existsSync2(path)) {
|
|
1376
|
-
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}`);
|
|
1377
2064
|
}
|
|
1378
|
-
|
|
1379
|
-
const content = readFileSync(path, "utf-8");
|
|
1380
|
-
const lineCount = content.split("\n").filter((l) => l.trim()).length;
|
|
1381
|
-
return { exists: true, sizeBytes: stat.size, lineCount };
|
|
1382
|
-
}
|
|
1383
|
-
|
|
1384
|
-
// src/kernel/observation/shell-hooks.ts
|
|
1385
|
-
function generateZshHooks(monitorFile, sessionId) {
|
|
1386
|
-
return `
|
|
1387
|
-
# ZAM monitor hooks for session ${sessionId}
|
|
1388
|
-
export __ZAM_MONITOR_FILE="${monitorFile}"
|
|
1389
|
-
export __ZAM_MONITOR_SEQ=0
|
|
1390
|
-
export __ZAM_MONITOR_SESSION="${sessionId}"
|
|
1391
|
-
|
|
1392
|
-
__zam_ts() {
|
|
1393
|
-
if [[ -n "\${EPOCHREALTIME:-}" ]]; then
|
|
1394
|
-
local sec="\${EPOCHREALTIME%%.*}"
|
|
1395
|
-
local frac="\${EPOCHREALTIME##*.}"
|
|
1396
|
-
frac="\${frac:0:3}"
|
|
1397
|
-
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"
|
|
1398
|
-
else
|
|
1399
|
-
date -u '+%Y-%m-%dT%H:%M:%SZ'
|
|
1400
|
-
fi
|
|
1401
|
-
}
|
|
1402
|
-
|
|
1403
|
-
__zam_preexec() {
|
|
1404
|
-
(( __ZAM_MONITOR_SEQ++ ))
|
|
1405
|
-
local cmd="\${1//\\"/\\\\\\"}"
|
|
1406
|
-
local cwd="\${PWD//\\"/\\\\\\"}"
|
|
1407
|
-
local ts="$(__zam_ts)"
|
|
1408
|
-
printf '{"type":"command_start","ts":"%s","command":"%s","cwd":"%s","seq":%d,"pid":%d}\\n' \\
|
|
1409
|
-
"$ts" "$cmd" "$cwd" "$__ZAM_MONITOR_SEQ" "$$" \\
|
|
1410
|
-
>> "$__ZAM_MONITOR_FILE"
|
|
1411
|
-
}
|
|
1412
|
-
|
|
1413
|
-
__zam_precmd() {
|
|
1414
|
-
local exit_code=$?
|
|
1415
|
-
[[ $__ZAM_MONITOR_SEQ -eq 0 ]] && return
|
|
1416
|
-
local ts="$(__zam_ts)"
|
|
1417
|
-
printf '{"type":"command_end","ts":"%s","exit_code":%d,"seq":%d,"pid":%d}\\n' \\
|
|
1418
|
-
"$ts" "$exit_code" "$__ZAM_MONITOR_SEQ" "$$" \\
|
|
1419
|
-
>> "$__ZAM_MONITOR_FILE"
|
|
1420
|
-
}
|
|
1421
|
-
|
|
1422
|
-
autoload -Uz add-zsh-hook
|
|
1423
|
-
add-zsh-hook preexec __zam_preexec
|
|
1424
|
-
add-zsh-hook precmd __zam_precmd
|
|
1425
|
-
|
|
1426
|
-
echo "ZAM monitor active for session $__ZAM_MONITOR_SESSION"
|
|
1427
|
-
`.trim();
|
|
2065
|
+
return { cardId: card.id, token };
|
|
1428
2066
|
}
|
|
1429
|
-
function
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
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
|
+
}
|
|
1439
2139
|
}
|
|
1440
2140
|
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
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);
|
|
1451
2162
|
}
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
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
|
+
};
|
|
1462
2178
|
}
|
|
1463
2179
|
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
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
|
+
};
|
|
1469
2321
|
}
|
|
1470
|
-
function
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
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
|
+
};
|
|
1479
2341
|
}
|
|
1480
|
-
function
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
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);
|
|
1489
2362
|
}
|
|
1490
2363
|
|
|
1491
|
-
// src/kernel/
|
|
1492
|
-
function
|
|
1493
|
-
|
|
1494
|
-
const
|
|
1495
|
-
const
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
sessionSequences.set(sessionId, sequences);
|
|
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]);
|
|
1502
2374
|
}
|
|
1503
2375
|
}
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
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++;
|
|
1515
2405
|
} else {
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
sessionCount: 1,
|
|
1519
|
-
totalOccurrences: 1,
|
|
1520
|
-
examples: []
|
|
1521
|
-
});
|
|
2406
|
+
lastDomain = domain;
|
|
2407
|
+
consecutiveCount = 1;
|
|
1522
2408
|
}
|
|
2409
|
+
break;
|
|
1523
2410
|
}
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
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;
|
|
1531
2424
|
}
|
|
1532
2425
|
}
|
|
1533
2426
|
}
|
|
1534
|
-
|
|
1535
|
-
|
|
2427
|
+
return result;
|
|
2428
|
+
}
|
|
2429
|
+
|
|
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 }))
|
|
1536
2486
|
);
|
|
1537
|
-
const
|
|
1538
|
-
const
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
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
|
+
}
|
|
1550
2507
|
}
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
2508
|
+
return {
|
|
2509
|
+
items: capped,
|
|
2510
|
+
newCount,
|
|
2511
|
+
reviewCount,
|
|
2512
|
+
relearnCount,
|
|
2513
|
+
totalDomains: [...domainSet].sort()
|
|
2514
|
+
};
|
|
1558
2515
|
}
|
|
1559
|
-
function
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
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
|
+
};
|
|
2529
|
+
}
|
|
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++;
|
|
1566
2547
|
}
|
|
2548
|
+
position++;
|
|
1567
2549
|
}
|
|
1568
|
-
return
|
|
2550
|
+
return result;
|
|
1569
2551
|
}
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
2552
|
+
|
|
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");
|
|
1583
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
|
+
});
|
|
1584
2621
|
}
|
|
1585
|
-
|
|
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
|
+
});
|
|
2637
|
+
}
|
|
2638
|
+
const codexSkillsDir = join6(home, ".agents", "skills", "zam");
|
|
2639
|
+
try {
|
|
2640
|
+
if (!codexSourceSkill) {
|
|
2641
|
+
throw new Error("Codex skill source not found");
|
|
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
|
+
});
|
|
2656
|
+
}
|
|
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
|
|
2671
|
+
});
|
|
2672
|
+
}
|
|
2673
|
+
return results;
|
|
1586
2674
|
}
|
|
1587
|
-
function
|
|
1588
|
-
const
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
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
|
+
});
|
|
1598
2704
|
}
|
|
1599
|
-
}
|
|
1600
|
-
|
|
1601
|
-
|
|
2705
|
+
} catch {
|
|
2706
|
+
results.push({
|
|
2707
|
+
shell: "zsh",
|
|
2708
|
+
file: zshrc,
|
|
2709
|
+
success: false,
|
|
2710
|
+
alreadyHooked: false
|
|
2711
|
+
});
|
|
1602
2712
|
}
|
|
1603
2713
|
}
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
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
|
+
});
|
|
2733
|
+
}
|
|
2734
|
+
} catch {
|
|
2735
|
+
results.push({
|
|
2736
|
+
shell: "bash",
|
|
2737
|
+
file: bashrc,
|
|
2738
|
+
success: false,
|
|
2739
|
+
alreadyHooked: false
|
|
2740
|
+
});
|
|
2741
|
+
}
|
|
2742
|
+
}
|
|
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
|
+
});
|
|
2770
|
+
}
|
|
2771
|
+
} catch {
|
|
2772
|
+
results.push({
|
|
2773
|
+
shell: "powershell",
|
|
2774
|
+
file: profileFile,
|
|
2775
|
+
success: false,
|
|
2776
|
+
alreadyHooked: false
|
|
2777
|
+
});
|
|
1617
2778
|
}
|
|
1618
2779
|
}
|
|
1619
|
-
return
|
|
1620
|
-
}
|
|
1621
|
-
function generateSlug(steps) {
|
|
1622
|
-
return steps.map((s) => {
|
|
1623
|
-
const parts = s.split(/\s+/);
|
|
1624
|
-
return parts[parts.length - 1];
|
|
1625
|
-
}).join("-");
|
|
1626
|
-
}
|
|
1627
|
-
function describeSequence(steps) {
|
|
1628
|
-
return `Recurring pattern: ${steps.join(" \u2192 ")}`;
|
|
2780
|
+
return results;
|
|
1629
2781
|
}
|
|
1630
2782
|
|
|
1631
|
-
// src/kernel/
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
}
|
|
1651
|
-
}
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
"
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
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}]"
|
|
1660
2980
|
}
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
lines.push("");
|
|
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));
|
|
1668
2987
|
}
|
|
1669
|
-
return
|
|
2988
|
+
return str;
|
|
1670
2989
|
}
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
}
|
|
2990
|
+
|
|
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;
|
|
1680
3003
|
}
|
|
1681
|
-
return tasks;
|
|
1682
3004
|
}
|
|
1683
|
-
function
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
}
|
|
3005
|
+
function installFastFlowLM() {
|
|
3006
|
+
if (process.platform !== "win32") {
|
|
3007
|
+
return {
|
|
3008
|
+
success: false,
|
|
3009
|
+
message: "FastFlowLM is only supported on Windows."
|
|
3010
|
+
};
|
|
3011
|
+
}
|
|
3012
|
+
const hasFlm = hasCommand("flm") || existsSync7("C:\\Program Files\\flm\\flm.exe");
|
|
3013
|
+
if (hasFlm) {
|
|
3014
|
+
return { success: true, message: "FastFlowLM is already installed." };
|
|
3015
|
+
}
|
|
3016
|
+
if (!hasCommand("winget")) {
|
|
3017
|
+
return {
|
|
3018
|
+
success: false,
|
|
3019
|
+
message: "winget package manager was not found on this system."
|
|
3020
|
+
};
|
|
3021
|
+
}
|
|
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
|
+
};
|
|
1693
3034
|
}
|
|
1694
|
-
return refs;
|
|
1695
3035
|
}
|
|
1696
|
-
function
|
|
1697
|
-
const
|
|
1698
|
-
|
|
1699
|
-
|
|
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." };
|
|
1700
3044
|
}
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
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
|
+
}
|
|
1704
3095
|
}
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
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
|
+
}
|
|
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
|
+
}
|
|
1715
3137
|
}
|
|
3138
|
+
} catch {
|
|
1716
3139
|
}
|
|
1717
|
-
return
|
|
3140
|
+
return "en";
|
|
1718
3141
|
}
|
|
1719
3142
|
|
|
1720
|
-
// src/kernel/
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
(
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
for (const file of files) {
|
|
1728
|
-
const filePath = join3(goalsDir, file);
|
|
1729
|
-
const content = readFileSync2(filePath, "utf-8");
|
|
1730
|
-
const slug = basename(file, ".md");
|
|
1731
|
-
const goal = parseGoalFile(content, slug, filePath);
|
|
1732
|
-
const tasks = extractTasks(goal.body);
|
|
1733
|
-
const tokens = extractTokenRefs(goal.body);
|
|
1734
|
-
summaries.push({
|
|
1735
|
-
slug: goal.slug,
|
|
1736
|
-
title: goal.title,
|
|
1737
|
-
status: goal.status,
|
|
1738
|
-
parent: goal.parent,
|
|
1739
|
-
taskCount: tasks.length,
|
|
1740
|
-
tasksDone: tasks.filter((t) => t.done).length,
|
|
1741
|
-
tokenCount: tokens.length
|
|
1742
|
-
});
|
|
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 "";
|
|
1743
3150
|
}
|
|
1744
|
-
const statusOrder = {
|
|
1745
|
-
active: 0,
|
|
1746
|
-
paused: 1,
|
|
1747
|
-
completed: 2,
|
|
1748
|
-
abandoned: 3
|
|
1749
|
-
};
|
|
1750
|
-
summaries.sort((a, b) => {
|
|
1751
|
-
const statusDiff = statusOrder[a.status] - statusOrder[b.status];
|
|
1752
|
-
if (statusDiff !== 0) return statusDiff;
|
|
1753
|
-
return a.title.localeCompare(b.title);
|
|
1754
|
-
});
|
|
1755
|
-
return summaries;
|
|
1756
3151
|
}
|
|
1757
|
-
function
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
const
|
|
1761
|
-
return
|
|
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"))
|
|
3158
|
+
);
|
|
1762
3159
|
}
|
|
1763
|
-
function
|
|
1764
|
-
const
|
|
1765
|
-
|
|
1766
|
-
|
|
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";
|
|
1767
3183
|
}
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
${input.description}
|
|
1778
|
-
|
|
1779
|
-
## Tasks
|
|
3184
|
+
return {
|
|
3185
|
+
os,
|
|
3186
|
+
arch,
|
|
3187
|
+
hasRyzenNPU,
|
|
3188
|
+
hasAppleSilicon,
|
|
3189
|
+
recommendedRunner,
|
|
3190
|
+
recommendedModel
|
|
3191
|
+
};
|
|
3192
|
+
}
|
|
1780
3193
|
|
|
1781
|
-
|
|
1782
|
-
|
|
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
|
|
1783
3205
|
};
|
|
1784
|
-
writeFileSync(filePath, serializeGoal(goal), "utf-8");
|
|
1785
|
-
return goal;
|
|
1786
3206
|
}
|
|
1787
|
-
function
|
|
1788
|
-
const
|
|
1789
|
-
|
|
1790
|
-
goal.status = status;
|
|
1791
|
-
goal.updated = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
1792
|
-
writeFileSync(goal.filePath, serializeGoal(goal), "utf-8");
|
|
1793
|
-
return goal;
|
|
3207
|
+
function resolveRepoPath(db, type) {
|
|
3208
|
+
const paths = getRepoPaths(db);
|
|
3209
|
+
return paths[type];
|
|
1794
3210
|
}
|
|
1795
|
-
function
|
|
1796
|
-
const
|
|
1797
|
-
const
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
if (g.parent && bySlug.has(g.parent)) {
|
|
1802
|
-
const list = children.get(g.parent) ?? [];
|
|
1803
|
-
list.push(g);
|
|
1804
|
-
children.set(g.parent, list);
|
|
1805
|
-
}
|
|
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);
|
|
1806
3217
|
}
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
}
|
|
3218
|
+
if (paths.team) {
|
|
3219
|
+
const teamDir = resolve2(paths.team, "beliefs");
|
|
3220
|
+
if (existsSync8(teamDir)) dirs.push(teamDir);
|
|
1811
3221
|
}
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
const orgUrl = getSetting(db, "ado.org_url");
|
|
1818
|
-
const project = getSetting(db, "ado.project");
|
|
1819
|
-
const pat = getSetting(db, "ado.pat");
|
|
1820
|
-
if (!orgUrl || !project || !pat) return null;
|
|
1821
|
-
return { orgUrl: orgUrl.replace(/\/+$/, ""), project, pat };
|
|
1822
|
-
}
|
|
1823
|
-
function authHeader(pat) {
|
|
1824
|
-
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;
|
|
1825
3227
|
}
|
|
1826
|
-
|
|
1827
|
-
const
|
|
1828
|
-
const
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
const wiqlRes = await fetch(wiqlUrl, {
|
|
1833
|
-
method: "POST",
|
|
1834
|
-
headers: {
|
|
1835
|
-
Authorization: authHeader(pat),
|
|
1836
|
-
"Content-Type": "application/json"
|
|
1837
|
-
},
|
|
1838
|
-
body: JSON.stringify(wiqlBody)
|
|
1839
|
-
});
|
|
1840
|
-
if (!wiqlRes.ok) {
|
|
1841
|
-
const text = await wiqlRes.text();
|
|
1842
|
-
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);
|
|
1843
3234
|
}
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
const batchIds = ids.slice(0, 200);
|
|
1848
|
-
const fields = "System.Id,System.Title,System.State,System.WorkItemType,System.AssignedTo";
|
|
1849
|
-
const detailUrl = `${orgUrl}/${project}/_apis/wit/workitems?ids=${batchIds.join(",")}&fields=${fields}&api-version=7.1`;
|
|
1850
|
-
const detailRes = await fetch(detailUrl, {
|
|
1851
|
-
headers: { Authorization: authHeader(pat) }
|
|
1852
|
-
});
|
|
1853
|
-
if (!detailRes.ok) {
|
|
1854
|
-
const text = await detailRes.text();
|
|
1855
|
-
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);
|
|
1856
3238
|
}
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
type: wi.fields["System.WorkItemType"],
|
|
1863
|
-
assignedTo: wi.fields["System.AssignedTo"]?.displayName ?? ""
|
|
1864
|
-
}));
|
|
3239
|
+
if (paths.org) {
|
|
3240
|
+
const orgDir = resolve2(paths.org, "goals");
|
|
3241
|
+
if (existsSync8(orgDir)) dirs.push(orgDir);
|
|
3242
|
+
}
|
|
3243
|
+
return dirs;
|
|
1865
3244
|
}
|
|
1866
3245
|
export {
|
|
3246
|
+
DEFAULT_REVIEW_CONTEXT_MAX_CHARS,
|
|
1867
3247
|
addPrerequisite,
|
|
1868
3248
|
analyzeObservation,
|
|
1869
3249
|
buildReviewQueue,
|
|
1870
3250
|
cascadeBlock,
|
|
3251
|
+
clearADOCredentials,
|
|
3252
|
+
clearTursoCredentials,
|
|
1871
3253
|
createAgentSkill,
|
|
1872
3254
|
createFSRS,
|
|
1873
3255
|
createGoal,
|
|
1874
3256
|
createToken,
|
|
3257
|
+
deleteCardForUser,
|
|
1875
3258
|
deleteSetting,
|
|
3259
|
+
deleteToken,
|
|
1876
3260
|
deprecateToken,
|
|
3261
|
+
detectSystemLocale,
|
|
1877
3262
|
discoverSkills,
|
|
3263
|
+
distributeGlobalSkills,
|
|
1878
3264
|
endSession,
|
|
1879
3265
|
ensureCard,
|
|
1880
3266
|
ensureMonitorDir,
|
|
1881
3267
|
evaluateRating,
|
|
3268
|
+
executeReviewAction,
|
|
1882
3269
|
extractTasks,
|
|
1883
3270
|
extractTokenRefs,
|
|
1884
3271
|
fetchActiveWorkItems,
|
|
1885
3272
|
findTokens,
|
|
1886
3273
|
generateBashHooks,
|
|
1887
3274
|
generateBashUnhooks,
|
|
3275
|
+
generateConceptFreeCue,
|
|
3276
|
+
generatePowerShellHooks,
|
|
3277
|
+
generatePowerShellUnhooks,
|
|
1888
3278
|
generatePrompt,
|
|
1889
3279
|
generateZshHooks,
|
|
1890
3280
|
generateZshUnhooks,
|
|
3281
|
+
getADOCredentials,
|
|
1891
3282
|
getAgentSkill,
|
|
1892
3283
|
getAllSettings,
|
|
1893
3284
|
getAllSettingsDetailed,
|
|
1894
3285
|
getBlockedCards,
|
|
1895
3286
|
getCard,
|
|
3287
|
+
getCardById,
|
|
3288
|
+
getCardDeletionImpact,
|
|
1896
3289
|
getDefaultDbPath,
|
|
1897
3290
|
getDependents,
|
|
1898
3291
|
getDomainCompetence,
|
|
@@ -1902,34 +3295,57 @@ export {
|
|
|
1902
3295
|
getMonitorDir,
|
|
1903
3296
|
getMonitorLogStats,
|
|
1904
3297
|
getMonitorPath,
|
|
3298
|
+
getPackageSkillPath,
|
|
1905
3299
|
getPrerequisites,
|
|
3300
|
+
getRepoPaths,
|
|
1906
3301
|
getReviewsForCard,
|
|
1907
3302
|
getReviewsForUser,
|
|
1908
3303
|
getSessionSummary,
|
|
1909
3304
|
getSetting,
|
|
3305
|
+
getSystemProfile,
|
|
1910
3306
|
getTokenById,
|
|
1911
3307
|
getTokenBySlug,
|
|
3308
|
+
getTokenDeleteImpact,
|
|
3309
|
+
getTursoCredentials,
|
|
1912
3310
|
getUserStats,
|
|
3311
|
+
hasCommand,
|
|
3312
|
+
injectShellHooks,
|
|
3313
|
+
installFastFlowLM,
|
|
3314
|
+
installOllama,
|
|
1913
3315
|
interleave,
|
|
1914
3316
|
listAgentSkills,
|
|
1915
3317
|
listGoals,
|
|
1916
3318
|
listTokens,
|
|
1917
3319
|
loadADOConfig,
|
|
3320
|
+
loadCredentials,
|
|
1918
3321
|
logReview,
|
|
1919
3322
|
logStep,
|
|
3323
|
+
matchesFilePath,
|
|
1920
3324
|
monitorLogExists,
|
|
3325
|
+
normalizeLocale,
|
|
3326
|
+
normalizePath,
|
|
1921
3327
|
openDatabase,
|
|
1922
3328
|
openDatabaseWithSync,
|
|
1923
3329
|
pairCommands,
|
|
1924
3330
|
parseGoalFile,
|
|
1925
3331
|
parseMonitorLog,
|
|
1926
3332
|
readMonitorLog,
|
|
3333
|
+
resolveAllBeliefPaths,
|
|
3334
|
+
resolveAllGoalPaths,
|
|
3335
|
+
resolveReference,
|
|
3336
|
+
resolveRepoPath,
|
|
3337
|
+
resolveReviewContext,
|
|
3338
|
+
saveCredentials,
|
|
1927
3339
|
serializeGoal,
|
|
3340
|
+
setADOCredentials,
|
|
1928
3341
|
setSetting,
|
|
3342
|
+
setTursoCredentials,
|
|
1929
3343
|
startSession,
|
|
3344
|
+
t,
|
|
1930
3345
|
unblockReady,
|
|
1931
3346
|
updateCard,
|
|
1932
3347
|
updateGoalStatus,
|
|
3348
|
+
updateToken,
|
|
1933
3349
|
writeMonitorEvent
|
|
1934
3350
|
};
|
|
1935
3351
|
//# sourceMappingURL=index.js.map
|