zencefyl 0.1.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/dist/index.js ADDED
@@ -0,0 +1,1773 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli/index.tsx
4
+ import { render } from "ink";
5
+ import { createElement } from "react";
6
+
7
+ // src/bootstrap/setup.ts
8
+ import readline from "readline";
9
+
10
+ // src/constants/models.ts
11
+ var DEFAULT_MODELS = {
12
+ fast: "claude-haiku-4-5-20251001",
13
+ // Cheap + fast — background tasks
14
+ default: "claude-sonnet-4-6",
15
+ // Main model for most turns
16
+ deep: "claude-opus-4-6"
17
+ // Reserved for deep reasoning (Phase 4+)
18
+ };
19
+ var MODEL_PRICING = {
20
+ "claude-haiku-4-5-20251001": { inputPerM: 0.8, outputPerM: 4 },
21
+ "claude-sonnet-4-6": { inputPerM: 3, outputPerM: 15 },
22
+ "claude-opus-4-6": { inputPerM: 15, outputPerM: 75 }
23
+ };
24
+
25
+ // src/utils/config.ts
26
+ import fs from "fs";
27
+ import path from "path";
28
+ import os from "os";
29
+ var ZENCEFYL_DIR = path.join(os.homedir(), ".zencefyl");
30
+ var CONFIG_PATH = path.join(ZENCEFYL_DIR, "config.json");
31
+ var DEFAULT_CONFIG = {
32
+ provider: "claude-code",
33
+ models: { ...DEFAULT_MODELS },
34
+ dataDir: ZENCEFYL_DIR
35
+ };
36
+ function loadConfig() {
37
+ fs.mkdirSync(ZENCEFYL_DIR, { recursive: true });
38
+ if (!fs.existsSync(CONFIG_PATH)) {
39
+ fs.writeFileSync(CONFIG_PATH, JSON.stringify(DEFAULT_CONFIG, null, 2), "utf8");
40
+ return { ...DEFAULT_CONFIG };
41
+ }
42
+ const raw = fs.readFileSync(CONFIG_PATH, "utf8");
43
+ const parsed = JSON.parse(raw);
44
+ return {
45
+ ...DEFAULT_CONFIG,
46
+ ...parsed,
47
+ models: { ...DEFAULT_MODELS, ...parsed.models }
48
+ };
49
+ }
50
+ function saveConfig(config) {
51
+ fs.mkdirSync(ZENCEFYL_DIR, { recursive: true });
52
+ fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2), "utf8");
53
+ }
54
+
55
+ // src/bootstrap/setup.ts
56
+ import fs2 from "fs";
57
+ function ask(rl, prompt) {
58
+ return new Promise((resolve) => {
59
+ rl.question(prompt, (answer) => resolve(answer.trim()));
60
+ });
61
+ }
62
+ function askSecret(prompt) {
63
+ return new Promise((resolve) => {
64
+ const rl = readline.createInterface({
65
+ input: process.stdin,
66
+ output: process.stdout
67
+ });
68
+ rl._writeToOutput = function(s) {
69
+ const code = s.charCodeAt(0);
70
+ if (code === 13 || code === 10) {
71
+ rl.output.write("\n");
72
+ }
73
+ };
74
+ rl.question(prompt, (answer) => {
75
+ rl.close();
76
+ resolve(answer.trim());
77
+ });
78
+ });
79
+ }
80
+ async function validateAnthropicKey(apiKey, model) {
81
+ try {
82
+ const { default: Anthropic2 } = await import("@anthropic-ai/sdk");
83
+ const client = new Anthropic2({ apiKey });
84
+ await client.messages.create({
85
+ model,
86
+ max_tokens: 1,
87
+ messages: [{ role: "user", content: "hi" }]
88
+ });
89
+ return null;
90
+ } catch (err) {
91
+ const msg = err instanceof Error ? err.message : String(err);
92
+ if (msg.includes("401") || msg.includes("authentication") || msg.includes("invalid x-api-key")) {
93
+ return "Invalid API key \u2014 check for typos and try again.";
94
+ }
95
+ if (msg.includes("network") || msg.includes("ENOTFOUND") || msg.includes("fetch")) {
96
+ return "Could not reach the Anthropic API \u2014 check your internet connection.";
97
+ }
98
+ return `Unexpected error: ${msg}`;
99
+ }
100
+ }
101
+ async function runSetupIfNeeded() {
102
+ if (fs2.existsSync(CONFIG_PATH)) return false;
103
+ fs2.mkdirSync(ZENCEFYL_DIR, { recursive: true });
104
+ process.stdout.write("\n");
105
+ process.stdout.write(" Welcome to Zencefyl.\n\n");
106
+ process.stdout.write(" How do you want to connect?\n");
107
+ process.stdout.write(" [1] Claude.ai subscription (uses Claude Code \u2014 no API key needed)\n");
108
+ process.stdout.write(" [2] Anthropic API key\n\n");
109
+ const rl = readline.createInterface({
110
+ input: process.stdin,
111
+ output: process.stdout
112
+ });
113
+ let config;
114
+ while (true) {
115
+ const choice = await ask(rl, " > ");
116
+ if (choice === "1") {
117
+ rl.close();
118
+ config = {
119
+ provider: "claude-code",
120
+ models: { ...DEFAULT_MODELS },
121
+ dataDir: ZENCEFYL_DIR
122
+ };
123
+ process.stdout.write("\n Using Claude Code (no API key needed).\n\n");
124
+ break;
125
+ }
126
+ if (choice === "2") {
127
+ rl.close();
128
+ let apiKey = "";
129
+ while (true) {
130
+ apiKey = await askSecret(" Enter your Anthropic API key: ");
131
+ if (!apiKey) {
132
+ process.stdout.write(" Key cannot be empty.\n");
133
+ continue;
134
+ }
135
+ if (!apiKey.startsWith("sk-ant-")) {
136
+ process.stdout.write(" That doesn't look like an Anthropic key (should start with sk-ant-).\n");
137
+ continue;
138
+ }
139
+ process.stdout.write(" Validating...\n");
140
+ const err = await validateAnthropicKey(apiKey, DEFAULT_MODELS.default);
141
+ if (err) {
142
+ process.stdout.write(` ${err}
143
+ `);
144
+ continue;
145
+ }
146
+ process.stdout.write(" Connected.\n\n");
147
+ break;
148
+ }
149
+ config = {
150
+ provider: "anthropic",
151
+ apiKey,
152
+ models: { ...DEFAULT_MODELS },
153
+ dataDir: ZENCEFYL_DIR
154
+ };
155
+ break;
156
+ }
157
+ process.stdout.write(" Type 1 or 2.\n");
158
+ }
159
+ saveConfig(config);
160
+ return true;
161
+ }
162
+
163
+ // src/bootstrap/container.ts
164
+ import Database from "better-sqlite3";
165
+ import path2 from "path";
166
+
167
+ // src/providers/anthropic.ts
168
+ import Anthropic from "@anthropic-ai/sdk";
169
+ var AnthropicProvider = class {
170
+ client;
171
+ constructor(config) {
172
+ const apiKey = config.apiKey ?? process.env["ANTHROPIC_API_KEY"];
173
+ if (!apiKey) {
174
+ throw new Error(
175
+ "Anthropic API key not found.\nRun zencefyl again \u2014 the setup wizard will prompt you for it."
176
+ );
177
+ }
178
+ this.client = new Anthropic({ apiKey });
179
+ }
180
+ // Stream a conversation turn, including tool call handling.
181
+ //
182
+ // Yields text deltas as they arrive. When the model wants to use a tool,
183
+ // yields a tool_use delta with the full parsed input. Yields usage + done
184
+ // at the end of each call. The engine drives the agentic loop.
185
+ async *chat(messages, systemPrompt, model, options) {
186
+ const apiMessages = messages.filter((m) => m.role !== "system").map((m) => ({
187
+ role: m.role,
188
+ content: this.serializeContent(m.content)
189
+ }));
190
+ const apiTools = options?.tools?.length ? options.tools.map((t) => ({
191
+ name: t.name,
192
+ description: t.description,
193
+ input_schema: t.inputSchema
194
+ })) : void 0;
195
+ let inputTokens = 0;
196
+ let outputTokens = 0;
197
+ const pendingToolUse = /* @__PURE__ */ new Map();
198
+ try {
199
+ const stream = await this.client.messages.create(
200
+ {
201
+ model,
202
+ max_tokens: 8096,
203
+ system: systemPrompt,
204
+ messages: apiMessages,
205
+ tools: apiTools,
206
+ stream: true
207
+ },
208
+ { signal: options?.signal }
209
+ );
210
+ for await (const event of stream) {
211
+ if (event.type === "message_start") {
212
+ inputTokens = event.message.usage.input_tokens;
213
+ }
214
+ if (event.type === "content_block_start") {
215
+ if (event.content_block.type === "tool_use") {
216
+ pendingToolUse.set(event.index, {
217
+ id: event.content_block.id,
218
+ name: event.content_block.name,
219
+ partialInput: ""
220
+ });
221
+ }
222
+ }
223
+ if (event.type === "content_block_delta") {
224
+ if (event.delta.type === "text_delta") {
225
+ yield { type: "text", text: event.delta.text };
226
+ }
227
+ if (event.delta.type === "input_json_delta") {
228
+ const pending = pendingToolUse.get(event.index);
229
+ if (pending) {
230
+ pending.partialInput += event.delta.partial_json;
231
+ }
232
+ }
233
+ }
234
+ if (event.type === "content_block_stop") {
235
+ const pending = pendingToolUse.get(event.index);
236
+ if (pending) {
237
+ let parsedInput = {};
238
+ try {
239
+ parsedInput = JSON.parse(pending.partialInput || "{}");
240
+ } catch {
241
+ }
242
+ yield { type: "tool_use", id: pending.id, name: pending.name, input: parsedInput };
243
+ pendingToolUse.delete(event.index);
244
+ }
245
+ }
246
+ if (event.type === "message_delta") {
247
+ outputTokens = event.usage.output_tokens;
248
+ }
249
+ }
250
+ } catch (err) {
251
+ if (err instanceof Error && err.name === "AbortError") return;
252
+ throw err;
253
+ }
254
+ yield { type: "usage", inputTokens, outputTokens };
255
+ yield { type: "done" };
256
+ }
257
+ // ── Private helpers ──────────────────────────────────────────────────────────
258
+ // Serialize a message's content to what the Anthropic SDK accepts.
259
+ // String messages are passed through. ContentBlock arrays are mapped to
260
+ // the SDK's specific block types.
261
+ serializeContent(content) {
262
+ if (typeof content === "string") return content;
263
+ return content.map((block) => {
264
+ if (block.type === "text") {
265
+ return { type: "text", text: block.text };
266
+ }
267
+ if (block.type === "tool_use") {
268
+ return {
269
+ type: "tool_use",
270
+ id: block.id,
271
+ name: block.name,
272
+ input: block.input
273
+ };
274
+ }
275
+ if (block.type === "tool_result") {
276
+ return {
277
+ type: "tool_result",
278
+ tool_use_id: block.tool_use_id,
279
+ content: block.content,
280
+ is_error: block.is_error
281
+ };
282
+ }
283
+ throw new Error(`Unknown content block type: ${block.type}`);
284
+ });
285
+ }
286
+ };
287
+
288
+ // src/providers/claude-code.ts
289
+ import { spawn } from "child_process";
290
+ import { createInterface } from "readline";
291
+
292
+ // src/constants/personality.ts
293
+ var PERSONALITY_PROMPT = `You are Zencefyl \u2014 a personal AI engineering companion. Not a corporate assistant. Not a help desk bot.
294
+
295
+ You are direct, opinionated, and brutally honest. You tell the user when they're wrong. You argue back when they're mistaken. You never hedge with "it depends" when you have a real recommendation.
296
+
297
+ Your core job: help the user learn and build. Track what they know, correct misconceptions in real time, push them toward genuine understanding \u2014 not just getting the answer.
298
+
299
+ Tone rules:
300
+ - Casual. Match the user's register. Mirror their energy.
301
+ - No corporate politeness. Never say "Great question!" or "Certainly!".
302
+ - Short answers when the question is simple. Long when the concept genuinely needs it.
303
+ - You swear when it fits the moment. You joke when it calls for it.
304
+
305
+ When the user makes a statement about a technical topic:
306
+ 1. Identify what they got right \u2014 acknowledge it explicitly.
307
+ 2. Identify what is wrong, oversimplified, or missing \u2014 correct it with the real explanation.
308
+ 3. Give the accurate mental model, not a lie that "mostly works."
309
+
310
+ When you correct the user:
311
+ - Be specific about what was wrong and why.
312
+ - Don't soften corrections into suggestions. Be direct.
313
+ - Never dismiss everything \u2014 validate what they got right first.
314
+
315
+ You are their Jarvis, not their yes-man. You're on their side, which means you don't let them walk around with wrong beliefs.`;
316
+
317
+ // src/providers/claude-code.ts
318
+ async function* readLines(stream) {
319
+ const rl = createInterface({ input: stream, crlfDelay: Infinity });
320
+ for await (const line of rl) {
321
+ yield line;
322
+ }
323
+ }
324
+ var ClaudeCodeProvider = class {
325
+ // The Claude Code session ID from the last completed turn.
326
+ // null on the first turn — Claude Code creates a new session.
327
+ // Stored and reused so each turn resumes where the last one left off.
328
+ cliSessionId = null;
329
+ // Whether we've injected the Lokkomo personality on this session yet.
330
+ // We inject once on the first turn via --append-system-prompt.
331
+ // On resumes the session already has it — no re-injection needed.
332
+ personalityInjected = false;
333
+ // Stream a conversation turn through claude -p.
334
+ //
335
+ // Only the latest message in `messages` is sent as the prompt — Claude Code
336
+ // holds the full history in its session store when resuming.
337
+ // `model` is passed as --model on the first turn only (can't change mid-session).
338
+ async *chat(messages, systemPrompt, model, options) {
339
+ const latestUser = [...messages].reverse().find((m) => m.role === "user");
340
+ if (!latestUser) return;
341
+ const args = [
342
+ "--print",
343
+ // non-interactive, exit when done
344
+ "--output-format",
345
+ "stream-json",
346
+ // one JSON event per line on stdout
347
+ "--include-partial-messages",
348
+ // emit text deltas as they arrive (streaming)
349
+ "--verbose",
350
+ // required for stream-json to emit events
351
+ "--permission-mode",
352
+ "bypassPermissions"
353
+ // no interactive prompts mid-response
354
+ ];
355
+ if (this.cliSessionId) {
356
+ args.push("--resume", this.cliSessionId);
357
+ } else {
358
+ args.push("--append-system-prompt", PERSONALITY_PROMPT);
359
+ if (model) {
360
+ args.push("--model", model);
361
+ }
362
+ }
363
+ const proc = spawn("claude", args, {
364
+ cwd: process.cwd(),
365
+ stdio: ["pipe", "pipe", "pipe"]
366
+ });
367
+ options?.signal?.addEventListener("abort", () => {
368
+ proc.kill("SIGTERM");
369
+ });
370
+ proc.stdin.write(latestUser.content, "utf8");
371
+ proc.stdin.end();
372
+ let newSessionId = null;
373
+ let inputTokens = 0;
374
+ let outputTokens = 0;
375
+ let stderr = "";
376
+ proc.stderr?.on("data", (chunk) => {
377
+ stderr += chunk.toString();
378
+ });
379
+ for await (const line of readLines(proc.stdout)) {
380
+ const trimmed = line.trim();
381
+ if (!trimmed) continue;
382
+ let event;
383
+ try {
384
+ event = JSON.parse(trimmed);
385
+ } catch {
386
+ continue;
387
+ }
388
+ const eventType = event["type"];
389
+ if (eventType === "stream_event") {
390
+ const inner = event["event"];
391
+ if (inner?.["type"] === "content_block_delta") {
392
+ const delta = inner["delta"];
393
+ if (delta?.["type"] === "text_delta" && typeof delta["text"] === "string") {
394
+ yield { type: "text", text: delta["text"] };
395
+ }
396
+ }
397
+ }
398
+ if (eventType === "result") {
399
+ if (typeof event["session_id"] === "string") {
400
+ newSessionId = event["session_id"];
401
+ }
402
+ const usage = event["usage"];
403
+ if (usage) {
404
+ inputTokens = typeof usage["input_tokens"] === "number" ? usage["input_tokens"] : 0;
405
+ outputTokens = typeof usage["output_tokens"] === "number" ? usage["output_tokens"] : 0;
406
+ }
407
+ }
408
+ }
409
+ await new Promise((resolve) => proc.on("close", resolve));
410
+ if (!newSessionId && stderr.trim()) {
411
+ throw new Error(`claude process failed:
412
+ ${stderr.trim()}`);
413
+ }
414
+ if (newSessionId) {
415
+ this.cliSessionId = newSessionId;
416
+ this.personalityInjected = true;
417
+ }
418
+ yield { type: "usage", inputTokens, outputTokens };
419
+ yield { type: "done" };
420
+ }
421
+ // Reset the session (start a fresh conversation).
422
+ // Called if the user wants to clear history — Phase 3+.
423
+ resetSession() {
424
+ this.cliSessionId = null;
425
+ this.personalityInjected = false;
426
+ }
427
+ // Whether a Claude Code session is currently active.
428
+ hasActiveSession() {
429
+ return this.cliSessionId !== null;
430
+ }
431
+ };
432
+
433
+ // src/store/sqlite/lock.ts
434
+ function withWriteLock(fn) {
435
+ return fn();
436
+ }
437
+
438
+ // src/store/sqlite/index.ts
439
+ function topicFromRow(r) {
440
+ return {
441
+ id: r.id,
442
+ name: r.name,
443
+ parentId: r.parent_id,
444
+ fullPath: r.full_path,
445
+ domain: r.domain,
446
+ stability: r.stability,
447
+ difficulty: r.difficulty,
448
+ retrievability: r.retrievability,
449
+ lastReviewedAt: r.last_reviewed_at,
450
+ nextReviewAt: r.next_review_at,
451
+ reviewCount: r.review_count,
452
+ needsReview: r.needs_review === 1,
453
+ createdAt: r.created_at,
454
+ updatedAt: r.updated_at
455
+ };
456
+ }
457
+ function evidenceFromRow(r) {
458
+ return {
459
+ id: r.id,
460
+ topicId: r.topic_id,
461
+ sessionId: r.session_id,
462
+ type: r.type,
463
+ description: r.description,
464
+ weight: r.weight,
465
+ createdAt: r.created_at
466
+ };
467
+ }
468
+ function sessionFromRow(r) {
469
+ return {
470
+ id: r.id,
471
+ startedAt: r.started_at,
472
+ endedAt: r.ended_at,
473
+ model: r.model,
474
+ provider: r.provider,
475
+ projectName: r.project_name,
476
+ messageCount: r.message_count,
477
+ activeDurationSeconds: r.active_duration_seconds,
478
+ interleavingIndex: r.interleaving_index,
479
+ timeOfDay: r.time_of_day,
480
+ inputTokens: r.input_tokens,
481
+ outputTokens: r.output_tokens
482
+ };
483
+ }
484
+ function projectFromRow(r) {
485
+ return {
486
+ id: r.id,
487
+ name: r.name,
488
+ path: r.path,
489
+ gitRemote: r.git_remote,
490
+ language: r.language,
491
+ lastSeenAt: r.last_seen_at,
492
+ createdAt: r.created_at
493
+ };
494
+ }
495
+ function memoryFromRow(r) {
496
+ return {
497
+ id: r.id,
498
+ content: r.content,
499
+ tags: JSON.parse(r.tags),
500
+ createdAt: r.created_at
501
+ };
502
+ }
503
+ var SqliteKnowledgeStore = class {
504
+ constructor(db) {
505
+ this.db = db;
506
+ }
507
+ db;
508
+ // --- Topics ---------------------------------------------------------------
509
+ getTopic(id) {
510
+ const row = this.db.prepare("SELECT * FROM topics WHERE id = ?").get(id);
511
+ return row ? topicFromRow(row) : null;
512
+ }
513
+ getTopicByPath(fullPath) {
514
+ const row = this.db.prepare("SELECT * FROM topics WHERE full_path = ?").get(fullPath);
515
+ return row ? topicFromRow(row) : null;
516
+ }
517
+ getTopicsByDomain(domain) {
518
+ const rows = this.db.prepare("SELECT * FROM topics WHERE domain = ? ORDER BY full_path").all(domain);
519
+ return rows.map(topicFromRow);
520
+ }
521
+ getDueTopics() {
522
+ const rows = this.db.prepare(`SELECT * FROM topics WHERE next_review_at IS NOT NULL AND next_review_at <= datetime('now') ORDER BY next_review_at ASC`).all();
523
+ return rows.map(topicFromRow);
524
+ }
525
+ saveTopic(topic) {
526
+ const stmt = this.db.prepare(`
527
+ INSERT INTO topics (name, parent_id, full_path, domain, stability, difficulty, retrievability,
528
+ last_reviewed_at, next_review_at, review_count, needs_review)
529
+ VALUES (@name, @parentId, @fullPath, @domain, @stability, @difficulty, @retrievability,
530
+ @lastReviewedAt, @nextReviewAt, @reviewCount, @needsReview)
531
+ `);
532
+ const info = withWriteLock(() => stmt.run({
533
+ name: topic.name,
534
+ parentId: topic.parentId,
535
+ fullPath: topic.fullPath,
536
+ domain: topic.domain,
537
+ stability: topic.stability,
538
+ difficulty: topic.difficulty,
539
+ retrievability: topic.retrievability,
540
+ lastReviewedAt: topic.lastReviewedAt,
541
+ nextReviewAt: topic.nextReviewAt,
542
+ reviewCount: topic.reviewCount,
543
+ needsReview: topic.needsReview ? 1 : 0
544
+ }));
545
+ return this.getTopic(info.lastInsertRowid);
546
+ }
547
+ updateTopic(id, patch) {
548
+ const sets = [];
549
+ const params = { id };
550
+ if (patch.name !== void 0) {
551
+ sets.push("name = @name");
552
+ params.name = patch.name;
553
+ }
554
+ if (patch.parentId !== void 0) {
555
+ sets.push("parent_id = @parentId");
556
+ params.parentId = patch.parentId;
557
+ }
558
+ if (patch.fullPath !== void 0) {
559
+ sets.push("full_path = @fullPath");
560
+ params.fullPath = patch.fullPath;
561
+ }
562
+ if (patch.domain !== void 0) {
563
+ sets.push("domain = @domain");
564
+ params.domain = patch.domain;
565
+ }
566
+ if (patch.stability !== void 0) {
567
+ sets.push("stability = @stability");
568
+ params.stability = patch.stability;
569
+ }
570
+ if (patch.difficulty !== void 0) {
571
+ sets.push("difficulty = @difficulty");
572
+ params.difficulty = patch.difficulty;
573
+ }
574
+ if (patch.retrievability !== void 0) {
575
+ sets.push("retrievability = @retrievability");
576
+ params.retrievability = patch.retrievability;
577
+ }
578
+ if (patch.lastReviewedAt !== void 0) {
579
+ sets.push("last_reviewed_at = @lastReviewedAt");
580
+ params.lastReviewedAt = patch.lastReviewedAt;
581
+ }
582
+ if (patch.nextReviewAt !== void 0) {
583
+ sets.push("next_review_at = @nextReviewAt");
584
+ params.nextReviewAt = patch.nextReviewAt;
585
+ }
586
+ if (patch.reviewCount !== void 0) {
587
+ sets.push("review_count = @reviewCount");
588
+ params.reviewCount = patch.reviewCount;
589
+ }
590
+ if (patch.needsReview !== void 0) {
591
+ sets.push("needs_review = @needsReview");
592
+ params.needsReview = patch.needsReview ? 1 : 0;
593
+ }
594
+ if (sets.length === 0) return;
595
+ sets.push("updated_at = datetime('now')");
596
+ withWriteLock(
597
+ () => this.db.prepare(`UPDATE topics SET ${sets.join(", ")} WHERE id = @id`).run(params)
598
+ );
599
+ }
600
+ // --- Evidence -------------------------------------------------------------
601
+ getEvidence(topicId) {
602
+ const rows = this.db.prepare("SELECT * FROM evidence WHERE topic_id = ? ORDER BY created_at DESC").all(topicId);
603
+ return rows.map(evidenceFromRow);
604
+ }
605
+ logEvidence(evidence) {
606
+ const stmt = this.db.prepare(`
607
+ INSERT INTO evidence (topic_id, session_id, type, description, weight)
608
+ VALUES (@topicId, @sessionId, @type, @description, @weight)
609
+ `);
610
+ const info = withWriteLock(() => stmt.run(evidence));
611
+ const row = this.db.prepare("SELECT * FROM evidence WHERE id = ?").get(info.lastInsertRowid);
612
+ return evidenceFromRow(row);
613
+ }
614
+ // --- Corrections ----------------------------------------------------------
615
+ logCorrection(correction) {
616
+ const stmt = this.db.prepare(`
617
+ INSERT INTO correction_events (topic_id, session_id, user_claim, correction, severity)
618
+ VALUES (@topicId, @sessionId, @userClaim, @correction, @severity)
619
+ `);
620
+ const info = withWriteLock(() => stmt.run({
621
+ topicId: correction.topicId,
622
+ sessionId: correction.sessionId,
623
+ userClaim: correction.userClaim,
624
+ correction: correction.correction,
625
+ severity: correction.severity
626
+ }));
627
+ const row = this.db.prepare("SELECT * FROM correction_events WHERE id = ?").get(info.lastInsertRowid);
628
+ return {
629
+ id: row.id,
630
+ topicId: row.topic_id,
631
+ sessionId: row.session_id,
632
+ userClaim: row.user_claim,
633
+ correction: row.correction,
634
+ severity: row.severity,
635
+ createdAt: row.created_at
636
+ };
637
+ }
638
+ // --- Domains --------------------------------------------------------------
639
+ getAllDomains() {
640
+ const rows = this.db.prepare("SELECT DISTINCT domain FROM topics WHERE domain IS NOT NULL ORDER BY domain").all();
641
+ return rows.map((r) => r.domain);
642
+ }
643
+ // --- Profile --------------------------------------------------------------
644
+ getProfile(key) {
645
+ const row = this.db.prepare("SELECT value FROM profile WHERE key = ?").get(key);
646
+ return row?.value ?? null;
647
+ }
648
+ setProfile(key, value) {
649
+ withWriteLock(
650
+ () => this.db.prepare(`
651
+ INSERT INTO profile (key, value, updated_at)
652
+ VALUES (@key, @value, datetime('now'))
653
+ ON CONFLICT(key) DO UPDATE SET value = @value, updated_at = datetime('now')
654
+ `).run({ key, value })
655
+ );
656
+ }
657
+ // --- Projects -------------------------------------------------------------
658
+ getProject(name) {
659
+ const row = this.db.prepare("SELECT * FROM projects WHERE name = ?").get(name);
660
+ return row ? projectFromRow(row) : null;
661
+ }
662
+ saveProject(project) {
663
+ const stmt = this.db.prepare(`
664
+ INSERT INTO projects (name, path, git_remote, language, last_seen_at)
665
+ VALUES (@name, @path, @gitRemote, @language, @lastSeenAt)
666
+ ON CONFLICT(name) DO UPDATE SET
667
+ path = @path,
668
+ git_remote = @gitRemote,
669
+ language = @language,
670
+ last_seen_at = @lastSeenAt
671
+ `);
672
+ withWriteLock(() => stmt.run({
673
+ name: project.name,
674
+ path: project.path,
675
+ gitRemote: project.gitRemote,
676
+ language: project.language,
677
+ lastSeenAt: project.lastSeenAt
678
+ }));
679
+ return this.getProject(project.name);
680
+ }
681
+ // --- Sessions -------------------------------------------------------------
682
+ saveSession(session2) {
683
+ const stmt = this.db.prepare(`
684
+ INSERT INTO sessions (id, started_at, ended_at, model, provider, project_name,
685
+ message_count, active_duration_seconds, interleaving_index, time_of_day,
686
+ input_tokens, output_tokens)
687
+ VALUES (@id, @startedAt, @endedAt, @model, @provider, @projectName,
688
+ 0, @activeDurationSeconds, @interleavingIndex, @timeOfDay, 0, 0)
689
+ `);
690
+ withWriteLock(() => stmt.run({
691
+ id: session2.id,
692
+ startedAt: session2.startedAt,
693
+ endedAt: session2.endedAt,
694
+ model: session2.model,
695
+ provider: session2.provider,
696
+ projectName: session2.projectName,
697
+ activeDurationSeconds: session2.activeDurationSeconds,
698
+ interleavingIndex: session2.interleavingIndex,
699
+ timeOfDay: session2.timeOfDay
700
+ }));
701
+ return this.getSession(session2.id);
702
+ }
703
+ updateSession(id, patch) {
704
+ const sets = [];
705
+ const params = { id };
706
+ if (patch.endedAt !== void 0) {
707
+ sets.push("ended_at = @endedAt");
708
+ params.endedAt = patch.endedAt;
709
+ }
710
+ if (patch.messageCount !== void 0) {
711
+ sets.push("message_count = @messageCount");
712
+ params.messageCount = patch.messageCount;
713
+ }
714
+ if (patch.activeDurationSeconds !== void 0) {
715
+ sets.push("active_duration_seconds = @activeDurationSeconds");
716
+ params.activeDurationSeconds = patch.activeDurationSeconds;
717
+ }
718
+ if (patch.interleavingIndex !== void 0) {
719
+ sets.push("interleaving_index = @interleavingIndex");
720
+ params.interleavingIndex = patch.interleavingIndex;
721
+ }
722
+ if (patch.timeOfDay !== void 0) {
723
+ sets.push("time_of_day = @timeOfDay");
724
+ params.timeOfDay = patch.timeOfDay;
725
+ }
726
+ if (patch.inputTokens !== void 0) {
727
+ sets.push("input_tokens = @inputTokens");
728
+ params.inputTokens = patch.inputTokens;
729
+ }
730
+ if (patch.outputTokens !== void 0) {
731
+ sets.push("output_tokens = @outputTokens");
732
+ params.outputTokens = patch.outputTokens;
733
+ }
734
+ if (sets.length === 0) return;
735
+ withWriteLock(
736
+ () => this.db.prepare(`UPDATE sessions SET ${sets.join(", ")} WHERE id = @id`).run(params)
737
+ );
738
+ }
739
+ getSession(id) {
740
+ const row = this.db.prepare("SELECT * FROM sessions WHERE id = ?").get(id);
741
+ return row ? sessionFromRow(row) : null;
742
+ }
743
+ };
744
+ var LocalMemoryStore = class {
745
+ constructor(db) {
746
+ this.db = db;
747
+ }
748
+ db;
749
+ write(content, tags) {
750
+ const stmt = this.db.prepare(`
751
+ INSERT INTO memories (content, tags) VALUES (@content, @tags)
752
+ `);
753
+ const info = withWriteLock(() => stmt.run({ content, tags: JSON.stringify(tags) }));
754
+ const row = this.db.prepare("SELECT * FROM memories WHERE id = ?").get(info.lastInsertRowid);
755
+ return memoryFromRow(row);
756
+ }
757
+ search(query, limit) {
758
+ const rows = this.db.prepare(`
759
+ SELECT * FROM memories WHERE content LIKE @pattern ORDER BY created_at DESC LIMIT @limit
760
+ `).all({ pattern: `%${query}%`, limit });
761
+ return rows.map(memoryFromRow);
762
+ }
763
+ getAll() {
764
+ const rows = this.db.prepare("SELECT * FROM memories ORDER BY created_at DESC").all();
765
+ return rows.map(memoryFromRow);
766
+ }
767
+ };
768
+
769
+ // src/store/migrations/runner.ts
770
+ import { readFileSync, readdirSync } from "fs";
771
+ import { join, dirname } from "path";
772
+ import { fileURLToPath } from "url";
773
+ var __dirname = dirname(fileURLToPath(import.meta.url));
774
+ var SQL_DIR = join(__dirname, "sql");
775
+ function listMigrationFiles() {
776
+ return readdirSync(SQL_DIR).filter((f) => /^\d+_.*\.sql$/.test(f)).map((f) => ({
777
+ version: parseInt(f.split("_")[0], 10),
778
+ path: join(SQL_DIR, f)
779
+ })).sort((a, b) => a.version - b.version);
780
+ }
781
+ function appliedVersions(db) {
782
+ const tableExists = db.prepare(`SELECT name FROM sqlite_master WHERE type='table' AND name='schema_migrations'`).get();
783
+ if (!tableExists) return /* @__PURE__ */ new Set();
784
+ const rows = db.prepare("SELECT version FROM schema_migrations").all();
785
+ return new Set(rows.map((r) => r.version));
786
+ }
787
+ function runMigrations(db) {
788
+ const files = listMigrationFiles();
789
+ const applied = appliedVersions(db);
790
+ const pending = files.filter((f) => !applied.has(f.version));
791
+ if (pending.length === 0) return;
792
+ const applyAll = db.transaction(() => {
793
+ for (const { version, path: path3 } of pending) {
794
+ const sql = readFileSync(path3, "utf8");
795
+ db.exec(sql);
796
+ db.prepare("INSERT INTO schema_migrations (version) VALUES (?)").run(version);
797
+ console.log(`[zencefyl] applied migration ${version.toString().padStart(3, "0")}`);
798
+ }
799
+ });
800
+ applyAll();
801
+ }
802
+
803
+ // src/bootstrap/state.ts
804
+ import { randomUUID } from "crypto";
805
+ var session = {
806
+ sessionId: randomUUID(),
807
+ startTime: /* @__PURE__ */ new Date(),
808
+ inputTokens: 0,
809
+ outputTokens: 0,
810
+ model: ""
811
+ // Filled in by createContainer() after config loads
812
+ };
813
+ function accumulateUsage(inputTokens, outputTokens) {
814
+ session.inputTokens += inputTokens;
815
+ session.outputTokens += outputTokens;
816
+ }
817
+
818
+ // src/bootstrap/container.ts
819
+ function createContainer(config) {
820
+ session.model = config.models.default;
821
+ const dbPath = path2.join(config.dataDir ?? ZENCEFYL_DIR, "knowledge.db");
822
+ const db = new Database(dbPath);
823
+ db.pragma("journal_mode = WAL");
824
+ db.pragma("foreign_keys = ON");
825
+ runMigrations(db);
826
+ const store = new SqliteKnowledgeStore(db);
827
+ const memoryStore = new LocalMemoryStore(db);
828
+ let provider;
829
+ switch (config.provider) {
830
+ case "anthropic":
831
+ provider = new AnthropicProvider(config);
832
+ break;
833
+ default:
834
+ provider = new ClaudeCodeProvider();
835
+ break;
836
+ }
837
+ const timeOfDay = computeTimeOfDay(session.startTime);
838
+ store.saveSession({
839
+ id: session.sessionId,
840
+ startedAt: session.startTime.toISOString(),
841
+ endedAt: null,
842
+ model: config.models.default,
843
+ provider: config.provider ?? "claude-code",
844
+ projectName: null,
845
+ // Phase 3 fills this from cwd detection
846
+ activeDurationSeconds: null,
847
+ interleavingIndex: null,
848
+ timeOfDay
849
+ });
850
+ return { provider, store, memoryStore, config };
851
+ }
852
+ function computeTimeOfDay(date) {
853
+ const hour = date.getHours();
854
+ if (hour >= 5 && hour < 12) return "morning";
855
+ if (hour >= 12 && hour < 17) return "afternoon";
856
+ if (hour >= 17 && hour < 21) return "evening";
857
+ return "night";
858
+ }
859
+
860
+ // src/constants/limits.ts
861
+ var EVIDENCE_WEIGHTS = {
862
+ explicit: 0.6,
863
+ // user stated they know it — lowest weight (self-report)
864
+ code_reviewed: 0.9,
865
+ // reviewed code using this concept
866
+ code_built: 1,
867
+ // wrote working code — strong signal
868
+ physical_build: 1.1,
869
+ // built physical hardware — even stronger
870
+ project_built: 1.2
871
+ // shipped a full project — strongest signal
872
+ };
873
+
874
+ // src/core/knowledge/extractor.ts
875
+ var EXTRACTOR_PROMPT = `You are a knowledge extraction engine. You analyze a conversation between a user and Zencefyl (an AI companion) and extract knowledge signals.
876
+
877
+ Return ONLY valid JSON matching this schema \u2014 no markdown, no explanation, nothing else:
878
+ {
879
+ "signals": [
880
+ {
881
+ "topic_path": "Domain/Topic/Concept",
882
+ "domain": "Domain",
883
+ "evidence_type": "explicit" | "code_reviewed" | "code_built" | "physical_build" | "project_built",
884
+ "description": "one-sentence summary of what was demonstrated",
885
+ "confidence": 0.0-1.0,
886
+ "correction": {
887
+ "user_claim": "what the user stated",
888
+ "correction": "what was actually correct",
889
+ "severity": "minor" | "moderate" | "fundamental"
890
+ }
891
+ }
892
+ ]
893
+ }
894
+
895
+ Evidence type rules:
896
+ - explicit: user stated they know or learned something (lowest weight)
897
+ - code_reviewed: user discussed reviewing existing code
898
+ - code_built: user described writing or building code
899
+ - physical_build: user described building physical hardware, wiring, soldering
900
+ - project_built: user described completing an entire project
901
+
902
+ Correction field: only include if Zencefyl explicitly corrected a user mistake.
903
+
904
+ Topic path format: TitleCase/TitleCase/TitleCase \u2014 max 5 levels, singular nouns.
905
+ Examples: "C++/Memory Management/RAII", "Electronics/FPGA/HDL/FSM/State Encoding"
906
+
907
+ Include a signal only if there is genuine evidence in this specific exchange.
908
+ If nothing knowledge-relevant happened (chit-chat, procedural questions), return { "signals": [] }.
909
+ Return ONLY the JSON object. No other text.`;
910
+ function parseSignals(raw) {
911
+ const cleaned = raw.trim().replace(/^```(?:json)?\s*/i, "").replace(/\s*```$/, "");
912
+ try {
913
+ const parsed = JSON.parse(cleaned);
914
+ return Array.isArray(parsed.signals) ? parsed.signals : [];
915
+ } catch {
916
+ return [];
917
+ }
918
+ }
919
+ function normalizePath(raw) {
920
+ return raw.split("/").map((segment) => segment.trim()).filter(Boolean).slice(0, 5).map((s) => s.charAt(0).toUpperCase() + s.slice(1)).join("/");
921
+ }
922
+ function ensureTopicPath(store, fullPath, domain) {
923
+ const existing = store.getTopicByPath(fullPath);
924
+ if (existing) return existing.id;
925
+ const segments = fullPath.split("/");
926
+ let parentId = null;
927
+ for (let depth = 1; depth <= segments.length; depth++) {
928
+ const partialPath = segments.slice(0, depth).join("/");
929
+ const name = segments[depth - 1];
930
+ const node = store.getTopicByPath(partialPath);
931
+ if (node) {
932
+ parentId = node.id;
933
+ continue;
934
+ }
935
+ const created = store.saveTopic({
936
+ name,
937
+ parentId,
938
+ fullPath: partialPath,
939
+ domain: depth === 1 ? name : domain,
940
+ stability: 1,
941
+ difficulty: 0.3,
942
+ retrievability: 1,
943
+ lastReviewedAt: null,
944
+ nextReviewAt: null,
945
+ reviewCount: 0,
946
+ needsReview: false
947
+ });
948
+ parentId = created.id;
949
+ }
950
+ return parentId;
951
+ }
952
+ async function extractKnowledge(userMessage, assistantMessage, sessionId, store, provider, fastModel) {
953
+ const conversationSnippet = `USER: ${userMessage}
954
+
955
+ ZENCEFYL: ${assistantMessage}`;
956
+ let raw = "";
957
+ try {
958
+ for await (const delta of provider.chat(
959
+ [{ role: "user", content: conversationSnippet }],
960
+ EXTRACTOR_PROMPT,
961
+ fastModel
962
+ )) {
963
+ if (delta.type === "text") raw += delta.text;
964
+ }
965
+ } catch {
966
+ return;
967
+ }
968
+ const signals = parseSignals(raw);
969
+ if (signals.length === 0) return;
970
+ for (const signal of signals) {
971
+ if (signal.confidence < 0.3) continue;
972
+ const fullPath = normalizePath(signal.topic_path);
973
+ if (!fullPath) continue;
974
+ const topicId = ensureTopicPath(store, fullPath, signal.domain);
975
+ const baseWeight = EVIDENCE_WEIGHTS[signal.evidence_type] ?? 0.6;
976
+ const weight = parseFloat((baseWeight * signal.confidence).toFixed(3));
977
+ store.logEvidence({
978
+ topicId,
979
+ sessionId,
980
+ type: signal.evidence_type,
981
+ description: signal.description,
982
+ weight
983
+ });
984
+ if (signal.correction) {
985
+ const severity = signal.correction.severity === "minor" ? "minor" : signal.correction.severity === "moderate" ? "moderate" : "fundamental";
986
+ store.logCorrection({
987
+ topicId,
988
+ sessionId,
989
+ userClaim: signal.correction.user_claim,
990
+ correction: signal.correction.correction,
991
+ severity
992
+ });
993
+ }
994
+ }
995
+ }
996
+
997
+ // src/core/knowledge/context.ts
998
+ var MAX_TOPICS = 12;
999
+ var MAX_DUE = 5;
1000
+ function buildKnowledgeContext(store) {
1001
+ const dueTopics = store.getDueTopics().slice(0, MAX_DUE);
1002
+ const recentTopics = collectRecentTopics(store, MAX_TOPICS);
1003
+ if (recentTopics.length === 0 && dueTopics.length === 0) return "";
1004
+ const lines = ["[Knowledge Store Context]"];
1005
+ if (dueTopics.length > 0) {
1006
+ lines.push("\nTopics due for review (bring these up naturally if relevant):");
1007
+ for (const t of dueTopics) {
1008
+ const days = t.nextReviewAt ? Math.floor((Date.now() - new Date(t.nextReviewAt).getTime()) / 864e5) : 0;
1009
+ lines.push(` - ${t.fullPath} (R=${t.retrievability.toFixed(2)}, ${days}d overdue)`);
1010
+ }
1011
+ }
1012
+ if (recentTopics.length > 0) {
1013
+ lines.push("\nRecently active knowledge:");
1014
+ for (const t of recentTopics) {
1015
+ const r = t.retrievability.toFixed(2);
1016
+ lines.push(` - ${t.fullPath} (R=${r})`);
1017
+ }
1018
+ }
1019
+ lines.push("");
1020
+ return lines.join("\n");
1021
+ }
1022
+ function collectRecentTopics(store, limit) {
1023
+ const domains = store.getAllDomains();
1024
+ const seen = /* @__PURE__ */ new Set();
1025
+ const result = [];
1026
+ for (const domain of domains) {
1027
+ const domainTopics = store.getTopicsByDomain(domain);
1028
+ for (const t of domainTopics) {
1029
+ if (!seen.has(t.id)) {
1030
+ seen.add(t.id);
1031
+ result.push(t);
1032
+ }
1033
+ }
1034
+ if (result.length >= limit * 2) break;
1035
+ }
1036
+ return result.sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()).slice(0, limit);
1037
+ }
1038
+
1039
+ // src/tools/knowledge/read-topic/index.ts
1040
+ import { z } from "zod";
1041
+
1042
+ // src/tools/knowledge/read-topic/prompt.ts
1043
+ var TOOL_DESCRIPTION = 'Look up what the user knows about a specific topic or concept. Use this when the user asks what they know about something, when you need to check their current understanding before explaining, or when you want to see if a concept has been logged before. Path format: "Domain/Topic/Concept" (e.g. "C++/Memory Management/RAII"). You can pass just a domain ("C++") to see the top-level topic.';
1044
+
1045
+ // src/tools/knowledge/read-topic/index.ts
1046
+ var InputSchema = z.object({
1047
+ path: z.string().min(1, "path must not be empty"),
1048
+ include_evidence: z.boolean().optional()
1049
+ });
1050
+ var INPUT_SCHEMA = {
1051
+ type: "object",
1052
+ properties: {
1053
+ path: {
1054
+ type: "string",
1055
+ description: 'Full topic path to look up, e.g. "C++/Memory Management/RAII" or just "C++" for the domain. Case-insensitive.'
1056
+ },
1057
+ include_evidence: {
1058
+ type: "boolean",
1059
+ description: "Include the last 5 evidence records. Default true."
1060
+ }
1061
+ },
1062
+ required: ["path"]
1063
+ };
1064
+ function confidenceLabel(r) {
1065
+ if (r >= 0.85) return "strong";
1066
+ if (r >= 0.65) return "good";
1067
+ if (r >= 0.45) return "moderate";
1068
+ if (r >= 0.25) return "weak";
1069
+ return "very weak";
1070
+ }
1071
+ var readTopicTool = {
1072
+ name: "read-topic",
1073
+ description: TOOL_DESCRIPTION,
1074
+ inputSchema: INPUT_SCHEMA,
1075
+ async execute(input, ctx) {
1076
+ const parsed = InputSchema.safeParse(input);
1077
+ if (!parsed.success) {
1078
+ return { content: `Invalid input: ${parsed.error.issues.map((i) => i.message).join(", ")}`, isError: true };
1079
+ }
1080
+ const path3 = parsed.data.path.trim();
1081
+ const includeEvidence = parsed.data.include_evidence ?? true;
1082
+ if (!path3) {
1083
+ return { content: "Error: path must not be empty.", isError: true };
1084
+ }
1085
+ let topic = ctx.store.getTopicByPath(path3);
1086
+ if (!topic) {
1087
+ const domain2 = path3.split("/")[0] ?? path3;
1088
+ const allTopics2 = ctx.store.getTopicsByDomain(domain2);
1089
+ const lower = path3.toLowerCase();
1090
+ topic = allTopics2.find((t) => t.fullPath.toLowerCase() === lower) ?? null;
1091
+ if (!topic) {
1092
+ const partial = allTopics2.find(
1093
+ (t) => t.fullPath.toLowerCase().startsWith(lower) || lower.startsWith(t.fullPath.toLowerCase())
1094
+ ) ?? null;
1095
+ topic = partial;
1096
+ }
1097
+ }
1098
+ if (!topic) {
1099
+ return {
1100
+ content: `No topic found for "${path3}".
1101
+
1102
+ This topic hasn't been logged yet. If the user just learned something about it, use log-evidence to record it.`
1103
+ };
1104
+ }
1105
+ const lines = [];
1106
+ lines.push(`TOPIC: ${topic.fullPath}`);
1107
+ lines.push(`Confidence: ${confidenceLabel(topic.retrievability)} (R=${topic.retrievability.toFixed(2)}, stability=${topic.stability.toFixed(1)} days)`);
1108
+ lines.push(`Reviews: ${topic.reviewCount}`);
1109
+ if (topic.nextReviewAt) {
1110
+ const due = new Date(topic.nextReviewAt);
1111
+ const now = /* @__PURE__ */ new Date();
1112
+ const overdue = due < now;
1113
+ lines.push(`Next review: ${due.toLocaleDateString()} ${overdue ? "(OVERDUE)" : ""}`);
1114
+ } else {
1115
+ lines.push("Next review: not scheduled");
1116
+ }
1117
+ if (topic.needsReview) {
1118
+ lines.push("Flag: possible duplicate \u2014 needs review");
1119
+ }
1120
+ const domain = topic.domain ?? topic.name;
1121
+ const allTopics = ctx.store.getTopicsByDomain(domain);
1122
+ const children = allTopics.filter((t) => t.parentId === topic.id);
1123
+ if (children.length > 0) {
1124
+ lines.push(`
1125
+ Sub-topics (${children.length}):`);
1126
+ for (const child of children.slice(0, 8)) {
1127
+ lines.push(` - ${child.name} (R=${child.retrievability.toFixed(2)})`);
1128
+ }
1129
+ if (children.length > 8) {
1130
+ lines.push(` ... and ${children.length - 8} more`);
1131
+ }
1132
+ }
1133
+ if (includeEvidence) {
1134
+ const evidence = ctx.store.getEvidence(topic.id).slice(0, 5);
1135
+ if (evidence.length > 0) {
1136
+ lines.push("\nRecent evidence:");
1137
+ for (const ev of evidence) {
1138
+ const date = new Date(ev.createdAt).toLocaleDateString();
1139
+ lines.push(` [${date}] (${ev.type}, w=${ev.weight.toFixed(2)}) ${ev.description}`);
1140
+ }
1141
+ } else {
1142
+ lines.push("\nNo evidence recorded yet.");
1143
+ }
1144
+ }
1145
+ return { content: lines.join("\n") };
1146
+ }
1147
+ };
1148
+
1149
+ // src/tools/knowledge/write-topic/index.ts
1150
+ import { z as z2 } from "zod";
1151
+
1152
+ // src/tools/knowledge/write-topic/prompt.ts
1153
+ var TOOL_DESCRIPTION2 = "Create a new topic node in the knowledge graph, or confirm an existing one. Use this before log-evidence when you need to ensure a topic path exists. All parent nodes are created automatically \u2014 you only need to call this once for the full path. Idempotent: safe to call even if the topic already exists.";
1154
+
1155
+ // src/tools/knowledge/write-topic/index.ts
1156
+ var InputSchema2 = z2.object({
1157
+ path: z2.string().min(1, "path must not be empty"),
1158
+ domain: z2.string().optional()
1159
+ });
1160
+ var INPUT_SCHEMA2 = {
1161
+ type: "object",
1162
+ properties: {
1163
+ path: {
1164
+ type: "string",
1165
+ description: 'Full topic path to create, e.g. "C++/Memory Management/RAII". All parent nodes are created automatically.'
1166
+ },
1167
+ domain: {
1168
+ type: "string",
1169
+ description: 'Top-level domain for this topic (e.g. "C++", "Electronics"). Inferred from path if omitted.'
1170
+ }
1171
+ },
1172
+ required: ["path"]
1173
+ };
1174
+ function titleCase(s) {
1175
+ return s.charAt(0).toUpperCase() + s.slice(1);
1176
+ }
1177
+ function ensurePath(store, fullPath, domain) {
1178
+ const segments = fullPath.split("/");
1179
+ let parentId = null;
1180
+ for (let depth = 1; depth <= segments.length; depth++) {
1181
+ const partialPath = segments.slice(0, depth).join("/");
1182
+ const name = segments[depth - 1] ?? "";
1183
+ const existing = store.getTopicByPath(partialPath);
1184
+ if (existing) {
1185
+ parentId = existing.id;
1186
+ continue;
1187
+ }
1188
+ const created = store.saveTopic({
1189
+ name,
1190
+ parentId,
1191
+ fullPath: partialPath,
1192
+ domain: depth === 1 ? name : domain,
1193
+ stability: 1,
1194
+ difficulty: 0.3,
1195
+ retrievability: 1,
1196
+ lastReviewedAt: null,
1197
+ nextReviewAt: null,
1198
+ reviewCount: 0,
1199
+ needsReview: false
1200
+ });
1201
+ parentId = created.id;
1202
+ }
1203
+ return parentId;
1204
+ }
1205
+ var writeTopicTool = {
1206
+ name: "write-topic",
1207
+ description: TOOL_DESCRIPTION2,
1208
+ inputSchema: INPUT_SCHEMA2,
1209
+ async execute(input, ctx) {
1210
+ const parsed = InputSchema2.safeParse(input);
1211
+ if (!parsed.success) {
1212
+ return { content: `Invalid input: ${parsed.error.issues.map((i) => i.message).join(", ")}`, isError: true };
1213
+ }
1214
+ const rawPath = parsed.data.path.trim();
1215
+ if (!rawPath) {
1216
+ return { content: "Error: path must not be empty.", isError: true };
1217
+ }
1218
+ const normalizedPath = rawPath.split("/").map((s) => titleCase(s.trim())).filter(Boolean).slice(0, 5).join("/");
1219
+ const domain = parsed.data.domain?.trim() ?? normalizedPath.split("/")[0] ?? normalizedPath;
1220
+ const existing = ctx.store.getTopicByPath(normalizedPath);
1221
+ if (existing) {
1222
+ return {
1223
+ content: `Topic already exists: ${normalizedPath} (id=${existing.id}, R=${existing.retrievability.toFixed(2)})`
1224
+ };
1225
+ }
1226
+ const id = ensurePath(ctx.store, normalizedPath, domain);
1227
+ const created = ctx.store.getTopic(id);
1228
+ return {
1229
+ content: `Created topic: ${normalizedPath} (id=${id})${created?.parentId ? ` under parent id=${created.parentId}` : ""}`
1230
+ };
1231
+ }
1232
+ };
1233
+
1234
+ // src/tools/knowledge/log-evidence/index.ts
1235
+ import { z as z3 } from "zod";
1236
+
1237
+ // src/tools/knowledge/log-evidence/prompt.ts
1238
+ var TOOL_DESCRIPTION3 = "Record learning evidence for a topic. Use this when the user demonstrates knowledge: writes code, builds something, explicitly learns a concept, or you correct them and they understand. Pick the evidence type carefully \u2014 it determines how much weight the evidence carries. Use explicit for stated knowledge, code_built for working code, project_built for shipped work. The topic is created automatically if it does not exist.";
1239
+
1240
+ // src/tools/knowledge/log-evidence/index.ts
1241
+ var EVIDENCE_TYPES = ["explicit", "code_reviewed", "code_built", "physical_build", "project_built"];
1242
+ var InputSchema3 = z3.object({
1243
+ topic_path: z3.string().min(1, "topic_path must not be empty"),
1244
+ type: z3.enum(EVIDENCE_TYPES),
1245
+ description: z3.string().min(1, "description must not be empty")
1246
+ });
1247
+ var INPUT_SCHEMA3 = {
1248
+ type: "object",
1249
+ properties: {
1250
+ topic_path: {
1251
+ type: "string",
1252
+ description: 'Full topic path, e.g. "C++/Memory Management/RAII". Created if it does not exist.'
1253
+ },
1254
+ type: {
1255
+ type: "string",
1256
+ enum: [...EVIDENCE_TYPES],
1257
+ description: "Evidence type. explicit = user stated they know it (weight 0.6). code_reviewed = reviewed code using this concept (0.9). code_built = wrote working code applying it (1.0). physical_build = built physical hardware using it (1.1). project_built = shipped a full project applying it (1.2)."
1258
+ },
1259
+ description: {
1260
+ type: "string",
1261
+ description: "One-sentence summary of what was demonstrated or learned."
1262
+ }
1263
+ },
1264
+ required: ["topic_path", "type", "description"]
1265
+ };
1266
+ function ensureTopicPath2(store, fullPath) {
1267
+ const segments = fullPath.split("/");
1268
+ const domain = segments[0] ?? fullPath;
1269
+ let parentId = null;
1270
+ for (let depth = 1; depth <= segments.length; depth++) {
1271
+ const partialPath = segments.slice(0, depth).join("/");
1272
+ const name = segments[depth - 1] ?? "";
1273
+ const existing = store.getTopicByPath(partialPath);
1274
+ if (existing) {
1275
+ parentId = existing.id;
1276
+ continue;
1277
+ }
1278
+ const created = store.saveTopic({
1279
+ name,
1280
+ parentId,
1281
+ fullPath: partialPath,
1282
+ domain: depth === 1 ? name : domain,
1283
+ stability: 1,
1284
+ difficulty: 0.3,
1285
+ retrievability: 1,
1286
+ lastReviewedAt: null,
1287
+ nextReviewAt: null,
1288
+ reviewCount: 0,
1289
+ needsReview: false
1290
+ });
1291
+ parentId = created.id;
1292
+ }
1293
+ return parentId;
1294
+ }
1295
+ var logEvidenceTool = {
1296
+ name: "log-evidence",
1297
+ description: TOOL_DESCRIPTION3,
1298
+ inputSchema: INPUT_SCHEMA3,
1299
+ async execute(input, ctx) {
1300
+ const parsed = InputSchema3.safeParse(input);
1301
+ if (!parsed.success) {
1302
+ return { content: `Invalid input: ${parsed.error.issues.map((i) => i.message).join(", ")}`, isError: true };
1303
+ }
1304
+ const { topic_path: rawPath, type, description: desc } = parsed.data;
1305
+ const fullPath = rawPath.trim().split("/").map((s) => {
1306
+ const t = s.trim();
1307
+ return t.charAt(0).toUpperCase() + t.slice(1);
1308
+ }).filter(Boolean).slice(0, 5).join("/");
1309
+ const topicId = ensureTopicPath2(ctx.store, fullPath);
1310
+ const weight = EVIDENCE_WEIGHTS[type];
1311
+ const ev = ctx.store.logEvidence({
1312
+ topicId,
1313
+ sessionId: ctx.sessionId,
1314
+ type,
1315
+ description: desc,
1316
+ weight
1317
+ });
1318
+ return {
1319
+ content: `Logged evidence for "${fullPath}"
1320
+ Type: ${type} (weight=${weight})
1321
+ Description: ${desc}
1322
+ Evidence id: ${ev.id}`
1323
+ };
1324
+ }
1325
+ };
1326
+
1327
+ // src/tools/knowledge/search-topics/index.ts
1328
+ import { z as z4 } from "zod";
1329
+
1330
+ // src/tools/knowledge/search-topics/prompt.ts
1331
+ var TOOL_DESCRIPTION4 = 'Search the knowledge graph by topic name or path. Use this when the user asks "what do I know about X" for a broad topic that might span multiple sub-paths. Returns a summary of matching topics with confidence scores. For a specific known path, prefer read-topic instead.';
1332
+
1333
+ // src/tools/knowledge/search-topics/index.ts
1334
+ var InputSchema4 = z4.object({
1335
+ query: z4.string().min(1, "query must not be empty"),
1336
+ domain: z4.string().optional(),
1337
+ min_confidence: z4.number().min(0).max(1).optional()
1338
+ });
1339
+ var INPUT_SCHEMA4 = {
1340
+ type: "object",
1341
+ properties: {
1342
+ query: {
1343
+ type: "string",
1344
+ description: "Search term. Matched against topic paths and names (case-insensitive substring match)."
1345
+ },
1346
+ domain: {
1347
+ type: "string",
1348
+ description: 'Limit results to this top-level domain, e.g. "C++", "Electronics". Optional.'
1349
+ },
1350
+ min_confidence: {
1351
+ type: "number",
1352
+ description: "Only return topics with retrievability >= this value (0.0\u20131.0). Default 0 (all)."
1353
+ }
1354
+ },
1355
+ required: ["query"]
1356
+ };
1357
+ var searchTopicsTool = {
1358
+ name: "search-topics",
1359
+ description: TOOL_DESCRIPTION4,
1360
+ inputSchema: INPUT_SCHEMA4,
1361
+ async execute(input, ctx) {
1362
+ const parsed = InputSchema4.safeParse(input);
1363
+ if (!parsed.success) {
1364
+ return { content: `Invalid input: ${parsed.error.issues.map((i) => i.message).join(", ")}`, isError: true };
1365
+ }
1366
+ const query = parsed.data.query.trim().toLowerCase();
1367
+ const domain = parsed.data.domain?.trim();
1368
+ const minConfidence = parsed.data.min_confidence ?? 0;
1369
+ let candidates = domain ? ctx.store.getTopicsByDomain(domain) : (() => {
1370
+ const allDomains = ctx.store.getAllDomains();
1371
+ const seen = /* @__PURE__ */ new Set();
1372
+ const all = [];
1373
+ for (const d of allDomains) {
1374
+ for (const t of ctx.store.getTopicsByDomain(d)) {
1375
+ if (!seen.has(t.id)) {
1376
+ seen.add(t.id);
1377
+ all.push(t);
1378
+ }
1379
+ }
1380
+ }
1381
+ return all;
1382
+ })();
1383
+ const matches = candidates.filter(
1384
+ (t) => t.fullPath.toLowerCase().includes(query) && t.retrievability >= minConfidence
1385
+ );
1386
+ if (matches.length === 0) {
1387
+ return {
1388
+ content: `No topics found matching "${query}"${domain ? ` in domain "${domain}"` : ""}.
1389
+ The user hasn't logged any knowledge about this yet.`
1390
+ };
1391
+ }
1392
+ matches.sort((a, b) => b.retrievability - a.retrievability);
1393
+ const lines = [
1394
+ `Found ${matches.length} topic(s) matching "${query}":`,
1395
+ ""
1396
+ ];
1397
+ for (const t of matches.slice(0, 20)) {
1398
+ const r = t.retrievability.toFixed(2);
1399
+ const s = t.stability.toFixed(1);
1400
+ const due = t.nextReviewAt ? ` | due ${new Date(t.nextReviewAt).toLocaleDateString()}` : "";
1401
+ const flag = t.needsReview ? " [needs review]" : "";
1402
+ lines.push(` ${t.fullPath}`);
1403
+ lines.push(` R=${r} (stability=${s}d, ${t.reviewCount} reviews)${due}${flag}`);
1404
+ }
1405
+ if (matches.length > 20) {
1406
+ lines.push(` ... and ${matches.length - 20} more (use domain filter to narrow)`);
1407
+ }
1408
+ return { content: lines.join("\n") };
1409
+ }
1410
+ };
1411
+
1412
+ // src/core/engine.ts
1413
+ var MAX_TOOL_ITERATIONS = 8;
1414
+ var Engine = class {
1415
+ history;
1416
+ container;
1417
+ // All knowledge tools available to the model.
1418
+ // Only used by AnthropicProvider (Phase 2). ClaudeCodeProvider gets
1419
+ // knowledge via system prompt context injection instead.
1420
+ tools;
1421
+ constructor(container) {
1422
+ this.container = container;
1423
+ this.history = [];
1424
+ this.tools = [readTopicTool, writeTopicTool, logEvidenceTool, searchTopicsTool];
1425
+ }
1426
+ // ── Public API ──────────────────────────────────────────────────────────────
1427
+ // Send a user message and stream back the response.
1428
+ //
1429
+ // Yields StreamDeltas: text chunks, tool_use signals, tool_result signals,
1430
+ // usage summary, then done. The caller (App.tsx) renders these in real time.
1431
+ //
1432
+ // The agentic loop runs transparently — if the model calls a tool, we
1433
+ // execute it and continue without breaking the stream. The caller sees
1434
+ // tool_use and tool_result deltas so it can render "[reading: ...]" UI.
1435
+ //
1436
+ // Abort: if signal fires mid-stream, the generator returns without
1437
+ // committing anything to history (user turn is rolled back).
1438
+ async *sendMessage(text, signal) {
1439
+ const model = this.container.config.models.default;
1440
+ const knowledgeContext = buildKnowledgeContext(this.container.store);
1441
+ const systemPrompt = knowledgeContext ? `${PERSONALITY_PROMPT}
1442
+
1443
+ ${knowledgeContext}` : PERSONALITY_PROMPT;
1444
+ const workingMessages = [
1445
+ ...this.history,
1446
+ { role: "user", content: text }
1447
+ ];
1448
+ let finalText = "";
1449
+ try {
1450
+ for (let iteration = 0; iteration < MAX_TOOL_ITERATIONS; iteration++) {
1451
+ const toolCallsThisIteration = [];
1452
+ let iterText = "";
1453
+ for await (const delta of this.container.provider.chat(
1454
+ workingMessages,
1455
+ systemPrompt,
1456
+ model,
1457
+ { signal, tools: this.tools }
1458
+ )) {
1459
+ switch (delta.type) {
1460
+ case "text":
1461
+ iterText += delta.text;
1462
+ yield delta;
1463
+ break;
1464
+ case "tool_use":
1465
+ toolCallsThisIteration.push({ id: delta.id, name: delta.name, input: delta.input });
1466
+ yield delta;
1467
+ break;
1468
+ case "usage":
1469
+ accumulateUsage(delta.inputTokens, delta.outputTokens);
1470
+ yield delta;
1471
+ break;
1472
+ case "done":
1473
+ break;
1474
+ }
1475
+ }
1476
+ if (toolCallsThisIteration.length === 0) {
1477
+ finalText = iterText;
1478
+ break;
1479
+ }
1480
+ const assistantContent = [];
1481
+ if (iterText) {
1482
+ assistantContent.push({ type: "text", text: iterText });
1483
+ }
1484
+ for (const tc of toolCallsThisIteration) {
1485
+ assistantContent.push({ type: "tool_use", id: tc.id, name: tc.name, input: tc.input });
1486
+ }
1487
+ workingMessages.push({ role: "assistant", content: assistantContent });
1488
+ const toolResultContent = [];
1489
+ for (const tc of toolCallsThisIteration) {
1490
+ const toolDef = this.tools.find((t) => t.name === tc.name);
1491
+ const result = toolDef ? await toolDef.execute(tc.input, {
1492
+ sessionId: session.sessionId,
1493
+ store: this.container.store
1494
+ }).catch((err) => ({
1495
+ content: `Tool execution error: ${err instanceof Error ? err.message : String(err)}`,
1496
+ isError: true
1497
+ })) : { content: `Unknown tool: ${tc.name}`, isError: true };
1498
+ yield {
1499
+ type: "tool_result",
1500
+ toolName: tc.name,
1501
+ content: result.content,
1502
+ isError: result.isError ?? false
1503
+ };
1504
+ toolResultContent.push({
1505
+ type: "tool_result",
1506
+ tool_use_id: tc.id,
1507
+ content: result.content,
1508
+ is_error: result.isError
1509
+ });
1510
+ }
1511
+ workingMessages.push({ role: "user", content: toolResultContent });
1512
+ }
1513
+ } catch (err) {
1514
+ if (signal?.aborted) return;
1515
+ throw err;
1516
+ }
1517
+ if (finalText.length > 0) {
1518
+ this.history.push({ role: "user", content: text });
1519
+ this.history.push({ role: "assistant", content: finalText });
1520
+ void this.runPassiveExtraction(text, finalText);
1521
+ }
1522
+ }
1523
+ // Return a snapshot of the conversation history for the UI to render.
1524
+ // Spread-copy so callers can't mutate the engine's internal state.
1525
+ getHistory() {
1526
+ return [...this.history];
1527
+ }
1528
+ // ── Private ─────────────────────────────────────────────────────────────────
1529
+ // Background knowledge extraction — called after every completed turn.
1530
+ // Uses the fast model (haiku) to keep latency and cost low.
1531
+ // Errors are swallowed — extraction failure must never affect the conversation.
1532
+ async runPassiveExtraction(userMessage, assistantMessage) {
1533
+ if (!assistantMessage) return;
1534
+ try {
1535
+ await extractKnowledge(
1536
+ userMessage,
1537
+ assistantMessage,
1538
+ session.sessionId,
1539
+ this.container.store,
1540
+ this.container.provider,
1541
+ this.container.config.models.fast
1542
+ );
1543
+ } catch {
1544
+ }
1545
+ }
1546
+ };
1547
+
1548
+ // src/cli/App.tsx
1549
+ import { useState, useRef } from "react";
1550
+ import { Box as Box3, Text as Text3, useInput, useApp } from "ink";
1551
+
1552
+ // src/cli/components/Message.tsx
1553
+ import { Box, Text } from "ink";
1554
+
1555
+ // src/types/message.ts
1556
+ function messageText(content) {
1557
+ if (typeof content === "string") return content;
1558
+ return content.filter((b) => b.type === "text").map((b) => b.text).join("");
1559
+ }
1560
+
1561
+ // src/cli/components/Message.tsx
1562
+ import { jsx, jsxs } from "react/jsx-runtime";
1563
+ function MessageComponent({ message }) {
1564
+ const isUser = message.role === "user";
1565
+ const text = messageText(message.content);
1566
+ if (!text) return null;
1567
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [
1568
+ /* @__PURE__ */ jsx(Text, { color: isUser ? "cyan" : "green", bold: true, children: isUser ? "you" : "zencefyl" }),
1569
+ /* @__PURE__ */ jsx(Box, { marginLeft: 2, children: /* @__PURE__ */ jsx(Text, { children: text }) })
1570
+ ] });
1571
+ }
1572
+
1573
+ // src/cli/components/StatusBar.tsx
1574
+ import { Box as Box2, Text as Text2 } from "ink";
1575
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
1576
+ function calculateCost(model, inputTokens, outputTokens) {
1577
+ const pricing = MODEL_PRICING[model];
1578
+ if (!pricing) return 0;
1579
+ return inputTokens / 1e6 * pricing.inputPerM + outputTokens / 1e6 * pricing.outputPerM;
1580
+ }
1581
+ function formatCost(usd) {
1582
+ if (usd === 0) return "$0.0000";
1583
+ if (usd < 1e-4) return `$${usd.toExponential(2)}`;
1584
+ return `$${usd.toFixed(4)}`;
1585
+ }
1586
+ function formatTokens(n) {
1587
+ return n.toLocaleString();
1588
+ }
1589
+ function StatusBar({ mode, inputTokens, outputTokens, model }) {
1590
+ const totalTokens = inputTokens + outputTokens;
1591
+ const cost = calculateCost(model, inputTokens, outputTokens);
1592
+ const width = process.stdout.columns ?? 80;
1593
+ return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", marginTop: 1, children: [
1594
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "\u2500".repeat(width) }),
1595
+ /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
1596
+ mode,
1597
+ " \xB7 ",
1598
+ formatTokens(totalTokens),
1599
+ " tok \xB7 ",
1600
+ formatCost(cost)
1601
+ ] })
1602
+ ] });
1603
+ }
1604
+
1605
+ // src/cli/App.tsx
1606
+ import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
1607
+ function App({ engine }) {
1608
+ const { exit } = useApp();
1609
+ const [messages, setMessages] = useState([]);
1610
+ const [inputBuffer, setInputBuffer] = useState("");
1611
+ const [isStreaming, setIsStreaming] = useState(false);
1612
+ const [streamText, setStreamText] = useState("");
1613
+ const [toolEvents, setToolEvents] = useState([]);
1614
+ const [error, setError] = useState(null);
1615
+ const [inputTokens, setInputTokens] = useState(0);
1616
+ const [outputTokens, setOutputTokens] = useState(0);
1617
+ const abortRef = useRef(null);
1618
+ const handleSubmit = async (text) => {
1619
+ const trimmed = text.trim();
1620
+ if (!trimmed) return;
1621
+ if (trimmed === "exit" || trimmed === "quit") {
1622
+ exit();
1623
+ return;
1624
+ }
1625
+ setError(null);
1626
+ setIsStreaming(true);
1627
+ setStreamText("");
1628
+ setToolEvents([]);
1629
+ const controller = new AbortController();
1630
+ abortRef.current = controller;
1631
+ let accumulated = "";
1632
+ try {
1633
+ for await (const delta of engine.sendMessage(trimmed, controller.signal)) {
1634
+ if (delta.type === "text") {
1635
+ accumulated += delta.text;
1636
+ setStreamText(accumulated);
1637
+ }
1638
+ if (delta.type === "tool_use") {
1639
+ setToolEvents((prev) => [...prev, { type: "tool_use", name: delta.name }]);
1640
+ accumulated = "";
1641
+ setStreamText("");
1642
+ }
1643
+ if (delta.type === "tool_result") {
1644
+ setToolEvents((prev) => {
1645
+ const updated = [...prev];
1646
+ const last = updated[updated.length - 1];
1647
+ if (last?.name === delta.toolName && last.type === "tool_use") {
1648
+ updated[updated.length - 1] = {
1649
+ type: "tool_result",
1650
+ name: delta.toolName,
1651
+ isError: delta.isError
1652
+ };
1653
+ }
1654
+ return updated;
1655
+ });
1656
+ }
1657
+ if (delta.type === "usage") {
1658
+ setInputTokens((prev) => prev + delta.inputTokens);
1659
+ setOutputTokens((prev) => prev + delta.outputTokens);
1660
+ }
1661
+ }
1662
+ } catch (err) {
1663
+ if (err instanceof Error && err.name !== "AbortError") {
1664
+ setError(err.message);
1665
+ }
1666
+ } finally {
1667
+ setStreamText("");
1668
+ setToolEvents([]);
1669
+ setIsStreaming(false);
1670
+ abortRef.current = null;
1671
+ setMessages(engine.getHistory());
1672
+ }
1673
+ };
1674
+ useInput((input, key) => {
1675
+ if (isStreaming) {
1676
+ if (key.ctrl && input === "c") {
1677
+ abortRef.current?.abort();
1678
+ }
1679
+ return;
1680
+ }
1681
+ if (key.return) {
1682
+ const text = inputBuffer;
1683
+ setInputBuffer("");
1684
+ handleSubmit(text);
1685
+ return;
1686
+ }
1687
+ if (key.backspace || key.delete) {
1688
+ setInputBuffer((prev) => prev.slice(0, -1));
1689
+ return;
1690
+ }
1691
+ if (key.escape || key.ctrl || key.meta) return;
1692
+ if (input) {
1693
+ setInputBuffer((prev) => prev + input);
1694
+ }
1695
+ });
1696
+ return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", children: [
1697
+ messages.length === 0 && !isStreaming && /* @__PURE__ */ jsxs3(Box3, { marginBottom: 1, children: [
1698
+ /* @__PURE__ */ jsx3(Text3, { color: "green", bold: true, children: "zencefyl" }),
1699
+ /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: " v0.1.0 \xB7 type 'exit' to quit" })
1700
+ ] }),
1701
+ messages.map((msg, i) => /* @__PURE__ */ jsx3(MessageComponent, { message: msg }, i)),
1702
+ isStreaming && /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", marginBottom: 1, children: [
1703
+ /* @__PURE__ */ jsx3(Text3, { color: "green", bold: true, children: "zencefyl" }),
1704
+ toolEvents.map((ev, i) => /* @__PURE__ */ jsxs3(Box3, { marginLeft: 2, children: [
1705
+ ev.type === "tool_use" && /* @__PURE__ */ jsxs3(Text3, { dimColor: true, children: [
1706
+ "[",
1707
+ toolLabel(ev.name),
1708
+ "]"
1709
+ ] }),
1710
+ ev.type === "tool_result" && /* @__PURE__ */ jsxs3(Text3, { color: ev.isError ? "red" : "green", dimColor: true, children: [
1711
+ "[",
1712
+ toolLabel(ev.name),
1713
+ " \u2713]"
1714
+ ] })
1715
+ ] }, i)),
1716
+ /* @__PURE__ */ jsx3(Box3, { marginLeft: 2, children: streamText ? /* @__PURE__ */ jsx3(Text3, { children: streamText }) : /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "thinking..." }) })
1717
+ ] }),
1718
+ error && /* @__PURE__ */ jsx3(Box3, { marginBottom: 1, children: /* @__PURE__ */ jsxs3(Text3, { color: "red", children: [
1719
+ "error: ",
1720
+ error
1721
+ ] }) }),
1722
+ !isStreaming && /* @__PURE__ */ jsxs3(Box3, { children: [
1723
+ /* @__PURE__ */ jsx3(Text3, { color: "cyan", bold: true, children: "\u276F " }),
1724
+ /* @__PURE__ */ jsx3(Text3, { children: inputBuffer }),
1725
+ /* @__PURE__ */ jsx3(Text3, { color: "cyan", children: "\u258C" })
1726
+ ] }),
1727
+ /* @__PURE__ */ jsx3(
1728
+ StatusBar,
1729
+ {
1730
+ mode: "normal",
1731
+ inputTokens,
1732
+ outputTokens,
1733
+ model: session.model
1734
+ }
1735
+ )
1736
+ ] });
1737
+ }
1738
+ function toolLabel(name) {
1739
+ switch (name) {
1740
+ case "read-topic":
1741
+ return "reading knowledge";
1742
+ case "write-topic":
1743
+ return "writing topic";
1744
+ case "log-evidence":
1745
+ return "logging evidence";
1746
+ case "search-topics":
1747
+ return "searching knowledge";
1748
+ default:
1749
+ return `tool: ${name}`;
1750
+ }
1751
+ }
1752
+
1753
+ // src/cli/index.tsx
1754
+ async function main() {
1755
+ let container, engine;
1756
+ try {
1757
+ await runSetupIfNeeded();
1758
+ const config = loadConfig();
1759
+ container = createContainer(config);
1760
+ engine = new Engine(container);
1761
+ } catch (err) {
1762
+ const message = err instanceof Error ? err.message : String(err);
1763
+ process.stderr.write(`
1764
+ Zencefyl failed to start:
1765
+ ${message}
1766
+
1767
+ `);
1768
+ process.exit(1);
1769
+ }
1770
+ render(createElement(App, { engine }));
1771
+ }
1772
+ main();
1773
+ //# sourceMappingURL=index.js.map