zam-core 0.3.11 → 0.3.12

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/cli/index.js CHANGED
@@ -1,14 +1,14 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/cli/index.ts
4
- import { readFileSync as readFileSync11 } from "fs";
5
- import { dirname as dirname10, join as join20 } from "path";
4
+ import { readFileSync as readFileSync13 } from "fs";
5
+ import { dirname as dirname10, join as join21 } from "path";
6
6
  import { fileURLToPath as fileURLToPath5 } from "url";
7
7
  import { Command as Command23 } from "commander";
8
8
 
9
9
  // src/cli/commands/agent.ts
10
- import { existsSync as existsSync10 } from "fs";
11
- import { join as join9 } from "path";
10
+ import { existsSync as existsSync11 } from "fs";
11
+ import { join as join10 } from "path";
12
12
  import { Command } from "commander";
13
13
 
14
14
  // src/kernel/analytics/stats.ts
@@ -1891,11 +1891,11 @@ function analyzeObservation(commands, tokenPatterns) {
1891
1891
  const ratings = [];
1892
1892
  for (const tp of tokenPatterns) {
1893
1893
  const matchIndices = [];
1894
- const matchedTexts = [];
1894
+ const matchedTexts2 = [];
1895
1895
  for (let i = 0; i < commands.length; i++) {
1896
1896
  if (matchesToken(commands[i].command, tp.patterns)) {
1897
1897
  matchIndices.push(i);
1898
- matchedTexts.push(commands[i].command);
1898
+ matchedTexts2.push(commands[i].command);
1899
1899
  matchedSet.add(i);
1900
1900
  }
1901
1901
  }
@@ -1985,7 +1985,7 @@ function analyzeObservation(commands, tokenPatterns) {
1985
1985
  medianGapMs,
1986
1986
  thinkingGapMs
1987
1987
  },
1988
- matchedCommandTexts: matchedTexts
1988
+ matchedCommandTexts: matchedTexts2
1989
1989
  });
1990
1990
  }
1991
1991
  const unmatchedCommands = [];
@@ -2347,6 +2347,240 @@ async function unblockReady(db, userId) {
2347
2347
  return { unblocked };
2348
2348
  }
2349
2349
 
2350
+ // src/kernel/observation/ui-observer-io.ts
2351
+ import { appendFileSync as appendFileSync2, existsSync as existsSync5, mkdirSync as mkdirSync4, readFileSync as readFileSync5 } from "fs";
2352
+ import { homedir as homedir4 } from "os";
2353
+ import { join as join5 } from "path";
2354
+
2355
+ // src/kernel/observation/ui-observer.ts
2356
+ var UI_OBSERVATION_PROTOCOL_VERSION = 1;
2357
+ var OBSERVATION_KINDS = /* @__PURE__ */ new Set([
2358
+ "progress",
2359
+ "step-completed",
2360
+ "error",
2361
+ "help-seeking",
2362
+ "uncertain",
2363
+ "privacy-pause",
2364
+ "heartbeat"
2365
+ ]);
2366
+ var ACTION_TYPES = /* @__PURE__ */ new Set([
2367
+ "click",
2368
+ "shortcut",
2369
+ "typing",
2370
+ "scroll",
2371
+ "window-change"
2372
+ ]);
2373
+ var EVIDENCE_TYPES = /* @__PURE__ */ new Set([
2374
+ "uia",
2375
+ "keyframe",
2376
+ "clip",
2377
+ "window"
2378
+ ]);
2379
+ function isUiObservationReport(value) {
2380
+ if (!isRecord(value)) return false;
2381
+ if (value.version !== UI_OBSERVATION_PROTOCOL_VERSION) return false;
2382
+ if (!isNonEmptyString(value.sessionId)) return false;
2383
+ if (!isNonNegativeInteger(value.sequence)) return false;
2384
+ if (!isNonEmptyString(value.observedFrom)) return false;
2385
+ if (!isNonEmptyString(value.observedTo)) return false;
2386
+ if (typeof value.kind !== "string" || !OBSERVATION_KINDS.has(value.kind)) {
2387
+ return false;
2388
+ }
2389
+ if (!isApplication(value.application)) return false;
2390
+ if (!isNonEmptyString(value.summary)) return false;
2391
+ if (!Array.isArray(value.actions) || !value.actions.every(isObservedAction)) {
2392
+ return false;
2393
+ }
2394
+ if (!Array.isArray(value.evidence) || !value.evidence.every(isEvidenceRef)) {
2395
+ return false;
2396
+ }
2397
+ if (!Array.isArray(value.candidateTokens) || !value.candidateTokens.every(isCandidateToken)) {
2398
+ return false;
2399
+ }
2400
+ return isConfidence(value.confidence);
2401
+ }
2402
+ function parseUiObservationLog(jsonl) {
2403
+ const reports = [];
2404
+ for (const line of jsonl.split("\n")) {
2405
+ const trimmed = line.trim();
2406
+ if (!trimmed) continue;
2407
+ try {
2408
+ const value = JSON.parse(trimmed);
2409
+ if (isUiObservationReport(value)) {
2410
+ reports.push(value);
2411
+ }
2412
+ } catch {
2413
+ }
2414
+ }
2415
+ return reports.sort((left, right) => left.sequence - right.sequence);
2416
+ }
2417
+ function isApplication(value) {
2418
+ if (!isRecord(value) || !isNonEmptyString(value.processName)) return false;
2419
+ if (value.processId !== void 0 && !isNonNegativeInteger(value.processId)) {
2420
+ return false;
2421
+ }
2422
+ return value.windowTitle === void 0 || typeof value.windowTitle === "string";
2423
+ }
2424
+ function isObservedAction(value) {
2425
+ if (!isRecord(value) || typeof value.type !== "string") return false;
2426
+ if (!ACTION_TYPES.has(value.type)) return false;
2427
+ if (value.target !== void 0 && typeof value.target !== "string") {
2428
+ return false;
2429
+ }
2430
+ return value.result === void 0 || typeof value.result === "string";
2431
+ }
2432
+ function isEvidenceRef(value) {
2433
+ if (!isRecord(value) || typeof value.type !== "string") return false;
2434
+ return EVIDENCE_TYPES.has(value.type) && isNonEmptyString(value.ref) && typeof value.redacted === "boolean";
2435
+ }
2436
+ function isCandidateToken(value) {
2437
+ return isRecord(value) && isNonEmptyString(value.slug) && isConfidence(value.confidence) && isNonEmptyString(value.rationale);
2438
+ }
2439
+ function isRecord(value) {
2440
+ return typeof value === "object" && value !== null && !Array.isArray(value);
2441
+ }
2442
+ function isNonEmptyString(value) {
2443
+ return typeof value === "string" && value.trim().length > 0;
2444
+ }
2445
+ function isNonNegativeInteger(value) {
2446
+ return Number.isInteger(value) && Number(value) >= 0;
2447
+ }
2448
+ function isConfidence(value) {
2449
+ return typeof value === "number" && value >= 0 && value <= 1;
2450
+ }
2451
+
2452
+ // src/kernel/observation/ui-observer-io.ts
2453
+ var DEFAULT_UI_OBSERVER_DIR = join5(homedir4(), ".zam", "observer");
2454
+ function getUiObserverDir() {
2455
+ return process.env.ZAM_OBSERVER_DIR || DEFAULT_UI_OBSERVER_DIR;
2456
+ }
2457
+ function getUiObservationPath(sessionId) {
2458
+ if (!/^[A-Za-z0-9_-]+$/.test(sessionId)) {
2459
+ throw new Error(`Invalid observer session ID: ${sessionId}`);
2460
+ }
2461
+ return join5(getUiObserverDir(), `${sessionId}.reports.jsonl`);
2462
+ }
2463
+ function ensureUiObserverDir() {
2464
+ const dir = getUiObserverDir();
2465
+ if (!existsSync5(dir)) {
2466
+ mkdirSync4(dir, { recursive: true, mode: 448 });
2467
+ }
2468
+ }
2469
+ function uiObservationLogExists(sessionId) {
2470
+ return existsSync5(getUiObservationPath(sessionId));
2471
+ }
2472
+ function readUiObservationLog(sessionId) {
2473
+ const path = getUiObservationPath(sessionId);
2474
+ if (!existsSync5(path)) return [];
2475
+ return parseUiObservationLog(readFileSync5(path, "utf8"));
2476
+ }
2477
+ function appendUiObservationReport(report) {
2478
+ if (!isUiObservationReport(report)) {
2479
+ throw new Error("Invalid UI observation report");
2480
+ }
2481
+ ensureUiObserverDir();
2482
+ appendFileSync2(
2483
+ getUiObservationPath(report.sessionId),
2484
+ `${JSON.stringify(report)}
2485
+ `,
2486
+ { encoding: "utf8", mode: 384 }
2487
+ );
2488
+ }
2489
+
2490
+ // src/kernel/observation/ui-observer-synthesis.ts
2491
+ var MIN_UI_CONFIDENCE = 0.6;
2492
+ function kindToRating(kind) {
2493
+ switch (kind) {
2494
+ case "step-completed":
2495
+ return 4;
2496
+ case "progress":
2497
+ return 3;
2498
+ case "error":
2499
+ case "help-seeking":
2500
+ return 2;
2501
+ default:
2502
+ return null;
2503
+ }
2504
+ }
2505
+ function toSynthesisConfidence(confidence) {
2506
+ return confidence >= 0.85 ? "high" : "medium";
2507
+ }
2508
+ function confidenceRank(confidence) {
2509
+ return confidence === "high" ? 2 : 1;
2510
+ }
2511
+ function buildEvidence(report) {
2512
+ return {
2513
+ matchedCommands: report.actions.length,
2514
+ helpSeeking: report.kind === "help-seeking",
2515
+ errorCount: report.kind === "error" ? 1 : 0,
2516
+ selfCorrections: 0,
2517
+ medianGapMs: null,
2518
+ thinkingGapMs: null
2519
+ };
2520
+ }
2521
+ function matchedTexts(report) {
2522
+ const texts = [report.summary];
2523
+ for (const action of report.actions) {
2524
+ const label = [action.type, action.target, action.result].filter(Boolean).join(" ");
2525
+ if (label) texts.push(label);
2526
+ }
2527
+ return texts;
2528
+ }
2529
+ function buildUiSynthesisCandidates(reports, tokens, applied, minConfidence) {
2530
+ const minRank = confidenceRank(minConfidence);
2531
+ const bestBySlug = /* @__PURE__ */ new Map();
2532
+ let skippedLowConfidence = 0;
2533
+ for (const report of reports) {
2534
+ if (report.confidence < MIN_UI_CONFIDENCE) continue;
2535
+ const fallbackRating = kindToRating(report.kind);
2536
+ const candidates2 = report.candidateTokens.length > 0 ? report.candidateTokens.map((candidate) => ({
2537
+ slug: candidate.slug,
2538
+ confidence: candidate.confidence,
2539
+ rating: fallbackRating
2540
+ })) : [];
2541
+ for (const candidate of candidates2) {
2542
+ const token = tokens.get(candidate.slug);
2543
+ if (!token || applied.has(token.id)) continue;
2544
+ const synthesisConfidence = toSynthesisConfidence(candidate.confidence);
2545
+ if (confidenceRank(synthesisConfidence) < minRank) {
2546
+ skippedLowConfidence++;
2547
+ continue;
2548
+ }
2549
+ const inferredRating = candidate.rating ?? 3;
2550
+ const existing = bestBySlug.get(candidate.slug);
2551
+ if (existing && existing.reportConfidence >= candidate.confidence) {
2552
+ continue;
2553
+ }
2554
+ bestBySlug.set(candidate.slug, {
2555
+ token,
2556
+ inferredRating,
2557
+ confidence: synthesisConfidence,
2558
+ evidence: buildEvidence(report),
2559
+ matchedCommandTexts: matchedTexts(report),
2560
+ reportConfidence: candidate.confidence
2561
+ });
2562
+ }
2563
+ }
2564
+ const candidates = [...bestBySlug.values()].sort((left, right) => right.reportConfidence - left.reportConfidence).map((entry) => ({
2565
+ tokenId: entry.token.id,
2566
+ tokenSlug: entry.token.slug,
2567
+ concept: entry.token.concept,
2568
+ domain: entry.token.domain,
2569
+ inferredRating: entry.inferredRating,
2570
+ confidence: entry.confidence,
2571
+ evidence: entry.evidence,
2572
+ matchedCommandTexts: entry.matchedCommandTexts
2573
+ }));
2574
+ return { candidates, skippedLowConfidence };
2575
+ }
2576
+ function uiObservationTimeSpan(reports) {
2577
+ if (reports.length === 0) return null;
2578
+ const start = reports[0].observedFrom;
2579
+ const end = reports[reports.length - 1].observedTo;
2580
+ const durationMs = Math.max(0, Date.parse(end) - Date.parse(start));
2581
+ return { start, end, durationMs };
2582
+ }
2583
+
2350
2584
  // src/kernel/observation/session-synthesis.ts
2351
2585
  function parseSynthesisRow(row) {
2352
2586
  return {
@@ -2354,7 +2588,7 @@ function parseSynthesisRow(row) {
2354
2588
  evidence: JSON.parse(row.evidence)
2355
2589
  };
2356
2590
  }
2357
- function confidenceRank(confidence) {
2591
+ function confidenceRank2(confidence) {
2358
2592
  return confidence === "high" ? 2 : confidence === "medium" ? 1 : 0;
2359
2593
  }
2360
2594
  function normalizeSkillStep(step) {
@@ -2416,20 +2650,50 @@ async function prepareSessionSynthesis(db, input8) {
2416
2650
  validPatterns.push(pattern);
2417
2651
  tokens.set(pattern.slug, token);
2418
2652
  }
2419
- const commands = input8.commands ?? pairCommands(readMonitorLog(input8.sessionId));
2420
- const analysis = analyzeObservation(commands, validPatterns);
2421
2653
  const applied = new Set(
2422
2654
  (await getSessionSynthesisRecords(db, input8.sessionId)).map(
2423
2655
  (record) => record.token_id
2424
2656
  )
2425
2657
  );
2426
- const minRank = confidenceRank(input8.minConfidence ?? "medium");
2658
+ const minConfidence = input8.minConfidence ?? "medium";
2659
+ if (session.execution_context === "ui") {
2660
+ const reports = readUiObservationLog(input8.sessionId);
2661
+ for (const report of reports) {
2662
+ for (const candidate of report.candidateTokens) {
2663
+ if (tokens.has(candidate.slug)) continue;
2664
+ const token = await getTokenBySlug(db, candidate.slug);
2665
+ if (token && !token.deprecated_at) {
2666
+ tokens.set(candidate.slug, token);
2667
+ }
2668
+ }
2669
+ }
2670
+ const { candidates: candidates2, skippedLowConfidence: skippedLowConfidence2 } = buildUiSynthesisCandidates(
2671
+ reports,
2672
+ tokens,
2673
+ applied,
2674
+ minConfidence
2675
+ );
2676
+ return {
2677
+ sessionId: session.id,
2678
+ userId: session.user_id,
2679
+ patternCount: validPatterns.length,
2680
+ commandCount: reports.length,
2681
+ alreadyApplied: applied.size,
2682
+ skippedLowConfidence: skippedLowConfidence2,
2683
+ candidates: candidates2,
2684
+ unmatchedCommands: [],
2685
+ timeSpan: uiObservationTimeSpan(reports)
2686
+ };
2687
+ }
2688
+ const commands = input8.commands ?? pairCommands(readMonitorLog(input8.sessionId));
2689
+ const analysis = analyzeObservation(commands, validPatterns);
2690
+ const minRank = confidenceRank2(minConfidence);
2427
2691
  let skippedLowConfidence = 0;
2428
2692
  const candidates = [];
2429
2693
  for (const rating of analysis.ratings) {
2430
2694
  const token = tokens.get(rating.tokenSlug);
2431
2695
  if (!token || rating.rating == null || applied.has(token.id)) continue;
2432
- if (confidenceRank(rating.confidence) < minRank) {
2696
+ if (confidenceRank2(rating.confidence) < minRank) {
2433
2697
  skippedLowConfidence++;
2434
2698
  continue;
2435
2699
  }
@@ -3026,8 +3290,8 @@ function generatePrompt(input8) {
3026
3290
  }
3027
3291
 
3028
3292
  // src/kernel/recall/reference-resolver.ts
3029
- import { existsSync as existsSync5, readFileSync as readFileSync5 } from "fs";
3030
- import { dirname as dirname3, join as join5, resolve } from "path";
3293
+ import { existsSync as existsSync6, readFileSync as readFileSync6 } from "fs";
3294
+ import { dirname as dirname3, join as join6, resolve } from "path";
3031
3295
  var DEFAULT_REVIEW_CONTEXT_MAX_CHARS = 6e3;
3032
3296
  function htmlToText(html) {
3033
3297
  let content = html;
@@ -3088,11 +3352,11 @@ async function resolveReference(sourceLink) {
3088
3352
  const filePath = anchorIndex2 !== -1 ? fullPathWithAnchor.slice(0, anchorIndex2) : fullPathWithAnchor;
3089
3353
  const anchor2 = anchorIndex2 !== -1 ? fullPathWithAnchor.slice(anchorIndex2) : "";
3090
3354
  const parentDir = dirname3(process.cwd());
3091
- const localRepoPath = join5(parentDir, repo);
3092
- const localFilePath = join5(localRepoPath, filePath);
3093
- if (existsSync5(localFilePath)) {
3355
+ const localRepoPath = join6(parentDir, repo);
3356
+ const localFilePath = join6(localRepoPath, filePath);
3357
+ if (existsSync6(localFilePath)) {
3094
3358
  try {
3095
- let fileContent = readFileSync5(localFilePath, "utf-8");
3359
+ let fileContent = readFileSync6(localFilePath, "utf-8");
3096
3360
  if (anchor2) {
3097
3361
  fileContent = extractLines(fileContent, anchor2);
3098
3362
  }
@@ -3146,9 +3410,9 @@ Link: ${cleaned}`,
3146
3410
  const relativePath = anchorIndex !== -1 ? cleaned.slice(0, anchorIndex) : cleaned;
3147
3411
  const anchor = anchorIndex !== -1 ? cleaned.slice(anchorIndex) : "";
3148
3412
  const absolutePath = resolve(process.cwd(), relativePath);
3149
- if (existsSync5(absolutePath)) {
3413
+ if (existsSync6(absolutePath)) {
3150
3414
  try {
3151
- let fileContent = readFileSync5(absolutePath, "utf-8");
3415
+ let fileContent = readFileSync6(absolutePath, "utf-8");
3152
3416
  if (anchor) {
3153
3417
  fileContent = extractLines(fileContent, anchor);
3154
3418
  }
@@ -3400,44 +3664,44 @@ function intersperseNew(reviews, newCards, interval) {
3400
3664
 
3401
3665
  // src/kernel/system/hooks.ts
3402
3666
  import {
3403
- appendFileSync as appendFileSync2,
3667
+ appendFileSync as appendFileSync3,
3404
3668
  copyFileSync,
3405
- existsSync as existsSync6,
3406
- mkdirSync as mkdirSync4,
3407
- readFileSync as readFileSync6,
3669
+ existsSync as existsSync7,
3670
+ mkdirSync as mkdirSync5,
3671
+ readFileSync as readFileSync7,
3408
3672
  writeFileSync as writeFileSync3
3409
3673
  } from "fs";
3410
- import { homedir as homedir4 } from "os";
3411
- import { join as join6 } from "path";
3674
+ import { homedir as homedir5 } from "os";
3675
+ import { join as join7 } from "path";
3412
3676
  import { fileURLToPath } from "url";
3413
- var HOME = homedir4();
3677
+ var HOME = homedir5();
3414
3678
  function getPackageSkillPath(agent = "default") {
3415
3679
  const packageRoot2 = [
3416
3680
  fileURLToPath(new URL("../../..", import.meta.url)),
3417
3681
  fileURLToPath(new URL("../..", import.meta.url)),
3418
3682
  fileURLToPath(new URL("..", import.meta.url))
3419
- ].find((candidate) => existsSync6(join6(candidate, "package.json"))) ?? "";
3683
+ ].find((candidate) => existsSync7(join7(candidate, "package.json"))) ?? "";
3420
3684
  if (!packageRoot2) return "";
3421
3685
  if (agent === "codex") {
3422
- const codexPath = join6(packageRoot2, ".agents", "skills", "zam", "SKILL.md");
3423
- if (existsSync6(codexPath)) return codexPath;
3686
+ const codexPath = join7(packageRoot2, ".agents", "skills", "zam", "SKILL.md");
3687
+ if (existsSync7(codexPath)) return codexPath;
3424
3688
  return "";
3425
3689
  }
3426
3690
  if (agent === "claude") {
3427
- const claudePath = join6(
3691
+ const claudePath = join7(
3428
3692
  packageRoot2,
3429
3693
  ".claude",
3430
3694
  "skills",
3431
3695
  "zam",
3432
3696
  "SKILL.md"
3433
3697
  );
3434
- if (existsSync6(claudePath)) return claudePath;
3698
+ if (existsSync7(claudePath)) return claudePath;
3435
3699
  return "";
3436
3700
  }
3437
- let path = join6(packageRoot2, ".agent", "skills", "zam", "SKILL.md");
3438
- if (existsSync6(path)) return path;
3439
- path = join6(packageRoot2, ".claude", "skills", "zam", "SKILL.md");
3440
- if (existsSync6(path)) return path;
3701
+ let path = join7(packageRoot2, ".agent", "skills", "zam", "SKILL.md");
3702
+ if (existsSync7(path)) return path;
3703
+ path = join7(packageRoot2, ".claude", "skills", "zam", "SKILL.md");
3704
+ if (existsSync7(path)) return path;
3441
3705
  return "";
3442
3706
  }
3443
3707
  function distributeGlobalSkills(home = HOME) {
@@ -3449,16 +3713,16 @@ function distributeGlobalSkills(home = HOME) {
3449
3713
  console.warn("Could not find ZAM source SKILL.md in the package folder.");
3450
3714
  return results;
3451
3715
  }
3452
- const claudeSkillsDir = join6(home, ".claude", "skills", "zam");
3716
+ const claudeSkillsDir = join7(home, ".claude", "skills", "zam");
3453
3717
  try {
3454
3718
  if (!claudeSourceSkill) {
3455
3719
  throw new Error("Claude skill source not found");
3456
3720
  }
3457
- mkdirSync4(claudeSkillsDir, { recursive: true });
3458
- copyFileSync(claudeSourceSkill, join6(claudeSkillsDir, "SKILL.md"));
3721
+ mkdirSync5(claudeSkillsDir, { recursive: true });
3722
+ copyFileSync(claudeSourceSkill, join7(claudeSkillsDir, "SKILL.md"));
3459
3723
  results.push({
3460
3724
  name: "Claude Code Global",
3461
- path: join6(claudeSkillsDir, "SKILL.md"),
3725
+ path: join7(claudeSkillsDir, "SKILL.md"),
3462
3726
  success: true
3463
3727
  });
3464
3728
  } catch (_err) {
@@ -3468,13 +3732,13 @@ function distributeGlobalSkills(home = HOME) {
3468
3732
  success: false
3469
3733
  });
3470
3734
  }
3471
- const geminiSkillsDir = join6(home, ".gemini", "skills", "zam");
3735
+ const geminiSkillsDir = join7(home, ".gemini", "skills", "zam");
3472
3736
  try {
3473
- mkdirSync4(geminiSkillsDir, { recursive: true });
3474
- copyFileSync(sourceSkill, join6(geminiSkillsDir, "SKILL.md"));
3737
+ mkdirSync5(geminiSkillsDir, { recursive: true });
3738
+ copyFileSync(sourceSkill, join7(geminiSkillsDir, "SKILL.md"));
3475
3739
  results.push({
3476
3740
  name: "Gemini CLI Global",
3477
- path: join6(geminiSkillsDir, "SKILL.md"),
3741
+ path: join7(geminiSkillsDir, "SKILL.md"),
3478
3742
  success: true
3479
3743
  });
3480
3744
  } catch (_err) {
@@ -3484,16 +3748,16 @@ function distributeGlobalSkills(home = HOME) {
3484
3748
  success: false
3485
3749
  });
3486
3750
  }
3487
- const codexSkillsDir = join6(home, ".agents", "skills", "zam");
3751
+ const codexSkillsDir = join7(home, ".agents", "skills", "zam");
3488
3752
  try {
3489
3753
  if (!codexSourceSkill) {
3490
3754
  throw new Error("Codex skill source not found");
3491
3755
  }
3492
- mkdirSync4(codexSkillsDir, { recursive: true });
3493
- copyFileSync(codexSourceSkill, join6(codexSkillsDir, "SKILL.md"));
3756
+ mkdirSync5(codexSkillsDir, { recursive: true });
3757
+ copyFileSync(codexSourceSkill, join7(codexSkillsDir, "SKILL.md"));
3494
3758
  results.push({
3495
3759
  name: "Codex Global",
3496
- path: join6(codexSkillsDir, "SKILL.md"),
3760
+ path: join7(codexSkillsDir, "SKILL.md"),
3497
3761
  success: true
3498
3762
  });
3499
3763
  } catch (_err) {
@@ -3503,13 +3767,13 @@ function distributeGlobalSkills(home = HOME) {
3503
3767
  success: false
3504
3768
  });
3505
3769
  }
3506
- const gooseSkillsDir = join6(home, ".goose", "skills", "zam");
3770
+ const gooseSkillsDir = join7(home, ".goose", "skills", "zam");
3507
3771
  try {
3508
- mkdirSync4(gooseSkillsDir, { recursive: true });
3509
- copyFileSync(sourceSkill, join6(gooseSkillsDir, "SKILL.md"));
3772
+ mkdirSync5(gooseSkillsDir, { recursive: true });
3773
+ copyFileSync(sourceSkill, join7(gooseSkillsDir, "SKILL.md"));
3510
3774
  results.push({
3511
3775
  name: "Goose Global",
3512
- path: join6(gooseSkillsDir, "SKILL.md"),
3776
+ path: join7(gooseSkillsDir, "SKILL.md"),
3513
3777
  success: true
3514
3778
  });
3515
3779
  } catch (_err) {
@@ -3553,14 +3817,14 @@ function Start-ZamMonitor {
3553
3817
  `;
3554
3818
  function installHook(file, hook, oldHook) {
3555
3819
  try {
3556
- const content = existsSync6(file) ? readFileSync6(file, "utf8") : "";
3820
+ const content = existsSync7(file) ? readFileSync7(file, "utf8") : "";
3557
3821
  if (content.includes(HOOK_MARKER)) {
3558
3822
  return { success: true, alreadyHooked: true };
3559
3823
  }
3560
3824
  if (content.includes(oldHook.trim())) {
3561
3825
  writeFileSync3(file, content.replace(oldHook.trim(), hook.trim()), "utf8");
3562
3826
  } else {
3563
- appendFileSync2(file, hook);
3827
+ appendFileSync3(file, hook);
3564
3828
  }
3565
3829
  return { success: true, alreadyHooked: false };
3566
3830
  } catch {
@@ -3569,24 +3833,24 @@ function installHook(file, hook, oldHook) {
3569
3833
  }
3570
3834
  function injectShellHooks(home = HOME) {
3571
3835
  const results = [];
3572
- const zshrc = join6(home, ".zshrc");
3573
- if (existsSync6(zshrc)) {
3836
+ const zshrc = join7(home, ".zshrc");
3837
+ if (existsSync7(zshrc)) {
3574
3838
  const status = installHook(zshrc, posixHook("zsh"), POSIX_OLD_HOOK);
3575
3839
  results.push({ shell: "zsh", file: zshrc, ...status });
3576
3840
  }
3577
- const bashrc = join6(home, ".bashrc");
3578
- if (existsSync6(bashrc)) {
3841
+ const bashrc = join7(home, ".bashrc");
3842
+ if (existsSync7(bashrc)) {
3579
3843
  const status = installHook(bashrc, posixHook("bash"), POSIX_OLD_HOOK);
3580
3844
  results.push({ shell: "bash", file: bashrc, ...status });
3581
3845
  }
3582
3846
  const pwshDirs = [
3583
- join6(home, "Documents", "PowerShell"),
3584
- join6(home, "Documents", "WindowsPowerShell")
3847
+ join7(home, "Documents", "PowerShell"),
3848
+ join7(home, "Documents", "WindowsPowerShell")
3585
3849
  ];
3586
3850
  for (const dir of pwshDirs) {
3587
- const profileFile = join6(dir, "Microsoft.PowerShell_profile.ps1");
3851
+ const profileFile = join7(dir, "Microsoft.PowerShell_profile.ps1");
3588
3852
  try {
3589
- mkdirSync4(dir, { recursive: true });
3853
+ mkdirSync5(dir, { recursive: true });
3590
3854
  const status = installHook(
3591
3855
  profileFile,
3592
3856
  POWERSHELL_HOOK,
@@ -3818,21 +4082,21 @@ function t(locale, key, params = {}) {
3818
4082
  }
3819
4083
 
3820
4084
  // src/kernel/system/install-config.ts
3821
- import { existsSync as existsSync7, mkdirSync as mkdirSync5, readFileSync as readFileSync7, writeFileSync as writeFileSync4 } from "fs";
3822
- import { homedir as homedir5 } from "os";
3823
- import { dirname as dirname4, join as join7 } from "path";
3824
- var DEFAULT_CONFIG_PATH = join7(homedir5(), ".zam", "config.json");
4085
+ import { existsSync as existsSync8, mkdirSync as mkdirSync6, readFileSync as readFileSync8, writeFileSync as writeFileSync4 } from "fs";
4086
+ import { homedir as homedir6 } from "os";
4087
+ import { dirname as dirname4, join as join8 } from "path";
4088
+ var DEFAULT_CONFIG_PATH = join8(homedir6(), ".zam", "config.json");
3825
4089
  function loadInstallConfig(path = DEFAULT_CONFIG_PATH) {
3826
- if (!existsSync7(path)) return {};
4090
+ if (!existsSync8(path)) return {};
3827
4091
  try {
3828
- return JSON.parse(readFileSync7(path, "utf-8"));
4092
+ return JSON.parse(readFileSync8(path, "utf-8"));
3829
4093
  } catch {
3830
4094
  return {};
3831
4095
  }
3832
4096
  }
3833
4097
  function saveInstallConfig(config, path = DEFAULT_CONFIG_PATH) {
3834
4098
  const dir = dirname4(path);
3835
- if (!existsSync7(dir)) mkdirSync5(dir, { recursive: true });
4099
+ if (!existsSync8(dir)) mkdirSync6(dir, { recursive: true });
3836
4100
  writeFileSync4(path, `${JSON.stringify(config, null, 2)}
3837
4101
  `, "utf-8");
3838
4102
  }
@@ -3864,9 +4128,9 @@ function detectSyncProvider(dir) {
3864
4128
 
3865
4129
  // src/kernel/system/installer.ts
3866
4130
  import { execFileSync, execSync } from "child_process";
3867
- import { existsSync as existsSync8 } from "fs";
3868
- import { homedir as homedir6 } from "os";
3869
- import { join as join8 } from "path";
4131
+ import { existsSync as existsSync9 } from "fs";
4132
+ import { homedir as homedir7 } from "os";
4133
+ import { join as join9 } from "path";
3870
4134
  function hasCommand(cmd) {
3871
4135
  try {
3872
4136
  const checkCmd2 = process.platform === "win32" ? `where ${cmd}` : `which ${cmd}`;
@@ -3883,7 +4147,7 @@ function installFastFlowLM() {
3883
4147
  message: "FastFlowLM is only supported on Windows."
3884
4148
  };
3885
4149
  }
3886
- const hasFlm = hasCommand("flm") || existsSync8("C:\\Program Files\\flm\\flm.exe");
4150
+ const hasFlm = hasCommand("flm") || existsSync9("C:\\Program Files\\flm\\flm.exe");
3887
4151
  if (hasFlm) {
3888
4152
  return { success: true, message: "FastFlowLM is already installed." };
3889
4153
  }
@@ -3910,8 +4174,8 @@ function installFastFlowLM() {
3910
4174
  function installOllama() {
3911
4175
  const isMac = process.platform === "darwin";
3912
4176
  const isWin = process.platform === "win32";
3913
- const hasOllama = hasCommand("ollama") || isMac && existsSync8("/Applications/Ollama.app") || isWin && existsSync8(
3914
- join8(homedir6(), "AppData", "Local", "Programs", "Ollama", "ollama.exe")
4177
+ const hasOllama = hasCommand("ollama") || isMac && existsSync9("/Applications/Ollama.app") || isWin && existsSync9(
4178
+ join9(homedir7(), "AppData", "Local", "Programs", "Ollama", "ollama.exe")
3915
4179
  );
3916
4180
  if (hasOllama) {
3917
4181
  return { success: true, message: "Ollama is already installed." };
@@ -3971,8 +4235,8 @@ function installOllama() {
3971
4235
  function resolveOllamaCommand() {
3972
4236
  if (hasCommand("ollama")) return "ollama";
3973
4237
  const candidates = process.platform === "win32" ? [
3974
- join8(
3975
- homedir6(),
4238
+ join9(
4239
+ homedir7(),
3976
4240
  "AppData",
3977
4241
  "Local",
3978
4242
  "Programs",
@@ -3980,7 +4244,7 @@ function resolveOllamaCommand() {
3980
4244
  "ollama.exe"
3981
4245
  )
3982
4246
  ] : process.platform === "darwin" ? ["/Applications/Ollama.app/Contents/Resources/ollama"] : [];
3983
- return candidates.find((candidate) => existsSync8(candidate));
4247
+ return candidates.find((candidate) => existsSync9(candidate));
3984
4248
  }
3985
4249
  function prepareLocalModel(runner, model) {
3986
4250
  if (runner === "fastflowlm") {
@@ -4168,7 +4432,7 @@ function getSystemProfile() {
4168
4432
  }
4169
4433
 
4170
4434
  // src/kernel/system/repos.ts
4171
- import { existsSync as existsSync9 } from "fs";
4435
+ import { existsSync as existsSync10 } from "fs";
4172
4436
  import { resolve as resolve2 } from "path";
4173
4437
  async function getRepoPaths(db) {
4174
4438
  const personalSetting = await getSetting(db, "repo.personal") || await getSetting(db, "personal.workspace_dir");
@@ -4277,7 +4541,7 @@ var C = {
4277
4541
  };
4278
4542
  var SUPPORTED_AGENTS = ["opencode"];
4279
4543
  function agentsMdPresent(cwd = process.cwd()) {
4280
- return existsSync10(join9(cwd, "AGENTS.md"));
4544
+ return existsSync11(join10(cwd, "AGENTS.md"));
4281
4545
  }
4282
4546
  function printStatus() {
4283
4547
  const installed = hasCommand("opencode");
@@ -4319,13 +4583,13 @@ var agentCommand = new Command("agent").description("Provision and inspect the a
4319
4583
 
4320
4584
  // src/cli/commands/bridge.ts
4321
4585
  import { readdirSync as readdirSync2 } from "fs";
4322
- import { homedir as homedir7 } from "os";
4323
- import { join as join10 } from "path";
4586
+ import { homedir as homedir8 } from "os";
4587
+ import { join as join11 } from "path";
4324
4588
  import { Command as Command2 } from "commander";
4325
4589
 
4326
4590
  // src/cli/llm/client.ts
4327
4591
  import { spawn } from "child_process";
4328
- import { existsSync as existsSync11 } from "fs";
4592
+ import { existsSync as existsSync12 } from "fs";
4329
4593
  var DEFAULT_LLM_URL = "http://localhost:8000/v1";
4330
4594
  var DEFAULT_LLM_MODEL = "qwen3.5:4b";
4331
4595
  var DEFAULT_LLM_API_KEY = "sk-none";
@@ -4338,6 +4602,16 @@ async function getLlmConfig(db) {
4338
4602
  locale: await getSetting(db, "system.locale") || "en"
4339
4603
  };
4340
4604
  }
4605
+ async function getVisionConfig(db) {
4606
+ const base = await getLlmConfig(db);
4607
+ return {
4608
+ enabled: await getSetting(db, "llm.vision.enabled") === "true",
4609
+ url: await getSetting(db, "llm.vision.url") || base.url,
4610
+ model: await getSetting(db, "llm.vision.model") || base.model,
4611
+ apiKey: await getSetting(db, "llm.vision.api_key") || base.apiKey,
4612
+ locale: base.locale
4613
+ };
4614
+ }
4341
4615
  var LANGUAGE_NAMES = {
4342
4616
  en: "English",
4343
4617
  de: "German",
@@ -4531,6 +4805,38 @@ async function getAvailableModels(url, apiKey = DEFAULT_LLM_API_KEY) {
4531
4805
  return [];
4532
4806
  }
4533
4807
  }
4808
+ async function checkVisionReadiness(db) {
4809
+ const { enabled, url, model, apiKey } = await getVisionConfig(db);
4810
+ const visionModelSetting = await getSetting(db, "llm.vision.model");
4811
+ const visionModelExplicit = !!visionModelSetting;
4812
+ let online = false;
4813
+ let availableModels = [];
4814
+ let modelAvailable = false;
4815
+ if (enabled) {
4816
+ online = await isLlmOnline(url);
4817
+ if (online) {
4818
+ availableModels = await getAvailableModels(url, apiKey);
4819
+ modelAvailable = availableModels.length === 0 || availableModels.some(
4820
+ (candidate) => candidate.toLowerCase() === model.toLowerCase()
4821
+ );
4822
+ }
4823
+ }
4824
+ let warning;
4825
+ if (enabled && online && modelAvailable && !visionModelExplicit) {
4826
+ warning = `No explicit vision model configured (llm.vision.model). Falling back to base model "${model}", which may not support image input. Set a multimodal model: zam settings set llm.vision.model <model>`;
4827
+ }
4828
+ return {
4829
+ enabled,
4830
+ online,
4831
+ url,
4832
+ model,
4833
+ modelAvailable,
4834
+ availableModels,
4835
+ usable: enabled && online && modelAvailable,
4836
+ visionModelExplicit,
4837
+ warning
4838
+ };
4839
+ }
4534
4840
  function detectRunner(url, model) {
4535
4841
  let runner = "unknown";
4536
4842
  let port = "8000";
@@ -4551,7 +4857,7 @@ function spawnLocalRunner(url, model) {
4551
4857
  const { runner, port } = detectRunner(url, model);
4552
4858
  try {
4553
4859
  if (runner === "fastflowlm") {
4554
- const flmExe = existsSync11("C:\\Program Files\\flm\\flm.exe") ? "C:\\Program Files\\flm\\flm.exe" : "flm";
4860
+ const flmExe = existsSync12("C:\\Program Files\\flm\\flm.exe") ? "C:\\Program Files\\flm\\flm.exe" : "flm";
4555
4861
  if (!hasCommand("flm") && flmExe === "flm") return;
4556
4862
  spawn(flmExe, ["serve", model, "--port", port], {
4557
4863
  detached: true,
@@ -4572,7 +4878,7 @@ function spawnLocalRunner(url, model) {
4572
4878
  async function startLocalRunner(url, model, locale) {
4573
4879
  const { runner, port } = detectRunner(url, model);
4574
4880
  if (runner === "fastflowlm") {
4575
- const flmExe = existsSync11("C:\\Program Files\\flm\\flm.exe") ? "C:\\Program Files\\flm\\flm.exe" : "flm";
4881
+ const flmExe = existsSync12("C:\\Program Files\\flm\\flm.exe") ? "C:\\Program Files\\flm\\flm.exe" : "flm";
4576
4882
  if (!hasCommand("flm") && flmExe === "flm") {
4577
4883
  console.warn(
4578
4884
  "\x1B[31m\u2717 FastFlowLM is configured but could not be found on the system.\x1B[0m"
@@ -4839,6 +5145,266 @@ async function ensureHighQualityQuestion(db, token) {
4839
5145
  return null;
4840
5146
  }
4841
5147
 
5148
+ // src/cli/llm/vision.ts
5149
+ import { readFileSync as readFileSync9 } from "fs";
5150
+ import { basename as basename2 } from "path";
5151
+ var LANGUAGE_NAMES2 = {
5152
+ en: "English",
5153
+ de: "German",
5154
+ es: "Spanish",
5155
+ fr: "French",
5156
+ pt: "Portuguese",
5157
+ zh: "Chinese",
5158
+ ja: "Japanese"
5159
+ };
5160
+ var OBSERVATION_KINDS2 = /* @__PURE__ */ new Set([
5161
+ "progress",
5162
+ "step-completed",
5163
+ "error",
5164
+ "help-seeking",
5165
+ "uncertain",
5166
+ "privacy-pause",
5167
+ "heartbeat"
5168
+ ]);
5169
+ var ACTION_TYPES2 = /* @__PURE__ */ new Set([
5170
+ "click",
5171
+ "shortcut",
5172
+ "typing",
5173
+ "scroll",
5174
+ "window-change"
5175
+ ]);
5176
+ async function observeUiSnapshotViaLLM(db, input8) {
5177
+ const cfg = await getVisionConfig(db);
5178
+ if (!cfg.enabled) {
5179
+ throw new Error(
5180
+ "Vision observation is disabled in settings (llm.vision.enabled)"
5181
+ );
5182
+ }
5183
+ const imageBytes = readFileSync9(input8.imagePath);
5184
+ const imageUrl = `data:image/png;base64,${imageBytes.toString("base64")}`;
5185
+ const model = input8.model ?? cfg.model;
5186
+ const content = await requestVisionDraft({
5187
+ url: cfg.url,
5188
+ apiKey: cfg.apiKey || DEFAULT_LLM_API_KEY,
5189
+ model,
5190
+ locale: cfg.locale,
5191
+ imageUrl,
5192
+ input: input8
5193
+ });
5194
+ let draft;
5195
+ try {
5196
+ draft = extractDraft(content);
5197
+ } catch {
5198
+ return uncertainReport(
5199
+ input8,
5200
+ "Vision model returned output that could not be parsed as JSON."
5201
+ );
5202
+ }
5203
+ const report = buildReport(input8, draft);
5204
+ if (!isUiObservationReport(report)) {
5205
+ return uncertainReport(
5206
+ input8,
5207
+ "Vision model returned an invalid UI report draft."
5208
+ );
5209
+ }
5210
+ return report;
5211
+ }
5212
+ async function requestVisionDraft(args) {
5213
+ const language = LANGUAGE_NAMES2[args.locale] ?? "English";
5214
+ const schema = `{
5215
+ "kind": "progress | step-completed | error | help-seeking | uncertain",
5216
+ "summary": "short factual UI summary in ${language}",
5217
+ "actions": [{"type": "click | shortcut | typing | scroll | window-change", "target": "optional UI target", "result": "optional visible result"}],
5218
+ "candidateTokens": [{"slug": "optional-kebab-case-skill-token", "confidence": 0.0, "rationale": "why this token may matter"}],
5219
+ "confidence": 0.0
5220
+ }`;
5221
+ const res = await fetchWithInteractiveTimeout(
5222
+ `${args.url}/chat/completions`,
5223
+ {
5224
+ method: "POST",
5225
+ headers: {
5226
+ "Content-Type": "application/json",
5227
+ Authorization: `Bearer ${args.apiKey}`
5228
+ },
5229
+ body: JSON.stringify({
5230
+ model: args.model,
5231
+ messages: [
5232
+ {
5233
+ role: "system",
5234
+ content: "You are ZAM's UI observer. Return only strict JSON. Do not include markdown, prose, or fields outside the requested schema."
5235
+ },
5236
+ {
5237
+ role: "user",
5238
+ content: [
5239
+ {
5240
+ type: "text",
5241
+ text: `Observe this Windows application snapshot for a learning session.
5242
+ Application process: ${args.input.application.processName}
5243
+ Window title: ${args.input.application.windowTitle ?? "(unknown)"}
5244
+
5245
+ Return this JSON draft only:
5246
+ ${schema}`
5247
+ },
5248
+ {
5249
+ type: "image_url",
5250
+ image_url: { url: args.imageUrl }
5251
+ }
5252
+ ]
5253
+ }
5254
+ ],
5255
+ temperature: 0,
5256
+ max_tokens: args.input.maxTokens ?? 450
5257
+ }),
5258
+ locale: args.locale,
5259
+ hardTimeoutMs: args.input.hardTimeoutMs ?? 18e4
5260
+ }
5261
+ );
5262
+ if (!res.ok) {
5263
+ const errorText = await res.text().catch(() => "");
5264
+ if (errorText.includes("image") && (errorText.includes("not support") || errorText.includes("unsupported"))) {
5265
+ throw new Error(
5266
+ `Vision model "${args.model}" does not support image input. Set a multimodal model: zam settings set llm.vision.model <model>`
5267
+ );
5268
+ }
5269
+ throw new Error(
5270
+ `Vision LLM request failed: ${res.statusText} (${res.status}) - ${errorText}`
5271
+ );
5272
+ }
5273
+ const data = await res.json();
5274
+ if (data.error !== void 0) {
5275
+ const errorMsg = formatModelError(data.error);
5276
+ if (errorMsg.includes("image") && (errorMsg.includes("not support") || errorMsg.includes("unsupported"))) {
5277
+ throw new Error(
5278
+ `Vision model "${args.model}" does not support image input. Set a multimodal model: zam settings set llm.vision.model <model>`
5279
+ );
5280
+ }
5281
+ throw new Error(`Vision model failed: ${errorMsg}`);
5282
+ }
5283
+ const content = data.choices?.[0]?.message?.content;
5284
+ if (!content || typeof content !== "string") {
5285
+ throw new Error("Empty response from vision model");
5286
+ }
5287
+ return content.trim();
5288
+ }
5289
+ function extractDraft(content) {
5290
+ const fenced = content.match(/```(?:json)?\s*([\s\S]*?)```/i);
5291
+ const candidate = (fenced?.[1] ?? content).trim();
5292
+ try {
5293
+ return JSON.parse(candidate);
5294
+ } catch {
5295
+ const start = candidate.indexOf("{");
5296
+ const end = candidate.lastIndexOf("}");
5297
+ if (start === -1 || end <= start) throw new Error("no JSON object found");
5298
+ return JSON.parse(
5299
+ candidate.slice(start, end + 1)
5300
+ );
5301
+ }
5302
+ }
5303
+ function buildReport(input8, draft) {
5304
+ return {
5305
+ version: UI_OBSERVATION_PROTOCOL_VERSION,
5306
+ sessionId: input8.sessionId,
5307
+ sequence: input8.sequence,
5308
+ observedFrom: input8.observedFrom,
5309
+ observedTo: input8.observedTo,
5310
+ kind: parseKind(draft.kind),
5311
+ application: input8.application,
5312
+ summary: parseSummary(draft.summary),
5313
+ actions: parseActions(draft.actions),
5314
+ evidence: [
5315
+ {
5316
+ type: "keyframe",
5317
+ ref: input8.evidenceRef ?? basename2(input8.imagePath),
5318
+ redacted: input8.redacted ?? false
5319
+ }
5320
+ ],
5321
+ candidateTokens: parseCandidateTokens(draft.candidateTokens),
5322
+ confidence: parseConfidence(draft.confidence, 0.35)
5323
+ };
5324
+ }
5325
+ function uncertainReport(input8, summary) {
5326
+ return {
5327
+ version: UI_OBSERVATION_PROTOCOL_VERSION,
5328
+ sessionId: input8.sessionId,
5329
+ sequence: input8.sequence,
5330
+ observedFrom: input8.observedFrom,
5331
+ observedTo: input8.observedTo,
5332
+ kind: "uncertain",
5333
+ application: input8.application,
5334
+ summary,
5335
+ actions: [],
5336
+ evidence: [
5337
+ {
5338
+ type: "keyframe",
5339
+ ref: input8.evidenceRef ?? basename2(input8.imagePath),
5340
+ redacted: input8.redacted ?? false
5341
+ }
5342
+ ],
5343
+ candidateTokens: [],
5344
+ confidence: 0.1
5345
+ };
5346
+ }
5347
+ function parseKind(value) {
5348
+ if (typeof value === "string" && OBSERVATION_KINDS2.has(value)) {
5349
+ return value;
5350
+ }
5351
+ return "uncertain";
5352
+ }
5353
+ function parseSummary(value) {
5354
+ if (typeof value === "string" && value.trim().length > 0) {
5355
+ return value.trim();
5356
+ }
5357
+ return "Vision model did not provide a factual UI summary.";
5358
+ }
5359
+ function parseActions(value) {
5360
+ if (!Array.isArray(value)) return [];
5361
+ return value.flatMap((item) => {
5362
+ if (!isRecord2(item) || typeof item.type !== "string") return [];
5363
+ if (!ACTION_TYPES2.has(item.type)) return [];
5364
+ const action = { type: item.type };
5365
+ if (typeof item.target === "string" && item.target.trim()) {
5366
+ action.target = item.target.trim();
5367
+ }
5368
+ if (typeof item.result === "string" && item.result.trim()) {
5369
+ action.result = item.result.trim();
5370
+ }
5371
+ return [action];
5372
+ });
5373
+ }
5374
+ function parseCandidateTokens(value) {
5375
+ if (!Array.isArray(value)) return [];
5376
+ return value.flatMap((item) => {
5377
+ if (!isRecord2(item)) return [];
5378
+ if (typeof item.slug !== "string" || !/^[A-Za-z0-9._-]+$/.test(item.slug)) {
5379
+ return [];
5380
+ }
5381
+ if (typeof item.rationale !== "string" || !item.rationale.trim()) {
5382
+ return [];
5383
+ }
5384
+ return [
5385
+ {
5386
+ slug: item.slug,
5387
+ confidence: parseConfidence(item.confidence, 0.2),
5388
+ rationale: item.rationale.trim()
5389
+ }
5390
+ ];
5391
+ });
5392
+ }
5393
+ function parseConfidence(value, fallback) {
5394
+ if (typeof value !== "number" || !Number.isFinite(value)) return fallback;
5395
+ return Math.max(0, Math.min(1, value));
5396
+ }
5397
+ function formatModelError(error) {
5398
+ if (typeof error === "string") return error;
5399
+ if (isRecord2(error) && typeof error.message === "string") {
5400
+ return error.message;
5401
+ }
5402
+ return JSON.stringify(error);
5403
+ }
5404
+ function isRecord2(value) {
5405
+ return typeof value === "object" && value !== null && !Array.isArray(value);
5406
+ }
5407
+
4842
5408
  // src/cli/commands/resolve-user.ts
4843
5409
  async function ensureDefaultUser(db, preferredUserId) {
4844
5410
  const stored = await getSetting(db, "user.id");
@@ -4892,6 +5458,13 @@ function jsonError(message) {
4892
5458
  console.log(JSON.stringify({ error: message }, null, 2));
4893
5459
  process.exit(1);
4894
5460
  }
5461
+ function parseNonNegativeIntegerOption(name, value) {
5462
+ const parsed = Number(value);
5463
+ if (!Number.isInteger(parsed) || parsed < 0) {
5464
+ jsonError(`${name} must be a non-negative integer`);
5465
+ }
5466
+ return parsed;
5467
+ }
4895
5468
  async function withDb2(fn) {
4896
5469
  await withDb(fn, jsonError);
4897
5470
  }
@@ -5131,6 +5704,45 @@ bridgeCommand.command("get-skill").description("Get an agent skill by slug (JSON
5131
5704
  });
5132
5705
  });
5133
5706
  });
5707
+ bridgeCommand.command("start-session").description("Start a ZAM learning session (JSON)").requiredOption("--task <task>", "Session task description").option(
5708
+ "--context <context>",
5709
+ "Execution context: shell | ui | reallife",
5710
+ "shell"
5711
+ ).option("--user <id>", "User ID (default: whoami)").action(async (opts) => {
5712
+ const context = opts.context;
5713
+ if (!["shell", "ui", "reallife"].includes(context)) {
5714
+ jsonError("context must be shell, ui, or reallife");
5715
+ }
5716
+ await withDb2(async (db) => {
5717
+ const userId = await resolveUser(opts, db, { json: true });
5718
+ const session = await startSession(db, {
5719
+ user_id: userId,
5720
+ task: opts.task,
5721
+ execution_context: context
5722
+ });
5723
+ jsonOut2({
5724
+ id: session.id,
5725
+ userId: session.user_id,
5726
+ task: session.task,
5727
+ executionContext: session.execution_context,
5728
+ startedAt: session.started_at,
5729
+ completedAt: session.completed_at
5730
+ });
5731
+ });
5732
+ });
5733
+ bridgeCommand.command("end-session").description("Complete an active ZAM learning session (JSON)").requiredOption("--session <id>", "Session ID").action(async (opts) => {
5734
+ await withDb2(async (db) => {
5735
+ const session = await endSession(db, opts.session);
5736
+ jsonOut2({
5737
+ id: session.id,
5738
+ userId: session.user_id,
5739
+ task: session.task,
5740
+ executionContext: session.execution_context,
5741
+ startedAt: session.started_at,
5742
+ completedAt: session.completed_at
5743
+ });
5744
+ });
5745
+ });
5134
5746
  bridgeCommand.command("get-monitor").description("Read monitor log for a session (JSON)").requiredOption("--session <id>", "Session ID").action((opts) => {
5135
5747
  if (!monitorLogExists(opts.session)) {
5136
5748
  jsonOut2({
@@ -5272,7 +5884,7 @@ bridgeCommand.command("discover-skills").description(
5272
5884
  "20"
5273
5885
  ).action(async (opts) => {
5274
5886
  try {
5275
- const monitorDir = join10(homedir7(), ".zam", "monitor");
5887
+ const monitorDir = join11(homedir8(), ".zam", "monitor");
5276
5888
  let files;
5277
5889
  try {
5278
5890
  files = readdirSync2(monitorDir).filter((f) => f.endsWith(".jsonl"));
@@ -5285,7 +5897,7 @@ bridgeCommand.command("discover-skills").description(
5285
5897
  return;
5286
5898
  }
5287
5899
  const limit = Number(opts.limit);
5288
- const sorted = files.map((f) => ({ name: f, path: join10(monitorDir, f) })).sort((a, b) => b.name.localeCompare(a.name)).slice(0, limit);
5900
+ const sorted = files.map((f) => ({ name: f, path: join11(monitorDir, f) })).sort((a, b) => b.name.localeCompare(a.name)).slice(0, limit);
5289
5901
  const sessionCommands = /* @__PURE__ */ new Map();
5290
5902
  for (const file of sorted) {
5291
5903
  const sessionId = file.name.replace(".jsonl", "");
@@ -5320,6 +5932,81 @@ bridgeCommand.command("discover-skills").description(
5320
5932
  jsonError(err.message);
5321
5933
  }
5322
5934
  });
5935
+ bridgeCommand.command("observe-ui-watch").description(
5936
+ "Poll live UI observer watch reports for a ZAM learning session (JSON)"
5937
+ ).requiredOption(
5938
+ "--session <id>",
5939
+ "ZAM session ID (also the observer log key)"
5940
+ ).option("--after <n>", "Only return observations after this sequence").option("--limit <n>", "Maximum observations to return", "100").action(async (opts) => {
5941
+ await withDb2(async (db) => {
5942
+ const session = await db.prepare("SELECT id, execution_context FROM sessions WHERE id = ?").get(opts.session);
5943
+ if (!session) {
5944
+ jsonError(`Session not found: ${opts.session}`);
5945
+ }
5946
+ const after = opts.after === void 0 ? void 0 : parseNonNegativeIntegerOption("after", opts.after);
5947
+ const limit = parseNonNegativeIntegerOption("limit", opts.limit);
5948
+ const observations = readUiObservationLog(opts.session).filter((report) => after === void 0 || report.sequence > after).slice(0, limit);
5949
+ const last = observations[observations.length - 1];
5950
+ jsonOut2({
5951
+ sessionId: opts.session,
5952
+ executionContext: session.execution_context,
5953
+ observationSource: "ui",
5954
+ logExists: uiObservationLogExists(opts.session),
5955
+ after: after ?? null,
5956
+ count: observations.length,
5957
+ nextSequence: last?.sequence ?? after ?? null,
5958
+ observations
5959
+ });
5960
+ });
5961
+ });
5962
+ bridgeCommand.command("get-observations").description("Read UI observer reports for a session (JSON)").requiredOption("--session <id>", "Observer session ID").option("--after <n>", "Only return observations after this sequence").option("--limit <n>", "Maximum observations to return", "100").action((opts) => {
5963
+ try {
5964
+ const after = opts.after === void 0 ? void 0 : parseNonNegativeIntegerOption("after", opts.after);
5965
+ const limit = parseNonNegativeIntegerOption("limit", opts.limit);
5966
+ const observations = readUiObservationLog(opts.session).filter((report) => after === void 0 || report.sequence > after).slice(0, limit);
5967
+ const last = observations[observations.length - 1];
5968
+ jsonOut2({
5969
+ sessionId: opts.session,
5970
+ after: after ?? null,
5971
+ count: observations.length,
5972
+ nextSequence: last?.sequence ?? after ?? null,
5973
+ observations
5974
+ });
5975
+ } catch (err) {
5976
+ jsonError(err.message);
5977
+ }
5978
+ });
5979
+ bridgeCommand.command("observe-ui-snapshot").description(
5980
+ "Analyze a captured UI snapshot with the configured vision LLM (JSON)"
5981
+ ).requiredOption("--session <id>", "Observer session ID").requiredOption("--sequence <n>", "Monotonic observation sequence number").requiredOption("--image <path>", "PNG snapshot path").requiredOption("--observed-from <iso>", "Observation window start time").requiredOption("--observed-to <iso>", "Observation window end time").requiredOption("--process-name <name>", "Observed application process name").option("--process-id <n>", "Observed application process ID").option("--window-title <title>", "Observed window title").option("--evidence-ref <ref>", "Evidence reference to put in the report").option("--model <model>", "Override configured LLM model for this request").option("--max-tokens <n>", "Model response token budget").option("--timeout <ms>", "Hard request timeout in milliseconds").option("--redacted", "Mark the snapshot evidence as redacted").option("--write-log", "Append the generated report to the session JSONL").action(async (opts) => {
5982
+ const sequence = parseNonNegativeIntegerOption("sequence", opts.sequence);
5983
+ const processId = opts.processId === void 0 ? void 0 : parseNonNegativeIntegerOption("process-id", opts.processId);
5984
+ const maxTokens = opts.maxTokens === void 0 ? void 0 : parseNonNegativeIntegerOption("max-tokens", opts.maxTokens);
5985
+ const hardTimeoutMs = opts.timeout === void 0 ? void 0 : parseNonNegativeIntegerOption("timeout", opts.timeout);
5986
+ await withDb2(async (db) => {
5987
+ const report = await observeUiSnapshotViaLLM(db, {
5988
+ sessionId: opts.session,
5989
+ sequence,
5990
+ observedFrom: opts.observedFrom,
5991
+ observedTo: opts.observedTo,
5992
+ imagePath: opts.image,
5993
+ application: {
5994
+ processName: opts.processName,
5995
+ processId,
5996
+ windowTitle: opts.windowTitle
5997
+ },
5998
+ evidenceRef: opts.evidenceRef,
5999
+ redacted: opts.redacted === true,
6000
+ model: opts.model,
6001
+ maxTokens,
6002
+ hardTimeoutMs
6003
+ });
6004
+ if (opts.writeLog === true) {
6005
+ appendUiObservationReport(report);
6006
+ }
6007
+ jsonOut2(report);
6008
+ });
6009
+ });
5323
6010
  bridgeCommand.command("check-llm").description("Check if LLM is enabled and online (JSON)").action(async () => {
5324
6011
  await withDb2(async (db) => {
5325
6012
  const { enabled, url, model, apiKey } = await getLlmConfig(db);
@@ -5345,6 +6032,13 @@ bridgeCommand.command("check-llm").description("Check if LLM is enabled and onli
5345
6032
  });
5346
6033
  });
5347
6034
  });
6035
+ bridgeCommand.command("check-vision").description(
6036
+ "Check if UI observer vision analysis is enabled and ready (JSON)"
6037
+ ).action(async () => {
6038
+ await withDb2(async (db) => {
6039
+ jsonOut2(await checkVisionReadiness(db));
6040
+ });
6041
+ });
5348
6042
  bridgeCommand.command("ensure-llm").description(
5349
6043
  "Start the local LLM server if needed and report readiness (JSON)"
5350
6044
  ).option(
@@ -5532,7 +6226,7 @@ bridgeCommand.command("get-neighborhood").description(
5532
6226
  bridgeCommand.command("serve").description("Start the persistent JSON-RPC stdin/stdout server").option("--stdin", "Use stdin/stdout for communication").action(async (_opts) => {
5533
6227
  isServeMode = true;
5534
6228
  const {
5535
- appendFileSync: appendFileSync3,
6229
+ appendFileSync: appendFileSync4,
5536
6230
  existsSync: fileExists,
5537
6231
  mkdirSync: makeDir
5538
6232
  } = await import("fs");
@@ -5543,7 +6237,7 @@ bridgeCommand.command("serve").description("Start the persistent JSON-RPC stdin/
5543
6237
  const logDiag = (msg) => {
5544
6238
  try {
5545
6239
  if (!fileExists(logDir)) makeDir(logDir, { recursive: true });
5546
- appendFileSync3(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] ${msg}
6240
+ appendFileSync4(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] ${msg}
5547
6241
  `);
5548
6242
  } catch {
5549
6243
  }
@@ -5994,19 +6688,19 @@ async function setupTurso(urlArg, tokenArg, mode) {
5994
6688
 
5995
6689
  // src/cli/commands/git-sync.ts
5996
6690
  import { execSync as execSync4 } from "child_process";
5997
- import { chmodSync as chmodSync2, existsSync as existsSync12, writeFileSync as writeFileSync5 } from "fs";
5998
- import { join as join11 } from "path";
6691
+ import { chmodSync as chmodSync2, existsSync as existsSync13, writeFileSync as writeFileSync5 } from "fs";
6692
+ import { join as join12 } from "path";
5999
6693
  import { Command as Command5 } from "commander";
6000
6694
  function installHook2() {
6001
- const gitDir = join11(process.cwd(), ".git");
6002
- if (!existsSync12(gitDir)) {
6695
+ const gitDir = join12(process.cwd(), ".git");
6696
+ if (!existsSync13(gitDir)) {
6003
6697
  console.error(
6004
6698
  "Error: Current directory is not the root of a Git repository."
6005
6699
  );
6006
6700
  process.exit(1);
6007
6701
  }
6008
- const hooksDir = join11(gitDir, "hooks");
6009
- const hookPath = join11(hooksDir, "post-commit");
6702
+ const hooksDir = join12(gitDir, "hooks");
6703
+ const hookPath = join12(hooksDir, "post-commit");
6010
6704
  const hookContent = `#!/bin/sh
6011
6705
  # ZAM Spaced Repetition Auto-Stale Hook
6012
6706
  # Triggered automatically on git commits to decay modified concept cards.
@@ -6115,7 +6809,7 @@ ZAM Auto-Stale Complete: Scanned ${changedFiles.length} file(s).`
6115
6809
  });
6116
6810
 
6117
6811
  // src/cli/commands/goal.ts
6118
- import { existsSync as existsSync13, mkdirSync as mkdirSync6 } from "fs";
6812
+ import { existsSync as existsSync14, mkdirSync as mkdirSync7 } from "fs";
6119
6813
  import { resolve as resolve3 } from "path";
6120
6814
  import { input as input2 } from "@inquirer/prompts";
6121
6815
  import { Command as Command6 } from "commander";
@@ -6139,7 +6833,7 @@ goalCommand.command("list").description("List all goals").option(
6139
6833
  "Filter by status (active, completed, paused, abandoned)"
6140
6834
  ).option("--tree", "Show goals as a tree with parent/child relationships").option("--json", "Output as JSON").action(async (opts) => {
6141
6835
  const goalsDir = await resolveGoalsDir();
6142
- if (!existsSync13(goalsDir)) {
6836
+ if (!existsSync14(goalsDir)) {
6143
6837
  console.error(`Goals directory not found: ${goalsDir}`);
6144
6838
  console.error(
6145
6839
  "Set it with: zam settings set personal.goals_dir /path/to/goals"
@@ -6240,8 +6934,8 @@ ${"\u2500".repeat(50)}`);
6240
6934
  });
6241
6935
  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) => {
6242
6936
  const goalsDir = await resolveGoalsDir();
6243
- if (!existsSync13(goalsDir)) {
6244
- mkdirSync6(goalsDir, { recursive: true });
6937
+ if (!existsSync14(goalsDir)) {
6938
+ mkdirSync7(goalsDir, { recursive: true });
6245
6939
  }
6246
6940
  let slug = opts.slug;
6247
6941
  let title = opts.title;
@@ -6300,21 +6994,21 @@ goalCommand.command("status <slug> <status>").description("Update a goal's statu
6300
6994
  });
6301
6995
 
6302
6996
  // src/cli/commands/init.ts
6303
- import { existsSync as existsSync14, mkdirSync as mkdirSync7, writeFileSync as writeFileSync6 } from "fs";
6304
- import { homedir as homedir8 } from "os";
6305
- import { join as join12 } from "path";
6997
+ import { existsSync as existsSync15, mkdirSync as mkdirSync8, writeFileSync as writeFileSync6 } from "fs";
6998
+ import { homedir as homedir9 } from "os";
6999
+ import { join as join13 } from "path";
6306
7000
  import { confirm, input as input3 } from "@inquirer/prompts";
6307
7001
  import { Command as Command7 } from "commander";
6308
- var HOME2 = homedir8();
7002
+ var HOME2 = homedir9();
6309
7003
  function printLine(char = "\u2550", len = 60, color = "\x1B[36m") {
6310
7004
  console.log(`${color}${char.repeat(len)}\x1B[0m`);
6311
7005
  }
6312
7006
  function bootstrapSandboxWorkspace(workspaceDir) {
6313
- mkdirSync7(join12(workspaceDir, "beliefs"), { recursive: true });
6314
- mkdirSync7(join12(workspaceDir, "goals"), { recursive: true });
6315
- mkdirSync7(join12(workspaceDir, "skills"), { recursive: true });
6316
- const worldviewFile = join12(workspaceDir, "beliefs", "worldview.md");
6317
- if (!existsSync14(worldviewFile)) {
7007
+ mkdirSync8(join13(workspaceDir, "beliefs"), { recursive: true });
7008
+ mkdirSync8(join13(workspaceDir, "goals"), { recursive: true });
7009
+ mkdirSync8(join13(workspaceDir, "skills"), { recursive: true });
7010
+ const worldviewFile = join13(workspaceDir, "beliefs", "worldview.md");
7011
+ if (!existsSync15(worldviewFile)) {
6318
7012
  writeFileSync6(
6319
7013
  worldviewFile,
6320
7014
  `# Personal Worldview
@@ -6327,8 +7021,8 @@ Here, I declare the core concepts and principles I want to master.
6327
7021
  "utf8"
6328
7022
  );
6329
7023
  }
6330
- const goalsFile = join12(workspaceDir, "goals", "goals.md");
6331
- if (!existsSync14(goalsFile)) {
7024
+ const goalsFile = join13(workspaceDir, "goals", "goals.md");
7025
+ if (!existsSync15(goalsFile)) {
6332
7026
  writeFileSync6(
6333
7027
  goalsFile,
6334
7028
  `# Personal Goals
@@ -6351,7 +7045,7 @@ var initCommand = new Command7("init").description("Launch the guided interactiv
6351
7045
  );
6352
7046
  printLine();
6353
7047
  console.log("\n\x1B[1m[1/5] Setting up Local Workspace Sandbox\x1B[0m");
6354
- const defaultWorkspace = join12(HOME2, "Documents", "zam");
7048
+ const defaultWorkspace = join13(HOME2, "Documents", "zam");
6355
7049
  const workspacePath = await input3({
6356
7050
  message: "Choose your ZAM workspace directory:",
6357
7051
  default: defaultWorkspace
@@ -7051,7 +7745,7 @@ ${"\u2550".repeat(50)}`);
7051
7745
  import { execFileSync as execFileSync2, execSync as execSync5 } from "child_process";
7052
7746
  import { unlinkSync, writeFileSync as writeFileSync7 } from "fs";
7053
7747
  import { tmpdir } from "os";
7054
- import { basename as basename2, join as join13 } from "path";
7748
+ import { basename as basename3, join as join14 } from "path";
7055
7749
  import { Command as Command9 } from "commander";
7056
7750
  function isPowerShellShell(shell) {
7057
7751
  return shell === "pwsh" || shell === "powershell";
@@ -7073,7 +7767,7 @@ function detectShell() {
7073
7767
  if (process.platform === "win32")
7074
7768
  return findExecutable("pwsh.exe") ? "pwsh" : "powershell";
7075
7769
  const shell = process.env.SHELL ?? "";
7076
- return basename2(shell) === "bash" ? "bash" : "zsh";
7770
+ return basename3(shell) === "bash" ? "bash" : "zsh";
7077
7771
  }
7078
7772
  var monitorCommand = new Command9("monitor").description(
7079
7773
  "Shell observation for real-time task monitoring"
@@ -7216,8 +7910,8 @@ function resolveZamInvocation(shell) {
7216
7910
  if (installed) {
7217
7911
  return isPowerShellShell(shell) ? `& ${psSingleQuoted2(installed)}` : installed;
7218
7912
  }
7219
- const projectRoot = join13(import.meta.dirname, "..", "..", "..");
7220
- const cliSource = join13(projectRoot, "src/cli/index.ts");
7913
+ const projectRoot = join14(import.meta.dirname, "..", "..", "..");
7914
+ const cliSource = join14(projectRoot, "src/cli/index.ts");
7221
7915
  if (isPowerShellShell(shell)) {
7222
7916
  return `& npx --prefix ${psSingleQuoted2(projectRoot)} tsx ${psSingleQuoted2(cliSource)}`;
7223
7917
  }
@@ -7307,7 +8001,7 @@ end tell` : `tell application "Terminal"
7307
8001
  activate
7308
8002
  do script "${escaped}"
7309
8003
  end tell`;
7310
- const tmpFile = join13(tmpdir(), `zam-monitor-${sessionId}.scpt`);
8004
+ const tmpFile = join14(tmpdir(), `zam-monitor-${sessionId}.scpt`);
7311
8005
  try {
7312
8006
  writeFileSync7(tmpFile, appleScript);
7313
8007
  execSync5(`osascript ${JSON.stringify(tmpFile)}`, { stdio: "ignore" });
@@ -7358,8 +8052,8 @@ Run this manually in a new PowerShell terminal:
7358
8052
  }
7359
8053
 
7360
8054
  // src/cli/commands/profile.ts
7361
- import { homedir as homedir9 } from "os";
7362
- import { dirname as dirname5, join as join14, resolve as resolve4 } from "path";
8055
+ import { homedir as homedir10 } from "os";
8056
+ import { dirname as dirname5, join as join15, resolve as resolve4 } from "path";
7363
8057
  import { Command as Command10 } from "commander";
7364
8058
  var C2 = {
7365
8059
  reset: "\x1B[0m",
@@ -7369,7 +8063,7 @@ var C2 = {
7369
8063
  green: "\x1B[32m"
7370
8064
  };
7371
8065
  function defaultPersonalDir() {
7372
- return join14(homedir9(), "Documents", "zam");
8066
+ return join15(homedir10(), "Documents", "zam");
7373
8067
  }
7374
8068
  function render(profile) {
7375
8069
  const sync = profile.syncProvider ? `${C2.green}${profile.syncProvider}${C2.reset} ${C2.dim}(good for cross-device snapshots)${C2.reset}` : `${C2.dim}local folder (use a synced folder or snapshots to move between machines)${C2.reset}`;
@@ -7530,7 +8224,7 @@ ${"\u2550".repeat(50)}`);
7530
8224
  });
7531
8225
 
7532
8226
  // src/cli/commands/session.ts
7533
- import { readFileSync as readFileSync8 } from "fs";
8227
+ import { readFileSync as readFileSync10 } from "fs";
7534
8228
  import { input as input6, select as select2 } from "@inquirer/prompts";
7535
8229
  import { Command as Command12 } from "commander";
7536
8230
  var sessionCommand = new Command12("session").description(
@@ -7711,7 +8405,7 @@ function loadPatternFile(path) {
7711
8405
  if (!path) return [];
7712
8406
  let parsed;
7713
8407
  try {
7714
- parsed = JSON.parse(readFileSync8(path, "utf-8"));
8408
+ parsed = JSON.parse(readFileSync10(path, "utf-8"));
7715
8409
  } catch (err) {
7716
8410
  throw new Error(
7717
8411
  `Cannot read synthesis patterns from ${path}: ${err.message}`
@@ -7901,11 +8595,23 @@ sessionCommand.command("end").description("End a session and show summary").requ
7901
8595
  });
7902
8596
 
7903
8597
  // src/cli/commands/settings.ts
7904
- import { existsSync as existsSync15 } from "fs";
8598
+ import { existsSync as existsSync16 } from "fs";
7905
8599
  import { Command as Command13 } from "commander";
7906
8600
  var settingsCommand = new Command13("settings").description(
7907
8601
  "Manage user settings"
7908
8602
  );
8603
+ var BOOLEAN_SETTING_KEYS = /* @__PURE__ */ new Set(["llm.enabled", "llm.vision.enabled"]);
8604
+ function normalizeSettingValue(key, value) {
8605
+ if (!BOOLEAN_SETTING_KEYS.has(key)) return value;
8606
+ const lower = value.toLowerCase();
8607
+ if (lower === "on" || lower === "enable" || lower === "enabled" || lower === "true") {
8608
+ return "true";
8609
+ }
8610
+ if (lower === "off" || lower === "disable" || lower === "disabled" || lower === "false") {
8611
+ return "false";
8612
+ }
8613
+ return value;
8614
+ }
7909
8615
  settingsCommand.command("show").description("Show all settings").option("--json", "Output as JSON").action(async (opts) => {
7910
8616
  await withDb(async (db) => {
7911
8617
  if (opts.json) {
@@ -7943,15 +8649,7 @@ settingsCommand.command("get <key>").description("Get a single setting").option(
7943
8649
  });
7944
8650
  settingsCommand.command("set <key> <value>").description("Set a setting").option("--quiet", "Suppress output").action(async (key, value, opts) => {
7945
8651
  await withDb(async (db) => {
7946
- let parsedVal = value;
7947
- if (key === "llm.enabled") {
7948
- const lower = value.toLowerCase();
7949
- if (lower === "on" || lower === "enable" || lower === "enabled" || lower === "true") {
7950
- parsedVal = "true";
7951
- } else if (lower === "off" || lower === "disable" || lower === "disabled" || lower === "false") {
7952
- parsedVal = "false";
7953
- }
7954
- }
8652
+ const parsedVal = normalizeSettingValue(key, value);
7955
8653
  await setSetting(db, key, parsedVal);
7956
8654
  if (!opts.quiet) {
7957
8655
  console.log(`Set ${key} = ${parsedVal}`);
@@ -8056,7 +8754,7 @@ settingsCommand.command("repos").description("Show or set Personal, Team, and Or
8056
8754
  console.log("\nValidation:");
8057
8755
  for (const [name, path] of Object.entries(paths)) {
8058
8756
  if (path) {
8059
- const exists = existsSync15(path);
8757
+ const exists = existsSync16(path);
8060
8758
  console.log(
8061
8759
  ` ${name.padEnd(9)}: ${exists ? "\x1B[32m\u2713 Valid folder\x1B[0m" : "\x1B[31m\u2717 Directory does not exist\x1B[0m"}`
8062
8760
  );
@@ -8068,41 +8766,41 @@ settingsCommand.command("repos").description("Show or set Personal, Team, and Or
8068
8766
  });
8069
8767
 
8070
8768
  // src/cli/commands/setup.ts
8071
- import { copyFileSync as copyFileSync2, existsSync as existsSync16, mkdirSync as mkdirSync8, writeFileSync as writeFileSync8 } from "fs";
8072
- import { basename as basename3, dirname as dirname6, join as join15 } from "path";
8769
+ import { copyFileSync as copyFileSync2, existsSync as existsSync17, mkdirSync as mkdirSync9, writeFileSync as writeFileSync8 } from "fs";
8770
+ import { basename as basename4, dirname as dirname6, join as join16 } from "path";
8073
8771
  import { fileURLToPath as fileURLToPath2 } from "url";
8074
8772
  import { Command as Command14 } from "commander";
8075
8773
  var packageRoot = [
8076
8774
  fileURLToPath2(new URL("../..", import.meta.url)),
8077
8775
  fileURLToPath2(new URL("../../..", import.meta.url))
8078
- ].find((candidate) => existsSync16(join15(candidate, "package.json"))) ?? fileURLToPath2(new URL("../..", import.meta.url));
8776
+ ].find((candidate) => existsSync17(join16(candidate, "package.json"))) ?? fileURLToPath2(new URL("../..", import.meta.url));
8079
8777
  var SKILL_PAIRS = [
8080
8778
  {
8081
- from: join15(packageRoot, ".claude", "skills", "zam", "SKILL.md"),
8082
- to: join15(".claude", "skills", "zam", "SKILL.md")
8779
+ from: join16(packageRoot, ".claude", "skills", "zam", "SKILL.md"),
8780
+ to: join16(".claude", "skills", "zam", "SKILL.md")
8083
8781
  },
8084
8782
  {
8085
- from: join15(packageRoot, ".agent", "skills", "zam", "SKILL.md"),
8086
- to: join15(".agent", "skills", "zam", "SKILL.md")
8783
+ from: join16(packageRoot, ".agent", "skills", "zam", "SKILL.md"),
8784
+ to: join16(".agent", "skills", "zam", "SKILL.md")
8087
8785
  },
8088
8786
  {
8089
- from: join15(packageRoot, ".agents", "skills", "zam", "SKILL.md"),
8090
- to: join15(".agents", "skills", "zam", "SKILL.md")
8787
+ from: join16(packageRoot, ".agents", "skills", "zam", "SKILL.md"),
8788
+ to: join16(".agents", "skills", "zam", "SKILL.md")
8091
8789
  }
8092
8790
  ];
8093
8791
  function copySkills(force, cwd = process.cwd()) {
8094
8792
  let anyAction = false;
8095
8793
  for (const { from, to } of SKILL_PAIRS) {
8096
- const dest = join15(cwd, to);
8097
- if (!existsSync16(from)) {
8794
+ const dest = join16(cwd, to);
8795
+ if (!existsSync17(from)) {
8098
8796
  console.warn(` warn source not found, skipping: ${from}`);
8099
8797
  continue;
8100
8798
  }
8101
- if (existsSync16(dest) && !force) {
8799
+ if (existsSync17(dest) && !force) {
8102
8800
  console.log(` skip ${to} (already present \xE2\u20AC\u201D use --force to update)`);
8103
8801
  continue;
8104
8802
  }
8105
- mkdirSync8(dirname6(dest), { recursive: true });
8803
+ mkdirSync9(dirname6(dest), { recursive: true });
8106
8804
  copyFileSync2(from, dest);
8107
8805
  console.log(` copy ${to}`);
8108
8806
  anyAction = true;
@@ -8131,12 +8829,12 @@ async function initDatabase(skipInit) {
8131
8829
  }
8132
8830
  function writeClaudeMd(skipClaudeMd, cwd = process.cwd()) {
8133
8831
  if (skipClaudeMd) return;
8134
- const dest = join15(cwd, "CLAUDE.md");
8135
- if (existsSync16(dest)) {
8832
+ const dest = join16(cwd, "CLAUDE.md");
8833
+ if (existsSync17(dest)) {
8136
8834
  console.log(` skip CLAUDE.md (already present)`);
8137
8835
  return;
8138
8836
  }
8139
- const name = basename3(cwd);
8837
+ const name = basename4(cwd);
8140
8838
  writeFileSync8(
8141
8839
  dest,
8142
8840
  `# ZAM Personal Kernel \xE2\u20AC\u201D ${name}
@@ -8165,12 +8863,12 @@ Use \`zam connector setup turso\` to store cloud credentials in
8165
8863
  }
8166
8864
  function writeAgentsMd(skipAgentsMd, cwd = process.cwd()) {
8167
8865
  if (skipAgentsMd) return;
8168
- const dest = join15(cwd, "AGENTS.md");
8169
- if (existsSync16(dest)) {
8866
+ const dest = join16(cwd, "AGENTS.md");
8867
+ if (existsSync17(dest)) {
8170
8868
  console.log(` skip AGENTS.md (already present)`);
8171
8869
  return;
8172
8870
  }
8173
- const name = basename3(cwd);
8871
+ const name = basename4(cwd);
8174
8872
  writeFileSync8(
8175
8873
  dest,
8176
8874
  `# ZAM Personal Kernel - ${name}
@@ -8311,9 +9009,9 @@ skillCommand.command("add").description("Register a new agent skill").requiredOp
8311
9009
  });
8312
9010
 
8313
9011
  // src/cli/commands/snapshot.ts
8314
- import { existsSync as existsSync17, mkdirSync as mkdirSync9, readFileSync as readFileSync9, writeFileSync as writeFileSync9 } from "fs";
8315
- import { homedir as homedir10 } from "os";
8316
- import { dirname as dirname7, join as join16 } from "path";
9012
+ import { existsSync as existsSync18, mkdirSync as mkdirSync10, readFileSync as readFileSync11, writeFileSync as writeFileSync9 } from "fs";
9013
+ import { homedir as homedir11 } from "os";
9014
+ import { dirname as dirname7, join as join17 } from "path";
8317
9015
  import { Command as Command16 } from "commander";
8318
9016
  function defaultOutName() {
8319
9017
  const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/\.\d+Z$/, "").replace(/:/g, "-");
@@ -8333,7 +9031,7 @@ var exportCmd = new Command16("export").description("Write a portable SQL-text s
8333
9031
  try {
8334
9032
  db = await openDatabaseWithSync({ initialize: true });
8335
9033
  const snapshot = await exportSnapshot(db);
8336
- const personalDir = await getSetting(db, "personal.workspace_dir") || join16(homedir10(), "Documents", "zam");
9034
+ const personalDir = await getSetting(db, "personal.workspace_dir") || join17(homedir11(), "Documents", "zam");
8337
9035
  await db.close();
8338
9036
  db = void 0;
8339
9037
  const manifest = verifySnapshot(snapshot);
@@ -8342,10 +9040,10 @@ var exportCmd = new Command16("export").description("Write a portable SQL-text s
8342
9040
  process.stdout.write(snapshot);
8343
9041
  return;
8344
9042
  }
8345
- const out = opts.out ?? join16(personalDir, "snapshots", defaultOutName());
9043
+ const out = opts.out ?? join17(personalDir, "snapshots", defaultOutName());
8346
9044
  const dir = dirname7(out);
8347
- if (dir && dir !== "." && !existsSync17(dir)) {
8348
- mkdirSync9(dir, { recursive: true });
9045
+ if (dir && dir !== "." && !existsSync18(dir)) {
9046
+ mkdirSync10(dir, { recursive: true });
8349
9047
  }
8350
9048
  writeFileSync9(out, snapshot, "utf-8");
8351
9049
  console.log(`Snapshot written: ${out}`);
@@ -8361,11 +9059,11 @@ var exportCmd = new Command16("export").description("Write a portable SQL-text s
8361
9059
  var importCmd = new Command16("import").description("Restore a snapshot into the database").argument("<file>", "Snapshot file to restore").option("--force", "Overwrite a non-empty database", false).action(async (file, opts) => {
8362
9060
  let db;
8363
9061
  try {
8364
- if (!existsSync17(file)) {
9062
+ if (!existsSync18(file)) {
8365
9063
  console.error(`Error: Snapshot file not found: ${file}`);
8366
9064
  process.exit(1);
8367
9065
  }
8368
- const snapshot = readFileSync9(file, "utf-8");
9066
+ const snapshot = readFileSync11(file, "utf-8");
8369
9067
  db = await openDatabaseWithSync({ initialize: true });
8370
9068
  const result = await importSnapshot(db, snapshot, { force: opts.force });
8371
9069
  await db.close();
@@ -8383,11 +9081,11 @@ var importCmd = new Command16("import").description("Restore a snapshot into the
8383
9081
  });
8384
9082
  var verifyCmd = new Command16("verify").description("Check a snapshot's manifest and checksum without importing").argument("<file>", "Snapshot file to verify").action((file) => {
8385
9083
  try {
8386
- if (!existsSync17(file)) {
9084
+ if (!existsSync18(file)) {
8387
9085
  console.error(`Error: Snapshot file not found: ${file}`);
8388
9086
  process.exit(1);
8389
9087
  }
8390
- const manifest = verifySnapshot(readFileSync9(file, "utf-8"));
9088
+ const manifest = verifySnapshot(readFileSync11(file, "utf-8"));
8391
9089
  const { total, nonEmpty } = summarize(manifest.tables);
8392
9090
  console.log(`Valid snapshot (format v${manifest.version})`);
8393
9091
  console.log(` created: ${manifest.createdAt}`);
@@ -8718,9 +9416,9 @@ tokenCommand.command("status").description("Show full status of a token for a us
8718
9416
 
8719
9417
  // src/cli/commands/ui.ts
8720
9418
  import { spawn as spawn2, spawnSync } from "child_process";
8721
- import { existsSync as existsSync18 } from "fs";
8722
- import { homedir as homedir11 } from "os";
8723
- import { dirname as dirname8, join as join17 } from "path";
9419
+ import { existsSync as existsSync19 } from "fs";
9420
+ import { homedir as homedir12 } from "os";
9421
+ import { dirname as dirname8, join as join18 } from "path";
8724
9422
  import { fileURLToPath as fileURLToPath3 } from "url";
8725
9423
  import { Command as Command19 } from "commander";
8726
9424
  var C3 = {
@@ -8736,8 +9434,8 @@ function findDesktopDir() {
8736
9434
  for (const start of starts) {
8737
9435
  let dir = start;
8738
9436
  for (let i = 0; i < 10; i++) {
8739
- if (existsSync18(join17(dir, "desktop", "src-tauri", "tauri.conf.json"))) {
8740
- return join17(dir, "desktop");
9437
+ if (existsSync19(join18(dir, "desktop", "src-tauri", "tauri.conf.json"))) {
9438
+ return join18(dir, "desktop");
8741
9439
  }
8742
9440
  const parent = dirname8(dir);
8743
9441
  if (parent === dir) break;
@@ -8747,30 +9445,30 @@ function findDesktopDir() {
8747
9445
  return null;
8748
9446
  }
8749
9447
  function findBuiltApp(desktopDir) {
8750
- const releaseDir = join17(desktopDir, "src-tauri", "target", "release");
9448
+ const releaseDir = join18(desktopDir, "src-tauri", "target", "release");
8751
9449
  if (process.platform === "win32") {
8752
9450
  for (const name of ["ZAM.exe", "zam.exe", "zam-desktop.exe"]) {
8753
- const p = join17(releaseDir, name);
8754
- if (existsSync18(p)) return p;
9451
+ const p = join18(releaseDir, name);
9452
+ if (existsSync19(p)) return p;
8755
9453
  }
8756
9454
  } else if (process.platform === "darwin") {
8757
- const app = join17(releaseDir, "bundle", "macos", "ZAM.app");
8758
- if (existsSync18(app)) return app;
9455
+ const app = join18(releaseDir, "bundle", "macos", "ZAM.app");
9456
+ if (existsSync19(app)) return app;
8759
9457
  } else {
8760
9458
  for (const name of ["zam", "ZAM", "zam-desktop"]) {
8761
- const p = join17(releaseDir, name);
8762
- if (existsSync18(p)) return p;
9459
+ const p = join18(releaseDir, name);
9460
+ if (existsSync19(p)) return p;
8763
9461
  }
8764
9462
  }
8765
9463
  return null;
8766
9464
  }
8767
9465
  function findInstalledApp() {
8768
9466
  const candidates = process.platform === "win32" ? [
8769
- process.env.LOCALAPPDATA && join17(process.env.LOCALAPPDATA, "Programs", "ZAM", "ZAM.exe"),
8770
- process.env.ProgramFiles && join17(process.env.ProgramFiles, "ZAM", "ZAM.exe"),
8771
- process.env["ProgramFiles(x86)"] && join17(process.env["ProgramFiles(x86)"], "ZAM", "ZAM.exe")
8772
- ] : process.platform === "darwin" ? ["/Applications/ZAM.app", join17(homedir11(), "Applications", "ZAM.app")] : ["/opt/ZAM/zam", "/usr/bin/zam-desktop"];
8773
- return candidates.find((candidate) => candidate && existsSync18(candidate)) || null;
9467
+ process.env.LOCALAPPDATA && join18(process.env.LOCALAPPDATA, "Programs", "ZAM", "ZAM.exe"),
9468
+ process.env.ProgramFiles && join18(process.env.ProgramFiles, "ZAM", "ZAM.exe"),
9469
+ process.env["ProgramFiles(x86)"] && join18(process.env["ProgramFiles(x86)"], "ZAM", "ZAM.exe")
9470
+ ] : process.platform === "darwin" ? ["/Applications/ZAM.app", join18(homedir12(), "Applications", "ZAM.app")] : ["/opt/ZAM/zam", "/usr/bin/zam-desktop"];
9471
+ return candidates.find((candidate) => candidate && existsSync19(candidate)) || null;
8774
9472
  }
8775
9473
  function runNpm(args, opts) {
8776
9474
  const res = spawnSync("npm", args, {
@@ -8781,7 +9479,7 @@ function runNpm(args, opts) {
8781
9479
  return res.status ?? 1;
8782
9480
  }
8783
9481
  function ensureDesktopDeps(desktopDir) {
8784
- if (existsSync18(join17(desktopDir, "node_modules"))) return true;
9482
+ if (existsSync19(join18(desktopDir, "node_modules"))) return true;
8785
9483
  console.log(
8786
9484
  `${C3.cyan}Installing desktop dependencies (one-time)...${C3.reset}`
8787
9485
  );
@@ -8807,13 +9505,13 @@ function requireRust() {
8807
9505
  function hasMsvcBuildTools() {
8808
9506
  if (process.platform !== "win32") return true;
8809
9507
  const pf86 = process.env["ProgramFiles(x86)"] || "C:\\Program Files (x86)";
8810
- const vswhere = join17(
9508
+ const vswhere = join18(
8811
9509
  pf86,
8812
9510
  "Microsoft Visual Studio",
8813
9511
  "Installer",
8814
9512
  "vswhere.exe"
8815
9513
  );
8816
- if (!existsSync18(vswhere)) return false;
9514
+ if (!existsSync19(vswhere)) return false;
8817
9515
  const res = spawnSync(
8818
9516
  vswhere,
8819
9517
  [
@@ -8845,7 +9543,7 @@ function requireMsvcOnWindows() {
8845
9543
  return false;
8846
9544
  }
8847
9545
  function warnIfCliMissing(repoRoot) {
8848
- if (!existsSync18(join17(repoRoot, "dist", "cli", "index.js"))) {
9546
+ if (!existsSync19(join18(repoRoot, "dist", "cli", "index.js"))) {
8849
9547
  console.warn(
8850
9548
  `${C3.yellow}\u26A0 CLI build not found (dist/cli/index.js). The GUI bridge needs it \u2014 run 'npm run build' at the repo root.${C3.reset}`
8851
9549
  );
@@ -8908,7 +9606,7 @@ var uiCommand = new Command19("ui").description("Launch the ZAM Desktop GUI (Act
8908
9606
  ).option("--shortcut", "Create Desktop + Start-menu shortcuts to the GUI").action((opts) => {
8909
9607
  const installedApp = findInstalledApp();
8910
9608
  if (!opts.dev && !opts.build && !opts.shortcut && installedApp) {
8911
- launchApp(installedApp, homedir11());
9609
+ launchApp(installedApp, homedir12());
8912
9610
  return;
8913
9611
  }
8914
9612
  const desktopDir = findDesktopDir();
@@ -8929,7 +9627,7 @@ var uiCommand = new Command19("ui").description("Launch the ZAM Desktop GUI (Act
8929
9627
  );
8930
9628
  const code = runNpm(["run", "tauri", "build"], { cwd: desktopDir });
8931
9629
  if (code === 0) {
8932
- const bundle = join17(
9630
+ const bundle = join18(
8933
9631
  desktopDir,
8934
9632
  "src-tauri",
8935
9633
  "target",
@@ -9005,8 +9703,8 @@ ${C3.dim}After that, 'zam ui' launches it directly.${C3.reset}`
9005
9703
  });
9006
9704
 
9007
9705
  // src/cli/commands/update.ts
9008
- import { readFileSync as readFileSync10 } from "fs";
9009
- import { dirname as dirname9, join as join18 } from "path";
9706
+ import { readFileSync as readFileSync12 } from "fs";
9707
+ import { dirname as dirname9, join as join19 } from "path";
9010
9708
  import { fileURLToPath as fileURLToPath4 } from "url";
9011
9709
  import { Command as Command20 } from "commander";
9012
9710
  var GITHUB_REPO = "zam-os/zam";
@@ -9029,7 +9727,7 @@ function currentVersion() {
9029
9727
  for (const up of ["..", "../..", "../../.."]) {
9030
9728
  try {
9031
9729
  const pkg2 = JSON.parse(
9032
- readFileSync10(join18(here, up, "package.json"), "utf-8")
9730
+ readFileSync12(join19(here, up, "package.json"), "utf-8")
9033
9731
  );
9034
9732
  if (pkg2.version) return pkg2.version;
9035
9733
  } catch {
@@ -9147,8 +9845,8 @@ var whoamiCommand = new Command21("whoami").description("Show or set the default
9147
9845
 
9148
9846
  // src/cli/commands/workspace.ts
9149
9847
  import { execSync as execSync6 } from "child_process";
9150
- import { existsSync as existsSync19, writeFileSync as writeFileSync10 } from "fs";
9151
- import { join as join19 } from "path";
9848
+ import { existsSync as existsSync20, writeFileSync as writeFileSync10 } from "fs";
9849
+ import { join as join20 } from "path";
9152
9850
  import { confirm as confirm3, input as input7 } from "@inquirer/prompts";
9153
9851
  import { Command as Command22 } from "commander";
9154
9852
  function runGit(cwd, args) {
@@ -9181,7 +9879,7 @@ workspaceCommand.command("publish").description("Publish your local workspace sa
9181
9879
  );
9182
9880
  process.exit(1);
9183
9881
  }
9184
- if (!existsSync19(workspaceDir)) {
9882
+ if (!existsSync20(workspaceDir)) {
9185
9883
  console.error(
9186
9884
  `\x1B[31m\u2717 Workspace directory does not exist: ${workspaceDir}\x1B[0m`
9187
9885
  );
@@ -9195,15 +9893,15 @@ Active workspace: \x1B[36m${workspaceDir}\x1B[0m`);
9195
9893
  );
9196
9894
  process.exit(1);
9197
9895
  }
9198
- const gitignorePath = join19(workspaceDir, ".gitignore");
9199
- if (!existsSync19(gitignorePath)) {
9896
+ const gitignorePath = join20(workspaceDir, ".gitignore");
9897
+ if (!existsSync20(gitignorePath)) {
9200
9898
  writeFileSync10(
9201
9899
  gitignorePath,
9202
9900
  "node_modules/\n.agent/\n.agents/\n.claude/\n.gemini/\n.goose/\n*.log\n",
9203
9901
  "utf8"
9204
9902
  );
9205
9903
  }
9206
- const hasGitRepo = existsSync19(join19(workspaceDir, ".git"));
9904
+ const hasGitRepo = existsSync20(join20(workspaceDir, ".git"));
9207
9905
  if (!hasGitRepo) {
9208
9906
  console.log("Initializing local Git repository...");
9209
9907
  runGit(workspaceDir, "init -b main");
@@ -9299,7 +9997,7 @@ Active workspace: \x1B[36m${workspaceDir}\x1B[0m`);
9299
9997
  // src/cli/index.ts
9300
9998
  var __dirname = dirname10(fileURLToPath5(import.meta.url));
9301
9999
  var pkg = JSON.parse(
9302
- readFileSync11(join20(__dirname, "..", "..", "package.json"), "utf-8")
10000
+ readFileSync13(join21(__dirname, "..", "..", "package.json"), "utf-8")
9303
10001
  );
9304
10002
  var program = new Command23();
9305
10003
  program.name("zam").description(