zam-core 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/skills/zam/SKILL.md +331 -0
- package/.gemini/skills/zam/SKILL.md +335 -0
- package/LICENSE +199 -0
- package/README.de.md +86 -0
- package/README.md +86 -0
- package/dist/cli/index.js +3661 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/index.d.ts +998 -0
- package/dist/index.js +1920 -0
- package/dist/index.js.map +1 -0
- package/package.json +56 -0
|
@@ -0,0 +1,3661 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/cli/index.ts
|
|
4
|
+
import { Command as Command15 } from "commander";
|
|
5
|
+
|
|
6
|
+
// src/cli/commands/init.ts
|
|
7
|
+
import { Command } from "commander";
|
|
8
|
+
|
|
9
|
+
// src/kernel/db/connection.ts
|
|
10
|
+
import Database from "libsql";
|
|
11
|
+
import { existsSync, mkdirSync } from "fs";
|
|
12
|
+
import { homedir } from "os";
|
|
13
|
+
import { dirname, join } from "path";
|
|
14
|
+
|
|
15
|
+
// src/kernel/db/schema.ts
|
|
16
|
+
var SCHEMA = `
|
|
17
|
+
-- Use WAL mode for concurrent reads (AI CLI + user CLI)
|
|
18
|
+
PRAGMA journal_mode = WAL;
|
|
19
|
+
PRAGMA foreign_keys = ON;
|
|
20
|
+
|
|
21
|
+
-- Knowledge tokens: atomic concepts/facts with Bloom levels
|
|
22
|
+
CREATE TABLE IF NOT EXISTS tokens (
|
|
23
|
+
id TEXT PRIMARY KEY,
|
|
24
|
+
slug TEXT UNIQUE NOT NULL,
|
|
25
|
+
concept TEXT NOT NULL,
|
|
26
|
+
domain TEXT NOT NULL DEFAULT '',
|
|
27
|
+
bloom_level INTEGER NOT NULL DEFAULT 1 CHECK (bloom_level BETWEEN 1 AND 5),
|
|
28
|
+
context TEXT NOT NULL DEFAULT '',
|
|
29
|
+
symbiosis_mode TEXT CHECK (symbiosis_mode IN ('shadowing', 'copilot', 'autonomy')),
|
|
30
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
31
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
32
|
+
deprecated_at TEXT
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
-- Prerequisite dependency graph: "to learn A, first know B"
|
|
36
|
+
CREATE TABLE IF NOT EXISTS prerequisites (
|
|
37
|
+
token_id TEXT NOT NULL REFERENCES tokens(id) ON DELETE CASCADE,
|
|
38
|
+
requires_id TEXT NOT NULL REFERENCES tokens(id) ON DELETE CASCADE,
|
|
39
|
+
PRIMARY KEY (token_id, requires_id)
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
-- Per-user scheduling state for each token (FSRS fields)
|
|
43
|
+
CREATE TABLE IF NOT EXISTS cards (
|
|
44
|
+
id TEXT PRIMARY KEY,
|
|
45
|
+
token_id TEXT NOT NULL REFERENCES tokens(id) ON DELETE CASCADE,
|
|
46
|
+
user_id TEXT NOT NULL,
|
|
47
|
+
stability REAL NOT NULL DEFAULT 0.0,
|
|
48
|
+
difficulty REAL NOT NULL DEFAULT 0.5,
|
|
49
|
+
elapsed_days REAL NOT NULL DEFAULT 0.0,
|
|
50
|
+
scheduled_days REAL NOT NULL DEFAULT 0.0,
|
|
51
|
+
reps INTEGER NOT NULL DEFAULT 0,
|
|
52
|
+
lapses INTEGER NOT NULL DEFAULT 0,
|
|
53
|
+
state TEXT NOT NULL DEFAULT 'new' CHECK (state IN ('new', 'learning', 'review', 'relearning')),
|
|
54
|
+
due_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
55
|
+
last_review_at TEXT,
|
|
56
|
+
blocked INTEGER NOT NULL DEFAULT 0,
|
|
57
|
+
UNIQUE(token_id, user_id)
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
-- Immutable review log: every rating event
|
|
61
|
+
CREATE TABLE IF NOT EXISTS review_logs (
|
|
62
|
+
id TEXT PRIMARY KEY,
|
|
63
|
+
card_id TEXT NOT NULL REFERENCES cards(id) ON DELETE CASCADE,
|
|
64
|
+
token_id TEXT NOT NULL REFERENCES tokens(id) ON DELETE CASCADE,
|
|
65
|
+
user_id TEXT NOT NULL,
|
|
66
|
+
rating INTEGER NOT NULL CHECK (rating BETWEEN 1 AND 4),
|
|
67
|
+
response_time_ms INTEGER,
|
|
68
|
+
reviewed_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
69
|
+
scheduled_at TEXT NOT NULL,
|
|
70
|
+
session_id TEXT REFERENCES sessions(id)
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
-- Work+learning sessions
|
|
74
|
+
CREATE TABLE IF NOT EXISTS sessions (
|
|
75
|
+
id TEXT PRIMARY KEY,
|
|
76
|
+
user_id TEXT NOT NULL,
|
|
77
|
+
task TEXT NOT NULL,
|
|
78
|
+
started_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
79
|
+
completed_at TEXT
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
-- Steps within a session: who did what
|
|
83
|
+
CREATE TABLE IF NOT EXISTS session_steps (
|
|
84
|
+
id TEXT PRIMARY KEY,
|
|
85
|
+
session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
|
|
86
|
+
token_id TEXT NOT NULL REFERENCES tokens(id) ON DELETE CASCADE,
|
|
87
|
+
done_by TEXT NOT NULL CHECK (done_by IN ('user', 'agent')),
|
|
88
|
+
rating INTEGER CHECK (rating BETWEEN 1 AND 4),
|
|
89
|
+
notes TEXT,
|
|
90
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
-- User configuration
|
|
94
|
+
CREATE TABLE IF NOT EXISTS user_config (
|
|
95
|
+
key TEXT PRIMARY KEY,
|
|
96
|
+
value TEXT NOT NULL,
|
|
97
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
-- Agent skills: task recipes the agent learns from user guidance
|
|
101
|
+
CREATE TABLE IF NOT EXISTS agent_skills (
|
|
102
|
+
id TEXT PRIMARY KEY,
|
|
103
|
+
slug TEXT NOT NULL UNIQUE,
|
|
104
|
+
description TEXT NOT NULL,
|
|
105
|
+
steps TEXT NOT NULL DEFAULT '[]', -- JSON array of step strings
|
|
106
|
+
token_slugs TEXT NOT NULL DEFAULT '[]', -- JSON array of related token slugs
|
|
107
|
+
source TEXT NOT NULL DEFAULT 'learned'
|
|
108
|
+
CHECK(source IN ('learned', 'builtin')),
|
|
109
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
110
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
-- Performance indexes
|
|
114
|
+
CREATE INDEX IF NOT EXISTS idx_tokens_domain ON tokens(domain);
|
|
115
|
+
CREATE INDEX IF NOT EXISTS idx_tokens_slug ON tokens(slug);
|
|
116
|
+
CREATE INDEX IF NOT EXISTS idx_prereqs_token ON prerequisites(token_id);
|
|
117
|
+
CREATE INDEX IF NOT EXISTS idx_prereqs_requires ON prerequisites(requires_id);
|
|
118
|
+
CREATE INDEX IF NOT EXISTS idx_cards_user_due ON cards(user_id, blocked, due_at);
|
|
119
|
+
CREATE INDEX IF NOT EXISTS idx_cards_token_user ON cards(token_id, user_id);
|
|
120
|
+
CREATE INDEX IF NOT EXISTS idx_review_logs_card ON review_logs(card_id);
|
|
121
|
+
CREATE INDEX IF NOT EXISTS idx_review_logs_user ON review_logs(user_id, reviewed_at);
|
|
122
|
+
CREATE INDEX IF NOT EXISTS idx_session_steps_session ON session_steps(session_id);
|
|
123
|
+
`;
|
|
124
|
+
|
|
125
|
+
// src/kernel/db/connection.ts
|
|
126
|
+
var DEFAULT_DB_DIR = join(homedir(), ".zam");
|
|
127
|
+
var DEFAULT_DB_PATH = join(DEFAULT_DB_DIR, "zam.db");
|
|
128
|
+
function openDatabase(options = {}) {
|
|
129
|
+
const dbPath = options.dbPath ?? DEFAULT_DB_PATH;
|
|
130
|
+
if (options.initialize) {
|
|
131
|
+
const dir = dirname(dbPath);
|
|
132
|
+
if (!existsSync(dir)) {
|
|
133
|
+
mkdirSync(dir, { recursive: true });
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
const dbOpts = {};
|
|
137
|
+
if (options.syncUrl) {
|
|
138
|
+
dbOpts.syncUrl = options.syncUrl;
|
|
139
|
+
}
|
|
140
|
+
if (options.authToken) {
|
|
141
|
+
dbOpts.authToken = options.authToken;
|
|
142
|
+
}
|
|
143
|
+
const db = new Database(dbPath, dbOpts);
|
|
144
|
+
db.pragma("journal_mode = WAL");
|
|
145
|
+
db.pragma("foreign_keys = ON");
|
|
146
|
+
db.pragma("busy_timeout = 5000");
|
|
147
|
+
if (options.initialize) {
|
|
148
|
+
db.exec(SCHEMA);
|
|
149
|
+
}
|
|
150
|
+
runMigrations(db);
|
|
151
|
+
if (options.syncUrl) {
|
|
152
|
+
db.sync();
|
|
153
|
+
}
|
|
154
|
+
return db;
|
|
155
|
+
}
|
|
156
|
+
function openDatabaseWithSync(options = {}) {
|
|
157
|
+
const db = openDatabase(options);
|
|
158
|
+
const syncUrl = db.prepare("SELECT value FROM user_config WHERE key = ?").get("turso.url");
|
|
159
|
+
const authToken = db.prepare("SELECT value FROM user_config WHERE key = ?").get("turso.token");
|
|
160
|
+
if (!syncUrl || !authToken) return db;
|
|
161
|
+
db.close();
|
|
162
|
+
return openDatabase({ ...options, syncUrl: syncUrl.value, authToken: authToken.value });
|
|
163
|
+
}
|
|
164
|
+
function getDefaultDbPath() {
|
|
165
|
+
return DEFAULT_DB_PATH;
|
|
166
|
+
}
|
|
167
|
+
function runMigrations(db) {
|
|
168
|
+
const sessionCols = db.pragma("table_info(sessions)");
|
|
169
|
+
if (sessionCols.length > 0 && !sessionCols.some((c) => c.name === "execution_context")) {
|
|
170
|
+
db.exec(
|
|
171
|
+
`ALTER TABLE sessions ADD COLUMN execution_context TEXT NOT NULL DEFAULT 'shell'`
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
const tokenCols = db.pragma("table_info(tokens)");
|
|
175
|
+
if (tokenCols.length > 0 && !tokenCols.some((c) => c.name === "deprecated_at")) {
|
|
176
|
+
db.exec(`ALTER TABLE tokens ADD COLUMN deprecated_at TEXT`);
|
|
177
|
+
}
|
|
178
|
+
db.exec(`
|
|
179
|
+
CREATE TABLE IF NOT EXISTS agent_skills (
|
|
180
|
+
id TEXT PRIMARY KEY,
|
|
181
|
+
slug TEXT NOT NULL UNIQUE,
|
|
182
|
+
description TEXT NOT NULL,
|
|
183
|
+
steps TEXT NOT NULL DEFAULT '[]',
|
|
184
|
+
token_slugs TEXT NOT NULL DEFAULT '[]',
|
|
185
|
+
source TEXT NOT NULL DEFAULT 'learned',
|
|
186
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
187
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
188
|
+
)
|
|
189
|
+
`);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// src/kernel/models/token.ts
|
|
193
|
+
import { ulid } from "ulid";
|
|
194
|
+
function createToken(db, input5) {
|
|
195
|
+
const id = ulid();
|
|
196
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
197
|
+
const bloom = input5.bloom_level ?? 1;
|
|
198
|
+
if (bloom < 1 || bloom > 5) {
|
|
199
|
+
throw new Error(`bloom_level must be between 1 and 5, got ${bloom}`);
|
|
200
|
+
}
|
|
201
|
+
db.prepare(`
|
|
202
|
+
INSERT INTO tokens (id, slug, concept, domain, bloom_level, context, symbiosis_mode, created_at, updated_at)
|
|
203
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
204
|
+
`).run(
|
|
205
|
+
id,
|
|
206
|
+
input5.slug,
|
|
207
|
+
input5.concept,
|
|
208
|
+
input5.domain ?? "",
|
|
209
|
+
bloom,
|
|
210
|
+
input5.context ?? "",
|
|
211
|
+
input5.symbiosis_mode ?? null,
|
|
212
|
+
now,
|
|
213
|
+
now
|
|
214
|
+
);
|
|
215
|
+
return getTokenById(db, id);
|
|
216
|
+
}
|
|
217
|
+
function getTokenBySlug(db, slug) {
|
|
218
|
+
return db.prepare("SELECT * FROM tokens WHERE slug = ?").get(slug);
|
|
219
|
+
}
|
|
220
|
+
function getTokenById(db, id) {
|
|
221
|
+
return db.prepare("SELECT * FROM tokens WHERE id = ?").get(id);
|
|
222
|
+
}
|
|
223
|
+
function deprecateToken(db, slug) {
|
|
224
|
+
const token = getTokenBySlug(db, slug);
|
|
225
|
+
if (!token) {
|
|
226
|
+
throw new Error(`Token not found: ${slug}`);
|
|
227
|
+
}
|
|
228
|
+
if (token.deprecated_at) {
|
|
229
|
+
throw new Error(`Token already deprecated: ${slug}`);
|
|
230
|
+
}
|
|
231
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
232
|
+
db.prepare("UPDATE tokens SET deprecated_at = ?, updated_at = ? WHERE slug = ?").run(
|
|
233
|
+
now,
|
|
234
|
+
now,
|
|
235
|
+
slug
|
|
236
|
+
);
|
|
237
|
+
return getTokenBySlug(db, slug);
|
|
238
|
+
}
|
|
239
|
+
function findTokens(db, query) {
|
|
240
|
+
const normalised = query.toLowerCase();
|
|
241
|
+
const qTokens = new Set(
|
|
242
|
+
normalised.split(/[\s,.\-_/\\:;!?()\[\]{}]+/).filter((t) => t.length > 2)
|
|
243
|
+
);
|
|
244
|
+
const tokens = db.prepare("SELECT * FROM tokens WHERE deprecated_at IS NULL").all();
|
|
245
|
+
const scored = [];
|
|
246
|
+
for (const t of tokens) {
|
|
247
|
+
const words = (t.slug + " " + t.concept + " " + t.domain).toLowerCase().split(/[\s,.\-_/\\:;!?()\[\]{}]+/).filter(Boolean);
|
|
248
|
+
let score = 0;
|
|
249
|
+
for (const w of words) {
|
|
250
|
+
if (qTokens.has(w)) score++;
|
|
251
|
+
}
|
|
252
|
+
if (t.concept.toLowerCase().includes(normalised.slice(0, 25))) {
|
|
253
|
+
score += 3;
|
|
254
|
+
}
|
|
255
|
+
if (score > 0) {
|
|
256
|
+
scored.push({ score, ...t });
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
scored.sort((a, b) => b.score - a.score);
|
|
260
|
+
return scored;
|
|
261
|
+
}
|
|
262
|
+
function listTokens(db, options) {
|
|
263
|
+
if (options?.domain) {
|
|
264
|
+
return db.prepare(
|
|
265
|
+
"SELECT * FROM tokens WHERE domain = ? AND deprecated_at IS NULL ORDER BY bloom_level, slug"
|
|
266
|
+
).all(options.domain);
|
|
267
|
+
}
|
|
268
|
+
return db.prepare(
|
|
269
|
+
"SELECT * FROM tokens WHERE deprecated_at IS NULL ORDER BY bloom_level, domain, slug"
|
|
270
|
+
).all();
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// src/kernel/models/prerequisite.ts
|
|
274
|
+
function addPrerequisite(db, tokenId, requiresId) {
|
|
275
|
+
if (tokenId === requiresId) {
|
|
276
|
+
throw new Error("A token cannot be a prerequisite of itself");
|
|
277
|
+
}
|
|
278
|
+
db.prepare(
|
|
279
|
+
"INSERT OR IGNORE INTO prerequisites (token_id, requires_id) VALUES (?, ?)"
|
|
280
|
+
).run(tokenId, requiresId);
|
|
281
|
+
}
|
|
282
|
+
function getPrerequisites(db, tokenId) {
|
|
283
|
+
return db.prepare(
|
|
284
|
+
`SELECT p.token_id, p.requires_id, t.slug, t.concept, t.domain, t.bloom_level
|
|
285
|
+
FROM prerequisites p
|
|
286
|
+
JOIN tokens t ON t.id = p.requires_id
|
|
287
|
+
WHERE p.token_id = ?`
|
|
288
|
+
).all(tokenId);
|
|
289
|
+
}
|
|
290
|
+
function getDependents(db, tokenId) {
|
|
291
|
+
return db.prepare(
|
|
292
|
+
`SELECT p.token_id, p.requires_id, t.slug, t.concept, t.domain, t.bloom_level
|
|
293
|
+
FROM prerequisites p
|
|
294
|
+
JOIN tokens t ON t.id = p.token_id
|
|
295
|
+
WHERE p.requires_id = ?`
|
|
296
|
+
).all(tokenId);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// src/kernel/models/card.ts
|
|
300
|
+
import { ulid as ulid2 } from "ulid";
|
|
301
|
+
function ensureCard(db, tokenId, userId) {
|
|
302
|
+
const existing = db.prepare("SELECT * FROM cards WHERE token_id = ? AND user_id = ?").get(tokenId, userId);
|
|
303
|
+
if (existing) return existing;
|
|
304
|
+
const id = ulid2();
|
|
305
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
306
|
+
db.prepare(
|
|
307
|
+
`INSERT INTO cards (id, token_id, user_id, due_at)
|
|
308
|
+
VALUES (?, ?, ?, ?)`
|
|
309
|
+
).run(id, tokenId, userId, now);
|
|
310
|
+
return db.prepare("SELECT * FROM cards WHERE id = ?").get(id);
|
|
311
|
+
}
|
|
312
|
+
function getCard(db, tokenId, userId) {
|
|
313
|
+
return db.prepare("SELECT * FROM cards WHERE token_id = ? AND user_id = ?").get(tokenId, userId);
|
|
314
|
+
}
|
|
315
|
+
function updateCard(db, cardId, updates) {
|
|
316
|
+
const fields = [];
|
|
317
|
+
const values = [];
|
|
318
|
+
if (updates.stability !== void 0) {
|
|
319
|
+
fields.push("stability = ?");
|
|
320
|
+
values.push(updates.stability);
|
|
321
|
+
}
|
|
322
|
+
if (updates.difficulty !== void 0) {
|
|
323
|
+
fields.push("difficulty = ?");
|
|
324
|
+
values.push(updates.difficulty);
|
|
325
|
+
}
|
|
326
|
+
if (updates.elapsed_days !== void 0) {
|
|
327
|
+
fields.push("elapsed_days = ?");
|
|
328
|
+
values.push(updates.elapsed_days);
|
|
329
|
+
}
|
|
330
|
+
if (updates.scheduled_days !== void 0) {
|
|
331
|
+
fields.push("scheduled_days = ?");
|
|
332
|
+
values.push(updates.scheduled_days);
|
|
333
|
+
}
|
|
334
|
+
if (updates.reps !== void 0) {
|
|
335
|
+
fields.push("reps = ?");
|
|
336
|
+
values.push(updates.reps);
|
|
337
|
+
}
|
|
338
|
+
if (updates.lapses !== void 0) {
|
|
339
|
+
fields.push("lapses = ?");
|
|
340
|
+
values.push(updates.lapses);
|
|
341
|
+
}
|
|
342
|
+
if (updates.state !== void 0) {
|
|
343
|
+
fields.push("state = ?");
|
|
344
|
+
values.push(updates.state);
|
|
345
|
+
}
|
|
346
|
+
if (updates.due_at !== void 0) {
|
|
347
|
+
fields.push("due_at = ?");
|
|
348
|
+
values.push(updates.due_at);
|
|
349
|
+
}
|
|
350
|
+
if (updates.last_review_at !== void 0) {
|
|
351
|
+
fields.push("last_review_at = ?");
|
|
352
|
+
values.push(updates.last_review_at);
|
|
353
|
+
}
|
|
354
|
+
if (updates.blocked !== void 0) {
|
|
355
|
+
fields.push("blocked = ?");
|
|
356
|
+
values.push(updates.blocked);
|
|
357
|
+
}
|
|
358
|
+
if (fields.length === 0) {
|
|
359
|
+
throw new Error("updateCard called with no fields to update");
|
|
360
|
+
}
|
|
361
|
+
values.push(cardId);
|
|
362
|
+
const result = db.prepare(`UPDATE cards SET ${fields.join(", ")} WHERE id = ?`).run(...values);
|
|
363
|
+
if (result.changes === 0) {
|
|
364
|
+
throw new Error(`Card not found: ${cardId}`);
|
|
365
|
+
}
|
|
366
|
+
return db.prepare("SELECT * FROM cards WHERE id = ?").get(cardId);
|
|
367
|
+
}
|
|
368
|
+
function getDueCards(db, userId, now) {
|
|
369
|
+
const cutoff = now ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
370
|
+
return db.prepare(
|
|
371
|
+
`SELECT c.*, t.slug, t.concept, t.domain, t.bloom_level
|
|
372
|
+
FROM cards c
|
|
373
|
+
JOIN tokens t ON t.id = c.token_id
|
|
374
|
+
WHERE c.user_id = ? AND c.blocked = 0 AND c.due_at <= ?
|
|
375
|
+
ORDER BY t.bloom_level ASC, c.due_at ASC`
|
|
376
|
+
).all(userId, cutoff);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// src/kernel/models/review.ts
|
|
380
|
+
import { ulid as ulid3 } from "ulid";
|
|
381
|
+
|
|
382
|
+
// src/kernel/models/session.ts
|
|
383
|
+
import { ulid as ulid4 } from "ulid";
|
|
384
|
+
function startSession(db, input5) {
|
|
385
|
+
const id = ulid4();
|
|
386
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
387
|
+
const ctx = input5.execution_context ?? "shell";
|
|
388
|
+
db.prepare(
|
|
389
|
+
`INSERT INTO sessions (id, user_id, task, execution_context, started_at)
|
|
390
|
+
VALUES (?, ?, ?, ?, ?)`
|
|
391
|
+
).run(id, input5.user_id, input5.task, ctx, now);
|
|
392
|
+
return db.prepare("SELECT * FROM sessions WHERE id = ?").get(id);
|
|
393
|
+
}
|
|
394
|
+
function endSession(db, sessionId) {
|
|
395
|
+
const session = db.prepare("SELECT * FROM sessions WHERE id = ?").get(sessionId);
|
|
396
|
+
if (!session) {
|
|
397
|
+
throw new Error(`Session not found: ${sessionId}`);
|
|
398
|
+
}
|
|
399
|
+
if (session.completed_at) {
|
|
400
|
+
throw new Error(`Session already completed: ${sessionId}`);
|
|
401
|
+
}
|
|
402
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
403
|
+
db.prepare("UPDATE sessions SET completed_at = ? WHERE id = ?").run(now, sessionId);
|
|
404
|
+
return db.prepare("SELECT * FROM sessions WHERE id = ?").get(sessionId);
|
|
405
|
+
}
|
|
406
|
+
function logStep(db, input5) {
|
|
407
|
+
if (input5.done_by !== "user" && input5.done_by !== "agent") {
|
|
408
|
+
throw new Error(`done_by must be 'user' or 'agent', got '${input5.done_by}'`);
|
|
409
|
+
}
|
|
410
|
+
if (input5.rating != null && (input5.rating < 1 || input5.rating > 4)) {
|
|
411
|
+
throw new Error(`Rating must be between 1 and 4, got ${input5.rating}`);
|
|
412
|
+
}
|
|
413
|
+
const session = db.prepare("SELECT id FROM sessions WHERE id = ?").get(input5.session_id);
|
|
414
|
+
if (!session) {
|
|
415
|
+
throw new Error(`Session not found: ${input5.session_id}`);
|
|
416
|
+
}
|
|
417
|
+
const id = ulid4();
|
|
418
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
419
|
+
db.prepare(
|
|
420
|
+
`INSERT INTO session_steps (id, session_id, token_id, done_by, rating, notes, created_at)
|
|
421
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`
|
|
422
|
+
).run(
|
|
423
|
+
id,
|
|
424
|
+
input5.session_id,
|
|
425
|
+
input5.token_id,
|
|
426
|
+
input5.done_by,
|
|
427
|
+
input5.rating ?? null,
|
|
428
|
+
input5.notes ?? null,
|
|
429
|
+
now
|
|
430
|
+
);
|
|
431
|
+
return db.prepare("SELECT * FROM session_steps WHERE id = ?").get(id);
|
|
432
|
+
}
|
|
433
|
+
function getSessionSummary(db, sessionId) {
|
|
434
|
+
const session = db.prepare("SELECT * FROM sessions WHERE id = ?").get(sessionId);
|
|
435
|
+
if (!session) {
|
|
436
|
+
throw new Error(`Session not found: ${sessionId}`);
|
|
437
|
+
}
|
|
438
|
+
const steps = db.prepare(
|
|
439
|
+
`SELECT ss.*, t.slug, t.concept, t.domain, t.bloom_level
|
|
440
|
+
FROM session_steps ss
|
|
441
|
+
JOIN tokens t ON t.id = ss.token_id
|
|
442
|
+
WHERE ss.session_id = ?
|
|
443
|
+
ORDER BY ss.created_at ASC`
|
|
444
|
+
).all(sessionId);
|
|
445
|
+
return { session, steps };
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// src/kernel/models/agent-skill.ts
|
|
449
|
+
import { ulid as ulid5 } from "ulid";
|
|
450
|
+
function parseRow(row) {
|
|
451
|
+
return {
|
|
452
|
+
...row,
|
|
453
|
+
steps: JSON.parse(row.steps),
|
|
454
|
+
token_slugs: JSON.parse(row.token_slugs)
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
function createAgentSkill(db, input5) {
|
|
458
|
+
const existing = db.prepare("SELECT * FROM agent_skills WHERE slug = ?").get(input5.slug);
|
|
459
|
+
if (existing) {
|
|
460
|
+
throw new Error(`Agent skill already exists: ${input5.slug}`);
|
|
461
|
+
}
|
|
462
|
+
const id = ulid5();
|
|
463
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
464
|
+
db.prepare(
|
|
465
|
+
`INSERT INTO agent_skills (id, slug, description, steps, token_slugs, source, created_at, updated_at)
|
|
466
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
|
|
467
|
+
).run(
|
|
468
|
+
id,
|
|
469
|
+
input5.slug,
|
|
470
|
+
input5.description,
|
|
471
|
+
JSON.stringify(input5.steps),
|
|
472
|
+
JSON.stringify(input5.token_slugs ?? []),
|
|
473
|
+
input5.source ?? "learned",
|
|
474
|
+
now,
|
|
475
|
+
now
|
|
476
|
+
);
|
|
477
|
+
return parseRow(
|
|
478
|
+
db.prepare("SELECT * FROM agent_skills WHERE id = ?").get(id)
|
|
479
|
+
);
|
|
480
|
+
}
|
|
481
|
+
function getAgentSkill(db, slug) {
|
|
482
|
+
const row = db.prepare("SELECT * FROM agent_skills WHERE slug = ?").get(slug);
|
|
483
|
+
return row ? parseRow(row) : void 0;
|
|
484
|
+
}
|
|
485
|
+
function listAgentSkills(db) {
|
|
486
|
+
const rows = db.prepare("SELECT * FROM agent_skills ORDER BY created_at ASC").all();
|
|
487
|
+
return rows.map(parseRow);
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// src/kernel/models/settings.ts
|
|
491
|
+
function getSetting(db, key) {
|
|
492
|
+
const row = db.prepare("SELECT value FROM user_config WHERE key = ?").get(key);
|
|
493
|
+
return row?.value;
|
|
494
|
+
}
|
|
495
|
+
function getAllSettings(db) {
|
|
496
|
+
const rows = db.prepare("SELECT key, value FROM user_config ORDER BY key").all();
|
|
497
|
+
const map = {};
|
|
498
|
+
for (const row of rows) {
|
|
499
|
+
map[row.key] = row.value;
|
|
500
|
+
}
|
|
501
|
+
return map;
|
|
502
|
+
}
|
|
503
|
+
function getAllSettingsDetailed(db) {
|
|
504
|
+
return db.prepare("SELECT key, value, updated_at FROM user_config ORDER BY key").all();
|
|
505
|
+
}
|
|
506
|
+
function setSetting(db, key, value) {
|
|
507
|
+
db.prepare(
|
|
508
|
+
`INSERT INTO user_config (key, value, updated_at)
|
|
509
|
+
VALUES (?, ?, datetime('now'))
|
|
510
|
+
ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at`
|
|
511
|
+
).run(key, value);
|
|
512
|
+
}
|
|
513
|
+
function deleteSetting(db, key) {
|
|
514
|
+
const result = db.prepare("DELETE FROM user_config WHERE key = ?").run(key);
|
|
515
|
+
return result.changes > 0;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
// src/kernel/scheduler/fsrs.ts
|
|
519
|
+
var DEFAULT_W = [
|
|
520
|
+
0.4072,
|
|
521
|
+
1.1829,
|
|
522
|
+
3.1262,
|
|
523
|
+
15.4722,
|
|
524
|
+
// w0–w3: initial stability per rating
|
|
525
|
+
7.2102,
|
|
526
|
+
0.5316,
|
|
527
|
+
1.0651,
|
|
528
|
+
// w4–w6: difficulty
|
|
529
|
+
92e-4,
|
|
530
|
+
1.5988,
|
|
531
|
+
0.1176,
|
|
532
|
+
1.0014,
|
|
533
|
+
// w7–w10: stability after forgetting
|
|
534
|
+
2.0032,
|
|
535
|
+
0.0266,
|
|
536
|
+
0.3077,
|
|
537
|
+
0.15,
|
|
538
|
+
// w11–w14: stability increase
|
|
539
|
+
0,
|
|
540
|
+
2.7849,
|
|
541
|
+
0.3477,
|
|
542
|
+
0.6831
|
|
543
|
+
// w15–w18: additional parameters
|
|
544
|
+
];
|
|
545
|
+
var DEFAULT_REQUEST_RETENTION = 0.9;
|
|
546
|
+
function clamp(value, lo, hi) {
|
|
547
|
+
return Math.min(hi, Math.max(lo, value));
|
|
548
|
+
}
|
|
549
|
+
function daysBetween(a, b) {
|
|
550
|
+
return (b.getTime() - a.getTime()) / (1e3 * 60 * 60 * 24);
|
|
551
|
+
}
|
|
552
|
+
function initialStability(w, rating) {
|
|
553
|
+
return w[rating - 1];
|
|
554
|
+
}
|
|
555
|
+
function initialDifficulty(w, rating) {
|
|
556
|
+
return clamp(w[4] - Math.exp(w[5] * (rating - 1)) + 1, 1, 10);
|
|
557
|
+
}
|
|
558
|
+
function nextDifficulty(w, d, rating) {
|
|
559
|
+
const d0ForGood = initialDifficulty(w, 3);
|
|
560
|
+
const updated = w[7] * d0ForGood + (1 - w[7]) * (d - w[6] * (rating - 3));
|
|
561
|
+
return clamp(updated, 1, 10);
|
|
562
|
+
}
|
|
563
|
+
function retrievability(elapsed, stability) {
|
|
564
|
+
if (stability <= 0) return 0;
|
|
565
|
+
return Math.pow(1 + elapsed / (9 * stability), -1);
|
|
566
|
+
}
|
|
567
|
+
function stabilityAfterSuccess(w, s, d, r, rating) {
|
|
568
|
+
const hardPenalty = rating === 2 ? w[15] : 1;
|
|
569
|
+
const easyBonus = rating === 4 ? w[16] : 1;
|
|
570
|
+
const inner = Math.exp(w[8]) * (11 - d) * Math.pow(s, -w[9]) * (Math.exp(w[10] * (1 - r)) - 1) * hardPenalty * easyBonus;
|
|
571
|
+
return s * (inner + 1);
|
|
572
|
+
}
|
|
573
|
+
function stabilityAfterForgetting(w, s, d, r) {
|
|
574
|
+
return w[11] * Math.pow(d, -w[12]) * (Math.pow(s + 1, w[13]) - 1) * Math.exp(w[14] * (1 - r));
|
|
575
|
+
}
|
|
576
|
+
function nextInterval(stability, requestRetention) {
|
|
577
|
+
const interval = 9 * stability * (1 / requestRetention - 1);
|
|
578
|
+
return Math.max(1, Math.round(interval));
|
|
579
|
+
}
|
|
580
|
+
function createFSRS(params) {
|
|
581
|
+
const resolvedParams = {
|
|
582
|
+
w: params?.w ?? [...DEFAULT_W],
|
|
583
|
+
requestRetention: params?.requestRetention ?? DEFAULT_REQUEST_RETENTION
|
|
584
|
+
};
|
|
585
|
+
function schedule(card, rating, now) {
|
|
586
|
+
const reviewTime = now ?? /* @__PURE__ */ new Date();
|
|
587
|
+
const w = resolvedParams.w;
|
|
588
|
+
const elapsed = card.lastReviewAt !== null ? Math.max(0, daysBetween(card.lastReviewAt, reviewTime)) : 0;
|
|
589
|
+
if (card.state === "new") {
|
|
590
|
+
const s = initialStability(w, rating);
|
|
591
|
+
const d = initialDifficulty(w, rating);
|
|
592
|
+
const interval2 = nextInterval(s, resolvedParams.requestRetention);
|
|
593
|
+
const dueAt2 = new Date(reviewTime);
|
|
594
|
+
dueAt2.setDate(dueAt2.getDate() + interval2);
|
|
595
|
+
return {
|
|
596
|
+
stability: s,
|
|
597
|
+
difficulty: d,
|
|
598
|
+
elapsedDays: 0,
|
|
599
|
+
scheduledDays: interval2,
|
|
600
|
+
reps: rating >= 2 ? 1 : 0,
|
|
601
|
+
lapses: rating === 1 ? 1 : 0,
|
|
602
|
+
state: "learning",
|
|
603
|
+
dueAt: dueAt2,
|
|
604
|
+
lastReviewAt: reviewTime
|
|
605
|
+
};
|
|
606
|
+
}
|
|
607
|
+
const r = retrievability(elapsed, card.stability);
|
|
608
|
+
let newStability;
|
|
609
|
+
let newDifficulty;
|
|
610
|
+
let newReps;
|
|
611
|
+
let newLapses;
|
|
612
|
+
let newState;
|
|
613
|
+
if (rating === 1) {
|
|
614
|
+
newStability = stabilityAfterForgetting(w, card.stability, card.difficulty, r);
|
|
615
|
+
newDifficulty = nextDifficulty(w, card.difficulty, rating);
|
|
616
|
+
newReps = 0;
|
|
617
|
+
newLapses = card.lapses + 1;
|
|
618
|
+
newState = "relearning";
|
|
619
|
+
} else {
|
|
620
|
+
newStability = stabilityAfterSuccess(w, card.stability, card.difficulty, r, rating);
|
|
621
|
+
newDifficulty = nextDifficulty(w, card.difficulty, rating);
|
|
622
|
+
newReps = card.reps + 1;
|
|
623
|
+
newLapses = card.lapses;
|
|
624
|
+
newState = "review";
|
|
625
|
+
}
|
|
626
|
+
const interval = nextInterval(newStability, resolvedParams.requestRetention);
|
|
627
|
+
const dueAt = new Date(reviewTime);
|
|
628
|
+
dueAt.setDate(dueAt.getDate() + interval);
|
|
629
|
+
return {
|
|
630
|
+
stability: newStability,
|
|
631
|
+
difficulty: newDifficulty,
|
|
632
|
+
elapsedDays: elapsed,
|
|
633
|
+
scheduledDays: interval,
|
|
634
|
+
reps: newReps,
|
|
635
|
+
lapses: newLapses,
|
|
636
|
+
state: newState,
|
|
637
|
+
dueAt,
|
|
638
|
+
lastReviewAt: reviewTime
|
|
639
|
+
};
|
|
640
|
+
}
|
|
641
|
+
return {
|
|
642
|
+
schedule,
|
|
643
|
+
params: Object.freeze(resolvedParams)
|
|
644
|
+
};
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
// src/kernel/scheduler/blocker.ts
|
|
648
|
+
function cascadeBlock(db, userId, tokenSlug) {
|
|
649
|
+
const token = getTokenBySlug(db, tokenSlug);
|
|
650
|
+
if (!token) {
|
|
651
|
+
throw new Error(`Unknown token slug: ${tokenSlug}`);
|
|
652
|
+
}
|
|
653
|
+
ensureCard(db, token.id, userId);
|
|
654
|
+
db.prepare(
|
|
655
|
+
"UPDATE cards SET blocked = 1 WHERE token_id = ? AND user_id = ?"
|
|
656
|
+
).run(token.id, userId);
|
|
657
|
+
const prereqs = getPrerequisites(db, token.id);
|
|
658
|
+
const surfaced = [];
|
|
659
|
+
for (const prereq of prereqs) {
|
|
660
|
+
const card = ensureCard(db, prereq.requires_id, userId);
|
|
661
|
+
if (card.blocked === 1) {
|
|
662
|
+
const prereqOfPrereq = db.prepare("SELECT COUNT(*) as n FROM prerequisites WHERE token_id = ?").get(prereq.requires_id);
|
|
663
|
+
if (prereqOfPrereq.n === 0) {
|
|
664
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
665
|
+
db.prepare(
|
|
666
|
+
"UPDATE cards SET blocked = 0, due_at = ? WHERE token_id = ? AND user_id = ?"
|
|
667
|
+
).run(now, prereq.requires_id, userId);
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
surfaced.push({
|
|
671
|
+
slug: prereq.slug,
|
|
672
|
+
concept: prereq.concept,
|
|
673
|
+
bloomLevel: prereq.bloom_level
|
|
674
|
+
});
|
|
675
|
+
}
|
|
676
|
+
return {
|
|
677
|
+
blockedSlug: tokenSlug,
|
|
678
|
+
prerequisites: surfaced
|
|
679
|
+
};
|
|
680
|
+
}
|
|
681
|
+
function unblockReady(db, userId) {
|
|
682
|
+
const blockedCards = db.prepare(
|
|
683
|
+
`SELECT c.token_id, t.slug, t.concept
|
|
684
|
+
FROM cards c
|
|
685
|
+
JOIN tokens t ON t.id = c.token_id
|
|
686
|
+
WHERE c.user_id = ? AND c.blocked = 1`
|
|
687
|
+
).all(userId);
|
|
688
|
+
const unblocked = [];
|
|
689
|
+
for (const card of blockedCards) {
|
|
690
|
+
const totalPrereqs = db.prepare("SELECT COUNT(*) as n FROM prerequisites WHERE token_id = ?").get(card.token_id);
|
|
691
|
+
const metPrereqs = db.prepare(
|
|
692
|
+
`SELECT COUNT(*) as n FROM cards c
|
|
693
|
+
JOIN prerequisites p ON p.requires_id = c.token_id
|
|
694
|
+
WHERE p.token_id = ? AND c.user_id = ? AND c.reps >= 1 AND c.blocked = 0`
|
|
695
|
+
).get(card.token_id, userId);
|
|
696
|
+
if (totalPrereqs.n === 0 || metPrereqs.n === totalPrereqs.n) {
|
|
697
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
698
|
+
db.prepare(
|
|
699
|
+
"UPDATE cards SET blocked = 0, due_at = ? WHERE token_id = ? AND user_id = ?"
|
|
700
|
+
).run(now, card.token_id, userId);
|
|
701
|
+
unblocked.push({ slug: card.slug, concept: card.concept });
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
return { unblocked };
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
// src/kernel/scheduler/interleaver.ts
|
|
708
|
+
function interleave(items, maxConsecutive = 2) {
|
|
709
|
+
if (items.length <= 1) return [...items];
|
|
710
|
+
const byDomain = /* @__PURE__ */ new Map();
|
|
711
|
+
for (const item of items) {
|
|
712
|
+
const group = byDomain.get(item.domain);
|
|
713
|
+
if (group) {
|
|
714
|
+
group.push(item);
|
|
715
|
+
} else {
|
|
716
|
+
byDomain.set(item.domain, [item]);
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
if (byDomain.size === 1) return [...items];
|
|
720
|
+
const result = [];
|
|
721
|
+
let consecutiveCount = 0;
|
|
722
|
+
let lastDomain = null;
|
|
723
|
+
const cursors = /* @__PURE__ */ new Map();
|
|
724
|
+
for (const domain of byDomain.keys()) {
|
|
725
|
+
cursors.set(domain, 0);
|
|
726
|
+
}
|
|
727
|
+
while (result.length < items.length) {
|
|
728
|
+
const activeDomains = [...byDomain.entries()].filter(([domain]) => cursors.get(domain) < byDomain.get(domain).length).sort((a, b) => {
|
|
729
|
+
const remainA = a[1].length - cursors.get(a[0]);
|
|
730
|
+
const remainB = b[1].length - cursors.get(b[0]);
|
|
731
|
+
return remainB - remainA;
|
|
732
|
+
});
|
|
733
|
+
if (activeDomains.length === 0) break;
|
|
734
|
+
let pickedThisRound = false;
|
|
735
|
+
for (const [domain, group] of activeDomains) {
|
|
736
|
+
const cursor = cursors.get(domain);
|
|
737
|
+
if (cursor >= group.length) continue;
|
|
738
|
+
if (domain === lastDomain && consecutiveCount >= maxConsecutive) {
|
|
739
|
+
continue;
|
|
740
|
+
}
|
|
741
|
+
result.push(group[cursor]);
|
|
742
|
+
cursors.set(domain, cursor + 1);
|
|
743
|
+
pickedThisRound = true;
|
|
744
|
+
if (domain === lastDomain) {
|
|
745
|
+
consecutiveCount++;
|
|
746
|
+
} else {
|
|
747
|
+
lastDomain = domain;
|
|
748
|
+
consecutiveCount = 1;
|
|
749
|
+
}
|
|
750
|
+
break;
|
|
751
|
+
}
|
|
752
|
+
if (!pickedThisRound) {
|
|
753
|
+
for (const [domain, group] of activeDomains) {
|
|
754
|
+
const cursor = cursors.get(domain);
|
|
755
|
+
if (cursor >= group.length) continue;
|
|
756
|
+
result.push(group[cursor]);
|
|
757
|
+
cursors.set(domain, cursor + 1);
|
|
758
|
+
if (domain === lastDomain) {
|
|
759
|
+
consecutiveCount++;
|
|
760
|
+
} else {
|
|
761
|
+
lastDomain = domain;
|
|
762
|
+
consecutiveCount = 1;
|
|
763
|
+
}
|
|
764
|
+
break;
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
return result;
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
// src/kernel/scheduler/queue.ts
|
|
772
|
+
function buildReviewQueue(db, options) {
|
|
773
|
+
const maxNew = options.maxNew ?? 10;
|
|
774
|
+
const maxReviews = options.maxReviews ?? 50;
|
|
775
|
+
const now = options.now ?? /* @__PURE__ */ new Date();
|
|
776
|
+
const nowISO = now.toISOString();
|
|
777
|
+
const dueRows = db.prepare(
|
|
778
|
+
`SELECT
|
|
779
|
+
c.id AS card_id,
|
|
780
|
+
c.token_id AS token_id,
|
|
781
|
+
t.slug AS slug,
|
|
782
|
+
t.concept AS concept,
|
|
783
|
+
t.domain AS domain,
|
|
784
|
+
t.bloom_level AS bloom_level,
|
|
785
|
+
c.state AS state,
|
|
786
|
+
c.due_at AS due_at
|
|
787
|
+
FROM cards c
|
|
788
|
+
JOIN tokens t ON t.id = c.token_id
|
|
789
|
+
WHERE c.user_id = ?
|
|
790
|
+
AND c.blocked = 0
|
|
791
|
+
AND c.due_at <= ?
|
|
792
|
+
AND c.state IN ('review', 'relearning', 'learning')
|
|
793
|
+
AND t.deprecated_at IS NULL
|
|
794
|
+
ORDER BY c.due_at ASC`
|
|
795
|
+
).all(options.userId, nowISO);
|
|
796
|
+
const newRows = db.prepare(
|
|
797
|
+
`SELECT
|
|
798
|
+
c.id AS card_id,
|
|
799
|
+
c.token_id AS token_id,
|
|
800
|
+
t.slug AS slug,
|
|
801
|
+
t.concept AS concept,
|
|
802
|
+
t.domain AS domain,
|
|
803
|
+
t.bloom_level AS bloom_level,
|
|
804
|
+
c.state AS state,
|
|
805
|
+
c.due_at AS due_at
|
|
806
|
+
FROM cards c
|
|
807
|
+
JOIN tokens t ON t.id = c.token_id
|
|
808
|
+
WHERE c.user_id = ?
|
|
809
|
+
AND c.blocked = 0
|
|
810
|
+
AND c.state = 'new'
|
|
811
|
+
AND t.deprecated_at IS NULL
|
|
812
|
+
ORDER BY t.bloom_level ASC, t.slug ASC
|
|
813
|
+
LIMIT ?`
|
|
814
|
+
).all(options.userId, maxNew);
|
|
815
|
+
const nowMs = now.getTime();
|
|
816
|
+
const sortedDue = [...dueRows].sort((a, b) => {
|
|
817
|
+
const overdueA = nowMs - new Date(a.due_at).getTime();
|
|
818
|
+
const overdueB = nowMs - new Date(b.due_at).getTime();
|
|
819
|
+
return overdueB - overdueA;
|
|
820
|
+
});
|
|
821
|
+
const interleavedDue = interleave(
|
|
822
|
+
sortedDue.map((row) => ({ ...rowToItem(row), domain: row.domain }))
|
|
823
|
+
);
|
|
824
|
+
const newItems = newRows.map(rowToItem);
|
|
825
|
+
const merged = intersperseNew(interleavedDue, newItems, 5);
|
|
826
|
+
const capped = merged.slice(0, maxReviews);
|
|
827
|
+
let newCount = 0;
|
|
828
|
+
let reviewCount = 0;
|
|
829
|
+
let relearnCount = 0;
|
|
830
|
+
const domainSet = /* @__PURE__ */ new Set();
|
|
831
|
+
for (const item of capped) {
|
|
832
|
+
domainSet.add(item.domain);
|
|
833
|
+
switch (item.state) {
|
|
834
|
+
case "new":
|
|
835
|
+
newCount++;
|
|
836
|
+
break;
|
|
837
|
+
case "relearning":
|
|
838
|
+
relearnCount++;
|
|
839
|
+
break;
|
|
840
|
+
default:
|
|
841
|
+
reviewCount++;
|
|
842
|
+
break;
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
return {
|
|
846
|
+
items: capped,
|
|
847
|
+
newCount,
|
|
848
|
+
reviewCount,
|
|
849
|
+
relearnCount,
|
|
850
|
+
totalDomains: [...domainSet].sort()
|
|
851
|
+
};
|
|
852
|
+
}
|
|
853
|
+
function rowToItem(row) {
|
|
854
|
+
return {
|
|
855
|
+
cardId: row.card_id,
|
|
856
|
+
tokenId: row.token_id,
|
|
857
|
+
slug: row.slug,
|
|
858
|
+
concept: row.concept,
|
|
859
|
+
domain: row.domain,
|
|
860
|
+
bloomLevel: row.bloom_level,
|
|
861
|
+
state: row.state,
|
|
862
|
+
dueAt: row.due_at
|
|
863
|
+
};
|
|
864
|
+
}
|
|
865
|
+
function intersperseNew(reviews, newCards, interval) {
|
|
866
|
+
if (newCards.length === 0) return [...reviews];
|
|
867
|
+
if (reviews.length === 0) return [...newCards];
|
|
868
|
+
const result = [];
|
|
869
|
+
let reviewIdx = 0;
|
|
870
|
+
let newIdx = 0;
|
|
871
|
+
let position = 0;
|
|
872
|
+
while (reviewIdx < reviews.length || newIdx < newCards.length) {
|
|
873
|
+
if (newIdx < newCards.length && position > 0 && position % interval === interval - 1) {
|
|
874
|
+
result.push(newCards[newIdx]);
|
|
875
|
+
newIdx++;
|
|
876
|
+
} else if (reviewIdx < reviews.length) {
|
|
877
|
+
result.push(reviews[reviewIdx]);
|
|
878
|
+
reviewIdx++;
|
|
879
|
+
} else if (newIdx < newCards.length) {
|
|
880
|
+
result.push(newCards[newIdx]);
|
|
881
|
+
newIdx++;
|
|
882
|
+
}
|
|
883
|
+
position++;
|
|
884
|
+
}
|
|
885
|
+
return result;
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
// src/kernel/recall/prompter.ts
|
|
889
|
+
var BLOOM_VERBS = {
|
|
890
|
+
1: "Remember",
|
|
891
|
+
2: "Understand",
|
|
892
|
+
3: "Apply",
|
|
893
|
+
4: "Analyze",
|
|
894
|
+
5: "Synthesize"
|
|
895
|
+
};
|
|
896
|
+
var BLOOM_PROMPTS = {
|
|
897
|
+
1: (c) => `What is: ${c}?`,
|
|
898
|
+
2: (c) => `Explain how this works: ${c}`,
|
|
899
|
+
3: (c) => `Apply this concept: ${c}`,
|
|
900
|
+
4: (c) => `Analyze the trade-offs: ${c}`,
|
|
901
|
+
5: (c) => `Design a solution using: ${c}`
|
|
902
|
+
};
|
|
903
|
+
function generatePrompt(input5) {
|
|
904
|
+
const bloom = input5.bloomLevel >= 1 && input5.bloomLevel <= 5 ? input5.bloomLevel : 1;
|
|
905
|
+
return {
|
|
906
|
+
cardId: input5.cardId,
|
|
907
|
+
tokenId: input5.tokenId,
|
|
908
|
+
slug: input5.slug,
|
|
909
|
+
question: BLOOM_PROMPTS[bloom](input5.concept),
|
|
910
|
+
concept: input5.concept,
|
|
911
|
+
domain: input5.domain,
|
|
912
|
+
bloomLevel: bloom,
|
|
913
|
+
bloomVerb: BLOOM_VERBS[bloom],
|
|
914
|
+
hints: []
|
|
915
|
+
};
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
// src/kernel/recall/evaluator.ts
|
|
919
|
+
import { ulid as ulid6 } from "ulid";
|
|
920
|
+
function evaluateRating(db, input5) {
|
|
921
|
+
const card = db.prepare("SELECT * FROM cards WHERE id = ?").get(input5.cardId);
|
|
922
|
+
if (!card) {
|
|
923
|
+
throw new Error(`Card not found: ${input5.cardId}`);
|
|
924
|
+
}
|
|
925
|
+
const now = /* @__PURE__ */ new Date();
|
|
926
|
+
const fsrs = createFSRS();
|
|
927
|
+
const schedulingCard = {
|
|
928
|
+
stability: card.stability,
|
|
929
|
+
difficulty: card.difficulty,
|
|
930
|
+
elapsedDays: card.elapsed_days,
|
|
931
|
+
scheduledDays: card.scheduled_days,
|
|
932
|
+
reps: card.reps,
|
|
933
|
+
lapses: card.lapses,
|
|
934
|
+
state: card.state,
|
|
935
|
+
dueAt: new Date(card.due_at),
|
|
936
|
+
lastReviewAt: card.last_review_at ? new Date(card.last_review_at) : null
|
|
937
|
+
};
|
|
938
|
+
const updated = fsrs.schedule(schedulingCard, input5.rating, now);
|
|
939
|
+
updateCard(db, input5.cardId, {
|
|
940
|
+
stability: updated.stability,
|
|
941
|
+
difficulty: updated.difficulty,
|
|
942
|
+
elapsed_days: updated.elapsedDays,
|
|
943
|
+
scheduled_days: updated.scheduledDays,
|
|
944
|
+
reps: updated.reps,
|
|
945
|
+
lapses: updated.lapses,
|
|
946
|
+
state: updated.state,
|
|
947
|
+
due_at: updated.dueAt.toISOString(),
|
|
948
|
+
last_review_at: now.toISOString()
|
|
949
|
+
});
|
|
950
|
+
db.prepare(
|
|
951
|
+
`INSERT INTO review_logs (id, card_id, token_id, user_id, rating, response_time_ms, reviewed_at, scheduled_at, session_id)
|
|
952
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
953
|
+
).run(
|
|
954
|
+
ulid6(),
|
|
955
|
+
input5.cardId,
|
|
956
|
+
input5.tokenId,
|
|
957
|
+
input5.userId,
|
|
958
|
+
input5.rating,
|
|
959
|
+
input5.responseTimeMs ?? null,
|
|
960
|
+
now.toISOString(),
|
|
961
|
+
card.due_at,
|
|
962
|
+
input5.sessionId ?? null
|
|
963
|
+
);
|
|
964
|
+
return {
|
|
965
|
+
nextDueAt: updated.dueAt.toISOString(),
|
|
966
|
+
stability: updated.stability,
|
|
967
|
+
difficulty: updated.difficulty,
|
|
968
|
+
state: updated.state,
|
|
969
|
+
scheduledDays: updated.scheduledDays,
|
|
970
|
+
reps: updated.reps,
|
|
971
|
+
lapses: updated.lapses
|
|
972
|
+
};
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
// src/kernel/analytics/stats.ts
|
|
976
|
+
function q(db, sql, ...params) {
|
|
977
|
+
return db.prepare(sql).get(...params);
|
|
978
|
+
}
|
|
979
|
+
function getUserStats(db, userId) {
|
|
980
|
+
return {
|
|
981
|
+
userId,
|
|
982
|
+
totalTokens: q(db, "SELECT COUNT(*) as n FROM tokens").n,
|
|
983
|
+
cardsInDeck: q(db, "SELECT COUNT(*) as n FROM cards WHERE user_id = ?", userId).n,
|
|
984
|
+
dueToday: q(
|
|
985
|
+
db,
|
|
986
|
+
"SELECT COUNT(*) as n FROM cards WHERE user_id = ? AND blocked = 0 AND due_at <= datetime('now')",
|
|
987
|
+
userId
|
|
988
|
+
).n,
|
|
989
|
+
blocked: q(db, "SELECT COUNT(*) as n FROM cards WHERE user_id = ? AND blocked = 1", userId).n,
|
|
990
|
+
mature: q(
|
|
991
|
+
db,
|
|
992
|
+
"SELECT COUNT(*) as n FROM cards WHERE user_id = ? AND reps >= 3 AND stability >= 21",
|
|
993
|
+
userId
|
|
994
|
+
).n,
|
|
995
|
+
avgStability: (() => {
|
|
996
|
+
const v = q(db, "SELECT AVG(stability) as v FROM cards WHERE user_id = ? AND reps > 0", userId);
|
|
997
|
+
return v.v ? Math.round(v.v * 100) / 100 : null;
|
|
998
|
+
})(),
|
|
999
|
+
totalSessions: q(db, "SELECT COUNT(*) as n FROM sessions WHERE user_id = ?", userId).n,
|
|
1000
|
+
lastSession: (() => {
|
|
1001
|
+
const r = db.prepare(
|
|
1002
|
+
"SELECT started_at FROM sessions WHERE user_id = ? ORDER BY started_at DESC LIMIT 1"
|
|
1003
|
+
).get(userId);
|
|
1004
|
+
return r?.started_at ?? null;
|
|
1005
|
+
})()
|
|
1006
|
+
};
|
|
1007
|
+
}
|
|
1008
|
+
function getDomainCompetence(db, userId) {
|
|
1009
|
+
const domains = db.prepare(
|
|
1010
|
+
`SELECT DISTINCT t.domain FROM cards c
|
|
1011
|
+
JOIN tokens t ON t.id = c.token_id
|
|
1012
|
+
WHERE c.user_id = ? AND t.domain != ''`
|
|
1013
|
+
).all(userId);
|
|
1014
|
+
return domains.map((d) => {
|
|
1015
|
+
const total = q(
|
|
1016
|
+
db,
|
|
1017
|
+
`SELECT COUNT(*) as n FROM cards c
|
|
1018
|
+
JOIN tokens t ON t.id = c.token_id
|
|
1019
|
+
WHERE c.user_id = ? AND t.domain = ?`,
|
|
1020
|
+
userId,
|
|
1021
|
+
d.domain
|
|
1022
|
+
).n;
|
|
1023
|
+
const mature = q(
|
|
1024
|
+
db,
|
|
1025
|
+
`SELECT COUNT(*) as n FROM cards c
|
|
1026
|
+
JOIN tokens t ON t.id = c.token_id
|
|
1027
|
+
WHERE c.user_id = ? AND t.domain = ? AND c.reps >= 3 AND c.stability >= 21`,
|
|
1028
|
+
userId,
|
|
1029
|
+
d.domain
|
|
1030
|
+
).n;
|
|
1031
|
+
const avgStab = q(
|
|
1032
|
+
db,
|
|
1033
|
+
`SELECT AVG(c.stability) as v FROM cards c
|
|
1034
|
+
JOIN tokens t ON t.id = c.token_id
|
|
1035
|
+
WHERE c.user_id = ? AND t.domain = ? AND c.reps > 0`,
|
|
1036
|
+
userId,
|
|
1037
|
+
d.domain
|
|
1038
|
+
).v ?? 0;
|
|
1039
|
+
const reviews = q(
|
|
1040
|
+
db,
|
|
1041
|
+
`SELECT COUNT(*) as total,
|
|
1042
|
+
SUM(CASE WHEN rating >= 2 THEN 1 ELSE 0 END) as passed
|
|
1043
|
+
FROM review_logs
|
|
1044
|
+
WHERE user_id = ? AND token_id IN (SELECT id FROM tokens WHERE domain = ?)`,
|
|
1045
|
+
userId,
|
|
1046
|
+
d.domain
|
|
1047
|
+
);
|
|
1048
|
+
const retentionRate = reviews.total > 0 ? reviews.passed / reviews.total : 0;
|
|
1049
|
+
let suggestedMode;
|
|
1050
|
+
if (retentionRate > 0.9 && avgStab > 30) {
|
|
1051
|
+
suggestedMode = "autonomy";
|
|
1052
|
+
} else if (retentionRate > 0.7 && avgStab > 7) {
|
|
1053
|
+
suggestedMode = "copilot";
|
|
1054
|
+
} else {
|
|
1055
|
+
suggestedMode = "shadowing";
|
|
1056
|
+
}
|
|
1057
|
+
return {
|
|
1058
|
+
domain: d.domain,
|
|
1059
|
+
totalCards: total,
|
|
1060
|
+
matureCards: mature,
|
|
1061
|
+
avgStability: Math.round(avgStab * 100) / 100,
|
|
1062
|
+
retentionRate: Math.round(retentionRate * 1e3) / 1e3,
|
|
1063
|
+
suggestedMode
|
|
1064
|
+
};
|
|
1065
|
+
});
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
// src/kernel/observation/analyzer.ts
|
|
1069
|
+
function parseMonitorLog(jsonl) {
|
|
1070
|
+
const events = [];
|
|
1071
|
+
for (const line of jsonl.split("\n")) {
|
|
1072
|
+
const trimmed = line.trim();
|
|
1073
|
+
if (!trimmed) continue;
|
|
1074
|
+
try {
|
|
1075
|
+
events.push(JSON.parse(trimmed));
|
|
1076
|
+
} catch {
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
return events;
|
|
1080
|
+
}
|
|
1081
|
+
function pairCommands(events) {
|
|
1082
|
+
const starts = /* @__PURE__ */ new Map();
|
|
1083
|
+
const records = [];
|
|
1084
|
+
for (const e of events) {
|
|
1085
|
+
if (e.type === "command_start" && e.seq != null) {
|
|
1086
|
+
const key = `${e.pid ?? 0}:${e.seq}`;
|
|
1087
|
+
starts.set(key, e);
|
|
1088
|
+
} else if (e.type === "command_end" && e.seq != null) {
|
|
1089
|
+
const key = `${e.pid ?? 0}:${e.seq}`;
|
|
1090
|
+
const start = starts.get(key);
|
|
1091
|
+
if (start) {
|
|
1092
|
+
const startMs = new Date(start.ts).getTime();
|
|
1093
|
+
const endMs = new Date(e.ts).getTime();
|
|
1094
|
+
records.push({
|
|
1095
|
+
seq: e.seq,
|
|
1096
|
+
pid: e.pid ?? 0,
|
|
1097
|
+
command: start.command ?? "",
|
|
1098
|
+
cwd: start.cwd ?? "",
|
|
1099
|
+
startedAt: start.ts,
|
|
1100
|
+
endedAt: e.ts,
|
|
1101
|
+
durationMs: endMs - startMs,
|
|
1102
|
+
exitCode: e.exit_code ?? null
|
|
1103
|
+
});
|
|
1104
|
+
starts.delete(key);
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
for (const [, start] of starts) {
|
|
1109
|
+
records.push({
|
|
1110
|
+
seq: start.seq ?? 0,
|
|
1111
|
+
pid: start.pid ?? 0,
|
|
1112
|
+
command: start.command ?? "",
|
|
1113
|
+
cwd: start.cwd ?? "",
|
|
1114
|
+
startedAt: start.ts,
|
|
1115
|
+
endedAt: null,
|
|
1116
|
+
durationMs: null,
|
|
1117
|
+
exitCode: null
|
|
1118
|
+
});
|
|
1119
|
+
}
|
|
1120
|
+
records.sort((a, b) => new Date(a.startedAt).getTime() - new Date(b.startedAt).getTime());
|
|
1121
|
+
return records;
|
|
1122
|
+
}
|
|
1123
|
+
var HELP_PATTERNS = ["--help", "man ", "tldr ", "help "];
|
|
1124
|
+
var HELP_WINDOW_MS = 6e4;
|
|
1125
|
+
function matchesToken(command, patterns) {
|
|
1126
|
+
const lower = command.toLowerCase();
|
|
1127
|
+
return patterns.some((p) => lower.includes(p.toLowerCase()));
|
|
1128
|
+
}
|
|
1129
|
+
function isHelpCommand(command) {
|
|
1130
|
+
const lower = command.toLowerCase();
|
|
1131
|
+
return HELP_PATTERNS.some((p) => lower.includes(p));
|
|
1132
|
+
}
|
|
1133
|
+
function commandPrefix(command) {
|
|
1134
|
+
return command.split(/\s+/).slice(0, 2).join(" ").toLowerCase();
|
|
1135
|
+
}
|
|
1136
|
+
function computeMedian(values) {
|
|
1137
|
+
if (values.length === 0) return null;
|
|
1138
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
1139
|
+
const mid = Math.floor(sorted.length / 2);
|
|
1140
|
+
return sorted.length % 2 !== 0 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2;
|
|
1141
|
+
}
|
|
1142
|
+
function analyzeObservation(commands, tokenPatterns) {
|
|
1143
|
+
const matchedSet = /* @__PURE__ */ new Set();
|
|
1144
|
+
const ratings = [];
|
|
1145
|
+
for (const tp of tokenPatterns) {
|
|
1146
|
+
const matchIndices = [];
|
|
1147
|
+
const matchedTexts = [];
|
|
1148
|
+
for (let i = 0; i < commands.length; i++) {
|
|
1149
|
+
if (matchesToken(commands[i].command, tp.patterns)) {
|
|
1150
|
+
matchIndices.push(i);
|
|
1151
|
+
matchedTexts.push(commands[i].command);
|
|
1152
|
+
matchedSet.add(i);
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
if (matchIndices.length === 0) {
|
|
1156
|
+
ratings.push({
|
|
1157
|
+
tokenSlug: tp.slug,
|
|
1158
|
+
rating: null,
|
|
1159
|
+
confidence: "low",
|
|
1160
|
+
evidence: {
|
|
1161
|
+
matchedCommands: 0,
|
|
1162
|
+
helpSeeking: false,
|
|
1163
|
+
errorCount: 0,
|
|
1164
|
+
selfCorrections: 0,
|
|
1165
|
+
medianGapMs: null,
|
|
1166
|
+
thinkingGapMs: null
|
|
1167
|
+
},
|
|
1168
|
+
matchedCommandTexts: []
|
|
1169
|
+
});
|
|
1170
|
+
continue;
|
|
1171
|
+
}
|
|
1172
|
+
let helpSeeking = false;
|
|
1173
|
+
for (const mi of matchIndices) {
|
|
1174
|
+
const matchTime = new Date(commands[mi].startedAt).getTime();
|
|
1175
|
+
for (let j = 0; j < commands.length; j++) {
|
|
1176
|
+
if (j === mi) continue;
|
|
1177
|
+
const cmdTime = new Date(commands[j].startedAt).getTime();
|
|
1178
|
+
if (cmdTime >= matchTime - HELP_WINDOW_MS && cmdTime < matchTime) {
|
|
1179
|
+
if (isHelpCommand(commands[j].command)) {
|
|
1180
|
+
helpSeeking = true;
|
|
1181
|
+
break;
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
if (helpSeeking) break;
|
|
1186
|
+
}
|
|
1187
|
+
let errorCount = 0;
|
|
1188
|
+
for (const mi of matchIndices) {
|
|
1189
|
+
if (commands[mi].exitCode != null && commands[mi].exitCode !== 0) {
|
|
1190
|
+
errorCount++;
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
let selfCorrections = 0;
|
|
1194
|
+
const prefixGroups = /* @__PURE__ */ new Map();
|
|
1195
|
+
for (const mi of matchIndices) {
|
|
1196
|
+
const prefix = commandPrefix(commands[mi].command);
|
|
1197
|
+
prefixGroups.set(prefix, (prefixGroups.get(prefix) ?? 0) + 1);
|
|
1198
|
+
}
|
|
1199
|
+
for (const count of prefixGroups.values()) {
|
|
1200
|
+
if (count > 1) selfCorrections += count - 1;
|
|
1201
|
+
}
|
|
1202
|
+
const gaps = [];
|
|
1203
|
+
for (let k = 1; k < matchIndices.length; k++) {
|
|
1204
|
+
const prev = commands[matchIndices[k - 1]];
|
|
1205
|
+
const curr = commands[matchIndices[k]];
|
|
1206
|
+
if (prev.endedAt) {
|
|
1207
|
+
const gap = new Date(curr.startedAt).getTime() - new Date(prev.endedAt).getTime();
|
|
1208
|
+
if (gap >= 0) gaps.push(gap);
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
let thinkingGapMs = null;
|
|
1212
|
+
const firstMatchIdx = matchIndices[0];
|
|
1213
|
+
if (firstMatchIdx > 0) {
|
|
1214
|
+
const prev = commands[firstMatchIdx - 1];
|
|
1215
|
+
if (prev.endedAt) {
|
|
1216
|
+
thinkingGapMs = new Date(commands[firstMatchIdx].startedAt).getTime() - new Date(prev.endedAt).getTime();
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
const medianGapMs = computeMedian(gaps);
|
|
1220
|
+
const rating = inferRating({
|
|
1221
|
+
helpSeeking,
|
|
1222
|
+
errorCount,
|
|
1223
|
+
selfCorrections,
|
|
1224
|
+
medianGapMs,
|
|
1225
|
+
thinkingGapMs,
|
|
1226
|
+
matchedCommands: matchIndices.length
|
|
1227
|
+
});
|
|
1228
|
+
const confidence = matchIndices.length >= 3 ? "high" : matchIndices.length >= 2 ? "medium" : "low";
|
|
1229
|
+
ratings.push({
|
|
1230
|
+
tokenSlug: tp.slug,
|
|
1231
|
+
rating,
|
|
1232
|
+
confidence,
|
|
1233
|
+
evidence: {
|
|
1234
|
+
matchedCommands: matchIndices.length,
|
|
1235
|
+
helpSeeking,
|
|
1236
|
+
errorCount,
|
|
1237
|
+
selfCorrections,
|
|
1238
|
+
medianGapMs,
|
|
1239
|
+
thinkingGapMs
|
|
1240
|
+
},
|
|
1241
|
+
matchedCommandTexts: matchedTexts
|
|
1242
|
+
});
|
|
1243
|
+
}
|
|
1244
|
+
const unmatchedCommands = [];
|
|
1245
|
+
for (let i = 0; i < commands.length; i++) {
|
|
1246
|
+
if (!matchedSet.has(i) && !isHelpCommand(commands[i].command)) {
|
|
1247
|
+
unmatchedCommands.push(commands[i].command);
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
let timeSpan = null;
|
|
1251
|
+
if (commands.length > 0) {
|
|
1252
|
+
const first = commands[0];
|
|
1253
|
+
const last = commands[commands.length - 1];
|
|
1254
|
+
const endTs = last.endedAt ?? last.startedAt;
|
|
1255
|
+
timeSpan = {
|
|
1256
|
+
start: first.startedAt,
|
|
1257
|
+
end: endTs,
|
|
1258
|
+
durationMs: new Date(endTs).getTime() - new Date(first.startedAt).getTime()
|
|
1259
|
+
};
|
|
1260
|
+
}
|
|
1261
|
+
return { ratings, unmatchedCommands, timeSpan };
|
|
1262
|
+
}
|
|
1263
|
+
function inferRating(signals) {
|
|
1264
|
+
const { helpSeeking, errorCount, selfCorrections, medianGapMs, thinkingGapMs } = signals;
|
|
1265
|
+
let negatives = 0;
|
|
1266
|
+
if (helpSeeking) negatives += 2;
|
|
1267
|
+
if (errorCount >= 3) negatives += 3;
|
|
1268
|
+
else if (errorCount >= 1) negatives += 1;
|
|
1269
|
+
if (selfCorrections >= 2) negatives += 2;
|
|
1270
|
+
else if (selfCorrections >= 1) negatives += 1;
|
|
1271
|
+
if (medianGapMs != null && medianGapMs > 3e4) negatives += 2;
|
|
1272
|
+
else if (medianGapMs != null && medianGapMs > 1e4) negatives += 1;
|
|
1273
|
+
if (thinkingGapMs != null && thinkingGapMs > 3e4) negatives += 1;
|
|
1274
|
+
if (negatives >= 5) return 1;
|
|
1275
|
+
if (negatives >= 3) return 2;
|
|
1276
|
+
if (negatives >= 1) return 3;
|
|
1277
|
+
return 4;
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
// src/kernel/observation/monitor-io.ts
|
|
1281
|
+
import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync, appendFileSync, statSync } from "fs";
|
|
1282
|
+
import { homedir as homedir2 } from "os";
|
|
1283
|
+
import { join as join2 } from "path";
|
|
1284
|
+
var MONITOR_DIR = join2(homedir2(), ".zam", "monitor");
|
|
1285
|
+
function getMonitorPath(sessionId) {
|
|
1286
|
+
return join2(MONITOR_DIR, `${sessionId}.jsonl`);
|
|
1287
|
+
}
|
|
1288
|
+
function ensureMonitorDir() {
|
|
1289
|
+
if (!existsSync2(MONITOR_DIR)) {
|
|
1290
|
+
mkdirSync2(MONITOR_DIR, { recursive: true, mode: 448 });
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
function writeMonitorEvent(sessionId, event) {
|
|
1294
|
+
ensureMonitorDir();
|
|
1295
|
+
const path = getMonitorPath(sessionId);
|
|
1296
|
+
appendFileSync(path, JSON.stringify(event) + "\n");
|
|
1297
|
+
}
|
|
1298
|
+
function readMonitorLog(sessionId) {
|
|
1299
|
+
const path = getMonitorPath(sessionId);
|
|
1300
|
+
if (!existsSync2(path)) {
|
|
1301
|
+
return [];
|
|
1302
|
+
}
|
|
1303
|
+
const content = readFileSync(path, "utf-8");
|
|
1304
|
+
return parseMonitorLog(content);
|
|
1305
|
+
}
|
|
1306
|
+
function monitorLogExists(sessionId) {
|
|
1307
|
+
return existsSync2(getMonitorPath(sessionId));
|
|
1308
|
+
}
|
|
1309
|
+
function getMonitorLogStats(sessionId) {
|
|
1310
|
+
const path = getMonitorPath(sessionId);
|
|
1311
|
+
if (!existsSync2(path)) {
|
|
1312
|
+
return { exists: false, sizeBytes: 0, lineCount: 0 };
|
|
1313
|
+
}
|
|
1314
|
+
const stat = statSync(path);
|
|
1315
|
+
const content = readFileSync(path, "utf-8");
|
|
1316
|
+
const lineCount = content.split("\n").filter((l) => l.trim()).length;
|
|
1317
|
+
return { exists: true, sizeBytes: stat.size, lineCount };
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
// src/kernel/observation/shell-hooks.ts
|
|
1321
|
+
function generateZshHooks(monitorFile, sessionId) {
|
|
1322
|
+
return `
|
|
1323
|
+
# ZAM monitor hooks for session ${sessionId}
|
|
1324
|
+
export __ZAM_MONITOR_FILE="${monitorFile}"
|
|
1325
|
+
export __ZAM_MONITOR_SEQ=0
|
|
1326
|
+
export __ZAM_MONITOR_SESSION="${sessionId}"
|
|
1327
|
+
|
|
1328
|
+
__zam_ts() {
|
|
1329
|
+
if [[ -n "\${EPOCHREALTIME:-}" ]]; then
|
|
1330
|
+
local sec="\${EPOCHREALTIME%%.*}"
|
|
1331
|
+
local frac="\${EPOCHREALTIME##*.}"
|
|
1332
|
+
frac="\${frac:0:3}"
|
|
1333
|
+
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"
|
|
1334
|
+
else
|
|
1335
|
+
date -u '+%Y-%m-%dT%H:%M:%SZ'
|
|
1336
|
+
fi
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1339
|
+
__zam_preexec() {
|
|
1340
|
+
(( __ZAM_MONITOR_SEQ++ ))
|
|
1341
|
+
local cmd="\${1//\\"/\\\\\\"}"
|
|
1342
|
+
local cwd="\${PWD//\\"/\\\\\\"}"
|
|
1343
|
+
local ts="$(__zam_ts)"
|
|
1344
|
+
printf '{"type":"command_start","ts":"%s","command":"%s","cwd":"%s","seq":%d,"pid":%d}\\n' \\
|
|
1345
|
+
"$ts" "$cmd" "$cwd" "$__ZAM_MONITOR_SEQ" "$$" \\
|
|
1346
|
+
>> "$__ZAM_MONITOR_FILE"
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
__zam_precmd() {
|
|
1350
|
+
local exit_code=$?
|
|
1351
|
+
[[ $__ZAM_MONITOR_SEQ -eq 0 ]] && return
|
|
1352
|
+
local ts="$(__zam_ts)"
|
|
1353
|
+
printf '{"type":"command_end","ts":"%s","exit_code":%d,"seq":%d,"pid":%d}\\n' \\
|
|
1354
|
+
"$ts" "$exit_code" "$__ZAM_MONITOR_SEQ" "$$" \\
|
|
1355
|
+
>> "$__ZAM_MONITOR_FILE"
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1358
|
+
autoload -Uz add-zsh-hook
|
|
1359
|
+
add-zsh-hook preexec __zam_preexec
|
|
1360
|
+
add-zsh-hook precmd __zam_precmd
|
|
1361
|
+
|
|
1362
|
+
echo "ZAM monitor active for session $__ZAM_MONITOR_SESSION"
|
|
1363
|
+
`.trim();
|
|
1364
|
+
}
|
|
1365
|
+
function generateBashHooks(monitorFile, sessionId) {
|
|
1366
|
+
return `
|
|
1367
|
+
# ZAM monitor hooks for session ${sessionId}
|
|
1368
|
+
export __ZAM_MONITOR_FILE="${monitorFile}"
|
|
1369
|
+
export __ZAM_MONITOR_SEQ=0
|
|
1370
|
+
export __ZAM_MONITOR_SESSION="${sessionId}"
|
|
1371
|
+
export __ZAM_MONITOR_CMD_ACTIVE=0
|
|
1372
|
+
|
|
1373
|
+
__zam_ts() {
|
|
1374
|
+
date -u '+%Y-%m-%dT%H:%M:%SZ'
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
__zam_debug_trap() {
|
|
1378
|
+
[[ "$__ZAM_MONITOR_CMD_ACTIVE" -eq 1 ]] && return
|
|
1379
|
+
__ZAM_MONITOR_CMD_ACTIVE=1
|
|
1380
|
+
(( __ZAM_MONITOR_SEQ++ ))
|
|
1381
|
+
local cmd="\${BASH_COMMAND//\\"/\\\\\\"}"
|
|
1382
|
+
local cwd="\${PWD//\\"/\\\\\\"}"
|
|
1383
|
+
local ts="$(__zam_ts)"
|
|
1384
|
+
printf '{"type":"command_start","ts":"%s","command":"%s","cwd":"%s","seq":%d,"pid":%d}\\n' \\
|
|
1385
|
+
"$ts" "$cmd" "$cwd" "$__ZAM_MONITOR_SEQ" "$$" \\
|
|
1386
|
+
>> "$__ZAM_MONITOR_FILE"
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
__zam_prompt_cmd() {
|
|
1390
|
+
local exit_code=$?
|
|
1391
|
+
if [[ "$__ZAM_MONITOR_CMD_ACTIVE" -eq 1 ]]; then
|
|
1392
|
+
__ZAM_MONITOR_CMD_ACTIVE=0
|
|
1393
|
+
local ts="$(__zam_ts)"
|
|
1394
|
+
printf '{"type":"command_end","ts":"%s","exit_code":%d,"seq":%d,"pid":%d}\\n' \\
|
|
1395
|
+
"$ts" "$exit_code" "$__ZAM_MONITOR_SEQ" "$$" \\
|
|
1396
|
+
>> "$__ZAM_MONITOR_FILE"
|
|
1397
|
+
fi
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1400
|
+
trap '__zam_debug_trap' DEBUG
|
|
1401
|
+
PROMPT_COMMAND="__zam_prompt_cmd;\${PROMPT_COMMAND:-}"
|
|
1402
|
+
|
|
1403
|
+
echo "ZAM monitor active for session $__ZAM_MONITOR_SESSION"
|
|
1404
|
+
`.trim();
|
|
1405
|
+
}
|
|
1406
|
+
function generateZshUnhooks() {
|
|
1407
|
+
return `
|
|
1408
|
+
# Remove ZAM monitor hooks
|
|
1409
|
+
add-zsh-hook -d preexec __zam_preexec 2>/dev/null
|
|
1410
|
+
add-zsh-hook -d precmd __zam_precmd 2>/dev/null
|
|
1411
|
+
unset -f __zam_preexec __zam_precmd __zam_ts 2>/dev/null
|
|
1412
|
+
unset __ZAM_MONITOR_FILE __ZAM_MONITOR_SEQ __ZAM_MONITOR_SESSION 2>/dev/null
|
|
1413
|
+
echo "ZAM monitor stopped."
|
|
1414
|
+
`.trim();
|
|
1415
|
+
}
|
|
1416
|
+
function generateBashUnhooks() {
|
|
1417
|
+
return `
|
|
1418
|
+
# Remove ZAM monitor hooks
|
|
1419
|
+
trap - DEBUG
|
|
1420
|
+
PROMPT_COMMAND="\${PROMPT_COMMAND/__zam_prompt_cmd;/}"
|
|
1421
|
+
unset -f __zam_debug_trap __zam_prompt_cmd __zam_ts 2>/dev/null
|
|
1422
|
+
unset __ZAM_MONITOR_FILE __ZAM_MONITOR_SEQ __ZAM_MONITOR_SESSION __ZAM_MONITOR_CMD_ACTIVE 2>/dev/null
|
|
1423
|
+
echo "ZAM monitor stopped."
|
|
1424
|
+
`.trim();
|
|
1425
|
+
}
|
|
1426
|
+
|
|
1427
|
+
// src/kernel/observation/skill-discovery.ts
|
|
1428
|
+
function discoverSkills(sessionCommands, options = {}) {
|
|
1429
|
+
const minSessions = options.minSessions ?? 2;
|
|
1430
|
+
const minLen = options.minSequenceLength ?? 2;
|
|
1431
|
+
const maxLen = options.maxSequenceLength ?? 5;
|
|
1432
|
+
const existing = new Set(options.existingSkillSlugs ?? []);
|
|
1433
|
+
const sessionSequences = /* @__PURE__ */ new Map();
|
|
1434
|
+
for (const [sessionId, commands] of sessionCommands) {
|
|
1435
|
+
const sequences = extractSequences(commands, minLen, maxLen);
|
|
1436
|
+
if (sequences.length > 0) {
|
|
1437
|
+
sessionSequences.set(sessionId, sequences);
|
|
1438
|
+
}
|
|
1439
|
+
}
|
|
1440
|
+
const sequenceIndex = /* @__PURE__ */ new Map();
|
|
1441
|
+
for (const [, sequences] of sessionSequences) {
|
|
1442
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1443
|
+
for (const seq of sequences) {
|
|
1444
|
+
const key = seq.join(" \u2192 ");
|
|
1445
|
+
if (seen.has(key)) continue;
|
|
1446
|
+
seen.add(key);
|
|
1447
|
+
const entry = sequenceIndex.get(key);
|
|
1448
|
+
if (entry) {
|
|
1449
|
+
entry.sessionCount++;
|
|
1450
|
+
entry.totalOccurrences++;
|
|
1451
|
+
} else {
|
|
1452
|
+
sequenceIndex.set(key, {
|
|
1453
|
+
steps: seq,
|
|
1454
|
+
sessionCount: 1,
|
|
1455
|
+
totalOccurrences: 1,
|
|
1456
|
+
examples: []
|
|
1457
|
+
});
|
|
1458
|
+
}
|
|
1459
|
+
}
|
|
1460
|
+
}
|
|
1461
|
+
const lastSessionId = [...sessionCommands.keys()].pop();
|
|
1462
|
+
if (lastSessionId) {
|
|
1463
|
+
const lastCommands = sessionCommands.get(lastSessionId);
|
|
1464
|
+
for (const [key, entry] of sequenceIndex) {
|
|
1465
|
+
if (entry.sessionCount >= minSessions) {
|
|
1466
|
+
entry.examples = findExamplesForSequence(lastCommands, entry.steps);
|
|
1467
|
+
}
|
|
1468
|
+
}
|
|
1469
|
+
}
|
|
1470
|
+
const candidates = [...sequenceIndex.values()].filter(
|
|
1471
|
+
(s) => s.sessionCount >= minSessions
|
|
1472
|
+
);
|
|
1473
|
+
const pruned = removeSubsequences(candidates);
|
|
1474
|
+
const proposals = [];
|
|
1475
|
+
for (const seq of pruned) {
|
|
1476
|
+
const slug = generateSlug(seq.steps);
|
|
1477
|
+
if (existing.has(slug)) continue;
|
|
1478
|
+
proposals.push({
|
|
1479
|
+
slug,
|
|
1480
|
+
description: describeSequence(seq.steps),
|
|
1481
|
+
steps: seq.steps,
|
|
1482
|
+
sessionCount: seq.sessionCount,
|
|
1483
|
+
confidence: seq.sessionCount >= 4 ? "high" : seq.sessionCount >= 3 ? "medium" : "low",
|
|
1484
|
+
examples: seq.examples
|
|
1485
|
+
});
|
|
1486
|
+
}
|
|
1487
|
+
const confidenceOrder = { high: 0, medium: 1, low: 2 };
|
|
1488
|
+
proposals.sort((a, b) => {
|
|
1489
|
+
const confDiff = confidenceOrder[a.confidence] - confidenceOrder[b.confidence];
|
|
1490
|
+
if (confDiff !== 0) return confDiff;
|
|
1491
|
+
return b.sessionCount - a.sessionCount;
|
|
1492
|
+
});
|
|
1493
|
+
return proposals;
|
|
1494
|
+
}
|
|
1495
|
+
function normalizeCommand(command) {
|
|
1496
|
+
const parts = command.trim().split(/\s+/);
|
|
1497
|
+
const multiWord = ["docker compose", "npm run", "npx", "git"];
|
|
1498
|
+
const lower = command.toLowerCase();
|
|
1499
|
+
for (const mw of multiWord) {
|
|
1500
|
+
if (lower.startsWith(mw) && parts.length >= mw.split(" ").length + 1) {
|
|
1501
|
+
return parts.slice(0, mw.split(" ").length + 1).join(" ").toLowerCase();
|
|
1502
|
+
}
|
|
1503
|
+
}
|
|
1504
|
+
return parts.slice(0, Math.min(2, parts.length)).join(" ").toLowerCase();
|
|
1505
|
+
}
|
|
1506
|
+
function extractSequences(commands, minLen, maxLen) {
|
|
1507
|
+
const filtered = commands.filter((c) => {
|
|
1508
|
+
const lower = c.command.toLowerCase().trim();
|
|
1509
|
+
return lower.length > 0 && !lower.startsWith("cd ") && lower !== "cd" && lower !== "ls" && lower !== "pwd" && lower !== "clear" && lower !== "exit" && !lower.startsWith("echo ");
|
|
1510
|
+
});
|
|
1511
|
+
const normalized = filtered.map((c) => normalizeCommand(c.command));
|
|
1512
|
+
const sequences = [];
|
|
1513
|
+
for (let len = minLen; len <= maxLen; len++) {
|
|
1514
|
+
for (let i = 0; i <= normalized.length - len; i++) {
|
|
1515
|
+
const seq = normalized.slice(i, i + len);
|
|
1516
|
+
if (new Set(seq).size >= 2) {
|
|
1517
|
+
sequences.push(seq);
|
|
1518
|
+
}
|
|
1519
|
+
}
|
|
1520
|
+
}
|
|
1521
|
+
return sequences;
|
|
1522
|
+
}
|
|
1523
|
+
function findExamplesForSequence(commands, steps) {
|
|
1524
|
+
const normalized = commands.map((c) => ({
|
|
1525
|
+
norm: normalizeCommand(c.command),
|
|
1526
|
+
full: c.command
|
|
1527
|
+
}));
|
|
1528
|
+
for (let i = 0; i <= normalized.length - steps.length; i++) {
|
|
1529
|
+
let match = true;
|
|
1530
|
+
for (let j = 0; j < steps.length; j++) {
|
|
1531
|
+
if (normalized[i + j].norm !== steps[j]) {
|
|
1532
|
+
match = false;
|
|
1533
|
+
break;
|
|
1534
|
+
}
|
|
1535
|
+
}
|
|
1536
|
+
if (match) {
|
|
1537
|
+
return normalized.slice(i, i + steps.length).map((n) => n.full);
|
|
1538
|
+
}
|
|
1539
|
+
}
|
|
1540
|
+
return [];
|
|
1541
|
+
}
|
|
1542
|
+
function removeSubsequences(candidates) {
|
|
1543
|
+
const sorted = [...candidates].sort((a, b) => b.steps.length - a.steps.length);
|
|
1544
|
+
const result = [];
|
|
1545
|
+
for (const candidate of sorted) {
|
|
1546
|
+
const key = candidate.steps.join(" \u2192 ");
|
|
1547
|
+
const isSubsequence = result.some((longer) => {
|
|
1548
|
+
const longerKey = longer.steps.join(" \u2192 ");
|
|
1549
|
+
return longerKey.includes(key) && longerKey !== key;
|
|
1550
|
+
});
|
|
1551
|
+
if (!isSubsequence) {
|
|
1552
|
+
result.push(candidate);
|
|
1553
|
+
}
|
|
1554
|
+
}
|
|
1555
|
+
return result;
|
|
1556
|
+
}
|
|
1557
|
+
function generateSlug(steps) {
|
|
1558
|
+
return steps.map((s) => {
|
|
1559
|
+
const parts = s.split(/\s+/);
|
|
1560
|
+
return parts[parts.length - 1];
|
|
1561
|
+
}).join("-");
|
|
1562
|
+
}
|
|
1563
|
+
function describeSequence(steps) {
|
|
1564
|
+
return `Recurring pattern: ${steps.join(" \u2192 ")}`;
|
|
1565
|
+
}
|
|
1566
|
+
|
|
1567
|
+
// src/kernel/goals/engine.ts
|
|
1568
|
+
import { readdirSync, readFileSync as readFileSync2, writeFileSync, existsSync as existsSync3 } from "fs";
|
|
1569
|
+
import { join as join3, basename } from "path";
|
|
1570
|
+
|
|
1571
|
+
// src/kernel/goals/parser.ts
|
|
1572
|
+
function parseGoalFile(content, slug, filePath) {
|
|
1573
|
+
const { frontmatter, body } = splitFrontmatter(content);
|
|
1574
|
+
const validStatuses = ["active", "completed", "paused", "abandoned"];
|
|
1575
|
+
const status = validStatuses.includes(frontmatter.status) ? frontmatter.status : "active";
|
|
1576
|
+
const now = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
1577
|
+
return {
|
|
1578
|
+
slug,
|
|
1579
|
+
title: frontmatter.title || slug,
|
|
1580
|
+
status,
|
|
1581
|
+
parent: frontmatter.parent || null,
|
|
1582
|
+
created: frontmatter.created || now,
|
|
1583
|
+
updated: frontmatter.updated || now,
|
|
1584
|
+
body,
|
|
1585
|
+
filePath
|
|
1586
|
+
};
|
|
1587
|
+
}
|
|
1588
|
+
function serializeGoal(goal) {
|
|
1589
|
+
const lines = [
|
|
1590
|
+
"---",
|
|
1591
|
+
`title: ${goal.title}`,
|
|
1592
|
+
`status: ${goal.status}`
|
|
1593
|
+
];
|
|
1594
|
+
if (goal.parent) {
|
|
1595
|
+
lines.push(`parent: ${goal.parent}`);
|
|
1596
|
+
}
|
|
1597
|
+
lines.push(`created: ${goal.created}`);
|
|
1598
|
+
lines.push(`updated: ${goal.updated}`);
|
|
1599
|
+
lines.push("---");
|
|
1600
|
+
lines.push("");
|
|
1601
|
+
if (goal.body.trim()) {
|
|
1602
|
+
lines.push(goal.body.trim());
|
|
1603
|
+
lines.push("");
|
|
1604
|
+
}
|
|
1605
|
+
return lines.join("\n");
|
|
1606
|
+
}
|
|
1607
|
+
function extractTasks(body) {
|
|
1608
|
+
const tasks = [];
|
|
1609
|
+
const taskRegex = /^[-*]\s+\[([ xX])\]\s+(.+)$/gm;
|
|
1610
|
+
let match;
|
|
1611
|
+
while ((match = taskRegex.exec(body)) !== null) {
|
|
1612
|
+
tasks.push({
|
|
1613
|
+
done: match[1] !== " ",
|
|
1614
|
+
text: match[2].trim()
|
|
1615
|
+
});
|
|
1616
|
+
}
|
|
1617
|
+
return tasks;
|
|
1618
|
+
}
|
|
1619
|
+
function extractTokenRefs(body) {
|
|
1620
|
+
const tokensSection = body.match(/## Tokens\n([\s\S]*?)(?=\n## |\n*$)/);
|
|
1621
|
+
if (!tokensSection) return [];
|
|
1622
|
+
const refs = [];
|
|
1623
|
+
const lines = tokensSection[1].split("\n");
|
|
1624
|
+
for (const line of lines) {
|
|
1625
|
+
const match = line.match(/^[-*]\s+(\S+)/);
|
|
1626
|
+
if (match) {
|
|
1627
|
+
refs.push(match[1]);
|
|
1628
|
+
}
|
|
1629
|
+
}
|
|
1630
|
+
return refs;
|
|
1631
|
+
}
|
|
1632
|
+
function splitFrontmatter(content) {
|
|
1633
|
+
const trimmed = content.trim();
|
|
1634
|
+
if (!trimmed.startsWith("---")) {
|
|
1635
|
+
return { frontmatter: {}, body: trimmed };
|
|
1636
|
+
}
|
|
1637
|
+
const endIndex = trimmed.indexOf("---", 3);
|
|
1638
|
+
if (endIndex === -1) {
|
|
1639
|
+
return { frontmatter: {}, body: trimmed };
|
|
1640
|
+
}
|
|
1641
|
+
const fmBlock = trimmed.slice(3, endIndex).trim();
|
|
1642
|
+
const body = trimmed.slice(endIndex + 3).trim();
|
|
1643
|
+
const frontmatter = {};
|
|
1644
|
+
for (const line of fmBlock.split("\n")) {
|
|
1645
|
+
const colonIndex = line.indexOf(":");
|
|
1646
|
+
if (colonIndex === -1) continue;
|
|
1647
|
+
const key = line.slice(0, colonIndex).trim();
|
|
1648
|
+
const value = line.slice(colonIndex + 1).trim();
|
|
1649
|
+
if (key && value) {
|
|
1650
|
+
frontmatter[key] = value;
|
|
1651
|
+
}
|
|
1652
|
+
}
|
|
1653
|
+
return { frontmatter, body };
|
|
1654
|
+
}
|
|
1655
|
+
|
|
1656
|
+
// src/kernel/goals/engine.ts
|
|
1657
|
+
function listGoals(goalsDir) {
|
|
1658
|
+
if (!existsSync3(goalsDir)) return [];
|
|
1659
|
+
const files = readdirSync(goalsDir).filter(
|
|
1660
|
+
(f) => f.endsWith(".md") && f !== "README.md"
|
|
1661
|
+
);
|
|
1662
|
+
const summaries = [];
|
|
1663
|
+
for (const file of files) {
|
|
1664
|
+
const filePath = join3(goalsDir, file);
|
|
1665
|
+
const content = readFileSync2(filePath, "utf-8");
|
|
1666
|
+
const slug = basename(file, ".md");
|
|
1667
|
+
const goal = parseGoalFile(content, slug, filePath);
|
|
1668
|
+
const tasks = extractTasks(goal.body);
|
|
1669
|
+
const tokens = extractTokenRefs(goal.body);
|
|
1670
|
+
summaries.push({
|
|
1671
|
+
slug: goal.slug,
|
|
1672
|
+
title: goal.title,
|
|
1673
|
+
status: goal.status,
|
|
1674
|
+
parent: goal.parent,
|
|
1675
|
+
taskCount: tasks.length,
|
|
1676
|
+
tasksDone: tasks.filter((t) => t.done).length,
|
|
1677
|
+
tokenCount: tokens.length
|
|
1678
|
+
});
|
|
1679
|
+
}
|
|
1680
|
+
const statusOrder = {
|
|
1681
|
+
active: 0,
|
|
1682
|
+
paused: 1,
|
|
1683
|
+
completed: 2,
|
|
1684
|
+
abandoned: 3
|
|
1685
|
+
};
|
|
1686
|
+
summaries.sort((a, b) => {
|
|
1687
|
+
const statusDiff = statusOrder[a.status] - statusOrder[b.status];
|
|
1688
|
+
if (statusDiff !== 0) return statusDiff;
|
|
1689
|
+
return a.title.localeCompare(b.title);
|
|
1690
|
+
});
|
|
1691
|
+
return summaries;
|
|
1692
|
+
}
|
|
1693
|
+
function getGoal(goalsDir, slug) {
|
|
1694
|
+
const filePath = join3(goalsDir, `${slug}.md`);
|
|
1695
|
+
if (!existsSync3(filePath)) return void 0;
|
|
1696
|
+
const content = readFileSync2(filePath, "utf-8");
|
|
1697
|
+
return parseGoalFile(content, slug, filePath);
|
|
1698
|
+
}
|
|
1699
|
+
function createGoal(goalsDir, input5) {
|
|
1700
|
+
const filePath = join3(goalsDir, `${input5.slug}.md`);
|
|
1701
|
+
if (existsSync3(filePath)) {
|
|
1702
|
+
throw new Error(`Goal already exists: ${input5.slug}`);
|
|
1703
|
+
}
|
|
1704
|
+
const now = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
1705
|
+
const goal = {
|
|
1706
|
+
slug: input5.slug,
|
|
1707
|
+
title: input5.title,
|
|
1708
|
+
status: input5.status ?? "active",
|
|
1709
|
+
parent: input5.parent ?? null,
|
|
1710
|
+
created: now,
|
|
1711
|
+
updated: now,
|
|
1712
|
+
body: input5.description ? `## Description
|
|
1713
|
+
${input5.description}
|
|
1714
|
+
|
|
1715
|
+
## Tasks
|
|
1716
|
+
|
|
1717
|
+
## Tokens` : "## Description\n\n## Tasks\n\n## Tokens",
|
|
1718
|
+
filePath
|
|
1719
|
+
};
|
|
1720
|
+
writeFileSync(filePath, serializeGoal(goal), "utf-8");
|
|
1721
|
+
return goal;
|
|
1722
|
+
}
|
|
1723
|
+
function updateGoalStatus(goalsDir, slug, status) {
|
|
1724
|
+
const goal = getGoal(goalsDir, slug);
|
|
1725
|
+
if (!goal) throw new Error(`Goal not found: ${slug}`);
|
|
1726
|
+
goal.status = status;
|
|
1727
|
+
goal.updated = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
1728
|
+
writeFileSync(goal.filePath, serializeGoal(goal), "utf-8");
|
|
1729
|
+
return goal;
|
|
1730
|
+
}
|
|
1731
|
+
function getGoalTree(goalsDir) {
|
|
1732
|
+
const all = listGoals(goalsDir);
|
|
1733
|
+
const bySlug = new Map(all.map((g) => [g.slug, g]));
|
|
1734
|
+
const roots = [];
|
|
1735
|
+
const children = /* @__PURE__ */ new Map();
|
|
1736
|
+
for (const g of all) {
|
|
1737
|
+
if (g.parent && bySlug.has(g.parent)) {
|
|
1738
|
+
const list = children.get(g.parent) ?? [];
|
|
1739
|
+
list.push(g);
|
|
1740
|
+
children.set(g.parent, list);
|
|
1741
|
+
}
|
|
1742
|
+
}
|
|
1743
|
+
for (const g of all) {
|
|
1744
|
+
if (!g.parent || !bySlug.has(g.parent)) {
|
|
1745
|
+
roots.push({ ...g, children: children.get(g.slug) ?? [] });
|
|
1746
|
+
}
|
|
1747
|
+
}
|
|
1748
|
+
return roots;
|
|
1749
|
+
}
|
|
1750
|
+
|
|
1751
|
+
// src/kernel/connectors/azure-devops.ts
|
|
1752
|
+
function loadADOConfig(db) {
|
|
1753
|
+
const orgUrl = getSetting(db, "ado.org_url");
|
|
1754
|
+
const project = getSetting(db, "ado.project");
|
|
1755
|
+
const pat = getSetting(db, "ado.pat");
|
|
1756
|
+
if (!orgUrl || !project || !pat) return null;
|
|
1757
|
+
return { orgUrl: orgUrl.replace(/\/+$/, ""), project, pat };
|
|
1758
|
+
}
|
|
1759
|
+
function authHeader(pat) {
|
|
1760
|
+
return `Basic ${Buffer.from(`:${pat}`).toString("base64")}`;
|
|
1761
|
+
}
|
|
1762
|
+
async function fetchActiveWorkItems(config) {
|
|
1763
|
+
const { orgUrl, project, pat } = config;
|
|
1764
|
+
const wiqlUrl = `${orgUrl}/${project}/_apis/wit/wiql?api-version=7.1`;
|
|
1765
|
+
const wiqlBody = {
|
|
1766
|
+
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`
|
|
1767
|
+
};
|
|
1768
|
+
const wiqlRes = await fetch(wiqlUrl, {
|
|
1769
|
+
method: "POST",
|
|
1770
|
+
headers: {
|
|
1771
|
+
Authorization: authHeader(pat),
|
|
1772
|
+
"Content-Type": "application/json"
|
|
1773
|
+
},
|
|
1774
|
+
body: JSON.stringify(wiqlBody)
|
|
1775
|
+
});
|
|
1776
|
+
if (!wiqlRes.ok) {
|
|
1777
|
+
const text = await wiqlRes.text();
|
|
1778
|
+
throw new Error(`ADO WIQL query failed (${wiqlRes.status}): ${text}`);
|
|
1779
|
+
}
|
|
1780
|
+
const wiqlData = await wiqlRes.json();
|
|
1781
|
+
const ids = wiqlData.workItems.map((wi) => wi.id);
|
|
1782
|
+
if (ids.length === 0) return [];
|
|
1783
|
+
const batchIds = ids.slice(0, 200);
|
|
1784
|
+
const fields = "System.Id,System.Title,System.State,System.WorkItemType,System.AssignedTo";
|
|
1785
|
+
const detailUrl = `${orgUrl}/${project}/_apis/wit/workitems?ids=${batchIds.join(",")}&fields=${fields}&api-version=7.1`;
|
|
1786
|
+
const detailRes = await fetch(detailUrl, {
|
|
1787
|
+
headers: { Authorization: authHeader(pat) }
|
|
1788
|
+
});
|
|
1789
|
+
if (!detailRes.ok) {
|
|
1790
|
+
const text = await detailRes.text();
|
|
1791
|
+
throw new Error(`ADO work items fetch failed (${detailRes.status}): ${text}`);
|
|
1792
|
+
}
|
|
1793
|
+
const detailData = await detailRes.json();
|
|
1794
|
+
return detailData.value.map((wi) => ({
|
|
1795
|
+
id: wi.id,
|
|
1796
|
+
title: wi.fields["System.Title"],
|
|
1797
|
+
state: wi.fields["System.State"],
|
|
1798
|
+
type: wi.fields["System.WorkItemType"],
|
|
1799
|
+
assignedTo: wi.fields["System.AssignedTo"]?.displayName ?? ""
|
|
1800
|
+
}));
|
|
1801
|
+
}
|
|
1802
|
+
|
|
1803
|
+
// src/cli/commands/init.ts
|
|
1804
|
+
var initCommand = new Command("init").description("Initialize the ZAM database and config directory").action(() => {
|
|
1805
|
+
try {
|
|
1806
|
+
const dbPath = getDefaultDbPath();
|
|
1807
|
+
const db = openDatabase({ initialize: true });
|
|
1808
|
+
db.close();
|
|
1809
|
+
console.log(`Initialized ZAM database at ${dbPath}`);
|
|
1810
|
+
} catch (err) {
|
|
1811
|
+
console.error("Failed to initialize:", err.message);
|
|
1812
|
+
process.exit(1);
|
|
1813
|
+
}
|
|
1814
|
+
});
|
|
1815
|
+
|
|
1816
|
+
// src/cli/commands/setup.ts
|
|
1817
|
+
import { Command as Command2 } from "commander";
|
|
1818
|
+
import { fileURLToPath } from "url";
|
|
1819
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync3, copyFileSync, writeFileSync as writeFileSync2 } from "fs";
|
|
1820
|
+
import { join as join4, dirname as dirname2, basename as basename2 } from "path";
|
|
1821
|
+
var packageRoot = fileURLToPath(new URL("../..", import.meta.url));
|
|
1822
|
+
var SKILL_PAIRS = [
|
|
1823
|
+
{
|
|
1824
|
+
from: join4(packageRoot, ".claude", "skills", "zam", "SKILL.md"),
|
|
1825
|
+
to: join4(".claude", "skills", "zam", "SKILL.md")
|
|
1826
|
+
},
|
|
1827
|
+
{
|
|
1828
|
+
from: join4(packageRoot, ".gemini", "skills", "zam", "SKILL.md"),
|
|
1829
|
+
to: join4(".gemini", "skills", "zam", "SKILL.md")
|
|
1830
|
+
}
|
|
1831
|
+
];
|
|
1832
|
+
function copySkills(force) {
|
|
1833
|
+
const cwd = process.cwd();
|
|
1834
|
+
let anyAction = false;
|
|
1835
|
+
for (const { from, to } of SKILL_PAIRS) {
|
|
1836
|
+
const dest = join4(cwd, to);
|
|
1837
|
+
if (!existsSync4(from)) {
|
|
1838
|
+
console.warn(` warn source not found, skipping: ${from}`);
|
|
1839
|
+
continue;
|
|
1840
|
+
}
|
|
1841
|
+
if (existsSync4(dest) && !force) {
|
|
1842
|
+
console.log(` skip ${to} (already present \u2014 use --force to update)`);
|
|
1843
|
+
continue;
|
|
1844
|
+
}
|
|
1845
|
+
mkdirSync3(dirname2(dest), { recursive: true });
|
|
1846
|
+
copyFileSync(from, dest);
|
|
1847
|
+
console.log(` copy ${to}`);
|
|
1848
|
+
anyAction = true;
|
|
1849
|
+
}
|
|
1850
|
+
if (!anyAction && !force) {
|
|
1851
|
+
console.log(
|
|
1852
|
+
"\nSkill files are already up to date. Run with --force to overwrite."
|
|
1853
|
+
);
|
|
1854
|
+
}
|
|
1855
|
+
}
|
|
1856
|
+
function initDatabase(skipInit) {
|
|
1857
|
+
if (skipInit) return;
|
|
1858
|
+
try {
|
|
1859
|
+
const dbPath = getDefaultDbPath();
|
|
1860
|
+
const db = openDatabase({ initialize: true });
|
|
1861
|
+
db.close();
|
|
1862
|
+
console.log(` init ZAM database at ${dbPath}`);
|
|
1863
|
+
} catch (err) {
|
|
1864
|
+
const msg = err.message;
|
|
1865
|
+
if (!msg.includes("already")) {
|
|
1866
|
+
console.warn(` warn database init: ${msg}`);
|
|
1867
|
+
} else {
|
|
1868
|
+
console.log(` skip database already initialized`);
|
|
1869
|
+
}
|
|
1870
|
+
}
|
|
1871
|
+
}
|
|
1872
|
+
function writeClaudeMd(skipClaudeMd) {
|
|
1873
|
+
if (skipClaudeMd) return;
|
|
1874
|
+
const dest = join4(process.cwd(), "CLAUDE.md");
|
|
1875
|
+
if (existsSync4(dest)) {
|
|
1876
|
+
console.log(` skip CLAUDE.md (already present)`);
|
|
1877
|
+
return;
|
|
1878
|
+
}
|
|
1879
|
+
const name = basename2(process.cwd());
|
|
1880
|
+
writeFileSync2(
|
|
1881
|
+
dest,
|
|
1882
|
+
`# ZAM Personal Kernel \u2014 ${name}
|
|
1883
|
+
|
|
1884
|
+
This is a ZAM personal instance. ZAM builds lasting skills through spaced
|
|
1885
|
+
repetition during real work \u2014 not separate study sessions.
|
|
1886
|
+
|
|
1887
|
+
## First time here?
|
|
1888
|
+
Run \`/setup\` in Claude Code or Gemini CLI to complete first-time setup.
|
|
1889
|
+
|
|
1890
|
+
## Regular use
|
|
1891
|
+
Run \`/zam\` to start a learning session on whatever you are working on.
|
|
1892
|
+
|
|
1893
|
+
## What lives here
|
|
1894
|
+
- \`beliefs/\` \u2014 your worldview, approved by git commit
|
|
1895
|
+
- \`goals/\` \u2014 your objectives, decomposed into tasks and learning tokens
|
|
1896
|
+
|
|
1897
|
+
## Fast-changing data
|
|
1898
|
+
Learning tokens, cards, and review history live in \`~/.zam/zam.db\` (local
|
|
1899
|
+
SQLite, not committed to git). Use \`zam connector setup turso\` to enable
|
|
1900
|
+
cloud sync across machines.
|
|
1901
|
+
`,
|
|
1902
|
+
"utf8"
|
|
1903
|
+
);
|
|
1904
|
+
console.log(` write CLAUDE.md`);
|
|
1905
|
+
}
|
|
1906
|
+
var setupCommand = new Command2("setup").description(
|
|
1907
|
+
"Distribute ZAM skill files into this personal instance and initialize the database"
|
|
1908
|
+
).option(
|
|
1909
|
+
"--force",
|
|
1910
|
+
"overwrite existing skill files (use after upgrading zam)",
|
|
1911
|
+
false
|
|
1912
|
+
).option("--skip-init", "skip database initialization", false).option("--skip-claude-md", "skip CLAUDE.md generation", false).action(
|
|
1913
|
+
(opts) => {
|
|
1914
|
+
console.log(`Setting up ZAM in ${process.cwd()}
|
|
1915
|
+
`);
|
|
1916
|
+
copySkills(opts.force);
|
|
1917
|
+
initDatabase(opts.skipInit);
|
|
1918
|
+
writeClaudeMd(opts.skipClaudeMd);
|
|
1919
|
+
console.log(
|
|
1920
|
+
"\nDone. Run `zam whoami --set <your-id>` to set your identity, then open Claude Code or Gemini CLI and run /zam to start a learning session."
|
|
1921
|
+
);
|
|
1922
|
+
}
|
|
1923
|
+
);
|
|
1924
|
+
|
|
1925
|
+
// src/cli/commands/token.ts
|
|
1926
|
+
import { Command as Command3 } from "commander";
|
|
1927
|
+
|
|
1928
|
+
// src/cli/commands/resolve-user.ts
|
|
1929
|
+
function resolveUser(opts, db, resolveOpts) {
|
|
1930
|
+
if (opts.user) return opts.user;
|
|
1931
|
+
const stored = getSetting(db, "user.id");
|
|
1932
|
+
if (stored) return stored;
|
|
1933
|
+
const message = "No user specified. Set a default with: zam whoami --set <id>";
|
|
1934
|
+
if (resolveOpts?.json) {
|
|
1935
|
+
console.log(JSON.stringify({ error: message }, null, 2));
|
|
1936
|
+
} else {
|
|
1937
|
+
console.error(message);
|
|
1938
|
+
}
|
|
1939
|
+
process.exit(1);
|
|
1940
|
+
}
|
|
1941
|
+
|
|
1942
|
+
// src/cli/commands/token.ts
|
|
1943
|
+
function withDb(fn) {
|
|
1944
|
+
let db;
|
|
1945
|
+
try {
|
|
1946
|
+
db = openDatabase();
|
|
1947
|
+
fn(db);
|
|
1948
|
+
} catch (err) {
|
|
1949
|
+
console.error("Error:", err.message);
|
|
1950
|
+
process.exit(1);
|
|
1951
|
+
} finally {
|
|
1952
|
+
db?.close();
|
|
1953
|
+
}
|
|
1954
|
+
}
|
|
1955
|
+
var tokenCommand = new Command3("token").description("Manage knowledge tokens");
|
|
1956
|
+
tokenCommand.command("register").description("Register a new knowledge token").requiredOption("--slug <slug>", "Unique token slug").requiredOption("--concept <concept>", "Concept description").option("--domain <domain>", "Knowledge domain", "").option("--bloom <level>", "Bloom taxonomy level (1-5)", "1").option("--json", "Output as JSON").action((opts) => {
|
|
1957
|
+
withDb((db) => {
|
|
1958
|
+
const token = createToken(db, {
|
|
1959
|
+
slug: opts.slug,
|
|
1960
|
+
concept: opts.concept,
|
|
1961
|
+
domain: opts.domain,
|
|
1962
|
+
bloom_level: Number(opts.bloom)
|
|
1963
|
+
});
|
|
1964
|
+
if (opts.json) {
|
|
1965
|
+
console.log(JSON.stringify(token, null, 2));
|
|
1966
|
+
} else {
|
|
1967
|
+
console.log(`Registered token: ${token.slug} (${token.id})`);
|
|
1968
|
+
console.log(` Concept: ${token.concept}`);
|
|
1969
|
+
console.log(` Domain: ${token.domain || "(none)"}`);
|
|
1970
|
+
console.log(` Bloom: ${token.bloom_level}`);
|
|
1971
|
+
}
|
|
1972
|
+
});
|
|
1973
|
+
});
|
|
1974
|
+
tokenCommand.command("find").description("Fuzzy search for tokens").requiredOption("--query <query>", "Search query").option("--json", "Output as JSON").action((opts) => {
|
|
1975
|
+
withDb((db) => {
|
|
1976
|
+
const results = findTokens(db, opts.query);
|
|
1977
|
+
if (opts.json) {
|
|
1978
|
+
console.log(JSON.stringify(results, null, 2));
|
|
1979
|
+
return;
|
|
1980
|
+
}
|
|
1981
|
+
if (results.length === 0) {
|
|
1982
|
+
console.log("No tokens found.");
|
|
1983
|
+
return;
|
|
1984
|
+
}
|
|
1985
|
+
console.log(`Found ${results.length} token(s):
|
|
1986
|
+
`);
|
|
1987
|
+
console.log(
|
|
1988
|
+
"Score Slug Concept Domain Bloom"
|
|
1989
|
+
);
|
|
1990
|
+
console.log("\u2500".repeat(90));
|
|
1991
|
+
for (const t of results) {
|
|
1992
|
+
console.log(
|
|
1993
|
+
`${String(t.score).padEnd(6)} ${t.slug.padEnd(21)} ${t.concept.slice(0, 31).padEnd(31)} ${(t.domain || "-").padEnd(11)} ${t.bloom_level}`
|
|
1994
|
+
);
|
|
1995
|
+
}
|
|
1996
|
+
});
|
|
1997
|
+
});
|
|
1998
|
+
tokenCommand.command("list").description("List all tokens").option("--domain <domain>", "Filter by domain").option("--json", "Output as JSON").action((opts) => {
|
|
1999
|
+
withDb((db) => {
|
|
2000
|
+
const tokens = listTokens(db, opts.domain ? { domain: opts.domain } : void 0);
|
|
2001
|
+
if (opts.json) {
|
|
2002
|
+
console.log(JSON.stringify(tokens, null, 2));
|
|
2003
|
+
return;
|
|
2004
|
+
}
|
|
2005
|
+
if (tokens.length === 0) {
|
|
2006
|
+
console.log("No tokens registered.");
|
|
2007
|
+
return;
|
|
2008
|
+
}
|
|
2009
|
+
console.log(
|
|
2010
|
+
"Slug Concept Domain Bloom"
|
|
2011
|
+
);
|
|
2012
|
+
console.log("\u2500".repeat(80));
|
|
2013
|
+
for (const t of tokens) {
|
|
2014
|
+
console.log(
|
|
2015
|
+
`${t.slug.padEnd(21)} ${t.concept.slice(0, 31).padEnd(31)} ${(t.domain || "-").padEnd(11)} ${t.bloom_level}`
|
|
2016
|
+
);
|
|
2017
|
+
}
|
|
2018
|
+
console.log(`
|
|
2019
|
+
${tokens.length} token(s) total.`);
|
|
2020
|
+
});
|
|
2021
|
+
});
|
|
2022
|
+
tokenCommand.command("prereq").description("Add a prerequisite edge between tokens").requiredOption("--token <slug>", "Token that requires a prerequisite").requiredOption("--requires <slug>", "Required prerequisite token").option("--json", "Output as JSON").action((opts) => {
|
|
2023
|
+
withDb((db) => {
|
|
2024
|
+
const token = getTokenBySlug(db, opts.token);
|
|
2025
|
+
if (!token) {
|
|
2026
|
+
console.error(`Token not found: ${opts.token}`);
|
|
2027
|
+
process.exit(1);
|
|
2028
|
+
}
|
|
2029
|
+
const requires = getTokenBySlug(db, opts.requires);
|
|
2030
|
+
if (!requires) {
|
|
2031
|
+
console.error(`Prerequisite token not found: ${opts.requires}`);
|
|
2032
|
+
process.exit(1);
|
|
2033
|
+
}
|
|
2034
|
+
addPrerequisite(db, token.id, requires.id);
|
|
2035
|
+
if (opts.json) {
|
|
2036
|
+
console.log(JSON.stringify({ token: opts.token, requires: opts.requires }, null, 2));
|
|
2037
|
+
} else {
|
|
2038
|
+
console.log(`Added prerequisite: ${opts.token} requires ${opts.requires}`);
|
|
2039
|
+
}
|
|
2040
|
+
});
|
|
2041
|
+
});
|
|
2042
|
+
tokenCommand.command("deprecate").description("Mark a token as deprecated (excluded from reviews, not deleted)").requiredOption("--slug <slug>", "Token slug to deprecate").option("--json", "Output as JSON").action((opts) => {
|
|
2043
|
+
withDb((db) => {
|
|
2044
|
+
const token = deprecateToken(db, opts.slug);
|
|
2045
|
+
if (opts.json) {
|
|
2046
|
+
console.log(JSON.stringify(token, null, 2));
|
|
2047
|
+
} else {
|
|
2048
|
+
console.log(`Deprecated: ${token.slug}`);
|
|
2049
|
+
console.log(` Concept: ${token.concept}`);
|
|
2050
|
+
console.log(` At: ${token.deprecated_at}`);
|
|
2051
|
+
}
|
|
2052
|
+
});
|
|
2053
|
+
});
|
|
2054
|
+
tokenCommand.command("status").description("Show full status of a token for a user").requiredOption("--token <slug>", "Token slug").option("--user <id>", "User ID (default: whoami)").option("--json", "Output as JSON").action((opts) => {
|
|
2055
|
+
withDb((db) => {
|
|
2056
|
+
const userId = resolveUser(opts, db);
|
|
2057
|
+
const token = getTokenBySlug(db, opts.token);
|
|
2058
|
+
if (!token) {
|
|
2059
|
+
console.error(`Token not found: ${opts.token}`);
|
|
2060
|
+
process.exit(1);
|
|
2061
|
+
}
|
|
2062
|
+
const card = getCard(db, token.id, userId);
|
|
2063
|
+
const prereqs = getPrerequisites(db, token.id);
|
|
2064
|
+
const dependents = getDependents(db, token.id);
|
|
2065
|
+
const status = {
|
|
2066
|
+
token,
|
|
2067
|
+
card: card ?? null,
|
|
2068
|
+
prerequisites: prereqs,
|
|
2069
|
+
dependents
|
|
2070
|
+
};
|
|
2071
|
+
if (opts.json) {
|
|
2072
|
+
console.log(JSON.stringify(status, null, 2));
|
|
2073
|
+
return;
|
|
2074
|
+
}
|
|
2075
|
+
console.log(`Token: ${token.slug} (${token.id})`);
|
|
2076
|
+
console.log(` Concept: ${token.concept}`);
|
|
2077
|
+
console.log(` Domain: ${token.domain || "(none)"}`);
|
|
2078
|
+
console.log(` Bloom: ${token.bloom_level}`);
|
|
2079
|
+
console.log();
|
|
2080
|
+
if (card) {
|
|
2081
|
+
console.log("Card status:");
|
|
2082
|
+
console.log(` State: ${card.state}`);
|
|
2083
|
+
console.log(` Due at: ${card.due_at}`);
|
|
2084
|
+
console.log(` Stability: ${card.stability}`);
|
|
2085
|
+
console.log(` Difficulty: ${card.difficulty}`);
|
|
2086
|
+
console.log(` Reps: ${card.reps}`);
|
|
2087
|
+
console.log(` Lapses: ${card.lapses}`);
|
|
2088
|
+
console.log(` Blocked: ${card.blocked ? "Yes" : "No"}`);
|
|
2089
|
+
} else {
|
|
2090
|
+
console.log("No card exists for this user yet.");
|
|
2091
|
+
}
|
|
2092
|
+
console.log();
|
|
2093
|
+
if (prereqs.length > 0) {
|
|
2094
|
+
console.log("Prerequisites:");
|
|
2095
|
+
for (const p of prereqs) {
|
|
2096
|
+
console.log(` - ${p.slug}: ${p.concept} (bloom ${p.bloom_level})`);
|
|
2097
|
+
}
|
|
2098
|
+
} else {
|
|
2099
|
+
console.log("No prerequisites.");
|
|
2100
|
+
}
|
|
2101
|
+
if (dependents.length > 0) {
|
|
2102
|
+
console.log("\nDependents:");
|
|
2103
|
+
for (const d of dependents) {
|
|
2104
|
+
console.log(` - ${d.slug}: ${d.concept} (bloom ${d.bloom_level})`);
|
|
2105
|
+
}
|
|
2106
|
+
}
|
|
2107
|
+
});
|
|
2108
|
+
});
|
|
2109
|
+
|
|
2110
|
+
// src/cli/commands/card.ts
|
|
2111
|
+
import { Command as Command4 } from "commander";
|
|
2112
|
+
function withDb2(fn) {
|
|
2113
|
+
let db;
|
|
2114
|
+
try {
|
|
2115
|
+
db = openDatabase();
|
|
2116
|
+
fn(db);
|
|
2117
|
+
} catch (err) {
|
|
2118
|
+
console.error("Error:", err.message);
|
|
2119
|
+
process.exit(1);
|
|
2120
|
+
} finally {
|
|
2121
|
+
db?.close();
|
|
2122
|
+
}
|
|
2123
|
+
}
|
|
2124
|
+
var cardCommand = new Command4("card").description("Manage spaced-repetition cards");
|
|
2125
|
+
cardCommand.command("due").description("Show due tokens for a user").option("--user <id>", "User ID (default: whoami)").option("--json", "Output as JSON").option("--summary", "Show only counts per domain (no slugs or concepts)").action((opts) => {
|
|
2126
|
+
withDb2((db) => {
|
|
2127
|
+
const userId = resolveUser(opts, db);
|
|
2128
|
+
const dueCards = getDueCards(db, userId);
|
|
2129
|
+
if (opts.json) {
|
|
2130
|
+
console.log(JSON.stringify(dueCards, null, 2));
|
|
2131
|
+
return;
|
|
2132
|
+
}
|
|
2133
|
+
if (dueCards.length === 0) {
|
|
2134
|
+
console.log("No cards due for review.");
|
|
2135
|
+
return;
|
|
2136
|
+
}
|
|
2137
|
+
if (opts.summary) {
|
|
2138
|
+
const byDomain = /* @__PURE__ */ new Map();
|
|
2139
|
+
for (const c of dueCards) {
|
|
2140
|
+
const d = c.domain || "general";
|
|
2141
|
+
const entry = byDomain.get(d) ?? { count: 0, blooms: [] };
|
|
2142
|
+
entry.count++;
|
|
2143
|
+
entry.blooms.push(c.bloom_level);
|
|
2144
|
+
byDomain.set(d, entry);
|
|
2145
|
+
}
|
|
2146
|
+
console.log(`${dueCards.length} card(s) due:
|
|
2147
|
+
`);
|
|
2148
|
+
console.log(
|
|
2149
|
+
"Domain Count Bloom levels"
|
|
2150
|
+
);
|
|
2151
|
+
console.log("\u2500".repeat(45));
|
|
2152
|
+
for (const [domain, { count, blooms }] of [...byDomain.entries()].sort()) {
|
|
2153
|
+
const bloomStr = blooms.sort().join(", ");
|
|
2154
|
+
console.log(
|
|
2155
|
+
`${domain.padEnd(16)} ${String(count).padEnd(6)} ${bloomStr}`
|
|
2156
|
+
);
|
|
2157
|
+
}
|
|
2158
|
+
return;
|
|
2159
|
+
}
|
|
2160
|
+
console.log(`${dueCards.length} card(s) due:
|
|
2161
|
+
`);
|
|
2162
|
+
console.log(
|
|
2163
|
+
"Slug Concept Domain Bloom State"
|
|
2164
|
+
);
|
|
2165
|
+
console.log("\u2500".repeat(90));
|
|
2166
|
+
for (const c of dueCards) {
|
|
2167
|
+
console.log(
|
|
2168
|
+
`${c.slug.padEnd(21)} ${c.concept.slice(0, 31).padEnd(31)} ${(c.domain || "-").padEnd(11)} ${String(c.bloom_level).padEnd(6)} ${c.state}`
|
|
2169
|
+
);
|
|
2170
|
+
}
|
|
2171
|
+
});
|
|
2172
|
+
});
|
|
2173
|
+
cardCommand.command("update").description("Apply a rating to a card").option("--user <id>", "User ID (default: whoami)").requiredOption("--token <slug>", "Token slug").requiredOption("--rating <n>", "Rating (1=Again, 2=Hard, 3=Good, 4=Easy)").option("--json", "Output as JSON").option("--quiet", "Suppress output (exit code only)").action((opts) => {
|
|
2174
|
+
withDb2((db) => {
|
|
2175
|
+
const userId = resolveUser(opts, db);
|
|
2176
|
+
const token = getTokenBySlug(db, opts.token);
|
|
2177
|
+
if (!token) {
|
|
2178
|
+
console.error(`Token not found: ${opts.token}`);
|
|
2179
|
+
process.exit(1);
|
|
2180
|
+
}
|
|
2181
|
+
const card = ensureCard(db, token.id, userId);
|
|
2182
|
+
const rating = Number(opts.rating);
|
|
2183
|
+
if (rating < 1 || rating > 4) {
|
|
2184
|
+
console.error("Rating must be between 1 and 4.");
|
|
2185
|
+
process.exit(1);
|
|
2186
|
+
}
|
|
2187
|
+
const result = evaluateRating(db, {
|
|
2188
|
+
cardId: card.id,
|
|
2189
|
+
tokenId: token.id,
|
|
2190
|
+
userId,
|
|
2191
|
+
rating
|
|
2192
|
+
});
|
|
2193
|
+
if (rating === 1) {
|
|
2194
|
+
const prereqs = getPrerequisites(db, token.id);
|
|
2195
|
+
if (prereqs.length > 0) {
|
|
2196
|
+
const blockResult = cascadeBlock(db, userId, token.slug);
|
|
2197
|
+
if (opts.quiet) return;
|
|
2198
|
+
if (opts.json) {
|
|
2199
|
+
console.log(JSON.stringify({ evaluation: result, blocked: blockResult }, null, 2));
|
|
2200
|
+
} else {
|
|
2201
|
+
console.log(`Rated ${token.slug} as Again (1) \u2014 next due: ${result.nextDueAt}`);
|
|
2202
|
+
console.log(`Blocked ${blockResult.blockedSlug}. Prerequisites surfaced:`);
|
|
2203
|
+
for (const p of blockResult.prerequisites) {
|
|
2204
|
+
console.log(` - ${p.slug}: ${p.concept}`);
|
|
2205
|
+
}
|
|
2206
|
+
}
|
|
2207
|
+
return;
|
|
2208
|
+
}
|
|
2209
|
+
}
|
|
2210
|
+
if (opts.quiet) return;
|
|
2211
|
+
if (opts.json) {
|
|
2212
|
+
console.log(JSON.stringify(result, null, 2));
|
|
2213
|
+
} else {
|
|
2214
|
+
const ratingLabels = { 1: "Again", 2: "Hard", 3: "Good", 4: "Easy" };
|
|
2215
|
+
console.log(`Rated ${token.slug} as ${ratingLabels[rating]} (${rating})`);
|
|
2216
|
+
console.log(` Next due: ${result.nextDueAt}`);
|
|
2217
|
+
console.log(` Stability: ${result.stability.toFixed(2)}`);
|
|
2218
|
+
console.log(` State: ${result.state}`);
|
|
2219
|
+
console.log(` Reps: ${result.reps}`);
|
|
2220
|
+
}
|
|
2221
|
+
});
|
|
2222
|
+
});
|
|
2223
|
+
cardCommand.command("unblock").description("Unblock cards whose prerequisites are met").option("--user <id>", "User ID (default: whoami)").option("--json", "Output as JSON").option("--quiet", "Suppress output (exit code only)").action((opts) => {
|
|
2224
|
+
withDb2((db) => {
|
|
2225
|
+
const userId = resolveUser(opts, db);
|
|
2226
|
+
const result = unblockReady(db, userId);
|
|
2227
|
+
if (opts.quiet) return;
|
|
2228
|
+
if (opts.json) {
|
|
2229
|
+
console.log(JSON.stringify(result, null, 2));
|
|
2230
|
+
return;
|
|
2231
|
+
}
|
|
2232
|
+
if (result.unblocked.length === 0) {
|
|
2233
|
+
console.log("No cards ready to unblock.");
|
|
2234
|
+
} else {
|
|
2235
|
+
console.log(`Unblocked ${result.unblocked.length} card(s):`);
|
|
2236
|
+
for (const u of result.unblocked) {
|
|
2237
|
+
console.log(` - ${u.slug}: ${u.concept}`);
|
|
2238
|
+
}
|
|
2239
|
+
}
|
|
2240
|
+
});
|
|
2241
|
+
});
|
|
2242
|
+
|
|
2243
|
+
// src/cli/commands/session.ts
|
|
2244
|
+
import { Command as Command5 } from "commander";
|
|
2245
|
+
import { select, input } from "@inquirer/prompts";
|
|
2246
|
+
function withDb3(fn) {
|
|
2247
|
+
let db;
|
|
2248
|
+
try {
|
|
2249
|
+
db = openDatabase();
|
|
2250
|
+
fn(db);
|
|
2251
|
+
} catch (err) {
|
|
2252
|
+
console.error("Error:", err.message);
|
|
2253
|
+
process.exit(1);
|
|
2254
|
+
} finally {
|
|
2255
|
+
db?.close();
|
|
2256
|
+
}
|
|
2257
|
+
}
|
|
2258
|
+
var sessionCommand = new Command5("session").description("Manage learning sessions");
|
|
2259
|
+
sessionCommand.command("start").description("Start a new learning session (review \u2192 task)").option("--user <id>", "User ID (default: whoami)").option("--task <description>", "Task description (interactive if omitted)").option("--context <level>", "Execution context: shell | ui | reallife (default: shell)", "shell").option("--skip-review", "Skip the repetition phase and go straight to task selection").option("--review-minutes <n>", "Maximum minutes for the repetition phase (default: 20)", "20").option("--json", "Output as JSON").option("--quiet", "Output only the session ID").action(async (opts) => {
|
|
2260
|
+
let db;
|
|
2261
|
+
try {
|
|
2262
|
+
db = openDatabase();
|
|
2263
|
+
const validContexts = ["shell", "ui", "reallife"];
|
|
2264
|
+
if (!validContexts.includes(opts.context)) {
|
|
2265
|
+
console.error(`Invalid context: ${opts.context}. Must be one of: ${validContexts.join(", ")}`);
|
|
2266
|
+
process.exit(1);
|
|
2267
|
+
}
|
|
2268
|
+
const userId = resolveUser(opts, db);
|
|
2269
|
+
const reviewMinutes = Number(opts.reviewMinutes);
|
|
2270
|
+
if (!opts.skipReview && !opts.quiet && !opts.json) {
|
|
2271
|
+
const reviewResults = await runRepetitionPhase(db, userId, reviewMinutes);
|
|
2272
|
+
if (reviewResults.reviewed > 0) {
|
|
2273
|
+
console.log();
|
|
2274
|
+
}
|
|
2275
|
+
}
|
|
2276
|
+
let task = opts.task;
|
|
2277
|
+
if (!task && !opts.quiet && !opts.json) {
|
|
2278
|
+
task = await selectTask(db);
|
|
2279
|
+
}
|
|
2280
|
+
if (!task) {
|
|
2281
|
+
console.error("Task description is required. Use --task or run interactively.");
|
|
2282
|
+
process.exit(1);
|
|
2283
|
+
}
|
|
2284
|
+
const session = startSession(db, {
|
|
2285
|
+
user_id: userId,
|
|
2286
|
+
task,
|
|
2287
|
+
execution_context: opts.context
|
|
2288
|
+
});
|
|
2289
|
+
db.close();
|
|
2290
|
+
if (opts.quiet) {
|
|
2291
|
+
console.log(session.id);
|
|
2292
|
+
} else if (opts.json) {
|
|
2293
|
+
console.log(JSON.stringify(session, null, 2));
|
|
2294
|
+
} else {
|
|
2295
|
+
console.log(`
|
|
2296
|
+
Session started: ${session.id}`);
|
|
2297
|
+
console.log(` User: ${session.user_id}`);
|
|
2298
|
+
console.log(` Task: ${session.task}`);
|
|
2299
|
+
console.log(` Context: ${session.execution_context}`);
|
|
2300
|
+
console.log(` Started: ${session.started_at}`);
|
|
2301
|
+
}
|
|
2302
|
+
} catch (err) {
|
|
2303
|
+
db?.close();
|
|
2304
|
+
if (err.name === "ExitPromptError") {
|
|
2305
|
+
console.log("\nSession cancelled.");
|
|
2306
|
+
process.exit(0);
|
|
2307
|
+
}
|
|
2308
|
+
console.error("Error:", err.message);
|
|
2309
|
+
process.exit(1);
|
|
2310
|
+
}
|
|
2311
|
+
});
|
|
2312
|
+
async function runRepetitionPhase(db, userId, maxMinutes) {
|
|
2313
|
+
const queue = buildReviewQueue(db, { userId });
|
|
2314
|
+
if (queue.items.length === 0) {
|
|
2315
|
+
console.log("No cards due for review \u2014 moving to task selection.\n");
|
|
2316
|
+
return { reviewed: 0, skipped: false };
|
|
2317
|
+
}
|
|
2318
|
+
console.log("\u2550".repeat(50));
|
|
2319
|
+
console.log("Phase 1: Repetition");
|
|
2320
|
+
console.log("\u2550".repeat(50));
|
|
2321
|
+
console.log(`${queue.items.length} card(s) due`);
|
|
2322
|
+
console.log(` New: ${queue.newCount} Review: ${queue.reviewCount} Relearn: ${queue.relearnCount}`);
|
|
2323
|
+
console.log(` Domains: ${queue.totalDomains.join(", ")}`);
|
|
2324
|
+
console.log(` Time limit: ${maxMinutes} minutes (skip anytime with 's')`);
|
|
2325
|
+
console.log();
|
|
2326
|
+
const startTime = Date.now();
|
|
2327
|
+
const timeLimitMs = maxMinutes * 60 * 1e3;
|
|
2328
|
+
let reviewed = 0;
|
|
2329
|
+
for (const item of queue.items) {
|
|
2330
|
+
if (Date.now() - startTime >= timeLimitMs) {
|
|
2331
|
+
console.log(`
|
|
2332
|
+
Time limit reached (${maxMinutes} min). Moving to task selection.`);
|
|
2333
|
+
break;
|
|
2334
|
+
}
|
|
2335
|
+
reviewed++;
|
|
2336
|
+
const prompt = generatePrompt({
|
|
2337
|
+
cardId: item.cardId,
|
|
2338
|
+
tokenId: item.tokenId,
|
|
2339
|
+
slug: item.slug,
|
|
2340
|
+
concept: item.concept,
|
|
2341
|
+
domain: item.domain,
|
|
2342
|
+
bloomLevel: item.bloomLevel
|
|
2343
|
+
});
|
|
2344
|
+
const elapsed = Math.round((Date.now() - startTime) / 6e4);
|
|
2345
|
+
console.log(`[${reviewed}/${queue.items.length}] ${prompt.bloomVerb} (Bloom ${prompt.bloomLevel}) \u2014 ${elapsed}/${maxMinutes} min`);
|
|
2346
|
+
console.log(`Domain: ${prompt.domain || "(none)"}`);
|
|
2347
|
+
console.log(`
|
|
2348
|
+
${prompt.question}
|
|
2349
|
+
`);
|
|
2350
|
+
const rating = await select({
|
|
2351
|
+
message: "How did you do?",
|
|
2352
|
+
choices: [
|
|
2353
|
+
{ name: "1 - Again (forgot)", value: 1 },
|
|
2354
|
+
{ name: "2 - Hard", value: 2 },
|
|
2355
|
+
{ name: "3 - Good", value: 3 },
|
|
2356
|
+
{ name: "4 - Easy", value: 4 },
|
|
2357
|
+
{ name: "s - Skip to task selection", value: 0 }
|
|
2358
|
+
]
|
|
2359
|
+
});
|
|
2360
|
+
if (rating === 0) {
|
|
2361
|
+
console.log("Skipping to task selection.");
|
|
2362
|
+
reviewed--;
|
|
2363
|
+
return { reviewed, skipped: true };
|
|
2364
|
+
}
|
|
2365
|
+
const evalResult = evaluateRating(db, {
|
|
2366
|
+
cardId: item.cardId,
|
|
2367
|
+
tokenId: item.tokenId,
|
|
2368
|
+
userId,
|
|
2369
|
+
rating
|
|
2370
|
+
});
|
|
2371
|
+
if (rating === 1) {
|
|
2372
|
+
const prereqs = getPrerequisites(db, item.tokenId);
|
|
2373
|
+
if (prereqs.length > 0) {
|
|
2374
|
+
const blockResult = cascadeBlock(db, userId, item.slug);
|
|
2375
|
+
console.log(` Blocked ${blockResult.blockedSlug}. Review these prerequisites:`);
|
|
2376
|
+
for (const p of blockResult.prerequisites) {
|
|
2377
|
+
console.log(` - ${p.slug}: ${p.concept}`);
|
|
2378
|
+
}
|
|
2379
|
+
}
|
|
2380
|
+
}
|
|
2381
|
+
const ratingLabels = { 1: "Again", 2: "Hard", 3: "Good", 4: "Easy" };
|
|
2382
|
+
console.log(` ${ratingLabels[rating]} \u2014 next due: ${evalResult.nextDueAt}
|
|
2383
|
+
`);
|
|
2384
|
+
}
|
|
2385
|
+
if (reviewed > 0) {
|
|
2386
|
+
console.log("\u2500".repeat(50));
|
|
2387
|
+
console.log(`Repetition complete \u2014 ${reviewed} card(s) reviewed.`);
|
|
2388
|
+
}
|
|
2389
|
+
return { reviewed, skipped: false };
|
|
2390
|
+
}
|
|
2391
|
+
async function selectTask(db) {
|
|
2392
|
+
console.log("\u2550".repeat(50));
|
|
2393
|
+
console.log("Phase 2: Task Selection");
|
|
2394
|
+
console.log("\u2550".repeat(50));
|
|
2395
|
+
const adoConfig = loadADOConfig(db);
|
|
2396
|
+
if (adoConfig) {
|
|
2397
|
+
const items = await fetchActiveWorkItems(adoConfig);
|
|
2398
|
+
if (items.length > 0) {
|
|
2399
|
+
const choices = items.map((wi) => ({
|
|
2400
|
+
name: `[${wi.type}] ${wi.title} (${wi.state})`,
|
|
2401
|
+
value: `[ADO-${wi.id}] ${wi.title}`
|
|
2402
|
+
}));
|
|
2403
|
+
choices.push({ name: "Enter a custom task...", value: "__custom__" });
|
|
2404
|
+
const picked = await select({
|
|
2405
|
+
message: `${items.length} active work item(s) \u2014 pick one:`,
|
|
2406
|
+
choices
|
|
2407
|
+
});
|
|
2408
|
+
if (picked !== "__custom__") return picked;
|
|
2409
|
+
} else {
|
|
2410
|
+
console.log("No active work items found in Azure DevOps.");
|
|
2411
|
+
}
|
|
2412
|
+
}
|
|
2413
|
+
return input({ message: "Task description:" });
|
|
2414
|
+
}
|
|
2415
|
+
sessionCommand.command("log").description("Log a step within a session").requiredOption("--session <id>", "Session ID").requiredOption("--token <slug>", "Token slug").requiredOption("--done-by <who>", "Who performed the step (user or agent)").option("--rating <n>", "Rating (1-4)").option("--json", "Output as JSON").option("--quiet", "Suppress output (exit code only)").action((opts) => {
|
|
2416
|
+
withDb3((db) => {
|
|
2417
|
+
const token = getTokenBySlug(db, opts.token);
|
|
2418
|
+
if (!token) {
|
|
2419
|
+
console.error(`Token not found: ${opts.token}`);
|
|
2420
|
+
process.exit(1);
|
|
2421
|
+
}
|
|
2422
|
+
const step = logStep(db, {
|
|
2423
|
+
session_id: opts.session,
|
|
2424
|
+
token_id: token.id,
|
|
2425
|
+
done_by: opts.doneBy,
|
|
2426
|
+
rating: opts.rating ? Number(opts.rating) : void 0
|
|
2427
|
+
});
|
|
2428
|
+
if (opts.quiet) return;
|
|
2429
|
+
if (opts.json) {
|
|
2430
|
+
console.log(JSON.stringify(step, null, 2));
|
|
2431
|
+
} else {
|
|
2432
|
+
console.log(`Step logged: ${step.id}`);
|
|
2433
|
+
console.log(` Token: ${opts.token}`);
|
|
2434
|
+
console.log(` Done by: ${step.done_by}`);
|
|
2435
|
+
if (step.rating != null) {
|
|
2436
|
+
console.log(` Rating: ${step.rating}`);
|
|
2437
|
+
}
|
|
2438
|
+
}
|
|
2439
|
+
});
|
|
2440
|
+
});
|
|
2441
|
+
sessionCommand.command("end").description("End a session and show summary").requiredOption("--session <id>", "Session ID").option("--json", "Output as JSON").action((opts) => {
|
|
2442
|
+
withDb3((db) => {
|
|
2443
|
+
endSession(db, opts.session);
|
|
2444
|
+
const summary = getSessionSummary(db, opts.session);
|
|
2445
|
+
if (opts.json) {
|
|
2446
|
+
console.log(JSON.stringify(summary, null, 2));
|
|
2447
|
+
return;
|
|
2448
|
+
}
|
|
2449
|
+
console.log(`Session ${summary.session.id} completed.`);
|
|
2450
|
+
console.log(` Task: ${summary.session.task}`);
|
|
2451
|
+
console.log(` Started: ${summary.session.started_at}`);
|
|
2452
|
+
console.log(` Completed: ${summary.session.completed_at}`);
|
|
2453
|
+
console.log(` Steps: ${summary.steps.length}`);
|
|
2454
|
+
if (summary.steps.length > 0) {
|
|
2455
|
+
console.log("\nSteps:");
|
|
2456
|
+
console.log(
|
|
2457
|
+
" Token Done by Rating Concept"
|
|
2458
|
+
);
|
|
2459
|
+
console.log(" " + "\u2500".repeat(70));
|
|
2460
|
+
for (const s of summary.steps) {
|
|
2461
|
+
console.log(
|
|
2462
|
+
` ${s.slug.padEnd(21)} ${s.done_by.padEnd(8)} ${String(s.rating ?? "-").padEnd(7)} ${s.concept.slice(0, 30)}`
|
|
2463
|
+
);
|
|
2464
|
+
}
|
|
2465
|
+
}
|
|
2466
|
+
});
|
|
2467
|
+
});
|
|
2468
|
+
|
|
2469
|
+
// src/cli/commands/stats.ts
|
|
2470
|
+
import { Command as Command6 } from "commander";
|
|
2471
|
+
function withDb4(fn) {
|
|
2472
|
+
let db;
|
|
2473
|
+
try {
|
|
2474
|
+
db = openDatabase();
|
|
2475
|
+
fn(db);
|
|
2476
|
+
} catch (err) {
|
|
2477
|
+
console.error("Error:", err.message);
|
|
2478
|
+
process.exit(1);
|
|
2479
|
+
} finally {
|
|
2480
|
+
db?.close();
|
|
2481
|
+
}
|
|
2482
|
+
}
|
|
2483
|
+
var statsCommand = new Command6("stats").description("Show learning dashboard for a user").option("--user <id>", "User ID (default: whoami)").option("--json", "Output as JSON").action((opts) => {
|
|
2484
|
+
withDb4((db) => {
|
|
2485
|
+
const userId = resolveUser(opts, db);
|
|
2486
|
+
const stats = getUserStats(db, userId);
|
|
2487
|
+
const domains = getDomainCompetence(db, userId);
|
|
2488
|
+
if (opts.json) {
|
|
2489
|
+
console.log(JSON.stringify({ stats, domains }, null, 2));
|
|
2490
|
+
return;
|
|
2491
|
+
}
|
|
2492
|
+
console.log(`Learning Dashboard \u2014 ${stats.userId}`);
|
|
2493
|
+
console.log("\u2550".repeat(50));
|
|
2494
|
+
console.log(` Total tokens: ${stats.totalTokens}`);
|
|
2495
|
+
console.log(` Cards in deck: ${stats.cardsInDeck}`);
|
|
2496
|
+
console.log(` Due today: ${stats.dueToday}`);
|
|
2497
|
+
console.log(` Blocked: ${stats.blocked}`);
|
|
2498
|
+
console.log(` Mature: ${stats.mature}`);
|
|
2499
|
+
console.log(` Avg stability: ${stats.avgStability ?? "N/A"}`);
|
|
2500
|
+
console.log(` Total sessions: ${stats.totalSessions}`);
|
|
2501
|
+
console.log(` Last session: ${stats.lastSession ?? "N/A"}`);
|
|
2502
|
+
if (domains.length > 0) {
|
|
2503
|
+
console.log("\nDomain Competence:");
|
|
2504
|
+
console.log("\u2500".repeat(80));
|
|
2505
|
+
console.log(
|
|
2506
|
+
" Domain Cards Mature Stability Retention Suggested Mode"
|
|
2507
|
+
);
|
|
2508
|
+
console.log(" " + "\u2500".repeat(74));
|
|
2509
|
+
for (const d of domains) {
|
|
2510
|
+
console.log(
|
|
2511
|
+
` ${d.domain.padEnd(17)} ${String(d.totalCards).padEnd(6)} ${String(d.matureCards).padEnd(7)} ${String(d.avgStability).padEnd(10)} ${(d.retentionRate * 100).toFixed(1).padStart(5)}% ${d.suggestedMode}`
|
|
2512
|
+
);
|
|
2513
|
+
}
|
|
2514
|
+
}
|
|
2515
|
+
});
|
|
2516
|
+
});
|
|
2517
|
+
|
|
2518
|
+
// src/cli/commands/review.ts
|
|
2519
|
+
import { Command as Command7 } from "commander";
|
|
2520
|
+
import { select as select2 } from "@inquirer/prompts";
|
|
2521
|
+
var reviewCommand = new Command7("review").description("Start an interactive review session").option("--user <id>", "User ID (default: whoami)").option("--max-new <n>", "Maximum new cards", "10").option("--max-reviews <n>", "Maximum review cards", "50").action(async (opts) => {
|
|
2522
|
+
let db;
|
|
2523
|
+
try {
|
|
2524
|
+
db = openDatabase();
|
|
2525
|
+
const userId = resolveUser(opts, db);
|
|
2526
|
+
const queue = buildReviewQueue(db, {
|
|
2527
|
+
userId,
|
|
2528
|
+
maxNew: Number(opts.maxNew),
|
|
2529
|
+
maxReviews: Number(opts.maxReviews)
|
|
2530
|
+
});
|
|
2531
|
+
if (queue.items.length === 0) {
|
|
2532
|
+
console.log("No cards due for review. You're all caught up!");
|
|
2533
|
+
db.close();
|
|
2534
|
+
return;
|
|
2535
|
+
}
|
|
2536
|
+
console.log(`
|
|
2537
|
+
Review session: ${queue.items.length} card(s)`);
|
|
2538
|
+
console.log(` New: ${queue.newCount} Review: ${queue.reviewCount} Relearn: ${queue.relearnCount}`);
|
|
2539
|
+
console.log(` Domains: ${queue.totalDomains.join(", ")}`);
|
|
2540
|
+
console.log();
|
|
2541
|
+
let completed = 0;
|
|
2542
|
+
const results = [];
|
|
2543
|
+
for (const item of queue.items) {
|
|
2544
|
+
completed++;
|
|
2545
|
+
const prompt = generatePrompt({
|
|
2546
|
+
cardId: item.cardId,
|
|
2547
|
+
tokenId: item.tokenId,
|
|
2548
|
+
slug: item.slug,
|
|
2549
|
+
concept: item.concept,
|
|
2550
|
+
domain: item.domain,
|
|
2551
|
+
bloomLevel: item.bloomLevel
|
|
2552
|
+
});
|
|
2553
|
+
console.log(`
|
|
2554
|
+
[${completed}/${queue.items.length}] ${prompt.bloomVerb} (Bloom ${prompt.bloomLevel})`);
|
|
2555
|
+
console.log(`Domain: ${prompt.domain || "(none)"}`);
|
|
2556
|
+
console.log(`
|
|
2557
|
+
${prompt.question}
|
|
2558
|
+
`);
|
|
2559
|
+
const rating = await select2({
|
|
2560
|
+
message: "How did you do?",
|
|
2561
|
+
choices: [
|
|
2562
|
+
{ name: "1 - Again (forgot)", value: 1 },
|
|
2563
|
+
{ name: "2 - Hard", value: 2 },
|
|
2564
|
+
{ name: "3 - Good", value: 3 },
|
|
2565
|
+
{ name: "4 - Easy", value: 4 }
|
|
2566
|
+
]
|
|
2567
|
+
});
|
|
2568
|
+
const evalResult = evaluateRating(db, {
|
|
2569
|
+
cardId: item.cardId,
|
|
2570
|
+
tokenId: item.tokenId,
|
|
2571
|
+
userId,
|
|
2572
|
+
rating
|
|
2573
|
+
});
|
|
2574
|
+
if (rating === 1) {
|
|
2575
|
+
const prereqs = getPrerequisites(db, item.tokenId);
|
|
2576
|
+
if (prereqs.length > 0) {
|
|
2577
|
+
const blockResult = cascadeBlock(db, userId, item.slug);
|
|
2578
|
+
console.log(` Blocked ${blockResult.blockedSlug}. Review these prerequisites:`);
|
|
2579
|
+
for (const p of blockResult.prerequisites) {
|
|
2580
|
+
console.log(` - ${p.slug}: ${p.concept}`);
|
|
2581
|
+
}
|
|
2582
|
+
}
|
|
2583
|
+
}
|
|
2584
|
+
const ratingLabels = { 1: "Again", 2: "Hard", 3: "Good", 4: "Easy" };
|
|
2585
|
+
console.log(` ${ratingLabels[rating]} \u2014 next due: ${evalResult.nextDueAt}`);
|
|
2586
|
+
results.push({
|
|
2587
|
+
slug: item.slug,
|
|
2588
|
+
rating,
|
|
2589
|
+
nextDue: evalResult.nextDueAt
|
|
2590
|
+
});
|
|
2591
|
+
}
|
|
2592
|
+
console.log("\n" + "\u2550".repeat(50));
|
|
2593
|
+
console.log("Review session complete!");
|
|
2594
|
+
console.log(` Cards reviewed: ${results.length}`);
|
|
2595
|
+
const avgRating = results.reduce((s, r) => s + r.rating, 0) / results.length;
|
|
2596
|
+
console.log(` Average rating: ${avgRating.toFixed(1)}`);
|
|
2597
|
+
const forgot = results.filter((r) => r.rating === 1).length;
|
|
2598
|
+
if (forgot > 0) {
|
|
2599
|
+
console.log(` Forgot: ${forgot} card(s)`);
|
|
2600
|
+
}
|
|
2601
|
+
db.close();
|
|
2602
|
+
} catch (err) {
|
|
2603
|
+
db?.close();
|
|
2604
|
+
if (err.name === "ExitPromptError") {
|
|
2605
|
+
console.log("\nReview session cancelled.");
|
|
2606
|
+
process.exit(0);
|
|
2607
|
+
}
|
|
2608
|
+
console.error("Error:", err.message);
|
|
2609
|
+
process.exit(1);
|
|
2610
|
+
}
|
|
2611
|
+
});
|
|
2612
|
+
|
|
2613
|
+
// src/cli/commands/bridge.ts
|
|
2614
|
+
import { Command as Command8 } from "commander";
|
|
2615
|
+
import { readdirSync as readdirSync2 } from "fs";
|
|
2616
|
+
import { homedir as homedir3 } from "os";
|
|
2617
|
+
import { join as join5 } from "path";
|
|
2618
|
+
function jsonOut(data) {
|
|
2619
|
+
console.log(JSON.stringify(data, null, 2));
|
|
2620
|
+
}
|
|
2621
|
+
function jsonError(message) {
|
|
2622
|
+
console.log(JSON.stringify({ error: message }, null, 2));
|
|
2623
|
+
process.exit(1);
|
|
2624
|
+
}
|
|
2625
|
+
function withDb5(fn) {
|
|
2626
|
+
let db;
|
|
2627
|
+
try {
|
|
2628
|
+
db = openDatabase();
|
|
2629
|
+
fn(db);
|
|
2630
|
+
} catch (err) {
|
|
2631
|
+
db?.close();
|
|
2632
|
+
jsonError(err.message);
|
|
2633
|
+
} finally {
|
|
2634
|
+
db?.close();
|
|
2635
|
+
}
|
|
2636
|
+
}
|
|
2637
|
+
var bridgeCommand = new Command8("bridge").description("Machine-readable JSON protocol for AI integration");
|
|
2638
|
+
bridgeCommand.command("check-due").description("Check due cards for a user (JSON)").option("--user <id>", "User ID (default: whoami)").action((opts) => {
|
|
2639
|
+
withDb5((db) => {
|
|
2640
|
+
const userId = resolveUser(opts, db, { json: true });
|
|
2641
|
+
const dueCards = getDueCards(db, userId);
|
|
2642
|
+
const domains = [...new Set(dueCards.map((c) => c.domain).filter(Boolean))].sort();
|
|
2643
|
+
jsonOut({
|
|
2644
|
+
userId,
|
|
2645
|
+
dueCount: dueCards.length,
|
|
2646
|
+
domains,
|
|
2647
|
+
cards: dueCards.map((c) => ({
|
|
2648
|
+
cardId: c.id,
|
|
2649
|
+
tokenId: c.token_id,
|
|
2650
|
+
slug: c.slug,
|
|
2651
|
+
concept: c.concept,
|
|
2652
|
+
domain: c.domain,
|
|
2653
|
+
bloomLevel: c.bloom_level,
|
|
2654
|
+
state: c.state,
|
|
2655
|
+
dueAt: c.due_at
|
|
2656
|
+
}))
|
|
2657
|
+
});
|
|
2658
|
+
});
|
|
2659
|
+
});
|
|
2660
|
+
bridgeCommand.command("get-review").description("Get next review card with prompt (JSON)").option("--user <id>", "User ID (default: whoami)").action((opts) => {
|
|
2661
|
+
withDb5((db) => {
|
|
2662
|
+
const userId = resolveUser(opts, db, { json: true });
|
|
2663
|
+
const queue = buildReviewQueue(db, { userId, maxReviews: 1, maxNew: 1 });
|
|
2664
|
+
if (queue.items.length === 0) {
|
|
2665
|
+
jsonOut({
|
|
2666
|
+
userId,
|
|
2667
|
+
hasReview: false,
|
|
2668
|
+
card: null,
|
|
2669
|
+
prompt: null,
|
|
2670
|
+
queueSize: 0
|
|
2671
|
+
});
|
|
2672
|
+
return;
|
|
2673
|
+
}
|
|
2674
|
+
const item = queue.items[0];
|
|
2675
|
+
const prompt = generatePrompt({
|
|
2676
|
+
cardId: item.cardId,
|
|
2677
|
+
tokenId: item.tokenId,
|
|
2678
|
+
slug: item.slug,
|
|
2679
|
+
concept: item.concept,
|
|
2680
|
+
domain: item.domain,
|
|
2681
|
+
bloomLevel: item.bloomLevel
|
|
2682
|
+
});
|
|
2683
|
+
const fullQueue = buildReviewQueue(db, { userId });
|
|
2684
|
+
jsonOut({
|
|
2685
|
+
userId,
|
|
2686
|
+
hasReview: true,
|
|
2687
|
+
card: item,
|
|
2688
|
+
prompt,
|
|
2689
|
+
queueSize: fullQueue.items.length
|
|
2690
|
+
});
|
|
2691
|
+
});
|
|
2692
|
+
});
|
|
2693
|
+
bridgeCommand.command("submit").description("Submit a rating for a card (JSON)").option("--user <id>", "User ID (default: whoami)").requiredOption("--card-id <id>", "Card ID").requiredOption("--rating <n>", "Rating (1-4)").action((opts) => {
|
|
2694
|
+
withDb5((db) => {
|
|
2695
|
+
const userId = resolveUser(opts, db, { json: true });
|
|
2696
|
+
const rating = Number(opts.rating);
|
|
2697
|
+
if (rating < 1 || rating > 4) {
|
|
2698
|
+
jsonError("Rating must be between 1 and 4");
|
|
2699
|
+
}
|
|
2700
|
+
const card = db.prepare("SELECT * FROM cards WHERE id = ?").get(opts.cardId);
|
|
2701
|
+
if (!card) {
|
|
2702
|
+
jsonError(`Card not found: ${opts.cardId}`);
|
|
2703
|
+
}
|
|
2704
|
+
const result = evaluateRating(db, {
|
|
2705
|
+
cardId: opts.cardId,
|
|
2706
|
+
tokenId: card.token_id,
|
|
2707
|
+
userId,
|
|
2708
|
+
rating
|
|
2709
|
+
});
|
|
2710
|
+
let blocked = null;
|
|
2711
|
+
if (rating === 1) {
|
|
2712
|
+
const token = db.prepare("SELECT slug FROM tokens WHERE id = ?").get(card.token_id);
|
|
2713
|
+
if (token) {
|
|
2714
|
+
const prereqs = getPrerequisites(db, card.token_id);
|
|
2715
|
+
if (prereqs.length > 0) {
|
|
2716
|
+
blocked = cascadeBlock(db, userId, token.slug);
|
|
2717
|
+
}
|
|
2718
|
+
}
|
|
2719
|
+
}
|
|
2720
|
+
jsonOut({
|
|
2721
|
+
success: true,
|
|
2722
|
+
rating,
|
|
2723
|
+
evaluation: result,
|
|
2724
|
+
blocked
|
|
2725
|
+
});
|
|
2726
|
+
});
|
|
2727
|
+
});
|
|
2728
|
+
bridgeCommand.command("get-skill").description("Get an agent skill by slug (JSON)").requiredOption("--slug <slug>", "Skill slug").action((opts) => {
|
|
2729
|
+
withDb5((db) => {
|
|
2730
|
+
const skill = getAgentSkill(db, opts.slug);
|
|
2731
|
+
if (!skill) {
|
|
2732
|
+
jsonError(`Skill not found: ${opts.slug}`);
|
|
2733
|
+
}
|
|
2734
|
+
jsonOut({
|
|
2735
|
+
slug: skill.slug,
|
|
2736
|
+
description: skill.description,
|
|
2737
|
+
steps: skill.steps,
|
|
2738
|
+
tokenSlugs: skill.token_slugs,
|
|
2739
|
+
source: skill.source
|
|
2740
|
+
});
|
|
2741
|
+
});
|
|
2742
|
+
});
|
|
2743
|
+
bridgeCommand.command("get-monitor").description("Read monitor log for a session (JSON)").requiredOption("--session <id>", "Session ID").action((opts) => {
|
|
2744
|
+
if (!monitorLogExists(opts.session)) {
|
|
2745
|
+
jsonOut({ sessionId: opts.session, exists: false, commands: [], timeSpan: null });
|
|
2746
|
+
return;
|
|
2747
|
+
}
|
|
2748
|
+
const events = readMonitorLog(opts.session);
|
|
2749
|
+
const commands = pairCommands(events);
|
|
2750
|
+
let timeSpan = null;
|
|
2751
|
+
if (commands.length > 0) {
|
|
2752
|
+
const first = commands[0];
|
|
2753
|
+
const last = commands[commands.length - 1];
|
|
2754
|
+
const endTs = last.endedAt ?? last.startedAt;
|
|
2755
|
+
timeSpan = {
|
|
2756
|
+
start: first.startedAt,
|
|
2757
|
+
end: endTs,
|
|
2758
|
+
durationMs: new Date(endTs).getTime() - new Date(first.startedAt).getTime()
|
|
2759
|
+
};
|
|
2760
|
+
}
|
|
2761
|
+
jsonOut({
|
|
2762
|
+
sessionId: opts.session,
|
|
2763
|
+
exists: true,
|
|
2764
|
+
commands: commands.map((c) => ({
|
|
2765
|
+
seq: c.seq,
|
|
2766
|
+
command: c.command,
|
|
2767
|
+
cwd: c.cwd,
|
|
2768
|
+
startedAt: c.startedAt,
|
|
2769
|
+
endedAt: c.endedAt,
|
|
2770
|
+
durationMs: c.durationMs,
|
|
2771
|
+
exitCode: c.exitCode
|
|
2772
|
+
})),
|
|
2773
|
+
timeSpan
|
|
2774
|
+
});
|
|
2775
|
+
});
|
|
2776
|
+
bridgeCommand.command("analyze-monitor").description("Analyze monitor log with token patterns from stdin (JSON)").requiredOption("--session <id>", "Session ID").action(async (opts) => {
|
|
2777
|
+
try {
|
|
2778
|
+
if (!monitorLogExists(opts.session)) {
|
|
2779
|
+
jsonOut({ sessionId: opts.session, ratings: [], unmatchedCommands: [], timeSpan: null });
|
|
2780
|
+
return;
|
|
2781
|
+
}
|
|
2782
|
+
const chunks = [];
|
|
2783
|
+
for await (const chunk of process.stdin) {
|
|
2784
|
+
chunks.push(chunk);
|
|
2785
|
+
}
|
|
2786
|
+
const raw = Buffer.concat(chunks).toString("utf-8").trim();
|
|
2787
|
+
if (!raw) {
|
|
2788
|
+
jsonError("No input received on stdin. Pipe JSON with token patterns.");
|
|
2789
|
+
}
|
|
2790
|
+
let data;
|
|
2791
|
+
try {
|
|
2792
|
+
data = JSON.parse(raw);
|
|
2793
|
+
} catch {
|
|
2794
|
+
jsonError("Invalid JSON input");
|
|
2795
|
+
}
|
|
2796
|
+
if (!Array.isArray(data.patterns)) {
|
|
2797
|
+
jsonError("JSON must include 'patterns' array");
|
|
2798
|
+
}
|
|
2799
|
+
const events = readMonitorLog(opts.session);
|
|
2800
|
+
const commands = pairCommands(events);
|
|
2801
|
+
const result = analyzeObservation(commands, data.patterns);
|
|
2802
|
+
jsonOut({
|
|
2803
|
+
sessionId: opts.session,
|
|
2804
|
+
...result
|
|
2805
|
+
});
|
|
2806
|
+
} catch (err) {
|
|
2807
|
+
jsonError(err.message);
|
|
2808
|
+
}
|
|
2809
|
+
});
|
|
2810
|
+
bridgeCommand.command("add-token").description("Create a token + card from JSON stdin").option("--user <id>", "User ID (default: whoami)").action(async (opts) => {
|
|
2811
|
+
let db;
|
|
2812
|
+
try {
|
|
2813
|
+
const chunks = [];
|
|
2814
|
+
for await (const chunk of process.stdin) {
|
|
2815
|
+
chunks.push(chunk);
|
|
2816
|
+
}
|
|
2817
|
+
const raw = Buffer.concat(chunks).toString("utf-8").trim();
|
|
2818
|
+
if (!raw) {
|
|
2819
|
+
jsonError("No input received on stdin. Pipe JSON with token data.");
|
|
2820
|
+
}
|
|
2821
|
+
let data;
|
|
2822
|
+
try {
|
|
2823
|
+
data = JSON.parse(raw);
|
|
2824
|
+
} catch {
|
|
2825
|
+
jsonError("Invalid JSON input");
|
|
2826
|
+
}
|
|
2827
|
+
if (!data.slug || !data.concept) {
|
|
2828
|
+
jsonError("JSON must include 'slug' and 'concept' fields");
|
|
2829
|
+
}
|
|
2830
|
+
db = openDatabase();
|
|
2831
|
+
const userId = resolveUser(opts, db, { json: true });
|
|
2832
|
+
const token = createToken(db, {
|
|
2833
|
+
slug: data.slug,
|
|
2834
|
+
concept: data.concept,
|
|
2835
|
+
domain: data.domain,
|
|
2836
|
+
bloom_level: data.bloom_level ?? 1,
|
|
2837
|
+
context: data.context,
|
|
2838
|
+
symbiosis_mode: data.symbiosis_mode
|
|
2839
|
+
});
|
|
2840
|
+
const card = ensureCard(db, token.id, userId);
|
|
2841
|
+
jsonOut({
|
|
2842
|
+
success: true,
|
|
2843
|
+
token,
|
|
2844
|
+
card: {
|
|
2845
|
+
id: card.id,
|
|
2846
|
+
tokenId: card.token_id,
|
|
2847
|
+
userId: card.user_id,
|
|
2848
|
+
state: card.state,
|
|
2849
|
+
dueAt: card.due_at,
|
|
2850
|
+
blocked: card.blocked
|
|
2851
|
+
}
|
|
2852
|
+
});
|
|
2853
|
+
db.close();
|
|
2854
|
+
} catch (err) {
|
|
2855
|
+
db?.close();
|
|
2856
|
+
if (err.message) {
|
|
2857
|
+
jsonError(err.message);
|
|
2858
|
+
}
|
|
2859
|
+
}
|
|
2860
|
+
});
|
|
2861
|
+
bridgeCommand.command("discover-skills").description("Analyze monitor logs across sessions to discover recurring patterns").option("--min-sessions <n>", "Minimum sessions a pattern must appear in (default: 2)", "2").option("--limit <n>", "Max number of sessions to analyze (default: 20)", "20").action((opts) => {
|
|
2862
|
+
try {
|
|
2863
|
+
const monitorDir = join5(homedir3(), ".zam", "monitor");
|
|
2864
|
+
let files;
|
|
2865
|
+
try {
|
|
2866
|
+
files = readdirSync2(monitorDir).filter((f) => f.endsWith(".jsonl"));
|
|
2867
|
+
} catch {
|
|
2868
|
+
jsonOut({ proposals: [], message: "No monitor logs found." });
|
|
2869
|
+
return;
|
|
2870
|
+
}
|
|
2871
|
+
if (files.length === 0) {
|
|
2872
|
+
jsonOut({ proposals: [], message: "No monitor logs found." });
|
|
2873
|
+
return;
|
|
2874
|
+
}
|
|
2875
|
+
const limit = Number(opts.limit);
|
|
2876
|
+
const sorted = files.map((f) => ({ name: f, path: join5(monitorDir, f) })).sort((a, b) => b.name.localeCompare(a.name)).slice(0, limit);
|
|
2877
|
+
const sessionCommands = /* @__PURE__ */ new Map();
|
|
2878
|
+
for (const file of sorted) {
|
|
2879
|
+
const sessionId = file.name.replace(".jsonl", "");
|
|
2880
|
+
const events = readMonitorLog(sessionId);
|
|
2881
|
+
const commands = pairCommands(events);
|
|
2882
|
+
if (commands.length > 0) {
|
|
2883
|
+
sessionCommands.set(sessionId, commands);
|
|
2884
|
+
}
|
|
2885
|
+
}
|
|
2886
|
+
if (sessionCommands.size === 0) {
|
|
2887
|
+
jsonOut({ proposals: [], message: "No command data in monitor logs." });
|
|
2888
|
+
return;
|
|
2889
|
+
}
|
|
2890
|
+
let existingSkillSlugs = [];
|
|
2891
|
+
let db;
|
|
2892
|
+
try {
|
|
2893
|
+
db = openDatabase();
|
|
2894
|
+
existingSkillSlugs = listAgentSkills(db).map((s) => s.slug);
|
|
2895
|
+
} catch {
|
|
2896
|
+
} finally {
|
|
2897
|
+
db?.close();
|
|
2898
|
+
}
|
|
2899
|
+
const proposals = discoverSkills(sessionCommands, {
|
|
2900
|
+
minSessions: Number(opts.minSessions),
|
|
2901
|
+
existingSkillSlugs
|
|
2902
|
+
});
|
|
2903
|
+
jsonOut({
|
|
2904
|
+
sessionsAnalyzed: sessionCommands.size,
|
|
2905
|
+
proposals
|
|
2906
|
+
});
|
|
2907
|
+
} catch (err) {
|
|
2908
|
+
jsonError(err.message);
|
|
2909
|
+
}
|
|
2910
|
+
});
|
|
2911
|
+
|
|
2912
|
+
// src/cli/commands/skill.ts
|
|
2913
|
+
import { Command as Command9 } from "commander";
|
|
2914
|
+
function withDb6(fn) {
|
|
2915
|
+
let db;
|
|
2916
|
+
try {
|
|
2917
|
+
db = openDatabase();
|
|
2918
|
+
fn(db);
|
|
2919
|
+
} catch (err) {
|
|
2920
|
+
console.error("Error:", err.message);
|
|
2921
|
+
process.exit(1);
|
|
2922
|
+
} finally {
|
|
2923
|
+
db?.close();
|
|
2924
|
+
}
|
|
2925
|
+
}
|
|
2926
|
+
var skillCommand = new Command9("skill").description("Manage agent skill entries (task recipes)");
|
|
2927
|
+
skillCommand.command("list").description("List all agent skills").option("--json", "Output as JSON").action((opts) => {
|
|
2928
|
+
withDb6((db) => {
|
|
2929
|
+
const skills = listAgentSkills(db);
|
|
2930
|
+
if (opts.json) {
|
|
2931
|
+
console.log(JSON.stringify(skills, null, 2));
|
|
2932
|
+
return;
|
|
2933
|
+
}
|
|
2934
|
+
if (skills.length === 0) {
|
|
2935
|
+
console.log("No agent skills registered yet.");
|
|
2936
|
+
return;
|
|
2937
|
+
}
|
|
2938
|
+
console.log(`Agent Skills (${skills.length})`);
|
|
2939
|
+
console.log("\u2500".repeat(60));
|
|
2940
|
+
for (const s of skills) {
|
|
2941
|
+
console.log(` ${s.slug.padEnd(30)} [${s.source}] ${s.description.slice(0, 40)}`);
|
|
2942
|
+
console.log(` ${s.steps.length} step(s) tokens: ${s.token_slugs.join(", ") || "none"}`);
|
|
2943
|
+
}
|
|
2944
|
+
});
|
|
2945
|
+
});
|
|
2946
|
+
skillCommand.command("show").description("Show a specific agent skill").requiredOption("--slug <slug>", "Skill slug").option("--json", "Output as JSON").action((opts) => {
|
|
2947
|
+
withDb6((db) => {
|
|
2948
|
+
const skill = getAgentSkill(db, opts.slug);
|
|
2949
|
+
if (!skill) {
|
|
2950
|
+
console.error(`Skill not found: ${opts.slug}`);
|
|
2951
|
+
process.exit(1);
|
|
2952
|
+
}
|
|
2953
|
+
if (opts.json) {
|
|
2954
|
+
console.log(JSON.stringify(skill, null, 2));
|
|
2955
|
+
return;
|
|
2956
|
+
}
|
|
2957
|
+
console.log(`Skill: ${skill.slug}`);
|
|
2958
|
+
console.log(` Description: ${skill.description}`);
|
|
2959
|
+
console.log(` Source: ${skill.source}`);
|
|
2960
|
+
console.log(` Tokens: ${skill.token_slugs.join(", ") || "none"}`);
|
|
2961
|
+
console.log(` Created: ${skill.created_at}`);
|
|
2962
|
+
console.log(`
|
|
2963
|
+
Steps:`);
|
|
2964
|
+
skill.steps.forEach((step, i) => {
|
|
2965
|
+
console.log(` ${i + 1}. ${step}`);
|
|
2966
|
+
});
|
|
2967
|
+
});
|
|
2968
|
+
});
|
|
2969
|
+
skillCommand.command("add").description("Register a new agent skill").requiredOption("--slug <slug>", "Unique skill identifier").requiredOption("--description <text>", "One-sentence description").requiredOption("--steps <json>", "JSON array of step strings").option("--tokens <slugs>", "Comma-separated token slugs related to this skill").option("--source <type>", "Source: learned | builtin (default: learned)", "learned").option("--json", "Output as JSON").action((opts) => {
|
|
2970
|
+
withDb6((db) => {
|
|
2971
|
+
let steps;
|
|
2972
|
+
try {
|
|
2973
|
+
steps = JSON.parse(opts.steps);
|
|
2974
|
+
if (!Array.isArray(steps)) throw new Error("steps must be a JSON array");
|
|
2975
|
+
} catch {
|
|
2976
|
+
console.error("Invalid --steps: must be a valid JSON array of strings");
|
|
2977
|
+
process.exit(1);
|
|
2978
|
+
}
|
|
2979
|
+
const tokenSlugs = opts.tokens ? opts.tokens.split(",").map((s) => s.trim()).filter(Boolean) : [];
|
|
2980
|
+
const skill = createAgentSkill(db, {
|
|
2981
|
+
slug: opts.slug,
|
|
2982
|
+
description: opts.description,
|
|
2983
|
+
steps,
|
|
2984
|
+
token_slugs: tokenSlugs,
|
|
2985
|
+
source: opts.source
|
|
2986
|
+
});
|
|
2987
|
+
if (opts.json) {
|
|
2988
|
+
console.log(JSON.stringify(skill, null, 2));
|
|
2989
|
+
} else {
|
|
2990
|
+
console.log(`Skill registered: ${skill.slug}`);
|
|
2991
|
+
console.log(` ${skill.steps.length} step(s) saved`);
|
|
2992
|
+
}
|
|
2993
|
+
});
|
|
2994
|
+
});
|
|
2995
|
+
|
|
2996
|
+
// src/cli/commands/monitor.ts
|
|
2997
|
+
import { Command as Command10 } from "commander";
|
|
2998
|
+
import { basename as basename3, join as join6 } from "path";
|
|
2999
|
+
import { execSync } from "child_process";
|
|
3000
|
+
import { writeFileSync as writeFileSync3, unlinkSync } from "fs";
|
|
3001
|
+
import { tmpdir } from "os";
|
|
3002
|
+
function detectShell() {
|
|
3003
|
+
const shell = process.env.SHELL ?? "";
|
|
3004
|
+
return basename3(shell) === "bash" ? "bash" : "zsh";
|
|
3005
|
+
}
|
|
3006
|
+
var monitorCommand = new Command10("monitor").description("Shell observation for real-time task monitoring");
|
|
3007
|
+
monitorCommand.command("start").description("Output shell hook code to install monitoring (wrap with eval)").requiredOption("--session <id>", "Session ID to monitor").option("--shell <type>", "Shell type: zsh | bash (auto-detected from $SHELL)").action((opts) => {
|
|
3008
|
+
let db;
|
|
3009
|
+
try {
|
|
3010
|
+
db = openDatabase();
|
|
3011
|
+
const session = db.prepare("SELECT id, completed_at FROM sessions WHERE id = ?").get(opts.session);
|
|
3012
|
+
if (!session) {
|
|
3013
|
+
console.error(`# Error: Session not found: ${opts.session}`);
|
|
3014
|
+
process.exit(1);
|
|
3015
|
+
}
|
|
3016
|
+
if (session.completed_at) {
|
|
3017
|
+
console.error(`# Error: Session already completed: ${opts.session}`);
|
|
3018
|
+
process.exit(1);
|
|
3019
|
+
}
|
|
3020
|
+
} catch (err) {
|
|
3021
|
+
console.error(`# Error: ${err.message}`);
|
|
3022
|
+
process.exit(1);
|
|
3023
|
+
} finally {
|
|
3024
|
+
db?.close();
|
|
3025
|
+
}
|
|
3026
|
+
ensureMonitorDir();
|
|
3027
|
+
const monitorFile = getMonitorPath(opts.session);
|
|
3028
|
+
const meta = {
|
|
3029
|
+
type: "monitor_meta",
|
|
3030
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3031
|
+
event: "start",
|
|
3032
|
+
session_id: opts.session,
|
|
3033
|
+
shell: opts.shell ?? detectShell(),
|
|
3034
|
+
pid: process.pid
|
|
3035
|
+
};
|
|
3036
|
+
writeMonitorEvent(opts.session, meta);
|
|
3037
|
+
const shell = opts.shell ?? detectShell();
|
|
3038
|
+
if (shell === "bash") {
|
|
3039
|
+
console.log(generateBashHooks(monitorFile, opts.session));
|
|
3040
|
+
} else {
|
|
3041
|
+
console.log(generateZshHooks(monitorFile, opts.session));
|
|
3042
|
+
}
|
|
3043
|
+
});
|
|
3044
|
+
monitorCommand.command("stop").description("Output shell code to remove monitoring hooks (wrap with eval)").requiredOption("--session <id>", "Session ID").option("--shell <type>", "Shell type: zsh | bash (auto-detected from $SHELL)").action((opts) => {
|
|
3045
|
+
if (monitorLogExists(opts.session)) {
|
|
3046
|
+
const meta = {
|
|
3047
|
+
type: "monitor_meta",
|
|
3048
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3049
|
+
event: "stop",
|
|
3050
|
+
session_id: opts.session
|
|
3051
|
+
};
|
|
3052
|
+
writeMonitorEvent(opts.session, meta);
|
|
3053
|
+
}
|
|
3054
|
+
const shell = opts.shell ?? detectShell();
|
|
3055
|
+
if (shell === "bash") {
|
|
3056
|
+
console.log(generateBashUnhooks());
|
|
3057
|
+
} else {
|
|
3058
|
+
console.log(generateZshUnhooks());
|
|
3059
|
+
}
|
|
3060
|
+
});
|
|
3061
|
+
monitorCommand.command("status").description("Show monitoring status for a session").requiredOption("--session <id>", "Session ID").option("--json", "Output as JSON").action((opts) => {
|
|
3062
|
+
const stats = getMonitorLogStats(opts.session);
|
|
3063
|
+
if (!stats.exists) {
|
|
3064
|
+
if (opts.json) {
|
|
3065
|
+
console.log(JSON.stringify({ exists: false }));
|
|
3066
|
+
} else {
|
|
3067
|
+
console.log(`No monitor log found for session ${opts.session}`);
|
|
3068
|
+
}
|
|
3069
|
+
return;
|
|
3070
|
+
}
|
|
3071
|
+
const events = readMonitorLog(opts.session);
|
|
3072
|
+
const commands = pairCommands(events);
|
|
3073
|
+
const errors = commands.filter((c) => c.exitCode != null && c.exitCode !== 0).length;
|
|
3074
|
+
const meta = events.find((e) => e.type === "monitor_meta" && e.event === "start");
|
|
3075
|
+
const stopped = events.some((e) => e.type === "monitor_meta" && e.event === "stop");
|
|
3076
|
+
const result = {
|
|
3077
|
+
sessionId: opts.session,
|
|
3078
|
+
exists: true,
|
|
3079
|
+
active: !stopped,
|
|
3080
|
+
shell: meta?.shell ?? "unknown",
|
|
3081
|
+
totalCommands: commands.length,
|
|
3082
|
+
errors,
|
|
3083
|
+
sizeBytes: stats.sizeBytes,
|
|
3084
|
+
timeSpan: commands.length > 0 ? {
|
|
3085
|
+
start: commands[0].startedAt,
|
|
3086
|
+
end: commands[commands.length - 1].endedAt ?? commands[commands.length - 1].startedAt
|
|
3087
|
+
} : null
|
|
3088
|
+
};
|
|
3089
|
+
if (opts.json) {
|
|
3090
|
+
console.log(JSON.stringify(result, null, 2));
|
|
3091
|
+
return;
|
|
3092
|
+
}
|
|
3093
|
+
console.log(`Monitor: ${opts.session}`);
|
|
3094
|
+
console.log(` Status: ${result.active ? "active" : "stopped"}`);
|
|
3095
|
+
console.log(` Shell: ${result.shell}`);
|
|
3096
|
+
console.log(` Commands: ${result.totalCommands}`);
|
|
3097
|
+
console.log(` Errors: ${result.errors}`);
|
|
3098
|
+
if (result.timeSpan) {
|
|
3099
|
+
console.log(` From: ${result.timeSpan.start}`);
|
|
3100
|
+
console.log(` To: ${result.timeSpan.end}`);
|
|
3101
|
+
}
|
|
3102
|
+
});
|
|
3103
|
+
function resolveZamBin() {
|
|
3104
|
+
try {
|
|
3105
|
+
const which = execSync("which zam 2>/dev/null", { encoding: "utf-8" }).trim();
|
|
3106
|
+
if (which) return which;
|
|
3107
|
+
} catch {
|
|
3108
|
+
}
|
|
3109
|
+
const projectRoot = join6(import.meta.dirname, "..", "..", "..");
|
|
3110
|
+
return `npx --prefix ${JSON.stringify(projectRoot)} tsx ${join6(projectRoot, "src/cli/index.ts")}`;
|
|
3111
|
+
}
|
|
3112
|
+
function isItermRunning() {
|
|
3113
|
+
try {
|
|
3114
|
+
const result = execSync(
|
|
3115
|
+
`osascript -e 'tell application "System Events" to (name of processes) contains "iTerm2"' 2>/dev/null`,
|
|
3116
|
+
{ encoding: "utf-8" }
|
|
3117
|
+
).trim();
|
|
3118
|
+
return result === "true";
|
|
3119
|
+
} catch {
|
|
3120
|
+
return false;
|
|
3121
|
+
}
|
|
3122
|
+
}
|
|
3123
|
+
monitorCommand.command("open").description("Open a new monitored terminal window for a session").requiredOption("--session <id>", "Session ID to monitor").option("--dir <path>", "Working directory (defaults to cwd)").option("--shell <type>", "Shell type: zsh | bash (auto-detected from $SHELL)").action((opts) => {
|
|
3124
|
+
let db;
|
|
3125
|
+
try {
|
|
3126
|
+
db = openDatabase();
|
|
3127
|
+
const session = db.prepare("SELECT id, completed_at FROM sessions WHERE id = ?").get(opts.session);
|
|
3128
|
+
if (!session) {
|
|
3129
|
+
console.error(`Error: Session not found: ${opts.session}`);
|
|
3130
|
+
process.exit(1);
|
|
3131
|
+
}
|
|
3132
|
+
if (session.completed_at) {
|
|
3133
|
+
console.error(`Error: Session already completed: ${opts.session}`);
|
|
3134
|
+
process.exit(1);
|
|
3135
|
+
}
|
|
3136
|
+
if (!getSetting(db, "monitor_method")) {
|
|
3137
|
+
setSetting(db, "monitor_method", "terminal");
|
|
3138
|
+
}
|
|
3139
|
+
} catch (err) {
|
|
3140
|
+
console.error(`Error: ${err.message}`);
|
|
3141
|
+
process.exit(1);
|
|
3142
|
+
} finally {
|
|
3143
|
+
db?.close();
|
|
3144
|
+
}
|
|
3145
|
+
const dir = opts.dir ?? process.cwd();
|
|
3146
|
+
const zamBin = resolveZamBin();
|
|
3147
|
+
const shellSetup = `cd ${JSON.stringify(dir)} && eval "$(${zamBin} monitor start --session ${opts.session})"`;
|
|
3148
|
+
if (process.platform === "darwin") {
|
|
3149
|
+
openMacTerminal(shellSetup, opts.session, dir);
|
|
3150
|
+
} else {
|
|
3151
|
+
console.log(`Run this in a new terminal:
|
|
3152
|
+
`);
|
|
3153
|
+
console.log(` ${shellSetup}
|
|
3154
|
+
`);
|
|
3155
|
+
console.log(`(Automatic terminal opening is only supported on macOS for now.)`);
|
|
3156
|
+
}
|
|
3157
|
+
});
|
|
3158
|
+
function openMacTerminal(shellSetup, sessionId, dir) {
|
|
3159
|
+
const useIterm = isItermRunning();
|
|
3160
|
+
const escaped = shellSetup.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
3161
|
+
const appleScript = useIterm ? `tell application "iTerm2"
|
|
3162
|
+
activate
|
|
3163
|
+
set newWindow to (create window with default profile)
|
|
3164
|
+
tell current session of newWindow
|
|
3165
|
+
write text "${escaped}"
|
|
3166
|
+
end tell
|
|
3167
|
+
end tell` : `tell application "Terminal"
|
|
3168
|
+
activate
|
|
3169
|
+
do script "${escaped}"
|
|
3170
|
+
end tell`;
|
|
3171
|
+
const tmpFile = join6(tmpdir(), `zam-monitor-${sessionId}.scpt`);
|
|
3172
|
+
try {
|
|
3173
|
+
writeFileSync3(tmpFile, appleScript);
|
|
3174
|
+
execSync(`osascript ${JSON.stringify(tmpFile)}`, { stdio: "ignore" });
|
|
3175
|
+
console.log(`Opened ${useIterm ? "iTerm2" : "Terminal.app"} window with monitoring for session ${sessionId}`);
|
|
3176
|
+
console.log(` Directory: ${dir}`);
|
|
3177
|
+
} catch (err) {
|
|
3178
|
+
console.error(`Failed to open terminal: ${err.message}`);
|
|
3179
|
+
console.log(`
|
|
3180
|
+
Run this manually in a new terminal:
|
|
3181
|
+
`);
|
|
3182
|
+
console.log(` ${shellSetup}`);
|
|
3183
|
+
} finally {
|
|
3184
|
+
try {
|
|
3185
|
+
unlinkSync(tmpFile);
|
|
3186
|
+
} catch {
|
|
3187
|
+
}
|
|
3188
|
+
}
|
|
3189
|
+
}
|
|
3190
|
+
|
|
3191
|
+
// src/cli/commands/settings.ts
|
|
3192
|
+
import { Command as Command11 } from "commander";
|
|
3193
|
+
function withDb7(fn) {
|
|
3194
|
+
let db;
|
|
3195
|
+
try {
|
|
3196
|
+
db = openDatabase();
|
|
3197
|
+
fn(db);
|
|
3198
|
+
} catch (err) {
|
|
3199
|
+
console.error("Error:", err.message);
|
|
3200
|
+
process.exit(1);
|
|
3201
|
+
} finally {
|
|
3202
|
+
db?.close();
|
|
3203
|
+
}
|
|
3204
|
+
}
|
|
3205
|
+
var settingsCommand = new Command11("settings").description("Manage user settings");
|
|
3206
|
+
settingsCommand.command("show").description("Show all settings").option("--json", "Output as JSON").action((opts) => {
|
|
3207
|
+
withDb7((db) => {
|
|
3208
|
+
if (opts.json) {
|
|
3209
|
+
console.log(JSON.stringify(getAllSettings(db), null, 2));
|
|
3210
|
+
return;
|
|
3211
|
+
}
|
|
3212
|
+
const settings = getAllSettingsDetailed(db);
|
|
3213
|
+
if (settings.length === 0) {
|
|
3214
|
+
console.log("No settings configured.");
|
|
3215
|
+
return;
|
|
3216
|
+
}
|
|
3217
|
+
console.log("Settings:\n");
|
|
3218
|
+
console.log("Key Value Updated");
|
|
3219
|
+
console.log("\u2500".repeat(65));
|
|
3220
|
+
for (const s of settings) {
|
|
3221
|
+
console.log(
|
|
3222
|
+
`${s.key.padEnd(20)} ${s.value.padEnd(20)} ${s.updated_at}`
|
|
3223
|
+
);
|
|
3224
|
+
}
|
|
3225
|
+
});
|
|
3226
|
+
});
|
|
3227
|
+
settingsCommand.command("get").description("Get a single setting").requiredOption("--key <key>", "Setting key").option("--json", "Output as JSON").action((opts) => {
|
|
3228
|
+
withDb7((db) => {
|
|
3229
|
+
const value = getSetting(db, opts.key);
|
|
3230
|
+
if (opts.json) {
|
|
3231
|
+
console.log(JSON.stringify({ key: opts.key, value: value ?? null }));
|
|
3232
|
+
return;
|
|
3233
|
+
}
|
|
3234
|
+
if (value === void 0) {
|
|
3235
|
+
console.log(`Not set: ${opts.key}`);
|
|
3236
|
+
} else {
|
|
3237
|
+
console.log(value);
|
|
3238
|
+
}
|
|
3239
|
+
});
|
|
3240
|
+
});
|
|
3241
|
+
settingsCommand.command("set").description("Set a setting").requiredOption("--key <key>", "Setting key").requiredOption("--value <value>", "Setting value").option("--quiet", "Suppress output").action((opts) => {
|
|
3242
|
+
withDb7((db) => {
|
|
3243
|
+
setSetting(db, opts.key, opts.value);
|
|
3244
|
+
if (!opts.quiet) {
|
|
3245
|
+
console.log(`Set ${opts.key} = ${opts.value}`);
|
|
3246
|
+
}
|
|
3247
|
+
});
|
|
3248
|
+
});
|
|
3249
|
+
settingsCommand.command("delete").description("Delete a setting").requiredOption("--key <key>", "Setting key").option("--quiet", "Suppress output").action((opts) => {
|
|
3250
|
+
withDb7((db) => {
|
|
3251
|
+
const deleted = deleteSetting(db, opts.key);
|
|
3252
|
+
if (!opts.quiet) {
|
|
3253
|
+
if (deleted) {
|
|
3254
|
+
console.log(`Deleted: ${opts.key}`);
|
|
3255
|
+
} else {
|
|
3256
|
+
console.log(`Not found: ${opts.key}`);
|
|
3257
|
+
}
|
|
3258
|
+
}
|
|
3259
|
+
});
|
|
3260
|
+
});
|
|
3261
|
+
|
|
3262
|
+
// src/cli/commands/whoami.ts
|
|
3263
|
+
import { Command as Command12 } from "commander";
|
|
3264
|
+
function withDb8(fn) {
|
|
3265
|
+
let db;
|
|
3266
|
+
try {
|
|
3267
|
+
db = openDatabase();
|
|
3268
|
+
fn(db);
|
|
3269
|
+
} catch (err) {
|
|
3270
|
+
console.error("Error:", err.message);
|
|
3271
|
+
process.exit(1);
|
|
3272
|
+
} finally {
|
|
3273
|
+
db?.close();
|
|
3274
|
+
}
|
|
3275
|
+
}
|
|
3276
|
+
var whoamiCommand = new Command12("whoami").description("Show or set the default user identity").option("--set <id>", "Set the default user ID").option("--clear", "Remove the default user ID").option("--json", "Output as JSON").action((opts) => {
|
|
3277
|
+
withDb8((db) => {
|
|
3278
|
+
if (opts.set) {
|
|
3279
|
+
setSetting(db, "user.id", opts.set);
|
|
3280
|
+
if (opts.json) {
|
|
3281
|
+
console.log(JSON.stringify({ userId: opts.set }));
|
|
3282
|
+
} else {
|
|
3283
|
+
console.log(`Default user set to: ${opts.set}`);
|
|
3284
|
+
}
|
|
3285
|
+
return;
|
|
3286
|
+
}
|
|
3287
|
+
if (opts.clear) {
|
|
3288
|
+
const deleted = deleteSetting(db, "user.id");
|
|
3289
|
+
if (opts.json) {
|
|
3290
|
+
console.log(JSON.stringify({ userId: null, cleared: deleted }));
|
|
3291
|
+
} else if (deleted) {
|
|
3292
|
+
console.log("Default user cleared.");
|
|
3293
|
+
} else {
|
|
3294
|
+
console.log("No default user was set.");
|
|
3295
|
+
}
|
|
3296
|
+
return;
|
|
3297
|
+
}
|
|
3298
|
+
const userId = getSetting(db, "user.id");
|
|
3299
|
+
if (opts.json) {
|
|
3300
|
+
console.log(JSON.stringify({ userId: userId ?? null }));
|
|
3301
|
+
return;
|
|
3302
|
+
}
|
|
3303
|
+
if (userId) {
|
|
3304
|
+
console.log(userId);
|
|
3305
|
+
} else {
|
|
3306
|
+
console.log("No default user set. Use: zam whoami --set <id>");
|
|
3307
|
+
}
|
|
3308
|
+
});
|
|
3309
|
+
});
|
|
3310
|
+
|
|
3311
|
+
// src/cli/commands/connector.ts
|
|
3312
|
+
import { Command as Command13 } from "commander";
|
|
3313
|
+
import { input as input3, password } from "@inquirer/prompts";
|
|
3314
|
+
function withDb9(fn) {
|
|
3315
|
+
let db;
|
|
3316
|
+
try {
|
|
3317
|
+
db = openDatabase();
|
|
3318
|
+
fn(db);
|
|
3319
|
+
} catch (err) {
|
|
3320
|
+
console.error("Error:", err.message);
|
|
3321
|
+
process.exit(1);
|
|
3322
|
+
} finally {
|
|
3323
|
+
db?.close();
|
|
3324
|
+
}
|
|
3325
|
+
}
|
|
3326
|
+
var connectorCommand = new Command13("connector").description("Manage external service connectors");
|
|
3327
|
+
connectorCommand.command("setup").description("Configure a connector").argument("<type>", "Connector type (ado, turso)").action(async (type) => {
|
|
3328
|
+
if (type === "turso") {
|
|
3329
|
+
return setupTurso();
|
|
3330
|
+
}
|
|
3331
|
+
if (type !== "ado") {
|
|
3332
|
+
console.error(`Unknown connector type: ${type}. Supported: ado, turso`);
|
|
3333
|
+
process.exit(1);
|
|
3334
|
+
}
|
|
3335
|
+
let db;
|
|
3336
|
+
try {
|
|
3337
|
+
const orgUrl = await input3({
|
|
3338
|
+
message: "Organization URL (e.g. https://dev.azure.com/myorg):"
|
|
3339
|
+
});
|
|
3340
|
+
const project = await input3({
|
|
3341
|
+
message: "Project name:"
|
|
3342
|
+
});
|
|
3343
|
+
const pat = await password({
|
|
3344
|
+
message: "Personal Access Token:"
|
|
3345
|
+
});
|
|
3346
|
+
if (!orgUrl || !project || !pat) {
|
|
3347
|
+
console.error("All fields are required.");
|
|
3348
|
+
process.exit(1);
|
|
3349
|
+
}
|
|
3350
|
+
db = openDatabase();
|
|
3351
|
+
setSetting(db, "ado.org_url", orgUrl.replace(/\/+$/, ""));
|
|
3352
|
+
setSetting(db, "ado.project", project);
|
|
3353
|
+
setSetting(db, "ado.pat", pat);
|
|
3354
|
+
db.close();
|
|
3355
|
+
console.log(`Azure DevOps connector configured for ${orgUrl}/${project}`);
|
|
3356
|
+
} catch (err) {
|
|
3357
|
+
db?.close();
|
|
3358
|
+
if (err.name === "ExitPromptError") {
|
|
3359
|
+
console.log("\nSetup cancelled.");
|
|
3360
|
+
process.exit(0);
|
|
3361
|
+
}
|
|
3362
|
+
console.error("Error:", err.message);
|
|
3363
|
+
process.exit(1);
|
|
3364
|
+
}
|
|
3365
|
+
});
|
|
3366
|
+
connectorCommand.command("tasks").description("List active tasks from connected board").option("--json", "Output as JSON").action(async (opts) => {
|
|
3367
|
+
let db;
|
|
3368
|
+
try {
|
|
3369
|
+
db = openDatabase();
|
|
3370
|
+
const config = loadADOConfig(db);
|
|
3371
|
+
db.close();
|
|
3372
|
+
if (!config) {
|
|
3373
|
+
console.error("No connector configured. Run: zam connector setup ado");
|
|
3374
|
+
process.exit(1);
|
|
3375
|
+
}
|
|
3376
|
+
const items = await fetchActiveWorkItems(config);
|
|
3377
|
+
if (opts.json) {
|
|
3378
|
+
console.log(JSON.stringify(items, null, 2));
|
|
3379
|
+
return;
|
|
3380
|
+
}
|
|
3381
|
+
if (items.length === 0) {
|
|
3382
|
+
console.log("No active work items assigned to you.");
|
|
3383
|
+
return;
|
|
3384
|
+
}
|
|
3385
|
+
console.log(`${items.length} active work item(s):
|
|
3386
|
+
`);
|
|
3387
|
+
console.log(
|
|
3388
|
+
"ID Type State Title"
|
|
3389
|
+
);
|
|
3390
|
+
console.log("\u2500".repeat(80));
|
|
3391
|
+
for (const wi of items) {
|
|
3392
|
+
console.log(
|
|
3393
|
+
`${String(wi.id).padEnd(8)} ${wi.type.padEnd(13)} ${wi.state.padEnd(11)} ${wi.title.slice(0, 45)}`
|
|
3394
|
+
);
|
|
3395
|
+
}
|
|
3396
|
+
} catch (err) {
|
|
3397
|
+
db?.close();
|
|
3398
|
+
console.error("Error:", err.message);
|
|
3399
|
+
process.exit(1);
|
|
3400
|
+
}
|
|
3401
|
+
});
|
|
3402
|
+
connectorCommand.command("clear").description("Remove a connector configuration").argument("<type>", "Connector type (ado, turso)").action((type) => {
|
|
3403
|
+
if (type === "turso") {
|
|
3404
|
+
withDb9((db) => {
|
|
3405
|
+
deleteSetting(db, "turso.url");
|
|
3406
|
+
deleteSetting(db, "turso.token");
|
|
3407
|
+
console.log("Turso cloud sync removed. Database remains local-only.");
|
|
3408
|
+
});
|
|
3409
|
+
return;
|
|
3410
|
+
}
|
|
3411
|
+
if (type !== "ado") {
|
|
3412
|
+
console.error(`Unknown connector type: ${type}. Supported: ado, turso`);
|
|
3413
|
+
process.exit(1);
|
|
3414
|
+
}
|
|
3415
|
+
withDb9((db) => {
|
|
3416
|
+
deleteSetting(db, "ado.org_url");
|
|
3417
|
+
deleteSetting(db, "ado.project");
|
|
3418
|
+
deleteSetting(db, "ado.pat");
|
|
3419
|
+
console.log("Azure DevOps connector removed.");
|
|
3420
|
+
});
|
|
3421
|
+
});
|
|
3422
|
+
connectorCommand.command("sync").description("Trigger a manual sync with Turso cloud database").action(() => {
|
|
3423
|
+
let db;
|
|
3424
|
+
try {
|
|
3425
|
+
db = openDatabaseWithSync();
|
|
3426
|
+
const url = getSetting(db, "turso.url");
|
|
3427
|
+
if (!url) {
|
|
3428
|
+
console.error("No Turso cloud database configured. Run: zam connector setup turso");
|
|
3429
|
+
process.exit(1);
|
|
3430
|
+
}
|
|
3431
|
+
db.sync();
|
|
3432
|
+
console.log(`Synced with ${url}`);
|
|
3433
|
+
db.close();
|
|
3434
|
+
} catch (err) {
|
|
3435
|
+
db?.close();
|
|
3436
|
+
console.error("Error:", err.message);
|
|
3437
|
+
process.exit(1);
|
|
3438
|
+
}
|
|
3439
|
+
});
|
|
3440
|
+
async function setupTurso() {
|
|
3441
|
+
let db;
|
|
3442
|
+
try {
|
|
3443
|
+
const url = await input3({
|
|
3444
|
+
message: "Turso database URL (e.g. libsql://my-db-user.turso.io):"
|
|
3445
|
+
});
|
|
3446
|
+
const token = await password({
|
|
3447
|
+
message: "Auth token:"
|
|
3448
|
+
});
|
|
3449
|
+
if (!url || !token) {
|
|
3450
|
+
console.error("Both URL and token are required.");
|
|
3451
|
+
process.exit(1);
|
|
3452
|
+
}
|
|
3453
|
+
db = openDatabase();
|
|
3454
|
+
setSetting(db, "turso.url", url);
|
|
3455
|
+
setSetting(db, "turso.token", token);
|
|
3456
|
+
db.close();
|
|
3457
|
+
db = openDatabaseWithSync();
|
|
3458
|
+
db.sync();
|
|
3459
|
+
db.close();
|
|
3460
|
+
console.log(`Turso cloud sync configured and verified: ${url}`);
|
|
3461
|
+
} catch (err) {
|
|
3462
|
+
db?.close();
|
|
3463
|
+
if (err.name === "ExitPromptError") {
|
|
3464
|
+
console.log("\nSetup cancelled.");
|
|
3465
|
+
process.exit(0);
|
|
3466
|
+
}
|
|
3467
|
+
console.error("Error:", err.message);
|
|
3468
|
+
process.exit(1);
|
|
3469
|
+
}
|
|
3470
|
+
}
|
|
3471
|
+
|
|
3472
|
+
// src/cli/commands/goal.ts
|
|
3473
|
+
import { Command as Command14 } from "commander";
|
|
3474
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync4 } from "fs";
|
|
3475
|
+
import { resolve } from "path";
|
|
3476
|
+
import { input as input4 } from "@inquirer/prompts";
|
|
3477
|
+
function resolveGoalsDir() {
|
|
3478
|
+
let goalsDir;
|
|
3479
|
+
let db;
|
|
3480
|
+
try {
|
|
3481
|
+
db = openDatabase();
|
|
3482
|
+
goalsDir = getSetting(db, "personal.goals_dir");
|
|
3483
|
+
} catch {
|
|
3484
|
+
} finally {
|
|
3485
|
+
db?.close();
|
|
3486
|
+
}
|
|
3487
|
+
return goalsDir ? resolve(goalsDir) : resolve("goals");
|
|
3488
|
+
}
|
|
3489
|
+
var goalCommand = new Command14("goal").description("Manage learning goals (markdown files)");
|
|
3490
|
+
goalCommand.command("list").description("List all goals").option("--status <status>", "Filter by status (active, completed, paused, abandoned)").option("--tree", "Show goals as a tree with parent/child relationships").option("--json", "Output as JSON").action((opts) => {
|
|
3491
|
+
const goalsDir = resolveGoalsDir();
|
|
3492
|
+
if (!existsSync5(goalsDir)) {
|
|
3493
|
+
console.error(`Goals directory not found: ${goalsDir}`);
|
|
3494
|
+
console.error("Set it with: zam settings set personal.goals_dir /path/to/goals");
|
|
3495
|
+
process.exit(1);
|
|
3496
|
+
}
|
|
3497
|
+
if (opts.tree) {
|
|
3498
|
+
const tree = getGoalTree(goalsDir);
|
|
3499
|
+
const filtered = opts.status ? tree.filter((g) => g.status === opts.status) : tree;
|
|
3500
|
+
if (opts.json) {
|
|
3501
|
+
console.log(JSON.stringify(filtered, null, 2));
|
|
3502
|
+
return;
|
|
3503
|
+
}
|
|
3504
|
+
if (filtered.length === 0) {
|
|
3505
|
+
console.log("No goals found.");
|
|
3506
|
+
return;
|
|
3507
|
+
}
|
|
3508
|
+
for (const root of filtered) {
|
|
3509
|
+
printGoalLine(root, 0);
|
|
3510
|
+
for (const child of root.children) {
|
|
3511
|
+
printGoalLine(child, 1);
|
|
3512
|
+
}
|
|
3513
|
+
}
|
|
3514
|
+
return;
|
|
3515
|
+
}
|
|
3516
|
+
let goals = listGoals(goalsDir);
|
|
3517
|
+
if (opts.status) {
|
|
3518
|
+
goals = goals.filter((g) => g.status === opts.status);
|
|
3519
|
+
}
|
|
3520
|
+
if (opts.json) {
|
|
3521
|
+
console.log(JSON.stringify(goals, null, 2));
|
|
3522
|
+
return;
|
|
3523
|
+
}
|
|
3524
|
+
if (goals.length === 0) {
|
|
3525
|
+
console.log("No goals found.");
|
|
3526
|
+
return;
|
|
3527
|
+
}
|
|
3528
|
+
console.log("Goals:");
|
|
3529
|
+
console.log(" " + "\u2500".repeat(70));
|
|
3530
|
+
for (const g of goals) {
|
|
3531
|
+
printGoalLine(g, 0);
|
|
3532
|
+
}
|
|
3533
|
+
});
|
|
3534
|
+
function printGoalLine(g, indent) {
|
|
3535
|
+
const prefix = " ".repeat(indent + 1);
|
|
3536
|
+
const statusIcon = {
|
|
3537
|
+
active: "[*]",
|
|
3538
|
+
paused: "[-]",
|
|
3539
|
+
completed: "[x]",
|
|
3540
|
+
abandoned: "[ ]"
|
|
3541
|
+
};
|
|
3542
|
+
const icon = statusIcon[g.status] || "[ ]";
|
|
3543
|
+
const tasks = g.taskCount > 0 ? ` (${g.tasksDone}/${g.taskCount} tasks)` : "";
|
|
3544
|
+
console.log(`${prefix}${icon} ${g.title}${tasks} \u2014 ${g.slug}`);
|
|
3545
|
+
}
|
|
3546
|
+
goalCommand.command("show <slug>").description("Show a goal's details").option("--json", "Output as JSON").action((slug, opts) => {
|
|
3547
|
+
const goalsDir = resolveGoalsDir();
|
|
3548
|
+
const goal = getGoal(goalsDir, slug);
|
|
3549
|
+
if (!goal) {
|
|
3550
|
+
console.error(`Goal not found: ${slug}`);
|
|
3551
|
+
process.exit(1);
|
|
3552
|
+
}
|
|
3553
|
+
if (opts.json) {
|
|
3554
|
+
const tasks2 = extractTasks(goal.body);
|
|
3555
|
+
const tokens2 = extractTokenRefs(goal.body);
|
|
3556
|
+
console.log(JSON.stringify({ ...goal, tasks: tasks2, tokens: tokens2 }, null, 2));
|
|
3557
|
+
return;
|
|
3558
|
+
}
|
|
3559
|
+
console.log(`Title: ${goal.title}`);
|
|
3560
|
+
console.log(`Slug: ${goal.slug}`);
|
|
3561
|
+
console.log(`Status: ${goal.status}`);
|
|
3562
|
+
if (goal.parent) console.log(`Parent: ${goal.parent}`);
|
|
3563
|
+
console.log(`Created: ${goal.created}`);
|
|
3564
|
+
console.log(`Updated: ${goal.updated}`);
|
|
3565
|
+
const tasks = extractTasks(goal.body);
|
|
3566
|
+
if (tasks.length > 0) {
|
|
3567
|
+
console.log(`
|
|
3568
|
+
Tasks (${tasks.filter((t) => t.done).length}/${tasks.length}):`);
|
|
3569
|
+
for (const t of tasks) {
|
|
3570
|
+
console.log(` [${t.done ? "x" : " "}] ${t.text}`);
|
|
3571
|
+
}
|
|
3572
|
+
}
|
|
3573
|
+
const tokens = extractTokenRefs(goal.body);
|
|
3574
|
+
if (tokens.length > 0) {
|
|
3575
|
+
console.log(`
|
|
3576
|
+
Tokens:`);
|
|
3577
|
+
for (const ref of tokens) {
|
|
3578
|
+
console.log(` - ${ref}`);
|
|
3579
|
+
}
|
|
3580
|
+
}
|
|
3581
|
+
if (goal.body) {
|
|
3582
|
+
console.log(`
|
|
3583
|
+
${"\u2500".repeat(50)}`);
|
|
3584
|
+
console.log(goal.body);
|
|
3585
|
+
}
|
|
3586
|
+
});
|
|
3587
|
+
goalCommand.command("create").description("Create a new goal").option("--slug <slug>", "Goal slug (used as filename)").option("--title <title>", "Goal title").option("--parent <slug>", "Parent goal slug").option("--description <text>", "Goal description").option("--json", "Output as JSON").action(async (opts) => {
|
|
3588
|
+
const goalsDir = resolveGoalsDir();
|
|
3589
|
+
if (!existsSync5(goalsDir)) {
|
|
3590
|
+
mkdirSync4(goalsDir, { recursive: true });
|
|
3591
|
+
}
|
|
3592
|
+
let slug = opts.slug;
|
|
3593
|
+
let title = opts.title;
|
|
3594
|
+
const parent = opts.parent;
|
|
3595
|
+
const description = opts.description;
|
|
3596
|
+
if (!slug || !title) {
|
|
3597
|
+
try {
|
|
3598
|
+
if (!title) {
|
|
3599
|
+
title = await input4({ message: "Goal title:" });
|
|
3600
|
+
}
|
|
3601
|
+
if (!slug) {
|
|
3602
|
+
const suggested = title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
3603
|
+
slug = await input4({
|
|
3604
|
+
message: "Goal slug (filename):",
|
|
3605
|
+
default: suggested
|
|
3606
|
+
});
|
|
3607
|
+
}
|
|
3608
|
+
} catch (err) {
|
|
3609
|
+
if (err.name === "ExitPromptError") {
|
|
3610
|
+
console.log("\nCancelled.");
|
|
3611
|
+
process.exit(0);
|
|
3612
|
+
}
|
|
3613
|
+
throw err;
|
|
3614
|
+
}
|
|
3615
|
+
}
|
|
3616
|
+
const goal = createGoal(goalsDir, { slug, title, parent, description });
|
|
3617
|
+
if (opts.json) {
|
|
3618
|
+
console.log(JSON.stringify(goal, null, 2));
|
|
3619
|
+
return;
|
|
3620
|
+
}
|
|
3621
|
+
console.log(`Goal created: ${goal.slug}`);
|
|
3622
|
+
console.log(` Title: ${goal.title}`);
|
|
3623
|
+
console.log(` Status: ${goal.status}`);
|
|
3624
|
+
console.log(` File: ${goal.filePath}`);
|
|
3625
|
+
});
|
|
3626
|
+
goalCommand.command("status <slug> <status>").description("Update a goal's status (active, paused, completed, abandoned)").option("--json", "Output as JSON").action((slug, status, opts) => {
|
|
3627
|
+
const validStatuses = ["active", "completed", "paused", "abandoned"];
|
|
3628
|
+
if (!validStatuses.includes(status)) {
|
|
3629
|
+
console.error(`Invalid status: ${status}. Must be one of: ${validStatuses.join(", ")}`);
|
|
3630
|
+
process.exit(1);
|
|
3631
|
+
}
|
|
3632
|
+
const goalsDir = resolveGoalsDir();
|
|
3633
|
+
const goal = updateGoalStatus(goalsDir, slug, status);
|
|
3634
|
+
if (opts.json) {
|
|
3635
|
+
console.log(JSON.stringify(goal, null, 2));
|
|
3636
|
+
return;
|
|
3637
|
+
}
|
|
3638
|
+
console.log(`Goal ${slug} updated to: ${status}`);
|
|
3639
|
+
});
|
|
3640
|
+
|
|
3641
|
+
// src/cli/index.ts
|
|
3642
|
+
var program = new Command15();
|
|
3643
|
+
program.name("zam").description(
|
|
3644
|
+
"The Symbiotic Learning Kernel: Elevating Human Intelligence through AI Collaboration."
|
|
3645
|
+
).version("0.3.0");
|
|
3646
|
+
program.addCommand(initCommand);
|
|
3647
|
+
program.addCommand(setupCommand);
|
|
3648
|
+
program.addCommand(tokenCommand);
|
|
3649
|
+
program.addCommand(cardCommand);
|
|
3650
|
+
program.addCommand(sessionCommand);
|
|
3651
|
+
program.addCommand(statsCommand);
|
|
3652
|
+
program.addCommand(reviewCommand);
|
|
3653
|
+
program.addCommand(bridgeCommand);
|
|
3654
|
+
program.addCommand(skillCommand);
|
|
3655
|
+
program.addCommand(monitorCommand);
|
|
3656
|
+
program.addCommand(settingsCommand);
|
|
3657
|
+
program.addCommand(whoamiCommand);
|
|
3658
|
+
program.addCommand(connectorCommand);
|
|
3659
|
+
program.addCommand(goalCommand);
|
|
3660
|
+
program.parse();
|
|
3661
|
+
//# sourceMappingURL=index.js.map
|