@kanadego/dsh-heartbeat 1.9.3 → 2.0.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.
Files changed (57) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +103 -110
  3. package/client.js +12 -0
  4. package/config/policy.json +74 -74
  5. package/config/profile-schema.json +306 -306
  6. package/dist/{bindings-225SI7KC.js → bindings-U2M3CO2D.js} +3 -3
  7. package/dist/{chunk-TY6UP74X.js → chunk-3DNGY4DS.js} +2 -2
  8. package/dist/{chunk-CUOSICYJ.js → chunk-544QOJN5.js} +397 -23
  9. package/dist/chunk-544QOJN5.js.map +1 -0
  10. package/dist/{chunk-TRO4E6WG.js → chunk-6MVKWICX.js} +587 -139
  11. package/dist/chunk-6MVKWICX.js.map +1 -0
  12. package/dist/{chunk-WLZSABBH.js → chunk-6XM6KY7M.js} +27 -10
  13. package/dist/chunk-6XM6KY7M.js.map +1 -0
  14. package/dist/chunk-AKIJ4LS2.js +680 -0
  15. package/dist/chunk-AKIJ4LS2.js.map +1 -0
  16. package/dist/{chunk-KB5SMG3F.js → chunk-DC5HMCJC.js} +3 -3
  17. package/dist/{chunk-3MDBU6WY.js → chunk-DZ7YCFGX.js} +37 -7
  18. package/dist/chunk-DZ7YCFGX.js.map +1 -0
  19. package/dist/{chunk-WRUTATW4.js → chunk-IBBAISSW.js} +13 -2
  20. package/dist/chunk-IBBAISSW.js.map +1 -0
  21. package/dist/{chunk-XYYEJK3W.js → chunk-IFRMKVIC.js} +82 -21
  22. package/dist/chunk-IFRMKVIC.js.map +1 -0
  23. package/dist/{chunk-XFNCTOW7.js → chunk-NCPKOWS7.js} +52 -4
  24. package/dist/chunk-NCPKOWS7.js.map +1 -0
  25. package/dist/{chunk-JDH2LDFS.js → chunk-TVX4HV4Y.js} +4 -6
  26. package/dist/{chunk-JDH2LDFS.js.map → chunk-TVX4HV4Y.js.map} +1 -1
  27. package/dist/cli/index.js +149 -26
  28. package/dist/cli/index.js.map +1 -1
  29. package/dist/{inbox-P26DDMWC.js → inbox-OWBK6PE2.js} +8 -7
  30. package/dist/index.d.ts +48 -315
  31. package/dist/index.js +259 -918
  32. package/dist/index.js.map +1 -1
  33. package/dist/migrate-TQ5PB2DI.js +27 -0
  34. package/dist/{orchestrator-ZPRGVDCB.js → orchestrator-P7AEWVYR.js} +23 -12
  35. package/dist/{pool-UTXQJJ5C.js → pool-2T6TVS2L.js} +4 -5
  36. package/dist/{snapshot-K5VHETTK.js → snapshot-F5B2DD56.js} +5 -5
  37. package/package.json +84 -83
  38. package/dist/chunk-3MDBU6WY.js.map +0 -1
  39. package/dist/chunk-CUOSICYJ.js.map +0 -1
  40. package/dist/chunk-IV2ZWQA3.js +0 -56
  41. package/dist/chunk-IV2ZWQA3.js.map +0 -1
  42. package/dist/chunk-K5Y6JP2B.js +0 -399
  43. package/dist/chunk-K5Y6JP2B.js.map +0 -1
  44. package/dist/chunk-TRO4E6WG.js.map +0 -1
  45. package/dist/chunk-WLZSABBH.js.map +0 -1
  46. package/dist/chunk-WRUTATW4.js.map +0 -1
  47. package/dist/chunk-XFNCTOW7.js.map +0 -1
  48. package/dist/chunk-XYYEJK3W.js.map +0 -1
  49. package/dist/migrate-D4VKAKYT.js +0 -21
  50. /package/dist/{bindings-225SI7KC.js.map → bindings-U2M3CO2D.js.map} +0 -0
  51. /package/dist/{chunk-TY6UP74X.js.map → chunk-3DNGY4DS.js.map} +0 -0
  52. /package/dist/{chunk-KB5SMG3F.js.map → chunk-DC5HMCJC.js.map} +0 -0
  53. /package/dist/{inbox-P26DDMWC.js.map → inbox-OWBK6PE2.js.map} +0 -0
  54. /package/dist/{migrate-D4VKAKYT.js.map → migrate-TQ5PB2DI.js.map} +0 -0
  55. /package/dist/{orchestrator-ZPRGVDCB.js.map → orchestrator-P7AEWVYR.js.map} +0 -0
  56. /package/dist/{pool-UTXQJJ5C.js.map → pool-2T6TVS2L.js.map} +0 -0
  57. /package/dist/{snapshot-K5VHETTK.js.map → snapshot-F5B2DD56.js.map} +0 -0
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/core/paths.ts","../src/core/data-relocate.ts","../src/core/file-lock.ts","../src/core/fs-walk.ts","../src/vault/vault.ts"],"sourcesContent":["// Workspace/data directory resolution and package asset location.\n//\n// The data dir is the ONLY directory the plugin writes to at runtime\n// (requirement 8). Resolution order: env override > plugin config > default\n// (`$DSH_HOME/heartbeat-data`) — with a legacy fallback to `packageRoot()/data`\n// for installs that predate v1.9.2, which is relocated out of the package on\n// first sight (issue #2: a reinstall replaced the package dir and took the\n// user's data with it). All fs writes must go through the path guard created\n// from this dir (see core/path-guard.ts).\n\nimport fs from 'node:fs';\nimport os from 'node:os';\nimport path from 'node:path';\nimport { fileURLToPath } from 'node:url';\nimport { hasPayload, relocateDataDir, type RelocateResult } from './data-relocate.js';\n\nexport interface WorkspacePaths {\n /** Guard boundary and the only writable tree at runtime. */\n dataDir: string;\n settingsDir: string;\n logsDir: string;\n tmpDir: string;\n exportsDir: string;\n /** Package root (where package.json lives). Read-only at runtime. */\n packageRoot: string;\n configDir: string;\n assetsDir: string;\n /**\n * Set only when a legacy package-local data dir was found: the outcome of\n * the one-time relocation. Absent for explicit dirs and fresh installs.\n */\n relocation?: RelocateResult;\n}\n\nexport interface WorkspaceConfigInput {\n dataDir?: string | null;\n}\n\nlet resolved: WorkspacePaths | null = null;\n\n/** Walk up from a module file to the directory containing package.json. */\nexport function findPackageRoot(startFile: string): string {\n let dir = path.dirname(path.resolve(startFile));\n for (;;) {\n if (fs.existsSync(path.join(dir, 'package.json'))) return dir;\n const parent = path.dirname(dir);\n if (parent === dir) throw new Error('package.json not found above ' + startFile);\n dir = parent;\n }\n}\n\nexport function packageRoot(): string {\n return findPackageRoot(fileURLToPath(import.meta.url));\n}\n\n/**\n * Default data dir: `$DSH_HOME/heartbeat-data` (or `~/.dsh/heartbeat-data`).\n * Deliberately outside the package tree — reinstalling or upgrading the plugin\n * replaces that directory and must not touch user data (issue #2).\n */\nexport function defaultDataDir(\n env: NodeJS.ProcessEnv = process.env,\n home: string = os.homedir(),\n): string {\n return path.join(dshHomeDir(env, home), 'heartbeat-data');\n}\n\n/**\n * The DSH home: `$DSH_HOME` when set, else `~/.dsh`.\n *\n * HB-20 (2026-10-17): this is the ONLY place that computes it. Callers used to\n * hard-code `path.join(os.homedir(), '.dsh')`, which silently ignored the\n * `$DSH_HOME` override — a relocated host got its sessions/storages read from\n * the wrong tree (and, for the data dir, wrote to the wrong one).\n */\nexport function dshHomeDir(\n env: NodeJS.ProcessEnv = process.env,\n home: string = os.homedir(),\n): string {\n const override = env.DSH_HOME?.trim();\n return override && override.length > 0 ? override : path.join(home, '.dsh');\n}\n\n/**\n * Resolve and create the workspace layout. Idempotent; must be called once\n * from the plugin entry before any fs write happens.\n */\nexport function initWorkspace(config: WorkspaceConfigInput = {}): WorkspacePaths {\n if (resolved) return resolved;\n const root = packageRoot();\n const explicit = process.env.HEARTBEAT_DATA_DIR || config.dataDir;\n const homeDir = path.resolve(defaultDataDir());\n const legacyDir = path.join(root, 'data');\n let dataDir: string;\n let relocation: RelocateResult | undefined;\n if (explicit) {\n // Explicit configuration always wins, and never moves anything.\n dataDir = path.resolve(explicit);\n } else if (hasPayload(homeDir)) {\n // Already on the new home (a previous run, or a reinstall that kept it).\n dataDir = homeDir;\n } else if (hasPayload(legacyDir)) {\n // Pre-v1.9.2 install: the data lived inside the package. Move it out, and\n // keep serving the legacy tree unless the move verified end to end.\n relocation = relocateDataDir({ srcDir: legacyDir, dstDir: homeDir });\n dataDir = relocation.action === 'relocated' ? homeDir : legacyDir;\n } else {\n dataDir = homeDir;\n }\n const paths: WorkspacePaths = {\n dataDir,\n settingsDir: path.join(dataDir, 'settings'),\n logsDir: path.join(dataDir, 'logs'),\n tmpDir: path.join(dataDir, 'tmp'),\n exportsDir: path.join(dataDir, 'exports'),\n packageRoot: root,\n configDir: path.join(root, 'config'),\n assetsDir: path.join(root, 'assets'),\n ...(relocation ? { relocation } : {}),\n };\n // Bootstrap creation happens before the guard exists; creating exactly one\n // directory tree at a known location is the safe window for this.\n for (const dir of [\n paths.dataDir,\n paths.settingsDir,\n paths.logsDir,\n paths.tmpDir,\n paths.exportsDir,\n ]) {\n fs.mkdirSync(dir, { recursive: true });\n }\n resolved = paths;\n return paths;\n}\n\n/** Access already-initialized paths. Throws if initWorkspace was not called. */\nexport function workspace(): WorkspacePaths {\n if (!resolved) throw new Error('workspace not initialized: call initWorkspace() first');\n return resolved;\n}\n\n/** Test-only: forget the resolved singleton so a later init re-resolves. */\nexport function resetWorkspaceForTest(): void {\n resolved = null;\n}\n","// One-time relocation of the data dir out of the package tree (issue #2).\n//\n// Installs created before v1.9.2 defaulted to `packageRoot()/data`, and every\n// `dsh plugin add` / upgrade replaces the package directory wholesale — so a\n// reinstall deleted the profile, the gate and the ledger, which the original\n// design meant to keep until an explicit CLI `burn`. The default now lives at\n// `$DSH_HOME/heartbeat-data`; this module moves an existing package-local tree\n// there.\n//\n// Safety order — a failure at any point leaves the original untouched:\n// 1. copy src -> backup (kept until the move is verified and committed)\n// 2. copy src -> dst.staging-<stamp>\n// 3. verify staging against src: same file list, same size + sha256, and\n// every plaintext .json/.jsonl still parses\n// 4. rename staging -> dst (same volume, atomic)\n// 5. only then delete src and the backup\n// The caller keeps using `src` unless the result is `relocated`.\n\nimport fs from 'node:fs';\nimport path from 'node:path';\nimport { createHash } from 'node:crypto';\n\n/** Directories the bootstrap always creates: their presence alone is no payload. */\nconst SKELETON_DIRS = ['settings', 'logs', 'tmp', 'exports'];\n/** Scratch space (decrypt windows, downloads): never copied, never verified. */\nconst SCRATCH_DIR = 'tmp';\nconst MAGIC_V1 = Buffer.from('KHBV1', 'ascii');\nconst MAGIC_V2 = Buffer.from('KHBV2', 'ascii');\n\nexport interface RelocateOptions {\n srcDir: string;\n dstDir: string;\n /** Injectable clock, so tests get deterministic backup/staging names. */\n now?: number;\n}\n\nexport type RelocateResult =\n | {\n action: 'relocated';\n /** Package-local tree the data came from (now deleted). */\n srcDir: string;\n dstDir: string;\n backupDir: string;\n files: number;\n bytes: number;\n /** Plaintext JSON/JSONL that does not parse (verified byte-identical). */\n warnings: string[];\n /** Paths that could not be deleted after the move (cosmetic leftovers). */\n leftovers: string[];\n }\n | { action: 'skipped'; reason: 'source-missing' | 'no-payload' | 'destination-exists' }\n | {\n action: 'failed';\n stage: 'backup' | 'copy' | 'verify' | 'commit';\n problems: string[];\n /** Null when the backup itself could not be created. */\n backupDir: string | null;\n };\n\nexport interface VerifyResult {\n /** Byte-for-byte the same file list, sizes and contents. */\n ok: boolean;\n files: number;\n bytes: number;\n problems: string[];\n /**\n * Plaintext JSON/JSONL files that do not parse. They are reported but do not\n * fail the move: an identical copy of a file that was already damaged is\n * still byte-for-byte the user's data, and refusing to relocate would strand\n * the data dir in the package forever.\n */\n warnings: string[];\n}\n\n/**\n * True when a data dir holds real user data. An empty bootstrap skeleton\n * (`settings/ logs/ tmp/ exports/`, all empty) does not count — otherwise a\n * freshly created default dir would shadow a legacy tree with actual data.\n */\nexport function hasPayload(dir: string): boolean {\n if (!fs.existsSync(dir)) return false;\n for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {\n if (!entry.isDirectory()) return true;\n if (!SKELETON_DIRS.includes(entry.name)) return true;\n if (entry.name === SCRATCH_DIR) continue;\n if (fs.readdirSync(path.join(dir, entry.name)).length > 0) return true;\n }\n return false;\n}\n\n/** Every file under `dir` (relative, `/`-separated, sorted), excluding tmp/. */\nfunction walk(dir: string, prefix = '', out: string[] = []): string[] {\n for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {\n const rel = prefix ? `${prefix}/${entry.name}` : entry.name;\n if (entry.isDirectory()) {\n if (!prefix && entry.name === SCRATCH_DIR) continue;\n walk(path.join(dir, entry.name), rel, out);\n } else if (entry.isFile()) {\n out.push(rel);\n }\n }\n return out.sort();\n}\n\nfunction copyTree(src: string, dst: string): void {\n const scratch = path.resolve(path.join(src, SCRATCH_DIR));\n fs.cpSync(src, dst, {\n recursive: true,\n force: true,\n preserveTimestamps: true,\n filter: (from) => path.resolve(from) !== scratch,\n });\n}\n\nfunction removeTree(dir: string): void {\n fs.rmSync(dir, { recursive: true, force: true });\n}\n\nfunction sha256(file: string): string {\n return createHash('sha256').update(fs.readFileSync(file)).digest('hex');\n}\n\n/**\n * Encrypted-container header check (local copy: importing vault here would\n * cycle through paths). Both generations count: KHBV1 is the DPAPI legacy\n * container, KHBV2 the AES-GCM one v2.0 writes.\n */\nfunction isEncryptedFile(file: string): boolean {\n const fd = fs.openSync(file, 'r');\n try {\n const head = Buffer.alloc(MAGIC_V2.length);\n const read = fs.readSync(fd, head, 0, head.length, 0);\n if (read === MAGIC_V1.length && head.subarray(0, MAGIC_V1.length).equals(MAGIC_V1)) return true;\n return read === MAGIC_V2.length && head.equals(MAGIC_V2);\n } finally {\n fs.closeSync(fd);\n }\n}\n\n/**\n * Schema-free readability check: an encrypted container is opaque here (its\n * bytes were already compared by hash), a plaintext .json/.jsonl must still\n * parse — that is what catches a half-written or truncated copy.\n */\nfunction parsesAsJson(file: string): boolean {\n if (isEncryptedFile(file)) return true;\n const text = fs.readFileSync(file, 'utf8');\n try {\n if (file.endsWith('.jsonl')) {\n for (const line of text.split('\\n')) if (line.trim()) JSON.parse(line);\n return true;\n }\n JSON.parse(text);\n return true;\n } catch {\n return false;\n }\n}\n\n/**\n * Compare a copy against its source: identical file list, identical size and\n * content (the hard gate), plus a readability report on the plaintext\n * JSON/JSONL in the copy (warnings only — see VerifyResult).\n */\nexport function verifyTrees(srcDir: string, dstDir: string): VerifyResult {\n const problems: string[] = [];\n const warnings: string[] = [];\n let files = 0;\n let bytes = 0;\n if (!fs.existsSync(dstDir)) {\n return { ok: false, files: 0, bytes: 0, problems: [`copy missing: ${dstDir}`], warnings };\n }\n const expected = walk(srcDir);\n const actual = walk(dstDir);\n const actualSet = new Set(actual);\n const expectedSet = new Set(expected);\n for (const rel of expected) if (!actualSet.has(rel)) problems.push(`missing: ${rel}`);\n for (const rel of actual) if (!expectedSet.has(rel)) problems.push(`unexpected: ${rel}`);\n for (const rel of expected) {\n const from = path.join(srcDir, rel);\n const to = path.join(dstDir, rel);\n if (!fs.existsSync(to)) continue; // already reported above\n const sizeFrom = fs.statSync(from).size;\n const sizeTo = fs.statSync(to).size;\n files += 1;\n bytes += sizeFrom;\n if (sizeFrom !== sizeTo) {\n problems.push(`size differs: ${rel} (${sizeFrom} -> ${sizeTo})`);\n continue;\n }\n if (sha256(from) !== sha256(to)) {\n problems.push(`content differs: ${rel}`);\n continue;\n }\n if (/\\.(json|jsonl)$/i.test(rel) && !parsesAsJson(to)) warnings.push(`unparseable: ${rel}`);\n }\n return { ok: problems.length === 0, files, bytes, problems, warnings };\n}\n\nfunction stamp(now: number): string {\n const d = new Date(now);\n const pad = (n: number): string => String(n).padStart(2, '0');\n return `${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}-${pad(d.getHours())}${pad(d.getMinutes())}${pad(d.getSeconds())}`;\n}\n\nfunction errorText(error: unknown): string {\n return error instanceof Error ? error.message : String(error);\n}\n\n/**\n * Move a package-local data dir to its new home. Never throws: every failure\n * is reported as `failed` with the original tree left in place.\n */\nexport function relocateDataDir(opts: RelocateOptions): RelocateResult {\n const srcDir = path.resolve(opts.srcDir);\n const dstDir = path.resolve(opts.dstDir);\n if (!fs.existsSync(srcDir)) return { action: 'skipped', reason: 'source-missing' };\n if (!hasPayload(srcDir)) return { action: 'skipped', reason: 'no-payload' };\n if (hasPayload(dstDir)) return { action: 'skipped', reason: 'destination-exists' };\n\n const suffix = stamp(opts.now ?? Date.now());\n const backupDir = `${srcDir}.bak-relocate-${suffix}`;\n const stagingDir = `${dstDir}.staging-${suffix}`;\n\n // 1. backup first — the user asked for a copy that outlives a failed move.\n try {\n copyTree(srcDir, backupDir);\n } catch (error) {\n return { action: 'failed', stage: 'backup', problems: [errorText(error)], backupDir: null };\n }\n\n // 2. copy to a staging dir beside the destination (same volume → atomic rename).\n try {\n copyTree(srcDir, stagingDir);\n } catch (error) {\n return { action: 'failed', stage: 'copy', problems: [errorText(error)], backupDir };\n }\n\n // 3. verify before anything is deleted or published.\n const verified = verifyTrees(srcDir, stagingDir);\n if (!verified.ok) {\n try {\n removeTree(stagingDir);\n } catch {\n // A stranded staging dir is harmless: the next run overwrites it.\n }\n return { action: 'failed', stage: 'verify', problems: verified.problems, backupDir };\n }\n\n // 4. publish: the destination may hold an empty skeleton from an earlier\n // bootstrap, which is not payload (checked above), so clear it and rename.\n try {\n if (fs.existsSync(dstDir)) removeTree(dstDir);\n fs.renameSync(stagingDir, dstDir);\n } catch (error) {\n try {\n removeTree(stagingDir);\n } catch {\n // best effort\n }\n return { action: 'failed', stage: 'commit', problems: [errorText(error)], backupDir };\n }\n\n // 5. the new tree is live; the originals go last. A leftover only wastes\n // space — the destination already wins on the next resolve.\n const leftovers: string[] = [];\n try {\n removeTree(srcDir);\n } catch {\n leftovers.push(srcDir);\n }\n try {\n removeTree(backupDir);\n } catch {\n leftovers.push(backupDir);\n }\n\n return {\n action: 'relocated',\n srcDir,\n dstDir,\n backupDir,\n files: verified.files,\n bytes: verified.bytes,\n warnings: verified.warnings,\n leftovers,\n };\n}\n","// Cross-process advisory file lock (H-20, minimal tier).\n//\n// The seed pool and the observation inbox are WHOLE-FILE rewrites of DPAPI\n// ciphertext — the vault cannot append to them — and the CLI runs as a second\n// process against the same files. Two interleaved load→mutate→save cycles\n// therefore let one side silently overwrite the other's write. The full fix\n// (append-only journal + compaction) is v2.0 work; this keeps the writers\n// honest in the meantime.\n//\n// The lock itself is a sibling file created with O_EXCL. A lock older than\n// STALE_MS is treated as abandoned (its holder died mid-write) and reclaimed.\n//\n// HB-03 (2026-10-17 review) fixed the two ways the minimal tier was lying:\n// 1. Waiting out WAIT_MS and then running `fn` ANYWAY made the lock advisory\n// in name only — the very interleaved rewrite it exists to prevent could\n// still happen, with the loser's data silently lost. Acquisition failure\n// is now fail-closed: `fn` never runs without the lock. Callers that must\n// keep going use `tryWithFileLock` and handle the `null`.\n// 2. Staleness was mtime alone, so a LIVE holder whose write ran longer than\n// STALE_MS had its lock deleted underneath it (both writers then hold it).\n// A lock is now reclaimed only when the pid it names is no longer running.\n\nimport fs from 'node:fs';\n\nconst STALE_MS = 15_000;\nconst WAIT_MS = 3_000;\nconst POLL_MS = 25;\n/** A lock whose owner cannot be identified is only reclaimed after this long. */\nconst UNKNOWN_HOLDER_MS = 2 * STALE_MS;\n\n/** Thrown when the lock could not be taken: the guarded write did NOT happen. */\nexport class FileLockTimeoutError extends Error {\n readonly target: string;\n readonly waitedMs: number;\n constructor(target: string, waitedMs: number) {\n super(`file lock timeout after ${waitedMs}ms: ${target}`);\n this.name = 'FileLockTimeoutError';\n this.target = target;\n this.waitedMs = waitedMs;\n }\n}\n\nexport interface FileLockOpts {\n /** How long to wait for the lock; defaults to WAIT_MS (3s). */\n waitMs?: number;\n}\n\nfunction sleepSync(ms: number): void {\n Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);\n}\n\n/** Is the process named in the lock file still running? Every doubt (\"cannot\n * read it\", \"no pid in it\") counts as alive until UNKNOWN_HOLDER_MS has passed:\n * deleting a live holder's lock is the failure this replaced. */\nfunction holderAlive(lock: string, ageMs: number): boolean {\n let raw = '';\n try {\n raw = fs.readFileSync(lock, 'utf8');\n } catch {\n return ageMs < UNKNOWN_HOLDER_MS; // vanished or unreadable mid-write\n }\n const pid = Number.parseInt(raw.trim().split(/\\s+/)[0] ?? '', 10);\n if (!Number.isInteger(pid) || pid <= 0) return ageMs < UNKNOWN_HOLDER_MS;\n if (pid === process.pid) return true; // our own re-entrant look; never steal\n try {\n process.kill(pid, 0);\n return true;\n } catch (e) {\n return (e as NodeJS.ErrnoException).code === 'EPERM'; // alive, not ours\n }\n}\n\nfunction tryAcquire(lock: string): boolean {\n try {\n const fd = fs.openSync(lock, 'wx');\n fs.writeSync(fd, `${process.pid} ${new Date().toISOString()}\\n`);\n fs.closeSync(fd);\n return true;\n } catch (e) {\n if ((e as NodeJS.ErrnoException).code !== 'EEXIST') throw e;\n // Reclaim a lock whose holder is gone.\n try {\n const ageMs = Date.now() - fs.statSync(lock).mtimeMs;\n if (ageMs > STALE_MS && !holderAlive(lock, ageMs)) {\n fs.rmSync(lock, { force: true });\n return tryAcquire(lock);\n }\n } catch {\n return tryAcquire(lock); // vanished between open and stat\n }\n return false;\n }\n}\n\n/** Take the lock, or give up after `waitMs`. Never blocks past the deadline. */\nfunction acquire(lock: string, waitMs: number): boolean {\n const deadline = Date.now() + waitMs;\n for (;;) {\n if (tryAcquire(lock)) return true;\n if (Date.now() >= deadline) return false;\n sleepSync(POLL_MS);\n }\n}\n\nfunction release(lock: string): void {\n try {\n fs.rmSync(lock, { force: true });\n } catch { /* already gone */ }\n}\n\n/**\n * Run `fn` while holding the lock for `target`. Fail-closed (HB-03): if the\n * lock cannot be taken within `waitMs`, this throws FileLockTimeoutError and\n * `fn` is never called — a write without the lock is exactly the corruption the\n * lock exists to prevent. Use `tryWithFileLock` when \"skip this write\" is a\n * valid answer for the caller.\n */\nexport function withFileLock<T>(target: string, fn: () => T, opts: FileLockOpts = {}): T {\n const waitMs = opts.waitMs ?? WAIT_MS;\n const lock = `${target}.lock`;\n if (!acquire(lock, waitMs)) throw new FileLockTimeoutError(target, waitMs);\n try {\n return fn();\n } finally {\n release(lock);\n }\n}\n\n/**\n * `withFileLock`, but a lock that cannot be taken yields `null` instead of\n * throwing, and `fn` still never runs without the lock. Callers use this when\n * dropping the write (and saying so in the audit log) beats failing the beat.\n */\nexport function tryWithFileLock<T>(target: string, fn: () => T, opts: FileLockOpts = {}): T | null {\n const waitMs = opts.waitMs ?? WAIT_MS;\n const lock = `${target}.lock`;\n if (!acquire(lock, waitMs)) return null;\n try {\n return fn();\n } finally {\n release(lock);\n }\n}\n","// Recursive file matching below a data directory (HB-07 / HB-21).\n//\n// The burn list and `profile wipe` both need \"every file anywhere under\n// dataDir whose name matches this pattern\". Migration backups\n// (`${abs}.bak-migrate-${stamp}`) and crash leftovers (`.incoming-`) sit next\n// to whatever they copied, so a target that only reads the top level would\n// leave a full copy of the memory behind.\n\nimport fs from 'node:fs';\nimport path from 'node:path';\n\n/**\n * Relative POSIX-style paths of files below `root` whose NAME matches `match`,\n * sorted. The pattern is used as a plain test: a `g` flag is stripped so a\n * caller-supplied regexp cannot carry lastIndex state between files. `root`\n * must already be resolved/validated by the caller's path guard.\n */\nexport function matchFilesRecursive(root: string, match: RegExp): string[] {\n const re = new RegExp(match.source, match.flags.replace(/g/g, ''));\n const out: string[] = [];\n const walk = (dir: string, prefix: string): void => {\n let entries: fs.Dirent[];\n try {\n entries = fs.readdirSync(dir, { withFileTypes: true });\n } catch {\n return;\n }\n for (const e of entries) {\n const rel = prefix ? `${prefix}/${e.name}` : e.name;\n if (e.isDirectory()) {\n walk(path.join(dir, e.name), rel);\n continue;\n }\n if (!e.isFile()) continue;\n if (re.test(e.name)) out.push(rel);\n }\n };\n walk(root, '');\n return out.sort();\n}\n","// Transparent encryption read/write layer.\n//\n// Two container generations live side by side on disk:\n// KHBV1 legacy — DPAPI (CurrentUser) over the whole payload, produced by\n// assets/vault.ps1. Every read/write costs one PowerShell process,\n// which is why a beat that touches many files paid seconds of spawn\n// time (HB-11).\n// KHBV2 current — AES-256-GCM applied in-process, keyed by a 32-byte random\n// master key. The key itself is stored DPAPI-protected in\n// <dataDir>/vault.key, so the only thing that still needs DPAPI is\n// unwrapping that one file, and that happens once per process (cached).\n//\n// Migration contract (v2.0): an upgraded plugin keeps opening everything an\n// older version wrote — reads accept BOTH containers, while writes always\n// produce KHBV2. Legacy files are rewritten in place by upgradeVaultTree(),\n// which the CLI runs on demand. Going the other way is one-way: a 1.x build\n// cannot read KHBV2, so once `vault upgrade` has run, do not downgrade.\n// Encryption stays fail-closed: a missing or unreadable master key throws, and\n// no path ever falls back to plaintext.\n//\n// Plaintext never touches the disk on the KHBV2 path: the payload goes straight\n// from a Buffer through AES-GCM into the temp container that is renamed into\n// place. (KHBV1 needed a plaintext window under data/tmp for PowerShell.)\n\nimport fs from 'node:fs';\nimport path from 'node:path';\nimport { spawnSync } from 'node:child_process';\nimport { createCipheriv, createDecipheriv, randomBytes, randomUUID } from 'node:crypto';\nimport type { PathGuard } from '../core/path-guard.js';\nimport { workspace } from '../core/paths.js';\nimport { burnFileSync } from '../core/atomic-fs.js';\nimport { FileLockTimeoutError, withFileLock } from '../core/file-lock.js';\nimport { matchFilesRecursive } from '../core/fs-walk.js';\n\n/** Legacy container: 5-byte magic + DPAPI blob. */\nconst MAGIC_V1 = Buffer.from('KHBV1', 'ascii');\n/** Current container: magic + version + nonce + tag + ciphertext. */\nconst MAGIC_V2 = Buffer.from('KHBV2', 'ascii');\nconst V2_VERSION = 1;\nconst V2_NONCE_BYTES = 12;\nconst V2_TAG_BYTES = 16;\nconst V2_HEADER_BYTES = MAGIC_V2.length + 1 + V2_NONCE_BYTES + V2_TAG_BYTES;\n/** Additional authenticated data: pins the header, so a container cannot be relabelled. */\nconst V2_AAD = Buffer.concat([MAGIC_V2, Buffer.from([V2_VERSION])]);\nconst KEY_BYTES = 32;\nconst KEY_FILE_NAME = 'vault.key';\nconst VAULT_TIMEOUT_MS = 30_000;\n\nlet cachedScriptPath: string | null = null;\n\nfunction vaultScriptPath(): string {\n if (cachedScriptPath) return cachedScriptPath;\n const script = path.join(workspace().assetsDir, 'vault.ps1');\n if (!fs.existsSync(script)) throw new Error(`vault script missing: ${script}`);\n cachedScriptPath = script;\n return script;\n}\n\nfunction tmpPlainName(target: string): string {\n return path.join(workspace().tmpDir, `.${path.basename(target)}.${randomUUID().slice(0, 8)}.plain`);\n}\n\nfunction tmpEncName(target: string): string {\n return path.join(workspace().tmpDir, `.${path.basename(target)}.${randomUUID().slice(0, 8)}.enc`);\n}\n\nfunction runVault(guard: PathGuard, args: string[]): void {\n const r = spawnSync(\n 'powershell.exe',\n ['-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', vaultScriptPath(), ...args],\n { timeout: VAULT_TIMEOUT_MS, encoding: 'utf8' },\n );\n if (r.error) throw new Error(`vault spawn failed: ${String(r.error)}`);\n if (r.status !== 0) {\n const detail = (r.stderr || r.stdout || '').trim().split('\\n').slice(-3).join('; ');\n throw new Error(`vault ${args[0]} failed (exit ${r.status}): ${detail}`);\n }\n}\n\n/** How long a scratch file may sit before it is treated as crash debris. */\nconst SCRATCH_STALE_MS = 10 * 60_000;\nlet scratchSwept = false;\n\n/**\n * Burn plaintext scratch files a crash left behind in data/tmp (低优先级 review,\n * 2026-10-17). The writers there are wrapped in `finally`, but a kill leaves the\n * bytes on disk. Only files older than SCRATCH_STALE_MS are touched, so a\n * concurrent process's live window is never yanked out from under it.\n */\nexport function sweepVaultScratch(): number {\n if (scratchSwept) return 0;\n scratchSwept = true;\n let burned = 0;\n try {\n const dir = workspace().tmpDir;\n for (const name of fs.readdirSync(dir)) {\n if (!/\\.(plain|enc)$/.test(name)) continue;\n const p = path.join(dir, name);\n try {\n if (Date.now() - fs.statSync(p).mtimeMs < SCRATCH_STALE_MS) continue;\n } catch {\n continue; // vanished between readdir and stat\n }\n burnFileSync(p);\n burned += 1;\n }\n } catch {\n /* no tmp dir yet — nothing to sweep */\n }\n return burned;\n}\n\nexport type ContainerKind = 'v2' | 'v1' | 'plain';\n\n/** Which container a file carries, decided from its first bytes. */\nexport function containerKind(file: string): ContainerKind {\n if (!fs.existsSync(file)) return 'plain';\n const fd = fs.openSync(file, 'r');\n try {\n const head = Buffer.alloc(MAGIC_V2.length);\n const read = fs.readSync(fd, head, 0, head.length, 0);\n if (read === MAGIC_V1.length && head.subarray(0, MAGIC_V1.length).equals(MAGIC_V1)) return 'v1';\n if (read === MAGIC_V2.length && head.equals(MAGIC_V2)) return 'v2';\n return 'plain';\n } finally {\n fs.closeSync(fd);\n }\n}\n\n/** True for either container generation (plaintext files stay plaintext). */\nexport function isEncrypted(file: string): boolean {\n return containerKind(file) !== 'plain';\n}\n\nfunction encryptBlob(key: Buffer, plain: Buffer): Buffer {\n const nonce = randomBytes(V2_NONCE_BYTES);\n const cipher = createCipheriv('aes-256-gcm', key, nonce);\n cipher.setAAD(V2_AAD);\n const body = Buffer.concat([cipher.update(plain), cipher.final()]);\n return Buffer.concat([MAGIC_V2, Buffer.from([V2_VERSION]), nonce, cipher.getAuthTag(), body]);\n}\n\nfunction decryptBlob(key: Buffer, raw: Buffer): Buffer {\n if (raw.length < V2_HEADER_BYTES) throw new Error('vault: truncated KHBV2 container');\n if (!raw.subarray(0, MAGIC_V2.length).equals(MAGIC_V2)) throw new Error('vault: missing KHBV2 header');\n const version = raw[MAGIC_V2.length]!;\n if (version !== V2_VERSION) throw new Error(`vault: unsupported KHBV2 version ${version}`);\n const nonceAt = MAGIC_V2.length + 1;\n const tagAt = nonceAt + V2_NONCE_BYTES;\n const bodyAt = tagAt + V2_TAG_BYTES;\n const decipher = createDecipheriv('aes-256-gcm', key, raw.subarray(nonceAt, tagAt));\n decipher.setAAD(Buffer.concat([MAGIC_V2, Buffer.from([version])]));\n decipher.setAuthTag(raw.subarray(tagAt, bodyAt));\n // final() throws when the tag does not verify (tampered or wrong key).\n return Buffer.concat([decipher.update(raw.subarray(bodyAt)), decipher.final()]);\n}\n\nfunction legacyUnprotect(guard: PathGuard, file: string, out: string): void {\n runVault(guard, ['unprotect', '-InFile', file, '-OutFile', out]);\n}\n\nfunction legacyProtect(guard: PathGuard, inFile: string, out: string): void {\n runVault(guard, ['protect', '-InFile', inFile, '-OutFile', out]);\n}\n\n/** Path of the DPAPI-wrapped master key that every KHBV2 container is keyed by. */\nexport function vaultKeyPath(): string {\n return path.join(workspace().dataDir, KEY_FILE_NAME);\n}\n\nlet cachedMasterKey: Buffer | null = null;\n\n/** Drop the cached key and the once-per-process sweep flag. Tests use it after\n * pointing the workspace elsewhere. */\nexport function resetVaultKeyCacheForTest(): void {\n cachedMasterKey = null;\n scratchSwept = false;\n}\n\nfunction decodeKeyFile(guard: PathGuard, file: string): Buffer {\n const kind = containerKind(file);\n if (kind === 'v2') {\n throw new Error(\n `vault: ${file} is an AES container, but the master key cannot be stored AES-encrypted — restore it from a backup or re-import the data`,\n );\n }\n if (kind !== 'v1') throw new Error(`vault: ${file} is not a DPAPI container (not a usable master key)`);\n const tmp = tmpPlainName(file);\n try {\n legacyUnprotect(guard, file, tmp);\n const key = fs.readFileSync(tmp);\n if (key.length !== KEY_BYTES) {\n throw new Error(`vault: master key has ${key.length} bytes, expected ${KEY_BYTES}`);\n }\n return key;\n } finally {\n burnFileSync(tmp);\n }\n}\n\n/**\n * The process-wide master key. Created on first use under a file lock, then\n * cached for the rest of the process's life.\n *\n * A key file that EXISTS but cannot be read is a hard error, never a reason to\n * generate a fresh key: every container already on disk is encrypted with the\n * old one, so replacing it would silently turn the user's memory into garbage.\n */\nexport function masterKey(guard: PathGuard): Buffer {\n if (cachedMasterKey) return cachedMasterKey;\n sweepVaultScratch();\n const file = guard.assert(vaultKeyPath());\n if (fs.existsSync(file)) {\n cachedMasterKey = decodeKeyFile(guard, file);\n return cachedMasterKey;\n }\n fs.mkdirSync(path.dirname(file), { recursive: true });\n // HB-03: acquisition is fail-closed, so a lock held by another process that is\n // creating the key right now surfaces as FileLockTimeoutError instead of a\n // second key. Re-check before bubbling it up: `rename` publishes atomically,\n // so the file is either absent or complete, and a second key would scramble\n // every container the first one already wrote.\n let created: Buffer | null;\n try {\n created = withFileLock(file, (): Buffer | null => {\n if (fs.existsSync(file)) return null; // another process created it first\n const plain = tmpPlainName(file);\n const enc = tmpEncName(file);\n try {\n const key = randomBytes(KEY_BYTES);\n fs.writeFileSync(plain, key);\n legacyProtect(guard, plain, enc);\n // Open the container we are about to publish: a master key nobody can\n // unwrap is dead data, so verify the round trip before it goes live.\n const check = tmpPlainName(file);\n try {\n legacyUnprotect(guard, enc, check);\n if (!fs.readFileSync(check).equals(key)) throw new Error('vault: master key round-trip mismatch');\n } finally {\n burnFileSync(check);\n }\n fs.renameSync(enc, file);\n return key;\n } finally {\n burnFileSync(plain);\n fs.rmSync(enc, { force: true });\n }\n });\n } catch (e) {\n if (e instanceof FileLockTimeoutError && fs.existsSync(file)) {\n cachedMasterKey = decodeKeyFile(guard, file);\n return cachedMasterKey;\n }\n throw e;\n }\n cachedMasterKey = created ?? decodeKeyFile(guard, file);\n return cachedMasterKey;\n}\n\n/** Ciphertext of any container generation -> plaintext bytes. */\nfunction readContainer(guard: PathGuard, file: string): Buffer {\n const kind = containerKind(file);\n if (kind === 'v2') return decryptBlob(masterKey(guard), fs.readFileSync(file));\n if (kind === 'v1') {\n const tmp = tmpPlainName(file);\n try {\n legacyUnprotect(guard, file, tmp);\n return fs.readFileSync(tmp);\n } finally {\n burnFileSync(tmp);\n }\n }\n return fs.readFileSync(file);\n}\n\n/** Plaintext bytes -> KHBV2 container, renamed into place (atomic replace). */\nfunction writeContainer(guard: PathGuard, file: string, data: Buffer): void {\n const f = guard.assert(file);\n fs.mkdirSync(path.dirname(f), { recursive: true });\n const enc = tmpEncName(f);\n try {\n fs.writeFileSync(enc, encryptBlob(masterKey(guard), data));\n fs.renameSync(enc, f);\n } finally {\n fs.rmSync(enc, { force: true });\n }\n}\n\n/**\n * Read JSON: encrypted files are decrypted in memory (or through a shredded\n * data/tmp window for legacy containers); plaintext stays compatible.\n */\nexport function loadJson<T>(guard: PathGuard, file: string): T | null {\n const f = guard.assert(file);\n if (!fs.existsSync(f)) return null;\n if (containerKind(f) === 'plain') {\n try {\n return JSON.parse(fs.readFileSync(f, 'utf8')) as T;\n } catch {\n return null;\n }\n }\n // Encrypted file: a failure here is a damaged container or a foreign master\n // key. Report it instead of \"no file\" — callers treat null as an empty store\n // and would overwrite the user's data with a fresh one.\n return JSON.parse(readContainer(guard, f).toString('utf8')) as T;\n}\n\n/** Write JSON as a KHBV2 container. */\nexport function saveJson(guard: PathGuard, file: string, value: unknown): void {\n writeContainer(guard, file, Buffer.from(JSON.stringify(value, null, 2), 'utf8'));\n}\n\n/** Read a plaintext UTF-8 text file (ledger etc.), with fallback. */\nexport function readText(guard: PathGuard, file: string, fallback = ''): string {\n const f = guard.assert(file);\n try {\n return fs.readFileSync(f, 'utf8');\n } catch {\n return fallback;\n }\n}\n\n/** Write a plaintext UTF-8 text file atomically (ledger etc.). */\nexport function writeText(guard: PathGuard, file: string, content: string): void {\n const f = guard.assert(file);\n fs.mkdirSync(path.dirname(f), { recursive: true });\n const tmp = tmpPlainName(f);\n try {\n fs.writeFileSync(tmp, content, 'utf8');\n fs.renameSync(tmp, f);\n } finally {\n burnFileSync(tmp);\n }\n}\n\n/** Read an encrypted UTF-8 text file (e.g. seeds.jsonl). Missing -> null. */\nexport function loadEncryptedText(guard: PathGuard, file: string): string | null {\n const f = guard.assert(file);\n if (!fs.existsSync(f)) return null;\n return readContainer(guard, f).toString('utf8');\n}\n\n/** Write an encrypted UTF-8 text file as a KHBV2 container. */\nexport function saveEncryptedText(guard: PathGuard, file: string, content: string): void {\n writeContainer(guard, file, Buffer.from(content, 'utf8'));\n}\n\n/** Encrypt any binary file (e.g. screenshot): inFile -> encrypted outFile. */\nexport function encryptFile(guard: PathGuard, inFile: string, outFile: string): void {\n const inPath = guard.assert(inFile);\n writeContainer(guard, outFile, fs.readFileSync(inPath));\n}\n\n/**\n * Decrypt an encrypted file to a plaintext output (caller must place the\n * output under data/tmp and delete it after use; \"use then burn\" discipline).\n */\nexport function decryptFile(guard: PathGuard, inFile: string, outFile: string): void {\n const inPath = guard.assert(inFile);\n const outPath = guard.assert(outFile);\n fs.mkdirSync(path.dirname(outPath), { recursive: true });\n const data = readContainer(guard, inPath);\n const tmp = tmpPlainName(outPath);\n try {\n fs.writeFileSync(tmp, data);\n fs.renameSync(tmp, outPath);\n } finally {\n burnFileSync(tmp);\n }\n}\n\nexport interface VaultUpgradeResult {\n /** Files whose header was inspected. */\n scanned: number;\n /** Legacy KHBV1 containers rewritten as KHBV2. */\n upgraded: number;\n /** Files that could not be migrated (the original file is left untouched). */\n failed: { file: string; reason: string }[];\n}\n\n/** Rewrite one legacy container as KHBV2. Returns false when there is nothing to do. */\nexport function upgradeVaultFile(guard: PathGuard, file: string): boolean {\n const f = guard.assert(file);\n if (containerKind(f) !== 'v1') return false;\n const data = readContainer(guard, f); // DPAPI unwrap (throws if it is not ours)\n writeContainer(guard, f, data); // AES-GCM wrap; the rename replaces the original\n return true;\n}\n\n/**\n * Walk every file under the data dir and rewrite legacy containers in place, so\n * an upgraded plugin stops paying a PowerShell process per legacy file. The\n * tmp scratch dir and the master key itself are left alone.\n */\nexport function upgradeVaultTree(guard: PathGuard, dataDir: string): VaultUpgradeResult {\n const result: VaultUpgradeResult = { scanned: 0, upgraded: 0, failed: [] };\n for (const rel of matchFilesRecursive(dataDir, /./)) {\n if (rel === KEY_FILE_NAME || rel.startsWith('tmp/') || rel.startsWith('tmp\\\\')) continue;\n const abs = path.join(dataDir, rel.split('/').join(path.sep));\n try {\n result.scanned += 1;\n if (upgradeVaultFile(guard, abs)) result.upgraded += 1;\n } catch (e) {\n result.failed.push({ file: rel, reason: (e as Error).message });\n }\n }\n return result;\n}\n\n/**\n * Rewrite ONE legacy container, for callers that have to stay inside a bounded\n * window: the beat's maintenance phase calls this once per beat, so a back\n * catalogue drains gradually instead of spending a whole maintenance window (or\n * a startup) on DPAPI round trips. A file that cannot be migrated is left\n * exactly as it is and retried next beat; `vault upgrade` remains the \"do them\n * all now\" path.\n */\nexport function upgradeOneLegacyVaultFile(guard: PathGuard, dataDir: string): string | null {\n for (const rel of matchFilesRecursive(dataDir, /./)) {\n if (rel === KEY_FILE_NAME || rel.startsWith('tmp/') || rel.startsWith('tmp\\\\')) continue;\n const abs = path.join(dataDir, rel.split('/').join(path.sep));\n try {\n if (upgradeVaultFile(guard, abs)) return rel;\n } catch {\n /* unreadable container: keep the original and try again next beat */\n }\n }\n return null;\n}\n"],"mappings":"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","names":["fs","path","path","fs","fs","fs","path","walk","fs","path","MAGIC_V1","MAGIC_V2","path","fs","fs","path"]}
@@ -4,10 +4,10 @@ import {
4
4
  journalFilePath,
5
5
  replayJournal,
6
6
  snapshotFilePath
7
- } from "./chunk-CUOSICYJ.js";
7
+ } from "./chunk-544QOJN5.js";
8
8
  import {
9
9
  saveJson
10
- } from "./chunk-K5Y6JP2B.js";
10
+ } from "./chunk-AKIJ4LS2.js";
11
11
 
12
12
  // src/profile/snapshot.ts
13
13
  import fs from "fs";
@@ -84,4 +84,4 @@ export {
84
84
  snapshotProfile,
85
85
  snapshotIfDue
86
86
  };
87
- //# sourceMappingURL=chunk-KB5SMG3F.js.map
87
+ //# sourceMappingURL=chunk-DC5HMCJC.js.map
@@ -1,10 +1,8 @@
1
- import {
2
- withFileLock
3
- } from "./chunk-IV2ZWQA3.js";
4
1
  import {
5
2
  loadEncryptedText,
6
- saveEncryptedText
7
- } from "./chunk-K5Y6JP2B.js";
3
+ saveEncryptedText,
4
+ tryWithFileLock
5
+ } from "./chunk-AKIJ4LS2.js";
8
6
 
9
7
  // src/profile/inbox.ts
10
8
  import fs from "fs";
@@ -19,7 +17,7 @@ function truncateNote(note) {
19
17
  return oneLine.length > MAX_NOTE_CHARS ? oneLine.slice(0, MAX_NOTE_CHARS) + "\u2026" : oneLine;
20
18
  }
21
19
  function inboxAppend(guard, file, item) {
22
- return withFileLock(file, () => inboxAppendLocked(guard, file, item));
20
+ return tryWithFileLock(file, () => inboxAppendLocked(guard, file, item));
23
21
  }
24
22
  function inboxAppendLocked(guard, file, item) {
25
23
  const full = {
@@ -64,6 +62,37 @@ function inboxDrain(guard, file) {
64
62
  function inboxClear(guard, file) {
65
63
  saveEncryptedText(guard, file, "");
66
64
  }
65
+ function inboxRemoveIds(guard, file, ids, keys) {
66
+ const result = tryWithFileLock(file, () => {
67
+ const raw = loadEncryptedText(guard, file);
68
+ if (!raw) return { removed: 0, kept: 0 };
69
+ let removed = 0;
70
+ let kept = 0;
71
+ const out = [];
72
+ for (const line of raw.split("\n")) {
73
+ const trimmed = line.trim();
74
+ if (!trimmed) continue;
75
+ let item;
76
+ try {
77
+ item = JSON.parse(trimmed);
78
+ } catch {
79
+ removed += 1;
80
+ continue;
81
+ }
82
+ const byId = typeof item.id === "string" && ids.has(item.id);
83
+ const byKey = keys !== void 0 && keys.has(`${item.kind}#${item.ref}`);
84
+ if (byId || byKey) {
85
+ removed += 1;
86
+ continue;
87
+ }
88
+ out.push(JSON.stringify(item));
89
+ kept += 1;
90
+ }
91
+ saveEncryptedText(guard, file, out.length > 0 ? out.join("\n") + "\n" : "");
92
+ return { removed, kept };
93
+ });
94
+ return result ?? { removed: 0, kept: 0, conflicted: true };
95
+ }
67
96
  function dedupeItems(items) {
68
97
  const seen = /* @__PURE__ */ new Set();
69
98
  const out = [];
@@ -106,8 +135,9 @@ export {
106
135
  inboxCount,
107
136
  inboxDrain,
108
137
  inboxClear,
138
+ inboxRemoveIds,
109
139
  dedupeItems,
110
140
  inboxHealthCheck,
111
141
  inboxFileExists
112
142
  };
113
- //# sourceMappingURL=chunk-3MDBU6WY.js.map
143
+ //# sourceMappingURL=chunk-DZ7YCFGX.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/profile/inbox.ts"],"sourcesContent":["// Observation inbox (design doc §3.2): append-only queue of pending\n// observations; drained by the consolidation run. DPAPI-encrypted. Items hold\n// pointers + at most one short sentence - never raw conversation/screen text.\n\nimport fs from 'node:fs';\nimport path from 'node:path';\nimport { randomUUID } from 'node:crypto';\nimport type { PathGuard } from '../core/path-guard.js';\nimport { loadEncryptedText, saveEncryptedText } from '../vault/vault.js';\nimport { tryWithFileLock } from '../core/file-lock.js';\nimport type { InboxItem } from './types.js';\n\nconst MAX_NOTE_CHARS = 120;\n\nexport function inboxFilePath(dataDir: string): string {\n return path.join(dataDir, 'profile_inbox.jsonl');\n}\n\nfunction truncateNote(note: string): string {\n const oneLine = note.replace(/\\r?\\n/g, ' ').trim();\n return oneLine.length > MAX_NOTE_CHARS ? oneLine.slice(0, MAX_NOTE_CHARS) + '…' : oneLine;\n}\n\nexport function inboxAppend(\n guard: PathGuard,\n file: string,\n item: Omit<InboxItem, 'id'> & { id?: string },\n): InboxItem | null {\n // H-20 (minimal tier): the append is a whole-file encrypted rewrite, so it\n // needs the same cross-process lock the pool uses — the CLI can be draining\n // or writing the same file while a beat appends.\n //\n // HB-03: fail-closed — when another process holds the lock, the observation is\n // DROPPED and the caller audits it. Writing without the lock would rewrite the\n // whole file from a stale read and silently delete their drain.\n return tryWithFileLock(file, () => inboxAppendLocked(guard, file, item));\n}\n\nfunction inboxAppendLocked(\n guard: PathGuard,\n file: string,\n item: Omit<InboxItem, 'id'> & { id?: string },\n): InboxItem {\n const full: InboxItem = {\n id: item.id ?? randomUUID().slice(0, 8),\n kind: item.kind,\n at: item.at,\n ref: item.ref,\n note: truncateNote(item.note),\n };\n const prev = loadEncryptedText(guard, file) ?? '';\n // H-16 (minimal tier): this file is a whole-file encrypted rewrite, so a\n // repeated observation used to cost a full decrypt + encrypt + write for a\n // duplicate row. The §3.7 dedupe key is kind+ref — if that row is already\n // queued, hand it back instead of appending a second copy. (A stalled cursor\n // used to grow the queue here and rely on the consumer's dedupeItems.)\n for (const line of prev.split('\\n')) {\n const trimmed = line.trim();\n if (!trimmed) continue;\n try {\n const existing = JSON.parse(trimmed) as InboxItem;\n if (existing.kind === full.kind && existing.ref === full.ref) return existing;\n } catch {\n // corrupt line: ignore (the queue is re-derivable from its sources)\n }\n }\n saveEncryptedText(guard, file, prev + JSON.stringify(full) + '\\n');\n return full;\n}\n\nexport function inboxCount(guard: PathGuard, file: string): number {\n const raw = loadEncryptedText(guard, file);\n if (!raw) return 0;\n return raw.split('\\n').filter((l) => l.trim()).length;\n}\n\n/**\n * Drain all items. `commit` controls crash semantics: on consolidation failure\n * the inbox must SURVIVE (§3.7), so the caller drains with commit=false first,\n * and only rewrites an empty inbox after the run succeeded.\n */\nexport function inboxDrain(guard: PathGuard, file: string): InboxItem[] {\n const raw = loadEncryptedText(guard, file);\n if (!raw) return [];\n const items: InboxItem[] = [];\n for (const line of raw.split('\\n')) {\n const trimmed = line.trim();\n if (!trimmed) continue;\n try {\n items.push(JSON.parse(trimmed) as InboxItem);\n } catch {\n // corrupt line: drop (queue is re-derivable from sources)\n }\n }\n return items;\n}\n\nexport function inboxClear(guard: PathGuard, file: string): void {\n saveEncryptedText(guard, file, '');\n}\n\n/**\n * Drop only the items this run actually drained (HB-02). Clearing the whole\n * file after a successful run also wiped observations that arrived while the\n * model was thinking — the beat keeps appending during those minutes, and the\n * inbox is the only record of a pending observation. Matching is by id, with\n * the kind+ref dedupe key as a fallback for rows written before ids existed.\n * Unparseable rows go away exactly as inboxDrain drops them.\n */\nexport function inboxRemoveIds(\n guard: PathGuard,\n file: string,\n ids: ReadonlySet<string>,\n keys?: ReadonlySet<string>,\n): { removed: number; kept: number; conflicted?: true } {\n // HB-03: fail-closed. `conflicted` means another process held the lock, so\n // nothing was removed — the caller must say so in the audit log instead of\n // reporting a clean drain (the observations are still queued).\n const result = tryWithFileLock(file, () => {\n const raw = loadEncryptedText(guard, file);\n if (!raw) return { removed: 0, kept: 0 };\n let removed = 0;\n let kept = 0;\n const out: string[] = [];\n for (const line of raw.split('\\n')) {\n const trimmed = line.trim();\n if (!trimmed) continue;\n let item: InboxItem;\n try {\n item = JSON.parse(trimmed) as InboxItem;\n } catch {\n removed += 1;\n continue;\n }\n const byId = typeof item.id === 'string' && ids.has(item.id);\n const byKey = keys !== undefined && keys.has(`${item.kind}#${item.ref}`);\n if (byId || byKey) {\n removed += 1;\n continue;\n }\n out.push(JSON.stringify(item));\n kept += 1;\n }\n saveEncryptedText(guard, file, out.length > 0 ? out.join('\\n') + '\\n' : '');\n return { removed, kept };\n });\n return result ?? { removed: 0, kept: 0, conflicted: true };\n}\n\n/** Dedupe key (§3.7): same source kind + same ref must not flood the queue. */\nexport function dedupeItems(items: InboxItem[]): InboxItem[] {\n const seen = new Set<string>();\n const out: InboxItem[] = [];\n for (const it of items) {\n const key = `${it.kind}#${it.ref}`;\n if (seen.has(key)) continue;\n seen.add(key);\n out.push(it);\n }\n return out;\n}\n\nexport function inboxHealthCheck(guard: PathGuard, file: string): { total: number; corrupt: number; duplicates: number } {\n const raw = loadEncryptedText(guard, file) ?? '';\n let total = 0;\n let corrupt = 0;\n const keys = new Set<string>();\n let duplicates = 0;\n for (const line of raw.split('\\n')) {\n const trimmed = line.trim();\n if (!trimmed) continue;\n try {\n const it = JSON.parse(trimmed) as InboxItem;\n total += 1;\n const key = `${it.kind}#${it.ref}`;\n if (keys.has(key)) duplicates += 1;\n keys.add(key);\n } catch {\n corrupt += 1;\n }\n }\n return { total, corrupt, duplicates };\n}\n\nexport function inboxFileExists(guard: PathGuard, file: string): boolean {\n return fs.existsSync(guard.assert(file));\n}\n"],"mappings":";;;;;;;AAIA,OAAO,QAAQ;AACf,OAAO,UAAU;AACjB,SAAS,kBAAkB;AAM3B,IAAM,iBAAiB;AAEhB,SAAS,cAAc,SAAyB;AACrD,SAAO,KAAK,KAAK,SAAS,qBAAqB;AACjD;AAEA,SAAS,aAAa,MAAsB;AAC1C,QAAM,UAAU,KAAK,QAAQ,UAAU,GAAG,EAAE,KAAK;AACjD,SAAO,QAAQ,SAAS,iBAAiB,QAAQ,MAAM,GAAG,cAAc,IAAI,WAAM;AACpF;AAEO,SAAS,YACd,OACA,MACA,MACkB;AAQlB,SAAO,gBAAgB,MAAM,MAAM,kBAAkB,OAAO,MAAM,IAAI,CAAC;AACzE;AAEA,SAAS,kBACP,OACA,MACA,MACW;AACX,QAAM,OAAkB;AAAA,IACtB,IAAI,KAAK,MAAM,WAAW,EAAE,MAAM,GAAG,CAAC;AAAA,IACtC,MAAM,KAAK;AAAA,IACX,IAAI,KAAK;AAAA,IACT,KAAK,KAAK;AAAA,IACV,MAAM,aAAa,KAAK,IAAI;AAAA,EAC9B;AACA,QAAM,OAAO,kBAAkB,OAAO,IAAI,KAAK;AAM/C,aAAW,QAAQ,KAAK,MAAM,IAAI,GAAG;AACnC,UAAM,UAAU,KAAK,KAAK;AAC1B,QAAI,CAAC,QAAS;AACd,QAAI;AACF,YAAM,WAAW,KAAK,MAAM,OAAO;AACnC,UAAI,SAAS,SAAS,KAAK,QAAQ,SAAS,QAAQ,KAAK,IAAK,QAAO;AAAA,IACvE,QAAQ;AAAA,IAER;AAAA,EACF;AACA,oBAAkB,OAAO,MAAM,OAAO,KAAK,UAAU,IAAI,IAAI,IAAI;AACjE,SAAO;AACT;AAEO,SAAS,WAAW,OAAkB,MAAsB;AACjE,QAAM,MAAM,kBAAkB,OAAO,IAAI;AACzC,MAAI,CAAC,IAAK,QAAO;AACjB,SAAO,IAAI,MAAM,IAAI,EAAE,OAAO,CAAC,MAAM,EAAE,KAAK,CAAC,EAAE;AACjD;AAOO,SAAS,WAAW,OAAkB,MAA2B;AACtE,QAAM,MAAM,kBAAkB,OAAO,IAAI;AACzC,MAAI,CAAC,IAAK,QAAO,CAAC;AAClB,QAAM,QAAqB,CAAC;AAC5B,aAAW,QAAQ,IAAI,MAAM,IAAI,GAAG;AAClC,UAAM,UAAU,KAAK,KAAK;AAC1B,QAAI,CAAC,QAAS;AACd,QAAI;AACF,YAAM,KAAK,KAAK,MAAM,OAAO,CAAc;AAAA,IAC7C,QAAQ;AAAA,IAER;AAAA,EACF;AACA,SAAO;AACT;AAEO,SAAS,WAAW,OAAkB,MAAoB;AAC/D,oBAAkB,OAAO,MAAM,EAAE;AACnC;AAUO,SAAS,eACd,OACA,MACA,KACA,MACsD;AAItD,QAAM,SAAS,gBAAgB,MAAM,MAAM;AACzC,UAAM,MAAM,kBAAkB,OAAO,IAAI;AACzC,QAAI,CAAC,IAAK,QAAO,EAAE,SAAS,GAAG,MAAM,EAAE;AACvC,QAAI,UAAU;AACd,QAAI,OAAO;AACX,UAAM,MAAgB,CAAC;AACvB,eAAW,QAAQ,IAAI,MAAM,IAAI,GAAG;AAClC,YAAM,UAAU,KAAK,KAAK;AAC1B,UAAI,CAAC,QAAS;AACd,UAAI;AACJ,UAAI;AACF,eAAO,KAAK,MAAM,OAAO;AAAA,MAC3B,QAAQ;AACN,mBAAW;AACX;AAAA,MACF;AACA,YAAM,OAAO,OAAO,KAAK,OAAO,YAAY,IAAI,IAAI,KAAK,EAAE;AAC3D,YAAM,QAAQ,SAAS,UAAa,KAAK,IAAI,GAAG,KAAK,IAAI,IAAI,KAAK,GAAG,EAAE;AACvE,UAAI,QAAQ,OAAO;AACjB,mBAAW;AACX;AAAA,MACF;AACA,UAAI,KAAK,KAAK,UAAU,IAAI,CAAC;AAC7B,cAAQ;AAAA,IACV;AACA,sBAAkB,OAAO,MAAM,IAAI,SAAS,IAAI,IAAI,KAAK,IAAI,IAAI,OAAO,EAAE;AAC1E,WAAO,EAAE,SAAS,KAAK;AAAA,EACzB,CAAC;AACD,SAAO,UAAU,EAAE,SAAS,GAAG,MAAM,GAAG,YAAY,KAAK;AAC3D;AAGO,SAAS,YAAY,OAAiC;AAC3D,QAAM,OAAO,oBAAI,IAAY;AAC7B,QAAM,MAAmB,CAAC;AAC1B,aAAW,MAAM,OAAO;AACtB,UAAM,MAAM,GAAG,GAAG,IAAI,IAAI,GAAG,GAAG;AAChC,QAAI,KAAK,IAAI,GAAG,EAAG;AACnB,SAAK,IAAI,GAAG;AACZ,QAAI,KAAK,EAAE;AAAA,EACb;AACA,SAAO;AACT;AAEO,SAAS,iBAAiB,OAAkB,MAAsE;AACvH,QAAM,MAAM,kBAAkB,OAAO,IAAI,KAAK;AAC9C,MAAI,QAAQ;AACZ,MAAI,UAAU;AACd,QAAM,OAAO,oBAAI,IAAY;AAC7B,MAAI,aAAa;AACjB,aAAW,QAAQ,IAAI,MAAM,IAAI,GAAG;AAClC,UAAM,UAAU,KAAK,KAAK;AAC1B,QAAI,CAAC,QAAS;AACd,QAAI;AACF,YAAM,KAAK,KAAK,MAAM,OAAO;AAC7B,eAAS;AACT,YAAM,MAAM,GAAG,GAAG,IAAI,IAAI,GAAG,GAAG;AAChC,UAAI,KAAK,IAAI,GAAG,EAAG,eAAc;AACjC,WAAK,IAAI,GAAG;AAAA,IACd,QAAQ;AACN,iBAAW;AAAA,IACb;AAAA,EACF;AACA,SAAO,EAAE,OAAO,SAAS,WAAW;AACtC;AAEO,SAAS,gBAAgB,OAAkB,MAAuB;AACvE,SAAO,GAAG,WAAW,MAAM,OAAO,IAAI,CAAC;AACzC;","names":[]}
@@ -10,10 +10,21 @@ function tmpSibling(target, tag = "w") {
10
10
  }
11
11
  function atomicWriteFileSync(target, data) {
12
12
  const tmp = tmpSibling(target);
13
+ let fd = null;
13
14
  try {
14
- fs.writeFileSync(tmp, data);
15
+ fd = fs.openSync(tmp, "w");
16
+ fs.writeFileSync(fd, data);
17
+ fs.fsyncSync(fd);
18
+ fs.closeSync(fd);
19
+ fd = null;
15
20
  fs.renameSync(tmp, target);
16
21
  } finally {
22
+ if (fd !== null) {
23
+ try {
24
+ fs.closeSync(fd);
25
+ } catch {
26
+ }
27
+ }
17
28
  fs.rmSync(tmp, { force: true });
18
29
  }
19
30
  }
@@ -44,4 +55,4 @@ export {
44
55
  shredFileSync,
45
56
  burnFileSync
46
57
  };
47
- //# sourceMappingURL=chunk-WRUTATW4.js.map
58
+ //# sourceMappingURL=chunk-IBBAISSW.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/core/atomic-fs.ts"],"sourcesContent":["// Crash-safe file replacement: write to a random-suffix temp file in the SAME\n// directory as the target, then rename over it. Same-directory rename stays\n// on one volume (atomic) and Node's rename replaces existing files on Windows.\n\nimport fs from 'node:fs';\nimport path from 'node:path';\nimport { randomUUID, randomFillSync } from 'node:crypto';\n\nexport function tmpSibling(target: string, tag = 'w'): string {\n return path.join(\n path.dirname(target),\n `.${path.basename(target)}.${tag}-${randomUUID().slice(0, 8)}.tmp`,\n );\n}\n\nexport function atomicWriteFileSync(target: string, data: string | Uint8Array): void {\n const tmp = tmpSibling(target);\n let fd: number | null = null;\n try {\n fd = fs.openSync(tmp, 'w');\n fs.writeFileSync(fd, data);\n // 低优先级 review (2026-10-17): rename is atomic against a crash of THIS\n // process, but without a flush the renamed file can still point at blocks\n // the OS never wrote — a power loss then leaves an empty or partial file\n // where a complete one is promised. Flush before publishing.\n fs.fsyncSync(fd);\n fs.closeSync(fd);\n fd = null;\n fs.renameSync(tmp, target);\n } finally {\n if (fd !== null) {\n try { fs.closeSync(fd); } catch { /* the write path already failed */ }\n }\n fs.rmSync(tmp, { force: true });\n }\n}\n\nexport function atomicWriteJsonSync(target: string, value: unknown): void {\n atomicWriteFileSync(target, JSON.stringify(value, null, 2));\n}\n\n/** Overwrite a file's bytes with random data before unlinking. */\nexport function shredFileSync(target: string, passes = 3): void {\n const stat = fs.statSync(target);\n if (!stat.isFile()) throw new Error(`shred: not a file: ${target}`);\n const buf = Buffer.alloc(Math.max(stat.size, 1));\n for (let i = 0; i < passes; i++) {\n randomFillSync(buf);\n fs.writeFileSync(target, buf);\n }\n fs.rmSync(target, { force: true });\n}\n\n/**\n * Best-effort burn for a plaintext window: shred it when it exists, ignore it\n * when it is already gone (H-59 — `rm` alone leaves the bytes on disk).\n */\nexport function burnFileSync(target: string): void {\n try {\n shredFileSync(target);\n } catch {\n fs.rmSync(target, { force: true });\n }\n}\n"],"mappings":";AAIA,OAAO,QAAQ;AACf,OAAO,UAAU;AACjB,SAAS,YAAY,sBAAsB;AAEpC,SAAS,WAAW,QAAgB,MAAM,KAAa;AAC5D,SAAO,KAAK;AAAA,IACV,KAAK,QAAQ,MAAM;AAAA,IACnB,IAAI,KAAK,SAAS,MAAM,CAAC,IAAI,GAAG,IAAI,WAAW,EAAE,MAAM,GAAG,CAAC,CAAC;AAAA,EAC9D;AACF;AAEO,SAAS,oBAAoB,QAAgB,MAAiC;AACnF,QAAM,MAAM,WAAW,MAAM;AAC7B,MAAI,KAAoB;AACxB,MAAI;AACF,SAAK,GAAG,SAAS,KAAK,GAAG;AACzB,OAAG,cAAc,IAAI,IAAI;AAKzB,OAAG,UAAU,EAAE;AACf,OAAG,UAAU,EAAE;AACf,SAAK;AACL,OAAG,WAAW,KAAK,MAAM;AAAA,EAC3B,UAAE;AACA,QAAI,OAAO,MAAM;AACf,UAAI;AAAE,WAAG,UAAU,EAAE;AAAA,MAAG,QAAQ;AAAA,MAAsC;AAAA,IACxE;AACA,OAAG,OAAO,KAAK,EAAE,OAAO,KAAK,CAAC;AAAA,EAChC;AACF;AAEO,SAAS,oBAAoB,QAAgB,OAAsB;AACxE,sBAAoB,QAAQ,KAAK,UAAU,OAAO,MAAM,CAAC,CAAC;AAC5D;AAGO,SAAS,cAAc,QAAgB,SAAS,GAAS;AAC9D,QAAM,OAAO,GAAG,SAAS,MAAM;AAC/B,MAAI,CAAC,KAAK,OAAO,EAAG,OAAM,IAAI,MAAM,sBAAsB,MAAM,EAAE;AAClE,QAAM,MAAM,OAAO,MAAM,KAAK,IAAI,KAAK,MAAM,CAAC,CAAC;AAC/C,WAAS,IAAI,GAAG,IAAI,QAAQ,KAAK;AAC/B,mBAAe,GAAG;AAClB,OAAG,cAAc,QAAQ,GAAG;AAAA,EAC9B;AACA,KAAG,OAAO,QAAQ,EAAE,OAAO,KAAK,CAAC;AACnC;AAMO,SAAS,aAAa,QAAsB;AACjD,MAAI;AACF,kBAAc,MAAM;AAAA,EACtB,QAAQ;AACN,OAAG,OAAO,QAAQ,EAAE,OAAO,KAAK,CAAC;AAAA,EACnC;AACF;","names":[]}
@@ -1,6 +1,10 @@
1
+ import {
2
+ readText,
3
+ writeText
4
+ } from "./chunk-AKIJ4LS2.js";
1
5
  import {
2
6
  atomicWriteFileSync
3
- } from "./chunk-WRUTATW4.js";
7
+ } from "./chunk-IBBAISSW.js";
4
8
 
5
9
  // src/core/path-guard.ts
6
10
  import fs from "fs";
@@ -191,10 +195,61 @@ function updateUserPolicy(guard, settingsDir, patch) {
191
195
  return merged;
192
196
  }
193
197
 
198
+ // src/excerpt/excerpt.ts
199
+ import path3 from "path";
200
+ import { randomUUID } from "crypto";
201
+ var EXCERPT_HEADER = "# \u6458\u6284\u672C\n\n> \u7559\u53E5\u5B50\u548C\u5C0F\u4E8B\u7684\u5730\u65B9\u3002\u53EF\u4EE5\u76F4\u63A5\u624B\u7F16\uFF0C\u6539\u4E71\u4E86\u4E5F\u4E0D\u5F71\u54CD\uFF1A\u8BA4\u4E0D\u51FA\u7684\u884C\u4F1A\u539F\u6837\u7559\u7740\u3002\n\n";
202
+ function excerptFilePath(dataDir) {
203
+ return path3.join(dataDir, "excerpts.md");
204
+ }
205
+ var LINE_RE = /^- \[(\d{4}-\d{2}-\d{2}) (\d{2}:\d{2})\]\[#([0-9a-f]{6})\] (.*)$/;
206
+ function renderEntry(e) {
207
+ return `- [${e.date} ${e.time}][#${e.id}] ${e.text}`;
208
+ }
209
+ function readExcerpts(guard, file) {
210
+ const raw = readText(guard, file, EXCERPT_HEADER);
211
+ const rawLines = [];
212
+ const entries = [];
213
+ for (const line of raw.split("\n")) {
214
+ const m = LINE_RE.exec(line);
215
+ if (m) entries.push({ date: m[1], time: m[2], id: m[3], text: m[4] });
216
+ rawLines.push(line);
217
+ }
218
+ return { entries, rawLines };
219
+ }
220
+ function appendExcerpt(guard, file, text, now = Date.now()) {
221
+ const trimmed = text.trim();
222
+ if (!trimmed) throw new Error("excerpt must not be empty");
223
+ const d = new Date(now);
224
+ const pad = (n) => String(n).padStart(2, "0");
225
+ const entry = {
226
+ id: randomUUID().slice(0, 6),
227
+ date: `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`,
228
+ time: `${pad(d.getHours())}:${pad(d.getMinutes())}`,
229
+ text: trimmed.replace(/\r?\n/g, " ")
230
+ };
231
+ const { rawLines } = readExcerpts(guard, file);
232
+ rawLines.push(renderEntry(entry));
233
+ writeText(guard, file, rawLines.join("\n").replace(/\n*$/, "\n"));
234
+ return entry;
235
+ }
236
+ function removeExcerpt(guard, file, key) {
237
+ const { rawLines, entries } = readExcerpts(guard, file);
238
+ const target = entries.find((e) => e.id === key || e.text.includes(key));
239
+ if (!target) return null;
240
+ const out = rawLines.filter((line) => !line.startsWith(`- [${target.date} ${target.time}][#${target.id}] `));
241
+ writeText(guard, file, out.join("\n").replace(/\n*$/, "\n"));
242
+ return target;
243
+ }
244
+ function recentExcerpts(guard, file, limit) {
245
+ const { entries } = readExcerpts(guard, file);
246
+ return limit > 0 ? entries.slice(-limit) : [];
247
+ }
248
+
194
249
  // src/core/preset-install.ts
195
250
  import fs3 from "fs";
196
251
  import os from "os";
197
- import path3 from "path";
252
+ import path4 from "path";
198
253
  import { fileURLToPath } from "url";
199
254
  var COMPOSITION_FILE = "agent.cordis.yml";
200
255
  var METADATA_FILE = "preset.yml";
@@ -202,14 +257,14 @@ var BUNDLED_PRESET_ID = "heartbeat";
202
257
  function bundledPresetDir(moduleUrl, id = BUNDLED_PRESET_ID) {
203
258
  let dir;
204
259
  try {
205
- dir = path3.dirname(fileURLToPath(moduleUrl));
260
+ dir = path4.dirname(fileURLToPath(moduleUrl));
206
261
  } catch {
207
262
  return void 0;
208
263
  }
209
264
  for (let depth = 0; depth < 5; depth += 1) {
210
- const candidate = path3.join(dir, "assets", "presets", id);
211
- if (fs3.existsSync(path3.join(candidate, COMPOSITION_FILE))) return candidate;
212
- const parent = path3.dirname(dir);
265
+ const candidate = path4.join(dir, "assets", "presets", id);
266
+ if (fs3.existsSync(path4.join(candidate, COMPOSITION_FILE))) return candidate;
267
+ const parent = path4.dirname(dir);
213
268
  if (parent === dir) break;
214
269
  dir = parent;
215
270
  }
@@ -219,12 +274,12 @@ function userPresetRoot(roots) {
219
274
  const found = roots?.find(
220
275
  (root) => root?.trust === "user" && typeof root.path === "string" && root.path.length > 0
221
276
  );
222
- return found?.path === void 0 ? void 0 : path3.resolve(found.path);
277
+ return found?.path === void 0 ? void 0 : path4.resolve(found.path);
223
278
  }
224
279
  function conventionalUserPresetRoot(env = process.env, home = os.homedir()) {
225
280
  const override = env.DSH_HOME?.trim();
226
- const root = override && override.length > 0 ? override : path3.join(home, ".dsh");
227
- return path3.join(root, ".agent-presets");
281
+ const root = override && override.length > 0 ? override : path4.join(home, ".dsh");
282
+ return path4.join(root, ".agent-presets");
228
283
  }
229
284
  function installBundledPreset(options) {
230
285
  const id = options.id && options.id.length > 0 ? options.id : BUNDLED_PRESET_ID;
@@ -256,12 +311,12 @@ function installBundledPreset(options) {
256
311
  detail: "the roster mounts no user preset root (includeUserRoot=false)"
257
312
  };
258
313
  }
259
- const dir = path3.join(root, id);
260
- const composition = path3.join(dir, COMPOSITION_FILE);
314
+ const dir = path4.join(root, id);
315
+ const composition = path4.join(dir, COMPOSITION_FILE);
261
316
  try {
262
317
  if (fs3.existsSync(composition)) {
263
318
  if (options.force !== true) {
264
- const drifted = !sameBytes(composition, path3.join(bundledDir, COMPOSITION_FILE));
319
+ const drifted = !sameBytes(composition, path4.join(bundledDir, COMPOSITION_FILE));
265
320
  return {
266
321
  action: "exists",
267
322
  id,
@@ -270,7 +325,7 @@ function installBundledPreset(options) {
270
325
  detail: drifted ? "kept as-is (differs from the bundled template)" : "kept as-is"
271
326
  };
272
327
  }
273
- fs3.copyFileSync(path3.join(bundledDir, COMPOSITION_FILE), composition);
328
+ fs3.copyFileSync(path4.join(bundledDir, COMPOSITION_FILE), composition);
274
329
  return {
275
330
  action: "restored",
276
331
  id,
@@ -281,9 +336,9 @@ function installBundledPreset(options) {
281
336
  }
282
337
  const existed = fs3.existsSync(dir);
283
338
  fs3.mkdirSync(dir, { recursive: true });
284
- fs3.copyFileSync(path3.join(bundledDir, COMPOSITION_FILE), composition);
285
- const metadata = path3.join(dir, METADATA_FILE);
286
- if (!fs3.existsSync(metadata)) fs3.copyFileSync(path3.join(bundledDir, METADATA_FILE), metadata);
339
+ fs3.copyFileSync(path4.join(bundledDir, COMPOSITION_FILE), composition);
340
+ const metadata = path4.join(dir, METADATA_FILE);
341
+ if (!fs3.existsSync(metadata)) fs3.copyFileSync(path4.join(bundledDir, METADATA_FILE), metadata);
287
342
  return {
288
343
  action: existed ? "repaired" : "created",
289
344
  id,
@@ -301,16 +356,16 @@ function describeInstall(result) {
301
356
  return `preset ${result.id} ${result.action}${where}${why}`;
302
357
  }
303
358
  function presetStatus(moduleUrl, id = BUNDLED_PRESET_ID, root = conventionalUserPresetRoot()) {
304
- const dir = path3.join(root, id);
359
+ const dir = path4.join(root, id);
305
360
  const bundledDir = bundledPresetDir(moduleUrl, id);
306
- const installed = fs3.existsSync(path3.join(dir, COMPOSITION_FILE));
361
+ const installed = fs3.existsSync(path4.join(dir, COMPOSITION_FILE));
307
362
  return {
308
363
  id,
309
364
  dir,
310
365
  ...bundledDir === void 0 ? {} : { bundledDir },
311
366
  installed,
312
- compositionMatches: installed && bundledDir !== void 0 && sameBytes(path3.join(dir, COMPOSITION_FILE), path3.join(bundledDir, COMPOSITION_FILE)),
313
- metadataMatches: bundledDir !== void 0 && fs3.existsSync(path3.join(dir, METADATA_FILE)) && sameBytes(path3.join(dir, METADATA_FILE), path3.join(bundledDir, METADATA_FILE))
367
+ compositionMatches: installed && bundledDir !== void 0 && sameBytes(path4.join(dir, COMPOSITION_FILE), path4.join(bundledDir, COMPOSITION_FILE)),
368
+ metadataMatches: bundledDir !== void 0 && fs3.existsSync(path4.join(dir, METADATA_FILE)) && sameBytes(path4.join(dir, METADATA_FILE), path4.join(bundledDir, METADATA_FILE))
314
369
  };
315
370
  }
316
371
  function sameBytes(left, right) {
@@ -326,6 +381,12 @@ export {
326
381
  deepMerge,
327
382
  loadPolicy,
328
383
  updateUserPolicy,
384
+ EXCERPT_HEADER,
385
+ excerptFilePath,
386
+ readExcerpts,
387
+ appendExcerpt,
388
+ removeExcerpt,
389
+ recentExcerpts,
329
390
  BUNDLED_PRESET_ID,
330
391
  userPresetRoot,
331
392
  conventionalUserPresetRoot,
@@ -333,4 +394,4 @@ export {
333
394
  describeInstall,
334
395
  presetStatus
335
396
  };
336
- //# sourceMappingURL=chunk-XYYEJK3W.js.map
397
+ //# sourceMappingURL=chunk-IFRMKVIC.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/core/path-guard.ts","../src/config/schema.ts","../src/config/load.ts","../src/excerpt/excerpt.ts","../src/core/preset-install.ts"],"sourcesContent":["// Path whitelist guard (requirement 8 / design doc §10.2).\n//\n// Every fs write/delete must have its target canonicalized first, then be\n// checked against the canonical workspace prefix with a separator boundary.\n// A raw startsWith check is bypassable via \"..\", symlinks/junctions, and\n// case variants; canonical realpath closes all of those on Windows.\n\nimport fs from 'node:fs';\nimport path from 'node:path';\n\nexport class PathOutsideWorkspaceError extends Error {\n constructor(target: string, workspace: string) {\n super(`path outside workspace: \"${target}\" (workspace: \"${workspace}\")`);\n this.name = 'PathOutsideWorkspaceError';\n }\n}\n\n/**\n * Resolve to an absolute canonical path. For targets that do not exist yet,\n * realpath the deepest existing ancestor and re-append the virtual tail, so\n * planned files under the workspace validate while `..` escapes still resolve\n * through the real filesystem.\n */\nexport function canonicalize(target: string): string {\n const abs = path.resolve(target);\n try {\n return fs.realpathSync(abs);\n } catch {\n // Walk up from the missing leaf, recording each missing component, until\n // an existing ancestor is found; re-append the missing tail afterwards.\n const tail: string[] = [];\n let dir = abs;\n for (;;) {\n const base = path.basename(dir);\n const parent = path.dirname(dir);\n if (parent === dir) {\n throw new Error(`cannot canonicalize \"${target}\": no existing ancestor`);\n }\n tail.push(base);\n dir = parent;\n try {\n const realDir = fs.realpathSync(dir);\n return path.join(realDir, ...tail.reverse());\n } catch {\n continue;\n }\n }\n }\n}\n\n/**\n * True when `targetCanon` equals the workspace dir or lies under it.\n * Comparison is case-insensitive (Windows filesystems) and requires a\n * separator boundary so `D:\\ws-data-evil` does not match workspace `D:\\ws-data`.\n */\nexport function isInsideWorkspace(workspaceCanon: string, targetCanon: string): boolean {\n const norm = (p: string) => {\n let n = path.normalize(p).toLowerCase();\n if (!n.endsWith(path.sep)) n += path.sep;\n return n;\n };\n const w = norm(workspaceCanon);\n const t = norm(targetCanon);\n return t === w || t.startsWith(w);\n}\n\nexport interface PathGuard {\n /** Canonical workspace boundary. */\n readonly workspace: string;\n /** Canonicalize then validate; returns the canonical path or throws. */\n assert(target: string): string;\n /** Canonicalize then validate; returns null instead of throwing. */\n check(target: string): string | null;\n}\n\nexport function createPathGuard(workspaceDir: string): PathGuard {\n const workspace = canonicalize(workspaceDir);\n const guard: PathGuard = {\n workspace,\n check(target: string): string | null {\n const canon = canonicalize(target);\n return isInsideWorkspace(workspace, canon) ? canon : null;\n },\n assert(target: string): string {\n const canon = guard.check(target);\n if (canon === null) throw new PathOutsideWorkspaceError(target, workspace);\n return canon;\n },\n };\n return guard;\n}\n","// Policy shape + runtime validation. Factory defaults live in config/policy.json\n// (read-only); the user layer in data/settings/policy.json overrides via deep\n// merge (design doc §13, D3).\n\nexport interface QuietHours {\n start: string; // \"HH:MM\"\n end: string; // \"HH:MM\"\n}\n\nexport interface BrowseWindow {\n start: string;\n end: string;\n}\n\nexport interface Policy {\n heartbeat: { intervalMin: number; idleMode: boolean; archiveRotatedHome: boolean };\n gate: {\n maxDailySend: number;\n cooldownMinutes: number;\n quietHours: QuietHours;\n };\n browse: {\n windows: BrowseWindow[];\n minIntervalHours: number;\n maxSeedsPerVisit: number;\n };\n seeds: {\n maxActive: number;\n ttlDays: { news: number; fandom: number; scene: number; promise: number };\n coldBenchDays: number;\n /** Archive-area cap (v1.9.0): gc trims archived seeds beyond this, oldest first. */\n archiveCap: number;\n retireAfterUsed: number;\n scoreWeights: { freshness: number; unused: number; confidence: number };\n };\n profile: {\n consolidation: { minIntervalHours: number; inboxBacklog: number };\n partitionCap: number;\n maxOpsPerRun: number;\n confidenceCap: { chat: number; screen: number; browse: number };\n volatileDays: number;\n stableLowActivityDays: number;\n psyEnabled: boolean;\n };\n /** Observation depth (v1.8.0): how much of each user message enters the inbox. */\n observe: { maxChars: number; perBeat: number };\n weekly: {\n enabled: boolean;\n /** v2.0 (item 5): how many past reports the store keeps and the card lists. */\n keepReports: number;\n };\n retention: { envPulseHours: number; decisionLogDays: number };\n}\n\nconst HHMM = /^([01]\\d|2[0-3]):[0-5]\\d$/;\n\nfunction isPlainObject(v: unknown): v is Record<string, unknown> {\n return typeof v === 'object' && v !== null && !Array.isArray(v);\n}\n\nfunction fail(msg: string): never {\n throw new Error(`policy: ${msg}`);\n}\n\nexport function assertPolicy(input: unknown): asserts input is Policy {\n if (!isPlainObject(input)) fail('root must be an object');\n const p = input;\n const hb = p.heartbeat;\n if (!isPlainObject(hb)) fail('heartbeat missing');\n if (typeof hb.intervalMin !== 'number' || hb.intervalMin < 1 || hb.intervalMin > 1440) {\n fail('heartbeat.intervalMin must be a number in [1, 1440]');\n }\n if (typeof hb.idleMode !== 'boolean') fail('heartbeat.idleMode must be boolean');\n if (typeof hb.archiveRotatedHome !== 'boolean') fail('heartbeat.archiveRotatedHome must be boolean');\n const g = p.gate;\n if (!isPlainObject(g)) fail('gate missing');\n if (typeof g.maxDailySend !== 'number' || g.maxDailySend < 0) fail('gate.maxDailySend must be >= 0');\n if (typeof g.cooldownMinutes !== 'number' || g.cooldownMinutes < 0) fail('gate.cooldownMinutes must be >= 0');\n const qh = g.quietHours;\n if (!isPlainObject(qh)) fail('gate.quietHours missing');\n if (typeof qh.start !== 'string' || !HHMM.test(qh.start) || typeof qh.end !== 'string' || !HHMM.test(qh.end)) {\n fail('gate.quietHours must be {start:\"HH:MM\", end:\"HH:MM\"}');\n }\n const b = p.browse;\n if (!isPlainObject(b)) fail('browse missing');\n if (!Array.isArray(b.windows) || b.windows.length === 0) fail('browse.windows must be a non-empty array');\n for (const w of b.windows) {\n if (!isPlainObject(w)) fail('browse.windows entries must be objects');\n if (typeof w.start !== 'string' || !HHMM.test(w.start) || typeof w.end !== 'string' || !HHMM.test(w.end)) {\n fail('browse.windows entries must be {start:\"HH:MM\", end:\"HH:MM\"}');\n }\n }\n if (typeof b.minIntervalHours !== 'number' || b.minIntervalHours <= 0) fail('browse.minIntervalHours must be > 0');\n if (typeof b.maxSeedsPerVisit !== 'number' || b.maxSeedsPerVisit < 1) fail('browse.maxSeedsPerVisit must be >= 1');\n const s = p.seeds;\n if (!isPlainObject(s)) fail('seeds missing');\n if (typeof s.maxActive !== 'number' || s.maxActive < 1) fail('seeds.maxActive must be >= 1');\n if (!isPlainObject(s.ttlDays)) fail('seeds.ttlDays missing');\n for (const k of ['news', 'fandom', 'scene', 'promise'] as const) {\n if (typeof s.ttlDays[k] !== 'number') fail(`seeds.ttlDays.${k} missing`);\n }\n if (typeof s.coldBenchDays !== 'number') fail('seeds.coldBenchDays missing');\n if (typeof s.archiveCap !== 'number' || s.archiveCap < 1) fail('seeds.archiveCap must be >= 1');\n if (typeof s.retireAfterUsed !== 'number' || s.retireAfterUsed < 1) fail('seeds.retireAfterUsed must be >= 1');\n if (!isPlainObject(s.scoreWeights)) fail('seeds.scoreWeights missing');\n const pr = p.profile;\n if (!isPlainObject(pr)) fail('profile missing');\n const c = pr.consolidation;\n if (!isPlainObject(c)) fail('profile.consolidation missing');\n if (typeof c.minIntervalHours !== 'number' || typeof c.inboxBacklog !== 'number') fail('profile.consolidation fields missing');\n if (typeof pr.partitionCap !== 'number' || pr.partitionCap < 1) fail('profile.partitionCap must be >= 1');\n if (typeof pr.maxOpsPerRun !== 'number' || pr.maxOpsPerRun < 1) fail('profile.maxOpsPerRun must be >= 1');\n const cc = pr.confidenceCap;\n if (!isPlainObject(cc)) fail('profile.confidenceCap missing');\n if (typeof cc.chat !== 'number' || typeof cc.screen !== 'number' || typeof cc.browse !== 'number') {\n fail('profile.confidenceCap fields missing');\n }\n if (typeof pr.volatileDays !== 'number' || pr.volatileDays < 1) fail('profile.volatileDays must be >= 1');\n if (typeof pr.stableLowActivityDays !== 'number' || pr.stableLowActivityDays < 1) fail('profile.stableLowActivityDays must be >= 1');\n if (typeof pr.psyEnabled !== 'boolean') fail('profile.psyEnabled must be boolean');\n const ob = p.observe;\n if (!isPlainObject(ob)) fail('observe missing');\n if (typeof ob.maxChars !== 'number' || ob.maxChars < 20 || ob.maxChars > 2000) fail('observe.maxChars must be in [20, 2000]');\n if (typeof ob.perBeat !== 'number' || ob.perBeat < 1 || ob.perBeat > 100) fail('observe.perBeat must be in [1, 100]');\n if (!isPlainObject(p.weekly)) fail('weekly missing');\n if (typeof p.weekly.enabled !== 'boolean') fail('weekly.enabled must be boolean');\n if (typeof p.weekly.keepReports !== 'number' || p.weekly.keepReports < 1 || p.weekly.keepReports > 52) {\n fail('weekly.keepReports must be a number in [1, 52]');\n }\n const r = p.retention;\n if (!isPlainObject(r)) fail('retention missing');\n if (typeof r.envPulseHours !== 'number' || typeof r.decisionLogDays !== 'number') fail('retention fields missing');\n}\n\n/** Recursive merge: user values win; objects merge, arrays and scalars replace. */\nexport function deepMerge<T>(base: T, override: unknown): T {\n if (!isPlainObject(base) || !isPlainObject(override)) {\n return (override === undefined ? base : (override as T));\n }\n const out: Record<string, unknown> = { ...base };\n for (const [k, v] of Object.entries(override)) {\n out[k] = v === undefined ? (base as Record<string, unknown>)[k] : deepMerge((base as Record<string, unknown>)[k], v);\n }\n return out as T;\n}","// Two-layer policy loading (D3): factory defaults (config/policy.json, read-only)\n// + user layer (data/settings/policy.json). Missing user layer is normal; an\n// invalid factory file or user layer is fail-closed (throw at startup).\n\nimport fs from 'node:fs';\nimport path from 'node:path';\nimport type { PathGuard } from '../core/path-guard.js';\nimport { assertPolicy, deepMerge, type Policy } from './schema.js';\nimport { atomicWriteFileSync } from '../core/atomic-fs.js';\n\nexport const USER_POLICY_FILE = 'policy.json';\n\nexport function loadPolicy(guard: PathGuard, configDir: string, settingsDir: string): Policy {\n const factoryPath = path.join(configDir, 'policy.json');\n let factoryRaw: unknown;\n try {\n factoryRaw = JSON.parse(fs.readFileSync(factoryPath, 'utf8'));\n } catch (e) {\n throw new Error(`factory policy unreadable at ${factoryPath}: ${String(e)}`);\n }\n assertPolicy(factoryRaw);\n\n const userPath = guard.assert(path.join(settingsDir, USER_POLICY_FILE));\n let merged: Policy = factoryRaw;\n if (fs.existsSync(userPath)) {\n try {\n const userRaw: unknown = JSON.parse(fs.readFileSync(userPath, 'utf8'));\n merged = deepMerge(factoryRaw, userRaw);\n } catch (e) {\n throw new Error(`user policy layer unparseable at ${userPath}: ${String(e)}`);\n }\n }\n assertPolicy(merged);\n return merged;\n}\n\n/**\n * Merge `patch` into the user policy layer (data/settings/policy.json) without\n * touching the factory file. Used by the settings card for policy-backed\n * toggles (psyEnabled, v1.7.0). No full-Policy validation here: the layer is\n * merged over the factory and validated together at the next loadPolicy.\n */\nexport function updateUserPolicy(guard: PathGuard, settingsDir: string, patch: Record<string, unknown>): Record<string, unknown> {\n const userPath = guard.assert(path.join(settingsDir, USER_POLICY_FILE));\n let user: Record<string, unknown> = {};\n try {\n user = JSON.parse(fs.readFileSync(userPath, 'utf8')) as Record<string, unknown>;\n if (!user || typeof user !== 'object' || Array.isArray(user)) user = {};\n } catch {\n user = {}; // fresh layer\n }\n const merged = deepMerge(user, patch) as Record<string, unknown>;\n fs.mkdirSync(path.dirname(userPath), { recursive: true });\n atomicWriteFileSync(userPath, JSON.stringify(merged, null, 2) + '\\n');\n return merged;\n}\n","// Excerpt book (\"摘抄本\") - the place for lines worth keeping.\n//\n// Deliberately plaintext Markdown, like the ledger: a notebook that cannot be\n// read is not a notebook. The two plaintext files in data/ are the ledger\n// (his todos) and this one (her clippings); everything private stays in KHBV2\n// containers. The user may edit it by hand, so the parser is tolerant:\n// unrecognised lines are preserved verbatim on rewrite.\n\nimport path from 'node:path';\nimport { randomUUID } from 'node:crypto';\nimport type { PathGuard } from '../core/path-guard.js';\nimport { readText, writeText } from '../vault/vault.js';\n\nexport interface ExcerptEntry {\n id: string;\n date: string; // \"YYYY-MM-DD\"\n time: string; // \"HH:MM\"\n text: string;\n}\n\n/** Used both as the read fallback and as the file the settings card opens, so\n * a fresh notebook already carries the two lines that explain it. */\nexport const EXCERPT_HEADER = '# 摘抄本\\n\\n> 留句子和小事的地方。可以直接手编,改乱了也不影响:认不出的行会原样留着。\\n\\n';\n\nexport function excerptFilePath(dataDir: string): string {\n return path.join(dataDir, 'excerpts.md');\n}\n\nconst LINE_RE = /^- \\[(\\d{4}-\\d{2}-\\d{2}) (\\d{2}:\\d{2})\\]\\[#([0-9a-f]{6})\\] (.*)$/;\n\nfunction renderEntry(e: ExcerptEntry): string {\n return `- [${e.date} ${e.time}][#${e.id}] ${e.text}`;\n}\n\nexport function readExcerpts(guard: PathGuard, file: string): { entries: ExcerptEntry[]; rawLines: string[] } {\n const raw = readText(guard, file, EXCERPT_HEADER);\n const rawLines: string[] = [];\n const entries: ExcerptEntry[] = [];\n for (const line of raw.split('\\n')) {\n const m = LINE_RE.exec(line);\n if (m) entries.push({ date: m[1]!, time: m[2]!, id: m[3]!, text: m[4]! });\n rawLines.push(line);\n }\n return { entries, rawLines };\n}\n\n/** Append one clipping, stamped with the local wall clock (like the ledger). */\nexport function appendExcerpt(guard: PathGuard, file: string, text: string, now = Date.now()): ExcerptEntry {\n const trimmed = text.trim();\n if (!trimmed) throw new Error('excerpt must not be empty');\n const d = new Date(now);\n const pad = (n: number) => String(n).padStart(2, '0');\n const entry: ExcerptEntry = {\n id: randomUUID().slice(0, 6),\n date: `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`,\n time: `${pad(d.getHours())}:${pad(d.getMinutes())}`,\n text: trimmed.replace(/\\r?\\n/g, ' '),\n };\n const { rawLines } = readExcerpts(guard, file);\n rawLines.push(renderEntry(entry));\n writeText(guard, file, rawLines.join('\\n').replace(/\\n*$/, '\\n'));\n return entry;\n}\n\n/** Tear one page out by id (preferred) or unique text substring. Unlike the\n * ledger there is no done-state: a clipping either stays or is gone. */\nexport function removeExcerpt(guard: PathGuard, file: string, key: string): ExcerptEntry | null {\n const { rawLines, entries } = readExcerpts(guard, file);\n const target = entries.find((e) => e.id === key || e.text.includes(key));\n if (!target) return null;\n const out = rawLines.filter((line) => !line.startsWith(`- [${target.date} ${target.time}][#${target.id}] `));\n writeText(guard, file, out.join('\\n').replace(/\\n*$/, '\\n'));\n return target;\n}\n\n/** The tail of the notebook, oldest-first among the newest `limit` entries. */\nexport function recentExcerpts(guard: PathGuard, file: string, limit: number): ExcerptEntry[] {\n const { entries } = readExcerpts(guard, file);\n return limit > 0 ? entries.slice(-limit) : [];\n}\n","// Bundled agent-preset installation (contract C13).\n//\n// Why this exists: the heartbeat agent is created by the host `agents` service,\n// which does NOT go through the session-start preset picker. A bare agent joins\n// no preset, and dsh-agent-presets states the consequence verbatim — \"its tools,\n// prompt sections, and skill catalog resolve against the empty global layer\" —\n// so it cannot even see `web_search`. The preset is therefore not optional, and\n// asking the operator to hand-copy two YAML files was the most error-prone step\n// of setup. The plugin now materialises its own bundled template in the roster's\n// USER root on first run.\n//\n// Two properties keep that safe:\n// 1. An existing preset is NEVER overwritten — the composition file belongs to\n// whoever edited it. Only a missing (or ghost) directory is filled in.\n// 2. The target comes from the roster's OWN roots (`agentPresets.roots`,\n// trust === \"user\") rather than a guessed `~/.dsh`, so `$DSH_HOME` and a\n// configured home are honoured without re-deriving them here.\n//\n// Timing: dsh-agent-presets re-scans the filesystem on every read\n// (`list()` -> `discoverPresets` -> `scanRoot`, no cache), so a directory\n// created here is visible to the very next `mount()` — no restart in between.\n\nimport fs from 'node:fs';\nimport os from 'node:os';\nimport path from 'node:path';\nimport { fileURLToPath } from 'node:url';\n\nexport const COMPOSITION_FILE = 'agent.cordis.yml';\nexport const METADATA_FILE = 'preset.yml';\n/** The template this package ships, in `assets/presets/<id>/`. */\nexport const BUNDLED_PRESET_ID = 'heartbeat';\n\n/** One entry of the roster's root list (`AgentPresets.roots`). */\nexport interface PresetRootLike {\n path?: string;\n trust?: string;\n}\n\nexport type PresetInstallAction =\n | 'exists'\n | 'created'\n | 'repaired'\n | 'restored'\n | 'skipped-disabled'\n | 'skipped-custom-id'\n | 'skipped-no-root'\n | 'error';\n\nexport interface PresetInstallResult {\n action: PresetInstallAction;\n id: string;\n /** Where the preset lives (or would live). */\n dir?: string;\n /** Where the bundled template was read from. */\n bundledDir?: string;\n detail?: string;\n}\n\n/**\n * Locate the bundled template directory by walking up from a module URL.\n * Works from both entry points (`dist/index.js` and `dist/cli/index.js`) and\n * from the pnpm copy of an installed package.\n */\nexport function bundledPresetDir(\n moduleUrl: string,\n id: string = BUNDLED_PRESET_ID,\n): string | undefined {\n let dir: string;\n try {\n dir = path.dirname(fileURLToPath(moduleUrl));\n } catch {\n return undefined;\n }\n for (let depth = 0; depth < 5; depth += 1) {\n const candidate = path.join(dir, 'assets', 'presets', id);\n if (fs.existsSync(path.join(candidate, COMPOSITION_FILE))) return candidate;\n const parent = path.dirname(dir);\n if (parent === dir) break;\n dir = parent;\n }\n return undefined;\n}\n\n/** The roster's user-trust root, which is where locally authored presets live. */\nexport function userPresetRoot(roots?: readonly PresetRootLike[]): string | undefined {\n const found = roots?.find(\n (root) => root?.trust === 'user' && typeof root.path === 'string' && root.path.length > 0,\n );\n return found?.path === undefined ? undefined : path.resolve(found.path);\n}\n\n/**\n * `<dshHome>/.agent-presets` derived from the documented precedence\n * (`$DSH_HOME`, then `~/.dsh`). Only used where no roster is available — the\n * plugin itself prefers {@link userPresetRoot}.\n */\nexport function conventionalUserPresetRoot(\n env: NodeJS.ProcessEnv = process.env,\n home: string = os.homedir(),\n): string {\n const override = env.DSH_HOME?.trim();\n const root = override && override.length > 0 ? override : path.join(home, '.dsh');\n return path.join(root, '.agent-presets');\n}\n\nexport interface InstallPresetOptions {\n /** `import.meta.url` of the calling entry point. */\n moduleUrl: string;\n /** Preset id the heartbeat agent will join (config `agentPreset`). */\n id?: string;\n /** User-trust root reported by the roster. */\n root?: string;\n /** True when the roster answered — then an absent user root is a fact, not a guess. */\n rosterKnown?: boolean;\n /** `false` disables the install (config `installPreset`). */\n enabled?: boolean;\n /** Overwrite an existing composition file with the bundled one. */\n force?: boolean;\n}\n\n/**\n * Materialise the bundled preset in the roster's user root when it is absent.\n * Never destructive: an existing composition file is kept unless `force`.\n */\nexport function installBundledPreset(options: InstallPresetOptions): PresetInstallResult {\n const id = options.id && options.id.length > 0 ? options.id : BUNDLED_PRESET_ID;\n if (options.enabled === false) {\n return { action: 'skipped-disabled', id, detail: 'installPreset=false' };\n }\n const bundledDir = bundledPresetDir(options.moduleUrl, BUNDLED_PRESET_ID);\n if (id !== BUNDLED_PRESET_ID) {\n return {\n action: 'skipped-custom-id',\n id,\n ...(bundledDir === undefined ? {} : { bundledDir }),\n detail: `only \"${BUNDLED_PRESET_ID}\" ships with the plugin; \"${id}\" is yours to provide`,\n };\n }\n if (bundledDir === undefined) {\n return {\n action: 'error',\n id,\n detail: 'bundled template not found next to the plugin (assets/presets/heartbeat)',\n };\n }\n const root =\n options.root ?? (options.rosterKnown ? undefined : conventionalUserPresetRoot());\n if (root === undefined) {\n return {\n action: 'skipped-no-root',\n id,\n bundledDir,\n detail: 'the roster mounts no user preset root (includeUserRoot=false)',\n };\n }\n const dir = path.join(root, id);\n const composition = path.join(dir, COMPOSITION_FILE);\n try {\n if (fs.existsSync(composition)) {\n if (options.force !== true) {\n const drifted = !sameBytes(composition, path.join(bundledDir, COMPOSITION_FILE));\n return {\n action: 'exists',\n id,\n dir,\n bundledDir,\n detail: drifted ? 'kept as-is (differs from the bundled template)' : 'kept as-is',\n };\n }\n fs.copyFileSync(path.join(bundledDir, COMPOSITION_FILE), composition);\n return {\n action: 'restored',\n id,\n dir,\n bundledDir,\n detail: 'composition replaced from the bundled template',\n };\n }\n const existed = fs.existsSync(dir);\n fs.mkdirSync(dir, { recursive: true });\n fs.copyFileSync(path.join(bundledDir, COMPOSITION_FILE), composition);\n const metadata = path.join(dir, METADATA_FILE);\n // A directory with a composition but no metadata is legal; only fill a void.\n if (!fs.existsSync(metadata)) fs.copyFileSync(path.join(bundledDir, METADATA_FILE), metadata);\n return {\n action: existed ? 'repaired' : 'created',\n id,\n dir,\n bundledDir,\n detail: existed\n ? 'directory existed without a composition file (it occupied the id as a broken row)'\n : undefined,\n };\n } catch (error) {\n return { action: 'error', id, dir, bundledDir, detail: String(error).slice(0, 200) };\n }\n}\n\n/** One line suitable for the audit log and `ctx.logger`. */\nexport function describeInstall(result: PresetInstallResult): string {\n const where = result.dir === undefined ? '' : ` (${result.dir})`;\n const why = result.detail === undefined ? '' : ` — ${result.detail}`;\n return `preset ${result.id} ${result.action}${where}${why}`;\n}\n\nexport interface PresetStatus {\n id: string;\n dir: string;\n bundledDir?: string;\n installed: boolean;\n compositionMatches: boolean;\n metadataMatches: boolean;\n}\n\n/** Read-only inspection for the CLI (`preset status`). */\nexport function presetStatus(\n moduleUrl: string,\n id: string = BUNDLED_PRESET_ID,\n root: string = conventionalUserPresetRoot(),\n): PresetStatus {\n const dir = path.join(root, id);\n const bundledDir = bundledPresetDir(moduleUrl, id);\n const installed = fs.existsSync(path.join(dir, COMPOSITION_FILE));\n return {\n id,\n dir,\n ...(bundledDir === undefined ? {} : { bundledDir }),\n installed,\n compositionMatches:\n installed && bundledDir !== undefined && sameBytes(path.join(dir, COMPOSITION_FILE), path.join(bundledDir, COMPOSITION_FILE)),\n metadataMatches:\n bundledDir !== undefined &&\n fs.existsSync(path.join(dir, METADATA_FILE)) &&\n sameBytes(path.join(dir, METADATA_FILE), path.join(bundledDir, METADATA_FILE)),\n };\n}\n\nfunction sameBytes(left: string, right: string): boolean {\n try {\n return fs.readFileSync(left).equals(fs.readFileSync(right));\n } catch {\n return false;\n }\n}\n"],"mappings":";;;;;;;;;AAOA,OAAO,QAAQ;AACf,OAAO,UAAU;AAEV,IAAM,4BAAN,cAAwC,MAAM;AAAA,EACnD,YAAY,QAAgB,WAAmB;AAC7C,UAAM,4BAA4B,MAAM,kBAAkB,SAAS,IAAI;AACvE,SAAK,OAAO;AAAA,EACd;AACF;AAQO,SAAS,aAAa,QAAwB;AACnD,QAAM,MAAM,KAAK,QAAQ,MAAM;AAC/B,MAAI;AACF,WAAO,GAAG,aAAa,GAAG;AAAA,EAC5B,QAAQ;AAGN,UAAM,OAAiB,CAAC;AACxB,QAAI,MAAM;AACV,eAAS;AACP,YAAM,OAAO,KAAK,SAAS,GAAG;AAC9B,YAAM,SAAS,KAAK,QAAQ,GAAG;AAC/B,UAAI,WAAW,KAAK;AAClB,cAAM,IAAI,MAAM,wBAAwB,MAAM,yBAAyB;AAAA,MACzE;AACA,WAAK,KAAK,IAAI;AACd,YAAM;AACN,UAAI;AACF,cAAM,UAAU,GAAG,aAAa,GAAG;AACnC,eAAO,KAAK,KAAK,SAAS,GAAG,KAAK,QAAQ,CAAC;AAAA,MAC7C,QAAQ;AACN;AAAA,MACF;AAAA,IACF;AAAA,EACF;AACF;AAOO,SAAS,kBAAkB,gBAAwB,aAA8B;AACtF,QAAM,OAAO,CAAC,MAAc;AAC1B,QAAI,IAAI,KAAK,UAAU,CAAC,EAAE,YAAY;AACtC,QAAI,CAAC,EAAE,SAAS,KAAK,GAAG,EAAG,MAAK,KAAK;AACrC,WAAO;AAAA,EACT;AACA,QAAM,IAAI,KAAK,cAAc;AAC7B,QAAM,IAAI,KAAK,WAAW;AAC1B,SAAO,MAAM,KAAK,EAAE,WAAW,CAAC;AAClC;AAWO,SAAS,gBAAgB,cAAiC;AAC/D,QAAM,YAAY,aAAa,YAAY;AAC3C,QAAM,QAAmB;AAAA,IACvB;AAAA,IACA,MAAM,QAA+B;AACnC,YAAM,QAAQ,aAAa,MAAM;AACjC,aAAO,kBAAkB,WAAW,KAAK,IAAI,QAAQ;AAAA,IACvD;AAAA,IACA,OAAO,QAAwB;AAC7B,YAAM,QAAQ,MAAM,MAAM,MAAM;AAChC,UAAI,UAAU,KAAM,OAAM,IAAI,0BAA0B,QAAQ,SAAS;AACzE,aAAO;AAAA,IACT;AAAA,EACF;AACA,SAAO;AACT;;;ACpCA,IAAM,OAAO;AAEb,SAAS,cAAc,GAA0C;AAC/D,SAAO,OAAO,MAAM,YAAY,MAAM,QAAQ,CAAC,MAAM,QAAQ,CAAC;AAChE;AAEA,SAAS,KAAK,KAAoB;AAChC,QAAM,IAAI,MAAM,WAAW,GAAG,EAAE;AAClC;AAEO,SAAS,aAAa,OAAyC;AACpE,MAAI,CAAC,cAAc,KAAK,EAAG,MAAK,wBAAwB;AACxD,QAAM,IAAI;AACV,QAAM,KAAK,EAAE;AACb,MAAI,CAAC,cAAc,EAAE,EAAG,MAAK,mBAAmB;AAChD,MAAI,OAAO,GAAG,gBAAgB,YAAY,GAAG,cAAc,KAAK,GAAG,cAAc,MAAM;AACrF,SAAK,qDAAqD;AAAA,EAC5D;AACA,MAAI,OAAO,GAAG,aAAa,UAAW,MAAK,oCAAoC;AAC/E,MAAI,OAAO,GAAG,uBAAuB,UAAW,MAAK,8CAA8C;AACnG,QAAM,IAAI,EAAE;AACZ,MAAI,CAAC,cAAc,CAAC,EAAG,MAAK,cAAc;AAC1C,MAAI,OAAO,EAAE,iBAAiB,YAAY,EAAE,eAAe,EAAG,MAAK,gCAAgC;AACnG,MAAI,OAAO,EAAE,oBAAoB,YAAY,EAAE,kBAAkB,EAAG,MAAK,mCAAmC;AAC5G,QAAM,KAAK,EAAE;AACb,MAAI,CAAC,cAAc,EAAE,EAAG,MAAK,yBAAyB;AACtD,MAAI,OAAO,GAAG,UAAU,YAAY,CAAC,KAAK,KAAK,GAAG,KAAK,KAAK,OAAO,GAAG,QAAQ,YAAY,CAAC,KAAK,KAAK,GAAG,GAAG,GAAG;AAC5G,SAAK,sDAAsD;AAAA,EAC7D;AACA,QAAM,IAAI,EAAE;AACZ,MAAI,CAAC,cAAc,CAAC,EAAG,MAAK,gBAAgB;AAC5C,MAAI,CAAC,MAAM,QAAQ,EAAE,OAAO,KAAK,EAAE,QAAQ,WAAW,EAAG,MAAK,0CAA0C;AACxG,aAAW,KAAK,EAAE,SAAS;AACzB,QAAI,CAAC,cAAc,CAAC,EAAG,MAAK,wCAAwC;AACpE,QAAI,OAAO,EAAE,UAAU,YAAY,CAAC,KAAK,KAAK,EAAE,KAAK,KAAK,OAAO,EAAE,QAAQ,YAAY,CAAC,KAAK,KAAK,EAAE,GAAG,GAAG;AACxG,WAAK,6DAA6D;AAAA,IACpE;AAAA,EACF;AACA,MAAI,OAAO,EAAE,qBAAqB,YAAY,EAAE,oBAAoB,EAAG,MAAK,qCAAqC;AACjH,MAAI,OAAO,EAAE,qBAAqB,YAAY,EAAE,mBAAmB,EAAG,MAAK,sCAAsC;AACjH,QAAM,IAAI,EAAE;AACZ,MAAI,CAAC,cAAc,CAAC,EAAG,MAAK,eAAe;AAC3C,MAAI,OAAO,EAAE,cAAc,YAAY,EAAE,YAAY,EAAG,MAAK,8BAA8B;AAC3F,MAAI,CAAC,cAAc,EAAE,OAAO,EAAG,MAAK,uBAAuB;AAC3D,aAAW,KAAK,CAAC,QAAQ,UAAU,SAAS,SAAS,GAAY;AAC/D,QAAI,OAAO,EAAE,QAAQ,CAAC,MAAM,SAAU,MAAK,iBAAiB,CAAC,UAAU;AAAA,EACzE;AACA,MAAI,OAAO,EAAE,kBAAkB,SAAU,MAAK,6BAA6B;AAC3E,MAAI,OAAO,EAAE,eAAe,YAAY,EAAE,aAAa,EAAG,MAAK,+BAA+B;AAC9F,MAAI,OAAO,EAAE,oBAAoB,YAAY,EAAE,kBAAkB,EAAG,MAAK,oCAAoC;AAC7G,MAAI,CAAC,cAAc,EAAE,YAAY,EAAG,MAAK,4BAA4B;AACrE,QAAM,KAAK,EAAE;AACb,MAAI,CAAC,cAAc,EAAE,EAAG,MAAK,iBAAiB;AAC9C,QAAM,IAAI,GAAG;AACb,MAAI,CAAC,cAAc,CAAC,EAAG,MAAK,+BAA+B;AAC3D,MAAI,OAAO,EAAE,qBAAqB,YAAY,OAAO,EAAE,iBAAiB,SAAU,MAAK,sCAAsC;AAC7H,MAAI,OAAO,GAAG,iBAAiB,YAAY,GAAG,eAAe,EAAG,MAAK,mCAAmC;AACxG,MAAI,OAAO,GAAG,iBAAiB,YAAY,GAAG,eAAe,EAAG,MAAK,mCAAmC;AACxG,QAAM,KAAK,GAAG;AACd,MAAI,CAAC,cAAc,EAAE,EAAG,MAAK,+BAA+B;AAC5D,MAAI,OAAO,GAAG,SAAS,YAAY,OAAO,GAAG,WAAW,YAAY,OAAO,GAAG,WAAW,UAAU;AACjG,SAAK,sCAAsC;AAAA,EAC7C;AACA,MAAI,OAAO,GAAG,iBAAiB,YAAY,GAAG,eAAe,EAAG,MAAK,mCAAmC;AACxG,MAAI,OAAO,GAAG,0BAA0B,YAAY,GAAG,wBAAwB,EAAG,MAAK,4CAA4C;AACnI,MAAI,OAAO,GAAG,eAAe,UAAW,MAAK,oCAAoC;AACjF,QAAM,KAAK,EAAE;AACb,MAAI,CAAC,cAAc,EAAE,EAAG,MAAK,iBAAiB;AAC9C,MAAI,OAAO,GAAG,aAAa,YAAY,GAAG,WAAW,MAAM,GAAG,WAAW,IAAM,MAAK,wCAAwC;AAC5H,MAAI,OAAO,GAAG,YAAY,YAAY,GAAG,UAAU,KAAK,GAAG,UAAU,IAAK,MAAK,qCAAqC;AACpH,MAAI,CAAC,cAAc,EAAE,MAAM,EAAG,MAAK,gBAAgB;AACnD,MAAI,OAAO,EAAE,OAAO,YAAY,UAAW,MAAK,gCAAgC;AAChF,MAAI,OAAO,EAAE,OAAO,gBAAgB,YAAY,EAAE,OAAO,cAAc,KAAK,EAAE,OAAO,cAAc,IAAI;AACrG,SAAK,gDAAgD;AAAA,EACvD;AACA,QAAM,IAAI,EAAE;AACZ,MAAI,CAAC,cAAc,CAAC,EAAG,MAAK,mBAAmB;AAC/C,MAAI,OAAO,EAAE,kBAAkB,YAAY,OAAO,EAAE,oBAAoB,SAAU,MAAK,0BAA0B;AACnH;AAGO,SAAS,UAAa,MAAS,UAAsB;AAC1D,MAAI,CAAC,cAAc,IAAI,KAAK,CAAC,cAAc,QAAQ,GAAG;AACpD,WAAQ,aAAa,SAAY,OAAQ;AAAA,EAC3C;AACA,QAAM,MAA+B,EAAE,GAAG,KAAK;AAC/C,aAAW,CAAC,GAAG,CAAC,KAAK,OAAO,QAAQ,QAAQ,GAAG;AAC7C,QAAI,CAAC,IAAI,MAAM,SAAa,KAAiC,CAAC,IAAI,UAAW,KAAiC,CAAC,GAAG,CAAC;AAAA,EACrH;AACA,SAAO;AACT;;;AC5IA,OAAOA,SAAQ;AACf,OAAOC,WAAU;AAKV,IAAM,mBAAmB;AAEzB,SAAS,WAAW,OAAkB,WAAmB,aAA6B;AAC3F,QAAM,cAAcC,MAAK,KAAK,WAAW,aAAa;AACtD,MAAI;AACJ,MAAI;AACF,iBAAa,KAAK,MAAMC,IAAG,aAAa,aAAa,MAAM,CAAC;AAAA,EAC9D,SAAS,GAAG;AACV,UAAM,IAAI,MAAM,gCAAgC,WAAW,KAAK,OAAO,CAAC,CAAC,EAAE;AAAA,EAC7E;AACA,eAAa,UAAU;AAEvB,QAAM,WAAW,MAAM,OAAOD,MAAK,KAAK,aAAa,gBAAgB,CAAC;AACtE,MAAI,SAAiB;AACrB,MAAIC,IAAG,WAAW,QAAQ,GAAG;AAC3B,QAAI;AACF,YAAM,UAAmB,KAAK,MAAMA,IAAG,aAAa,UAAU,MAAM,CAAC;AACrE,eAAS,UAAU,YAAY,OAAO;AAAA,IACxC,SAAS,GAAG;AACV,YAAM,IAAI,MAAM,oCAAoC,QAAQ,KAAK,OAAO,CAAC,CAAC,EAAE;AAAA,IAC9E;AAAA,EACF;AACA,eAAa,MAAM;AACnB,SAAO;AACT;AAQO,SAAS,iBAAiB,OAAkB,aAAqB,OAAyD;AAC/H,QAAM,WAAW,MAAM,OAAOD,MAAK,KAAK,aAAa,gBAAgB,CAAC;AACtE,MAAI,OAAgC,CAAC;AACrC,MAAI;AACF,WAAO,KAAK,MAAMC,IAAG,aAAa,UAAU,MAAM,CAAC;AACnD,QAAI,CAAC,QAAQ,OAAO,SAAS,YAAY,MAAM,QAAQ,IAAI,EAAG,QAAO,CAAC;AAAA,EACxE,QAAQ;AACN,WAAO,CAAC;AAAA,EACV;AACA,QAAM,SAAS,UAAU,MAAM,KAAK;AACpC,EAAAA,IAAG,UAAUD,MAAK,QAAQ,QAAQ,GAAG,EAAE,WAAW,KAAK,CAAC;AACxD,sBAAoB,UAAU,KAAK,UAAU,QAAQ,MAAM,CAAC,IAAI,IAAI;AACpE,SAAO;AACT;;;AC/CA,OAAOE,WAAU;AACjB,SAAS,kBAAkB;AAapB,IAAM,iBAAiB;AAEvB,SAAS,gBAAgB,SAAyB;AACvD,SAAOC,MAAK,KAAK,SAAS,aAAa;AACzC;AAEA,IAAM,UAAU;AAEhB,SAAS,YAAY,GAAyB;AAC5C,SAAO,MAAM,EAAE,IAAI,IAAI,EAAE,IAAI,MAAM,EAAE,EAAE,KAAK,EAAE,IAAI;AACpD;AAEO,SAAS,aAAa,OAAkB,MAA+D;AAC5G,QAAM,MAAM,SAAS,OAAO,MAAM,cAAc;AAChD,QAAM,WAAqB,CAAC;AAC5B,QAAM,UAA0B,CAAC;AACjC,aAAW,QAAQ,IAAI,MAAM,IAAI,GAAG;AAClC,UAAM,IAAI,QAAQ,KAAK,IAAI;AAC3B,QAAI,EAAG,SAAQ,KAAK,EAAE,MAAM,EAAE,CAAC,GAAI,MAAM,EAAE,CAAC,GAAI,IAAI,EAAE,CAAC,GAAI,MAAM,EAAE,CAAC,EAAG,CAAC;AACxE,aAAS,KAAK,IAAI;AAAA,EACpB;AACA,SAAO,EAAE,SAAS,SAAS;AAC7B;AAGO,SAAS,cAAc,OAAkB,MAAc,MAAc,MAAM,KAAK,IAAI,GAAiB;AAC1G,QAAM,UAAU,KAAK,KAAK;AAC1B,MAAI,CAAC,QAAS,OAAM,IAAI,MAAM,2BAA2B;AACzD,QAAM,IAAI,IAAI,KAAK,GAAG;AACtB,QAAM,MAAM,CAAC,MAAc,OAAO,CAAC,EAAE,SAAS,GAAG,GAAG;AACpD,QAAM,QAAsB;AAAA,IAC1B,IAAI,WAAW,EAAE,MAAM,GAAG,CAAC;AAAA,IAC3B,MAAM,GAAG,EAAE,YAAY,CAAC,IAAI,IAAI,EAAE,SAAS,IAAI,CAAC,CAAC,IAAI,IAAI,EAAE,QAAQ,CAAC,CAAC;AAAA,IACrE,MAAM,GAAG,IAAI,EAAE,SAAS,CAAC,CAAC,IAAI,IAAI,EAAE,WAAW,CAAC,CAAC;AAAA,IACjD,MAAM,QAAQ,QAAQ,UAAU,GAAG;AAAA,EACrC;AACA,QAAM,EAAE,SAAS,IAAI,aAAa,OAAO,IAAI;AAC7C,WAAS,KAAK,YAAY,KAAK,CAAC;AAChC,YAAU,OAAO,MAAM,SAAS,KAAK,IAAI,EAAE,QAAQ,QAAQ,IAAI,CAAC;AAChE,SAAO;AACT;AAIO,SAAS,cAAc,OAAkB,MAAc,KAAkC;AAC9F,QAAM,EAAE,UAAU,QAAQ,IAAI,aAAa,OAAO,IAAI;AACtD,QAAM,SAAS,QAAQ,KAAK,CAAC,MAAM,EAAE,OAAO,OAAO,EAAE,KAAK,SAAS,GAAG,CAAC;AACvE,MAAI,CAAC,OAAQ,QAAO;AACpB,QAAM,MAAM,SAAS,OAAO,CAAC,SAAS,CAAC,KAAK,WAAW,MAAM,OAAO,IAAI,IAAI,OAAO,IAAI,MAAM,OAAO,EAAE,IAAI,CAAC;AAC3G,YAAU,OAAO,MAAM,IAAI,KAAK,IAAI,EAAE,QAAQ,QAAQ,IAAI,CAAC;AAC3D,SAAO;AACT;AAGO,SAAS,eAAe,OAAkB,MAAc,OAA+B;AAC5F,QAAM,EAAE,QAAQ,IAAI,aAAa,OAAO,IAAI;AAC5C,SAAO,QAAQ,IAAI,QAAQ,MAAM,CAAC,KAAK,IAAI,CAAC;AAC9C;;;ACzDA,OAAOC,SAAQ;AACf,OAAO,QAAQ;AACf,OAAOC,WAAU;AACjB,SAAS,qBAAqB;AAEvB,IAAM,mBAAmB;AACzB,IAAM,gBAAgB;AAEtB,IAAM,oBAAoB;AAiC1B,SAAS,iBACd,WACA,KAAa,mBACO;AACpB,MAAI;AACJ,MAAI;AACF,UAAMA,MAAK,QAAQ,cAAc,SAAS,CAAC;AAAA,EAC7C,QAAQ;AACN,WAAO;AAAA,EACT;AACA,WAAS,QAAQ,GAAG,QAAQ,GAAG,SAAS,GAAG;AACzC,UAAM,YAAYA,MAAK,KAAK,KAAK,UAAU,WAAW,EAAE;AACxD,QAAID,IAAG,WAAWC,MAAK,KAAK,WAAW,gBAAgB,CAAC,EAAG,QAAO;AAClE,UAAM,SAASA,MAAK,QAAQ,GAAG;AAC/B,QAAI,WAAW,IAAK;AACpB,UAAM;AAAA,EACR;AACA,SAAO;AACT;AAGO,SAAS,eAAe,OAAuD;AACpF,QAAM,QAAQ,OAAO;AAAA,IACnB,CAAC,SAAS,MAAM,UAAU,UAAU,OAAO,KAAK,SAAS,YAAY,KAAK,KAAK,SAAS;AAAA,EAC1F;AACA,SAAO,OAAO,SAAS,SAAY,SAAYA,MAAK,QAAQ,MAAM,IAAI;AACxE;AAOO,SAAS,2BACd,MAAyB,QAAQ,KACjC,OAAe,GAAG,QAAQ,GAClB;AACR,QAAM,WAAW,IAAI,UAAU,KAAK;AACpC,QAAM,OAAO,YAAY,SAAS,SAAS,IAAI,WAAWA,MAAK,KAAK,MAAM,MAAM;AAChF,SAAOA,MAAK,KAAK,MAAM,gBAAgB;AACzC;AAqBO,SAAS,qBAAqB,SAAoD;AACvF,QAAM,KAAK,QAAQ,MAAM,QAAQ,GAAG,SAAS,IAAI,QAAQ,KAAK;AAC9D,MAAI,QAAQ,YAAY,OAAO;AAC7B,WAAO,EAAE,QAAQ,oBAAoB,IAAI,QAAQ,sBAAsB;AAAA,EACzE;AACA,QAAM,aAAa,iBAAiB,QAAQ,WAAW,iBAAiB;AACxE,MAAI,OAAO,mBAAmB;AAC5B,WAAO;AAAA,MACL,QAAQ;AAAA,MACR;AAAA,MACA,GAAI,eAAe,SAAY,CAAC,IAAI,EAAE,WAAW;AAAA,MACjD,QAAQ,SAAS,iBAAiB,6BAA6B,EAAE;AAAA,IACnE;AAAA,EACF;AACA,MAAI,eAAe,QAAW;AAC5B,WAAO;AAAA,MACL,QAAQ;AAAA,MACR;AAAA,MACA,QAAQ;AAAA,IACV;AAAA,EACF;AACA,QAAM,OACJ,QAAQ,SAAS,QAAQ,cAAc,SAAY,2BAA2B;AAChF,MAAI,SAAS,QAAW;AACtB,WAAO;AAAA,MACL,QAAQ;AAAA,MACR;AAAA,MACA;AAAA,MACA,QAAQ;AAAA,IACV;AAAA,EACF;AACA,QAAM,MAAMA,MAAK,KAAK,MAAM,EAAE;AAC9B,QAAM,cAAcA,MAAK,KAAK,KAAK,gBAAgB;AACnD,MAAI;AACF,QAAID,IAAG,WAAW,WAAW,GAAG;AAC9B,UAAI,QAAQ,UAAU,MAAM;AAC1B,cAAM,UAAU,CAAC,UAAU,aAAaC,MAAK,KAAK,YAAY,gBAAgB,CAAC;AAC/E,eAAO;AAAA,UACL,QAAQ;AAAA,UACR;AAAA,UACA;AAAA,UACA;AAAA,UACA,QAAQ,UAAU,mDAAmD;AAAA,QACvE;AAAA,MACF;AACA,MAAAD,IAAG,aAAaC,MAAK,KAAK,YAAY,gBAAgB,GAAG,WAAW;AACpE,aAAO;AAAA,QACL,QAAQ;AAAA,QACR;AAAA,QACA;AAAA,QACA;AAAA,QACA,QAAQ;AAAA,MACV;AAAA,IACF;AACA,UAAM,UAAUD,IAAG,WAAW,GAAG;AACjC,IAAAA,IAAG,UAAU,KAAK,EAAE,WAAW,KAAK,CAAC;AACrC,IAAAA,IAAG,aAAaC,MAAK,KAAK,YAAY,gBAAgB,GAAG,WAAW;AACpE,UAAM,WAAWA,MAAK,KAAK,KAAK,aAAa;AAE7C,QAAI,CAACD,IAAG,WAAW,QAAQ,EAAG,CAAAA,IAAG,aAAaC,MAAK,KAAK,YAAY,aAAa,GAAG,QAAQ;AAC5F,WAAO;AAAA,MACL,QAAQ,UAAU,aAAa;AAAA,MAC/B;AAAA,MACA;AAAA,MACA;AAAA,MACA,QAAQ,UACJ,sFACA;AAAA,IACN;AAAA,EACF,SAAS,OAAO;AACd,WAAO,EAAE,QAAQ,SAAS,IAAI,KAAK,YAAY,QAAQ,OAAO,KAAK,EAAE,MAAM,GAAG,GAAG,EAAE;AAAA,EACrF;AACF;AAGO,SAAS,gBAAgB,QAAqC;AACnE,QAAM,QAAQ,OAAO,QAAQ,SAAY,KAAK,KAAK,OAAO,GAAG;AAC7D,QAAM,MAAM,OAAO,WAAW,SAAY,KAAK,WAAM,OAAO,MAAM;AAClE,SAAO,UAAU,OAAO,EAAE,IAAI,OAAO,MAAM,GAAG,KAAK,GAAG,GAAG;AAC3D;AAYO,SAAS,aACd,WACA,KAAa,mBACb,OAAe,2BAA2B,GAC5B;AACd,QAAM,MAAMA,MAAK,KAAK,MAAM,EAAE;AAC9B,QAAM,aAAa,iBAAiB,WAAW,EAAE;AACjD,QAAM,YAAYD,IAAG,WAAWC,MAAK,KAAK,KAAK,gBAAgB,CAAC;AAChE,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA,GAAI,eAAe,SAAY,CAAC,IAAI,EAAE,WAAW;AAAA,IACjD;AAAA,IACA,oBACE,aAAa,eAAe,UAAa,UAAUA,MAAK,KAAK,KAAK,gBAAgB,GAAGA,MAAK,KAAK,YAAY,gBAAgB,CAAC;AAAA,IAC9H,iBACE,eAAe,UACfD,IAAG,WAAWC,MAAK,KAAK,KAAK,aAAa,CAAC,KAC3C,UAAUA,MAAK,KAAK,KAAK,aAAa,GAAGA,MAAK,KAAK,YAAY,aAAa,CAAC;AAAA,EACjF;AACF;AAEA,SAAS,UAAU,MAAc,OAAwB;AACvD,MAAI;AACF,WAAOD,IAAG,aAAa,IAAI,EAAE,OAAOA,IAAG,aAAa,KAAK,CAAC;AAAA,EAC5D,QAAQ;AACN,WAAO;AAAA,EACT;AACF;","names":["fs","path","path","fs","path","path","fs","path"]}