zam-core 0.3.13 → 0.4.1

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 readFileSync13 } from "fs";
5
- import { dirname as dirname10, join as join21 } from "path";
4
+ import { readFileSync as readFileSync14 } from "fs";
5
+ import { dirname as dirname10, join as join22 } from "path";
6
6
  import { fileURLToPath as fileURLToPath5 } from "url";
7
- import { Command as Command23 } from "commander";
7
+ import { Command as Command24 } from "commander";
8
8
 
9
9
  // src/cli/commands/agent.ts
10
10
  import { existsSync as existsSync11 } from "fs";
11
- import { join as join10 } from "path";
11
+ import { join as join11 } from "path";
12
12
  import { Command } from "commander";
13
13
 
14
14
  // src/kernel/analytics/stats.ts
@@ -703,6 +703,66 @@ function isRemoteDatabasePath(dbPath) {
703
703
  function isDatabaseProvider(value) {
704
704
  return value === "local" || value === "native" || value === "remote";
705
705
  }
706
+ function cwdRequiresTursoCredentials() {
707
+ try {
708
+ const configPath = join2(process.cwd(), ".zam", "config.yaml");
709
+ if (existsSync2(configPath)) {
710
+ const configText = readFileSync2(configPath, "utf-8");
711
+ return /[\s\S]*turso:[\s\S]*url:/m.test(configText);
712
+ }
713
+ } catch (_e) {
714
+ }
715
+ return false;
716
+ }
717
+ function resolveDatabaseTarget(options = {}) {
718
+ const configuredCloud = options.useConfiguredCloud !== false && !options.dbPath && !options.syncUrl ? getTursoCredentials() : null;
719
+ if (cwdRequiresTursoCredentials() && !configuredCloud && options.useConfiguredCloud !== false && !options.dbPath && !options.syncUrl) {
720
+ throw new Error(
721
+ "Turso cloud database is configured in .zam/config.yaml but missing local credentials. Run: zam connector setup turso"
722
+ );
723
+ }
724
+ const dbPath = configuredCloud?.url ?? options.dbPath ?? DEFAULT_DB_PATH;
725
+ const isRemote = isRemoteDatabasePath(dbPath);
726
+ const isEmbeddedReplica = Boolean(options.syncUrl);
727
+ const provider = resolveProvider(options, configuredCloud?.mode, isRemote);
728
+ return {
729
+ dbPath,
730
+ provider,
731
+ isRemote,
732
+ isEmbeddedReplica,
733
+ configuredCloud
734
+ };
735
+ }
736
+ function getDatabaseTargetInfo(options = {}) {
737
+ const target = resolveDatabaseTarget(options);
738
+ if (target.isEmbeddedReplica) {
739
+ return {
740
+ kind: "turso-replica",
741
+ provider: target.provider,
742
+ location: target.dbPath,
743
+ syncUrl: options.syncUrl
744
+ };
745
+ }
746
+ if (target.provider === "remote" && target.isRemote) {
747
+ return {
748
+ kind: "turso-remote",
749
+ provider: target.provider,
750
+ location: target.dbPath
751
+ };
752
+ }
753
+ if (target.isRemote) {
754
+ return {
755
+ kind: "turso-native",
756
+ provider: target.provider,
757
+ location: target.dbPath
758
+ };
759
+ }
760
+ return {
761
+ kind: "local",
762
+ provider: target.provider,
763
+ location: target.dbPath
764
+ };
765
+ }
706
766
  function openLocalSqlite(dbPath) {
707
767
  const mod = require2("better-sqlite3");
708
768
  const BetterSqlite3 = "default" in mod ? mod.default : mod;
@@ -720,27 +780,7 @@ function loadLibsql() {
720
780
  }
721
781
  }
722
782
  async function openDatabase(options = {}) {
723
- const configuredCloud = options.useConfiguredCloud !== false && !options.dbPath && !options.syncUrl ? getTursoCredentials() : null;
724
- let requiresTurso = false;
725
- try {
726
- const configPath = join2(process.cwd(), ".zam", "config.yaml");
727
- if (existsSync2(configPath)) {
728
- const configText = readFileSync2(configPath, "utf-8");
729
- if (/[\s\S]*turso:[\s\S]*url:/m.test(configText)) {
730
- requiresTurso = true;
731
- }
732
- }
733
- } catch (_e) {
734
- }
735
- if (requiresTurso && !configuredCloud && options.useConfiguredCloud !== false && !options.dbPath && !options.syncUrl) {
736
- throw new Error(
737
- "Turso cloud database is configured in .zam/config.yaml but missing local credentials. Run: zam connector setup turso"
738
- );
739
- }
740
- const dbPath = configuredCloud?.url ?? options.dbPath ?? DEFAULT_DB_PATH;
741
- const isRemote = isRemoteDatabasePath(dbPath);
742
- const isEmbeddedReplica = Boolean(options.syncUrl);
743
- const provider = resolveProvider(options, configuredCloud?.mode, isRemote);
783
+ const { dbPath, provider, isRemote, isEmbeddedReplica, configuredCloud } = resolveDatabaseTarget(options);
744
784
  const shouldInitialize = options.initialize === true || !isRemote && !isEmbeddedReplica && !existsSync2(dbPath);
745
785
  if (provider === "remote") {
746
786
  const url = isRemote ? dbPath : options.syncUrl;
@@ -2086,6 +2126,415 @@ function getMonitorLogStats(sessionId) {
2086
2126
  return { exists: true, sizeBytes: stat.size, lineCount };
2087
2127
  }
2088
2128
 
2129
+ // src/kernel/observation/observer-sidecar-policy.ts
2130
+ import { mkdirSync as mkdirSync5, writeFileSync as writeFileSync3 } from "fs";
2131
+ import { join as join6 } from "path";
2132
+
2133
+ // src/kernel/observation/policy.ts
2134
+ var OBSERVER_POLICY_VERSION = 1;
2135
+ var DEFAULT_OBSERVER_POLICY = {
2136
+ version: OBSERVER_POLICY_VERSION,
2137
+ scope: "window",
2138
+ allowlist: [],
2139
+ denylist: [],
2140
+ consent: "per-session",
2141
+ retention: "none",
2142
+ redactWindowTitles: true,
2143
+ audioOptIn: false
2144
+ };
2145
+ var BUILT_IN_SENSITIVE_MATCHERS = [
2146
+ // Password managers
2147
+ "1password",
2148
+ "bitwarden",
2149
+ "keepass",
2150
+ "lastpass",
2151
+ "dashlane",
2152
+ "nordpass",
2153
+ "enpass",
2154
+ "proton pass",
2155
+ // Authentication / credential / UAC surfaces
2156
+ "credentialuibroker",
2157
+ "consentux",
2158
+ "logonui",
2159
+ "windowssecurity",
2160
+ "authenticator",
2161
+ // Conservative banking/title hints
2162
+ "online banking",
2163
+ "onlinebanking"
2164
+ ];
2165
+ function parseList(raw) {
2166
+ if (!raw) return [];
2167
+ return raw.split(",").map((entry) => entry.trim().toLowerCase()).filter((entry) => entry.length > 0);
2168
+ }
2169
+ function parseObserverList(raw) {
2170
+ return parseList(raw);
2171
+ }
2172
+ function parseScope(raw, defaultScope) {
2173
+ if (raw === "off" || raw === "window" || raw === "fullscreen") {
2174
+ return raw;
2175
+ }
2176
+ return defaultScope ?? DEFAULT_OBSERVER_POLICY.scope;
2177
+ }
2178
+ function parseConsent(raw, defaultConsent) {
2179
+ if (raw === "per-capture" || raw === "per-session" || raw === "standing") {
2180
+ return raw;
2181
+ }
2182
+ return defaultConsent ?? DEFAULT_OBSERVER_POLICY.consent;
2183
+ }
2184
+ function parseRetention(raw) {
2185
+ return raw === "none" || raw === "session" || raw === "persist" ? raw : DEFAULT_OBSERVER_POLICY.retention;
2186
+ }
2187
+ function parseBool(raw, fallback) {
2188
+ if (raw === void 0) return fallback;
2189
+ return raw === "true";
2190
+ }
2191
+ function parseObserverPolicy(raw, defaults) {
2192
+ return {
2193
+ version: OBSERVER_POLICY_VERSION,
2194
+ scope: parseScope(raw["observer.scope"], defaults?.scope),
2195
+ allowlist: parseList(raw["observer.allowlist"]),
2196
+ denylist: parseList(raw["observer.denylist"]),
2197
+ consent: parseConsent(raw["observer.consent"], defaults?.consent),
2198
+ retention: parseRetention(raw["observer.retention"]),
2199
+ redactWindowTitles: parseBool(
2200
+ raw["observer.redact_titles"],
2201
+ DEFAULT_OBSERVER_POLICY.redactWindowTitles
2202
+ ),
2203
+ audioOptIn: parseBool(
2204
+ raw["observer.audio"],
2205
+ DEFAULT_OBSERVER_POLICY.audioOptIn
2206
+ )
2207
+ };
2208
+ }
2209
+ function getDefaultsForSymbiosisMode(mode) {
2210
+ if (mode === "autonomy") {
2211
+ return { scope: "fullscreen", consent: "standing" };
2212
+ }
2213
+ return { scope: "window", consent: "per-session" };
2214
+ }
2215
+ async function resolveActiveSymbiosisMode(db) {
2216
+ const userId = await getSetting(db, "user.id") || "default";
2217
+ const activeSession = await db.prepare(
2218
+ "SELECT id FROM sessions WHERE user_id = ? AND completed_at IS NULL ORDER BY started_at DESC LIMIT 1"
2219
+ ).get(userId);
2220
+ if (activeSession) {
2221
+ const stepToken = await db.prepare(
2222
+ `SELECT t.symbiosis_mode, t.domain
2223
+ FROM session_steps s
2224
+ JOIN tokens t ON t.id = s.token_id
2225
+ WHERE s.session_id = ?
2226
+ ORDER BY s.created_at DESC, s.id DESC
2227
+ LIMIT 1`
2228
+ ).get(activeSession.id);
2229
+ if (stepToken) {
2230
+ if (stepToken.symbiosis_mode) {
2231
+ return stepToken.symbiosis_mode;
2232
+ }
2233
+ if (stepToken.domain) {
2234
+ const competence = await getDomainCompetence(db, userId);
2235
+ const comp = competence.find((c) => c.domain === stepToken.domain);
2236
+ if (comp) {
2237
+ return comp.suggestedMode;
2238
+ }
2239
+ }
2240
+ }
2241
+ }
2242
+ const lastReviewToken = await db.prepare(
2243
+ `SELECT t.symbiosis_mode, t.domain
2244
+ FROM review_logs r
2245
+ JOIN tokens t ON t.id = r.token_id
2246
+ WHERE r.user_id = ?
2247
+ ORDER BY r.reviewed_at DESC, r.id DESC
2248
+ LIMIT 1`
2249
+ ).get(userId);
2250
+ if (lastReviewToken) {
2251
+ if (lastReviewToken.symbiosis_mode) {
2252
+ return lastReviewToken.symbiosis_mode;
2253
+ }
2254
+ if (lastReviewToken.domain) {
2255
+ const competence = await getDomainCompetence(db, userId);
2256
+ const comp = competence.find((c) => c.domain === lastReviewToken.domain);
2257
+ if (comp) {
2258
+ return comp.suggestedMode;
2259
+ }
2260
+ }
2261
+ }
2262
+ return "shadowing";
2263
+ }
2264
+ async function resolveObserverPolicy(db) {
2265
+ const [scope, allowlist, denylist, consent, retention, redactTitles, audio] = await Promise.all([
2266
+ getSetting(db, "observer.scope"),
2267
+ getSetting(db, "observer.allowlist"),
2268
+ getSetting(db, "observer.denylist"),
2269
+ getSetting(db, "observer.consent"),
2270
+ getSetting(db, "observer.retention"),
2271
+ getSetting(db, "observer.redact_titles"),
2272
+ getSetting(db, "observer.audio")
2273
+ ]);
2274
+ const mode = await resolveActiveSymbiosisMode(db);
2275
+ const defaults = getDefaultsForSymbiosisMode(mode);
2276
+ return parseObserverPolicy(
2277
+ {
2278
+ "observer.scope": scope,
2279
+ "observer.allowlist": allowlist,
2280
+ "observer.denylist": denylist,
2281
+ "observer.consent": consent,
2282
+ "observer.retention": retention,
2283
+ "observer.redact_titles": redactTitles,
2284
+ "observer.audio": audio
2285
+ },
2286
+ defaults
2287
+ );
2288
+ }
2289
+ function haystacks(...values) {
2290
+ return values.filter((v) => typeof v === "string" && v.length > 0).map((v) => v.toLowerCase());
2291
+ }
2292
+ function deny(reason, denialReason) {
2293
+ return { allowed: false, reason, denialReason };
2294
+ }
2295
+ function matchBuiltInSensitive(processName, windowTitle) {
2296
+ const fields = haystacks(processName, windowTitle);
2297
+ for (const matcher of BUILT_IN_SENSITIVE_MATCHERS) {
2298
+ if (fields.some((field) => field.includes(matcher))) return matcher;
2299
+ }
2300
+ return null;
2301
+ }
2302
+ function matchDenylist(policy, processName, windowTitle) {
2303
+ const fields = haystacks(processName, windowTitle);
2304
+ for (const matcher of policy.denylist) {
2305
+ if (fields.some((field) => field.includes(matcher))) return matcher;
2306
+ }
2307
+ return null;
2308
+ }
2309
+ function inAllowlist(policy, processName) {
2310
+ if (policy.allowlist.length === 0) return true;
2311
+ if (!processName) return false;
2312
+ const name = processName.toLowerCase();
2313
+ return policy.allowlist.some((entry) => name.includes(entry));
2314
+ }
2315
+ function decidePreCapture(policy, request) {
2316
+ if (policy.scope === "off") {
2317
+ return deny("observer is disabled (observer.scope=off)", "scope-off");
2318
+ }
2319
+ if (policy.scope === "window" && !request.hasExplicitTarget) {
2320
+ return deny(
2321
+ "window scope requires a target: pass --process-name or --hwnd, or set observer.scope=fullscreen",
2322
+ "scope-requires-target"
2323
+ );
2324
+ }
2325
+ const requested = request.requestedProcessName;
2326
+ if (requested) {
2327
+ const sensitive = matchBuiltInSensitive(requested, null);
2328
+ if (sensitive) {
2329
+ return deny(`refusing sensitive surface (${sensitive})`, "sensitive");
2330
+ }
2331
+ const denied = matchDenylist(policy, requested, null);
2332
+ if (denied) {
2333
+ return deny(`process is denylisted (${denied})`, "denylisted");
2334
+ }
2335
+ if (policy.scope === "window" && !inAllowlist(policy, requested)) {
2336
+ return deny(
2337
+ `process not in observer.allowlist (${requested.toLowerCase()})`,
2338
+ "not-allowlisted"
2339
+ );
2340
+ }
2341
+ }
2342
+ return { allowed: true };
2343
+ }
2344
+ function decidePostCapture(policy, target) {
2345
+ const isFullscreen = target.method.toLowerCase().includes("fullscreen");
2346
+ if (policy.scope === "window" && isFullscreen) {
2347
+ return deny(
2348
+ "window scope but capture fell back to fullscreen (target window not resolved)",
2349
+ "scope-requires-target"
2350
+ );
2351
+ }
2352
+ const sensitive = matchBuiltInSensitive(
2353
+ target.processName,
2354
+ target.windowTitle
2355
+ );
2356
+ if (sensitive) {
2357
+ return deny(`refusing sensitive surface (${sensitive})`, "sensitive");
2358
+ }
2359
+ const denied = matchDenylist(policy, target.processName, target.windowTitle);
2360
+ if (denied) {
2361
+ return deny(`captured window is denylisted (${denied})`, "denylisted");
2362
+ }
2363
+ if (policy.scope === "window" && !inAllowlist(policy, target.processName)) {
2364
+ return deny(
2365
+ `captured window not in observer.allowlist (${target.processName ?? "unknown"})`,
2366
+ "not-allowlisted"
2367
+ );
2368
+ }
2369
+ return { allowed: true };
2370
+ }
2371
+ async function isObserverPolicyConfigured(db) {
2372
+ const settings = await getAllSettings(db);
2373
+ return Object.keys(settings).some((key) => key.startsWith("observer."));
2374
+ }
2375
+ var OBSERVER_POLICY_UNSET_HINT = "Observer policy is at defaults (scope=window: only a targeted window is captured). Configure with `zam observer status|grant|revoke` or `zam settings set observer.scope <off|window|fullscreen>`.";
2376
+
2377
+ // src/kernel/observation/ui-observer-io.ts
2378
+ import { appendFileSync as appendFileSync2, existsSync as existsSync5, mkdirSync as mkdirSync4, readFileSync as readFileSync5 } from "fs";
2379
+ import { homedir as homedir4 } from "os";
2380
+ import { join as join5 } from "path";
2381
+
2382
+ // src/kernel/observation/ui-observer.ts
2383
+ var UI_OBSERVATION_PROTOCOL_VERSION = 1;
2384
+ var OBSERVATION_KINDS = /* @__PURE__ */ new Set([
2385
+ "progress",
2386
+ "step-completed",
2387
+ "error",
2388
+ "help-seeking",
2389
+ "uncertain",
2390
+ "privacy-pause",
2391
+ "heartbeat"
2392
+ ]);
2393
+ var ACTION_TYPES = /* @__PURE__ */ new Set([
2394
+ "click",
2395
+ "shortcut",
2396
+ "typing",
2397
+ "scroll",
2398
+ "window-change"
2399
+ ]);
2400
+ var EVIDENCE_TYPES = /* @__PURE__ */ new Set([
2401
+ "uia",
2402
+ "keyframe",
2403
+ "clip",
2404
+ "window"
2405
+ ]);
2406
+ function isUiObservationReport(value) {
2407
+ if (!isRecord(value)) return false;
2408
+ if (value.version !== UI_OBSERVATION_PROTOCOL_VERSION) return false;
2409
+ if (!isNonEmptyString(value.sessionId)) return false;
2410
+ if (!isNonNegativeInteger(value.sequence)) return false;
2411
+ if (!isNonEmptyString(value.observedFrom)) return false;
2412
+ if (!isNonEmptyString(value.observedTo)) return false;
2413
+ if (typeof value.kind !== "string" || !OBSERVATION_KINDS.has(value.kind)) {
2414
+ return false;
2415
+ }
2416
+ if (!isApplication(value.application)) return false;
2417
+ if (!isNonEmptyString(value.summary)) return false;
2418
+ if (!Array.isArray(value.actions) || !value.actions.every(isObservedAction)) {
2419
+ return false;
2420
+ }
2421
+ if (!Array.isArray(value.evidence) || !value.evidence.every(isEvidenceRef)) {
2422
+ return false;
2423
+ }
2424
+ if (!Array.isArray(value.candidateTokens) || !value.candidateTokens.every(isCandidateToken)) {
2425
+ return false;
2426
+ }
2427
+ return isConfidence(value.confidence);
2428
+ }
2429
+ function parseUiObservationLog(jsonl) {
2430
+ const reports = [];
2431
+ for (const line of jsonl.split("\n")) {
2432
+ const trimmed = line.trim();
2433
+ if (!trimmed) continue;
2434
+ try {
2435
+ const value = JSON.parse(trimmed);
2436
+ if (isUiObservationReport(value)) {
2437
+ reports.push(value);
2438
+ }
2439
+ } catch {
2440
+ }
2441
+ }
2442
+ return reports.sort((left, right) => left.sequence - right.sequence);
2443
+ }
2444
+ function isApplication(value) {
2445
+ if (!isRecord(value) || !isNonEmptyString(value.processName)) return false;
2446
+ if (value.processId !== void 0 && !isNonNegativeInteger(value.processId)) {
2447
+ return false;
2448
+ }
2449
+ return value.windowTitle === void 0 || typeof value.windowTitle === "string";
2450
+ }
2451
+ function isObservedAction(value) {
2452
+ if (!isRecord(value) || typeof value.type !== "string") return false;
2453
+ if (!ACTION_TYPES.has(value.type)) return false;
2454
+ if (value.target !== void 0 && typeof value.target !== "string") {
2455
+ return false;
2456
+ }
2457
+ return value.result === void 0 || typeof value.result === "string";
2458
+ }
2459
+ function isEvidenceRef(value) {
2460
+ if (!isRecord(value) || typeof value.type !== "string") return false;
2461
+ return EVIDENCE_TYPES.has(value.type) && isNonEmptyString(value.ref) && typeof value.redacted === "boolean";
2462
+ }
2463
+ function isCandidateToken(value) {
2464
+ return isRecord(value) && isNonEmptyString(value.slug) && isConfidence(value.confidence) && isNonEmptyString(value.rationale);
2465
+ }
2466
+ function isRecord(value) {
2467
+ return typeof value === "object" && value !== null && !Array.isArray(value);
2468
+ }
2469
+ function isNonEmptyString(value) {
2470
+ return typeof value === "string" && value.trim().length > 0;
2471
+ }
2472
+ function isNonNegativeInteger(value) {
2473
+ return Number.isInteger(value) && Number(value) >= 0;
2474
+ }
2475
+ function isConfidence(value) {
2476
+ return typeof value === "number" && value >= 0 && value <= 1;
2477
+ }
2478
+
2479
+ // src/kernel/observation/ui-observer-io.ts
2480
+ var DEFAULT_UI_OBSERVER_DIR = join5(homedir4(), ".zam", "observer");
2481
+ function getUiObserverDir() {
2482
+ return process.env.ZAM_OBSERVER_DIR || DEFAULT_UI_OBSERVER_DIR;
2483
+ }
2484
+ function getUiObservationPath(sessionId) {
2485
+ if (!/^[A-Za-z0-9_-]+$/.test(sessionId)) {
2486
+ throw new Error(`Invalid observer session ID: ${sessionId}`);
2487
+ }
2488
+ return join5(getUiObserverDir(), `${sessionId}.reports.jsonl`);
2489
+ }
2490
+ function ensureUiObserverDir() {
2491
+ const dir = getUiObserverDir();
2492
+ if (!existsSync5(dir)) {
2493
+ mkdirSync4(dir, { recursive: true, mode: 448 });
2494
+ }
2495
+ }
2496
+ function uiObservationLogExists(sessionId) {
2497
+ return existsSync5(getUiObservationPath(sessionId));
2498
+ }
2499
+ function readUiObservationLog(sessionId) {
2500
+ const path = getUiObservationPath(sessionId);
2501
+ if (!existsSync5(path)) return [];
2502
+ return parseUiObservationLog(readFileSync5(path, "utf8"));
2503
+ }
2504
+ function appendUiObservationReport(report) {
2505
+ if (!isUiObservationReport(report)) {
2506
+ throw new Error("Invalid UI observation report");
2507
+ }
2508
+ ensureUiObserverDir();
2509
+ appendFileSync2(
2510
+ getUiObservationPath(report.sessionId),
2511
+ `${JSON.stringify(report)}
2512
+ `,
2513
+ { encoding: "utf8", mode: 384 }
2514
+ );
2515
+ }
2516
+
2517
+ // src/kernel/observation/observer-sidecar-policy.ts
2518
+ var SIDECAR_POLICY_FILE = "policy.json";
2519
+ function toSidecarPrivacyPolicy(policy) {
2520
+ return {
2521
+ allowProcesses: [...policy.allowlist],
2522
+ denyProcesses: [...policy.denylist],
2523
+ denyTitleMarkers: [...policy.denylist]
2524
+ };
2525
+ }
2526
+ async function syncObserverSidecarPolicy(db, dir = getUiObserverDir()) {
2527
+ const policy = toSidecarPrivacyPolicy(await resolveObserverPolicy(db));
2528
+ mkdirSync5(dir, { recursive: true, mode: 448 });
2529
+ const path = join6(dir, SIDECAR_POLICY_FILE);
2530
+ writeFileSync3(path, `${JSON.stringify(policy, null, 2)}
2531
+ `, {
2532
+ encoding: "utf8",
2533
+ mode: 384
2534
+ });
2535
+ return { path, policy };
2536
+ }
2537
+
2089
2538
  // src/kernel/observation/session-synthesis.ts
2090
2539
  import { ulid as ulid7 } from "ulid";
2091
2540
 
@@ -2316,184 +2765,44 @@ async function cascadeBlock(db, userId, tokenSlug) {
2316
2765
  const now = (/* @__PURE__ */ new Date()).toISOString();
2317
2766
  await db.prepare(
2318
2767
  "UPDATE cards SET blocked = 0, due_at = ? WHERE token_id = ? AND user_id = ?"
2319
- ).run(now, prereq.requires_id, userId);
2320
- }
2321
- }
2322
- surfaced.push({
2323
- slug: prereq.slug,
2324
- concept: prereq.concept,
2325
- bloomLevel: prereq.bloom_level
2326
- });
2327
- }
2328
- return {
2329
- blockedSlug: tokenSlug,
2330
- prerequisites: surfaced
2331
- };
2332
- }
2333
- async function unblockReady(db, userId) {
2334
- const blockedCards = await db.prepare(
2335
- `SELECT c.token_id, t.slug, t.concept
2336
- FROM cards c
2337
- JOIN tokens t ON t.id = c.token_id
2338
- WHERE c.user_id = ? AND c.blocked = 1`
2339
- ).all(userId);
2340
- const unblocked = [];
2341
- for (const card of blockedCards) {
2342
- const totalPrereqs = await db.prepare("SELECT COUNT(*) as n FROM prerequisites WHERE token_id = ?").get(card.token_id);
2343
- const metPrereqs = await db.prepare(
2344
- `SELECT COUNT(*) as n FROM cards c
2345
- JOIN prerequisites p ON p.requires_id = c.token_id
2346
- WHERE p.token_id = ? AND c.user_id = ? AND c.reps >= 1 AND c.blocked = 0`
2347
- ).get(card.token_id, userId);
2348
- if (totalPrereqs.n === 0 || metPrereqs.n === totalPrereqs.n) {
2349
- const now = (/* @__PURE__ */ new Date()).toISOString();
2350
- await db.prepare(
2351
- "UPDATE cards SET blocked = 0, due_at = ? WHERE token_id = ? AND user_id = ?"
2352
- ).run(now, card.token_id, userId);
2353
- unblocked.push({ slug: card.slug, concept: card.concept });
2354
- }
2355
- }
2356
- return { unblocked };
2357
- }
2358
-
2359
- // src/kernel/observation/ui-observer-io.ts
2360
- import { appendFileSync as appendFileSync2, existsSync as existsSync5, mkdirSync as mkdirSync4, readFileSync as readFileSync5 } from "fs";
2361
- import { homedir as homedir4 } from "os";
2362
- import { join as join5 } from "path";
2363
-
2364
- // src/kernel/observation/ui-observer.ts
2365
- var UI_OBSERVATION_PROTOCOL_VERSION = 1;
2366
- var OBSERVATION_KINDS = /* @__PURE__ */ new Set([
2367
- "progress",
2368
- "step-completed",
2369
- "error",
2370
- "help-seeking",
2371
- "uncertain",
2372
- "privacy-pause",
2373
- "heartbeat"
2374
- ]);
2375
- var ACTION_TYPES = /* @__PURE__ */ new Set([
2376
- "click",
2377
- "shortcut",
2378
- "typing",
2379
- "scroll",
2380
- "window-change"
2381
- ]);
2382
- var EVIDENCE_TYPES = /* @__PURE__ */ new Set([
2383
- "uia",
2384
- "keyframe",
2385
- "clip",
2386
- "window"
2387
- ]);
2388
- function isUiObservationReport(value) {
2389
- if (!isRecord(value)) return false;
2390
- if (value.version !== UI_OBSERVATION_PROTOCOL_VERSION) return false;
2391
- if (!isNonEmptyString(value.sessionId)) return false;
2392
- if (!isNonNegativeInteger(value.sequence)) return false;
2393
- if (!isNonEmptyString(value.observedFrom)) return false;
2394
- if (!isNonEmptyString(value.observedTo)) return false;
2395
- if (typeof value.kind !== "string" || !OBSERVATION_KINDS.has(value.kind)) {
2396
- return false;
2397
- }
2398
- if (!isApplication(value.application)) return false;
2399
- if (!isNonEmptyString(value.summary)) return false;
2400
- if (!Array.isArray(value.actions) || !value.actions.every(isObservedAction)) {
2401
- return false;
2402
- }
2403
- if (!Array.isArray(value.evidence) || !value.evidence.every(isEvidenceRef)) {
2404
- return false;
2405
- }
2406
- if (!Array.isArray(value.candidateTokens) || !value.candidateTokens.every(isCandidateToken)) {
2407
- return false;
2408
- }
2409
- return isConfidence(value.confidence);
2410
- }
2411
- function parseUiObservationLog(jsonl) {
2412
- const reports = [];
2413
- for (const line of jsonl.split("\n")) {
2414
- const trimmed = line.trim();
2415
- if (!trimmed) continue;
2416
- try {
2417
- const value = JSON.parse(trimmed);
2418
- if (isUiObservationReport(value)) {
2419
- reports.push(value);
2420
- }
2421
- } catch {
2422
- }
2423
- }
2424
- return reports.sort((left, right) => left.sequence - right.sequence);
2425
- }
2426
- function isApplication(value) {
2427
- if (!isRecord(value) || !isNonEmptyString(value.processName)) return false;
2428
- if (value.processId !== void 0 && !isNonNegativeInteger(value.processId)) {
2429
- return false;
2430
- }
2431
- return value.windowTitle === void 0 || typeof value.windowTitle === "string";
2432
- }
2433
- function isObservedAction(value) {
2434
- if (!isRecord(value) || typeof value.type !== "string") return false;
2435
- if (!ACTION_TYPES.has(value.type)) return false;
2436
- if (value.target !== void 0 && typeof value.target !== "string") {
2437
- return false;
2438
- }
2439
- return value.result === void 0 || typeof value.result === "string";
2440
- }
2441
- function isEvidenceRef(value) {
2442
- if (!isRecord(value) || typeof value.type !== "string") return false;
2443
- return EVIDENCE_TYPES.has(value.type) && isNonEmptyString(value.ref) && typeof value.redacted === "boolean";
2444
- }
2445
- function isCandidateToken(value) {
2446
- return isRecord(value) && isNonEmptyString(value.slug) && isConfidence(value.confidence) && isNonEmptyString(value.rationale);
2447
- }
2448
- function isRecord(value) {
2449
- return typeof value === "object" && value !== null && !Array.isArray(value);
2450
- }
2451
- function isNonEmptyString(value) {
2452
- return typeof value === "string" && value.trim().length > 0;
2453
- }
2454
- function isNonNegativeInteger(value) {
2455
- return Number.isInteger(value) && Number(value) >= 0;
2456
- }
2457
- function isConfidence(value) {
2458
- return typeof value === "number" && value >= 0 && value <= 1;
2459
- }
2460
-
2461
- // src/kernel/observation/ui-observer-io.ts
2462
- var DEFAULT_UI_OBSERVER_DIR = join5(homedir4(), ".zam", "observer");
2463
- function getUiObserverDir() {
2464
- return process.env.ZAM_OBSERVER_DIR || DEFAULT_UI_OBSERVER_DIR;
2465
- }
2466
- function getUiObservationPath(sessionId) {
2467
- if (!/^[A-Za-z0-9_-]+$/.test(sessionId)) {
2468
- throw new Error(`Invalid observer session ID: ${sessionId}`);
2469
- }
2470
- return join5(getUiObserverDir(), `${sessionId}.reports.jsonl`);
2471
- }
2472
- function ensureUiObserverDir() {
2473
- const dir = getUiObserverDir();
2474
- if (!existsSync5(dir)) {
2475
- mkdirSync4(dir, { recursive: true, mode: 448 });
2768
+ ).run(now, prereq.requires_id, userId);
2769
+ }
2770
+ }
2771
+ surfaced.push({
2772
+ slug: prereq.slug,
2773
+ concept: prereq.concept,
2774
+ bloomLevel: prereq.bloom_level
2775
+ });
2476
2776
  }
2777
+ return {
2778
+ blockedSlug: tokenSlug,
2779
+ prerequisites: surfaced
2780
+ };
2477
2781
  }
2478
- function uiObservationLogExists(sessionId) {
2479
- return existsSync5(getUiObservationPath(sessionId));
2480
- }
2481
- function readUiObservationLog(sessionId) {
2482
- const path = getUiObservationPath(sessionId);
2483
- if (!existsSync5(path)) return [];
2484
- return parseUiObservationLog(readFileSync5(path, "utf8"));
2485
- }
2486
- function appendUiObservationReport(report) {
2487
- if (!isUiObservationReport(report)) {
2488
- throw new Error("Invalid UI observation report");
2782
+ async function unblockReady(db, userId) {
2783
+ const blockedCards = await db.prepare(
2784
+ `SELECT c.token_id, t.slug, t.concept
2785
+ FROM cards c
2786
+ JOIN tokens t ON t.id = c.token_id
2787
+ WHERE c.user_id = ? AND c.blocked = 1`
2788
+ ).all(userId);
2789
+ const unblocked = [];
2790
+ for (const card of blockedCards) {
2791
+ const totalPrereqs = await db.prepare("SELECT COUNT(*) as n FROM prerequisites WHERE token_id = ?").get(card.token_id);
2792
+ const metPrereqs = await db.prepare(
2793
+ `SELECT COUNT(*) as n FROM cards c
2794
+ JOIN prerequisites p ON p.requires_id = c.token_id
2795
+ WHERE p.token_id = ? AND c.user_id = ? AND c.reps >= 1 AND c.blocked = 0`
2796
+ ).get(card.token_id, userId);
2797
+ if (totalPrereqs.n === 0 || metPrereqs.n === totalPrereqs.n) {
2798
+ const now = (/* @__PURE__ */ new Date()).toISOString();
2799
+ await db.prepare(
2800
+ "UPDATE cards SET blocked = 0, due_at = ? WHERE token_id = ? AND user_id = ?"
2801
+ ).run(now, card.token_id, userId);
2802
+ unblocked.push({ slug: card.slug, concept: card.concept });
2803
+ }
2489
2804
  }
2490
- ensureUiObserverDir();
2491
- appendFileSync2(
2492
- getUiObservationPath(report.sessionId),
2493
- `${JSON.stringify(report)}
2494
- `,
2495
- { encoding: "utf8", mode: 384 }
2496
- );
2805
+ return { unblocked };
2497
2806
  }
2498
2807
 
2499
2808
  // src/kernel/observation/ui-observer-synthesis.ts
@@ -3300,7 +3609,7 @@ function generatePrompt(input8) {
3300
3609
 
3301
3610
  // src/kernel/recall/reference-resolver.ts
3302
3611
  import { existsSync as existsSync6, readFileSync as readFileSync6 } from "fs";
3303
- import { dirname as dirname3, join as join6, resolve } from "path";
3612
+ import { dirname as dirname3, join as join7, resolve } from "path";
3304
3613
  var DEFAULT_REVIEW_CONTEXT_MAX_CHARS = 6e3;
3305
3614
  function htmlToText(html) {
3306
3615
  let content = html;
@@ -3361,8 +3670,8 @@ async function resolveReference(sourceLink) {
3361
3670
  const filePath = anchorIndex2 !== -1 ? fullPathWithAnchor.slice(0, anchorIndex2) : fullPathWithAnchor;
3362
3671
  const anchor2 = anchorIndex2 !== -1 ? fullPathWithAnchor.slice(anchorIndex2) : "";
3363
3672
  const parentDir = dirname3(process.cwd());
3364
- const localRepoPath = join6(parentDir, repo);
3365
- const localFilePath = join6(localRepoPath, filePath);
3673
+ const localRepoPath = join7(parentDir, repo);
3674
+ const localFilePath = join7(localRepoPath, filePath);
3366
3675
  if (existsSync6(localFilePath)) {
3367
3676
  try {
3368
3677
  let fileContent = readFileSync6(localFilePath, "utf-8");
@@ -3676,12 +3985,12 @@ import {
3676
3985
  appendFileSync as appendFileSync3,
3677
3986
  copyFileSync,
3678
3987
  existsSync as existsSync7,
3679
- mkdirSync as mkdirSync5,
3988
+ mkdirSync as mkdirSync6,
3680
3989
  readFileSync as readFileSync7,
3681
- writeFileSync as writeFileSync3
3990
+ writeFileSync as writeFileSync4
3682
3991
  } from "fs";
3683
3992
  import { homedir as homedir5 } from "os";
3684
- import { join as join7 } from "path";
3993
+ import { join as join8 } from "path";
3685
3994
  import { fileURLToPath } from "url";
3686
3995
  var HOME = homedir5();
3687
3996
  function getPackageSkillPath(agent = "default") {
@@ -3689,15 +3998,15 @@ function getPackageSkillPath(agent = "default") {
3689
3998
  fileURLToPath(new URL("../../..", import.meta.url)),
3690
3999
  fileURLToPath(new URL("../..", import.meta.url)),
3691
4000
  fileURLToPath(new URL("..", import.meta.url))
3692
- ].find((candidate) => existsSync7(join7(candidate, "package.json"))) ?? "";
4001
+ ].find((candidate) => existsSync7(join8(candidate, "package.json"))) ?? "";
3693
4002
  if (!packageRoot2) return "";
3694
4003
  if (agent === "codex") {
3695
- const codexPath = join7(packageRoot2, ".agents", "skills", "zam", "SKILL.md");
4004
+ const codexPath = join8(packageRoot2, ".agents", "skills", "zam", "SKILL.md");
3696
4005
  if (existsSync7(codexPath)) return codexPath;
3697
4006
  return "";
3698
4007
  }
3699
4008
  if (agent === "claude") {
3700
- const claudePath = join7(
4009
+ const claudePath = join8(
3701
4010
  packageRoot2,
3702
4011
  ".claude",
3703
4012
  "skills",
@@ -3707,9 +4016,9 @@ function getPackageSkillPath(agent = "default") {
3707
4016
  if (existsSync7(claudePath)) return claudePath;
3708
4017
  return "";
3709
4018
  }
3710
- let path = join7(packageRoot2, ".agent", "skills", "zam", "SKILL.md");
4019
+ let path = join8(packageRoot2, ".agent", "skills", "zam", "SKILL.md");
3711
4020
  if (existsSync7(path)) return path;
3712
- path = join7(packageRoot2, ".claude", "skills", "zam", "SKILL.md");
4021
+ path = join8(packageRoot2, ".claude", "skills", "zam", "SKILL.md");
3713
4022
  if (existsSync7(path)) return path;
3714
4023
  return "";
3715
4024
  }
@@ -3722,16 +4031,16 @@ function distributeGlobalSkills(home = HOME) {
3722
4031
  console.warn("Could not find ZAM source SKILL.md in the package folder.");
3723
4032
  return results;
3724
4033
  }
3725
- const claudeSkillsDir = join7(home, ".claude", "skills", "zam");
4034
+ const claudeSkillsDir = join8(home, ".claude", "skills", "zam");
3726
4035
  try {
3727
4036
  if (!claudeSourceSkill) {
3728
4037
  throw new Error("Claude skill source not found");
3729
4038
  }
3730
- mkdirSync5(claudeSkillsDir, { recursive: true });
3731
- copyFileSync(claudeSourceSkill, join7(claudeSkillsDir, "SKILL.md"));
4039
+ mkdirSync6(claudeSkillsDir, { recursive: true });
4040
+ copyFileSync(claudeSourceSkill, join8(claudeSkillsDir, "SKILL.md"));
3732
4041
  results.push({
3733
4042
  name: "Claude Code Global",
3734
- path: join7(claudeSkillsDir, "SKILL.md"),
4043
+ path: join8(claudeSkillsDir, "SKILL.md"),
3735
4044
  success: true
3736
4045
  });
3737
4046
  } catch (_err) {
@@ -3741,13 +4050,13 @@ function distributeGlobalSkills(home = HOME) {
3741
4050
  success: false
3742
4051
  });
3743
4052
  }
3744
- const geminiSkillsDir = join7(home, ".gemini", "skills", "zam");
4053
+ const geminiSkillsDir = join8(home, ".gemini", "skills", "zam");
3745
4054
  try {
3746
- mkdirSync5(geminiSkillsDir, { recursive: true });
3747
- copyFileSync(sourceSkill, join7(geminiSkillsDir, "SKILL.md"));
4055
+ mkdirSync6(geminiSkillsDir, { recursive: true });
4056
+ copyFileSync(sourceSkill, join8(geminiSkillsDir, "SKILL.md"));
3748
4057
  results.push({
3749
4058
  name: "Gemini CLI Global",
3750
- path: join7(geminiSkillsDir, "SKILL.md"),
4059
+ path: join8(geminiSkillsDir, "SKILL.md"),
3751
4060
  success: true
3752
4061
  });
3753
4062
  } catch (_err) {
@@ -3757,16 +4066,16 @@ function distributeGlobalSkills(home = HOME) {
3757
4066
  success: false
3758
4067
  });
3759
4068
  }
3760
- const codexSkillsDir = join7(home, ".agents", "skills", "zam");
4069
+ const codexSkillsDir = join8(home, ".agents", "skills", "zam");
3761
4070
  try {
3762
4071
  if (!codexSourceSkill) {
3763
4072
  throw new Error("Codex skill source not found");
3764
4073
  }
3765
- mkdirSync5(codexSkillsDir, { recursive: true });
3766
- copyFileSync(codexSourceSkill, join7(codexSkillsDir, "SKILL.md"));
4074
+ mkdirSync6(codexSkillsDir, { recursive: true });
4075
+ copyFileSync(codexSourceSkill, join8(codexSkillsDir, "SKILL.md"));
3767
4076
  results.push({
3768
4077
  name: "Codex Global",
3769
- path: join7(codexSkillsDir, "SKILL.md"),
4078
+ path: join8(codexSkillsDir, "SKILL.md"),
3770
4079
  success: true
3771
4080
  });
3772
4081
  } catch (_err) {
@@ -3776,13 +4085,13 @@ function distributeGlobalSkills(home = HOME) {
3776
4085
  success: false
3777
4086
  });
3778
4087
  }
3779
- const gooseSkillsDir = join7(home, ".goose", "skills", "zam");
4088
+ const gooseSkillsDir = join8(home, ".goose", "skills", "zam");
3780
4089
  try {
3781
- mkdirSync5(gooseSkillsDir, { recursive: true });
3782
- copyFileSync(sourceSkill, join7(gooseSkillsDir, "SKILL.md"));
4090
+ mkdirSync6(gooseSkillsDir, { recursive: true });
4091
+ copyFileSync(sourceSkill, join8(gooseSkillsDir, "SKILL.md"));
3783
4092
  results.push({
3784
4093
  name: "Goose Global",
3785
- path: join7(gooseSkillsDir, "SKILL.md"),
4094
+ path: join8(gooseSkillsDir, "SKILL.md"),
3786
4095
  success: true
3787
4096
  });
3788
4097
  } catch (_err) {
@@ -3831,7 +4140,7 @@ function installHook(file, hook, oldHook) {
3831
4140
  return { success: true, alreadyHooked: true };
3832
4141
  }
3833
4142
  if (content.includes(oldHook.trim())) {
3834
- writeFileSync3(file, content.replace(oldHook.trim(), hook.trim()), "utf8");
4143
+ writeFileSync4(file, content.replace(oldHook.trim(), hook.trim()), "utf8");
3835
4144
  } else {
3836
4145
  appendFileSync3(file, hook);
3837
4146
  }
@@ -3842,24 +4151,24 @@ function installHook(file, hook, oldHook) {
3842
4151
  }
3843
4152
  function injectShellHooks(home = HOME) {
3844
4153
  const results = [];
3845
- const zshrc = join7(home, ".zshrc");
4154
+ const zshrc = join8(home, ".zshrc");
3846
4155
  if (existsSync7(zshrc)) {
3847
4156
  const status = installHook(zshrc, posixHook("zsh"), POSIX_OLD_HOOK);
3848
4157
  results.push({ shell: "zsh", file: zshrc, ...status });
3849
4158
  }
3850
- const bashrc = join7(home, ".bashrc");
4159
+ const bashrc = join8(home, ".bashrc");
3851
4160
  if (existsSync7(bashrc)) {
3852
4161
  const status = installHook(bashrc, posixHook("bash"), POSIX_OLD_HOOK);
3853
4162
  results.push({ shell: "bash", file: bashrc, ...status });
3854
4163
  }
3855
4164
  const pwshDirs = [
3856
- join7(home, "Documents", "PowerShell"),
3857
- join7(home, "Documents", "WindowsPowerShell")
4165
+ join8(home, "Documents", "PowerShell"),
4166
+ join8(home, "Documents", "WindowsPowerShell")
3858
4167
  ];
3859
4168
  for (const dir of pwshDirs) {
3860
- const profileFile = join7(dir, "Microsoft.PowerShell_profile.ps1");
4169
+ const profileFile = join8(dir, "Microsoft.PowerShell_profile.ps1");
3861
4170
  try {
3862
- mkdirSync5(dir, { recursive: true });
4171
+ mkdirSync6(dir, { recursive: true });
3863
4172
  const status = installHook(
3864
4173
  profileFile,
3865
4174
  POWERSHELL_HOOK,
@@ -4091,10 +4400,10 @@ function t(locale, key, params = {}) {
4091
4400
  }
4092
4401
 
4093
4402
  // src/kernel/system/install-config.ts
4094
- import { existsSync as existsSync8, mkdirSync as mkdirSync6, readFileSync as readFileSync8, writeFileSync as writeFileSync4 } from "fs";
4403
+ import { existsSync as existsSync8, mkdirSync as mkdirSync7, readFileSync as readFileSync8, writeFileSync as writeFileSync5 } from "fs";
4095
4404
  import { homedir as homedir6 } from "os";
4096
- import { dirname as dirname4, join as join8 } from "path";
4097
- var DEFAULT_CONFIG_PATH = join8(homedir6(), ".zam", "config.json");
4405
+ import { dirname as dirname4, join as join9 } from "path";
4406
+ var DEFAULT_CONFIG_PATH = join9(homedir6(), ".zam", "config.json");
4098
4407
  function loadInstallConfig(path = DEFAULT_CONFIG_PATH) {
4099
4408
  if (!existsSync8(path)) return {};
4100
4409
  try {
@@ -4105,8 +4414,8 @@ function loadInstallConfig(path = DEFAULT_CONFIG_PATH) {
4105
4414
  }
4106
4415
  function saveInstallConfig(config, path = DEFAULT_CONFIG_PATH) {
4107
4416
  const dir = dirname4(path);
4108
- if (!existsSync8(dir)) mkdirSync6(dir, { recursive: true });
4109
- writeFileSync4(path, `${JSON.stringify(config, null, 2)}
4417
+ if (!existsSync8(dir)) mkdirSync7(dir, { recursive: true });
4418
+ writeFileSync5(path, `${JSON.stringify(config, null, 2)}
4110
4419
  `, "utf-8");
4111
4420
  }
4112
4421
  function getInstallMode(path = DEFAULT_CONFIG_PATH) {
@@ -4139,7 +4448,7 @@ function detectSyncProvider(dir) {
4139
4448
  import { execFileSync, execSync } from "child_process";
4140
4449
  import { existsSync as existsSync9 } from "fs";
4141
4450
  import { homedir as homedir7 } from "os";
4142
- import { join as join9 } from "path";
4451
+ import { join as join10 } from "path";
4143
4452
  function hasCommand(cmd) {
4144
4453
  try {
4145
4454
  const checkCmd2 = process.platform === "win32" ? `where ${cmd}` : `which ${cmd}`;
@@ -4184,7 +4493,7 @@ function installOllama() {
4184
4493
  const isMac = process.platform === "darwin";
4185
4494
  const isWin = process.platform === "win32";
4186
4495
  const hasOllama = hasCommand("ollama") || isMac && existsSync9("/Applications/Ollama.app") || isWin && existsSync9(
4187
- join9(homedir7(), "AppData", "Local", "Programs", "Ollama", "ollama.exe")
4496
+ join10(homedir7(), "AppData", "Local", "Programs", "Ollama", "ollama.exe")
4188
4497
  );
4189
4498
  if (hasOllama) {
4190
4499
  return { success: true, message: "Ollama is already installed." };
@@ -4244,7 +4553,7 @@ function installOllama() {
4244
4553
  function resolveOllamaCommand() {
4245
4554
  if (hasCommand("ollama")) return "ollama";
4246
4555
  const candidates = process.platform === "win32" ? [
4247
- join9(
4556
+ join10(
4248
4557
  homedir7(),
4249
4558
  "AppData",
4250
4559
  "Local",
@@ -4538,6 +4847,37 @@ function decideUpdate(input8) {
4538
4847
  };
4539
4848
  }
4540
4849
  }
4850
+ function planUpdate(decision) {
4851
+ if (!decision.updateAvailable) return [];
4852
+ switch (decision.channel) {
4853
+ case "developer":
4854
+ return [
4855
+ { kind: "git-pull", label: "Pull the latest source" },
4856
+ { kind: "npm-install", label: "Install dependencies" },
4857
+ { kind: "npm-build", label: "Rebuild the CLI" },
4858
+ {
4859
+ kind: "distribute-skills",
4860
+ label: "Refresh skill files (zam setup --force)"
4861
+ }
4862
+ ];
4863
+ case "winget":
4864
+ case "homebrew":
4865
+ return [
4866
+ {
4867
+ kind: "run-command",
4868
+ label: `Update via ${decision.channel}`,
4869
+ command: decision.command
4870
+ }
4871
+ ];
4872
+ case "direct":
4873
+ return [
4874
+ {
4875
+ kind: "self-update",
4876
+ label: "Apply the signed update via the desktop app"
4877
+ }
4878
+ ];
4879
+ }
4880
+ }
4541
4881
 
4542
4882
  // src/cli/commands/agent.ts
4543
4883
  var C = {
@@ -4550,7 +4890,7 @@ var C = {
4550
4890
  };
4551
4891
  var SUPPORTED_AGENTS = ["opencode"];
4552
4892
  function agentsMdPresent(cwd = process.cwd()) {
4553
- return existsSync11(join10(cwd, "AGENTS.md"));
4893
+ return existsSync11(join11(cwd, "AGENTS.md"));
4554
4894
  }
4555
4895
  function printStatus() {
4556
4896
  const installed = hasCommand("opencode");
@@ -4591,9 +4931,11 @@ var statusCmd = new Command("status").description("Show whether the agent is ins
4591
4931
  var agentCommand = new Command("agent").description("Provision and inspect the agent that drives ZAM sessions").addCommand(installCmd).addCommand(statusCmd).action(printStatus);
4592
4932
 
4593
4933
  // src/cli/commands/bridge.ts
4594
- import { readdirSync as readdirSync2 } from "fs";
4595
- import { homedir as homedir8 } from "os";
4596
- import { join as join11 } from "path";
4934
+ import { execFileSync as execFileSync2 } from "child_process";
4935
+ import { randomBytes } from "crypto";
4936
+ import { readdirSync as readdirSync2, readFileSync as readFileSync10, rmSync as rmSync2 } from "fs";
4937
+ import { homedir as homedir8, tmpdir } from "os";
4938
+ import { join as join12 } from "path";
4597
4939
  import { Command as Command2 } from "commander";
4598
4940
 
4599
4941
  // src/cli/llm/client.ts
@@ -4956,8 +5298,8 @@ async function startLocalRunner(url, model, locale) {
4956
5298
  process.stdout.write("\n");
4957
5299
  console.log(`\x1B[33m${t(locale, "wait_warning")}\x1B[0m`);
4958
5300
  console.log(`\x1B[2m${t(locale, "wait_info")}\x1B[0m`);
4959
- const { confirm: confirm4 } = await import("@inquirer/prompts");
4960
- const keepWaiting = await confirm4({
5301
+ const { confirm: confirm5 } = await import("@inquirer/prompts");
5302
+ const keepWaiting = await confirm5({
4961
5303
  message: t(locale, "keep_waiting_llm"),
4962
5304
  default: true
4963
5305
  }).catch(() => false);
@@ -5101,8 +5443,8 @@ async function fetchWithInteractiveTimeout(url, options = {}) {
5101
5443
  }
5102
5444
  console.log(`
5103
5445
  \x1B[33m${t(locale, "local_ai_working")}\x1B[0m`);
5104
- const { confirm: confirm4 } = await import("@inquirer/prompts");
5105
- const keepWaiting = await confirm4({
5446
+ const { confirm: confirm5 } = await import("@inquirer/prompts");
5447
+ const keepWaiting = await confirm5({
5106
5448
  message: t(locale, "keep_waiting"),
5107
5449
  default: true
5108
5450
  }).catch(() => false);
@@ -5730,13 +6072,15 @@ bridgeCommand.command("start-session").description("Start a ZAM learning session
5730
6072
  task: opts.task,
5731
6073
  execution_context: context
5732
6074
  });
6075
+ const observerPolicyHint = context === "ui" && !await isObserverPolicyConfigured(db) ? OBSERVER_POLICY_UNSET_HINT : void 0;
5733
6076
  jsonOut2({
5734
6077
  id: session.id,
5735
6078
  userId: session.user_id,
5736
6079
  task: session.task,
5737
6080
  executionContext: session.execution_context,
5738
6081
  startedAt: session.started_at,
5739
- completedAt: session.completed_at
6082
+ completedAt: session.completed_at,
6083
+ ...observerPolicyHint ? { observerPolicyHint } : {}
5740
6084
  });
5741
6085
  });
5742
6086
  });
@@ -5859,7 +6203,8 @@ bridgeCommand.command("add-token").description("Create a token + card from JSON
5859
6203
  bloom_level: data?.bloom_level ?? 1,
5860
6204
  context: data?.context,
5861
6205
  symbiosis_mode: data?.symbiosis_mode,
5862
- source_link: data?.source_link ?? null
6206
+ source_link: data?.source_link ?? null,
6207
+ question: data?.question ?? null
5863
6208
  });
5864
6209
  const card = await ensureCard(db, token.id, userId);
5865
6210
  jsonOut2({
@@ -5894,7 +6239,7 @@ bridgeCommand.command("discover-skills").description(
5894
6239
  "20"
5895
6240
  ).action(async (opts) => {
5896
6241
  try {
5897
- const monitorDir = join11(homedir8(), ".zam", "monitor");
6242
+ const monitorDir = join12(homedir8(), ".zam", "monitor");
5898
6243
  let files;
5899
6244
  try {
5900
6245
  files = readdirSync2(monitorDir).filter((f) => f.endsWith(".jsonl"));
@@ -5907,7 +6252,7 @@ bridgeCommand.command("discover-skills").description(
5907
6252
  return;
5908
6253
  }
5909
6254
  const limit = Number(opts.limit);
5910
- const sorted = files.map((f) => ({ name: f, path: join11(monitorDir, f) })).sort((a, b) => b.name.localeCompare(a.name)).slice(0, limit);
6255
+ const sorted = files.map((f) => ({ name: f, path: join12(monitorDir, f) })).sort((a, b) => b.name.localeCompare(a.name)).slice(0, limit);
5911
6256
  const sessionCommands = /* @__PURE__ */ new Map();
5912
6257
  for (const file of sorted) {
5913
6258
  const sessionId = file.name.replace(".jsonl", "");
@@ -5917,104 +6262,566 @@ bridgeCommand.command("discover-skills").description(
5917
6262
  sessionCommands.set(sessionId, commands);
5918
6263
  }
5919
6264
  }
5920
- if (sessionCommands.size === 0) {
5921
- jsonOut2({ proposals: [], message: "No command data in monitor logs." });
5922
- return;
5923
- }
5924
- let existingSkillSlugs = [];
5925
- let db;
5926
- try {
5927
- db = await openDatabase();
5928
- existingSkillSlugs = (await listAgentSkills(db)).map((s) => s.slug);
5929
- } catch {
5930
- } finally {
5931
- await db?.close();
5932
- }
5933
- const proposals = discoverSkills(sessionCommands, {
5934
- minSessions: Number(opts.minSessions),
5935
- existingSkillSlugs
5936
- });
5937
- jsonOut2({
5938
- sessionsAnalyzed: sessionCommands.size,
5939
- proposals
5940
- });
5941
- } catch (err) {
5942
- jsonError(err.message);
6265
+ if (sessionCommands.size === 0) {
6266
+ jsonOut2({ proposals: [], message: "No command data in monitor logs." });
6267
+ return;
6268
+ }
6269
+ let existingSkillSlugs = [];
6270
+ let db;
6271
+ try {
6272
+ db = await openDatabase();
6273
+ existingSkillSlugs = (await listAgentSkills(db)).map((s) => s.slug);
6274
+ } catch {
6275
+ } finally {
6276
+ await db?.close();
6277
+ }
6278
+ const proposals = discoverSkills(sessionCommands, {
6279
+ minSessions: Number(opts.minSessions),
6280
+ existingSkillSlugs
6281
+ });
6282
+ jsonOut2({
6283
+ sessionsAnalyzed: sessionCommands.size,
6284
+ proposals
6285
+ });
6286
+ } catch (err) {
6287
+ jsonError(err.message);
6288
+ }
6289
+ });
6290
+ bridgeCommand.command("observe-ui-watch").description(
6291
+ "Poll live UI observer watch reports for a ZAM learning session (JSON)"
6292
+ ).requiredOption(
6293
+ "--session <id>",
6294
+ "ZAM session ID (also the observer log key)"
6295
+ ).option("--after <n>", "Only return observations after this sequence").option("--limit <n>", "Maximum observations to return", "100").action(async (opts) => {
6296
+ await withDb2(async (db) => {
6297
+ const session = await db.prepare("SELECT id, execution_context FROM sessions WHERE id = ?").get(opts.session);
6298
+ if (!session) {
6299
+ jsonError(`Session not found: ${opts.session}`);
6300
+ }
6301
+ const after = opts.after === void 0 ? void 0 : parseNonNegativeIntegerOption("after", opts.after);
6302
+ const limit = parseNonNegativeIntegerOption("limit", opts.limit);
6303
+ const observations = readUiObservationLog(opts.session).filter((report) => after === void 0 || report.sequence > after).slice(0, limit);
6304
+ const last = observations[observations.length - 1];
6305
+ jsonOut2({
6306
+ sessionId: opts.session,
6307
+ executionContext: session.execution_context,
6308
+ observationSource: "ui",
6309
+ logExists: uiObservationLogExists(opts.session),
6310
+ after: after ?? null,
6311
+ count: observations.length,
6312
+ nextSequence: last?.sequence ?? after ?? null,
6313
+ observations
6314
+ });
6315
+ });
6316
+ });
6317
+ 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) => {
6318
+ try {
6319
+ const after = opts.after === void 0 ? void 0 : parseNonNegativeIntegerOption("after", opts.after);
6320
+ const limit = parseNonNegativeIntegerOption("limit", opts.limit);
6321
+ const observations = readUiObservationLog(opts.session).filter((report) => after === void 0 || report.sequence > after).slice(0, limit);
6322
+ const last = observations[observations.length - 1];
6323
+ jsonOut2({
6324
+ sessionId: opts.session,
6325
+ after: after ?? null,
6326
+ count: observations.length,
6327
+ nextSequence: last?.sequence ?? after ?? null,
6328
+ observations
6329
+ });
6330
+ } catch (err) {
6331
+ jsonError(err.message);
6332
+ }
6333
+ });
6334
+ bridgeCommand.command("observe-ui-snapshot").description(
6335
+ "Analyze a captured UI snapshot with the configured vision LLM (JSON)"
6336
+ ).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) => {
6337
+ const sequence = parseNonNegativeIntegerOption("sequence", opts.sequence);
6338
+ const processId = opts.processId === void 0 ? void 0 : parseNonNegativeIntegerOption("process-id", opts.processId);
6339
+ const maxTokens = opts.maxTokens === void 0 ? void 0 : parseNonNegativeIntegerOption("max-tokens", opts.maxTokens);
6340
+ const hardTimeoutMs = opts.timeout === void 0 ? void 0 : parseNonNegativeIntegerOption("timeout", opts.timeout);
6341
+ await withDb2(async (db) => {
6342
+ const report = await observeUiSnapshotViaLLM(db, {
6343
+ sessionId: opts.session,
6344
+ sequence,
6345
+ observedFrom: opts.observedFrom,
6346
+ observedTo: opts.observedTo,
6347
+ imagePath: opts.image,
6348
+ application: {
6349
+ processName: opts.processName,
6350
+ processId,
6351
+ windowTitle: opts.windowTitle
6352
+ },
6353
+ evidenceRef: opts.evidenceRef,
6354
+ redacted: opts.redacted === true,
6355
+ model: opts.model,
6356
+ maxTokens,
6357
+ hardTimeoutMs
6358
+ });
6359
+ if (opts.writeLog === true) {
6360
+ appendUiObservationReport(report);
6361
+ }
6362
+ jsonOut2(report);
6363
+ });
6364
+ });
6365
+ function resolveWindowsPowerShell() {
6366
+ try {
6367
+ execFileSync2("where.exe", ["pwsh.exe"], { stdio: "ignore" });
6368
+ return "pwsh";
6369
+ } catch {
6370
+ return "powershell";
6371
+ }
6372
+ }
6373
+ function captureScreenshot(outputPath, hwnd, processName) {
6374
+ const platform = process.platform;
6375
+ if (hwnd && !/^(0x)?[0-9a-fA-F]+$/.test(hwnd)) {
6376
+ throw new Error(`Invalid HWND format: ${hwnd}`);
6377
+ }
6378
+ if (processName && !/^[a-zA-Z0-9\-_.]+$/.test(processName)) {
6379
+ throw new Error(`Invalid process name format: ${processName}`);
6380
+ }
6381
+ if (platform === "win32") {
6382
+ const stdout = execFileSync2(
6383
+ resolveWindowsPowerShell(),
6384
+ [
6385
+ "-NoProfile",
6386
+ "-Command",
6387
+ `
6388
+ Add-Type -AssemblyName System.Windows.Forms
6389
+ Add-Type -AssemblyName System.Drawing
6390
+
6391
+ $code = @'
6392
+ using System;
6393
+ using System.Runtime.InteropServices;
6394
+
6395
+ public class Win32 {
6396
+ public delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam);
6397
+
6398
+ [DllImport("user32.dll")]
6399
+ public static extern bool SetProcessDPIAware();
6400
+
6401
+ [DllImport("user32.dll")]
6402
+ public static extern bool SetProcessDpiAwarenessContext(IntPtr dpiContext);
6403
+
6404
+ [DllImport("user32.dll")]
6405
+ public static extern bool GetWindowRect(IntPtr hWnd, out RECT rect);
6406
+
6407
+ [DllImport("user32.dll")]
6408
+ public static extern bool EnumWindows(EnumWindowsProc lpEnumFunc, IntPtr lParam);
6409
+
6410
+ [DllImport("user32.dll")]
6411
+ public static extern bool EnumChildWindows(IntPtr hWnd, EnumWindowsProc lpEnumFunc, IntPtr lParam);
6412
+
6413
+ [DllImport("user32.dll")]
6414
+ public static extern bool IsWindowVisible(IntPtr hWnd);
6415
+
6416
+ [DllImport("user32.dll", SetLastError=true)]
6417
+ public static extern int GetWindowText(IntPtr hWnd, System.Text.StringBuilder lpString, int nMaxCount);
6418
+
6419
+ [DllImport("user32.dll", SetLastError=true)]
6420
+ public static extern int GetWindowTextLength(IntPtr hWnd);
6421
+
6422
+ [DllImport("user32.dll")]
6423
+ public static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId);
6424
+
6425
+ [DllImport("user32.dll")]
6426
+ public static extern bool SetForegroundWindow(IntPtr hWnd);
6427
+
6428
+ [DllImport("user32.dll")]
6429
+ public static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);
6430
+
6431
+ [DllImport("user32.dll")]
6432
+ public static extern bool IsIconic(IntPtr hWnd);
6433
+
6434
+ [DllImport("user32.dll")]
6435
+ public static extern bool PrintWindow(IntPtr hWnd, IntPtr hdcBlt, uint nFlags);
6436
+
6437
+ [StructLayout(LayoutKind.Sequential)]
6438
+ public struct RECT {
6439
+ public int Left;
6440
+ public int Top;
6441
+ public int Right;
6442
+ public int Bottom;
6443
+ }
6444
+ }
6445
+ '@
6446
+ Add-Type -TypeDefinition $code
6447
+
6448
+ try {
6449
+ # DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2 = -4. Without this, Windows
6450
+ # can return logical window bounds while PrintWindow renders physical
6451
+ # pixels, causing high-DPI captures to crop the right/bottom edge.
6452
+ [Win32]::SetProcessDpiAwarenessContext([IntPtr](-4)) | Out-Null
6453
+ } catch {
6454
+ try { [Win32]::SetProcessDPIAware() | Out-Null } catch {}
6455
+ }
6456
+
6457
+ function Get-WindowTitle([IntPtr]$hWnd) {
6458
+ $length = [Win32]::GetWindowTextLength($hWnd)
6459
+ $capacity = [Math]::Max(1, $length + 1)
6460
+ $builder = New-Object System.Text.StringBuilder $capacity
6461
+ [Win32]::GetWindowText($hWnd, $builder, $builder.Capacity) | Out-Null
6462
+ $builder.ToString()
6463
+ }
6464
+
6465
+ function Get-WindowProcess([IntPtr]$hWnd) {
6466
+ [uint32]$processId = 0
6467
+ [Win32]::GetWindowThreadProcessId($hWnd, [ref]$processId) | Out-Null
6468
+ if ($processId -eq 0) { return $null }
6469
+ Get-Process -Id $processId -ErrorAction SilentlyContinue
6470
+ }
6471
+
6472
+ function Get-VisibleTopLevelWindows {
6473
+ $script:windowCandidates = New-Object System.Collections.ArrayList
6474
+ $callback = [Win32+EnumWindowsProc]{
6475
+ param([IntPtr]$candidateHwnd, [IntPtr]$lParam)
6476
+ if ([Win32]::IsWindowVisible($candidateHwnd)) {
6477
+ $candidateRect = New-Object Win32+RECT
6478
+ if ([Win32]::GetWindowRect($candidateHwnd, [ref]$candidateRect)) {
6479
+ $candidateWidth = $candidateRect.Right - $candidateRect.Left
6480
+ $candidateHeight = $candidateRect.Bottom - $candidateRect.Top
6481
+ if ($candidateWidth -gt 0 -and $candidateHeight -gt 0) {
6482
+ [void]$script:windowCandidates.Add($candidateHwnd)
6483
+ }
6484
+ }
6485
+ }
6486
+ return $true
6487
+ }
6488
+ [Win32]::EnumWindows($callback, [IntPtr]::Zero) | Out-Null
6489
+ $windows = $script:windowCandidates
6490
+ Remove-Variable -Name windowCandidates -Scope Script -ErrorAction SilentlyContinue
6491
+ $windows
6492
+ }
6493
+
6494
+ function Find-TopLevelWindowByProcessName([string]$name) {
6495
+ foreach ($candidateHwnd in Get-VisibleTopLevelWindows) {
6496
+ $candidateProcess = Get-WindowProcess $candidateHwnd
6497
+ if ($candidateProcess -and $candidateProcess.ProcessName -ieq $name) {
6498
+ return [pscustomobject]@{
6499
+ Hwnd = $candidateHwnd
6500
+ MatchedBy = "process-top-level-window"
6501
+ }
6502
+ }
6503
+
6504
+ $script:desiredChildProcessName = $name
6505
+ $script:foundChildProcessWindow = $false
6506
+ $childCallback = [Win32+EnumWindowsProc]{
6507
+ param([IntPtr]$childHwnd, [IntPtr]$lParam)
6508
+ $childProcess = Get-WindowProcess $childHwnd
6509
+ if ($childProcess -and $childProcess.ProcessName -ieq $script:desiredChildProcessName) {
6510
+ $script:foundChildProcessWindow = $true
6511
+ return $false
6512
+ }
6513
+ return $true
6514
+ }
6515
+ [Win32]::EnumChildWindows($candidateHwnd, $childCallback, [IntPtr]::Zero) | Out-Null
6516
+ $foundChild = $script:foundChildProcessWindow
6517
+ Remove-Variable -Name desiredChildProcessName -Scope Script -ErrorAction SilentlyContinue
6518
+ Remove-Variable -Name foundChildProcessWindow -Scope Script -ErrorAction SilentlyContinue
6519
+
6520
+ if ($foundChild) {
6521
+ return [pscustomobject]@{
6522
+ Hwnd = $candidateHwnd
6523
+ MatchedBy = "process-child-window"
6524
+ }
6525
+ }
6526
+ }
6527
+
6528
+ return $null
6529
+ }
6530
+
6531
+ function New-CaptureTarget([IntPtr]$hWnd, [string]$matchedBy) {
6532
+ $target = [ordered]@{
6533
+ requestedHwnd = if ($targetHwnd -ne '') { $targetHwnd } else { $null }
6534
+ requestedProcessName = if ($processName -ne '') { $processName } else { $null }
6535
+ matchedBy = $matchedBy
6536
+ hwnd = $null
6537
+ processId = $null
6538
+ processName = $null
6539
+ windowTitle = $null
6540
+ bounds = $null
6541
+ }
6542
+
6543
+ if ($hWnd -ne [IntPtr]::Zero) {
6544
+ $target.hwnd = $hWnd.ToInt64()
6545
+ $target.windowTitle = Get-WindowTitle $hWnd
6546
+
6547
+ $windowProcess = Get-WindowProcess $hWnd
6548
+ if ($windowProcess) {
6549
+ $target.processId = $windowProcess.Id
6550
+ $target.processName = $windowProcess.ProcessName
6551
+ }
6552
+
6553
+ $targetRect = New-Object Win32+RECT
6554
+ if ([Win32]::GetWindowRect($hWnd, [ref]$targetRect)) {
6555
+ $target.bounds = [ordered]@{
6556
+ left = $targetRect.Left
6557
+ top = $targetRect.Top
6558
+ right = $targetRect.Right
6559
+ bottom = $targetRect.Bottom
6560
+ width = $targetRect.Right - $targetRect.Left
6561
+ height = $targetRect.Bottom - $targetRect.Top
6562
+ }
6563
+ }
6564
+ }
6565
+
6566
+ $target
6567
+ }
6568
+
6569
+ function Write-CaptureResult([string]$method, [IntPtr]$hWnd, [string]$matchedBy) {
6570
+ $result = [ordered]@{
6571
+ method = $method
6572
+ target = New-CaptureTarget $hWnd $matchedBy
6573
+ }
6574
+ Write-Output ("CAPTURE_RESULT:" + ($result | ConvertTo-Json -Compress -Depth 6))
6575
+ }
6576
+
6577
+ $hwndVal = [IntPtr]::Zero
6578
+ $matchedBy = "fullscreen-fallback"
6579
+ $targetHwnd = '${hwnd || ""}'
6580
+ $processName = '${processName || ""}'
6581
+
6582
+ if ($targetHwnd -ne '') {
6583
+ if ($targetHwnd.StartsWith("0x")) {
6584
+ $hwndVal = [IntPtr][Convert]::ToInt64($targetHwnd, 16)
6585
+ } else {
6586
+ $hwndVal = [IntPtr][Convert]::ToInt64($targetHwnd, 10)
6587
+ }
6588
+ $matchedBy = "hwnd"
6589
+ } elseif ($processName -ne '') {
6590
+ $proc = Get-Process -Name $processName -ErrorAction SilentlyContinue | Where-Object {$_.MainWindowHandle -ne 0} | Select-Object -First 1
6591
+ if ($proc) {
6592
+ $hwndVal = $proc.MainWindowHandle
6593
+ $matchedBy = "process-main-window"
6594
+ } else {
6595
+ $proc = Get-Process -Name $processName -ErrorAction SilentlyContinue | Select-Object -First 1
6596
+ if ($proc) {
6597
+ $hwndVal = $proc.MainWindowHandle
6598
+ $matchedBy = "process-zero-main-window"
6599
+ }
6600
+ }
6601
+
6602
+ if ($hwndVal -eq [IntPtr]::Zero) {
6603
+ $windowMatch = Find-TopLevelWindowByProcessName $processName
6604
+ if ($windowMatch) {
6605
+ $hwndVal = $windowMatch.Hwnd
6606
+ $matchedBy = $windowMatch.MatchedBy
6607
+ }
6608
+ }
6609
+ }
6610
+
6611
+ if ($hwndVal -ne [IntPtr]::Zero) {
6612
+ if ([Win32]::IsIconic($hwndVal)) {
6613
+ [Win32]::ShowWindow($hwndVal, 9) | Out-Null # SW_RESTORE = 9
6614
+ Start-Sleep -Milliseconds 250
6615
+ }
6616
+
6617
+ $rect = New-Object Win32+RECT
6618
+ if ([Win32]::GetWindowRect($hwndVal, [ref]$rect)) {
6619
+ $width = $rect.Right - $rect.Left
6620
+ $height = $rect.Bottom - $rect.Top
6621
+ if ($width -gt 0 -and $height -gt 0) {
6622
+ $bitmap = New-Object System.Drawing.Bitmap($width, $height)
6623
+ $graphics = [System.Drawing.Graphics]::FromImage($bitmap)
6624
+ # PrintWindow renders the target window directly, regardless of
6625
+ # z-order, so an occluded or background window is still captured
6626
+ # correctly. SetForegroundWindow from a background process is
6627
+ # blocked by Windows, so CopyFromScreen would grab whatever sits
6628
+ # on top. PW_RENDERFULLCONTENT (0x2) handles modern/UWP windows.
6629
+ $hdc = $graphics.GetHdc()
6630
+ $printed = [Win32]::PrintWindow($hwndVal, $hdc, 2)
6631
+ $graphics.ReleaseHdc($hdc)
6632
+ $method = "printwindow"
6633
+
6634
+ # Black-frame guard: PrintWindow can return a near-black frame on
6635
+ # some hardware-accelerated / DirectComposition surfaces. Sample a
6636
+ # sparse grid; if it is essentially black, fall back to a foreground
6637
+ # CopyFromScreen grab so the capture self-heals on those drivers.
6638
+ $needFallback = -not $printed
6639
+ if (-not $needFallback) {
6640
+ $sum = 0.0
6641
+ $cnt = 0
6642
+ $stepX = [Math]::Max(1, [int]($width / 12))
6643
+ $stepY = [Math]::Max(1, [int]($height / 12))
6644
+ for ($sy = 0; $sy -lt $height; $sy += $stepY) {
6645
+ for ($sx = 0; $sx -lt $width; $sx += $stepX) {
6646
+ $px = $bitmap.GetPixel($sx, $sy)
6647
+ $sum += ($px.R + $px.G + $px.B) / 3.0
6648
+ $cnt++
6649
+ }
6650
+ }
6651
+ if ($cnt -gt 0 -and ($sum / $cnt) -lt 6) { $needFallback = $true }
6652
+ }
6653
+
6654
+ if ($needFallback) {
6655
+ [Win32]::SetForegroundWindow($hwndVal) | Out-Null
6656
+ Start-Sleep -Milliseconds 250
6657
+ $graphics.CopyFromScreen($rect.Left, $rect.Top, 0, 0, $bitmap.Size)
6658
+ $method = "copyfromscreen"
6659
+ }
6660
+ $bitmap.Save('${outputPath.replace(/\\/g, "\\\\")}', [System.Drawing.Imaging.ImageFormat]::Png)
6661
+ $graphics.Dispose()
6662
+ $bitmap.Dispose()
6663
+ Write-CaptureResult $method $hwndVal $matchedBy
6664
+ exit 0
6665
+ }
6666
+ }
6667
+ }
6668
+
6669
+ # Fallback: full primary screen
6670
+ $screen = [System.Windows.Forms.Screen]::PrimaryScreen.Bounds
6671
+ $bitmap = New-Object System.Drawing.Bitmap($screen.Width, $screen.Height)
6672
+ $graphics = [System.Drawing.Graphics]::FromImage($bitmap)
6673
+ $graphics.CopyFromScreen($screen.Location, [System.Drawing.Point]::Empty, $screen.Size)
6674
+ $bitmap.Save('${outputPath.replace(/\\/g, "\\\\")}', [System.Drawing.Imaging.ImageFormat]::Png)
6675
+ $graphics.Dispose()
6676
+ $bitmap.Dispose()
6677
+ Write-CaptureResult "fullscreen" $hwndVal $matchedBy
6678
+ `.trim()
6679
+ ],
6680
+ { stdio: "pipe", encoding: "utf8" }
6681
+ );
6682
+ const resultMatch = /CAPTURE_RESULT:(\{.*\})/.exec(stdout ?? "");
6683
+ if (resultMatch) {
6684
+ const parsed = JSON.parse(resultMatch[1]);
6685
+ return parsed;
6686
+ }
6687
+ const methodMatch = /CAPTURE_METHOD:(\w+)/.exec(stdout ?? "");
6688
+ return {
6689
+ method: methodMatch ? methodMatch[1] : "unknown",
6690
+ target: null
6691
+ };
6692
+ } else if (platform === "darwin") {
6693
+ if (hwnd) {
6694
+ const parsedHwnd = hwnd.startsWith("0x") ? parseInt(hwnd, 16) : parseInt(hwnd, 10);
6695
+ execFileSync2("screencapture", ["-l", String(parsedHwnd), outputPath], {
6696
+ stdio: "pipe"
6697
+ });
6698
+ return { method: "screencapture-window", target: null };
6699
+ } else if (processName) {
6700
+ try {
6701
+ const windowId = execFileSync2(
6702
+ "osascript",
6703
+ [
6704
+ "-e",
6705
+ `tell application "System Events" to get id of window 1 of process "${processName}"`
6706
+ ],
6707
+ { encoding: "utf8", stdio: ["pipe", "pipe", "ignore"] }
6708
+ ).trim();
6709
+ if (windowId && /^\d+$/.test(windowId)) {
6710
+ execFileSync2("screencapture", ["-l", windowId, outputPath], {
6711
+ stdio: "pipe"
6712
+ });
6713
+ return { method: "screencapture-window", target: null };
6714
+ }
6715
+ } catch {
6716
+ }
6717
+ execFileSync2("screencapture", ["-x", outputPath], { stdio: "pipe" });
6718
+ return { method: "screencapture-full", target: null };
6719
+ } else {
6720
+ execFileSync2("screencapture", ["-x", outputPath], { stdio: "pipe" });
6721
+ return { method: "screencapture-full", target: null };
6722
+ }
6723
+ } else {
6724
+ throw new Error(
6725
+ `Screen capture not supported on platform: ${platform}. Use zam-observer or provide --image.`
6726
+ );
5943
6727
  }
5944
- });
5945
- bridgeCommand.command("observe-ui-watch").description(
5946
- "Poll live UI observer watch reports for a ZAM learning session (JSON)"
5947
- ).requiredOption(
5948
- "--session <id>",
5949
- "ZAM session ID (also the observer log key)"
5950
- ).option("--after <n>", "Only return observations after this sequence").option("--limit <n>", "Maximum observations to return", "100").action(async (opts) => {
6728
+ }
6729
+ bridgeCommand.command("capture-ui").description("Capture a screenshot for agent-side vision analysis (JSON)").option("--session <id>", "ZAM session ID (for metadata)").option("--output <path>", "PNG output path (defaults to temp file)").option("--image <path>", "Skip capture; return an existing image instead").option("--hwnd <hwnd>", "Window handle (decimal or hex) to capture").option("--process-name <name>", "Process name to capture").action(async (opts) => {
5951
6730
  await withDb2(async (db) => {
5952
- const session = await db.prepare("SELECT id, execution_context FROM sessions WHERE id = ?").get(opts.session);
5953
- if (!session) {
5954
- jsonError(`Session not found: ${opts.session}`);
6731
+ const policy = await resolveObserverPolicy(db);
6732
+ const permission = {
6733
+ scope: policy.scope,
6734
+ consent: policy.consent,
6735
+ retention: policy.retention
6736
+ };
6737
+ const isProvided = Boolean(opts.image);
6738
+ if (!isProvided) {
6739
+ const pre = decidePreCapture(policy, {
6740
+ hasExplicitTarget: Boolean(opts.hwnd || opts.processName),
6741
+ requestedProcessName: opts.processName ?? null
6742
+ });
6743
+ if (!pre.allowed) {
6744
+ jsonOut2({
6745
+ sessionId: opts.session ?? null,
6746
+ granted: false,
6747
+ denied: true,
6748
+ denialReason: pre.denialReason,
6749
+ reason: pre.reason,
6750
+ capturedAt: (/* @__PURE__ */ new Date()).toISOString(),
6751
+ platform: process.platform,
6752
+ permission: { ...permission, granted: false }
6753
+ });
6754
+ return;
6755
+ }
5955
6756
  }
5956
- const after = opts.after === void 0 ? void 0 : parseNonNegativeIntegerOption("after", opts.after);
5957
- const limit = parseNonNegativeIntegerOption("limit", opts.limit);
5958
- const observations = readUiObservationLog(opts.session).filter((report) => after === void 0 || report.sequence > after).slice(0, limit);
5959
- const last = observations[observations.length - 1];
6757
+ const outputPath = opts.image ?? opts.output ?? join12(tmpdir(), `zam-capture-${randomBytes(4).toString("hex")}.png`);
6758
+ const captureResult = isProvided ? { method: "provided", target: null } : captureScreenshot(outputPath, opts.hwnd, opts.processName);
6759
+ if (!isProvided) {
6760
+ const post = decidePostCapture(policy, {
6761
+ method: captureResult.method,
6762
+ processName: captureResult.target?.processName ?? null,
6763
+ windowTitle: captureResult.target?.windowTitle ?? null
6764
+ });
6765
+ if (!post.allowed) {
6766
+ if (!opts.output) {
6767
+ try {
6768
+ rmSync2(outputPath, { force: true });
6769
+ } catch {
6770
+ }
6771
+ }
6772
+ jsonOut2({
6773
+ sessionId: opts.session ?? null,
6774
+ granted: false,
6775
+ denied: true,
6776
+ denialReason: post.denialReason,
6777
+ reason: post.reason,
6778
+ capturedAt: (/* @__PURE__ */ new Date()).toISOString(),
6779
+ platform: process.platform,
6780
+ permission: { ...permission, granted: false }
6781
+ });
6782
+ return;
6783
+ }
6784
+ }
6785
+ const imageBytes = readFileSync10(outputPath);
6786
+ const base64 = imageBytes.toString("base64");
5960
6787
  jsonOut2({
5961
- sessionId: opts.session,
5962
- executionContext: session.execution_context,
5963
- observationSource: "ui",
5964
- logExists: uiObservationLogExists(opts.session),
5965
- after: after ?? null,
5966
- count: observations.length,
5967
- nextSequence: last?.sequence ?? after ?? null,
5968
- observations
6788
+ sessionId: opts.session ?? null,
6789
+ granted: true,
6790
+ imagePath: outputPath,
6791
+ base64,
6792
+ mimeType: "image/png",
6793
+ captureMethod: captureResult.method,
6794
+ captureTarget: captureResult.target,
6795
+ capturedAt: (/* @__PURE__ */ new Date()).toISOString(),
6796
+ platform: process.platform,
6797
+ permission: { ...permission, granted: true }
5969
6798
  });
5970
6799
  });
5971
6800
  });
5972
- 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) => {
5973
- try {
5974
- const after = opts.after === void 0 ? void 0 : parseNonNegativeIntegerOption("after", opts.after);
5975
- const limit = parseNonNegativeIntegerOption("limit", opts.limit);
5976
- const observations = readUiObservationLog(opts.session).filter((report) => after === void 0 || report.sequence > after).slice(0, limit);
5977
- const last = observations[observations.length - 1];
6801
+ bridgeCommand.command("get-observer-policy").description(
6802
+ "Report the resolved observer policy so an agent can check before capturing (JSON)"
6803
+ ).action(async () => {
6804
+ await withDb2(async (db) => {
6805
+ const policy = await resolveObserverPolicy(db);
5978
6806
  jsonOut2({
5979
- sessionId: opts.session,
5980
- after: after ?? null,
5981
- count: observations.length,
5982
- nextSequence: last?.sequence ?? after ?? null,
5983
- observations
6807
+ scope: policy.scope,
6808
+ consent: policy.consent,
6809
+ retention: policy.retention,
6810
+ allowlist: policy.allowlist,
6811
+ denylist: policy.denylist,
6812
+ redactWindowTitles: policy.redactWindowTitles,
6813
+ audioOptIn: policy.audioOptIn,
6814
+ builtInSensitiveAlwaysRefused: true,
6815
+ builtInSensitiveMatchers: [...BUILT_IN_SENSITIVE_MATCHERS]
5984
6816
  });
5985
- } catch (err) {
5986
- jsonError(err.message);
5987
- }
6817
+ });
5988
6818
  });
5989
- bridgeCommand.command("observe-ui-snapshot").description(
5990
- "Analyze a captured UI snapshot with the configured vision LLM (JSON)"
5991
- ).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) => {
5992
- const sequence = parseNonNegativeIntegerOption("sequence", opts.sequence);
5993
- const processId = opts.processId === void 0 ? void 0 : parseNonNegativeIntegerOption("process-id", opts.processId);
5994
- const maxTokens = opts.maxTokens === void 0 ? void 0 : parseNonNegativeIntegerOption("max-tokens", opts.maxTokens);
5995
- const hardTimeoutMs = opts.timeout === void 0 ? void 0 : parseNonNegativeIntegerOption("timeout", opts.timeout);
6819
+ bridgeCommand.command("sync-observer-policy").description(
6820
+ "Write the resolved observer policy to the native sidecar file (JSON)"
6821
+ ).action(async () => {
5996
6822
  await withDb2(async (db) => {
5997
- const report = await observeUiSnapshotViaLLM(db, {
5998
- sessionId: opts.session,
5999
- sequence,
6000
- observedFrom: opts.observedFrom,
6001
- observedTo: opts.observedTo,
6002
- imagePath: opts.image,
6003
- application: {
6004
- processName: opts.processName,
6005
- processId,
6006
- windowTitle: opts.windowTitle
6007
- },
6008
- evidenceRef: opts.evidenceRef,
6009
- redacted: opts.redacted === true,
6010
- model: opts.model,
6011
- maxTokens,
6012
- hardTimeoutMs
6013
- });
6014
- if (opts.writeLog === true) {
6015
- appendUiObservationReport(report);
6016
- }
6017
- jsonOut2(report);
6823
+ const { path, policy } = await syncObserverSidecarPolicy(db);
6824
+ jsonOut2({ synced: true, path, policy });
6018
6825
  });
6019
6826
  });
6020
6827
  bridgeCommand.command("check-llm").description("Check if LLM is enabled and online (JSON)").action(async () => {
@@ -6698,26 +7505,26 @@ async function setupTurso(urlArg, tokenArg, mode) {
6698
7505
 
6699
7506
  // src/cli/commands/git-sync.ts
6700
7507
  import { execSync as execSync4 } from "child_process";
6701
- import { chmodSync as chmodSync2, existsSync as existsSync13, writeFileSync as writeFileSync5 } from "fs";
6702
- import { join as join12 } from "path";
7508
+ import { chmodSync as chmodSync2, existsSync as existsSync13, writeFileSync as writeFileSync6 } from "fs";
7509
+ import { join as join13 } from "path";
6703
7510
  import { Command as Command5 } from "commander";
6704
7511
  function installHook2() {
6705
- const gitDir = join12(process.cwd(), ".git");
7512
+ const gitDir = join13(process.cwd(), ".git");
6706
7513
  if (!existsSync13(gitDir)) {
6707
7514
  console.error(
6708
7515
  "Error: Current directory is not the root of a Git repository."
6709
7516
  );
6710
7517
  process.exit(1);
6711
7518
  }
6712
- const hooksDir = join12(gitDir, "hooks");
6713
- const hookPath = join12(hooksDir, "post-commit");
7519
+ const hooksDir = join13(gitDir, "hooks");
7520
+ const hookPath = join13(hooksDir, "post-commit");
6714
7521
  const hookContent = `#!/bin/sh
6715
7522
  # ZAM Spaced Repetition Auto-Stale Hook
6716
7523
  # Triggered automatically on git commits to decay modified concept cards.
6717
7524
  zam git-sync --commit HEAD --quiet
6718
7525
  `;
6719
7526
  try {
6720
- writeFileSync5(hookPath, hookContent, { encoding: "utf-8", flag: "w" });
7527
+ writeFileSync6(hookPath, hookContent, { encoding: "utf-8", flag: "w" });
6721
7528
  try {
6722
7529
  chmodSync2(hookPath, "755");
6723
7530
  } catch (_e) {
@@ -6819,7 +7626,7 @@ ZAM Auto-Stale Complete: Scanned ${changedFiles.length} file(s).`
6819
7626
  });
6820
7627
 
6821
7628
  // src/cli/commands/goal.ts
6822
- import { existsSync as existsSync14, mkdirSync as mkdirSync7 } from "fs";
7629
+ import { existsSync as existsSync14, mkdirSync as mkdirSync8 } from "fs";
6823
7630
  import { resolve as resolve3 } from "path";
6824
7631
  import { input as input2 } from "@inquirer/prompts";
6825
7632
  import { Command as Command6 } from "commander";
@@ -6945,7 +7752,7 @@ ${"\u2500".repeat(50)}`);
6945
7752
  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) => {
6946
7753
  const goalsDir = await resolveGoalsDir();
6947
7754
  if (!existsSync14(goalsDir)) {
6948
- mkdirSync7(goalsDir, { recursive: true });
7755
+ mkdirSync8(goalsDir, { recursive: true });
6949
7756
  }
6950
7757
  let slug = opts.slug;
6951
7758
  let title = opts.title;
@@ -7004,9 +7811,9 @@ goalCommand.command("status <slug> <status>").description("Update a goal's statu
7004
7811
  });
7005
7812
 
7006
7813
  // src/cli/commands/init.ts
7007
- import { existsSync as existsSync15, mkdirSync as mkdirSync8, writeFileSync as writeFileSync6 } from "fs";
7814
+ import { existsSync as existsSync15, mkdirSync as mkdirSync9, writeFileSync as writeFileSync7 } from "fs";
7008
7815
  import { homedir as homedir9 } from "os";
7009
- import { join as join13 } from "path";
7816
+ import { join as join14 } from "path";
7010
7817
  import { confirm, input as input3 } from "@inquirer/prompts";
7011
7818
  import { Command as Command7 } from "commander";
7012
7819
  var HOME2 = homedir9();
@@ -7014,12 +7821,12 @@ function printLine(char = "\u2550", len = 60, color = "\x1B[36m") {
7014
7821
  console.log(`${color}${char.repeat(len)}\x1B[0m`);
7015
7822
  }
7016
7823
  function bootstrapSandboxWorkspace(workspaceDir) {
7017
- mkdirSync8(join13(workspaceDir, "beliefs"), { recursive: true });
7018
- mkdirSync8(join13(workspaceDir, "goals"), { recursive: true });
7019
- mkdirSync8(join13(workspaceDir, "skills"), { recursive: true });
7020
- const worldviewFile = join13(workspaceDir, "beliefs", "worldview.md");
7824
+ mkdirSync9(join14(workspaceDir, "beliefs"), { recursive: true });
7825
+ mkdirSync9(join14(workspaceDir, "goals"), { recursive: true });
7826
+ mkdirSync9(join14(workspaceDir, "skills"), { recursive: true });
7827
+ const worldviewFile = join14(workspaceDir, "beliefs", "worldview.md");
7021
7828
  if (!existsSync15(worldviewFile)) {
7022
- writeFileSync6(
7829
+ writeFileSync7(
7023
7830
  worldviewFile,
7024
7831
  `# Personal Worldview
7025
7832
 
@@ -7031,9 +7838,9 @@ Here, I declare the core concepts and principles I want to master.
7031
7838
  "utf8"
7032
7839
  );
7033
7840
  }
7034
- const goalsFile = join13(workspaceDir, "goals", "goals.md");
7841
+ const goalsFile = join14(workspaceDir, "goals", "goals.md");
7035
7842
  if (!existsSync15(goalsFile)) {
7036
- writeFileSync6(
7843
+ writeFileSync7(
7037
7844
  goalsFile,
7038
7845
  `# Personal Goals
7039
7846
 
@@ -7055,7 +7862,7 @@ var initCommand = new Command7("init").description("Launch the guided interactiv
7055
7862
  );
7056
7863
  printLine();
7057
7864
  console.log("\n\x1B[1m[1/5] Setting up Local Workspace Sandbox\x1B[0m");
7058
- const defaultWorkspace = join13(HOME2, "Documents", "zam");
7865
+ const defaultWorkspace = join14(HOME2, "Documents", "zam");
7059
7866
  const workspacePath = await input3({
7060
7867
  message: "Choose your ZAM workspace directory:",
7061
7868
  default: defaultWorkspace
@@ -7752,10 +8559,10 @@ ${"\u2550".repeat(50)}`);
7752
8559
  });
7753
8560
 
7754
8561
  // src/cli/commands/monitor.ts
7755
- import { execFileSync as execFileSync2, execSync as execSync5 } from "child_process";
7756
- import { unlinkSync, writeFileSync as writeFileSync7 } from "fs";
7757
- import { tmpdir } from "os";
7758
- import { basename as basename3, join as join14 } from "path";
8562
+ import { execFileSync as execFileSync3, execSync as execSync5 } from "child_process";
8563
+ import { unlinkSync, writeFileSync as writeFileSync8 } from "fs";
8564
+ import { tmpdir as tmpdir2 } from "os";
8565
+ import { basename as basename3, join as join15 } from "path";
7759
8566
  import { Command as Command9 } from "commander";
7760
8567
  function isPowerShellShell(shell) {
7761
8568
  return shell === "pwsh" || shell === "powershell";
@@ -7906,11 +8713,23 @@ monitorCommand.command("status").description("Show monitoring status for a sessi
7906
8713
  console.log(` To: ${result.timeSpan.end}`);
7907
8714
  }
7908
8715
  });
8716
+ function selectWindowsExecutable(results, pathext = process.env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD") {
8717
+ if (results.length === 0) return null;
8718
+ const extensions = pathext.split(";").map((ext) => ext.trim().toLowerCase()).filter(Boolean);
8719
+ const runnable = results.find(
8720
+ (result) => extensions.some((ext) => result.toLowerCase().endsWith(ext))
8721
+ );
8722
+ return runnable ?? results[0];
8723
+ }
7909
8724
  function findExecutable(command) {
7910
8725
  try {
7911
8726
  const lookup = process.platform === "win32" ? `where.exe ${command}` : `command -v ${command}`;
7912
- const result = execSync5(lookup, { encoding: "utf-8" }).split(/\r?\n/)[0]?.trim();
7913
- return result || null;
8727
+ const results = execSync5(lookup, { encoding: "utf-8" }).split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
8728
+ if (results.length === 0) return null;
8729
+ if (process.platform === "win32") {
8730
+ return selectWindowsExecutable(results);
8731
+ }
8732
+ return results[0];
7914
8733
  } catch {
7915
8734
  return null;
7916
8735
  }
@@ -7920,8 +8739,8 @@ function resolveZamInvocation(shell) {
7920
8739
  if (installed) {
7921
8740
  return isPowerShellShell(shell) ? `& ${psSingleQuoted2(installed)}` : installed;
7922
8741
  }
7923
- const projectRoot = join14(import.meta.dirname, "..", "..", "..");
7924
- const cliSource = join14(projectRoot, "src/cli/index.ts");
8742
+ const projectRoot = join15(import.meta.dirname, "..", "..", "..");
8743
+ const cliSource = join15(projectRoot, "src/cli/index.ts");
7925
8744
  if (isPowerShellShell(shell)) {
7926
8745
  return `& npx --prefix ${psSingleQuoted2(projectRoot)} tsx ${psSingleQuoted2(cliSource)}`;
7927
8746
  }
@@ -8011,9 +8830,9 @@ end tell` : `tell application "Terminal"
8011
8830
  activate
8012
8831
  do script "${escaped}"
8013
8832
  end tell`;
8014
- const tmpFile = join14(tmpdir(), `zam-monitor-${sessionId}.scpt`);
8833
+ const tmpFile = join15(tmpdir2(), `zam-monitor-${sessionId}.scpt`);
8015
8834
  try {
8016
- writeFileSync7(tmpFile, appleScript);
8835
+ writeFileSync8(tmpFile, appleScript);
8017
8836
  execSync5(`osascript ${JSON.stringify(tmpFile)}`, { stdio: "ignore" });
8018
8837
  console.log(
8019
8838
  `Opened ${useIterm ? "iTerm2" : "Terminal.app"} window with monitoring for session ${sessionId}`
@@ -8040,9 +8859,10 @@ function openWindowsPowerShell(shellSetup, sessionId, dir, requestedShell) {
8040
8859
  `-FilePath ${psSingleQuoted2(executable)}`,
8041
8860
  `-ArgumentList @('-NoExit','-NoProfile','-Command',${psSingleQuoted2(shellSetup)})`
8042
8861
  ].join(" ");
8862
+ const launcher = findExecutable("pwsh.exe") ? "pwsh.exe" : "powershell.exe";
8043
8863
  try {
8044
- execFileSync2(
8045
- "powershell.exe",
8864
+ execFileSync3(
8865
+ launcher,
8046
8866
  ["-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", startCommand],
8047
8867
  {
8048
8868
  stdio: "ignore"
@@ -8061,10 +8881,73 @@ Run this manually in a new PowerShell terminal:
8061
8881
  }
8062
8882
  }
8063
8883
 
8884
+ // src/cli/commands/observer.ts
8885
+ import { Command as Command10 } from "commander";
8886
+ var observerCommand = new Command10("observer").description(
8887
+ "Configure what the UI observer may capture (Layer 2 policy)"
8888
+ );
8889
+ function applyObserverListChange(current, entry, op) {
8890
+ const normalized = entry.trim().toLowerCase();
8891
+ const list = parseObserverList(current);
8892
+ const next = op === "add" ? [.../* @__PURE__ */ new Set([...list, normalized])] : list.filter((item) => item !== normalized);
8893
+ return next.join(",");
8894
+ }
8895
+ observerCommand.command("status").description("Show the active observer policy").option("--json", "Output as JSON").action(async (opts) => {
8896
+ await withDb(async (db) => {
8897
+ const policy = await resolveObserverPolicy(db);
8898
+ if (opts.json) {
8899
+ console.log(JSON.stringify(policy, null, 2));
8900
+ return;
8901
+ }
8902
+ console.log("Observer policy:\n");
8903
+ console.log(` Scope: ${policy.scope}`);
8904
+ console.log(` Consent: ${policy.consent}`);
8905
+ console.log(` Retention: ${policy.retention}`);
8906
+ console.log(
8907
+ ` Allowlist: ${policy.allowlist.length ? policy.allowlist.join(", ") : "(any targeted window)"}`
8908
+ );
8909
+ console.log(
8910
+ ` Denylist: ${policy.denylist.length ? policy.denylist.join(", ") : "(none)"}`
8911
+ );
8912
+ console.log(` Redact titles: ${policy.redactWindowTitles}`);
8913
+ console.log(
8914
+ "\nBuilt-in sensitive surfaces (password managers, auth/UAC dialogs, banking) are always refused and cannot be allowlisted."
8915
+ );
8916
+ });
8917
+ });
8918
+ observerCommand.command("grant <process>").description(
8919
+ "Allow the observer to capture a process (adds it to observer.allowlist)"
8920
+ ).action(async (processName) => {
8921
+ await withDb(async (db) => {
8922
+ const next = applyObserverListChange(
8923
+ await getSetting(db, "observer.allowlist"),
8924
+ processName,
8925
+ "add"
8926
+ );
8927
+ await setSetting(db, "observer.allowlist", next);
8928
+ await syncObserverSidecarPolicy(db);
8929
+ console.log(`Granted: ${processName.trim().toLowerCase()}`);
8930
+ console.log(`observer.allowlist = ${next || "(empty)"}`);
8931
+ });
8932
+ });
8933
+ observerCommand.command("revoke <process>").description("Remove a process from observer.allowlist").action(async (processName) => {
8934
+ await withDb(async (db) => {
8935
+ const next = applyObserverListChange(
8936
+ await getSetting(db, "observer.allowlist"),
8937
+ processName,
8938
+ "remove"
8939
+ );
8940
+ await setSetting(db, "observer.allowlist", next);
8941
+ await syncObserverSidecarPolicy(db);
8942
+ console.log(`Revoked: ${processName.trim().toLowerCase()}`);
8943
+ console.log(`observer.allowlist = ${next || "(empty)"}`);
8944
+ });
8945
+ });
8946
+
8064
8947
  // src/cli/commands/profile.ts
8065
8948
  import { homedir as homedir10 } from "os";
8066
- import { dirname as dirname5, join as join15, resolve as resolve4 } from "path";
8067
- import { Command as Command10 } from "commander";
8949
+ import { dirname as dirname5, join as join16, resolve as resolve4 } from "path";
8950
+ import { Command as Command11 } from "commander";
8068
8951
  var C2 = {
8069
8952
  reset: "\x1B[0m",
8070
8953
  bold: "\x1B[1m",
@@ -8073,7 +8956,7 @@ var C2 = {
8073
8956
  green: "\x1B[32m"
8074
8957
  };
8075
8958
  function defaultPersonalDir() {
8076
- return join15(homedir10(), "Documents", "zam");
8959
+ return join16(homedir10(), "Documents", "zam");
8077
8960
  }
8078
8961
  function render(profile) {
8079
8962
  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}`;
@@ -8084,7 +8967,7 @@ function render(profile) {
8084
8967
  console.log(` data dir: ${C2.cyan}${profile.dataDir}${C2.reset}`);
8085
8968
  console.log(` database: ${C2.cyan}${profile.dbPath}${C2.reset}`);
8086
8969
  }
8087
- var profileCommand = new Command10("profile").description("Show or change this machine's ZAM install profile").option("--mode <mode>", "Set install mode: developer | default").option("--dir <path>", "Set the personal-content folder").option("--json", "Output as JSON").action(async (opts) => {
8970
+ var profileCommand = new Command11("profile").description("Show or change this machine's ZAM install profile").option("--mode <mode>", "Set install mode: developer | default").option("--dir <path>", "Set the personal-content folder").option("--json", "Output as JSON").action(async (opts) => {
8088
8971
  if (opts.mode && opts.mode !== "developer" && opts.mode !== "default") {
8089
8972
  console.error(`Invalid --mode: ${opts.mode}. Use developer or default.`);
8090
8973
  process.exit(1);
@@ -8120,8 +9003,8 @@ var profileCommand = new Command10("profile").description("Show or change this m
8120
9003
  });
8121
9004
 
8122
9005
  // src/cli/commands/review.ts
8123
- import { Command as Command11 } from "commander";
8124
- var reviewCommand = new Command11("review").description("Start an interactive review session").option("--user <id>", "User ID (default: whoami)").option("--max-new <n>", "Maximum new cards", "10").option("--max-reviews <n>", "Maximum review cards", "50").option("--no-resolve", "Skip resolving source_link into inline context").action(async (opts) => {
9006
+ import { Command as Command12 } from "commander";
9007
+ var reviewCommand = new Command12("review").description("Start an interactive review session").option("--user <id>", "User ID (default: whoami)").option("--max-new <n>", "Maximum new cards", "10").option("--max-reviews <n>", "Maximum review cards", "50").option("--no-resolve", "Skip resolving source_link into inline context").action(async (opts) => {
8125
9008
  let db;
8126
9009
  try {
8127
9010
  db = await openDatabase();
@@ -8234,10 +9117,10 @@ ${"\u2550".repeat(50)}`);
8234
9117
  });
8235
9118
 
8236
9119
  // src/cli/commands/session.ts
8237
- import { readFileSync as readFileSync10 } from "fs";
9120
+ import { readFileSync as readFileSync11 } from "fs";
8238
9121
  import { input as input6, select as select2 } from "@inquirer/prompts";
8239
- import { Command as Command12 } from "commander";
8240
- var sessionCommand = new Command12("session").description(
9122
+ import { Command as Command13 } from "commander";
9123
+ var sessionCommand = new Command13("session").description(
8241
9124
  "Manage learning sessions"
8242
9125
  );
8243
9126
  sessionCommand.command("start").description("Start a new learning session (review \u2192 task)").option("--user <id>", "User ID (default: whoami)").option("--task <description>", "Task description (interactive if omitted)").option(
@@ -8289,11 +9172,18 @@ sessionCommand.command("start").description("Start a new learning session (revie
8289
9172
  task,
8290
9173
  execution_context: opts.context
8291
9174
  });
9175
+ const observerHint = opts.context === "ui" && !await isObserverPolicyConfigured(db) ? OBSERVER_POLICY_UNSET_HINT : null;
8292
9176
  await db.close();
8293
9177
  if (opts.quiet) {
8294
9178
  console.log(session.id);
8295
9179
  } else if (opts.json) {
8296
- console.log(JSON.stringify(session, null, 2));
9180
+ console.log(
9181
+ JSON.stringify(
9182
+ observerHint ? { ...session, observerPolicyHint: observerHint } : session,
9183
+ null,
9184
+ 2
9185
+ )
9186
+ );
8297
9187
  } else {
8298
9188
  console.log(`
8299
9189
  Session started: ${session.id}`);
@@ -8301,6 +9191,10 @@ Session started: ${session.id}`);
8301
9191
  console.log(` Task: ${session.task}`);
8302
9192
  console.log(` Context: ${session.execution_context}`);
8303
9193
  console.log(` Started: ${session.started_at}`);
9194
+ if (observerHint) {
9195
+ console.log(`
9196
+ ${observerHint}`);
9197
+ }
8304
9198
  }
8305
9199
  } catch (err) {
8306
9200
  await db?.close();
@@ -8415,7 +9309,7 @@ function loadPatternFile(path) {
8415
9309
  if (!path) return [];
8416
9310
  let parsed;
8417
9311
  try {
8418
- parsed = JSON.parse(readFileSync10(path, "utf-8"));
9312
+ parsed = JSON.parse(readFileSync11(path, "utf-8"));
8419
9313
  } catch (err) {
8420
9314
  throw new Error(
8421
9315
  `Cannot read synthesis patterns from ${path}: ${err.message}`
@@ -8606,8 +9500,8 @@ sessionCommand.command("end").description("End a session and show summary").requ
8606
9500
 
8607
9501
  // src/cli/commands/settings.ts
8608
9502
  import { existsSync as existsSync16 } from "fs";
8609
- import { Command as Command13 } from "commander";
8610
- var settingsCommand = new Command13("settings").description(
9503
+ import { Command as Command14 } from "commander";
9504
+ var settingsCommand = new Command14("settings").description(
8611
9505
  "Manage user settings"
8612
9506
  );
8613
9507
  var BOOLEAN_SETTING_KEYS = /* @__PURE__ */ new Set(["llm.enabled", "llm.vision.enabled"]);
@@ -8661,6 +9555,9 @@ settingsCommand.command("set <key> <value>").description("Set a setting").option
8661
9555
  await withDb(async (db) => {
8662
9556
  const parsedVal = normalizeSettingValue(key, value);
8663
9557
  await setSetting(db, key, parsedVal);
9558
+ if (key.startsWith("observer.")) {
9559
+ await syncObserverSidecarPolicy(db);
9560
+ }
8664
9561
  if (!opts.quiet) {
8665
9562
  console.log(`Set ${key} = ${parsedVal}`);
8666
9563
  }
@@ -8669,6 +9566,9 @@ settingsCommand.command("set <key> <value>").description("Set a setting").option
8669
9566
  settingsCommand.command("delete <key>").description("Delete a setting").option("--quiet", "Suppress output").action(async (key, opts) => {
8670
9567
  await withDb(async (db) => {
8671
9568
  const deleted = await deleteSetting(db, key);
9569
+ if (key.startsWith("observer.")) {
9570
+ await syncObserverSidecarPolicy(db);
9571
+ }
8672
9572
  if (!opts.quiet) {
8673
9573
  if (deleted) {
8674
9574
  console.log(`Deleted: ${key}`);
@@ -8776,32 +9676,32 @@ settingsCommand.command("repos").description("Show or set Personal, Team, and Or
8776
9676
  });
8777
9677
 
8778
9678
  // src/cli/commands/setup.ts
8779
- import { copyFileSync as copyFileSync2, existsSync as existsSync17, mkdirSync as mkdirSync9, writeFileSync as writeFileSync8 } from "fs";
8780
- import { basename as basename4, dirname as dirname6, join as join16 } from "path";
9679
+ import { copyFileSync as copyFileSync2, existsSync as existsSync17, mkdirSync as mkdirSync10, writeFileSync as writeFileSync9 } from "fs";
9680
+ import { basename as basename4, dirname as dirname6, join as join17 } from "path";
8781
9681
  import { fileURLToPath as fileURLToPath2 } from "url";
8782
- import { Command as Command14 } from "commander";
9682
+ import { Command as Command15 } from "commander";
8783
9683
  var packageRoot = [
8784
9684
  fileURLToPath2(new URL("../..", import.meta.url)),
8785
9685
  fileURLToPath2(new URL("../../..", import.meta.url))
8786
- ].find((candidate) => existsSync17(join16(candidate, "package.json"))) ?? fileURLToPath2(new URL("../..", import.meta.url));
9686
+ ].find((candidate) => existsSync17(join17(candidate, "package.json"))) ?? fileURLToPath2(new URL("../..", import.meta.url));
8787
9687
  var SKILL_PAIRS = [
8788
9688
  {
8789
- from: join16(packageRoot, ".claude", "skills", "zam", "SKILL.md"),
8790
- to: join16(".claude", "skills", "zam", "SKILL.md")
9689
+ from: join17(packageRoot, ".claude", "skills", "zam", "SKILL.md"),
9690
+ to: join17(".claude", "skills", "zam", "SKILL.md")
8791
9691
  },
8792
9692
  {
8793
- from: join16(packageRoot, ".agent", "skills", "zam", "SKILL.md"),
8794
- to: join16(".agent", "skills", "zam", "SKILL.md")
9693
+ from: join17(packageRoot, ".agent", "skills", "zam", "SKILL.md"),
9694
+ to: join17(".agent", "skills", "zam", "SKILL.md")
8795
9695
  },
8796
9696
  {
8797
- from: join16(packageRoot, ".agents", "skills", "zam", "SKILL.md"),
8798
- to: join16(".agents", "skills", "zam", "SKILL.md")
9697
+ from: join17(packageRoot, ".agents", "skills", "zam", "SKILL.md"),
9698
+ to: join17(".agents", "skills", "zam", "SKILL.md")
8799
9699
  }
8800
9700
  ];
8801
9701
  function copySkills(force, cwd = process.cwd()) {
8802
9702
  let anyAction = false;
8803
9703
  for (const { from, to } of SKILL_PAIRS) {
8804
- const dest = join16(cwd, to);
9704
+ const dest = join17(cwd, to);
8805
9705
  if (!existsSync17(from)) {
8806
9706
  console.warn(` warn source not found, skipping: ${from}`);
8807
9707
  continue;
@@ -8810,7 +9710,7 @@ function copySkills(force, cwd = process.cwd()) {
8810
9710
  console.log(` skip ${to} (already present \xE2\u20AC\u201D use --force to update)`);
8811
9711
  continue;
8812
9712
  }
8813
- mkdirSync9(dirname6(dest), { recursive: true });
9713
+ mkdirSync10(dirname6(dest), { recursive: true });
8814
9714
  copyFileSync2(from, dest);
8815
9715
  console.log(` copy ${to}`);
8816
9716
  anyAction = true;
@@ -8821,13 +9721,25 @@ function copySkills(force, cwd = process.cwd()) {
8821
9721
  );
8822
9722
  }
8823
9723
  }
9724
+ function formatDatabaseInitTarget(target) {
9725
+ switch (target.kind) {
9726
+ case "local":
9727
+ return `ZAM database at ${target.location} (local SQLite)`;
9728
+ case "turso-remote":
9729
+ return `ZAM database via Turso remote at ${target.location}`;
9730
+ case "turso-native":
9731
+ return `ZAM database via Turso native driver at ${target.location}`;
9732
+ case "turso-replica":
9733
+ return `ZAM database replica at ${target.location} syncing from ${target.syncUrl}`;
9734
+ }
9735
+ }
8824
9736
  async function initDatabase(skipInit) {
8825
9737
  if (skipInit) return;
8826
9738
  try {
8827
- const dbPath = getDefaultDbPath();
9739
+ const target = getDatabaseTargetInfo();
8828
9740
  const db = await openDatabaseWithSync({ initialize: true });
8829
9741
  await db.close();
8830
- console.log(` init ZAM database at ${dbPath}`);
9742
+ console.log(` init ${formatDatabaseInitTarget(target)}`);
8831
9743
  } catch (err) {
8832
9744
  const msg = err.message;
8833
9745
  if (!msg.includes("already")) {
@@ -8839,13 +9751,13 @@ async function initDatabase(skipInit) {
8839
9751
  }
8840
9752
  function writeClaudeMd(skipClaudeMd, cwd = process.cwd()) {
8841
9753
  if (skipClaudeMd) return;
8842
- const dest = join16(cwd, "CLAUDE.md");
9754
+ const dest = join17(cwd, "CLAUDE.md");
8843
9755
  if (existsSync17(dest)) {
8844
9756
  console.log(` skip CLAUDE.md (already present)`);
8845
9757
  return;
8846
9758
  }
8847
9759
  const name = basename4(cwd);
8848
- writeFileSync8(
9760
+ writeFileSync9(
8849
9761
  dest,
8850
9762
  `# ZAM Personal Kernel \xE2\u20AC\u201D ${name}
8851
9763
 
@@ -8873,13 +9785,13 @@ Use \`zam connector setup turso\` to store cloud credentials in
8873
9785
  }
8874
9786
  function writeAgentsMd(skipAgentsMd, cwd = process.cwd()) {
8875
9787
  if (skipAgentsMd) return;
8876
- const dest = join16(cwd, "AGENTS.md");
9788
+ const dest = join17(cwd, "AGENTS.md");
8877
9789
  if (existsSync17(dest)) {
8878
9790
  console.log(` skip AGENTS.md (already present)`);
8879
9791
  return;
8880
9792
  }
8881
9793
  const name = basename4(cwd);
8882
- writeFileSync8(
9794
+ writeFileSync9(
8883
9795
  dest,
8884
9796
  `# ZAM Personal Kernel - ${name}
8885
9797
 
@@ -8912,7 +9824,7 @@ Codex discovers repository skills under \`.agents/skills/\`. Run
8912
9824
  );
8913
9825
  console.log(` write AGENTS.md`);
8914
9826
  }
8915
- var setupCommand = new Command14("setup").description(
9827
+ var setupCommand = new Command15("setup").description(
8916
9828
  "Distribute ZAM skill files into this personal instance and initialize the database"
8917
9829
  ).option(
8918
9830
  "--force",
@@ -8933,8 +9845,8 @@ var setupCommand = new Command14("setup").description(
8933
9845
  );
8934
9846
 
8935
9847
  // src/cli/commands/skill.ts
8936
- import { Command as Command15 } from "commander";
8937
- var skillCommand = new Command15("skill").description(
9848
+ import { Command as Command16 } from "commander";
9849
+ var skillCommand = new Command16("skill").description(
8938
9850
  "Manage agent skill entries (task recipes)"
8939
9851
  );
8940
9852
  skillCommand.command("list").description("List all agent skills").option("--json", "Output as JSON").action(async (opts) => {
@@ -9019,10 +9931,10 @@ skillCommand.command("add").description("Register a new agent skill").requiredOp
9019
9931
  });
9020
9932
 
9021
9933
  // src/cli/commands/snapshot.ts
9022
- import { existsSync as existsSync18, mkdirSync as mkdirSync10, readFileSync as readFileSync11, writeFileSync as writeFileSync9 } from "fs";
9934
+ import { existsSync as existsSync18, mkdirSync as mkdirSync11, readFileSync as readFileSync12, writeFileSync as writeFileSync10 } from "fs";
9023
9935
  import { homedir as homedir11 } from "os";
9024
- import { dirname as dirname7, join as join17 } from "path";
9025
- import { Command as Command16 } from "commander";
9936
+ import { dirname as dirname7, join as join18 } from "path";
9937
+ import { Command as Command17 } from "commander";
9026
9938
  function defaultOutName() {
9027
9939
  const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/\.\d+Z$/, "").replace(/:/g, "-");
9028
9940
  return `zam-snapshot-${stamp}.sql`;
@@ -9036,12 +9948,12 @@ function summarize(tables) {
9036
9948
  }
9037
9949
  return { total, nonEmpty };
9038
9950
  }
9039
- var exportCmd = new Command16("export").description("Write a portable SQL-text snapshot of the database").option("--out <file>", "Output file (use - for stdout)").action(async (opts) => {
9951
+ var exportCmd = new Command17("export").description("Write a portable SQL-text snapshot of the database").option("--out <file>", "Output file (use - for stdout)").action(async (opts) => {
9040
9952
  let db;
9041
9953
  try {
9042
9954
  db = await openDatabaseWithSync({ initialize: true });
9043
9955
  const snapshot = await exportSnapshot(db);
9044
- const personalDir = await getSetting(db, "personal.workspace_dir") || join17(homedir11(), "Documents", "zam");
9956
+ const personalDir = await getSetting(db, "personal.workspace_dir") || join18(homedir11(), "Documents", "zam");
9045
9957
  await db.close();
9046
9958
  db = void 0;
9047
9959
  const manifest = verifySnapshot(snapshot);
@@ -9050,12 +9962,12 @@ var exportCmd = new Command16("export").description("Write a portable SQL-text s
9050
9962
  process.stdout.write(snapshot);
9051
9963
  return;
9052
9964
  }
9053
- const out = opts.out ?? join17(personalDir, "snapshots", defaultOutName());
9965
+ const out = opts.out ?? join18(personalDir, "snapshots", defaultOutName());
9054
9966
  const dir = dirname7(out);
9055
9967
  if (dir && dir !== "." && !existsSync18(dir)) {
9056
- mkdirSync10(dir, { recursive: true });
9968
+ mkdirSync11(dir, { recursive: true });
9057
9969
  }
9058
- writeFileSync9(out, snapshot, "utf-8");
9970
+ writeFileSync10(out, snapshot, "utf-8");
9059
9971
  console.log(`Snapshot written: ${out}`);
9060
9972
  console.log(
9061
9973
  ` ${total} row(s)${nonEmpty.length ? ` \u2014 ${nonEmpty.join(", ")}` : ""}`
@@ -9066,14 +9978,14 @@ var exportCmd = new Command16("export").description("Write a portable SQL-text s
9066
9978
  process.exit(1);
9067
9979
  }
9068
9980
  });
9069
- 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) => {
9981
+ var importCmd = new Command17("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) => {
9070
9982
  let db;
9071
9983
  try {
9072
9984
  if (!existsSync18(file)) {
9073
9985
  console.error(`Error: Snapshot file not found: ${file}`);
9074
9986
  process.exit(1);
9075
9987
  }
9076
- const snapshot = readFileSync11(file, "utf-8");
9988
+ const snapshot = readFileSync12(file, "utf-8");
9077
9989
  db = await openDatabaseWithSync({ initialize: true });
9078
9990
  const result = await importSnapshot(db, snapshot, { force: opts.force });
9079
9991
  await db.close();
@@ -9089,13 +10001,13 @@ var importCmd = new Command16("import").description("Restore a snapshot into the
9089
10001
  process.exit(1);
9090
10002
  }
9091
10003
  });
9092
- var verifyCmd = new Command16("verify").description("Check a snapshot's manifest and checksum without importing").argument("<file>", "Snapshot file to verify").action((file) => {
10004
+ var verifyCmd = new Command17("verify").description("Check a snapshot's manifest and checksum without importing").argument("<file>", "Snapshot file to verify").action((file) => {
9093
10005
  try {
9094
10006
  if (!existsSync18(file)) {
9095
10007
  console.error(`Error: Snapshot file not found: ${file}`);
9096
10008
  process.exit(1);
9097
10009
  }
9098
- const manifest = verifySnapshot(readFileSync11(file, "utf-8"));
10010
+ const manifest = verifySnapshot(readFileSync12(file, "utf-8"));
9099
10011
  const { total, nonEmpty } = summarize(manifest.tables);
9100
10012
  console.log(`Valid snapshot (format v${manifest.version})`);
9101
10013
  console.log(` created: ${manifest.createdAt}`);
@@ -9107,11 +10019,11 @@ var verifyCmd = new Command16("verify").description("Check a snapshot's manifest
9107
10019
  process.exit(1);
9108
10020
  }
9109
10021
  });
9110
- var snapshotCommand = new Command16("snapshot").description("Export, import, or verify a portable database snapshot").addCommand(exportCmd).addCommand(importCmd).addCommand(verifyCmd);
10022
+ var snapshotCommand = new Command17("snapshot").description("Export, import, or verify a portable database snapshot").addCommand(exportCmd).addCommand(importCmd).addCommand(verifyCmd);
9111
10023
 
9112
10024
  // src/cli/commands/stats.ts
9113
- import { Command as Command17 } from "commander";
9114
- var statsCommand = new Command17("stats").description("Show learning dashboard for a user").option("--user <id>", "User ID (default: whoami)").option("--json", "Output as JSON").action(async (opts) => {
10025
+ import { Command as Command18 } from "commander";
10026
+ var statsCommand = new Command18("stats").description("Show learning dashboard for a user").option("--user <id>", "User ID (default: whoami)").option("--json", "Output as JSON").action(async (opts) => {
9115
10027
  await withDb(async (db) => {
9116
10028
  const userId = await resolveUser(opts, db);
9117
10029
  const stats = await getUserStats(db, userId);
@@ -9147,8 +10059,8 @@ var statsCommand = new Command17("stats").description("Show learning dashboard f
9147
10059
  });
9148
10060
 
9149
10061
  // src/cli/commands/token.ts
9150
- import { Command as Command18 } from "commander";
9151
- var tokenCommand = new Command18("token").description(
10062
+ import { Command as Command19 } from "commander";
10063
+ var tokenCommand = new Command19("token").description(
9152
10064
  "Manage knowledge tokens"
9153
10065
  );
9154
10066
  tokenCommand.command("register").description("Register a new knowledge token").requiredOption("--slug <slug>", "Unique token slug").requiredOption("--concept <concept>", "Concept description").option("--domain <domain>", "Knowledge domain", "").option("--bloom <level>", "Bloom taxonomy level (1-5)", "1").option("--source-link <link>", "Source file path or reference URL", "").option("--question <question>", "Specific question prompt for recall", "").option("--json", "Output as JSON").option("--quiet", "Suppress output (exit code only)").action(async (opts) => {
@@ -9437,9 +10349,9 @@ tokenCommand.command("status").description("Show full status of a token for a us
9437
10349
  import { spawn as spawn2, spawnSync } from "child_process";
9438
10350
  import { existsSync as existsSync19 } from "fs";
9439
10351
  import { homedir as homedir12 } from "os";
9440
- import { dirname as dirname8, join as join18 } from "path";
10352
+ import { dirname as dirname8, join as join19 } from "path";
9441
10353
  import { fileURLToPath as fileURLToPath3 } from "url";
9442
- import { Command as Command19 } from "commander";
10354
+ import { Command as Command20 } from "commander";
9443
10355
  var C3 = {
9444
10356
  reset: "\x1B[0m",
9445
10357
  red: "\x1B[31m",
@@ -9453,8 +10365,8 @@ function findDesktopDir() {
9453
10365
  for (const start of starts) {
9454
10366
  let dir = start;
9455
10367
  for (let i = 0; i < 10; i++) {
9456
- if (existsSync19(join18(dir, "desktop", "src-tauri", "tauri.conf.json"))) {
9457
- return join18(dir, "desktop");
10368
+ if (existsSync19(join19(dir, "desktop", "src-tauri", "tauri.conf.json"))) {
10369
+ return join19(dir, "desktop");
9458
10370
  }
9459
10371
  const parent = dirname8(dir);
9460
10372
  if (parent === dir) break;
@@ -9464,18 +10376,18 @@ function findDesktopDir() {
9464
10376
  return null;
9465
10377
  }
9466
10378
  function findBuiltApp(desktopDir) {
9467
- const releaseDir = join18(desktopDir, "src-tauri", "target", "release");
10379
+ const releaseDir = join19(desktopDir, "src-tauri", "target", "release");
9468
10380
  if (process.platform === "win32") {
9469
10381
  for (const name of ["ZAM.exe", "zam.exe", "zam-desktop.exe"]) {
9470
- const p = join18(releaseDir, name);
10382
+ const p = join19(releaseDir, name);
9471
10383
  if (existsSync19(p)) return p;
9472
10384
  }
9473
10385
  } else if (process.platform === "darwin") {
9474
- const app = join18(releaseDir, "bundle", "macos", "ZAM.app");
10386
+ const app = join19(releaseDir, "bundle", "macos", "ZAM.app");
9475
10387
  if (existsSync19(app)) return app;
9476
10388
  } else {
9477
10389
  for (const name of ["zam", "ZAM", "zam-desktop"]) {
9478
- const p = join18(releaseDir, name);
10390
+ const p = join19(releaseDir, name);
9479
10391
  if (existsSync19(p)) return p;
9480
10392
  }
9481
10393
  }
@@ -9483,10 +10395,10 @@ function findBuiltApp(desktopDir) {
9483
10395
  }
9484
10396
  function findInstalledApp() {
9485
10397
  const candidates = process.platform === "win32" ? [
9486
- process.env.LOCALAPPDATA && join18(process.env.LOCALAPPDATA, "Programs", "ZAM", "ZAM.exe"),
9487
- process.env.ProgramFiles && join18(process.env.ProgramFiles, "ZAM", "ZAM.exe"),
9488
- process.env["ProgramFiles(x86)"] && join18(process.env["ProgramFiles(x86)"], "ZAM", "ZAM.exe")
9489
- ] : process.platform === "darwin" ? ["/Applications/ZAM.app", join18(homedir12(), "Applications", "ZAM.app")] : ["/opt/ZAM/zam", "/usr/bin/zam-desktop"];
10398
+ process.env.LOCALAPPDATA && join19(process.env.LOCALAPPDATA, "Programs", "ZAM", "ZAM.exe"),
10399
+ process.env.ProgramFiles && join19(process.env.ProgramFiles, "ZAM", "ZAM.exe"),
10400
+ process.env["ProgramFiles(x86)"] && join19(process.env["ProgramFiles(x86)"], "ZAM", "ZAM.exe")
10401
+ ] : process.platform === "darwin" ? ["/Applications/ZAM.app", join19(homedir12(), "Applications", "ZAM.app")] : ["/opt/ZAM/zam", "/usr/bin/zam-desktop"];
9490
10402
  return candidates.find((candidate) => candidate && existsSync19(candidate)) || null;
9491
10403
  }
9492
10404
  function runNpm(args, opts) {
@@ -9498,7 +10410,7 @@ function runNpm(args, opts) {
9498
10410
  return res.status ?? 1;
9499
10411
  }
9500
10412
  function ensureDesktopDeps(desktopDir) {
9501
- if (existsSync19(join18(desktopDir, "node_modules"))) return true;
10413
+ if (existsSync19(join19(desktopDir, "node_modules"))) return true;
9502
10414
  console.log(
9503
10415
  `${C3.cyan}Installing desktop dependencies (one-time)...${C3.reset}`
9504
10416
  );
@@ -9524,7 +10436,7 @@ function requireRust() {
9524
10436
  function hasMsvcBuildTools() {
9525
10437
  if (process.platform !== "win32") return true;
9526
10438
  const pf86 = process.env["ProgramFiles(x86)"] || "C:\\Program Files (x86)";
9527
- const vswhere = join18(
10439
+ const vswhere = join19(
9528
10440
  pf86,
9529
10441
  "Microsoft Visual Studio",
9530
10442
  "Installer",
@@ -9562,7 +10474,7 @@ function requireMsvcOnWindows() {
9562
10474
  return false;
9563
10475
  }
9564
10476
  function warnIfCliMissing(repoRoot) {
9565
- if (!existsSync19(join18(repoRoot, "dist", "cli", "index.js"))) {
10477
+ if (!existsSync19(join19(repoRoot, "dist", "cli", "index.js"))) {
9566
10478
  console.warn(
9567
10479
  `${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}`
9568
10480
  );
@@ -9619,7 +10531,7 @@ function createShortcuts(appPath, repoRoot) {
9619
10531
  console.error(`${C3.red}\u2717 Could not create shortcuts.${C3.reset}`);
9620
10532
  }
9621
10533
  }
9622
- var uiCommand = new Command19("ui").description("Launch the ZAM Desktop GUI (Active-Recall Studio)").option("--dev", "Run in hot-reload development mode (needs Rust)").option(
10534
+ var uiCommand = new Command20("ui").description("Launch the ZAM Desktop GUI (Active-Recall Studio)").option("--dev", "Run in hot-reload development mode (needs Rust)").option(
9623
10535
  "--build",
9624
10536
  "Build the native installer for your OS (needs Rust, one-time)"
9625
10537
  ).option("--shortcut", "Create Desktop + Start-menu shortcuts to the GUI").action((opts) => {
@@ -9646,7 +10558,7 @@ var uiCommand = new Command19("ui").description("Launch the ZAM Desktop GUI (Act
9646
10558
  );
9647
10559
  const code = runNpm(["run", "tauri", "build"], { cwd: desktopDir });
9648
10560
  if (code === 0) {
9649
- const bundle = join18(
10561
+ const bundle = join19(
9650
10562
  desktopDir,
9651
10563
  "src-tauri",
9652
10564
  "target",
@@ -9722,10 +10634,12 @@ ${C3.dim}After that, 'zam ui' launches it directly.${C3.reset}`
9722
10634
  });
9723
10635
 
9724
10636
  // src/cli/commands/update.ts
9725
- import { readFileSync as readFileSync12 } from "fs";
9726
- import { dirname as dirname9, join as join19 } from "path";
10637
+ import { spawnSync as spawnSync2 } from "child_process";
10638
+ import { existsSync as existsSync20, readFileSync as readFileSync13, realpathSync } from "fs";
10639
+ import { dirname as dirname9, join as join20 } from "path";
9727
10640
  import { fileURLToPath as fileURLToPath4 } from "url";
9728
- import { Command as Command20 } from "commander";
10641
+ import { confirm as confirm3 } from "@inquirer/prompts";
10642
+ import { Command as Command21 } from "commander";
9729
10643
  var GITHUB_REPO = "zam-os/zam";
9730
10644
  var CHANNELS = [
9731
10645
  "developer",
@@ -9739,6 +10653,7 @@ var C4 = {
9739
10653
  cyan: "\x1B[36m",
9740
10654
  dim: "\x1B[2m",
9741
10655
  green: "\x1B[32m",
10656
+ red: "\x1B[31m",
9742
10657
  yellow: "\x1B[33m"
9743
10658
  };
9744
10659
  function currentVersion() {
@@ -9746,7 +10661,7 @@ function currentVersion() {
9746
10661
  for (const up of ["..", "../..", "../../.."]) {
9747
10662
  try {
9748
10663
  const pkg2 = JSON.parse(
9749
- readFileSync12(join19(here, up, "package.json"), "utf-8")
10664
+ readFileSync13(join20(here, up, "package.json"), "utf-8")
9750
10665
  );
9751
10666
  if (pkg2.version) return pkg2.version;
9752
10667
  } catch {
@@ -9754,6 +10669,16 @@ function currentVersion() {
9754
10669
  }
9755
10670
  return "0.0.0";
9756
10671
  }
10672
+ function versionAt(dir) {
10673
+ try {
10674
+ const pkg2 = JSON.parse(
10675
+ readFileSync13(join20(dir, "package.json"), "utf-8")
10676
+ );
10677
+ return pkg2.version ?? "unknown";
10678
+ } catch {
10679
+ return "unknown";
10680
+ }
10681
+ }
9757
10682
  async function fetchLatestVersion(repo) {
9758
10683
  const res = await fetch(
9759
10684
  `https://api.github.com/repos/${repo}/releases/latest`,
@@ -9792,7 +10717,7 @@ function render2(decision) {
9792
10717
  );
9793
10718
  }
9794
10719
  }
9795
- var checkCmd = new Command20("check").description("Check whether a newer ZAM has been released").option(
10720
+ var checkCmd = new Command21("check").description("Check whether a newer ZAM has been released").option(
9796
10721
  "--latest <version>",
9797
10722
  "Compare against this version instead of fetching"
9798
10723
  ).option("--channel <channel>", "Override the detected install channel").option("--json", "Output as JSON").action(
@@ -9823,11 +10748,154 @@ var checkCmd = new Command20("check").description("Check whether a newer ZAM has
9823
10748
  }
9824
10749
  }
9825
10750
  );
9826
- var updateCommand = new Command20("update").description("Check for ZAM updates").addCommand(checkCmd);
10751
+ function findSourceRepo() {
10752
+ let dir = realpathSync(dirname9(fileURLToPath4(import.meta.url)));
10753
+ let parent = dirname9(dir);
10754
+ while (parent !== dir) {
10755
+ if (existsSync20(join20(dir, ".git"))) return dir;
10756
+ dir = parent;
10757
+ parent = dirname9(dir);
10758
+ }
10759
+ return existsSync20(join20(dir, ".git")) ? dir : null;
10760
+ }
10761
+ function runGit(cwd, args, capture) {
10762
+ const res = spawnSync2("git", args, {
10763
+ cwd,
10764
+ stdio: capture ? ["ignore", "pipe", "pipe"] : "inherit",
10765
+ encoding: "utf8"
10766
+ });
10767
+ return { ok: res.status === 0, out: (res.stdout ?? "").trim() };
10768
+ }
10769
+ function runNpm2(args, cwd) {
10770
+ const res = spawnSync2("npm", args, {
10771
+ cwd,
10772
+ stdio: "inherit",
10773
+ shell: process.platform === "win32"
10774
+ });
10775
+ return res.status ?? 1;
10776
+ }
10777
+ function runShell(command) {
10778
+ const res = spawnSync2(command, { stdio: "inherit", shell: true });
10779
+ return res.status === 0;
10780
+ }
10781
+ function applyDeveloperUpdate(force) {
10782
+ if (!hasCommand("git")) {
10783
+ console.error(`${C4.red}\u2717${C4.reset} git was not found on PATH.`);
10784
+ process.exit(1);
10785
+ }
10786
+ const src = findSourceRepo();
10787
+ if (!src) {
10788
+ console.error(
10789
+ `${C4.red}\u2717${C4.reset} Could not locate the ZAM source checkout to update.`
10790
+ );
10791
+ process.exit(1);
10792
+ }
10793
+ const status = runGit(src, ["status", "--porcelain"], true);
10794
+ if (status.ok && status.out && !force) {
10795
+ console.error(
10796
+ `${C4.red}\u2717${C4.reset} The source checkout has uncommitted changes:
10797
+ ${status.out}
10798
+ Commit or stash them, or re-run with ${C4.cyan}--force${C4.reset}.`
10799
+ );
10800
+ process.exit(1);
10801
+ }
10802
+ console.log(
10803
+ `${C4.dim}\u2192 git pull --ff-only${C4.reset} ${C4.dim}(${src})${C4.reset}`
10804
+ );
10805
+ if (!runGit(src, ["pull", "--ff-only"], false).ok) {
10806
+ console.error(
10807
+ `${C4.red}\u2717${C4.reset} git pull failed \u2014 the branch may have diverged or you are offline. Resolve it manually, then retry.`
10808
+ );
10809
+ process.exit(1);
10810
+ }
10811
+ console.log(`${C4.dim}\u2192 npm install${C4.reset}`);
10812
+ if (runNpm2(["install"], src) !== 0) {
10813
+ console.error(`${C4.red}\u2717${C4.reset} npm install failed.`);
10814
+ process.exit(1);
10815
+ }
10816
+ console.log(`${C4.dim}\u2192 npm run build${C4.reset}`);
10817
+ if (runNpm2(["run", "build"], src) !== 0) {
10818
+ console.error(`${C4.red}\u2717${C4.reset} Build failed.`);
10819
+ process.exit(1);
10820
+ }
10821
+ console.log(`${C4.dim}\u2192 zam setup --force${C4.reset}`);
10822
+ const setup = spawnSync2(
10823
+ process.execPath,
10824
+ [join20(src, "dist", "cli", "index.js"), "setup", "--force"],
10825
+ { cwd: process.cwd(), stdio: "inherit" }
10826
+ );
10827
+ if (setup.status !== 0) {
10828
+ console.warn(
10829
+ `${C4.yellow}\u26A0${C4.reset} Skill refresh reported a problem \u2014 run '${C4.cyan}zam setup --force${C4.reset}' here manually.`
10830
+ );
10831
+ }
10832
+ console.log(
10833
+ `
10834
+ ${C4.green}\u2713${C4.reset} Updated to ${C4.cyan}${versionAt(src)}${C4.reset}. Restart your agent client (e.g. Claude Code) to load the refreshed ${C4.cyan}/zam${C4.reset} skill.`
10835
+ );
10836
+ }
10837
+ async function applyUpdate(opts) {
10838
+ try {
10839
+ const current = currentVersion();
10840
+ const latest = await fetchLatestVersion(GITHUB_REPO);
10841
+ const channel = getInstallChannel();
10842
+ const decision = decideUpdate({
10843
+ currentVersion: current,
10844
+ latestVersion: latest,
10845
+ channel
10846
+ });
10847
+ if (!decision.updateAvailable) {
10848
+ console.log(
10849
+ `${C4.green}\u2713${C4.reset} ZAM is up to date (${C4.cyan}${current}${C4.reset}).`
10850
+ );
10851
+ return;
10852
+ }
10853
+ console.log(
10854
+ `${C4.yellow}\u2191${C4.reset} ${C4.bold}Update available${C4.reset}: ${C4.dim}${current}${C4.reset} \u2192 ${C4.cyan}${latest}${C4.reset} ${C4.dim}(${channel} install)${C4.reset}`
10855
+ );
10856
+ for (const step of planUpdate(decision)) {
10857
+ console.log(` ${C4.dim}\u2022${C4.reset} ${step.label}`);
10858
+ }
10859
+ if (channel === "direct") {
10860
+ console.log(
10861
+ `
10862
+ ${C4.dim}This install updates through the desktop app's signed updater \u2014 open ZAM Desktop to install ${latest}.${C4.reset}`
10863
+ );
10864
+ return;
10865
+ }
10866
+ if (!opts.yes) {
10867
+ const ok = await confirm3({
10868
+ message: "Apply this update?",
10869
+ default: true
10870
+ });
10871
+ if (!ok) {
10872
+ console.log("Aborted.");
10873
+ return;
10874
+ }
10875
+ }
10876
+ console.log();
10877
+ if (channel === "winget" || channel === "homebrew") {
10878
+ if (!decision.command || !runShell(decision.command)) process.exit(1);
10879
+ return;
10880
+ }
10881
+ applyDeveloperUpdate(opts.force ?? false);
10882
+ } catch (err) {
10883
+ console.error("Error:", err.message);
10884
+ process.exit(1);
10885
+ }
10886
+ }
10887
+ var updateCommand = new Command21("update").description(
10888
+ "Update ZAM to the latest release (use `update check` to only check)"
10889
+ ).option("-y, --yes", "Apply without confirmation").option(
10890
+ "--force",
10891
+ "Update even if the source checkout has uncommitted changes"
10892
+ ).action(async (opts) => {
10893
+ await applyUpdate(opts);
10894
+ }).addCommand(checkCmd);
9827
10895
 
9828
10896
  // src/cli/commands/whoami.ts
9829
- import { Command as Command21 } from "commander";
9830
- var whoamiCommand = new Command21("whoami").description("Show or set the default user identity").option("--set <id>", "Set the default user ID").option("--clear", "Remove the default user ID").option("--json", "Output as JSON").action(async (opts) => {
10897
+ import { Command as Command22 } from "commander";
10898
+ var whoamiCommand = new Command22("whoami").description("Show or set the default user identity").option("--set <id>", "Set the default user ID").option("--clear", "Remove the default user ID").option("--json", "Output as JSON").action(async (opts) => {
9831
10899
  await withDb(async (db) => {
9832
10900
  if (opts.set) {
9833
10901
  await setSetting(db, "user.id", opts.set);
@@ -9864,11 +10932,11 @@ var whoamiCommand = new Command21("whoami").description("Show or set the default
9864
10932
 
9865
10933
  // src/cli/commands/workspace.ts
9866
10934
  import { execSync as execSync6 } from "child_process";
9867
- import { existsSync as existsSync20, writeFileSync as writeFileSync10 } from "fs";
9868
- import { join as join20 } from "path";
9869
- import { confirm as confirm3, input as input7 } from "@inquirer/prompts";
9870
- import { Command as Command22 } from "commander";
9871
- function runGit(cwd, args) {
10935
+ import { existsSync as existsSync21, writeFileSync as writeFileSync11 } from "fs";
10936
+ import { join as join21 } from "path";
10937
+ import { confirm as confirm4, input as input7 } from "@inquirer/prompts";
10938
+ import { Command as Command23 } from "commander";
10939
+ function runGit2(cwd, args) {
9872
10940
  try {
9873
10941
  return execSync6(`git ${args}`, {
9874
10942
  cwd,
@@ -9879,7 +10947,7 @@ function runGit(cwd, args) {
9879
10947
  throw new Error(`Git command failed: ${err.message}`);
9880
10948
  }
9881
10949
  }
9882
- var workspaceCommand = new Command22("workspace").description(
10950
+ var workspaceCommand = new Command23("workspace").description(
9883
10951
  "Manage your ZAM learning workspace"
9884
10952
  );
9885
10953
  workspaceCommand.command("publish").description("Publish your local workspace sandbox to GitHub").action(async () => {
@@ -9898,7 +10966,7 @@ workspaceCommand.command("publish").description("Publish your local workspace sa
9898
10966
  );
9899
10967
  process.exit(1);
9900
10968
  }
9901
- if (!existsSync20(workspaceDir)) {
10969
+ if (!existsSync21(workspaceDir)) {
9902
10970
  console.error(
9903
10971
  `\x1B[31m\u2717 Workspace directory does not exist: ${workspaceDir}\x1B[0m`
9904
10972
  );
@@ -9912,20 +10980,20 @@ Active workspace: \x1B[36m${workspaceDir}\x1B[0m`);
9912
10980
  );
9913
10981
  process.exit(1);
9914
10982
  }
9915
- const gitignorePath = join20(workspaceDir, ".gitignore");
9916
- if (!existsSync20(gitignorePath)) {
9917
- writeFileSync10(
10983
+ const gitignorePath = join21(workspaceDir, ".gitignore");
10984
+ if (!existsSync21(gitignorePath)) {
10985
+ writeFileSync11(
9918
10986
  gitignorePath,
9919
10987
  "node_modules/\n.agent/\n.agents/\n.claude/\n.gemini/\n.goose/\n*.log\n",
9920
10988
  "utf8"
9921
10989
  );
9922
10990
  }
9923
- const hasGitRepo = existsSync20(join20(workspaceDir, ".git"));
10991
+ const hasGitRepo = existsSync21(join21(workspaceDir, ".git"));
9924
10992
  if (!hasGitRepo) {
9925
10993
  console.log("Initializing local Git repository...");
9926
- runGit(workspaceDir, "init -b main");
9927
- runGit(workspaceDir, "add .");
9928
- runGit(
10994
+ runGit2(workspaceDir, "init -b main");
10995
+ runGit2(workspaceDir, "add .");
10996
+ runGit2(
9929
10997
  workspaceDir,
9930
10998
  'commit -m "chore: initial workspace sandbox bootstrap"'
9931
10999
  );
@@ -9937,14 +11005,14 @@ Active workspace: \x1B[36m${workspaceDir}\x1B[0m`);
9937
11005
  message: "Choose a name for your GitHub repository:",
9938
11006
  default: "zam-personal"
9939
11007
  });
9940
- const isPrivate = await confirm3({
11008
+ const isPrivate = await confirm4({
9941
11009
  message: "Should the repository be private?",
9942
11010
  default: true
9943
11011
  });
9944
11012
  const repoVisibility = isPrivate ? "--private" : "--public";
9945
11013
  if (hasCommand("gh")) {
9946
11014
  console.log("GitHub CLI detected! Automating repository creation...");
9947
- const proceedGh = await confirm3({
11015
+ const proceedGh = await confirm4({
9948
11016
  message: "Would you like ZAM to create the repository using the GitHub CLI?",
9949
11017
  default: true
9950
11018
  });
@@ -9989,16 +11057,16 @@ Active workspace: \x1B[36m${workspaceDir}\x1B[0m`);
9989
11057
  console.log("Linking remote repository and pushing...");
9990
11058
  let hasOrigin = false;
9991
11059
  try {
9992
- runGit(workspaceDir, "remote get-url origin");
11060
+ runGit2(workspaceDir, "remote get-url origin");
9993
11061
  hasOrigin = true;
9994
11062
  } catch {
9995
11063
  }
9996
11064
  if (hasOrigin) {
9997
- runGit(workspaceDir, `remote set-url origin ${githubUrl}`);
11065
+ runGit2(workspaceDir, `remote set-url origin ${githubUrl}`);
9998
11066
  } else {
9999
- runGit(workspaceDir, `remote add origin ${githubUrl}`);
11067
+ runGit2(workspaceDir, `remote add origin ${githubUrl}`);
10000
11068
  }
10001
- runGit(workspaceDir, "push -u origin main");
11069
+ runGit2(workspaceDir, "push -u origin main");
10002
11070
  console.log(
10003
11071
  "\x1B[32m\u2713 Successfully linked and pushed to GitHub!\x1B[0m"
10004
11072
  );
@@ -10016,9 +11084,9 @@ Active workspace: \x1B[36m${workspaceDir}\x1B[0m`);
10016
11084
  // src/cli/index.ts
10017
11085
  var __dirname = dirname10(fileURLToPath5(import.meta.url));
10018
11086
  var pkg = JSON.parse(
10019
- readFileSync13(join21(__dirname, "..", "..", "package.json"), "utf-8")
11087
+ readFileSync14(join22(__dirname, "..", "..", "package.json"), "utf-8")
10020
11088
  );
10021
- var program = new Command23();
11089
+ var program = new Command24();
10022
11090
  program.name("zam").description(
10023
11091
  "The Symbiotic Learning Kernel: Elevating Human Intelligence through AI Collaboration."
10024
11092
  ).version(pkg.version);
@@ -10034,6 +11102,7 @@ program.addCommand(uiCommand);
10034
11102
  program.addCommand(bridgeCommand);
10035
11103
  program.addCommand(skillCommand);
10036
11104
  program.addCommand(monitorCommand);
11105
+ program.addCommand(observerCommand);
10037
11106
  program.addCommand(settingsCommand);
10038
11107
  program.addCommand(whoamiCommand);
10039
11108
  program.addCommand(connectorCommand);