{
  "version": 3,
  "sources": ["../../../src/workers/config.ts", "../../../src/config/json-store.ts", "../../../src/daemon/config.ts", "../../../src/notifications/types.ts", "../../../src/tasks/types.ts", "../../../src/runtime-paths.ts", "../../../src/workers/paths.ts", "../../../src/workers/status.ts", "../../../src/workers/scope.ts", "../../../src/workers/render.ts", "../../../src/workers/viewer.ts", "../../../src/workers/standalone/status-line.ts"],
  "sourcesContent": ["/**\n * config.ts \u2014 the `workers` section of ~/.config/pai/config.json\n *\n * Everything that knows about worker providers reads this module: the CLI\n * (`pai worker \u2026`), the MCP tools (worker_*), and the Agent-routing hook.\n * Keep it free of commander/MCP imports so all three layers stay thin over it.\n *\n * Keys are never stored here \u2014 only paths to 0600 files. A provider without a\n * keyFile is a local server and gets the placeholder token \"local\".\n *\n * `protocol: \"openai\"`, `upstreamUrl` and `engine: \"codex\"` are reserved\n * schema fields for a later PAI version (proxy translation, Codex runner).\n * They parse, but every runtime path refuses them with a message that says\n * what to do instead.\n */\n\nimport { homedir } from \"node:os\";\nimport { join } from \"node:path\";\nimport { readJsonStrict, writeJsonAtomic } from \"../config/json-store.js\";\nimport { CONFIG_FILE } from \"../daemon/config.js\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\n/** Only \"anthropic\" works today; \"openai\" needs the PAI proxy (not built yet). */\nexport type WorkerProtocol = \"anthropic\" | \"openai\";\n\n/** Runner executable behind a provider. \"codex\" is reserved for a later version. */\nexport type WorkerEngine = \"claude\" | \"codex\";\n\nexport interface WorkerProvider {\n  enabled: boolean;\n  /** Wire protocol the baseUrl speaks. Only \"anthropic\" is supported now. */\n  protocol: WorkerProtocol;\n  /** Anthropic-compatible Messages API base. */\n  baseUrl: string;\n  /** 0600 file holding the auth token; null for local servers (\"local\"). */\n  keyFile: string | null;\n  models: {\n    default: string;\n    /** Alias used for cheap/fast work (spotcheck, research). */\n    fast?: string;\n  };\n  /** Extra environment variables for runs through this provider (string values). */\n  env: Record<string, string>;\n  note?: string;\n  /**\n   * OpenAI Chat Completions base (e.g. \"https://api.openai.com/v1\"). Reserved:\n   * only read once the PAI proxy exists; until then it is refused.\n   */\n  upstreamUrl?: string;\n  /** Reserved: the \"codex\" engine is not implemented yet. */\n  engine?: WorkerEngine;\n  /** Optional shell command printing 0\u2013100 (percent of quota used). */\n  quotaProbe?: string;\n  /** Auto-routing skips the provider at or above this percentage. Default 95. */\n  quotaSkipAt?: number;\n}\n\nexport interface WorkersPaneConfig {\n  enabled: boolean;\n  /** Font scale of the follow-pane profile relative to the iTerm default. */\n  fontScale: number;\n  /** Seconds a pane lingers after its worker goes quiet. */\n  autoExitSecs: number;\n}\n\nexport interface WorkersRoutingConfig {\n  /** Provider names in preference order; first usable one wins. */\n  order: string[];\n  /** Minutes a provider sits in cooldown after a quota/rate failure. */\n  cooldownMinutes: number;\n  /** Reroute a failed-before-first-tool run to the next provider. */\n  retryOnQuota: boolean;\n}\n\nexport interface WorkersConfig {\n  enabled: boolean;\n  /** Provider name, or \"auto\" for routing.order resolution. */\n  active: string | null;\n  providers: Record<string, WorkerProvider>;\n  /** Role \u2192 \"<provider>\" or \"<provider>/<modelAlias>\". */\n  roles: Record<string, string>;\n  pane: WorkersPaneConfig;\n  logDir: string;\n  routing: WorkersRoutingConfig;\n}\n\n// ---------------------------------------------------------------------------\n// Defaults\n// ---------------------------------------------------------------------------\n\nexport const DEFAULT_LOG_DIR = \"~/.claude/logs/workers\";\n\nexport const DEFAULT_PANE: WorkersPaneConfig = {\n  enabled: true,\n  fontScale: 0.6,\n  autoExitSecs: 10,\n};\n\nexport const DEFAULT_ROUTING: WorkersRoutingConfig = {\n  order: [],\n  cooldownMinutes: 30,\n  retryOnQuota: true,\n};\n\nexport function defaultWorkersConfig(): WorkersConfig {\n  return {\n    enabled: false,\n    active: null,\n    providers: {},\n    roles: {},\n    pane: { ...DEFAULT_PANE },\n    logDir: DEFAULT_LOG_DIR,\n    routing: { ...DEFAULT_ROUTING, order: [] },\n  };\n}\n\n// ---------------------------------------------------------------------------\n// Validation\n// ---------------------------------------------------------------------------\n\nexport class WorkersConfigError extends Error {}\n\nfunction bad(path: string, why: string): never {\n  throw new WorkersConfigError(`workers${path}: ${why}`);\n}\n\nfunction str(v: unknown): string {\n  return typeof v === \"string\" ? v : \"\";\n}\n\nfunction parseProvider(name: string, raw: unknown): WorkerProvider {\n  if (typeof raw !== \"object\" || raw === null) bad(`.providers.${name}`, \"must be an object\");\n  const p = raw as Record<string, unknown>;\n\n  const protocol = p.protocol === undefined ? \"anthropic\" : str(p.protocol);\n  if (protocol !== \"anthropic\" && protocol !== \"openai\") {\n    bad(`.providers.${name}.protocol`, `\"${str(p.protocol)}\" is neither \"anthropic\" nor \"openai\"`);\n  }\n  const engine = p.engine === undefined ? \"claude\" : str(p.engine);\n  if (engine !== \"claude\" && engine !== \"codex\") {\n    bad(`.providers.${name}.engine`, `\"${str(p.engine)}\" is neither \"claude\" nor \"codex\"`);\n  }\n\n  const baseUrl = str(p.baseUrl);\n  if (!baseUrl) bad(`.providers.${name}.baseUrl`, \"is required\");\n\n  const keyFile =\n    p.keyFile === undefined || p.keyFile === null || str(p.keyFile) === \"\"\n      ? null\n      : str(p.keyFile);\n\n  const modelsRaw = p.models === undefined ? {} : p.models;\n  if (typeof modelsRaw !== \"object\" || modelsRaw === null) {\n    bad(`.providers.${name}.models`, \"must be an object\");\n  }\n  const m = modelsRaw as Record<string, unknown>;\n  const defaultModel = str(m.default);\n  if (!defaultModel) bad(`.providers.${name}.models.default`, \"is required\");\n  const fast = m.fast === undefined ? undefined : str(m.fast);\n\n  const env: Record<string, string> = {};\n  if (p.env !== undefined) {\n    if (typeof p.env !== \"object\" || p.env === null || Array.isArray(p.env)) {\n      bad(`.providers.${name}.env`, \"must be an object of string values\");\n    }\n    for (const [k, v] of Object.entries(p.env as Record<string, unknown>)) {\n      if (typeof v !== \"string\") bad(`.providers.${name}.env.${k}`, \"must be a string\");\n      env[k] = v;\n    }\n  }\n\n  const quotaSkipAt = p.quotaSkipAt === undefined ? undefined : p.quotaSkipAt;\n  if (quotaSkipAt !== undefined) {\n    if (typeof quotaSkipAt !== \"number\" || quotaSkipAt < 0 || quotaSkipAt > 100) {\n      bad(`.providers.${name}.quotaSkipAt`, \"must be a number between 0 and 100\");\n    }\n  }\n\n  return {\n    enabled: p.enabled === undefined ? true : p.enabled === true,\n    protocol,\n    baseUrl,\n    keyFile,\n    models: fast ? { default: defaultModel, fast } : { default: defaultModel },\n    env,\n    ...(str(p.note) ? { note: str(p.note) } : {}),\n    ...(str(p.upstreamUrl) ? { upstreamUrl: str(p.upstreamUrl) } : {}),\n    ...(engine !== \"claude\" ? { engine } : {}),\n    ...(str(p.quotaProbe) ? { quotaProbe: str(p.quotaProbe) } : {}),\n    ...(quotaSkipAt !== undefined ? { quotaSkipAt } : {}),\n  };\n}\n\n/**\n * Parse and validate a raw `workers` value. Missing section \u2192 defaults.\n * Unknown-but-typed garbage \u2192 WorkersConfigError naming the offending field.\n */\nexport function parseWorkersConfig(raw: unknown): WorkersConfig {\n  const d = defaultWorkersConfig();\n  if (raw === undefined || raw === null) return d;\n  if (typeof raw !== \"object\" || Array.isArray(raw)) {\n    bad(\"\", \"section must be an object\");\n  }\n  const w = raw as Record<string, unknown>;\n\n  const providers: Record<string, WorkerProvider> = {};\n  if (w.providers !== undefined) {\n    if (typeof w.providers !== \"object\" || w.providers === null || Array.isArray(w.providers)) {\n      bad(\".providers\", \"must be an object keyed by provider name\");\n    }\n    for (const [name, p] of Object.entries(w.providers)) {\n      providers[name] = parseProvider(name, p);\n    }\n  }\n\n  const roles: Record<string, string> = {};\n  if (w.roles !== undefined) {\n    if (typeof w.roles !== \"object\" || w.roles === null || Array.isArray(w.roles)) {\n      bad(\".roles\", \"must be an object of role \u2192 provider[/alias]\");\n    }\n    for (const [role, target] of Object.entries(w.roles)) {\n      const t = str(target);\n      if (!t || t.includes(\" \")) bad(`.roles.${role}`, `invalid target \"${t}\"`);\n      roles[role] = t;\n    }\n  }\n\n  let pane = { ...DEFAULT_PANE };\n  if (w.pane !== undefined) {\n    if (typeof w.pane !== \"object\" || w.pane === null) bad(\".pane\", \"must be an object\");\n    const pc = w.pane as Record<string, unknown>;\n    if (pc.enabled !== undefined && typeof pc.enabled !== \"boolean\") bad(\".pane.enabled\", \"must be boolean\");\n    if (pc.fontScale !== undefined && (typeof pc.fontScale !== \"number\" || pc.fontScale <= 0)) {\n      bad(\".pane.fontScale\", \"must be a positive number\");\n    }\n    if (pc.autoExitSecs !== undefined && typeof pc.autoExitSecs !== \"number\") {\n      bad(\".pane.autoExitSecs\", \"must be a number\");\n    }\n    pane = {\n      enabled: pc.enabled === undefined ? DEFAULT_PANE.enabled : pc.enabled === true,\n      fontScale: pc.fontScale === undefined ? DEFAULT_PANE.fontScale : pc.fontScale,\n      autoExitSecs: pc.autoExitSecs === undefined ? DEFAULT_PANE.autoExitSecs : pc.autoExitSecs,\n    };\n  }\n\n  let routing = { ...DEFAULT_ROUTING, order: [] as string[] };\n  if (w.routing !== undefined) {\n    if (typeof w.routing !== \"object\" || w.routing === null) bad(\".routing\", \"must be an object\");\n    const r = w.routing as Record<string, unknown>;\n    if (r.order !== undefined) {\n      if (!Array.isArray(r.order) || r.order.some((x) => typeof x !== \"string\")) {\n        bad(\".routing.order\", \"must be an array of provider names\");\n      }\n      routing.order = r.order as string[];\n    }\n    if (r.cooldownMinutes !== undefined && typeof r.cooldownMinutes !== \"number\") {\n      bad(\".routing.cooldownMinutes\", \"must be a number\");\n    }\n    if (r.retryOnQuota !== undefined && typeof r.retryOnQuota !== \"boolean\") {\n      bad(\".routing.retryOnQuota\", \"must be boolean\");\n    }\n    routing = {\n      order: routing.order,\n      cooldownMinutes: r.cooldownMinutes === undefined ? DEFAULT_ROUTING.cooldownMinutes : r.cooldownMinutes,\n      retryOnQuota: r.retryOnQuota === undefined ? DEFAULT_ROUTING.retryOnQuota : r.retryOnQuota === true,\n    };\n  }\n\n  const active = w.active === undefined || w.active === null ? null : str(w.active);\n  if (active !== null && active !== \"auto\" && !(active in providers)) {\n    // Tolerated at parse time (a provider may have been removed while active\n    // still names it) but every consumer resolves it to a clear error.\n  }\n\n  return {\n    enabled: w.enabled === undefined ? d.enabled : w.enabled === true,\n    active,\n    providers,\n    roles,\n    pane,\n    logDir: str(w.logDir) || d.logDir,\n    routing,\n  };\n}\n\n// ---------------------------------------------------------------------------\n// Read / write\n// ---------------------------------------------------------------------------\n\n/**\n * Read the whole config file and return (raw, workers) \u2014 the raw record so\n * callers can rewrite it preserving every other section, the parsed+validated\n * workers section. Unreadable config throws (readJsonStrict), missing is fine.\n */\nexport function readWorkersSection(): {\n  raw: Record<string, unknown>;\n  workers: WorkersConfig;\n} {\n  const raw = readJsonStrict(CONFIG_FILE, \"~/.config/pai/config.json\");\n  return { raw, workers: parseWorkersConfig(raw.workers) };\n}\n\n/** Write the workers section back into the config file, atomically. */\nexport function writeWorkersSection(\n  raw: Record<string, unknown>,\n  workers: WorkersConfig\n): void {\n  raw.workers = workers;\n  writeJsonAtomic(CONFIG_FILE, raw, { label: \"~/.config/pai/config.json\" });\n}\n\n/** Expand a leading ~ (config values are written with `~` to stay portable). */\nexport function expandHome(p: string): string {\n  if (p === \"~\" || p.startsWith(\"~/\")) return join(homedir(), p.slice(1));\n  return p;\n}\n\n// ---------------------------------------------------------------------------\n// Reserved-field refusal (proxy / codex engine land in a later version)\n// ---------------------------------------------------------------------------\n\nexport function assertProviderRunnable(name: string, p: WorkerProvider): void {\n  if (p.protocol === \"openai\") {\n    throw new WorkersConfigError(\n      `provider \"${name}\" uses protocol \"openai\", which needs the PAI proxy \u2014 ` +\n        `not part of this PAI version yet. Put an Anthropic-compatible proxy ` +\n        `(such as claude-code-router) in front of the endpoint and set the ` +\n        `provider's baseUrl to the proxy instead.`\n    );\n  }\n  if (p.engine === \"codex\") {\n    throw new WorkersConfigError(\n      `provider \"${name}\" uses the \"codex\" engine, which is reserved for a ` +\n        `later PAI version. Remove \"engine\" (or set it to \"claude\") to run it ` +\n        `through Claude Code.`\n    );\n  }\n}\n\n/** Where a provider's key file lives, or null. Shared by run + test + add. */\nexport function providerKeyPath(p: WorkerProvider): string | null {\n  return p.keyFile ? expandHome(p.keyFile) : null;\n}\n\n/** Directory under which inline keys (MCP `key` field) are stored, 0600. */\nexport function keysDir(): string {\n  return join(homedir(), \".config\", \"pai\", \"keys\");\n}\n", "/**\n * json-store.ts \u2014 read/write JSON config files without destroying them\n *\n * The failure this exists to prevent:\n *\n *     try { return JSON.parse(read(path)); } catch { return {}; }\n *     ... later ...\n *     write(path, JSON.stringify(ourData));\n *\n * An unreadable file becomes an empty object, and the next write makes that\n * permanent. Silently, exit code 0. This shape appeared three times in this\n * repo against three different files, and twice in AIBroker.\n *\n * The distinction that matters is between *missing* and *unreadable*:\n *\n *   missing     \u2014 legitimate first run. Start fresh; writing is safe.\n *   unreadable  \u2014 the file exists and we could not parse it. Those bytes are\n *                 the only copy of something. Never overwrite them.\n *\n * Collapsing the second into the first is the bug.\n *\n * NOT everything deserves this guard. For a transient buffer \u2014 an undelivered\n * message queue, a cache \u2014 starting fresh IS the correct recovery, and\n * refusing to write would disable the feature permanently. Use `writeJsonAtomic`\n * alone there: it still prevents a crash from truncating a good file, without\n * blocking recovery. Reserve `readJsonStrict` for data a user cannot rebuild.\n */\n\nimport {\n  existsSync,\n  readFileSync,\n  writeFileSync,\n  copyFileSync,\n  renameSync,\n  unlinkSync,\n  mkdirSync,\n} from \"node:fs\";\nimport { dirname } from \"node:path\";\n\n/**\n * Read a JSON file, distinguishing \"absent\" from \"damaged\".\n *\n * @param path   file to read\n * @param label  how to name it to the user, e.g. \"~/.claude.json\"\n * @throws if the file exists but cannot be read or parsed\n */\nexport function readJsonStrict(path: string, label = path): Record<string, unknown> {\n  if (!existsSync(path)) return {};\n\n  let raw: string;\n  try {\n    raw = readFileSync(path, \"utf8\");\n  } catch (e) {\n    throw new Error(\n      `Could not read ${label}: ${e instanceof Error ? e.message : String(e)}\\n` +\n      `Refusing to continue \u2014 writing now would replace its contents with ours alone.`\n    );\n  }\n\n  try {\n    return JSON.parse(raw) as Record<string, unknown>;\n  } catch (e) {\n    throw new Error(\n      `${label} exists but is not valid JSON: ${e instanceof Error ? e.message : String(e)}\\n` +\n      `Refusing to continue \u2014 overwriting it would destroy whatever it holds.\\n` +\n      `Repair the file, or move it aside and re-run this command.`\n    );\n  }\n}\n\n/**\n * Write JSON without risking the existing file.\n *\n * Keeps a `.bak-pai` copy of the previous contents, then writes to a temp file\n * and renames. Rename is atomic within a filesystem, so a crash mid-write\n * leaves the original intact rather than truncated \u2014 which is how these files\n * become corrupt in the first place.\n */\nexport function writeJsonAtomic(\n  path: string,\n  data: Record<string, unknown>,\n  opts: { backup?: boolean; label?: string } = {}\n): void {\n  const { backup = true, label = path } = opts;\n  const serialized = JSON.stringify(data, null, 2) + \"\\n\";\n\n  const dir = dirname(path);\n  if (!existsSync(dir)) mkdirSync(dir, { recursive: true });\n\n  if (backup && existsSync(path)) {\n    try {\n      copyFileSync(path, `${path}.bak-pai`);\n    } catch (e) {\n      throw new Error(\n        `Could not back up ${label}: ${e instanceof Error ? e.message : String(e)}\\n` +\n        `Refusing to write without a backup.`\n      );\n    }\n  }\n\n  const tmp = `${path}.tmp-pai-${process.pid}`;\n  try {\n    writeFileSync(tmp, serialized, \"utf8\");\n    renameSync(tmp, path);\n  } catch (e) {\n    try { if (existsSync(tmp)) unlinkSync(tmp); } catch { /* best effort */ }\n    throw new Error(\n      `Failed to write ${label}: ${e instanceof Error ? e.message : String(e)}\\n` +\n      `The original is unchanged.`\n    );\n  }\n}\n", "/**\n * config.ts \u2014 Configuration loader for PAI Daemon\n *\n * Loads config from ~/.config/pai/config.json (XDG convention).\n * Deep-merges with defaults so partial configs work fine.\n * Expands ~ in path values at runtime.\n */\n\nimport { existsSync, readFileSync, mkdirSync, writeFileSync } from \"node:fs\";\nimport { homedir, userInfo } from \"node:os\";\nimport { join } from \"node:path\";\nimport type { NotificationConfig } from \"../notifications/types.js\";\nimport { DEFAULT_NOTIFICATION_CONFIG } from \"../notifications/types.js\";\nimport type { TaskConfig } from \"../tasks/types.js\";\nimport { DEFAULT_TASK_CONFIG } from \"../tasks/types.js\";\nimport { paiSocketPath } from \"../runtime-paths.js\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface SearchConfig {\n  /** Default search mode: 'keyword', 'semantic', or 'hybrid'. Default: 'keyword'. */\n  mode: \"keyword\" | \"semantic\" | \"hybrid\";\n  /** Enable cross-encoder reranking by default. Default: true. */\n  rerank: boolean;\n  /** Recency boost half-life in days. 0 = off. Default: 90. */\n  recencyBoostDays: number;\n  /** Default max results. Default: 10. */\n  defaultLimit: number;\n  /** Default snippet length for MCP results. Default: 200. */\n  snippetLength: number;\n}\n\nexport interface PostgresConfig {\n  /** Connection string \u2014 if set, overrides individual host/port/etc. fields */\n  connectionString?: string;\n  /** Postgres host (default: \"localhost\") */\n  host?: string;\n  /** Postgres port (default: 5432) */\n  port?: number;\n  /** Postgres database name (default: \"pai\") */\n  database?: string;\n  /** Postgres user (default: \"pai\") */\n  user?: string;\n  /** Postgres password (default: \"pai\") */\n  password?: string;\n  /** Maximum pool connections (default: 5) */\n  maxConnections?: number;\n  /** Connection timeout in ms (default: 5000) */\n  connectionTimeoutMs?: number;\n}\n\nexport interface PaiDaemonConfig {\n  /** Unix Domain Socket path for IPC */\n  socketPath: string;\n\n  /** Index schedule interval in seconds (default: 300 = 5 minutes) */\n  indexIntervalSecs: number;\n\n  /** Embedding schedule interval in seconds (default: 600 = 10 minutes) */\n  embedIntervalSecs: number;\n  /** Run an embed pass 60s after daemon start. Off by default: with a large\n   *  backlog it makes every restart a CPU storm, and it ignores the interval. */\n  embedOnStartup: boolean;\n\n  /** Local hour (0-23) to anchor the recurring index/embed cycle to. When unset,\n   *  the cycle is anchored to daemon start, so a daytime restart pins every\n   *  later pass to daytime too \u2014 a 24h interval does not by itself mean \"at\n   *  night\". Set this to run maintenance in a fixed window regardless of when\n   *  the machine last booted. */\n  maintenanceHour?: number;\n\n  /** Storage backend: \"sqlite\" (default) or \"postgres\" */\n  storageBackend: \"sqlite\" | \"postgres\";\n\n  /** PostgreSQL connection config (used when storageBackend = \"postgres\") */\n  postgres?: PostgresConfig;\n\n  /** Embedding model name (used for semantic/hybrid search) */\n  embeddingModel: string;\n\n  /** Log level */\n  logLevel: \"debug\" | \"info\" | \"warn\" | \"error\";\n\n  /** Obsidian vault root path for zettelkasten indexing. If set, vault indexing runs alongside project indexing. */\n  vaultPath?: string;\n\n  /** Registry project_id to use for vault chunks in memory_chunks. Default: auto-detected. */\n  vaultProjectId?: number;\n\n  /** Notification subsystem configuration */\n  notifications: NotificationConfig;\n\n  /** Search defaults \u2014 applied when MCP tool or CLI doesn't specify a value */\n  search: SearchConfig;\n\n  /** Task bus \u2014 optional external tracker for cross-session work */\n  tasks: TaskConfig;\n\n  /** Who \"me\" is \u2014 addresses that count as the user's own. */\n  identity: IdentityConfig;\n}\n\n/**\n * The user's own identity, for anything that delivers back to them.\n *\n * This exists so \"my own address\" is a fact the system can check rather than\n * something a model infers from context. An assistant deciding on the spot\n * whether an address looks like the user's is exactly the judgement that should\n * not be re-made per message.\n *\n * Empty by default and never guessed at install time: an empty `selfEmails`\n * means nothing is self-addressed, so anything reading this fails closed.\n */\nexport interface IdentityConfig {\n  /**\n   * Where digests and \"mail me X\" requests are delivered.\n   *\n   * Must be a mailbox separate from the account doing the sending. Gmail files\n   * a message sent from an account to itself \u2014 or to one of its own domain\n   * aliases \u2014 under Sent only, and it never reaches the inbox. The send reports\n   * success, so this fails silently and looks exactly like delivery. Observed\n   * 2026-08-01: owner@example.ch \u2192 owner@example.de, sent fine, invisible.\n   *\n   * Where a separate mailbox is not available, deliver by writing the message\n   * and adding the INBOX label to it rather than relying on the send path.\n   */\n  deliverTo?: string;\n\n  /**\n   * Every address that counts as the user's own.\n   *\n   * Used as an allowlist by anything that may act without review \u2014 outbound\n   * mail being the case that motivated it. Membership is the whole test: an\n   * address that is not listed is not the user's, however similar it looks.\n   * Plus-aliases and domain aliases must be listed explicitly rather than\n   * pattern-matched, because the patterns that would match them also match\n   * addresses belonging to other people.\n   */\n  selfEmails: string[];\n\n  /** The account used to send on the user's behalf, when one is configured. */\n  sendingAccount?: string;\n}\n\n// ---------------------------------------------------------------------------\n// Per-user Postgres isolation\n// ---------------------------------------------------------------------------\n\n/** Derive a per-user Postgres database name: pai_<username> */\nfunction perUserDbName(): string {\n  const username = userInfo().username;\n  // Sanitize: only allow alphanumeric and underscore for Postgres identifiers\n  const safe = username.replace(/[^a-zA-Z0-9_]/g, \"_\").toLowerCase();\n  return `pai_${safe}`;\n}\n\n/** Derive the per-user connection string */\nfunction perUserConnectionString(): string {\n  const db = perUserDbName();\n  return `postgresql://pai:pai@localhost:5432/${db}`;\n}\n\n// ---------------------------------------------------------------------------\n// Defaults\n// ---------------------------------------------------------------------------\n\nexport const DEFAULTS: PaiDaemonConfig = {\n  socketPath: paiSocketPath(),\n  indexIntervalSecs: 300,\n  embedIntervalSecs: 600,\n  embedOnStartup: false,\n  storageBackend: \"sqlite\",\n  postgres: {\n    connectionString: perUserConnectionString(),\n    maxConnections: 5,\n    connectionTimeoutMs: 5000,\n  },\n  embeddingModel: \"Snowflake/snowflake-arctic-embed-m-v1.5\",\n  logLevel: \"info\",\n  notifications: DEFAULT_NOTIFICATION_CONFIG,\n  tasks: DEFAULT_TASK_CONFIG,\n  // Deliberately empty. An install must not guess who the user is: a wrong\n  // guess here is an address that can be mailed without review.\n  identity: { selfEmails: [] },\n  search: {\n    mode: \"keyword\",\n    rerank: true,\n    recencyBoostDays: 90,\n    defaultLimit: 10,\n    snippetLength: 200,\n  },\n};\n\n/** Config template \u2014 generated at runtime so the DB name is per-user */\nfunction configTemplate(): string {\n  return `{\n  \"socketPath\": \"/tmp/pai.sock\",\n  \"indexIntervalSecs\": 300,\n  \"embedIntervalSecs\": 600,\n  \"storageBackend\": \"sqlite\",\n  \"postgres\": {\n    \"connectionString\": \"${perUserConnectionString()}\",\n    \"maxConnections\": 5,\n    \"connectionTimeoutMs\": 5000\n  },\n  \"embeddingModel\": \"Snowflake/snowflake-arctic-embed-m-v1.5\",\n  \"logLevel\": \"info\",\n  \"vaultPath\": \"\",\n  \"vaultProjectId\": 0,\n  \"search\": {\n    \"mode\": \"keyword\",\n    \"rerank\": true,\n    \"recencyBoostDays\": 90,\n    \"defaultLimit\": 10,\n    \"snippetLength\": 200\n  }\n}\n`;\n}\n\n// ---------------------------------------------------------------------------\n// Path helpers\n// ---------------------------------------------------------------------------\n\n/** Expand a leading ~ to the real home directory */\nexport function expandHome(p: string): string {\n  if (p === \"~\" || p.startsWith(\"~/\") || p.startsWith(\"~\\\\\")) {\n    return join(homedir(), p.slice(1));\n  }\n  return p;\n}\n\nexport const CONFIG_DIR = join(homedir(), \".config\", \"pai\");\nexport const CONFIG_FILE = join(CONFIG_DIR, \"config.json\");\n\n// ---------------------------------------------------------------------------\n// Deep merge (handles nested objects, not arrays)\n// ---------------------------------------------------------------------------\n\nfunction deepMerge<T extends object>(\n  target: T,\n  source: Record<string, unknown>\n): T {\n  const result = { ...target };\n  for (const key of Object.keys(source)) {\n    const srcVal = source[key];\n    if (srcVal === undefined || srcVal === null) continue;\n    const tgtVal = (target as Record<string, unknown>)[key];\n    if (\n      typeof srcVal === \"object\" &&\n      !Array.isArray(srcVal) &&\n      typeof tgtVal === \"object\" &&\n      tgtVal !== null &&\n      !Array.isArray(tgtVal)\n    ) {\n      (result as Record<string, unknown>)[key] = deepMerge(\n        tgtVal as object,\n        srcVal as Record<string, unknown>\n      );\n    } else {\n      (result as Record<string, unknown>)[key] = srcVal;\n    }\n  }\n  return result;\n}\n\n// ---------------------------------------------------------------------------\n// Config loader\n// ---------------------------------------------------------------------------\n\n/**\n * Load configuration from ~/.config/pai/config.json.\n * Returns defaults merged with any values found in the file.\n */\nexport function loadConfig(): PaiDaemonConfig {\n  if (!existsSync(CONFIG_FILE)) {\n    return { ...DEFAULTS };\n  }\n\n  let raw: string;\n  try {\n    raw = readFileSync(CONFIG_FILE, \"utf-8\");\n  } catch (e) {\n    process.stderr.write(\n      `[pai-daemon] Could not read config file at ${CONFIG_FILE}: ${e}\\n`\n    );\n    return { ...DEFAULTS };\n  }\n\n  let parsed: Record<string, unknown>;\n  try {\n    parsed = JSON.parse(raw) as Record<string, unknown>;\n  } catch (e) {\n    process.stderr.write(\n      `[pai-daemon] Config file is not valid JSON: ${e}\\n`\n    );\n    return { ...DEFAULTS };\n  }\n\n  // Compat: config.json may use \"obsidianVaultPath\" (legacy key) instead of \"vaultPath\".\n  // Map it across so the daemon picks it up correctly.\n  if (parsed.obsidianVaultPath && !parsed.vaultPath) {\n    parsed.vaultPath = parsed.obsidianVaultPath;\n    process.stderr.write(\n      `[pai-daemon] Config: mapped obsidianVaultPath \u2192 vaultPath (${parsed.vaultPath})\\n`\n    );\n  }\n\n  return deepMerge(DEFAULTS, parsed);\n}\n\n/**\n * Ensure ~/.config/pai/ exists and write a default config.json template\n * if none exists yet. Call this only from the `serve` command.\n */\nexport function ensureConfigDir(): void {\n  if (!existsSync(CONFIG_DIR)) {\n    mkdirSync(CONFIG_DIR, { recursive: true });\n    process.stderr.write(\n      `[pai-daemon] Created config directory: ${CONFIG_DIR}\\n`\n    );\n  }\n\n  if (!existsSync(CONFIG_FILE)) {\n    try {\n      writeFileSync(CONFIG_FILE, configTemplate(), \"utf-8\");\n      process.stderr.write(\n        `[pai-daemon] Wrote default config to: ${CONFIG_FILE}\\n`\n      );\n    } catch (e) {\n      process.stderr.write(\n        `[pai-daemon] Could not write default config: ${e}\\n`\n      );\n    }\n  }\n}\n", "/**\n * types.ts \u2014 Unified Notification Framework type definitions\n *\n * Defines the channel registry, event routing, and configuration schema\n * for PAI's notification subsystem.\n */\n\n// ---------------------------------------------------------------------------\n// Channel identifiers\n// ---------------------------------------------------------------------------\n\nexport type ChannelId = \"ntfy\" | \"whatsapp\" | \"macos\" | \"voice\" | \"cli\";\n\n// ---------------------------------------------------------------------------\n// Notification event types\n// ---------------------------------------------------------------------------\n\n/**\n * The semantic type of a notification event.\n * Used to route events to the appropriate channels.\n */\nexport type NotificationEvent =\n  | \"error\"\n  | \"progress\"\n  | \"completion\"\n  | \"info\"\n  | \"debug\";\n\n// ---------------------------------------------------------------------------\n// Notification mode\n// ---------------------------------------------------------------------------\n\n/**\n * The current notification mode.\n *\n * - \"auto\"      \u2014 Use the per-event routing table (default)\n * - \"voice\"     \u2014 All events go to voice (WhatsApp TTS)\n * - \"whatsapp\"  \u2014 All events go to WhatsApp text\n * - \"ntfy\"      \u2014 All events go to ntfy.sh\n * - \"macos\"     \u2014 All events go to macOS notifications\n * - \"cli\"       \u2014 All events go to CLI stdout only\n * - \"off\"       \u2014 Suppress all notifications\n */\nexport type NotificationMode =\n  | \"auto\"\n  | \"voice\"\n  | \"whatsapp\"\n  | \"ntfy\"\n  | \"macos\"\n  | \"cli\"\n  | \"off\";\n\n// ---------------------------------------------------------------------------\n// Per-channel configuration\n// ---------------------------------------------------------------------------\n\nexport interface NtfyChannelConfig {\n  enabled: boolean;\n  /** ntfy.sh topic URL, e.g. \"https://ntfy.sh/my-topic\" */\n  url?: string;\n  /** ntfy priority: min | low | default | high | urgent */\n  priority?: \"min\" | \"low\" | \"default\" | \"high\" | \"urgent\";\n}\n\nexport interface WhatsAppChannelConfig {\n  enabled: boolean;\n  /** Optional recipient (phone, JID, or contact name). Omit for self-chat. */\n  recipient?: string;\n}\n\nexport interface MacOsChannelConfig {\n  enabled: boolean;\n}\n\nexport interface VoiceChannelConfig {\n  enabled: boolean;\n  /** Kokoro voice name, e.g. \"bm_george\", \"af_bella\". Default: \"bm_george\" */\n  voiceName?: string;\n}\n\nexport interface CliChannelConfig {\n  enabled: boolean;\n}\n\nexport interface ChannelConfigs {\n  ntfy: NtfyChannelConfig;\n  whatsapp: WhatsAppChannelConfig;\n  macos: MacOsChannelConfig;\n  voice: VoiceChannelConfig;\n  cli: CliChannelConfig;\n}\n\n// ---------------------------------------------------------------------------\n// Routing table\n// ---------------------------------------------------------------------------\n\n/**\n * Maps each event type to the ordered list of channels that should receive it.\n * Only channels that are enabled in `channels` and present in this list are used.\n */\nexport type RoutingTable = {\n  [K in NotificationEvent]: ChannelId[];\n};\n\nexport const DEFAULT_ROUTING: RoutingTable = {\n  error:      [\"whatsapp\", \"macos\", \"ntfy\", \"cli\"],\n  completion: [\"whatsapp\", \"macos\", \"ntfy\", \"cli\"],\n  info:       [\"cli\"],\n  progress:   [\"cli\"],\n  debug:      [],\n};\n\n// ---------------------------------------------------------------------------\n// Top-level notification config (embedded in PaiDaemonConfig)\n// ---------------------------------------------------------------------------\n\nexport interface NotificationConfig {\n  /** Current routing mode. Default: \"auto\" */\n  mode: NotificationMode;\n  /** Per-channel configuration */\n  channels: ChannelConfigs;\n  /** Event \u2192 channel routing (used in \"auto\" mode) */\n  routing: RoutingTable;\n}\n\nexport const DEFAULT_CHANNELS: ChannelConfigs = {\n  ntfy: {\n    enabled: false,\n    url: undefined,\n    priority: \"default\",\n  },\n  whatsapp: {\n    enabled: true,\n    recipient: undefined,\n  },\n  macos: {\n    enabled: true,\n  },\n  voice: {\n    enabled: false,\n    voiceName: \"bm_george\",\n  },\n  cli: {\n    enabled: true,\n  },\n};\n\nexport const DEFAULT_NOTIFICATION_CONFIG: NotificationConfig = {\n  mode: \"auto\",\n  channels: DEFAULT_CHANNELS,\n  routing: DEFAULT_ROUTING,\n};\n\n// ---------------------------------------------------------------------------\n// Notification payload\n// ---------------------------------------------------------------------------\n\nexport interface NotificationPayload {\n  /** Semantic event type \u2014 used for routing */\n  event: NotificationEvent;\n  /** The notification message body */\n  message: string;\n  /** Optional title (used by macOS, ntfy) */\n  title?: string;\n}\n\n// ---------------------------------------------------------------------------\n// Provider interface\n// ---------------------------------------------------------------------------\n\nexport interface NotificationProvider {\n  readonly channelId: ChannelId;\n  /**\n   * Send a notification.\n   * Returns true on success, false on failure (failure is non-fatal).\n   */\n  send(payload: NotificationPayload, config: NotificationConfig): Promise<boolean>;\n}\n\n// ---------------------------------------------------------------------------\n// Send result\n// ---------------------------------------------------------------------------\n\nexport interface SendResult {\n  channelsAttempted: ChannelId[];\n  channelsSucceeded: ChannelId[];\n  channelsFailed: ChannelId[];\n  mode: NotificationMode;\n}\n", "/**\n * types.ts \u2014 Task Bus type definitions\n *\n * Defines the provider registry, ownership resolution, and configuration schema\n * for PAI's cross-session task subsystem.\n *\n * The task bus routes work between PAI sessions through an external tracker.\n * A session files a task; a routine reads it later and dispatches it to the\n * session that owns it \u2014 spawning one if none is running.\n *\n * See Notes/docs/task-bus.md for the architecture and its constraints.\n */\n\n// ---------------------------------------------------------------------------\n// Provider identifiers\n// ---------------------------------------------------------------------------\n\nexport type ProviderId = \"todoist\";\n\n// ---------------------------------------------------------------------------\n// Ownership\n// ---------------------------------------------------------------------------\n\n/**\n * Prefix marking a tracker label as a PAI ownership assertion.\n * A task labelled `pai:acme-api` is owned by the `acme-api` project.\n */\nexport const OWNER_LABEL_PREFIX = \"pai:\";\n\n/**\n * How a task's owner was determined. Recorded so the routine can explain\n * itself, and so a mis-resolution is diagnosable rather than silent.\n *\n * - \"label\"     \u2014 an explicit `pai:<project>` label (authoritative)\n * - \"container\" \u2014 the enclosing sub-project name matched a PAI alias (fallback)\n * - \"none\"      \u2014 unresolved; the task stays in the findings inbox\n */\nexport type OwnerSource = \"label\" | \"container\" | \"none\";\n\nexport interface TaskOwner {\n  /** Resolved PAI project short name, e.g. \"acme-api\". Null when UNROUTED. */\n  project: string | null;\n  /** Absolute path to the project root. Null when UNROUTED. */\n  rootPath: string | null;\n  source: OwnerSource;\n  /**\n   * The raw string that resolution was attempted against, kept for diagnostics\n   * when `source` is \"none\" \u2014 e.g. a \"Reading List \uD83D\uDCDA\" container matches no\n   * PAI project, which is expected rather than a fault.\n   */\n  rawHint?: string;\n}\n\n/** An unresolved owner. UNROUTED is a normal state, not an error. */\nexport const UNROUTED: TaskOwner = {\n  project: null,\n  rootPath: null,\n  source: \"none\",\n};\n\n// ---------------------------------------------------------------------------\n// Tasks\n// ---------------------------------------------------------------------------\n\nexport type TaskPriority = \"p1\" | \"p2\" | \"p3\" | \"p4\";\n\nexport interface Task {\n  /** Provider-native task ID. Opaque; never parsed. */\n  id: string;\n  title: string;\n  /**\n   * Full procedure AND reasoning \u2014 enough that the task is actionable months\n   * later, or by the user alone, without re-deriving anything. Enforced at\n   * filing time rather than left to discipline.\n   */\n  body: string;\n  owner: TaskOwner;\n  /** ISO 8601 date or datetime. Null when the task has no due date. */\n  due: string | null;\n  /**\n   * The tracker's own recurrence text, e.g. \"every day at 08:00\". Null for a\n   * one-off.\n   *\n   * Kept verbatim rather than parsed into a rule because it is also the only\n   * way to write a recurrence back: Todoist re-parses this string, and it is\n   * what lets a due date be restored without destroying the recurrence.\n   */\n  recurrence?: string | null;\n  priority: TaskPriority;\n  labels: string[];\n  /**\n   * Stable reference to the artifact this task is about. Prefer a `hook://`\n   * URL over a filesystem path \u2014 it survives renames and moves, and opens in\n   * DEVONthink To Go on iOS.\n   */\n  sourceUrl?: string;\n  /** True for organizational headers that cannot be completed. */\n  isHeader?: boolean;\n}\n\n/** A task being filed. `owner` is a project short name, resolved on write. */\nexport interface NewTask {\n  title: string;\n  body: string;\n  owner?: string | null;\n  due?: string;\n  priority?: TaskPriority;\n  labels?: string[];\n  sourceUrl?: string;\n  /**\n   * Sub-project to file into, created if absent.\n   *\n   * The convention is one sub-project per PAI project under the bus root. It\n   * previously existed only in the shape of the data, so every session had to\n   * re-derive it \u2014 and a session that inferred cautiously filed flat instead,\n   * which is exactly the pile the convention prevents.\n   */\n  into?: string;\n}\n\n// ---------------------------------------------------------------------------\n// Provider interface\n// ---------------------------------------------------------------------------\n\nexport interface ListOptions {\n  /** Only tasks due on or before this ISO date. Omit for all open tasks. */\n  dueBefore?: string;\n  /** Restrict to one resolved owner. Omit for every owner. */\n  owner?: string;\n  /** Include tasks that resolved to UNROUTED. Default: true. */\n  includeUnrouted?: boolean;\n  limit?: number;\n}\n\nexport interface TaskProvider {\n  readonly providerId: ProviderId;\n\n  /**\n   * False when no credential is configured. The bus degrades to a no-op\n   * rather than failing \u2014 a user without a tracker still gets working PAI.\n   */\n  isConfigured(): boolean;\n\n  listOpen(opts: ListOptions): Promise<Task[]>;\n  add(task: NewTask): Promise<Task>;\n  complete(id: string): Promise<void>;\n\n  /**\n   * Rewrite a task's due date through the tracker's natural-language field.\n   *\n   * Optional, and deliberately expressed as a string rather than a date: a\n   * recurring task's schedule and its next occurrence are the same field, so\n   * moving the date without the rule silently downgrades a routine to a one-off.\n   * A provider that cannot express both at once should not offer this.\n   */\n  setDue?(id: string, dueString: string): Promise<void>;\n\n  /**\n   * Sub-projects under the bus root \u2014 the set of addresses a task can be filed\n   * against, one per session.\n   *\n   * Optional because it is not universal: a tracker may address work by tag or\n   * list rather than by nested project, and forcing a nesting concept onto one\n   * that has none would mean faking it. A provider without these simply does\n   * not offer session-scoped inboxes, and callers say so rather than failing.\n   */\n  listSubProjects?(): Promise<Array<{ id: string; name: string }>>;\n  findOrCreateSubProject?(name: string): Promise<{ id: string; created: boolean }>;\n\n  /**\n   * The comment thread on a task, oldest first.\n   *\n   * Optional because not every tracker has threaded comments. Where it exists,\n   * the thread is usually where the reasoning lives \u2014 the question, the answer,\n   * the correction \u2014 and completing the task takes it out of view. That is what\n   * the archive exists to keep.\n   */\n  listComments?(taskId: string): Promise<Array<{ id: string; content: string; postedAt?: string }>>;\n\n  /**\n   * One task by id, whether open or completed.\n   *\n   * Needed because archiving runs at or after completion, and a completed task\n   * is gone from `listOpen` \u2014 which is exactly the moment its discussion stops\n   * being visible and most needs keeping.\n   */\n  getTask?(id: string): Promise<Task | null>;\n}\n\n// ---------------------------------------------------------------------------\n// Configuration\n// ---------------------------------------------------------------------------\n\nexport interface TodoistProviderConfig {\n  enabled: boolean;\n  /**\n   * API token. Resolution order is apiKey \u2192 TODOIST_API_KEY env \u2192 unconfigured.\n   *\n   * Never read this from another tool's config file. PAI ships as a product;\n   * scraping ~/.claude.json for a key belonging to the Todoist MCP is not\n   * acceptable even though the key is sitting there.\n   */\n  apiKey?: string;\n  /**\n   * Tracker project ID that roots the bus (the \"Claude \uD83E\uDD16\" project).\n   *\n   * Stored as an ID, never a name. Todoist's project search silently returns\n   * zero results for names containing emoji \u2014 resolving by name would report\n   * \"no tasks\" instead of failing, which is the exact class of silent failure\n   * this subsystem exists to surface.\n   */\n  rootProjectId?: string;\n  /** Section ID for the findings inbox. Tasks land here when UNROUTED. */\n  findingsSectionId?: string;\n}\n\nexport interface TaskConfig {\n  /** Master switch. When false the bus is inert. */\n  enabled: boolean;\n  providers: {\n    todoist: TodoistProviderConfig;\n  };\n  /**\n   * Dispatch work to the owning session automatically, spawning one if absent.\n   * Requires AIBroker. When false \u2014 or when AIBroker is unavailable \u2014 PAI\n   * reports which project owns each task and leaves acting to the user.\n   */\n  autoDispatch: boolean;\n\n  /**\n   * Seconds AIBroker may spend on a single dispatch, spawn included.\n   *\n   * Passed down to the transport so both sides share one deadline. Raise it on\n   * a loaded machine where sessions are slow to start accepting input.\n   */\n  dispatchTimeoutSecs?: number;\n\n  /**\n   * Project a task goes to when it carries the bare `pai` marker and its\n   * location says nothing \u2014 an Inbox capture, typically.\n   *\n   * This is the one thing a task's location cannot express: \"an AI should take\n   * this, and I do not know which one yet\". Everything else is answered by the\n   * project the task sits in.\n   *\n   * Unset means such a task stays UNROUTED, which is a legitimate choice: it\n   * then surfaces in the findings inbox for triage rather than being guessed at.\n   */\n  defaultOwner?: string;\n}\n\nexport const DEFAULT_TASK_CONFIG: TaskConfig = {\n  enabled: false,\n  providers: {\n    todoist: {\n      enabled: false,\n    },\n  },\n  autoDispatch: false,\n};\n\n// ---------------------------------------------------------------------------\n// Dispatch results\n// ---------------------------------------------------------------------------\n\n/**\n * What happened to one task during a dispatch run.\n *\n * - \"delivered\"    \u2014 sent to an already-running session, and confirmed submitted\n * - \"queued\"       \u2014 typed into a live session that was mid-turn, so submission\n *                    could not be confirmed inside the window. This is delivery:\n *                    Claude Code holds typed input until the current turn ends.\n *                    Never retried \u2014 the text is already in the input box, so a\n *                    second attempt is a second copy, not a retry. One trigger\n *                    arrived three times on 2026-08-01 for exactly that reason.\n * - \"spawned\"      \u2014 none running; one was launched, came up, and received it\n * - \"unrouted\"     \u2014 no owner resolved; left in the findings inbox\n * - \"unlaunchable\" \u2014 an owner resolved but no PAI alias exists to launch it\n * - \"unreachable\"  \u2014 a session was launched but never became ready to accept input\n * - \"skipped\"      \u2014 autoDispatch is off, or no transport; reported only\n *\n * `unlaunchable` and `unreachable` are distinct because the fixes differ:\n * the first is a setup gap (register an alias), the second is a runtime\n * failure (find out why the session did not come up). Collapsing them would\n * send users looking in the wrong place.\n *\n * None of these are errors. A task that could not be delivered is a routing\n * result to report, not an exception to throw.\n */\nexport type DispatchOutcome =\n  | \"delivered\"\n  | \"queued\"\n  | \"spawned\"\n  | \"unrouted\"\n  | \"unlaunchable\"\n  | \"unreachable\"\n  | \"skipped\";\n\nexport interface DispatchResult {\n  task: Task;\n  outcome: DispatchOutcome;\n  /** Session the task reached, when it reached one. */\n  session?: string;\n  /** Why the task did not reach a session. Present on failure outcomes. */\n  reason?: string;\n}\n", "/**\n * runtime-paths.ts \u2014 the shared files PAI talks to the running daemon through.\n *\n * Sockets, logs and pidfiles live at fixed absolute paths under /tmp, and every\n * one of them is shared with a *live* daemon. That makes them the same hazard\n * as any other piece of real user state a test can reach: writing one from a\n * test does not fail, it corrupts something already running, silently.\n *\n * The home guard in test/setup-home-guard.ts does not cover them. It works by\n * redirecting HOME, so it protects paths *derived* from the home directory and\n * nothing else \u2014 a hardcoded \"/tmp/pai.sock\" walks straight past it. Verified\n * on 2026-08-01 by snapshotting all five and running the suite: nothing writes\n * to them today. But \"no test does this yet\" is not a property, and the next\n * daemon test to be written would clobber the live socket with nothing to stop\n * it.\n *\n * So each path is read from an environment variable with the historical /tmp\n * value as its default. Behaviour is unchanged for every real invocation; the\n * test setup overrides the variables and the whole class becomes unreachable\n * rather than merely absent, the same way the home guard works.\n *\n * Read at call time, not at module load: the test setup file runs before any\n * module is imported, but resolving these eagerly would still bake in whatever\n * the environment held at import and make the override order matter.\n */\n\n/** IPC socket for the PAI daemon. */\nexport function paiSocketPath(): string {\n  return process.env.PAI_SOCKET_PATH ?? \"/tmp/pai.sock\";\n}\n\n/** IPC socket for the AIBroker daemon \u2014 read by PAI, owned by AIBroker. */\nexport function aibrokerSocketPath(): string {\n  return process.env.PAI_AIBROKER_SOCKET_PATH ?? \"/tmp/aibroker.sock\";\n}\n\n/** Where the daemon's stdout and stderr are collected. */\nexport function daemonLogPath(): string {\n  return process.env.PAI_DAEMON_LOG_PATH ?? \"/tmp/pai-daemon.log\";\n}\n\n/** Pidfile for the running daemon. */\nexport function daemonPidPath(): string {\n  return process.env.PAI_DAEMON_PID_PATH ?? \"/tmp/pai-daemon.pid\";\n}\n\n/** Where the launchd scheduler tick writes its output. */\nexport function schedulerLogPath(): string {\n  return process.env.PAI_SCHEDULER_LOG_PATH ?? \"/tmp/pai-scheduler.log\";\n}\n\n/**\n * Every runtime path, for the test guard to redirect in one place.\n *\n * Listed here rather than in the guard so that adding a path and forgetting to\n * protect it is not possible: the guard iterates this.\n */\nexport const RUNTIME_PATH_ENV_VARS = [\n  \"PAI_SOCKET_PATH\",\n  \"PAI_AIBROKER_SOCKET_PATH\",\n  \"PAI_DAEMON_LOG_PATH\",\n  \"PAI_DAEMON_PID_PATH\",\n  \"PAI_SCHEDULER_LOG_PATH\",\n] as const;\n", "/**\n * paths.ts \u2014 where worker run artefacts live.\n *\n * Everything a run writes (event mirror, status file, pane registry, routing\n * state, the empty MCP config) sits under one logDir so the whole tree is\n * disposable and configurable: `workers.logDir`, default ~/.claude/logs/workers.\n */\n\nimport { join } from \"node:path\";\nimport { expandHome, type WorkersConfig } from \"./config.js\";\n\n/** Absolute logDir for the given config. */\nexport function workersLogDir(config: WorkersConfig): string {\n  return expandHome(config.logDir);\n}\n\nexport function statusPath(logDir: string, id: string): string {\n  return join(logDir, `${id}.status`);\n}\n\nexport function eventsPath(logDir: string, id: string): string {\n  return join(logDir, `${id}.jsonl`);\n}\n\nexport function ledgerPath(logDir: string): string {\n  return join(logDir, \"ledger.log\");\n}\n\nexport function routingStatePath(logDir: string): string {\n  return join(logDir, \"routing-state.json\");\n}\n\nexport function panesDir(logDir: string): string {\n  return join(logDir, \"panes\");\n}\n\n/**\n * The strict empty MCP config handed to headless workers. Written into the\n * logDir on demand (never into the user's vendor config directory \u2014 this is\n * PAI state, not vendor state).\n */\nexport function noMcpConfigPath(logDir: string): string {\n  return join(logDir, \"no-mcp.json\");\n}\n", "/**\n * status.ts \u2014 the live per-worker status file.\n *\n * One JSON file per run, rewritten atomically on every turn, read by `ps`,\n * `follow`, the status line and the pane logic. Same field set the Python\n * runner wrote, plus `provider` (routing is multi-provider now) and `session`\n * (the AIBroker identity of the launching session, see scope.ts).\n */\n\nimport { existsSync, readFileSync, renameSync, writeFileSync, readdirSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { shortText } from \"./args.js\";\nimport { statusPath } from \"./paths.js\";\n\nexport interface WorkerSessionRef {\n  /** AIBroker/iTerm session id (the iTerm UUID). */\n  id: string;\n  /** AIBroker name of that session, when it has one. */\n  name: string;\n}\n\nexport interface WorkerStatus {\n  id: string;\n  pid: number;\n  label: string;\n  cwd: string;\n  /** iTerm session id of the launching terminal (\"\" outside iTerm). */\n  term: string;\n  provider: string;\n  model: string;\n  state: \"running\" | \"done\" | \"failed\" | \"killed\" | \"lost\";\n  started: string;\n  updated: string;\n  turns: number;\n  tools: number;\n  last: string;\n  rc: number | null;\n  secs: number | null;\n  /** Launching session resolved through AIBroker, when it was. */\n  session?: WorkerSessionRef | null;\n}\n\n/** Timestamp format shared by status files and the ledger. */\nexport function nowStamp(d: Date = new Date()): string {\n  const p = (n: number) => String(n).padStart(2, \"0\");\n  return (\n    `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ` +\n    `${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}`\n  );\n}\n\nexport function newWorkerId(d: Date = new Date(), pid = process.pid): string {\n  const p = (n: number) => String(n).padStart(2, \"0\");\n  return `${d.getFullYear()}${p(d.getMonth() + 1)}${p(d.getDate())}-${p(d.getHours())}${p(d.getMinutes())}${p(d.getSeconds())}-${pid}`;\n}\n\n/** Write status atomically (temp + rename) and stamp `updated`. */\nexport function saveStatus(logDir: string, status: WorkerStatus, d: Date = new Date()): void {\n  status.updated = nowStamp(d);\n  const path = statusPath(logDir, status.id);\n  const tmp = `${path}.tmp`;\n  writeFileSync(tmp, JSON.stringify(status), \"utf8\");\n  renameSync(tmp, path);\n}\n\n/** Load every status file in the logDir, oldest id first, skipping damage. */\nexport function loadStatuses(logDir: string): WorkerStatus[] {\n  if (!existsSync(logDir)) return [];\n  const out: WorkerStatus[] = [];\n  for (const name of readdirSync(logDir).sort()) {\n    if (!name.endsWith(\".status\")) continue;\n    try {\n      out.push(JSON.parse(readFileSync(join(logDir, name), \"utf8\")) as WorkerStatus);\n    } catch {\n      // a half-written or damaged status file is not worth a crash\n    }\n  }\n  return out;\n}\n\nexport function loadStatus(logDir: string, id: string): WorkerStatus | null {\n  const path = statusPath(logDir, id);\n  if (!existsSync(path)) return null;\n  try {\n    return JSON.parse(readFileSync(path, \"utf8\")) as WorkerStatus;\n  } catch {\n    return null;\n  }\n}\n\nexport function alive(pid: number | null | undefined): boolean {\n  if (!pid || pid <= 0) return false;\n  try {\n    process.kill(pid, 0);\n    return true;\n  } catch {\n    return false;\n  }\n}\n\n/** \"42s\" under 90s, \"7m\" above \u2014 the coarse age the table and bar show. */\nexport function ageOf(ts: string, now: Date = new Date()): string {\n  const t = Date.parse(ts.replace(\" \", \"T\"));\n  if (Number.isNaN(t)) return \"?\";\n  const s = Math.max(0, Math.floor((now.getTime() - t) / 1000));\n  return s < 90 ? `${s}s` : `${Math.floor(s / 60)}m`;\n}\n\n/** One-line description of a tool call, e.g. \"Bash: npm test\". */\nexport function describeTool(name: string, inp: unknown): string {\n  if (typeof inp !== \"object\" || inp === null) return name;\n  const i = inp as Record<string, unknown>;\n  const get = (k: string) => (typeof i[k] === \"string\" ? (i[k] as string) : \"\");\n  if (name === \"Bash\") return `Bash: ${shortText(get(\"command\"), 70)}`;\n  if (name === \"Read\" || name === \"Edit\" || name === \"Write\" || name === \"MultiEdit\") {\n    const file = get(\"file_path\").split(\"/\").pop() ?? \"\";\n    return `${name}: ${file}`;\n  }\n  if (name === \"Grep\" || name === \"Glob\") return `${name}: ${shortText(get(\"pattern\"), 50)}`;\n  if (name === \"WebSearch\") return `${name}: ${shortText(get(\"query\"), 50)}`;\n  if (name === \"WebFetch\") return `${name}: ${shortText(get(\"url\"), 60)}`;\n  return name;\n}\n", "/**\n * scope.ts \u2014 which workers \"belong\" to the terminal asking about them.\n *\n * Two identities, in priority order:\n *\n *   1. AIBroker's session registry (~/.aibroker/session-names.json): iTerm\n *      session UUID \u2192 session name. The launching session's UUID is stored in\n *      each worker's status file as `session.id`, so every pane of a named\n *      session sees its workers \u2014 regardless of tab layout.\n *   2. The iTerm tab key (`w<window>t<tab>` prefix of ITERM_SESSION_ID): the\n *      original heuristic, kept as the fallback when no registry entry\n *      matches (AIBroker absent, unnamed session, non-iTerm terminal).\n *\n * If neither is available, viewers fall back to \"all workers\".\n */\n\nimport { existsSync, readFileSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { join } from \"node:path\";\nimport type { WorkerStatus } from \"./status.js\";\n\nexport const AIBROKER_REGISTRY = join(homedir(), \".aibroker\", \"session-names.json\");\n\n/**\n * `w<window>t<tab>` prefix of an iTerm session id, \"\" if empty or malformed.\n * Panes split from the same tab share this prefix and differ only in `p<n>`.\n */\nexport function tabKey(term: string): string {\n  if (!term) return \"\";\n  const head = term.split(\"p\", 1)[0];\n  const parts = head.slice(1).split(\"t\");\n  if (head.startsWith(\"w\") && parts.length === 2 && parts.every((p) => /^\\d+$/.test(p))) {\n    return head;\n  }\n  return \"\";\n}\n\n/** Tab key of the iTerm tab this process runs in, \"\" if not inside iTerm2. */\nexport function currentTabKey(env: NodeJS.ProcessEnv = process.env): string {\n  return tabKey(env.ITERM_SESSION_ID ?? \"\");\n}\n\n/** The iTerm UUID part of an ITERM_SESSION_ID (after the last colon). */\nexport function itermUuid(term: string): string {\n  if (!term) return \"\";\n  return term.split(\":\").pop() ?? \"\";\n}\n\nexport interface SessionIdentity {\n  id: string;\n  name: string;\n}\n\n/**\n * Resolve the AIBroker session for an ITERM_SESSION_ID. Reads the persistent\n * name registry (the same store `aibroker_rename` writes); returns null when\n * AIBroker is absent or the session is not in it \u2014 callers then use the tab\n * key, which is the pre-AIBroker behaviour.\n */\nexport function resolveSession(\n  term: string,\n  registryPath: string = AIBROKER_REGISTRY\n): SessionIdentity | null {\n  const uuid = itermUuid(term);\n  if (!uuid) return null;\n  let names: Record<string, unknown>;\n  try {\n    if (!existsSync(registryPath)) return null;\n    names = JSON.parse(readFileSync(registryPath, \"utf8\")) as Record<string, unknown>;\n  } catch {\n    return null;\n  }\n  const name = names[uuid];\n  if (typeof name !== \"string\" || !name) return null;\n  return { id: uuid, name };\n}\n\n/**\n * True when `worker` was launched from `term` (the terminal asking about it).\n * Session-id match first; the tab key only when no registry entry matched \u2014\n * a session without a name keeps the tab-scoped behaviour it always had.\n */\nexport function workerInScope(worker: WorkerStatus, term: string): boolean {\n  if (!term) return false;\n  const uuid = itermUuid(term);\n  if (worker.session?.id && uuid) return worker.session.id === uuid;\n  return tabKey(worker.term) === tabKey(term) && tabKey(term) !== \"\";\n}\n\n/**\n * Registry key for pane files: the session id when AIBroker knows this\n * terminal, else the tab key, else the raw iTerm UUID.\n */\nexport function scopeKey(term: string): string {\n  const session = resolveSession(term);\n  if (session) return session.id;\n  return currentTabKey() || itermUuid(term);\n}\n\n/** `[Name]` when the worker has an AIBroker session name, else \"\". */\nexport function sessionTag(worker: { session?: { name?: string } | null }): string {\n  return worker.session?.name ? `[${worker.session.name}]` : \"\";\n}\n", "/**\n * render.ts \u2014 turn worker events into the lines a human reads.\n *\n * Ports the glm-ps transcript rendering: the gutter rules (dim `>` for reads\n * and searches, `$` for shell, magenta `~` + red/green diff for edits, green\n * `+` for writes), Read-result trimming (first 3 lines then a count), and the\n * result footer. Colors are applied only when the output is a TTY, so MCP\n * tool output stays plain text.\n */\n\nimport { relative, basename } from \"node:path\";\nimport { shortText } from \"./args.js\";\nimport { ageOf, type WorkerStatus, alive } from \"./status.js\";\nimport { sessionTag } from \"./scope.js\";\n\nexport type ColorEnabled = boolean;\n\nconst CODES = {\n  dim: \"2\",\n  bold: \"1\",\n  red: \"31\",\n  green: \"32\",\n  yellow: \"33\",\n  blue: \"34\",\n  mag: \"35\",\n  cyan: \"36\",\n} as const;\n\nexport type ColorName = keyof typeof CODES;\n\nexport function makeColor(enabled: ColorEnabled) {\n  return (name: ColorName, s: string): string =>\n    enabled ? `\\x1b[${CODES[name]}m${s}\\x1b[0m` : s;\n}\n\nexport type Paint = ReturnType<typeof makeColor>;\n\n/** Path relative to cwd when it lies inside it, otherwise unchanged. */\nexport function relPath(path: string, cwd: string): string {\n  if (!path || !cwd) return path;\n  let r: string;\n  try {\n    r = relative(cwd, path);\n  } catch {\n    return path;\n  }\n  return r.startsWith(\"..\") ? path : r;\n}\n\n/** Minimal line diff for Edit previews: common prefix/suffix, one hunk. */\nexport function unifiedDiffLines(oldStr: string, newStr: string): string[] {\n  const oldL = oldStr.split(\"\\n\");\n  const newL = newStr.split(\"\\n\");\n  let start = 0;\n  while (start < oldL.length && start < newL.length && oldL[start] === newL[start]) start++;\n  let endOld = oldL.length;\n  let endNew = newL.length;\n  while (endOld > start && endNew > start && oldL[endOld - 1] === newL[endNew - 1]) {\n    endOld--;\n    endNew--;\n  }\n  const out: string[] = [];\n  for (let i = start; i < endOld; i++) out.push(\"-\" + oldL[i]);\n  for (let i = start; i < endNew; i++) out.push(\"+\" + newL[i]);\n  return out;\n}\n\nexport interface ToolUseBlock {\n  type?: string;\n  text?: string;\n  name?: string;\n  id?: string;\n  input?: unknown;\n}\n\nexport interface StreamEventLike {\n  type?: string;\n  subtype?: string;\n  model?: string;\n  cwd?: string;\n  message?: { content?: ToolUseBlock[] };\n  result?: string;\n  is_error?: boolean;\n  num_turns?: number;\n  duration_ms?: number;\n}\n\nfunction renderToolUse(\n  c: Paint,\n  prefix: string,\n  name: string,\n  inp: unknown,\n  cwd: string\n): string[] {\n  const i = (typeof inp === \"object\" && inp !== null ? inp : {}) as Record<string, unknown>;\n  const get = (k: string) => (typeof i[k] === \"string\" ? (i[k] as string) : \"\");\n  if (name === \"Read\") {\n    return [`${prefix}${c(\"dim\", \">\")} Reading ${relPath(get(\"file_path\"), cwd)}`];\n  }\n  if (name === \"Grep\" || name === \"Glob\") {\n    return [\n      `${prefix}${c(\"dim\", \">\")} Searching ${c(\"cyan\", get(\"pattern\"))} in ${relPath(get(\"path\") || \".\", cwd)}`,\n    ];\n  }\n  if (name === \"Bash\") {\n    return [`${prefix}${c(\"dim\", \"$\")} ${shortText(get(\"command\"), 160)}`];\n  }\n  if (name === \"Edit\") {\n    const out = [`${prefix}${c(\"mag\", \"~\")} Editing ${relPath(get(\"file_path\"), cwd)}`];\n    for (const line of unifiedDiffLines(get(\"old_string\"), get(\"new_string\"))) {\n      if (line.startsWith(\"-\")) out.push(`${prefix}    ${c(\"red\", line)}`);\n      else if (line.startsWith(\"+\")) out.push(`${prefix}    ${c(\"green\", line)}`);\n      else out.push(`${prefix}    ${c(\"dim\", line)}`);\n    }\n    return out;\n  }\n  if (name === \"Write\") {\n    const n = get(\"content\").split(\"\\n\").length;\n    return [`${prefix}${c(\"green\", \"+\")} Writing ${relPath(get(\"file_path\"), cwd)} (${n} lines)`];\n  }\n  if (name === \"WebSearch\" || name === \"WebFetch\") {\n    return [`${prefix}${c(\"dim\", \">\")} ${name} ${shortText(get(\"query\") || get(\"url\"), 100)}`];\n  }\n  return [`${prefix}${c(\"dim\", \">\")} ${name} ${shortText(JSON.stringify(i), 120)}`];\n}\n\n/** Render one stream-json event; `tools` maps tool_use_id \u2192 tool name. */\nexport function renderEvent(\n  c: Paint,\n  prefix: string,\n  e: StreamEventLike,\n  cwd: string,\n  tools: Record<string, string>\n): string[] {\n  const out: string[] = [];\n  if (e.type === \"system\" && e.subtype === \"init\") {\n    out.push(\n      `${prefix}${c(\"dim\", `worker started \u00B7 model ${e.model ?? \"?\"} \u00B7 cwd ${basename(e.cwd || cwd || \"\")}`)}`\n    );\n  } else if (e.type === \"assistant\") {\n    for (const b of e.message?.content ?? []) {\n      if (b.type === \"text\" && (b.text ?? \"\").trim()) {\n        for (const ln of (b.text ?? \"\").trim().split(\"\\n\")) out.push(`${prefix}${ln}`);\n      } else if (b.type === \"tool_use\") {\n        out.push(...renderToolUse(c, prefix, b.name ?? \"?\", b.input, cwd));\n      }\n    }\n  } else if (e.type === \"user\") {\n    for (const b of e.message?.content ?? []) {\n      if (b.type !== \"tool_result\") continue;\n      let content: unknown = (b as { content?: unknown }).content ?? \"\";\n      if (Array.isArray(content)) {\n        content = content\n          .map((x) => (typeof x === \"object\" && x !== null && \"text\" in x ? String((x as { text?: string }).text ?? \"\") : \"\"))\n          .join(\"\\n\");\n      }\n      const text = String(content);\n      const isError = (b as { is_error?: boolean }).is_error === true;\n      if (!isError && tools[(b as { tool_use_id?: string }).tool_use_id ?? \"\"] === \"Read\") {\n        // keep Read results as the tool returned them (`<lineno>\\t<code>`),\n        // only their count is summarised\n        const lines = text.split(\"\\n\");\n        for (const ln of lines.slice(0, 3)) out.push(`${prefix}${c(\"dim\", ln)}`);\n        if (lines.length > 3) {\n          out.push(`${prefix}${c(\"dim\", `    \u2026 ${lines.length} lines`)}`);\n        }\n      } else if (isError) {\n        out.push(`${prefix}    ${c(\"red\", \"! \" + shortText(text, 200))}`);\n      } else if (text.trim()) {\n        out.push(`${prefix}    ${c(\"dim\", shortText(text, 120))}`);\n      }\n    }\n  } else if (e.type === \"result\") {\n    const ok = !e.is_error;\n    const mark = ok ? c(\"green\", \"\u2713 done\") : c(\"red\", \"\u2717 failed\");\n    out.push(`${prefix}${mark} \u00B7 ${e.num_turns ?? \"?\"} turns \u00B7 ${Math.floor((e.duration_ms ?? 0) / 1000)}s`);\n    for (const ln of String(e.result ?? \"\").trim().split(\"\\n\")) {\n      out.push(`${prefix}  ${ln}`);\n    }\n  }\n  return out;\n}\n\n/** Transcript header: id, provider, label, session name, project dir. */\nexport function headerLine(\n  c: Paint,\n  s: { id: string; label: string; cwd: string; provider?: string; session?: { name?: string } | null }\n): string {\n  const bits = [s.provider ? `[${s.provider}]` : \"\", sessionTag(s)].filter(Boolean).join(\" \");\n  const sep = bits ? `  ${bits}` : \"\";\n  return c(\"bold\", `\u2501\u2501 ${s.id}${sep}  ${s.label}  (${basename(s.cwd)})`);\n}\n\n/** The ps table (RUNNING + FINISHED last 8). */\nexport function renderTable(\n  c: Paint,\n  statuses: WorkerStatus[],\n  scopeLabel: string,\n  now: Date = new Date()\n): string {\n  const running: WorkerStatus[] = [];\n  const done: WorkerStatus[] = [];\n  for (const s of statuses) {\n    if (s.state === \"running\" && alive(s.pid)) running.push(s);\n    else {\n      if (s.state === \"running\") s.state = \"lost\";\n      done.push(s);\n    }\n  }\n  const clock = `${String(now.getHours()).padStart(2, \"0\")}:${String(now.getMinutes()).padStart(2, \"0\")}:${String(now.getSeconds()).padStart(2, \"0\")}`;\n  const lines: string[] = [c(\"bold\", `Workers  ${clock}`), \"\"];\n  lines.push(c(\"bold\", `RUNNING (${running.length})`));\n  if (!running.length) lines.push(\"  none\");\n  for (const s of running) {\n    lines.push(\n      `  ${c(\"cyan\", s.id)} [${s.provider}]  ${ageOf(s.started, now).padStart(4)} old  turns ${String(s.turns).padStart(2)}  tools ${String(s.tools).padStart(2)}  ${basename(s.cwd)}`\n    );\n    lines.push(`      task: ${s.label}`);\n    lines.push(`      now:  ${c(\"yellow\", s.last)}  (${ageOf(s.updated, now)} ago)`);\n  }\n  lines.push(\"\");\n  lines.push(c(\"bold\", \"FINISHED (last 8)\"));\n  for (const s of done.slice(-8)) {\n    const col = s.state === \"done\" ? \"green\" : \"red\";\n    const tag = sessionTag(s);\n    lines.push(\n      `  ${s.id} [${s.provider}]${tag ? \" \" + tag : \"\"}  ${c(col, s.state.padEnd(6))} rc=${s.rc}  ${String(s.secs ?? \"?\").padStart(4)}s  turns ${String(s.turns).padStart(2)}  tools ${String(s.tools).padStart(2)}  ${basename(s.cwd)}  ${s.label}`\n    );\n  }\n  lines.push(\"\");\n  lines.push(c(\"dim\", \"worker follow        live transcript of running workers\"));\n  lines.push(c(\"dim\", \"worker <id>          replay one worker\"));\n  lines.push(c(\"dim\", scopeLabel));\n  return lines.join(\"\\n\");\n}\n\n/** One-line status-bar summary (same shape glm-ps --status printed). */\nexport function renderStatusLine(\n  mine: WorkerStatus[],\n  now: Date = new Date()\n): string {\n  if (!mine.length) return \"\";\n  const running = mine.filter((s) => s.state === \"running\" && alive(s.pid));\n  const today = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, \"0\")}-${String(now.getDate()).padStart(2, \"0\")}`;\n  const doneToday = mine.filter((s) => s.state !== \"running\" && s.started.startsWith(today));\n  const ok = doneToday.filter((s) => s.state === \"done\").length;\n  const bad = doneToday.length - ok;\n  const providers = new Set(running.map((s) => s.provider));\n  const providerTag = providers.size === 1 ? [...providers][0] : \"workers\";\n  let head = `${providerTag} \u25B6${running.length}`;\n  const parts = running\n    .slice(0, 3)\n    .map((s) => `${s.id.slice(-4)} ${s.label.slice(0, 26)} ${ageOf(s.started, now)} \u00B7 ${s.last.slice(0, 30)}`);\n  if (parts.length) head += \"  \" + parts.join(\" | \");\n  if (doneToday.length) head += `   \u2713${ok} \u2717${bad} today`;\n  return head;\n}\n", "/**\n * viewer.ts \u2014 ps / follow / replay / status line over the worker logDir.\n *\n * Scoping: workers launched from this terminal's AIBroker session (or, as the\n * fallback, its iTerm tab) unless --all or an explicit worker id is given.\n * Outside iTerm, everything degrades to \"all workers\" \u2014 the Python behaviour.\n */\n\nimport { existsSync, openSync, readSync, closeSync, readFileSync } from \"node:fs\";\nimport { eventsPath } from \"./paths.js\";\nimport { alive, loadStatuses, type WorkerStatus } from \"./status.js\";\nimport { currentTabKey, resolveSession, workerInScope } from \"./scope.js\";\nimport {\n  headerLine,\n  makeColor,\n  renderEvent,\n  renderStatusLine,\n  renderTable,\n  type StreamEventLike,\n  type ToolUseBlock,\n} from \"./render.js\";\n\n// ---------------------------------------------------------------------------\n// ps\n// ---------------------------------------------------------------------------\n\nexport function psOutput(\n  logDir: string,\n  showAll: boolean,\n  env: NodeJS.ProcessEnv = process.env,\n  color = process.stdout.isTTY === true\n): string {\n  const c = makeColor(color);\n  const term = env.ITERM_SESSION_ID ?? \"\";\n  const statuses = loadStatuses(logDir);\n  const scoped = showAll || !term ? statuses : statuses.filter((s) => workerInScope(s, term));\n  const scopeLabel =\n    showAll || !term\n      ? \"scope: all workers\"\n      : resolveSession(term)\n        ? `scope: session ${resolveSession(term)!.name}`\n        : `scope: tab ${currentTabKey(env)} (this iTerm tab)`;\n  return renderTable(c, scoped, scopeLabel);\n}\n\n// ---------------------------------------------------------------------------\n// replay\n// ---------------------------------------------------------------------------\n\n/** The rendered transcript of one worker, from the start, as one string. */\nexport function replayOutput(\n  logDir: string,\n  wid: string,\n  color = process.stdout.isTTY === true,\n  tailLines?: number\n): string {\n  const path = eventsPath(logDir, wid);\n  if (!existsSync(path)) {\n    throw new Error(`no event log for ${wid}`);\n  }\n  const c = makeColor(color);\n  const st =\n    loadStatuses(logDir).find((s) => s.id === wid) ??\n    ({ id: wid, label: \"\", cwd: \"\", provider: \"\" } as WorkerStatus);\n  const tools: Record<string, string> = {};\n  const out: string[] = [headerLine(c, st)];\n  const lines = readFileSync(path, \"utf8\").split(\"\\n\");\n  for (const line of tailLines !== undefined ? lines.slice(-tailLines) : lines) {\n    if (!line.trim()) continue;\n    let e: StreamEventLike;\n    try {\n      e = JSON.parse(line) as StreamEventLike;\n    } catch {\n      continue;\n    }\n    collectTools(e, tools);\n    out.push(...renderEvent(c, \"  \", e, st.cwd ?? \"\", tools));\n  }\n  return out.join(\"\\n\");\n}\n\nfunction collectTools(e: StreamEventLike, tools: Record<string, string>): void {\n  if (e.type !== \"assistant\") return;\n  for (const b of e.message?.content ?? []) {\n    if (b.type === \"tool_use\" && b.id) tools[b.id] = b.name ?? \"?\";\n  }\n}\n\n// ---------------------------------------------------------------------------\n// follow\n// ---------------------------------------------------------------------------\n\ninterface FollowHandle {\n  fd: number;\n  buf: string;\n}\n\n/**\n * Tail one worker (target) or the running workers of this scope, live.\n * Auto-exit: with a target, wait `autoExit` seconds after its end; without\n * one, exit once no worker in scope has run for that many seconds in a row,\n * never within the first 30 s.\n */\nexport async function followWorkers(\n  logDir: string,\n  target: string | null,\n  showAll: boolean,\n  autoExit: number,\n  env: NodeJS.ProcessEnv = process.env,\n  color = process.stdout.isTTY === true\n): Promise<void> {\n  const c = makeColor(color);\n  const term = env.ITERM_SESSION_ID ?? \"\";\n  const scopeTab = target || showAll ? \"\" : currentTabKey(env);\n  const tools: Record<string, string> = {};\n  const handles = new Map<string, FollowHandle>();\n  const seenHeader = new Set<string>();\n  const finished = new Set<string>();\n  const started = Date.now();\n  let idleSince: number | null = null;\n  let aborted = false;\n  const onInt = () => {\n    aborted = true;\n  };\n  process.once(\"SIGINT\", onInt);\n\n  const runningIds = (): string[] =>\n    loadStatuses(logDir)\n      .filter(\n        (s) =>\n          s.state === \"running\" &&\n          alive(s.pid) &&\n          (target !== null || showAll || (scopeTab ? workerInScope(s, term) : true))\n      )\n      .map((s) => s.id);\n\n  try {\n    for (;;) {\n      if (aborted) return;\n      for (const wid of target ? [target] : runningIds()) {\n        if (handles.has(wid) || finished.has(wid)) continue;\n        const path = eventsPath(logDir, wid);\n        if (existsSync(path)) handles.set(wid, { fd: openSync(path, \"r\"), buf: \"\" });\n      }\n      const statuses = new Map(loadStatuses(logDir).map((s) => [s.id, s]));\n      const multi = handles.size > 1 || target === null;\n      let progressed = false;\n\n      for (const [wid, h] of [...handles.entries()]) {\n        const st = statuses.get(wid) ?? ({ id: wid, label: \"\", cwd: \"\", provider: \"\" } as WorkerStatus);\n        const prefix = multi ? c(\"cyan\", wid.slice(-4)) + c(\"dim\", \" \u2503 \") : \"  \";\n        // read everything appended since the last poll (fd position advances)\n        const buffer = Buffer.alloc(65536);\n        for (;;) {\n          let n: number;\n          try {\n            n = readSync(h.fd, buffer, 0, buffer.length, null);\n          } catch {\n            n = 0;\n          }\n          if (n <= 0) break;\n          h.buf += buffer.toString(\"utf8\", 0, n);\n        }\n        const lines = h.buf.split(\"\\n\");\n        h.buf = lines.pop() ?? \"\";\n        for (const line of lines) {\n          progressed = true;\n          if (!seenHeader.has(wid)) {\n            process.stdout.write(headerLine(c, st) + \"\\n\");\n            seenHeader.add(wid);\n          }\n          if (!line.trim()) continue;\n          let e: StreamEventLike;\n          try {\n            e = JSON.parse(line) as StreamEventLike;\n          } catch {\n            continue;\n          }\n          collectTools(e, tools);\n          for (const ln of renderEvent(c, prefix, e, st.cwd ?? \"\", tools)) {\n            process.stdout.write(ln + \"\\n\");\n          }\n          if (e.type === \"result\") finished.add(wid);\n        }\n        const ended =\n          finished.has(wid) ||\n          (st.state !== undefined && st.state !== \"running\" && !alive(st.pid));\n        if (ended) {\n          if (!finished.has(wid)) {\n            process.stdout.write(`${prefix}${c(\"red\", \"\u2717 \" + (st.state || \"ended\"))} \u00B7 ${st.last ?? \"\"}\\n`);\n            finished.add(wid);\n          }\n          closeSync(h.fd);\n          handles.delete(wid);\n        }\n      }\n\n      if (target && finished.has(target)) {\n        if (autoExit) {\n          await sleep(autoExit * 1000);\n          process.stdout.write(c(\"dim\", \"closing\") + \"\\n\");\n        }\n        return;\n      }\n      if (autoExit && !target) {\n        if (runningIds().length) {\n          idleSince = null;\n        } else if (idleSince === null) {\n          idleSince = Date.now();\n        } else if (Date.now() - started >= 30_000 && Date.now() - idleSince >= autoExit * 1000) {\n          process.stdout.write(c(\"dim\", \"closing\") + \"\\n\");\n          return;\n        }\n      }\n      if (!progressed) await sleep(500);\n    }\n  } finally {\n    process.removeListener(\"SIGINT\", onInt);\n    for (const h of handles.values()) {\n      try {\n        closeSync(h.fd);\n      } catch {\n        /* already closed */\n      }\n    }\n  }\n}\n\nfunction sleep(ms: number): Promise<void> {\n  return new Promise((r) => setTimeout(r, ms));\n}\n\n// ---------------------------------------------------------------------------\n// status line\n// ---------------------------------------------------------------------------\n\n/** Workers of this terminal for the status bar; \"\" when there are none. */\nexport function statusLineOutput(\n  logDir: string,\n  term: string,\n  cwd: string,\n  now: Date = new Date()\n): string {\n  const statuses = loadStatuses(logDir);\n  const mine = statuses.filter((s) => {\n    const sameScope = term && workerInScope(s, term);\n    const sameDir = cwd && s.cwd.startsWith(cwd);\n    return sameScope || (!term && sameDir);\n  });\n  return renderStatusLine(mine, now);\n}\n", "#!/usr/bin/env node\n\n/**\n * Standalone worker status-line entry point.\n *\n * statusline-command.sh calls this (argv: <ITERM_SESSION_ID> <PWD>) on every\n * status refresh, so it must stay a plain node script with no CLI framework\n * and no daemon IPC: read the config, read the worker statuses, print one\n * line or nothing. Built to dist/worker-status-line.mjs by scripts/build-hooks.mjs\n * and symlinked to ~/.claude/worker-status-line.mjs.\n */\n\nimport { readWorkersSection } from \"../config.js\";\nimport { workersLogDir } from \"../paths.js\";\nimport { statusLineOutput } from \"../viewer.js\";\n\nconst term = process.argv[2] ?? process.env.ITERM_SESSION_ID ?? \"\";\nconst cwd = process.argv[3] ?? process.cwd();\n\ntry {\n  const out = statusLineOutput(workersLogDir(readWorkersSection().workers), term, cwd);\n  if (out) process.stdout.write(out + \"\\n\");\n} catch {\n  // no workers section or a broken config: no line, never an error in the bar\n}\n"],
  "mappings": "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",
  "names": ["homedir", "join", "DEFAULT_ROUTING", "join", "homedir", "existsSync", "readFileSync", "renameSync", "writeFileSync", "join", "existsSync", "readFileSync", "join", "homedir", "join", "term", "term", "term", "bad", "term", "cwd"]
}
