zam-core 0.3.13 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/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
 
@@ -2317,183 +2766,43 @@ async function cascadeBlock(db, userId, tokenSlug) {
2317
2766
  await db.prepare(
2318
2767
  "UPDATE cards SET blocked = 0, due_at = ? WHERE token_id = ? AND user_id = ?"
2319
2768
  ).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 });
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",
@@ -4550,7 +4859,7 @@ var C = {
4550
4859
  };
4551
4860
  var SUPPORTED_AGENTS = ["opencode"];
4552
4861
  function agentsMdPresent(cwd = process.cwd()) {
4553
- return existsSync11(join10(cwd, "AGENTS.md"));
4862
+ return existsSync11(join11(cwd, "AGENTS.md"));
4554
4863
  }
4555
4864
  function printStatus() {
4556
4865
  const installed = hasCommand("opencode");
@@ -4591,9 +4900,11 @@ var statusCmd = new Command("status").description("Show whether the agent is ins
4591
4900
  var agentCommand = new Command("agent").description("Provision and inspect the agent that drives ZAM sessions").addCommand(installCmd).addCommand(statusCmd).action(printStatus);
4592
4901
 
4593
4902
  // 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";
4903
+ import { execFileSync as execFileSync2 } from "child_process";
4904
+ import { randomBytes } from "crypto";
4905
+ import { readdirSync as readdirSync2, readFileSync as readFileSync10, rmSync as rmSync2 } from "fs";
4906
+ import { homedir as homedir8, tmpdir } from "os";
4907
+ import { join as join12 } from "path";
4597
4908
  import { Command as Command2 } from "commander";
4598
4909
 
4599
4910
  // src/cli/llm/client.ts
@@ -5730,13 +6041,15 @@ bridgeCommand.command("start-session").description("Start a ZAM learning session
5730
6041
  task: opts.task,
5731
6042
  execution_context: context
5732
6043
  });
6044
+ const observerPolicyHint = context === "ui" && !await isObserverPolicyConfigured(db) ? OBSERVER_POLICY_UNSET_HINT : void 0;
5733
6045
  jsonOut2({
5734
6046
  id: session.id,
5735
6047
  userId: session.user_id,
5736
6048
  task: session.task,
5737
6049
  executionContext: session.execution_context,
5738
6050
  startedAt: session.started_at,
5739
- completedAt: session.completed_at
6051
+ completedAt: session.completed_at,
6052
+ ...observerPolicyHint ? { observerPolicyHint } : {}
5740
6053
  });
5741
6054
  });
5742
6055
  });
@@ -5859,7 +6172,8 @@ bridgeCommand.command("add-token").description("Create a token + card from JSON
5859
6172
  bloom_level: data?.bloom_level ?? 1,
5860
6173
  context: data?.context,
5861
6174
  symbiosis_mode: data?.symbiosis_mode,
5862
- source_link: data?.source_link ?? null
6175
+ source_link: data?.source_link ?? null,
6176
+ question: data?.question ?? null
5863
6177
  });
5864
6178
  const card = await ensureCard(db, token.id, userId);
5865
6179
  jsonOut2({
@@ -5894,7 +6208,7 @@ bridgeCommand.command("discover-skills").description(
5894
6208
  "20"
5895
6209
  ).action(async (opts) => {
5896
6210
  try {
5897
- const monitorDir = join11(homedir8(), ".zam", "monitor");
6211
+ const monitorDir = join12(homedir8(), ".zam", "monitor");
5898
6212
  let files;
5899
6213
  try {
5900
6214
  files = readdirSync2(monitorDir).filter((f) => f.endsWith(".jsonl"));
@@ -5907,7 +6221,7 @@ bridgeCommand.command("discover-skills").description(
5907
6221
  return;
5908
6222
  }
5909
6223
  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);
6224
+ const sorted = files.map((f) => ({ name: f, path: join12(monitorDir, f) })).sort((a, b) => b.name.localeCompare(a.name)).slice(0, limit);
5911
6225
  const sessionCommands = /* @__PURE__ */ new Map();
5912
6226
  for (const file of sorted) {
5913
6227
  const sessionId = file.name.replace(".jsonl", "");
@@ -6017,6 +6331,468 @@ bridgeCommand.command("observe-ui-snapshot").description(
6017
6331
  jsonOut2(report);
6018
6332
  });
6019
6333
  });
6334
+ function resolveWindowsPowerShell() {
6335
+ try {
6336
+ execFileSync2("where.exe", ["pwsh.exe"], { stdio: "ignore" });
6337
+ return "pwsh";
6338
+ } catch {
6339
+ return "powershell";
6340
+ }
6341
+ }
6342
+ function captureScreenshot(outputPath, hwnd, processName) {
6343
+ const platform = process.platform;
6344
+ if (hwnd && !/^(0x)?[0-9a-fA-F]+$/.test(hwnd)) {
6345
+ throw new Error(`Invalid HWND format: ${hwnd}`);
6346
+ }
6347
+ if (processName && !/^[a-zA-Z0-9\-_.]+$/.test(processName)) {
6348
+ throw new Error(`Invalid process name format: ${processName}`);
6349
+ }
6350
+ if (platform === "win32") {
6351
+ const stdout = execFileSync2(
6352
+ resolveWindowsPowerShell(),
6353
+ [
6354
+ "-NoProfile",
6355
+ "-Command",
6356
+ `
6357
+ Add-Type -AssemblyName System.Windows.Forms
6358
+ Add-Type -AssemblyName System.Drawing
6359
+
6360
+ $code = @'
6361
+ using System;
6362
+ using System.Runtime.InteropServices;
6363
+
6364
+ public class Win32 {
6365
+ public delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam);
6366
+
6367
+ [DllImport("user32.dll")]
6368
+ public static extern bool SetProcessDPIAware();
6369
+
6370
+ [DllImport("user32.dll")]
6371
+ public static extern bool SetProcessDpiAwarenessContext(IntPtr dpiContext);
6372
+
6373
+ [DllImport("user32.dll")]
6374
+ public static extern bool GetWindowRect(IntPtr hWnd, out RECT rect);
6375
+
6376
+ [DllImport("user32.dll")]
6377
+ public static extern bool EnumWindows(EnumWindowsProc lpEnumFunc, IntPtr lParam);
6378
+
6379
+ [DllImport("user32.dll")]
6380
+ public static extern bool EnumChildWindows(IntPtr hWnd, EnumWindowsProc lpEnumFunc, IntPtr lParam);
6381
+
6382
+ [DllImport("user32.dll")]
6383
+ public static extern bool IsWindowVisible(IntPtr hWnd);
6384
+
6385
+ [DllImport("user32.dll", SetLastError=true)]
6386
+ public static extern int GetWindowText(IntPtr hWnd, System.Text.StringBuilder lpString, int nMaxCount);
6387
+
6388
+ [DllImport("user32.dll", SetLastError=true)]
6389
+ public static extern int GetWindowTextLength(IntPtr hWnd);
6390
+
6391
+ [DllImport("user32.dll")]
6392
+ public static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId);
6393
+
6394
+ [DllImport("user32.dll")]
6395
+ public static extern bool SetForegroundWindow(IntPtr hWnd);
6396
+
6397
+ [DllImport("user32.dll")]
6398
+ public static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);
6399
+
6400
+ [DllImport("user32.dll")]
6401
+ public static extern bool IsIconic(IntPtr hWnd);
6402
+
6403
+ [DllImport("user32.dll")]
6404
+ public static extern bool PrintWindow(IntPtr hWnd, IntPtr hdcBlt, uint nFlags);
6405
+
6406
+ [StructLayout(LayoutKind.Sequential)]
6407
+ public struct RECT {
6408
+ public int Left;
6409
+ public int Top;
6410
+ public int Right;
6411
+ public int Bottom;
6412
+ }
6413
+ }
6414
+ '@
6415
+ Add-Type -TypeDefinition $code
6416
+
6417
+ try {
6418
+ # DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2 = -4. Without this, Windows
6419
+ # can return logical window bounds while PrintWindow renders physical
6420
+ # pixels, causing high-DPI captures to crop the right/bottom edge.
6421
+ [Win32]::SetProcessDpiAwarenessContext([IntPtr](-4)) | Out-Null
6422
+ } catch {
6423
+ try { [Win32]::SetProcessDPIAware() | Out-Null } catch {}
6424
+ }
6425
+
6426
+ function Get-WindowTitle([IntPtr]$hWnd) {
6427
+ $length = [Win32]::GetWindowTextLength($hWnd)
6428
+ $capacity = [Math]::Max(1, $length + 1)
6429
+ $builder = New-Object System.Text.StringBuilder $capacity
6430
+ [Win32]::GetWindowText($hWnd, $builder, $builder.Capacity) | Out-Null
6431
+ $builder.ToString()
6432
+ }
6433
+
6434
+ function Get-WindowProcess([IntPtr]$hWnd) {
6435
+ [uint32]$processId = 0
6436
+ [Win32]::GetWindowThreadProcessId($hWnd, [ref]$processId) | Out-Null
6437
+ if ($processId -eq 0) { return $null }
6438
+ Get-Process -Id $processId -ErrorAction SilentlyContinue
6439
+ }
6440
+
6441
+ function Get-VisibleTopLevelWindows {
6442
+ $script:windowCandidates = New-Object System.Collections.ArrayList
6443
+ $callback = [Win32+EnumWindowsProc]{
6444
+ param([IntPtr]$candidateHwnd, [IntPtr]$lParam)
6445
+ if ([Win32]::IsWindowVisible($candidateHwnd)) {
6446
+ $candidateRect = New-Object Win32+RECT
6447
+ if ([Win32]::GetWindowRect($candidateHwnd, [ref]$candidateRect)) {
6448
+ $candidateWidth = $candidateRect.Right - $candidateRect.Left
6449
+ $candidateHeight = $candidateRect.Bottom - $candidateRect.Top
6450
+ if ($candidateWidth -gt 0 -and $candidateHeight -gt 0) {
6451
+ [void]$script:windowCandidates.Add($candidateHwnd)
6452
+ }
6453
+ }
6454
+ }
6455
+ return $true
6456
+ }
6457
+ [Win32]::EnumWindows($callback, [IntPtr]::Zero) | Out-Null
6458
+ $windows = $script:windowCandidates
6459
+ Remove-Variable -Name windowCandidates -Scope Script -ErrorAction SilentlyContinue
6460
+ $windows
6461
+ }
6462
+
6463
+ function Find-TopLevelWindowByProcessName([string]$name) {
6464
+ foreach ($candidateHwnd in Get-VisibleTopLevelWindows) {
6465
+ $candidateProcess = Get-WindowProcess $candidateHwnd
6466
+ if ($candidateProcess -and $candidateProcess.ProcessName -ieq $name) {
6467
+ return [pscustomobject]@{
6468
+ Hwnd = $candidateHwnd
6469
+ MatchedBy = "process-top-level-window"
6470
+ }
6471
+ }
6472
+
6473
+ $script:desiredChildProcessName = $name
6474
+ $script:foundChildProcessWindow = $false
6475
+ $childCallback = [Win32+EnumWindowsProc]{
6476
+ param([IntPtr]$childHwnd, [IntPtr]$lParam)
6477
+ $childProcess = Get-WindowProcess $childHwnd
6478
+ if ($childProcess -and $childProcess.ProcessName -ieq $script:desiredChildProcessName) {
6479
+ $script:foundChildProcessWindow = $true
6480
+ return $false
6481
+ }
6482
+ return $true
6483
+ }
6484
+ [Win32]::EnumChildWindows($candidateHwnd, $childCallback, [IntPtr]::Zero) | Out-Null
6485
+ $foundChild = $script:foundChildProcessWindow
6486
+ Remove-Variable -Name desiredChildProcessName -Scope Script -ErrorAction SilentlyContinue
6487
+ Remove-Variable -Name foundChildProcessWindow -Scope Script -ErrorAction SilentlyContinue
6488
+
6489
+ if ($foundChild) {
6490
+ return [pscustomobject]@{
6491
+ Hwnd = $candidateHwnd
6492
+ MatchedBy = "process-child-window"
6493
+ }
6494
+ }
6495
+ }
6496
+
6497
+ return $null
6498
+ }
6499
+
6500
+ function New-CaptureTarget([IntPtr]$hWnd, [string]$matchedBy) {
6501
+ $target = [ordered]@{
6502
+ requestedHwnd = if ($targetHwnd -ne '') { $targetHwnd } else { $null }
6503
+ requestedProcessName = if ($processName -ne '') { $processName } else { $null }
6504
+ matchedBy = $matchedBy
6505
+ hwnd = $null
6506
+ processId = $null
6507
+ processName = $null
6508
+ windowTitle = $null
6509
+ bounds = $null
6510
+ }
6511
+
6512
+ if ($hWnd -ne [IntPtr]::Zero) {
6513
+ $target.hwnd = $hWnd.ToInt64()
6514
+ $target.windowTitle = Get-WindowTitle $hWnd
6515
+
6516
+ $windowProcess = Get-WindowProcess $hWnd
6517
+ if ($windowProcess) {
6518
+ $target.processId = $windowProcess.Id
6519
+ $target.processName = $windowProcess.ProcessName
6520
+ }
6521
+
6522
+ $targetRect = New-Object Win32+RECT
6523
+ if ([Win32]::GetWindowRect($hWnd, [ref]$targetRect)) {
6524
+ $target.bounds = [ordered]@{
6525
+ left = $targetRect.Left
6526
+ top = $targetRect.Top
6527
+ right = $targetRect.Right
6528
+ bottom = $targetRect.Bottom
6529
+ width = $targetRect.Right - $targetRect.Left
6530
+ height = $targetRect.Bottom - $targetRect.Top
6531
+ }
6532
+ }
6533
+ }
6534
+
6535
+ $target
6536
+ }
6537
+
6538
+ function Write-CaptureResult([string]$method, [IntPtr]$hWnd, [string]$matchedBy) {
6539
+ $result = [ordered]@{
6540
+ method = $method
6541
+ target = New-CaptureTarget $hWnd $matchedBy
6542
+ }
6543
+ Write-Output ("CAPTURE_RESULT:" + ($result | ConvertTo-Json -Compress -Depth 6))
6544
+ }
6545
+
6546
+ $hwndVal = [IntPtr]::Zero
6547
+ $matchedBy = "fullscreen-fallback"
6548
+ $targetHwnd = '${hwnd || ""}'
6549
+ $processName = '${processName || ""}'
6550
+
6551
+ if ($targetHwnd -ne '') {
6552
+ if ($targetHwnd.StartsWith("0x")) {
6553
+ $hwndVal = [IntPtr][Convert]::ToInt64($targetHwnd, 16)
6554
+ } else {
6555
+ $hwndVal = [IntPtr][Convert]::ToInt64($targetHwnd, 10)
6556
+ }
6557
+ $matchedBy = "hwnd"
6558
+ } elseif ($processName -ne '') {
6559
+ $proc = Get-Process -Name $processName -ErrorAction SilentlyContinue | Where-Object {$_.MainWindowHandle -ne 0} | Select-Object -First 1
6560
+ if ($proc) {
6561
+ $hwndVal = $proc.MainWindowHandle
6562
+ $matchedBy = "process-main-window"
6563
+ } else {
6564
+ $proc = Get-Process -Name $processName -ErrorAction SilentlyContinue | Select-Object -First 1
6565
+ if ($proc) {
6566
+ $hwndVal = $proc.MainWindowHandle
6567
+ $matchedBy = "process-zero-main-window"
6568
+ }
6569
+ }
6570
+
6571
+ if ($hwndVal -eq [IntPtr]::Zero) {
6572
+ $windowMatch = Find-TopLevelWindowByProcessName $processName
6573
+ if ($windowMatch) {
6574
+ $hwndVal = $windowMatch.Hwnd
6575
+ $matchedBy = $windowMatch.MatchedBy
6576
+ }
6577
+ }
6578
+ }
6579
+
6580
+ if ($hwndVal -ne [IntPtr]::Zero) {
6581
+ if ([Win32]::IsIconic($hwndVal)) {
6582
+ [Win32]::ShowWindow($hwndVal, 9) | Out-Null # SW_RESTORE = 9
6583
+ Start-Sleep -Milliseconds 250
6584
+ }
6585
+
6586
+ $rect = New-Object Win32+RECT
6587
+ if ([Win32]::GetWindowRect($hwndVal, [ref]$rect)) {
6588
+ $width = $rect.Right - $rect.Left
6589
+ $height = $rect.Bottom - $rect.Top
6590
+ if ($width -gt 0 -and $height -gt 0) {
6591
+ $bitmap = New-Object System.Drawing.Bitmap($width, $height)
6592
+ $graphics = [System.Drawing.Graphics]::FromImage($bitmap)
6593
+ # PrintWindow renders the target window directly, regardless of
6594
+ # z-order, so an occluded or background window is still captured
6595
+ # correctly. SetForegroundWindow from a background process is
6596
+ # blocked by Windows, so CopyFromScreen would grab whatever sits
6597
+ # on top. PW_RENDERFULLCONTENT (0x2) handles modern/UWP windows.
6598
+ $hdc = $graphics.GetHdc()
6599
+ $printed = [Win32]::PrintWindow($hwndVal, $hdc, 2)
6600
+ $graphics.ReleaseHdc($hdc)
6601
+ $method = "printwindow"
6602
+
6603
+ # Black-frame guard: PrintWindow can return a near-black frame on
6604
+ # some hardware-accelerated / DirectComposition surfaces. Sample a
6605
+ # sparse grid; if it is essentially black, fall back to a foreground
6606
+ # CopyFromScreen grab so the capture self-heals on those drivers.
6607
+ $needFallback = -not $printed
6608
+ if (-not $needFallback) {
6609
+ $sum = 0.0
6610
+ $cnt = 0
6611
+ $stepX = [Math]::Max(1, [int]($width / 12))
6612
+ $stepY = [Math]::Max(1, [int]($height / 12))
6613
+ for ($sy = 0; $sy -lt $height; $sy += $stepY) {
6614
+ for ($sx = 0; $sx -lt $width; $sx += $stepX) {
6615
+ $px = $bitmap.GetPixel($sx, $sy)
6616
+ $sum += ($px.R + $px.G + $px.B) / 3.0
6617
+ $cnt++
6618
+ }
6619
+ }
6620
+ if ($cnt -gt 0 -and ($sum / $cnt) -lt 6) { $needFallback = $true }
6621
+ }
6622
+
6623
+ if ($needFallback) {
6624
+ [Win32]::SetForegroundWindow($hwndVal) | Out-Null
6625
+ Start-Sleep -Milliseconds 250
6626
+ $graphics.CopyFromScreen($rect.Left, $rect.Top, 0, 0, $bitmap.Size)
6627
+ $method = "copyfromscreen"
6628
+ }
6629
+ $bitmap.Save('${outputPath.replace(/\\/g, "\\\\")}', [System.Drawing.Imaging.ImageFormat]::Png)
6630
+ $graphics.Dispose()
6631
+ $bitmap.Dispose()
6632
+ Write-CaptureResult $method $hwndVal $matchedBy
6633
+ exit 0
6634
+ }
6635
+ }
6636
+ }
6637
+
6638
+ # Fallback: full primary screen
6639
+ $screen = [System.Windows.Forms.Screen]::PrimaryScreen.Bounds
6640
+ $bitmap = New-Object System.Drawing.Bitmap($screen.Width, $screen.Height)
6641
+ $graphics = [System.Drawing.Graphics]::FromImage($bitmap)
6642
+ $graphics.CopyFromScreen($screen.Location, [System.Drawing.Point]::Empty, $screen.Size)
6643
+ $bitmap.Save('${outputPath.replace(/\\/g, "\\\\")}', [System.Drawing.Imaging.ImageFormat]::Png)
6644
+ $graphics.Dispose()
6645
+ $bitmap.Dispose()
6646
+ Write-CaptureResult "fullscreen" $hwndVal $matchedBy
6647
+ `.trim()
6648
+ ],
6649
+ { stdio: "pipe", encoding: "utf8" }
6650
+ );
6651
+ const resultMatch = /CAPTURE_RESULT:(\{.*\})/.exec(stdout ?? "");
6652
+ if (resultMatch) {
6653
+ const parsed = JSON.parse(resultMatch[1]);
6654
+ return parsed;
6655
+ }
6656
+ const methodMatch = /CAPTURE_METHOD:(\w+)/.exec(stdout ?? "");
6657
+ return {
6658
+ method: methodMatch ? methodMatch[1] : "unknown",
6659
+ target: null
6660
+ };
6661
+ } else if (platform === "darwin") {
6662
+ if (hwnd) {
6663
+ const parsedHwnd = hwnd.startsWith("0x") ? parseInt(hwnd, 16) : parseInt(hwnd, 10);
6664
+ execFileSync2("screencapture", ["-l", String(parsedHwnd), outputPath], {
6665
+ stdio: "pipe"
6666
+ });
6667
+ return { method: "screencapture-window", target: null };
6668
+ } else if (processName) {
6669
+ try {
6670
+ const windowId = execFileSync2(
6671
+ "osascript",
6672
+ [
6673
+ "-e",
6674
+ `tell application "System Events" to get id of window 1 of process "${processName}"`
6675
+ ],
6676
+ { encoding: "utf8", stdio: ["pipe", "pipe", "ignore"] }
6677
+ ).trim();
6678
+ if (windowId && /^\d+$/.test(windowId)) {
6679
+ execFileSync2("screencapture", ["-l", windowId, outputPath], {
6680
+ stdio: "pipe"
6681
+ });
6682
+ return { method: "screencapture-window", target: null };
6683
+ }
6684
+ } catch {
6685
+ }
6686
+ execFileSync2("screencapture", ["-x", outputPath], { stdio: "pipe" });
6687
+ return { method: "screencapture-full", target: null };
6688
+ } else {
6689
+ execFileSync2("screencapture", ["-x", outputPath], { stdio: "pipe" });
6690
+ return { method: "screencapture-full", target: null };
6691
+ }
6692
+ } else {
6693
+ throw new Error(
6694
+ `Screen capture not supported on platform: ${platform}. Use zam-observer or provide --image.`
6695
+ );
6696
+ }
6697
+ }
6698
+ 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) => {
6699
+ await withDb2(async (db) => {
6700
+ const policy = await resolveObserverPolicy(db);
6701
+ const permission = {
6702
+ scope: policy.scope,
6703
+ consent: policy.consent,
6704
+ retention: policy.retention
6705
+ };
6706
+ const isProvided = Boolean(opts.image);
6707
+ if (!isProvided) {
6708
+ const pre = decidePreCapture(policy, {
6709
+ hasExplicitTarget: Boolean(opts.hwnd || opts.processName),
6710
+ requestedProcessName: opts.processName ?? null
6711
+ });
6712
+ if (!pre.allowed) {
6713
+ jsonOut2({
6714
+ sessionId: opts.session ?? null,
6715
+ granted: false,
6716
+ denied: true,
6717
+ denialReason: pre.denialReason,
6718
+ reason: pre.reason,
6719
+ capturedAt: (/* @__PURE__ */ new Date()).toISOString(),
6720
+ platform: process.platform,
6721
+ permission: { ...permission, granted: false }
6722
+ });
6723
+ return;
6724
+ }
6725
+ }
6726
+ const outputPath = opts.image ?? opts.output ?? join12(tmpdir(), `zam-capture-${randomBytes(4).toString("hex")}.png`);
6727
+ const captureResult = isProvided ? { method: "provided", target: null } : captureScreenshot(outputPath, opts.hwnd, opts.processName);
6728
+ if (!isProvided) {
6729
+ const post = decidePostCapture(policy, {
6730
+ method: captureResult.method,
6731
+ processName: captureResult.target?.processName ?? null,
6732
+ windowTitle: captureResult.target?.windowTitle ?? null
6733
+ });
6734
+ if (!post.allowed) {
6735
+ if (!opts.output) {
6736
+ try {
6737
+ rmSync2(outputPath, { force: true });
6738
+ } catch {
6739
+ }
6740
+ }
6741
+ jsonOut2({
6742
+ sessionId: opts.session ?? null,
6743
+ granted: false,
6744
+ denied: true,
6745
+ denialReason: post.denialReason,
6746
+ reason: post.reason,
6747
+ capturedAt: (/* @__PURE__ */ new Date()).toISOString(),
6748
+ platform: process.platform,
6749
+ permission: { ...permission, granted: false }
6750
+ });
6751
+ return;
6752
+ }
6753
+ }
6754
+ const imageBytes = readFileSync10(outputPath);
6755
+ const base64 = imageBytes.toString("base64");
6756
+ jsonOut2({
6757
+ sessionId: opts.session ?? null,
6758
+ granted: true,
6759
+ imagePath: outputPath,
6760
+ base64,
6761
+ mimeType: "image/png",
6762
+ captureMethod: captureResult.method,
6763
+ captureTarget: captureResult.target,
6764
+ capturedAt: (/* @__PURE__ */ new Date()).toISOString(),
6765
+ platform: process.platform,
6766
+ permission: { ...permission, granted: true }
6767
+ });
6768
+ });
6769
+ });
6770
+ bridgeCommand.command("get-observer-policy").description(
6771
+ "Report the resolved observer policy so an agent can check before capturing (JSON)"
6772
+ ).action(async () => {
6773
+ await withDb2(async (db) => {
6774
+ const policy = await resolveObserverPolicy(db);
6775
+ jsonOut2({
6776
+ scope: policy.scope,
6777
+ consent: policy.consent,
6778
+ retention: policy.retention,
6779
+ allowlist: policy.allowlist,
6780
+ denylist: policy.denylist,
6781
+ redactWindowTitles: policy.redactWindowTitles,
6782
+ audioOptIn: policy.audioOptIn,
6783
+ builtInSensitiveAlwaysRefused: true,
6784
+ builtInSensitiveMatchers: [...BUILT_IN_SENSITIVE_MATCHERS]
6785
+ });
6786
+ });
6787
+ });
6788
+ bridgeCommand.command("sync-observer-policy").description(
6789
+ "Write the resolved observer policy to the native sidecar file (JSON)"
6790
+ ).action(async () => {
6791
+ await withDb2(async (db) => {
6792
+ const { path, policy } = await syncObserverSidecarPolicy(db);
6793
+ jsonOut2({ synced: true, path, policy });
6794
+ });
6795
+ });
6020
6796
  bridgeCommand.command("check-llm").description("Check if LLM is enabled and online (JSON)").action(async () => {
6021
6797
  await withDb2(async (db) => {
6022
6798
  const { enabled, url, model, apiKey } = await getLlmConfig(db);
@@ -6698,26 +7474,26 @@ async function setupTurso(urlArg, tokenArg, mode) {
6698
7474
 
6699
7475
  // src/cli/commands/git-sync.ts
6700
7476
  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";
7477
+ import { chmodSync as chmodSync2, existsSync as existsSync13, writeFileSync as writeFileSync6 } from "fs";
7478
+ import { join as join13 } from "path";
6703
7479
  import { Command as Command5 } from "commander";
6704
7480
  function installHook2() {
6705
- const gitDir = join12(process.cwd(), ".git");
7481
+ const gitDir = join13(process.cwd(), ".git");
6706
7482
  if (!existsSync13(gitDir)) {
6707
7483
  console.error(
6708
7484
  "Error: Current directory is not the root of a Git repository."
6709
7485
  );
6710
7486
  process.exit(1);
6711
7487
  }
6712
- const hooksDir = join12(gitDir, "hooks");
6713
- const hookPath = join12(hooksDir, "post-commit");
7488
+ const hooksDir = join13(gitDir, "hooks");
7489
+ const hookPath = join13(hooksDir, "post-commit");
6714
7490
  const hookContent = `#!/bin/sh
6715
7491
  # ZAM Spaced Repetition Auto-Stale Hook
6716
7492
  # Triggered automatically on git commits to decay modified concept cards.
6717
7493
  zam git-sync --commit HEAD --quiet
6718
7494
  `;
6719
7495
  try {
6720
- writeFileSync5(hookPath, hookContent, { encoding: "utf-8", flag: "w" });
7496
+ writeFileSync6(hookPath, hookContent, { encoding: "utf-8", flag: "w" });
6721
7497
  try {
6722
7498
  chmodSync2(hookPath, "755");
6723
7499
  } catch (_e) {
@@ -6819,7 +7595,7 @@ ZAM Auto-Stale Complete: Scanned ${changedFiles.length} file(s).`
6819
7595
  });
6820
7596
 
6821
7597
  // src/cli/commands/goal.ts
6822
- import { existsSync as existsSync14, mkdirSync as mkdirSync7 } from "fs";
7598
+ import { existsSync as existsSync14, mkdirSync as mkdirSync8 } from "fs";
6823
7599
  import { resolve as resolve3 } from "path";
6824
7600
  import { input as input2 } from "@inquirer/prompts";
6825
7601
  import { Command as Command6 } from "commander";
@@ -6945,7 +7721,7 @@ ${"\u2500".repeat(50)}`);
6945
7721
  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
7722
  const goalsDir = await resolveGoalsDir();
6947
7723
  if (!existsSync14(goalsDir)) {
6948
- mkdirSync7(goalsDir, { recursive: true });
7724
+ mkdirSync8(goalsDir, { recursive: true });
6949
7725
  }
6950
7726
  let slug = opts.slug;
6951
7727
  let title = opts.title;
@@ -7004,9 +7780,9 @@ goalCommand.command("status <slug> <status>").description("Update a goal's statu
7004
7780
  });
7005
7781
 
7006
7782
  // src/cli/commands/init.ts
7007
- import { existsSync as existsSync15, mkdirSync as mkdirSync8, writeFileSync as writeFileSync6 } from "fs";
7783
+ import { existsSync as existsSync15, mkdirSync as mkdirSync9, writeFileSync as writeFileSync7 } from "fs";
7008
7784
  import { homedir as homedir9 } from "os";
7009
- import { join as join13 } from "path";
7785
+ import { join as join14 } from "path";
7010
7786
  import { confirm, input as input3 } from "@inquirer/prompts";
7011
7787
  import { Command as Command7 } from "commander";
7012
7788
  var HOME2 = homedir9();
@@ -7014,12 +7790,12 @@ function printLine(char = "\u2550", len = 60, color = "\x1B[36m") {
7014
7790
  console.log(`${color}${char.repeat(len)}\x1B[0m`);
7015
7791
  }
7016
7792
  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");
7793
+ mkdirSync9(join14(workspaceDir, "beliefs"), { recursive: true });
7794
+ mkdirSync9(join14(workspaceDir, "goals"), { recursive: true });
7795
+ mkdirSync9(join14(workspaceDir, "skills"), { recursive: true });
7796
+ const worldviewFile = join14(workspaceDir, "beliefs", "worldview.md");
7021
7797
  if (!existsSync15(worldviewFile)) {
7022
- writeFileSync6(
7798
+ writeFileSync7(
7023
7799
  worldviewFile,
7024
7800
  `# Personal Worldview
7025
7801
 
@@ -7031,9 +7807,9 @@ Here, I declare the core concepts and principles I want to master.
7031
7807
  "utf8"
7032
7808
  );
7033
7809
  }
7034
- const goalsFile = join13(workspaceDir, "goals", "goals.md");
7810
+ const goalsFile = join14(workspaceDir, "goals", "goals.md");
7035
7811
  if (!existsSync15(goalsFile)) {
7036
- writeFileSync6(
7812
+ writeFileSync7(
7037
7813
  goalsFile,
7038
7814
  `# Personal Goals
7039
7815
 
@@ -7055,7 +7831,7 @@ var initCommand = new Command7("init").description("Launch the guided interactiv
7055
7831
  );
7056
7832
  printLine();
7057
7833
  console.log("\n\x1B[1m[1/5] Setting up Local Workspace Sandbox\x1B[0m");
7058
- const defaultWorkspace = join13(HOME2, "Documents", "zam");
7834
+ const defaultWorkspace = join14(HOME2, "Documents", "zam");
7059
7835
  const workspacePath = await input3({
7060
7836
  message: "Choose your ZAM workspace directory:",
7061
7837
  default: defaultWorkspace
@@ -7752,10 +8528,10 @@ ${"\u2550".repeat(50)}`);
7752
8528
  });
7753
8529
 
7754
8530
  // 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";
8531
+ import { execFileSync as execFileSync3, execSync as execSync5 } from "child_process";
8532
+ import { unlinkSync, writeFileSync as writeFileSync8 } from "fs";
8533
+ import { tmpdir as tmpdir2 } from "os";
8534
+ import { basename as basename3, join as join15 } from "path";
7759
8535
  import { Command as Command9 } from "commander";
7760
8536
  function isPowerShellShell(shell) {
7761
8537
  return shell === "pwsh" || shell === "powershell";
@@ -7906,11 +8682,23 @@ monitorCommand.command("status").description("Show monitoring status for a sessi
7906
8682
  console.log(` To: ${result.timeSpan.end}`);
7907
8683
  }
7908
8684
  });
8685
+ function selectWindowsExecutable(results, pathext = process.env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD") {
8686
+ if (results.length === 0) return null;
8687
+ const extensions = pathext.split(";").map((ext) => ext.trim().toLowerCase()).filter(Boolean);
8688
+ const runnable = results.find(
8689
+ (result) => extensions.some((ext) => result.toLowerCase().endsWith(ext))
8690
+ );
8691
+ return runnable ?? results[0];
8692
+ }
7909
8693
  function findExecutable(command) {
7910
8694
  try {
7911
8695
  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;
8696
+ const results = execSync5(lookup, { encoding: "utf-8" }).split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
8697
+ if (results.length === 0) return null;
8698
+ if (process.platform === "win32") {
8699
+ return selectWindowsExecutable(results);
8700
+ }
8701
+ return results[0];
7914
8702
  } catch {
7915
8703
  return null;
7916
8704
  }
@@ -7920,8 +8708,8 @@ function resolveZamInvocation(shell) {
7920
8708
  if (installed) {
7921
8709
  return isPowerShellShell(shell) ? `& ${psSingleQuoted2(installed)}` : installed;
7922
8710
  }
7923
- const projectRoot = join14(import.meta.dirname, "..", "..", "..");
7924
- const cliSource = join14(projectRoot, "src/cli/index.ts");
8711
+ const projectRoot = join15(import.meta.dirname, "..", "..", "..");
8712
+ const cliSource = join15(projectRoot, "src/cli/index.ts");
7925
8713
  if (isPowerShellShell(shell)) {
7926
8714
  return `& npx --prefix ${psSingleQuoted2(projectRoot)} tsx ${psSingleQuoted2(cliSource)}`;
7927
8715
  }
@@ -8011,9 +8799,9 @@ end tell` : `tell application "Terminal"
8011
8799
  activate
8012
8800
  do script "${escaped}"
8013
8801
  end tell`;
8014
- const tmpFile = join14(tmpdir(), `zam-monitor-${sessionId}.scpt`);
8802
+ const tmpFile = join15(tmpdir2(), `zam-monitor-${sessionId}.scpt`);
8015
8803
  try {
8016
- writeFileSync7(tmpFile, appleScript);
8804
+ writeFileSync8(tmpFile, appleScript);
8017
8805
  execSync5(`osascript ${JSON.stringify(tmpFile)}`, { stdio: "ignore" });
8018
8806
  console.log(
8019
8807
  `Opened ${useIterm ? "iTerm2" : "Terminal.app"} window with monitoring for session ${sessionId}`
@@ -8040,9 +8828,10 @@ function openWindowsPowerShell(shellSetup, sessionId, dir, requestedShell) {
8040
8828
  `-FilePath ${psSingleQuoted2(executable)}`,
8041
8829
  `-ArgumentList @('-NoExit','-NoProfile','-Command',${psSingleQuoted2(shellSetup)})`
8042
8830
  ].join(" ");
8831
+ const launcher = findExecutable("pwsh.exe") ? "pwsh.exe" : "powershell.exe";
8043
8832
  try {
8044
- execFileSync2(
8045
- "powershell.exe",
8833
+ execFileSync3(
8834
+ launcher,
8046
8835
  ["-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", startCommand],
8047
8836
  {
8048
8837
  stdio: "ignore"
@@ -8061,10 +8850,73 @@ Run this manually in a new PowerShell terminal:
8061
8850
  }
8062
8851
  }
8063
8852
 
8853
+ // src/cli/commands/observer.ts
8854
+ import { Command as Command10 } from "commander";
8855
+ var observerCommand = new Command10("observer").description(
8856
+ "Configure what the UI observer may capture (Layer 2 policy)"
8857
+ );
8858
+ function applyObserverListChange(current, entry, op) {
8859
+ const normalized = entry.trim().toLowerCase();
8860
+ const list = parseObserverList(current);
8861
+ const next = op === "add" ? [.../* @__PURE__ */ new Set([...list, normalized])] : list.filter((item) => item !== normalized);
8862
+ return next.join(",");
8863
+ }
8864
+ observerCommand.command("status").description("Show the active observer policy").option("--json", "Output as JSON").action(async (opts) => {
8865
+ await withDb(async (db) => {
8866
+ const policy = await resolveObserverPolicy(db);
8867
+ if (opts.json) {
8868
+ console.log(JSON.stringify(policy, null, 2));
8869
+ return;
8870
+ }
8871
+ console.log("Observer policy:\n");
8872
+ console.log(` Scope: ${policy.scope}`);
8873
+ console.log(` Consent: ${policy.consent}`);
8874
+ console.log(` Retention: ${policy.retention}`);
8875
+ console.log(
8876
+ ` Allowlist: ${policy.allowlist.length ? policy.allowlist.join(", ") : "(any targeted window)"}`
8877
+ );
8878
+ console.log(
8879
+ ` Denylist: ${policy.denylist.length ? policy.denylist.join(", ") : "(none)"}`
8880
+ );
8881
+ console.log(` Redact titles: ${policy.redactWindowTitles}`);
8882
+ console.log(
8883
+ "\nBuilt-in sensitive surfaces (password managers, auth/UAC dialogs, banking) are always refused and cannot be allowlisted."
8884
+ );
8885
+ });
8886
+ });
8887
+ observerCommand.command("grant <process>").description(
8888
+ "Allow the observer to capture a process (adds it to observer.allowlist)"
8889
+ ).action(async (processName) => {
8890
+ await withDb(async (db) => {
8891
+ const next = applyObserverListChange(
8892
+ await getSetting(db, "observer.allowlist"),
8893
+ processName,
8894
+ "add"
8895
+ );
8896
+ await setSetting(db, "observer.allowlist", next);
8897
+ await syncObserverSidecarPolicy(db);
8898
+ console.log(`Granted: ${processName.trim().toLowerCase()}`);
8899
+ console.log(`observer.allowlist = ${next || "(empty)"}`);
8900
+ });
8901
+ });
8902
+ observerCommand.command("revoke <process>").description("Remove a process from observer.allowlist").action(async (processName) => {
8903
+ await withDb(async (db) => {
8904
+ const next = applyObserverListChange(
8905
+ await getSetting(db, "observer.allowlist"),
8906
+ processName,
8907
+ "remove"
8908
+ );
8909
+ await setSetting(db, "observer.allowlist", next);
8910
+ await syncObserverSidecarPolicy(db);
8911
+ console.log(`Revoked: ${processName.trim().toLowerCase()}`);
8912
+ console.log(`observer.allowlist = ${next || "(empty)"}`);
8913
+ });
8914
+ });
8915
+
8064
8916
  // src/cli/commands/profile.ts
8065
8917
  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";
8918
+ import { dirname as dirname5, join as join16, resolve as resolve4 } from "path";
8919
+ import { Command as Command11 } from "commander";
8068
8920
  var C2 = {
8069
8921
  reset: "\x1B[0m",
8070
8922
  bold: "\x1B[1m",
@@ -8073,7 +8925,7 @@ var C2 = {
8073
8925
  green: "\x1B[32m"
8074
8926
  };
8075
8927
  function defaultPersonalDir() {
8076
- return join15(homedir10(), "Documents", "zam");
8928
+ return join16(homedir10(), "Documents", "zam");
8077
8929
  }
8078
8930
  function render(profile) {
8079
8931
  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 +8936,7 @@ function render(profile) {
8084
8936
  console.log(` data dir: ${C2.cyan}${profile.dataDir}${C2.reset}`);
8085
8937
  console.log(` database: ${C2.cyan}${profile.dbPath}${C2.reset}`);
8086
8938
  }
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) => {
8939
+ 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
8940
  if (opts.mode && opts.mode !== "developer" && opts.mode !== "default") {
8089
8941
  console.error(`Invalid --mode: ${opts.mode}. Use developer or default.`);
8090
8942
  process.exit(1);
@@ -8120,8 +8972,8 @@ var profileCommand = new Command10("profile").description("Show or change this m
8120
8972
  });
8121
8973
 
8122
8974
  // 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) => {
8975
+ import { Command as Command12 } from "commander";
8976
+ 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
8977
  let db;
8126
8978
  try {
8127
8979
  db = await openDatabase();
@@ -8234,10 +9086,10 @@ ${"\u2550".repeat(50)}`);
8234
9086
  });
8235
9087
 
8236
9088
  // src/cli/commands/session.ts
8237
- import { readFileSync as readFileSync10 } from "fs";
9089
+ import { readFileSync as readFileSync11 } from "fs";
8238
9090
  import { input as input6, select as select2 } from "@inquirer/prompts";
8239
- import { Command as Command12 } from "commander";
8240
- var sessionCommand = new Command12("session").description(
9091
+ import { Command as Command13 } from "commander";
9092
+ var sessionCommand = new Command13("session").description(
8241
9093
  "Manage learning sessions"
8242
9094
  );
8243
9095
  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 +9141,18 @@ sessionCommand.command("start").description("Start a new learning session (revie
8289
9141
  task,
8290
9142
  execution_context: opts.context
8291
9143
  });
9144
+ const observerHint = opts.context === "ui" && !await isObserverPolicyConfigured(db) ? OBSERVER_POLICY_UNSET_HINT : null;
8292
9145
  await db.close();
8293
9146
  if (opts.quiet) {
8294
9147
  console.log(session.id);
8295
9148
  } else if (opts.json) {
8296
- console.log(JSON.stringify(session, null, 2));
9149
+ console.log(
9150
+ JSON.stringify(
9151
+ observerHint ? { ...session, observerPolicyHint: observerHint } : session,
9152
+ null,
9153
+ 2
9154
+ )
9155
+ );
8297
9156
  } else {
8298
9157
  console.log(`
8299
9158
  Session started: ${session.id}`);
@@ -8301,6 +9160,10 @@ Session started: ${session.id}`);
8301
9160
  console.log(` Task: ${session.task}`);
8302
9161
  console.log(` Context: ${session.execution_context}`);
8303
9162
  console.log(` Started: ${session.started_at}`);
9163
+ if (observerHint) {
9164
+ console.log(`
9165
+ ${observerHint}`);
9166
+ }
8304
9167
  }
8305
9168
  } catch (err) {
8306
9169
  await db?.close();
@@ -8415,7 +9278,7 @@ function loadPatternFile(path) {
8415
9278
  if (!path) return [];
8416
9279
  let parsed;
8417
9280
  try {
8418
- parsed = JSON.parse(readFileSync10(path, "utf-8"));
9281
+ parsed = JSON.parse(readFileSync11(path, "utf-8"));
8419
9282
  } catch (err) {
8420
9283
  throw new Error(
8421
9284
  `Cannot read synthesis patterns from ${path}: ${err.message}`
@@ -8606,8 +9469,8 @@ sessionCommand.command("end").description("End a session and show summary").requ
8606
9469
 
8607
9470
  // src/cli/commands/settings.ts
8608
9471
  import { existsSync as existsSync16 } from "fs";
8609
- import { Command as Command13 } from "commander";
8610
- var settingsCommand = new Command13("settings").description(
9472
+ import { Command as Command14 } from "commander";
9473
+ var settingsCommand = new Command14("settings").description(
8611
9474
  "Manage user settings"
8612
9475
  );
8613
9476
  var BOOLEAN_SETTING_KEYS = /* @__PURE__ */ new Set(["llm.enabled", "llm.vision.enabled"]);
@@ -8661,6 +9524,9 @@ settingsCommand.command("set <key> <value>").description("Set a setting").option
8661
9524
  await withDb(async (db) => {
8662
9525
  const parsedVal = normalizeSettingValue(key, value);
8663
9526
  await setSetting(db, key, parsedVal);
9527
+ if (key.startsWith("observer.")) {
9528
+ await syncObserverSidecarPolicy(db);
9529
+ }
8664
9530
  if (!opts.quiet) {
8665
9531
  console.log(`Set ${key} = ${parsedVal}`);
8666
9532
  }
@@ -8669,6 +9535,9 @@ settingsCommand.command("set <key> <value>").description("Set a setting").option
8669
9535
  settingsCommand.command("delete <key>").description("Delete a setting").option("--quiet", "Suppress output").action(async (key, opts) => {
8670
9536
  await withDb(async (db) => {
8671
9537
  const deleted = await deleteSetting(db, key);
9538
+ if (key.startsWith("observer.")) {
9539
+ await syncObserverSidecarPolicy(db);
9540
+ }
8672
9541
  if (!opts.quiet) {
8673
9542
  if (deleted) {
8674
9543
  console.log(`Deleted: ${key}`);
@@ -8776,32 +9645,32 @@ settingsCommand.command("repos").description("Show or set Personal, Team, and Or
8776
9645
  });
8777
9646
 
8778
9647
  // 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";
9648
+ import { copyFileSync as copyFileSync2, existsSync as existsSync17, mkdirSync as mkdirSync10, writeFileSync as writeFileSync9 } from "fs";
9649
+ import { basename as basename4, dirname as dirname6, join as join17 } from "path";
8781
9650
  import { fileURLToPath as fileURLToPath2 } from "url";
8782
- import { Command as Command14 } from "commander";
9651
+ import { Command as Command15 } from "commander";
8783
9652
  var packageRoot = [
8784
9653
  fileURLToPath2(new URL("../..", import.meta.url)),
8785
9654
  fileURLToPath2(new URL("../../..", import.meta.url))
8786
- ].find((candidate) => existsSync17(join16(candidate, "package.json"))) ?? fileURLToPath2(new URL("../..", import.meta.url));
9655
+ ].find((candidate) => existsSync17(join17(candidate, "package.json"))) ?? fileURLToPath2(new URL("../..", import.meta.url));
8787
9656
  var SKILL_PAIRS = [
8788
9657
  {
8789
- from: join16(packageRoot, ".claude", "skills", "zam", "SKILL.md"),
8790
- to: join16(".claude", "skills", "zam", "SKILL.md")
9658
+ from: join17(packageRoot, ".claude", "skills", "zam", "SKILL.md"),
9659
+ to: join17(".claude", "skills", "zam", "SKILL.md")
8791
9660
  },
8792
9661
  {
8793
- from: join16(packageRoot, ".agent", "skills", "zam", "SKILL.md"),
8794
- to: join16(".agent", "skills", "zam", "SKILL.md")
9662
+ from: join17(packageRoot, ".agent", "skills", "zam", "SKILL.md"),
9663
+ to: join17(".agent", "skills", "zam", "SKILL.md")
8795
9664
  },
8796
9665
  {
8797
- from: join16(packageRoot, ".agents", "skills", "zam", "SKILL.md"),
8798
- to: join16(".agents", "skills", "zam", "SKILL.md")
9666
+ from: join17(packageRoot, ".agents", "skills", "zam", "SKILL.md"),
9667
+ to: join17(".agents", "skills", "zam", "SKILL.md")
8799
9668
  }
8800
9669
  ];
8801
9670
  function copySkills(force, cwd = process.cwd()) {
8802
9671
  let anyAction = false;
8803
9672
  for (const { from, to } of SKILL_PAIRS) {
8804
- const dest = join16(cwd, to);
9673
+ const dest = join17(cwd, to);
8805
9674
  if (!existsSync17(from)) {
8806
9675
  console.warn(` warn source not found, skipping: ${from}`);
8807
9676
  continue;
@@ -8810,7 +9679,7 @@ function copySkills(force, cwd = process.cwd()) {
8810
9679
  console.log(` skip ${to} (already present \xE2\u20AC\u201D use --force to update)`);
8811
9680
  continue;
8812
9681
  }
8813
- mkdirSync9(dirname6(dest), { recursive: true });
9682
+ mkdirSync10(dirname6(dest), { recursive: true });
8814
9683
  copyFileSync2(from, dest);
8815
9684
  console.log(` copy ${to}`);
8816
9685
  anyAction = true;
@@ -8821,13 +9690,25 @@ function copySkills(force, cwd = process.cwd()) {
8821
9690
  );
8822
9691
  }
8823
9692
  }
9693
+ function formatDatabaseInitTarget(target) {
9694
+ switch (target.kind) {
9695
+ case "local":
9696
+ return `ZAM database at ${target.location} (local SQLite)`;
9697
+ case "turso-remote":
9698
+ return `ZAM database via Turso remote at ${target.location}`;
9699
+ case "turso-native":
9700
+ return `ZAM database via Turso native driver at ${target.location}`;
9701
+ case "turso-replica":
9702
+ return `ZAM database replica at ${target.location} syncing from ${target.syncUrl}`;
9703
+ }
9704
+ }
8824
9705
  async function initDatabase(skipInit) {
8825
9706
  if (skipInit) return;
8826
9707
  try {
8827
- const dbPath = getDefaultDbPath();
9708
+ const target = getDatabaseTargetInfo();
8828
9709
  const db = await openDatabaseWithSync({ initialize: true });
8829
9710
  await db.close();
8830
- console.log(` init ZAM database at ${dbPath}`);
9711
+ console.log(` init ${formatDatabaseInitTarget(target)}`);
8831
9712
  } catch (err) {
8832
9713
  const msg = err.message;
8833
9714
  if (!msg.includes("already")) {
@@ -8839,13 +9720,13 @@ async function initDatabase(skipInit) {
8839
9720
  }
8840
9721
  function writeClaudeMd(skipClaudeMd, cwd = process.cwd()) {
8841
9722
  if (skipClaudeMd) return;
8842
- const dest = join16(cwd, "CLAUDE.md");
9723
+ const dest = join17(cwd, "CLAUDE.md");
8843
9724
  if (existsSync17(dest)) {
8844
9725
  console.log(` skip CLAUDE.md (already present)`);
8845
9726
  return;
8846
9727
  }
8847
9728
  const name = basename4(cwd);
8848
- writeFileSync8(
9729
+ writeFileSync9(
8849
9730
  dest,
8850
9731
  `# ZAM Personal Kernel \xE2\u20AC\u201D ${name}
8851
9732
 
@@ -8873,13 +9754,13 @@ Use \`zam connector setup turso\` to store cloud credentials in
8873
9754
  }
8874
9755
  function writeAgentsMd(skipAgentsMd, cwd = process.cwd()) {
8875
9756
  if (skipAgentsMd) return;
8876
- const dest = join16(cwd, "AGENTS.md");
9757
+ const dest = join17(cwd, "AGENTS.md");
8877
9758
  if (existsSync17(dest)) {
8878
9759
  console.log(` skip AGENTS.md (already present)`);
8879
9760
  return;
8880
9761
  }
8881
9762
  const name = basename4(cwd);
8882
- writeFileSync8(
9763
+ writeFileSync9(
8883
9764
  dest,
8884
9765
  `# ZAM Personal Kernel - ${name}
8885
9766
 
@@ -8912,7 +9793,7 @@ Codex discovers repository skills under \`.agents/skills/\`. Run
8912
9793
  );
8913
9794
  console.log(` write AGENTS.md`);
8914
9795
  }
8915
- var setupCommand = new Command14("setup").description(
9796
+ var setupCommand = new Command15("setup").description(
8916
9797
  "Distribute ZAM skill files into this personal instance and initialize the database"
8917
9798
  ).option(
8918
9799
  "--force",
@@ -8933,8 +9814,8 @@ var setupCommand = new Command14("setup").description(
8933
9814
  );
8934
9815
 
8935
9816
  // src/cli/commands/skill.ts
8936
- import { Command as Command15 } from "commander";
8937
- var skillCommand = new Command15("skill").description(
9817
+ import { Command as Command16 } from "commander";
9818
+ var skillCommand = new Command16("skill").description(
8938
9819
  "Manage agent skill entries (task recipes)"
8939
9820
  );
8940
9821
  skillCommand.command("list").description("List all agent skills").option("--json", "Output as JSON").action(async (opts) => {
@@ -9019,10 +9900,10 @@ skillCommand.command("add").description("Register a new agent skill").requiredOp
9019
9900
  });
9020
9901
 
9021
9902
  // src/cli/commands/snapshot.ts
9022
- import { existsSync as existsSync18, mkdirSync as mkdirSync10, readFileSync as readFileSync11, writeFileSync as writeFileSync9 } from "fs";
9903
+ import { existsSync as existsSync18, mkdirSync as mkdirSync11, readFileSync as readFileSync12, writeFileSync as writeFileSync10 } from "fs";
9023
9904
  import { homedir as homedir11 } from "os";
9024
- import { dirname as dirname7, join as join17 } from "path";
9025
- import { Command as Command16 } from "commander";
9905
+ import { dirname as dirname7, join as join18 } from "path";
9906
+ import { Command as Command17 } from "commander";
9026
9907
  function defaultOutName() {
9027
9908
  const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/\.\d+Z$/, "").replace(/:/g, "-");
9028
9909
  return `zam-snapshot-${stamp}.sql`;
@@ -9036,12 +9917,12 @@ function summarize(tables) {
9036
9917
  }
9037
9918
  return { total, nonEmpty };
9038
9919
  }
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) => {
9920
+ 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
9921
  let db;
9041
9922
  try {
9042
9923
  db = await openDatabaseWithSync({ initialize: true });
9043
9924
  const snapshot = await exportSnapshot(db);
9044
- const personalDir = await getSetting(db, "personal.workspace_dir") || join17(homedir11(), "Documents", "zam");
9925
+ const personalDir = await getSetting(db, "personal.workspace_dir") || join18(homedir11(), "Documents", "zam");
9045
9926
  await db.close();
9046
9927
  db = void 0;
9047
9928
  const manifest = verifySnapshot(snapshot);
@@ -9050,12 +9931,12 @@ var exportCmd = new Command16("export").description("Write a portable SQL-text s
9050
9931
  process.stdout.write(snapshot);
9051
9932
  return;
9052
9933
  }
9053
- const out = opts.out ?? join17(personalDir, "snapshots", defaultOutName());
9934
+ const out = opts.out ?? join18(personalDir, "snapshots", defaultOutName());
9054
9935
  const dir = dirname7(out);
9055
9936
  if (dir && dir !== "." && !existsSync18(dir)) {
9056
- mkdirSync10(dir, { recursive: true });
9937
+ mkdirSync11(dir, { recursive: true });
9057
9938
  }
9058
- writeFileSync9(out, snapshot, "utf-8");
9939
+ writeFileSync10(out, snapshot, "utf-8");
9059
9940
  console.log(`Snapshot written: ${out}`);
9060
9941
  console.log(
9061
9942
  ` ${total} row(s)${nonEmpty.length ? ` \u2014 ${nonEmpty.join(", ")}` : ""}`
@@ -9066,14 +9947,14 @@ var exportCmd = new Command16("export").description("Write a portable SQL-text s
9066
9947
  process.exit(1);
9067
9948
  }
9068
9949
  });
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) => {
9950
+ 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
9951
  let db;
9071
9952
  try {
9072
9953
  if (!existsSync18(file)) {
9073
9954
  console.error(`Error: Snapshot file not found: ${file}`);
9074
9955
  process.exit(1);
9075
9956
  }
9076
- const snapshot = readFileSync11(file, "utf-8");
9957
+ const snapshot = readFileSync12(file, "utf-8");
9077
9958
  db = await openDatabaseWithSync({ initialize: true });
9078
9959
  const result = await importSnapshot(db, snapshot, { force: opts.force });
9079
9960
  await db.close();
@@ -9089,13 +9970,13 @@ var importCmd = new Command16("import").description("Restore a snapshot into the
9089
9970
  process.exit(1);
9090
9971
  }
9091
9972
  });
9092
- var verifyCmd = new Command16("verify").description("Check a snapshot's manifest and checksum without importing").argument("<file>", "Snapshot file to verify").action((file) => {
9973
+ var verifyCmd = new Command17("verify").description("Check a snapshot's manifest and checksum without importing").argument("<file>", "Snapshot file to verify").action((file) => {
9093
9974
  try {
9094
9975
  if (!existsSync18(file)) {
9095
9976
  console.error(`Error: Snapshot file not found: ${file}`);
9096
9977
  process.exit(1);
9097
9978
  }
9098
- const manifest = verifySnapshot(readFileSync11(file, "utf-8"));
9979
+ const manifest = verifySnapshot(readFileSync12(file, "utf-8"));
9099
9980
  const { total, nonEmpty } = summarize(manifest.tables);
9100
9981
  console.log(`Valid snapshot (format v${manifest.version})`);
9101
9982
  console.log(` created: ${manifest.createdAt}`);
@@ -9107,11 +9988,11 @@ var verifyCmd = new Command16("verify").description("Check a snapshot's manifest
9107
9988
  process.exit(1);
9108
9989
  }
9109
9990
  });
9110
- var snapshotCommand = new Command16("snapshot").description("Export, import, or verify a portable database snapshot").addCommand(exportCmd).addCommand(importCmd).addCommand(verifyCmd);
9991
+ var snapshotCommand = new Command17("snapshot").description("Export, import, or verify a portable database snapshot").addCommand(exportCmd).addCommand(importCmd).addCommand(verifyCmd);
9111
9992
 
9112
9993
  // 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) => {
9994
+ import { Command as Command18 } from "commander";
9995
+ 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
9996
  await withDb(async (db) => {
9116
9997
  const userId = await resolveUser(opts, db);
9117
9998
  const stats = await getUserStats(db, userId);
@@ -9147,8 +10028,8 @@ var statsCommand = new Command17("stats").description("Show learning dashboard f
9147
10028
  });
9148
10029
 
9149
10030
  // src/cli/commands/token.ts
9150
- import { Command as Command18 } from "commander";
9151
- var tokenCommand = new Command18("token").description(
10031
+ import { Command as Command19 } from "commander";
10032
+ var tokenCommand = new Command19("token").description(
9152
10033
  "Manage knowledge tokens"
9153
10034
  );
9154
10035
  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 +10318,9 @@ tokenCommand.command("status").description("Show full status of a token for a us
9437
10318
  import { spawn as spawn2, spawnSync } from "child_process";
9438
10319
  import { existsSync as existsSync19 } from "fs";
9439
10320
  import { homedir as homedir12 } from "os";
9440
- import { dirname as dirname8, join as join18 } from "path";
10321
+ import { dirname as dirname8, join as join19 } from "path";
9441
10322
  import { fileURLToPath as fileURLToPath3 } from "url";
9442
- import { Command as Command19 } from "commander";
10323
+ import { Command as Command20 } from "commander";
9443
10324
  var C3 = {
9444
10325
  reset: "\x1B[0m",
9445
10326
  red: "\x1B[31m",
@@ -9453,8 +10334,8 @@ function findDesktopDir() {
9453
10334
  for (const start of starts) {
9454
10335
  let dir = start;
9455
10336
  for (let i = 0; i < 10; i++) {
9456
- if (existsSync19(join18(dir, "desktop", "src-tauri", "tauri.conf.json"))) {
9457
- return join18(dir, "desktop");
10337
+ if (existsSync19(join19(dir, "desktop", "src-tauri", "tauri.conf.json"))) {
10338
+ return join19(dir, "desktop");
9458
10339
  }
9459
10340
  const parent = dirname8(dir);
9460
10341
  if (parent === dir) break;
@@ -9464,18 +10345,18 @@ function findDesktopDir() {
9464
10345
  return null;
9465
10346
  }
9466
10347
  function findBuiltApp(desktopDir) {
9467
- const releaseDir = join18(desktopDir, "src-tauri", "target", "release");
10348
+ const releaseDir = join19(desktopDir, "src-tauri", "target", "release");
9468
10349
  if (process.platform === "win32") {
9469
10350
  for (const name of ["ZAM.exe", "zam.exe", "zam-desktop.exe"]) {
9470
- const p = join18(releaseDir, name);
10351
+ const p = join19(releaseDir, name);
9471
10352
  if (existsSync19(p)) return p;
9472
10353
  }
9473
10354
  } else if (process.platform === "darwin") {
9474
- const app = join18(releaseDir, "bundle", "macos", "ZAM.app");
10355
+ const app = join19(releaseDir, "bundle", "macos", "ZAM.app");
9475
10356
  if (existsSync19(app)) return app;
9476
10357
  } else {
9477
10358
  for (const name of ["zam", "ZAM", "zam-desktop"]) {
9478
- const p = join18(releaseDir, name);
10359
+ const p = join19(releaseDir, name);
9479
10360
  if (existsSync19(p)) return p;
9480
10361
  }
9481
10362
  }
@@ -9483,10 +10364,10 @@ function findBuiltApp(desktopDir) {
9483
10364
  }
9484
10365
  function findInstalledApp() {
9485
10366
  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"];
10367
+ process.env.LOCALAPPDATA && join19(process.env.LOCALAPPDATA, "Programs", "ZAM", "ZAM.exe"),
10368
+ process.env.ProgramFiles && join19(process.env.ProgramFiles, "ZAM", "ZAM.exe"),
10369
+ process.env["ProgramFiles(x86)"] && join19(process.env["ProgramFiles(x86)"], "ZAM", "ZAM.exe")
10370
+ ] : process.platform === "darwin" ? ["/Applications/ZAM.app", join19(homedir12(), "Applications", "ZAM.app")] : ["/opt/ZAM/zam", "/usr/bin/zam-desktop"];
9490
10371
  return candidates.find((candidate) => candidate && existsSync19(candidate)) || null;
9491
10372
  }
9492
10373
  function runNpm(args, opts) {
@@ -9498,7 +10379,7 @@ function runNpm(args, opts) {
9498
10379
  return res.status ?? 1;
9499
10380
  }
9500
10381
  function ensureDesktopDeps(desktopDir) {
9501
- if (existsSync19(join18(desktopDir, "node_modules"))) return true;
10382
+ if (existsSync19(join19(desktopDir, "node_modules"))) return true;
9502
10383
  console.log(
9503
10384
  `${C3.cyan}Installing desktop dependencies (one-time)...${C3.reset}`
9504
10385
  );
@@ -9524,7 +10405,7 @@ function requireRust() {
9524
10405
  function hasMsvcBuildTools() {
9525
10406
  if (process.platform !== "win32") return true;
9526
10407
  const pf86 = process.env["ProgramFiles(x86)"] || "C:\\Program Files (x86)";
9527
- const vswhere = join18(
10408
+ const vswhere = join19(
9528
10409
  pf86,
9529
10410
  "Microsoft Visual Studio",
9530
10411
  "Installer",
@@ -9562,7 +10443,7 @@ function requireMsvcOnWindows() {
9562
10443
  return false;
9563
10444
  }
9564
10445
  function warnIfCliMissing(repoRoot) {
9565
- if (!existsSync19(join18(repoRoot, "dist", "cli", "index.js"))) {
10446
+ if (!existsSync19(join19(repoRoot, "dist", "cli", "index.js"))) {
9566
10447
  console.warn(
9567
10448
  `${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
10449
  );
@@ -9619,7 +10500,7 @@ function createShortcuts(appPath, repoRoot) {
9619
10500
  console.error(`${C3.red}\u2717 Could not create shortcuts.${C3.reset}`);
9620
10501
  }
9621
10502
  }
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(
10503
+ 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
10504
  "--build",
9624
10505
  "Build the native installer for your OS (needs Rust, one-time)"
9625
10506
  ).option("--shortcut", "Create Desktop + Start-menu shortcuts to the GUI").action((opts) => {
@@ -9646,7 +10527,7 @@ var uiCommand = new Command19("ui").description("Launch the ZAM Desktop GUI (Act
9646
10527
  );
9647
10528
  const code = runNpm(["run", "tauri", "build"], { cwd: desktopDir });
9648
10529
  if (code === 0) {
9649
- const bundle = join18(
10530
+ const bundle = join19(
9650
10531
  desktopDir,
9651
10532
  "src-tauri",
9652
10533
  "target",
@@ -9722,10 +10603,10 @@ ${C3.dim}After that, 'zam ui' launches it directly.${C3.reset}`
9722
10603
  });
9723
10604
 
9724
10605
  // src/cli/commands/update.ts
9725
- import { readFileSync as readFileSync12 } from "fs";
9726
- import { dirname as dirname9, join as join19 } from "path";
10606
+ import { readFileSync as readFileSync13 } from "fs";
10607
+ import { dirname as dirname9, join as join20 } from "path";
9727
10608
  import { fileURLToPath as fileURLToPath4 } from "url";
9728
- import { Command as Command20 } from "commander";
10609
+ import { Command as Command21 } from "commander";
9729
10610
  var GITHUB_REPO = "zam-os/zam";
9730
10611
  var CHANNELS = [
9731
10612
  "developer",
@@ -9746,7 +10627,7 @@ function currentVersion() {
9746
10627
  for (const up of ["..", "../..", "../../.."]) {
9747
10628
  try {
9748
10629
  const pkg2 = JSON.parse(
9749
- readFileSync12(join19(here, up, "package.json"), "utf-8")
10630
+ readFileSync13(join20(here, up, "package.json"), "utf-8")
9750
10631
  );
9751
10632
  if (pkg2.version) return pkg2.version;
9752
10633
  } catch {
@@ -9792,7 +10673,7 @@ function render2(decision) {
9792
10673
  );
9793
10674
  }
9794
10675
  }
9795
- var checkCmd = new Command20("check").description("Check whether a newer ZAM has been released").option(
10676
+ var checkCmd = new Command21("check").description("Check whether a newer ZAM has been released").option(
9796
10677
  "--latest <version>",
9797
10678
  "Compare against this version instead of fetching"
9798
10679
  ).option("--channel <channel>", "Override the detected install channel").option("--json", "Output as JSON").action(
@@ -9823,11 +10704,11 @@ var checkCmd = new Command20("check").description("Check whether a newer ZAM has
9823
10704
  }
9824
10705
  }
9825
10706
  );
9826
- var updateCommand = new Command20("update").description("Check for ZAM updates").addCommand(checkCmd);
10707
+ var updateCommand = new Command21("update").description("Check for ZAM updates").addCommand(checkCmd);
9827
10708
 
9828
10709
  // 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) => {
10710
+ import { Command as Command22 } from "commander";
10711
+ 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
10712
  await withDb(async (db) => {
9832
10713
  if (opts.set) {
9833
10714
  await setSetting(db, "user.id", opts.set);
@@ -9864,10 +10745,10 @@ var whoamiCommand = new Command21("whoami").description("Show or set the default
9864
10745
 
9865
10746
  // src/cli/commands/workspace.ts
9866
10747
  import { execSync as execSync6 } from "child_process";
9867
- import { existsSync as existsSync20, writeFileSync as writeFileSync10 } from "fs";
9868
- import { join as join20 } from "path";
10748
+ import { existsSync as existsSync20, writeFileSync as writeFileSync11 } from "fs";
10749
+ import { join as join21 } from "path";
9869
10750
  import { confirm as confirm3, input as input7 } from "@inquirer/prompts";
9870
- import { Command as Command22 } from "commander";
10751
+ import { Command as Command23 } from "commander";
9871
10752
  function runGit(cwd, args) {
9872
10753
  try {
9873
10754
  return execSync6(`git ${args}`, {
@@ -9879,7 +10760,7 @@ function runGit(cwd, args) {
9879
10760
  throw new Error(`Git command failed: ${err.message}`);
9880
10761
  }
9881
10762
  }
9882
- var workspaceCommand = new Command22("workspace").description(
10763
+ var workspaceCommand = new Command23("workspace").description(
9883
10764
  "Manage your ZAM learning workspace"
9884
10765
  );
9885
10766
  workspaceCommand.command("publish").description("Publish your local workspace sandbox to GitHub").action(async () => {
@@ -9912,15 +10793,15 @@ Active workspace: \x1B[36m${workspaceDir}\x1B[0m`);
9912
10793
  );
9913
10794
  process.exit(1);
9914
10795
  }
9915
- const gitignorePath = join20(workspaceDir, ".gitignore");
10796
+ const gitignorePath = join21(workspaceDir, ".gitignore");
9916
10797
  if (!existsSync20(gitignorePath)) {
9917
- writeFileSync10(
10798
+ writeFileSync11(
9918
10799
  gitignorePath,
9919
10800
  "node_modules/\n.agent/\n.agents/\n.claude/\n.gemini/\n.goose/\n*.log\n",
9920
10801
  "utf8"
9921
10802
  );
9922
10803
  }
9923
- const hasGitRepo = existsSync20(join20(workspaceDir, ".git"));
10804
+ const hasGitRepo = existsSync20(join21(workspaceDir, ".git"));
9924
10805
  if (!hasGitRepo) {
9925
10806
  console.log("Initializing local Git repository...");
9926
10807
  runGit(workspaceDir, "init -b main");
@@ -10016,9 +10897,9 @@ Active workspace: \x1B[36m${workspaceDir}\x1B[0m`);
10016
10897
  // src/cli/index.ts
10017
10898
  var __dirname = dirname10(fileURLToPath5(import.meta.url));
10018
10899
  var pkg = JSON.parse(
10019
- readFileSync13(join21(__dirname, "..", "..", "package.json"), "utf-8")
10900
+ readFileSync14(join22(__dirname, "..", "..", "package.json"), "utf-8")
10020
10901
  );
10021
- var program = new Command23();
10902
+ var program = new Command24();
10022
10903
  program.name("zam").description(
10023
10904
  "The Symbiotic Learning Kernel: Elevating Human Intelligence through AI Collaboration."
10024
10905
  ).version(pkg.version);
@@ -10034,6 +10915,7 @@ program.addCommand(uiCommand);
10034
10915
  program.addCommand(bridgeCommand);
10035
10916
  program.addCommand(skillCommand);
10036
10917
  program.addCommand(monitorCommand);
10918
+ program.addCommand(observerCommand);
10037
10919
  program.addCommand(settingsCommand);
10038
10920
  program.addCommand(whoamiCommand);
10039
10921
  program.addCommand(connectorCommand);