@tekmidian/pai 0.66.1 → 0.66.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{aibroker-client-B8c42Lh8.mjs → aibroker-client-CHEEvJZW.mjs} +2 -2
- package/dist/{aibroker-client-C5Fw7DNz.mjs → aibroker-client-Dfv7j1Od.mjs} +3 -3
- package/dist/{aibroker-client-C5Fw7DNz.mjs.map → aibroker-client-Dfv7j1Od.mjs.map} +1 -1
- package/dist/{auto-route-o0BOsXn7.mjs → auto-route-BnizyALK.mjs} +65 -4
- package/dist/auto-route-BnizyALK.mjs.map +1 -0
- package/dist/auto-route-D6fW1Q8z.mjs +3 -0
- package/dist/{chain-CZfKIEq9.mjs → chain-D5876UQX.mjs} +3 -4
- package/dist/{chain-CZfKIEq9.mjs.map → chain-D5876UQX.mjs.map} +1 -1
- package/dist/cli/index.mjs +18 -23
- package/dist/cli/index.mjs.map +1 -1
- package/dist/cli/program.mjs +17 -22
- package/dist/{config-BbLFD7Uf.mjs → config-C-LiGlop.mjs} +2 -2
- package/dist/{config-BbLFD7Uf.mjs.map → config-C-LiGlop.mjs.map} +1 -1
- package/dist/{config-YinjgXEJ.mjs → config-DKIA7wgF.mjs} +1 -1
- package/dist/{context-handover-cache-pkHzmL3c.mjs → context-handover-cache-9PGvXRIw.mjs} +984 -10
- package/dist/context-handover-cache-9PGvXRIw.mjs.map +1 -0
- package/dist/daemon/index.mjs +17 -19
- package/dist/daemon/index.mjs.map +1 -1
- package/dist/daemon-BUgNvMUh.mjs +5029 -0
- package/dist/daemon-BUgNvMUh.mjs.map +1 -0
- package/dist/daemon-DL0eglMJ.mjs +18 -0
- package/dist/daemon-mcp/index.mjs +12 -9
- package/dist/daemon-mcp/index.mjs.map +1 -1
- package/dist/{embeddings-CcscYWwk.mjs → embeddings-CEBGrzwu.mjs} +1 -1
- package/dist/{embeddings-Bx3q0QOY.mjs → embeddings-DOLZnT1X.mjs} +1 -1
- package/dist/{embeddings-Bx3q0QOY.mjs.map → embeddings-DOLZnT1X.mjs.map} +1 -1
- package/dist/{env-JNEIrQWg.mjs → env-DiolswKQ.mjs} +1 -1
- package/dist/{env-JNEIrQWg.mjs.map → env-DiolswKQ.mjs.map} +1 -1
- package/dist/factory-DrBg24UT.mjs +8 -0
- package/dist/factory-Ypf8r0fK.mjs +2892 -0
- package/dist/factory-Ypf8r0fK.mjs.map +1 -0
- package/dist/{fallback-CIiq2Raw.mjs → fallback-D51R4shY.mjs} +23 -11
- package/dist/fallback-D51R4shY.mjs.map +1 -0
- package/dist/{chunker-BH4i-F2b.mjs → helpers-CZsi_49C.mjs} +221 -2
- package/dist/helpers-CZsi_49C.mjs.map +1 -0
- package/dist/hooks/worker-status-line.mjs.map +2 -2
- package/dist/index.mjs +6 -7
- package/dist/{ipc-client-D16Xw6Uo.mjs → ipc-client-BLYX51nG.mjs} +2 -2
- package/dist/{ipc-client-D16Xw6Uo.mjs.map → ipc-client-BLYX51nG.mjs.map} +1 -1
- package/dist/main-resolver-BYgP86C1.mjs +7 -0
- package/dist/{main-resolver-DPPwHNtn.mjs → main-resolver-kVkMJZUD.mjs} +6 -6
- package/dist/{main-resolver-DPPwHNtn.mjs.map → main-resolver-kVkMJZUD.mjs.map} +1 -1
- package/dist/{migrate-BD7D8EEh.mjs → migrate-Bq0esMOa.mjs} +2 -2
- package/dist/{migrate-BD7D8EEh.mjs.map → migrate-Bq0esMOa.mjs.map} +1 -1
- package/dist/{pai-marker-D1MMswkz.mjs → pai-marker-DXVpFsYz.mjs} +1 -1
- package/dist/{pai-marker-D1MMswkz.mjs.map → pai-marker-DXVpFsYz.mjs.map} +1 -1
- package/dist/{planner-Dm5Acyo1.mjs → planner-D5yt0EfA.mjs} +10 -7
- package/dist/{planner-Dm5Acyo1.mjs.map → planner-D5yt0EfA.mjs.map} +1 -1
- package/dist/postgres-BmLr0MUm.mjs +5 -0
- package/dist/{postgres-Ceqsa64C.mjs → postgres-D3xc2RB4.mjs} +364 -9
- package/dist/postgres-D3xc2RB4.mjs.map +1 -0
- package/dist/{program-JJ0TwSHI.mjs → program-DIXRtAFy.mjs} +77 -70
- package/dist/program-DIXRtAFy.mjs.map +1 -0
- package/dist/{query-feedback-BIaZTTFO.mjs → query-feedback-B7FYE4JR.mjs} +1 -1
- package/dist/{query-feedback-BIaZTTFO.mjs.map → query-feedback-B7FYE4JR.mjs.map} +1 -1
- package/dist/{reranker-DKv80KO5.mjs → reranker-3lnggwgq.mjs} +1 -1
- package/dist/{reranker-DKv80KO5.mjs.map → reranker-3lnggwgq.mjs.map} +1 -1
- package/dist/{reranker-CFiEzHQu.mjs → reranker-CZ2mP4cf.mjs} +1 -1
- package/dist/router-BK-hFeQ6.mjs +3 -0
- package/dist/{router-a5G7q8_3.mjs → router-BaTbc9VX.mjs} +2 -2
- package/dist/{router-a5G7q8_3.mjs.map → router-BaTbc9VX.mjs.map} +1 -1
- package/dist/{run-BCpbxqA5.mjs → run-CLYTOm0x.mjs} +599 -22
- package/dist/run-CLYTOm0x.mjs.map +1 -0
- package/dist/{run-env-DRcp7A8K.mjs → run-env-Bnmf6bRI.mjs} +2 -2
- package/dist/{run-env-DRcp7A8K.mjs.map → run-env-Bnmf6bRI.mjs.map} +1 -1
- package/dist/{runtime-paths-CHTg3ywb.mjs → runtime-paths-D3FKAm69.mjs} +1 -1
- package/dist/{runtime-paths-CHTg3ywb.mjs.map → runtime-paths-D3FKAm69.mjs.map} +1 -1
- package/dist/{search-BOSphCJ1.mjs → search-CNAGTiJP.mjs} +2 -2
- package/dist/{search-BOSphCJ1.mjs.map → search-CNAGTiJP.mjs.map} +1 -1
- package/dist/{sources-CijVso3n.mjs → sources-CM2g-CLT.mjs} +2 -2
- package/dist/{sources-CijVso3n.mjs.map → sources-CM2g-CLT.mjs.map} +1 -1
- package/dist/{stop-words-BdQuaE9K.mjs → stop-words-DtxaTWU_.mjs} +1 -1
- package/dist/{stop-words-BdQuaE9K.mjs.map → stop-words-DtxaTWU_.mjs.map} +1 -1
- package/dist/{utils-C9HsYDpO.mjs → utils-DddRwMsG.mjs} +2 -2
- package/dist/{utils-C9HsYDpO.mjs.map → utils-DddRwMsG.mjs.map} +1 -1
- package/dist/{zettelkasten-CtGHQWPU.mjs → zettelkasten-BhZMvmoK.mjs} +149 -7
- package/dist/zettelkasten-BhZMvmoK.mjs.map +1 -0
- package/dist/zettelkasten-BwV1pluL.mjs +5 -0
- package/docs/commands/worker.md +2 -0
- package/package.json +1 -1
- package/dist/async-C2Bm_Lal.mjs +0 -300
- package/dist/async-C2Bm_Lal.mjs.map +0 -1
- package/dist/auto-route-o0BOsXn7.mjs.map +0 -1
- package/dist/chunker-BH4i-F2b.mjs.map +0 -1
- package/dist/clusters-BCtD3fbe.mjs +0 -169
- package/dist/clusters-BCtD3fbe.mjs.map +0 -1
- package/dist/context-handover-cache-pkHzmL3c.mjs.map +0 -1
- package/dist/daemon-DXlaVCkw.mjs +0 -1741
- package/dist/daemon-DXlaVCkw.mjs.map +0 -1
- package/dist/daemon-DyORmxZu.mjs +0 -19
- package/dist/detector-BEPFsINR.mjs +0 -3
- package/dist/detector-BRTtAWrM.mjs +0 -65
- package/dist/detector-BRTtAWrM.mjs.map +0 -1
- package/dist/factory-CNpPQQ-5.mjs +0 -249
- package/dist/factory-CNpPQQ-5.mjs.map +0 -1
- package/dist/factory-DY7x73mE.mjs +0 -3
- package/dist/fallback-CIiq2Raw.mjs.map +0 -1
- package/dist/federation-db-BTyoufBh.mjs +0 -139
- package/dist/federation-db-BTyoufBh.mjs.map +0 -1
- package/dist/federation-db-HfIFc7FG.mjs +0 -3
- package/dist/helpers-BRCJg0G3.mjs +0 -223
- package/dist/helpers-BRCJg0G3.mjs.map +0 -1
- package/dist/indexer-backend-DFF2FrYx.mjs +0 -5
- package/dist/indexer-backend-isSLg6yE.mjs +0 -1
- package/dist/kg-entity-DCOcsVFD.mjs +0 -29
- package/dist/kg-entity-DCOcsVFD.mjs.map +0 -1
- package/dist/latent-ideas-wwAXquGe.mjs +0 -191
- package/dist/latent-ideas-wwAXquGe.mjs.map +0 -1
- package/dist/link-boost-CnI7UVMJ.mjs +0 -34
- package/dist/link-boost-CnI7UVMJ.mjs.map +0 -1
- package/dist/main-resolver-BKz_OuEh.mjs +0 -7
- package/dist/merge-2gqRPFu2.mjs +0 -3
- package/dist/merge-DgU9OgZy.mjs +0 -6
- package/dist/merge-DgU9OgZy.mjs.map +0 -1
- package/dist/module-paths-DdRzbkUI.mjs +0 -44
- package/dist/module-paths-DdRzbkUI.mjs.map +0 -1
- package/dist/neighborhood-2FsmzoxG.mjs +0 -114
- package/dist/neighborhood-2FsmzoxG.mjs.map +0 -1
- package/dist/note-context-De63k8na.mjs +0 -106
- package/dist/note-context-De63k8na.mjs.map +0 -1
- package/dist/postgres-Ceqsa64C.mjs.map +0 -1
- package/dist/program-JJ0TwSHI.mjs.map +0 -1
- package/dist/query-feedback-B_iigYj-.mjs +0 -3
- package/dist/registry-db-C7voqML9.mjs +0 -213
- package/dist/registry-db-C7voqML9.mjs.map +0 -1
- package/dist/registry-db-JHPhA8vF.mjs +0 -3
- package/dist/registry-postgres-nOfyBSIW.mjs +0 -795
- package/dist/registry-postgres-nOfyBSIW.mjs.map +0 -1
- package/dist/registry-sqlite-DrCW1aRK.mjs +0 -593
- package/dist/registry-sqlite-DrCW1aRK.mjs.map +0 -1
- package/dist/router-CXUGsv85.mjs +0 -3
- package/dist/run-BCpbxqA5.mjs.map +0 -1
- package/dist/search-Bf3Kub3F.mjs +0 -3
- package/dist/server-DgmAHyFK.mjs +0 -403
- package/dist/server-DgmAHyFK.mjs.map +0 -1
- package/dist/session-keepalive-DxjTHGcK.mjs +0 -582
- package/dist/session-keepalive-DxjTHGcK.mjs.map +0 -1
- package/dist/sqlite-CqsTy6xo.mjs +0 -936
- package/dist/sqlite-CqsTy6xo.mjs.map +0 -1
- package/dist/state-DW8zdweW.mjs +0 -3
- package/dist/state-HyjqTihC.mjs +0 -76
- package/dist/state-HyjqTihC.mjs.map +0 -1
- package/dist/themes-CTaOj3e1.mjs +0 -148
- package/dist/themes-CTaOj3e1.mjs.map +0 -1
- package/dist/tools-BcOKPBtg.mjs +0 -1489
- package/dist/tools-BcOKPBtg.mjs.map +0 -1
- package/dist/tools-C-_l0o8Z.mjs +0 -6
- package/dist/trace-CtV0RO6n.mjs +0 -137
- package/dist/trace-CtV0RO6n.mjs.map +0 -1
- package/dist/utils-BrX9FLeK.mjs +0 -3
- package/dist/vault-indexer-BG1sEVyM.mjs +0 -537
- package/dist/vault-indexer-BG1sEVyM.mjs.map +0 -1
- package/dist/wakeup-D8n33hYV.mjs +0 -335
- package/dist/wakeup-D8n33hYV.mjs.map +0 -1
- package/dist/work-queue-worker-4tLa5gpQ.mjs +0 -567
- package/dist/work-queue-worker-4tLa5gpQ.mjs.map +0 -1
- package/dist/work-queue-worker-rFZvTO3p.mjs +0 -11
- package/dist/zettelkasten-CtGHQWPU.mjs.map +0 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { h as smartDecodeDir } from "./utils-DddRwMsG.mjs";
|
|
2
2
|
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
@@ -212,4 +212,4 @@ async function migrateFromJson(backend, registryPath = join(homedir(), ".claude"
|
|
|
212
212
|
|
|
213
213
|
//#endregion
|
|
214
214
|
export { slugify as a, parseSessionFilename as i, decodeEncodedDir as n, migrateFromJson as r, buildEncodedDirMap as t };
|
|
215
|
-
//# sourceMappingURL=migrate-
|
|
215
|
+
//# sourceMappingURL=migrate-Bq0esMOa.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"migrate-BD7D8EEh.mjs","names":[],"sources":["../src/registry/migrate.ts"],"sourcesContent":["/**\n * Migration helper: imports the existing JSON session-registry into the\n * registry backend (SQLite or Postgres).\n *\n * Source file: ~/.claude/session-registry.json\n * Target: RegistryBackend → projects + sessions tables\n *\n * The JSON registry uses encoded directory names as keys (Claude Code's\n * encoding: leading `/` is replaced by `-`, then each remaining `/` is also\n * replaced by `-`). This module reverses that encoding to recover the real\n * filesystem path.\n *\n * Session note filenames are expected in one of two formats:\n * Modern: \"NNNN - YYYY-MM-DD - Description.md\" (space-dash-space)\n * Legacy: \"NNNN_YYYY-MM-DD_description.md\" (underscores)\n */\n\nimport { existsSync, readdirSync, readFileSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { join } from \"node:path\";\nimport type { RegistryBackend } from \"../storage/registry-interface.js\";\nimport { smartDecodeDir } from \"../cli/utils.js\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\n/** Shape of a single entry in session-registry.json */\ninterface RegistryEntry {\n /** Absolute path to the Notes/ directory for this project */\n notesDir?: string;\n /** Display name stored in the registry (optional) */\n displayName?: string;\n /** Any other keys the file might carry */\n [key: string]: unknown;\n}\n\n/** Top-level shape of session-registry.json */\ntype SessionRegistry = Record<string, RegistryEntry>;\n\n// ---------------------------------------------------------------------------\n// Encoding / decoding\n// ---------------------------------------------------------------------------\n\n/**\n * Build a lookup table from session-registry.json mapping encoded_dir →\n * original_path. This is the authoritative source for decoding because the\n * encoding is ambiguous: `/`, ` ` (space), `.` (dot), and `-` (literal\n * hyphen) all map to `-` or `--` in ways that cannot be uniquely reversed.\n *\n * Example:\n * `-Users-alice--ssh` encodes `/Users/alice/.ssh`\n * `-Users-alice-dev-projects-04---My-App-My-App-2020---2029`\n * encodes `/Users/alice/dev/projects/04 - My-App/My-App 2020 - 2029`\n *\n * @param jsonPath Path to session-registry.json.\n * Defaults to ~/.claude/session-registry.json.\n * @returns Map from encoded_dir → original_path, or empty map if the file is\n * missing / unparseable.\n */\nexport function buildEncodedDirMap(\n jsonPath: string = join(homedir(), \".claude\", \"session-registry.json\")\n): Map<string, string> {\n const map = new Map<string, string>();\n if (!existsSync(jsonPath)) return map;\n\n try {\n const raw = readFileSync(jsonPath, \"utf8\");\n const parsed = JSON.parse(raw) as Record<string, unknown>;\n\n // Support both formats:\n // list-based: { \"projects\": [ { \"encoded_dir\", \"original_path\" }, ... ] }\n // object-keyed: { \"<encoded_dir>\": { ... } } (original Claude format)\n if (Array.isArray(parsed.projects)) {\n for (const entry of parsed.projects as Array<Record<string, unknown>>) {\n const key = entry.encoded_dir as string | undefined;\n const val = entry.original_path as string | undefined;\n if (key && val) map.set(key, val);\n }\n } else {\n // Object-keyed format — keys are encoded dirs\n for (const [key, value] of Object.entries(parsed)) {\n if (key === \"version\") continue;\n const val = (value as Record<string, unknown>)?.original_path as\n | string\n | undefined;\n if (val) map.set(key, val);\n }\n }\n } catch {\n // Unparseable — return empty map; callers fall back to heuristic decode\n }\n\n return map;\n}\n\n/**\n * Reverse Claude Code's directory encoding.\n *\n * Claude Code's actual encoding rules:\n * - `/` (path separator) → `-`\n * - ` ` (space) → `--` (escaped)\n * - `.` (dot) → `--` (escaped)\n * - `-` (literal hyphen) → `--` (escaped)\n *\n * Because space, dot, and hyphen all encode to `--`, the encoding is\n * **lossy** — you cannot unambiguously reverse it. This function therefore\n * provides a *best-effort* heuristic decode (treating `--` as a literal `-`\n * which gives wrong results for paths with spaces or dots).\n *\n * PREFER using {@link buildEncodedDirMap} to get the authoritative mapping\n * from session-registry.json instead of calling this function directly.\n *\n * Examples (best-effort, may be wrong for paths with spaces/dots):\n * `-Users-alice-dev-apps-MyProject` → `/Users/alice/dev/apps/MyProject`\n * `-Users-alice--ssh` → `/Users/alice/-ssh` ← WRONG (actually .ssh)\n *\n * @param encoded The Claude-encoded directory name.\n * @param lookupMap Optional authoritative map from {@link buildEncodedDirMap}.\n * If provided and the key is found, that value is returned\n * instead of the heuristic result.\n */\nexport function decodeEncodedDir(\n encoded: string,\n lookupMap?: Map<string, string>\n): string {\n // Authoritative lookup wins — but only if the path actually exists on disk.\n // session-registry.json may contain stale or incorrectly decoded paths.\n if (lookupMap?.has(encoded)) {\n const mapped = lookupMap.get(encoded)!;\n if (existsSync(mapped)) return mapped;\n }\n\n // Filesystem-walking decode (handles spaces, dots, hyphens correctly)\n const smart = smartDecodeDir(encoded);\n if (smart) return smart;\n\n // Fall back to lookup map even if path doesn't exist (for display purposes)\n if (lookupMap?.has(encoded)) {\n return lookupMap.get(encoded)!;\n }\n\n // Last resort: every `-` maps to `/` (wrong for paths with spaces/dots/hyphens)\n if (encoded.startsWith(\"-\")) {\n return encoded.replace(/-/g, \"/\");\n }\n\n // Not a Claude-encoded path — return as-is\n return encoded;\n}\n\n// ---------------------------------------------------------------------------\n// Slug generation\n// ---------------------------------------------------------------------------\n\n/**\n * Derive a URL-safe kebab-case slug from an arbitrary string.\n *\n * Uses the last path component so that `/Users/alice/dev/my-app` → `my-app`.\n */\nexport function slugify(value: string): string {\n // Take last path segment if it looks like a path\n const segment = value.includes(\"/\")\n ? value.replace(/\\/$/, \"\").split(\"/\").pop() ?? value\n : value;\n\n return segment\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, \"-\") // non-alphanumeric runs → single dash\n .replace(/^-+|-+$/g, \"\"); // trim leading/trailing dashes\n}\n\n// ---------------------------------------------------------------------------\n// Session note parsing\n// ---------------------------------------------------------------------------\n\ninterface ParsedSession {\n number: number;\n date: string;\n slug: string;\n title: string;\n filename: string;\n}\n\n/** Match `0027 - 2026-01-04 - Some Description.md` */\nconst MODERN_RE = /^(\\d{4})\\s+-\\s+(\\d{4}-\\d{2}-\\d{2})\\s+-\\s+(.+)\\.md$/i;\n\n/** Match `0027_2026-01-04_some_description.md` */\nconst LEGACY_RE = /^(\\d{4})_(\\d{4}-\\d{2}-\\d{2})_(.+)\\.md$/i;\n\n/**\n * Attempt to parse a session note filename into its structured parts.\n *\n * Returns `null` if the filename does not match either known format.\n */\nexport function parseSessionFilename(\n filename: string\n): ParsedSession | null {\n let m = MODERN_RE.exec(filename);\n if (m) {\n const [, num, date, description] = m;\n return {\n number: parseInt(num, 10),\n date,\n slug: slugify(description),\n title: description.trim(),\n filename,\n };\n }\n\n m = LEGACY_RE.exec(filename);\n if (m) {\n const [, num, date, rawDesc] = m;\n const description = rawDesc.replace(/_/g, \" \");\n return {\n number: parseInt(num, 10),\n date,\n slug: slugify(description),\n title: description.trim(),\n filename,\n };\n }\n\n return null;\n}\n\n// ---------------------------------------------------------------------------\n// Migration\n// ---------------------------------------------------------------------------\n\nexport interface MigrationResult {\n projectsInserted: number;\n projectsSkipped: number;\n sessionsInserted: number;\n errors: string[];\n}\n\n/**\n * Migrate the existing JSON session-registry into the registry backend.\n *\n * @param backend Target RegistryBackend (SQLite or Postgres).\n * @param registryPath Path to session-registry.json.\n * Defaults to ~/.claude/session-registry.json.\n *\n * The migration is idempotent: projects and sessions that already exist\n * (matched by slug / project_id+number) are silently skipped.\n */\nexport async function migrateFromJson(\n backend: RegistryBackend,\n registryPath: string = join(homedir(), \".claude\", \"session-registry.json\")\n): Promise<MigrationResult> {\n const result: MigrationResult = {\n projectsInserted: 0,\n projectsSkipped: 0,\n sessionsInserted: 0,\n errors: [],\n };\n\n // ── Load source file ──────────────────────────────────────────────────────\n if (!existsSync(registryPath)) {\n result.errors.push(`Registry file not found: ${registryPath}`);\n return result;\n }\n\n let registry: SessionRegistry;\n try {\n const raw = readFileSync(registryPath, \"utf8\");\n registry = JSON.parse(raw) as SessionRegistry;\n } catch (err) {\n result.errors.push(`Failed to parse registry JSON: ${String(err)}`);\n return result;\n }\n\n const now = Date.now();\n\n // ── Build authoritative encoded-dir → path lookup ─────────────────────────\n const lookupMap = buildEncodedDirMap(registryPath);\n\n // ── Process each encoded directory entry ──────────────────────────────────\n for (const [encodedDir, entry] of Object.entries(registry)) {\n const rootPath = decodeEncodedDir(encodedDir, lookupMap);\n const baseSlug = slugify(rootPath);\n\n // --- Upsert project ---\n const { id: projectId, created } = await backend.createProjectWithSlugRetry({\n baseSlug,\n displayName:\n (entry.displayName as string | undefined) ??\n (rootPath.split(\"/\").pop() ?? rootPath),\n rootPath,\n encodedDir,\n createdAt: now,\n updatedAt: now,\n });\n if (created) result.projectsInserted++;\n else result.projectsSkipped++;\n\n // --- Scan Notes/ directory for session notes ---\n const notesDir =\n typeof entry.notesDir === \"string\"\n ? entry.notesDir\n : join(rootPath, \"Notes\");\n\n if (!existsSync(notesDir)) {\n // No notes directory — that is fine, project still gets created\n continue;\n }\n\n let files: string[];\n try {\n files = readdirSync(notesDir);\n } catch (err) {\n result.errors.push(\n `Cannot read notes dir ${notesDir}: ${String(err)}`\n );\n continue;\n }\n\n for (const filename of files) {\n if (!filename.endsWith(\".md\")) continue;\n\n const parsed = parseSessionFilename(filename);\n if (!parsed) continue;\n\n try {\n const inserted = await backend.upsertSessionIfAbsent({\n projectId,\n number: parsed.number,\n date: parsed.date,\n slug: parsed.slug,\n title: parsed.title,\n filename: parsed.filename,\n createdAt: now,\n });\n if (inserted) result.sessionsInserted++;\n } catch (err) {\n result.errors.push(\n `Failed to insert session ${filename}: ${String(err)}`\n );\n }\n }\n }\n\n return result;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"migrate-Bq0esMOa.mjs","names":[],"sources":["../src/registry/migrate.ts"],"sourcesContent":["/**\n * Migration helper: imports the existing JSON session-registry into the\n * registry backend (SQLite or Postgres).\n *\n * Source file: ~/.claude/session-registry.json\n * Target: RegistryBackend → projects + sessions tables\n *\n * The JSON registry uses encoded directory names as keys (Claude Code's\n * encoding: leading `/` is replaced by `-`, then each remaining `/` is also\n * replaced by `-`). This module reverses that encoding to recover the real\n * filesystem path.\n *\n * Session note filenames are expected in one of two formats:\n * Modern: \"NNNN - YYYY-MM-DD - Description.md\" (space-dash-space)\n * Legacy: \"NNNN_YYYY-MM-DD_description.md\" (underscores)\n */\n\nimport { existsSync, readdirSync, readFileSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { join } from \"node:path\";\nimport type { RegistryBackend } from \"../storage/registry-interface.js\";\nimport { smartDecodeDir } from \"../cli/utils.js\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\n/** Shape of a single entry in session-registry.json */\ninterface RegistryEntry {\n /** Absolute path to the Notes/ directory for this project */\n notesDir?: string;\n /** Display name stored in the registry (optional) */\n displayName?: string;\n /** Any other keys the file might carry */\n [key: string]: unknown;\n}\n\n/** Top-level shape of session-registry.json */\ntype SessionRegistry = Record<string, RegistryEntry>;\n\n// ---------------------------------------------------------------------------\n// Encoding / decoding\n// ---------------------------------------------------------------------------\n\n/**\n * Build a lookup table from session-registry.json mapping encoded_dir →\n * original_path. This is the authoritative source for decoding because the\n * encoding is ambiguous: `/`, ` ` (space), `.` (dot), and `-` (literal\n * hyphen) all map to `-` or `--` in ways that cannot be uniquely reversed.\n *\n * Example:\n * `-Users-alice--ssh` encodes `/Users/alice/.ssh`\n * `-Users-alice-dev-projects-04---My-App-My-App-2020---2029`\n * encodes `/Users/alice/dev/projects/04 - My-App/My-App 2020 - 2029`\n *\n * @param jsonPath Path to session-registry.json.\n * Defaults to ~/.claude/session-registry.json.\n * @returns Map from encoded_dir → original_path, or empty map if the file is\n * missing / unparseable.\n */\nexport function buildEncodedDirMap(\n jsonPath: string = join(homedir(), \".claude\", \"session-registry.json\")\n): Map<string, string> {\n const map = new Map<string, string>();\n if (!existsSync(jsonPath)) return map;\n\n try {\n const raw = readFileSync(jsonPath, \"utf8\");\n const parsed = JSON.parse(raw) as Record<string, unknown>;\n\n // Support both formats:\n // list-based: { \"projects\": [ { \"encoded_dir\", \"original_path\" }, ... ] }\n // object-keyed: { \"<encoded_dir>\": { ... } } (original Claude format)\n if (Array.isArray(parsed.projects)) {\n for (const entry of parsed.projects as Array<Record<string, unknown>>) {\n const key = entry.encoded_dir as string | undefined;\n const val = entry.original_path as string | undefined;\n if (key && val) map.set(key, val);\n }\n } else {\n // Object-keyed format — keys are encoded dirs\n for (const [key, value] of Object.entries(parsed)) {\n if (key === \"version\") continue;\n const val = (value as Record<string, unknown>)?.original_path as\n | string\n | undefined;\n if (val) map.set(key, val);\n }\n }\n } catch {\n // Unparseable — return empty map; callers fall back to heuristic decode\n }\n\n return map;\n}\n\n/**\n * Reverse Claude Code's directory encoding.\n *\n * Claude Code's actual encoding rules:\n * - `/` (path separator) → `-`\n * - ` ` (space) → `--` (escaped)\n * - `.` (dot) → `--` (escaped)\n * - `-` (literal hyphen) → `--` (escaped)\n *\n * Because space, dot, and hyphen all encode to `--`, the encoding is\n * **lossy** — you cannot unambiguously reverse it. This function therefore\n * provides a *best-effort* heuristic decode (treating `--` as a literal `-`\n * which gives wrong results for paths with spaces or dots).\n *\n * PREFER using {@link buildEncodedDirMap} to get the authoritative mapping\n * from session-registry.json instead of calling this function directly.\n *\n * Examples (best-effort, may be wrong for paths with spaces/dots):\n * `-Users-alice-dev-apps-MyProject` → `/Users/alice/dev/apps/MyProject`\n * `-Users-alice--ssh` → `/Users/alice/-ssh` ← WRONG (actually .ssh)\n *\n * @param encoded The Claude-encoded directory name.\n * @param lookupMap Optional authoritative map from {@link buildEncodedDirMap}.\n * If provided and the key is found, that value is returned\n * instead of the heuristic result.\n */\nexport function decodeEncodedDir(\n encoded: string,\n lookupMap?: Map<string, string>\n): string {\n // Authoritative lookup wins — but only if the path actually exists on disk.\n // session-registry.json may contain stale or incorrectly decoded paths.\n if (lookupMap?.has(encoded)) {\n const mapped = lookupMap.get(encoded)!;\n if (existsSync(mapped)) return mapped;\n }\n\n // Filesystem-walking decode (handles spaces, dots, hyphens correctly)\n const smart = smartDecodeDir(encoded);\n if (smart) return smart;\n\n // Fall back to lookup map even if path doesn't exist (for display purposes)\n if (lookupMap?.has(encoded)) {\n return lookupMap.get(encoded)!;\n }\n\n // Last resort: every `-` maps to `/` (wrong for paths with spaces/dots/hyphens)\n if (encoded.startsWith(\"-\")) {\n return encoded.replace(/-/g, \"/\");\n }\n\n // Not a Claude-encoded path — return as-is\n return encoded;\n}\n\n// ---------------------------------------------------------------------------\n// Slug generation\n// ---------------------------------------------------------------------------\n\n/**\n * Derive a URL-safe kebab-case slug from an arbitrary string.\n *\n * Uses the last path component so that `/Users/alice/dev/my-app` → `my-app`.\n */\nexport function slugify(value: string): string {\n // Take last path segment if it looks like a path\n const segment = value.includes(\"/\")\n ? value.replace(/\\/$/, \"\").split(\"/\").pop() ?? value\n : value;\n\n return segment\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, \"-\") // non-alphanumeric runs → single dash\n .replace(/^-+|-+$/g, \"\"); // trim leading/trailing dashes\n}\n\n// ---------------------------------------------------------------------------\n// Session note parsing\n// ---------------------------------------------------------------------------\n\ninterface ParsedSession {\n number: number;\n date: string;\n slug: string;\n title: string;\n filename: string;\n}\n\n/** Match `0027 - 2026-01-04 - Some Description.md` */\nconst MODERN_RE = /^(\\d{4})\\s+-\\s+(\\d{4}-\\d{2}-\\d{2})\\s+-\\s+(.+)\\.md$/i;\n\n/** Match `0027_2026-01-04_some_description.md` */\nconst LEGACY_RE = /^(\\d{4})_(\\d{4}-\\d{2}-\\d{2})_(.+)\\.md$/i;\n\n/**\n * Attempt to parse a session note filename into its structured parts.\n *\n * Returns `null` if the filename does not match either known format.\n */\nexport function parseSessionFilename(\n filename: string\n): ParsedSession | null {\n let m = MODERN_RE.exec(filename);\n if (m) {\n const [, num, date, description] = m;\n return {\n number: parseInt(num, 10),\n date,\n slug: slugify(description),\n title: description.trim(),\n filename,\n };\n }\n\n m = LEGACY_RE.exec(filename);\n if (m) {\n const [, num, date, rawDesc] = m;\n const description = rawDesc.replace(/_/g, \" \");\n return {\n number: parseInt(num, 10),\n date,\n slug: slugify(description),\n title: description.trim(),\n filename,\n };\n }\n\n return null;\n}\n\n// ---------------------------------------------------------------------------\n// Migration\n// ---------------------------------------------------------------------------\n\nexport interface MigrationResult {\n projectsInserted: number;\n projectsSkipped: number;\n sessionsInserted: number;\n errors: string[];\n}\n\n/**\n * Migrate the existing JSON session-registry into the registry backend.\n *\n * @param backend Target RegistryBackend (SQLite or Postgres).\n * @param registryPath Path to session-registry.json.\n * Defaults to ~/.claude/session-registry.json.\n *\n * The migration is idempotent: projects and sessions that already exist\n * (matched by slug / project_id+number) are silently skipped.\n */\nexport async function migrateFromJson(\n backend: RegistryBackend,\n registryPath: string = join(homedir(), \".claude\", \"session-registry.json\")\n): Promise<MigrationResult> {\n const result: MigrationResult = {\n projectsInserted: 0,\n projectsSkipped: 0,\n sessionsInserted: 0,\n errors: [],\n };\n\n // ── Load source file ──────────────────────────────────────────────────────\n if (!existsSync(registryPath)) {\n result.errors.push(`Registry file not found: ${registryPath}`);\n return result;\n }\n\n let registry: SessionRegistry;\n try {\n const raw = readFileSync(registryPath, \"utf8\");\n registry = JSON.parse(raw) as SessionRegistry;\n } catch (err) {\n result.errors.push(`Failed to parse registry JSON: ${String(err)}`);\n return result;\n }\n\n const now = Date.now();\n\n // ── Build authoritative encoded-dir → path lookup ─────────────────────────\n const lookupMap = buildEncodedDirMap(registryPath);\n\n // ── Process each encoded directory entry ──────────────────────────────────\n for (const [encodedDir, entry] of Object.entries(registry)) {\n const rootPath = decodeEncodedDir(encodedDir, lookupMap);\n const baseSlug = slugify(rootPath);\n\n // --- Upsert project ---\n const { id: projectId, created } = await backend.createProjectWithSlugRetry({\n baseSlug,\n displayName:\n (entry.displayName as string | undefined) ??\n (rootPath.split(\"/\").pop() ?? rootPath),\n rootPath,\n encodedDir,\n createdAt: now,\n updatedAt: now,\n });\n if (created) result.projectsInserted++;\n else result.projectsSkipped++;\n\n // --- Scan Notes/ directory for session notes ---\n const notesDir =\n typeof entry.notesDir === \"string\"\n ? entry.notesDir\n : join(rootPath, \"Notes\");\n\n if (!existsSync(notesDir)) {\n // No notes directory — that is fine, project still gets created\n continue;\n }\n\n let files: string[];\n try {\n files = readdirSync(notesDir);\n } catch (err) {\n result.errors.push(\n `Cannot read notes dir ${notesDir}: ${String(err)}`\n );\n continue;\n }\n\n for (const filename of files) {\n if (!filename.endsWith(\".md\")) continue;\n\n const parsed = parseSessionFilename(filename);\n if (!parsed) continue;\n\n try {\n const inserted = await backend.upsertSessionIfAbsent({\n projectId,\n number: parsed.number,\n date: parsed.date,\n slug: parsed.slug,\n title: parsed.title,\n filename: parsed.filename,\n createdAt: now,\n });\n if (inserted) result.sessionsInserted++;\n } catch (err) {\n result.errors.push(\n `Failed to insert session ${filename}: ${String(err)}`\n );\n }\n }\n }\n\n return result;\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pai-marker-D1MMswkz.mjs","names":[],"sources":["../src/registry/pai-marker.ts"],"sourcesContent":["/**\n * PAI.md marker file management.\n *\n * Each registered project gets a `Notes/PAI.md` file with a YAML frontmatter\n * `pai:` block that PAI manages. The rest of the file (body content, other\n * frontmatter keys) is user-owned and never modified by PAI.\n *\n * YAML parsing/updating is done with simple regex — no external dependency.\n */\n\nimport {\n existsSync,\n readFileSync,\n writeFileSync,\n mkdirSync,\n readdirSync,\n statSync,\n} from \"node:fs\";\nimport { join, resolve } from \"node:path\";\nimport { homedir } from \"node:os\";\n\n// ---------------------------------------------------------------------------\n// Container directories\n// ---------------------------------------------------------------------------\n\n/**\n * Directories that hold projects but are not themselves projects.\n *\n * Running a single Claude session from ~/Desktop is enough for the scanner to\n * register it as a project, after which every scan recreates a `Notes/PAI.md`\n * inside it. The user gets a Notes folder on their Desktop that reappears\n * however many times they delete it, containing a marker file that is\n * otherwise empty.\n *\n * The session is still worth tracking — it just belongs in the central notes\n * directory (~/.claude/projects/<encoded>/Notes) rather than as a folder\n * dropped into the user's Desktop.\n */\nconst CONTAINER_DIRS = [\n \"Desktop\",\n \"Documents\",\n \"Downloads\",\n \"Movies\",\n \"Music\",\n \"Pictures\",\n \"Public\",\n \"Library\",\n \"Applications\",\n];\n\n/**\n * True when a path is a place projects live rather than a project itself:\n * the home directory, a standard user container, or a system temp root.\n *\n * Only ever used to decline *creating* a marker. An existing marker is left\n * alone — the user may have put one there deliberately.\n */\nexport function isContainerDirectory(projectRoot: string): boolean {\n // Strip trailing slashes, but never strip the root itself down to \"\".\n const strip = (s: string): string => s.replace(/(?!^)\\/+$/, \"\");\n\n const p = strip(resolve(projectRoot));\n const home = strip(resolve(homedir()));\n\n if (p === home || p === \"/\") return true;\n if (p === \"/tmp\" || p === \"/private/tmp\" || p === \"/var/tmp\") return true;\n\n for (const dir of CONTAINER_DIRS) {\n if (p === join(home, dir)) return true;\n }\n\n return false;\n}\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface PaiMarker {\n /** Absolute path to the PAI.md file */\n path: string;\n /** The `slug` value from the `pai:` frontmatter block */\n slug: string;\n /** Absolute path to the project root (parent of Notes/) */\n projectRoot: string;\n}\n\n// ---------------------------------------------------------------------------\n// Template content (mirrors templates/pai-project.template.md)\n// ---------------------------------------------------------------------------\n\nconst TEMPLATE = `---\npai:\n slug: \"\\${SLUG}\"\n registered: \"\\${DATE}\"\n last_indexed: null\n status: active\n---\n\n# \\${DISPLAY_NAME}\n\n<!-- Everything below the YAML frontmatter is yours — PAI never modifies content here. -->\n<!-- Use this file for project notes, decisions, preferences, or anything you want. -->\n<!-- PAI only reads and updates the \\`pai:\\` block in the frontmatter above. -->\n`;\n\nfunction isoDate(): string {\n return new Date().toISOString().slice(0, 10);\n}\n\nfunction renderTemplate(slug: string, displayName: string): string {\n return TEMPLATE.replace(/\\$\\{SLUG\\}/g, slug)\n .replace(/\\$\\{DATE\\}/g, isoDate())\n .replace(/\\$\\{DISPLAY_NAME\\}/g, displayName);\n}\n\n// ---------------------------------------------------------------------------\n// YAML frontmatter helpers (regex-based, no external dependency)\n// ---------------------------------------------------------------------------\n\n/**\n * Split a markdown file with YAML frontmatter into its parts.\n *\n * Returns { frontmatter, body } where:\n * frontmatter — content between the opening and closing `---` delimiters\n * body — everything after the closing `---` line\n *\n * Returns null if the file does not begin with a `---` frontmatter block.\n */\nfunction parseFrontmatter(\n content: string\n): { frontmatter: string; body: string } | null {\n if (!content.startsWith(\"---\")) return null;\n\n // Skip past the opening \"---\" and its line ending\n const afterOpen = content.slice(3);\n const eolMatch = afterOpen.match(/^\\r?\\n/);\n if (!eolMatch) return null;\n\n const rest = afterOpen.slice(eolMatch[0].length);\n\n // Find closing \"---\" on its own line\n const closeMatch = rest.match(/^([\\s\\S]*?)\\n---[ \\t]*(\\r?\\n|$)/m);\n if (!closeMatch) return null;\n\n const frontmatter = closeMatch[1];\n const body = rest.slice(closeMatch[0].length);\n\n return { frontmatter, body };\n}\n\n/**\n * Extract a simple scalar YAML value from a block of YAML text.\n *\n * extractYamlValue(' slug: \"my-proj\"', \"slug\") → \"my-proj\"\n * extractYamlValue(' slug: my-proj', \"slug\") → \"my-proj\"\n * extractYamlValue(' last_indexed: null', \"last_indexed\") → \"null\"\n */\nfunction extractYamlValue(yamlBlock: string, key: string): string | null {\n const re = new RegExp(\n `^[ \\\\t]*${key}[ \\\\t]*:[ \\\\t]*\"?([^\"\\\\n\\\\r]*?)\"?[ \\\\t]*$`,\n \"m\"\n );\n const match = yamlBlock.match(re);\n if (!match) return null;\n return match[1].trim() || null;\n}\n\n/**\n * Replace the `pai:` mapping block inside a frontmatter string with\n * `newPaiBlock`. If no `pai:` block is found, appends it at the end.\n *\n * The regex captures the `pai:` key and all immediately-following indented\n * lines (the mapping values), then replaces the whole group.\n *\n * Edge case: the last indented line may not have a trailing newline when it\n * is the final line of the frontmatter string. We handle this by matching\n * lines that end with \\n OR with end-of-string.\n */\nfunction replacePaiBlock(frontmatter: string, newPaiBlock: string): string {\n // Normalise: ensure the frontmatter string ends with \\n so the regex\n // always finds a clean boundary after the last indented line.\n const fm = frontmatter.endsWith(\"\\n\") ? frontmatter : frontmatter + \"\\n\";\n\n // Match \"pai:\\n\" followed by any number of indented lines (each ending \\n).\n const paiRe = /^pai:[ \\t]*\\r?\\n(?:[ \\t]+[^\\r\\n]*\\r?\\n)*/m;\n if (paiRe.test(fm)) {\n // Replace and strip the extra trailing \\n we may have added.\n const replaced = fm.replace(paiRe, newPaiBlock);\n return frontmatter.endsWith(\"\\n\") ? replaced : replaced.replace(/\\n$/, \"\");\n }\n // pai: key not found — append it\n return fm + newPaiBlock;\n}\n\n/**\n * Build the canonical `pai:` YAML block (with a trailing newline).\n */\nfunction buildPaiBlock(\n slug: string,\n registered: string,\n lastIndexed: string | null,\n status: string\n): string {\n const lastIndexedStr =\n lastIndexed === null ? \"null\" : `\"${lastIndexed}\"`;\n return (\n `pai:\\n` +\n ` slug: \"${slug}\"\\n` +\n ` registered: \"${registered}\"\\n` +\n ` last_indexed: ${lastIndexedStr}\\n` +\n ` status: ${status}\\n`\n );\n}\n\n// ---------------------------------------------------------------------------\n// Public API\n// ---------------------------------------------------------------------------\n\n/**\n * Create or update `<projectRoot>/Notes/PAI.md`.\n *\n * - File absent: creates `Notes/` if needed, writes from template.\n * - File present: updates only the `pai:` frontmatter block; body and all\n * other frontmatter keys are preserved verbatim.\n *\n * @param projectRoot Absolute path to the project root directory.\n * @param slug PAI slug for this project.\n * @param displayName Human-readable name (defaults to slug if omitted).\n */\nexport function ensurePaiMarker(\n projectRoot: string,\n slug: string,\n displayName?: string\n): void {\n const notesDir = join(projectRoot, \"Notes\");\n const markerPath = join(notesDir, \"PAI.md\");\n const name = displayName ?? slug;\n\n // --- File does not exist — create from template ---\n if (!existsSync(markerPath)) {\n // Never create a Notes/ folder inside a container directory. Doing so is\n // what puts a Notes folder on the user's Desktop that comes back every\n // time it is deleted.\n if (isContainerDirectory(projectRoot)) return;\n\n mkdirSync(notesDir, { recursive: true });\n writeFileSync(markerPath, renderTemplate(slug, name), \"utf8\");\n return;\n }\n\n // --- File exists — update only the `pai:` block ---\n const raw = readFileSync(markerPath, \"utf8\");\n const parsed = parseFrontmatter(raw);\n\n if (!parsed) {\n // No YAML frontmatter — prepend a fresh one, treat the whole file as body.\n const paiBlock = buildPaiBlock(slug, isoDate(), null, \"active\");\n const newContent = `---\\n${paiBlock}---\\n\\n${raw}`;\n writeFileSync(markerPath, newContent, \"utf8\");\n return;\n }\n\n const { frontmatter, body } = parsed;\n\n // Preserve existing `registered` date so we don't reset it on re-scan.\n const existingRegistered =\n extractYamlValue(frontmatter, \"registered\") ?? isoDate();\n\n // Preserve existing `last_indexed` value (may be \"null\" string or a date).\n const rawLastIndexed = extractYamlValue(frontmatter, \"last_indexed\");\n const lastIndexed =\n rawLastIndexed === null || rawLastIndexed === \"null\"\n ? null\n : rawLastIndexed;\n\n // Preserve existing `status`.\n const existingStatus = extractYamlValue(frontmatter, \"status\") ?? \"active\";\n\n const newPaiBlock = buildPaiBlock(\n slug,\n existingRegistered,\n lastIndexed,\n existingStatus\n );\n\n const newFrontmatter = replacePaiBlock(frontmatter, newPaiBlock);\n\n // Ensure the frontmatter block ends with exactly one newline before the\n // closing --- delimiter.\n const fmWithNewline = newFrontmatter.endsWith(\"\\n\")\n ? newFrontmatter\n : newFrontmatter + \"\\n\";\n\n // Reconstruct the full file. Preserve whatever separator the body has.\n const newContent = `---\\n${fmWithNewline}---\\n${body}`;\n writeFileSync(markerPath, newContent, \"utf8\");\n}\n\n/**\n * Read PAI marker data from `<projectRoot>/Notes/PAI.md`.\n * Returns null if the file does not exist or contains no `pai:` block.\n */\nexport function readPaiMarker(\n projectRoot: string\n): { slug: string; registered: string; status: string } | null {\n const markerPath = join(projectRoot, \"Notes\", \"PAI.md\");\n if (!existsSync(markerPath)) return null;\n\n const raw = readFileSync(markerPath, \"utf8\");\n const parsed = parseFrontmatter(raw);\n if (!parsed) return null;\n\n const slug = extractYamlValue(parsed.frontmatter, \"slug\");\n if (!slug) return null;\n\n const registered =\n extractYamlValue(parsed.frontmatter, \"registered\") ?? \"\";\n const status =\n extractYamlValue(parsed.frontmatter, \"status\") ?? \"active\";\n\n return { slug, registered, status };\n}\n\n/**\n * Scan a list of parent directories for `<child>/Notes/PAI.md` marker files.\n * Each directory in `searchDirs` is scanned one level deep — its immediate\n * child directories are checked for a `Notes/PAI.md` file.\n *\n * Returns an array of PaiMarker objects for every valid marker found.\n * Invalid or malformed markers are silently skipped.\n *\n * @param searchDirs Absolute paths to parent directories.\n */\nexport function discoverPaiMarkers(searchDirs: string[]): PaiMarker[] {\n const results: PaiMarker[] = [];\n\n for (const dir of searchDirs) {\n if (!existsSync(dir)) continue;\n\n let children: string[];\n try {\n children = readdirSync(dir);\n } catch {\n continue;\n }\n\n for (const child of children) {\n if (child.startsWith(\".\")) continue;\n const childPath = join(dir, child);\n try {\n if (!statSync(childPath).isDirectory()) continue;\n } catch {\n continue;\n }\n\n const markerData = readPaiMarker(childPath);\n if (!markerData) continue;\n\n results.push({\n path: join(childPath, \"Notes\", \"PAI.md\"),\n slug: markerData.slug,\n projectRoot: childPath,\n });\n }\n }\n\n return results;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"pai-marker-DXVpFsYz.mjs","names":[],"sources":["../src/registry/pai-marker.ts"],"sourcesContent":["/**\n * PAI.md marker file management.\n *\n * Each registered project gets a `Notes/PAI.md` file with a YAML frontmatter\n * `pai:` block that PAI manages. The rest of the file (body content, other\n * frontmatter keys) is user-owned and never modified by PAI.\n *\n * YAML parsing/updating is done with simple regex — no external dependency.\n */\n\nimport {\n existsSync,\n readFileSync,\n writeFileSync,\n mkdirSync,\n readdirSync,\n statSync,\n} from \"node:fs\";\nimport { join, resolve } from \"node:path\";\nimport { homedir } from \"node:os\";\n\n// ---------------------------------------------------------------------------\n// Container directories\n// ---------------------------------------------------------------------------\n\n/**\n * Directories that hold projects but are not themselves projects.\n *\n * Running a single Claude session from ~/Desktop is enough for the scanner to\n * register it as a project, after which every scan recreates a `Notes/PAI.md`\n * inside it. The user gets a Notes folder on their Desktop that reappears\n * however many times they delete it, containing a marker file that is\n * otherwise empty.\n *\n * The session is still worth tracking — it just belongs in the central notes\n * directory (~/.claude/projects/<encoded>/Notes) rather than as a folder\n * dropped into the user's Desktop.\n */\nconst CONTAINER_DIRS = [\n \"Desktop\",\n \"Documents\",\n \"Downloads\",\n \"Movies\",\n \"Music\",\n \"Pictures\",\n \"Public\",\n \"Library\",\n \"Applications\",\n];\n\n/**\n * True when a path is a place projects live rather than a project itself:\n * the home directory, a standard user container, or a system temp root.\n *\n * Only ever used to decline *creating* a marker. An existing marker is left\n * alone — the user may have put one there deliberately.\n */\nexport function isContainerDirectory(projectRoot: string): boolean {\n // Strip trailing slashes, but never strip the root itself down to \"\".\n const strip = (s: string): string => s.replace(/(?!^)\\/+$/, \"\");\n\n const p = strip(resolve(projectRoot));\n const home = strip(resolve(homedir()));\n\n if (p === home || p === \"/\") return true;\n if (p === \"/tmp\" || p === \"/private/tmp\" || p === \"/var/tmp\") return true;\n\n for (const dir of CONTAINER_DIRS) {\n if (p === join(home, dir)) return true;\n }\n\n return false;\n}\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface PaiMarker {\n /** Absolute path to the PAI.md file */\n path: string;\n /** The `slug` value from the `pai:` frontmatter block */\n slug: string;\n /** Absolute path to the project root (parent of Notes/) */\n projectRoot: string;\n}\n\n// ---------------------------------------------------------------------------\n// Template content (mirrors templates/pai-project.template.md)\n// ---------------------------------------------------------------------------\n\nconst TEMPLATE = `---\npai:\n slug: \"\\${SLUG}\"\n registered: \"\\${DATE}\"\n last_indexed: null\n status: active\n---\n\n# \\${DISPLAY_NAME}\n\n<!-- Everything below the YAML frontmatter is yours — PAI never modifies content here. -->\n<!-- Use this file for project notes, decisions, preferences, or anything you want. -->\n<!-- PAI only reads and updates the \\`pai:\\` block in the frontmatter above. -->\n`;\n\nfunction isoDate(): string {\n return new Date().toISOString().slice(0, 10);\n}\n\nfunction renderTemplate(slug: string, displayName: string): string {\n return TEMPLATE.replace(/\\$\\{SLUG\\}/g, slug)\n .replace(/\\$\\{DATE\\}/g, isoDate())\n .replace(/\\$\\{DISPLAY_NAME\\}/g, displayName);\n}\n\n// ---------------------------------------------------------------------------\n// YAML frontmatter helpers (regex-based, no external dependency)\n// ---------------------------------------------------------------------------\n\n/**\n * Split a markdown file with YAML frontmatter into its parts.\n *\n * Returns { frontmatter, body } where:\n * frontmatter — content between the opening and closing `---` delimiters\n * body — everything after the closing `---` line\n *\n * Returns null if the file does not begin with a `---` frontmatter block.\n */\nfunction parseFrontmatter(\n content: string\n): { frontmatter: string; body: string } | null {\n if (!content.startsWith(\"---\")) return null;\n\n // Skip past the opening \"---\" and its line ending\n const afterOpen = content.slice(3);\n const eolMatch = afterOpen.match(/^\\r?\\n/);\n if (!eolMatch) return null;\n\n const rest = afterOpen.slice(eolMatch[0].length);\n\n // Find closing \"---\" on its own line\n const closeMatch = rest.match(/^([\\s\\S]*?)\\n---[ \\t]*(\\r?\\n|$)/m);\n if (!closeMatch) return null;\n\n const frontmatter = closeMatch[1];\n const body = rest.slice(closeMatch[0].length);\n\n return { frontmatter, body };\n}\n\n/**\n * Extract a simple scalar YAML value from a block of YAML text.\n *\n * extractYamlValue(' slug: \"my-proj\"', \"slug\") → \"my-proj\"\n * extractYamlValue(' slug: my-proj', \"slug\") → \"my-proj\"\n * extractYamlValue(' last_indexed: null', \"last_indexed\") → \"null\"\n */\nfunction extractYamlValue(yamlBlock: string, key: string): string | null {\n const re = new RegExp(\n `^[ \\\\t]*${key}[ \\\\t]*:[ \\\\t]*\"?([^\"\\\\n\\\\r]*?)\"?[ \\\\t]*$`,\n \"m\"\n );\n const match = yamlBlock.match(re);\n if (!match) return null;\n return match[1].trim() || null;\n}\n\n/**\n * Replace the `pai:` mapping block inside a frontmatter string with\n * `newPaiBlock`. If no `pai:` block is found, appends it at the end.\n *\n * The regex captures the `pai:` key and all immediately-following indented\n * lines (the mapping values), then replaces the whole group.\n *\n * Edge case: the last indented line may not have a trailing newline when it\n * is the final line of the frontmatter string. We handle this by matching\n * lines that end with \\n OR with end-of-string.\n */\nfunction replacePaiBlock(frontmatter: string, newPaiBlock: string): string {\n // Normalise: ensure the frontmatter string ends with \\n so the regex\n // always finds a clean boundary after the last indented line.\n const fm = frontmatter.endsWith(\"\\n\") ? frontmatter : frontmatter + \"\\n\";\n\n // Match \"pai:\\n\" followed by any number of indented lines (each ending \\n).\n const paiRe = /^pai:[ \\t]*\\r?\\n(?:[ \\t]+[^\\r\\n]*\\r?\\n)*/m;\n if (paiRe.test(fm)) {\n // Replace and strip the extra trailing \\n we may have added.\n const replaced = fm.replace(paiRe, newPaiBlock);\n return frontmatter.endsWith(\"\\n\") ? replaced : replaced.replace(/\\n$/, \"\");\n }\n // pai: key not found — append it\n return fm + newPaiBlock;\n}\n\n/**\n * Build the canonical `pai:` YAML block (with a trailing newline).\n */\nfunction buildPaiBlock(\n slug: string,\n registered: string,\n lastIndexed: string | null,\n status: string\n): string {\n const lastIndexedStr =\n lastIndexed === null ? \"null\" : `\"${lastIndexed}\"`;\n return (\n `pai:\\n` +\n ` slug: \"${slug}\"\\n` +\n ` registered: \"${registered}\"\\n` +\n ` last_indexed: ${lastIndexedStr}\\n` +\n ` status: ${status}\\n`\n );\n}\n\n// ---------------------------------------------------------------------------\n// Public API\n// ---------------------------------------------------------------------------\n\n/**\n * Create or update `<projectRoot>/Notes/PAI.md`.\n *\n * - File absent: creates `Notes/` if needed, writes from template.\n * - File present: updates only the `pai:` frontmatter block; body and all\n * other frontmatter keys are preserved verbatim.\n *\n * @param projectRoot Absolute path to the project root directory.\n * @param slug PAI slug for this project.\n * @param displayName Human-readable name (defaults to slug if omitted).\n */\nexport function ensurePaiMarker(\n projectRoot: string,\n slug: string,\n displayName?: string\n): void {\n const notesDir = join(projectRoot, \"Notes\");\n const markerPath = join(notesDir, \"PAI.md\");\n const name = displayName ?? slug;\n\n // --- File does not exist — create from template ---\n if (!existsSync(markerPath)) {\n // Never create a Notes/ folder inside a container directory. Doing so is\n // what puts a Notes folder on the user's Desktop that comes back every\n // time it is deleted.\n if (isContainerDirectory(projectRoot)) return;\n\n mkdirSync(notesDir, { recursive: true });\n writeFileSync(markerPath, renderTemplate(slug, name), \"utf8\");\n return;\n }\n\n // --- File exists — update only the `pai:` block ---\n const raw = readFileSync(markerPath, \"utf8\");\n const parsed = parseFrontmatter(raw);\n\n if (!parsed) {\n // No YAML frontmatter — prepend a fresh one, treat the whole file as body.\n const paiBlock = buildPaiBlock(slug, isoDate(), null, \"active\");\n const newContent = `---\\n${paiBlock}---\\n\\n${raw}`;\n writeFileSync(markerPath, newContent, \"utf8\");\n return;\n }\n\n const { frontmatter, body } = parsed;\n\n // Preserve existing `registered` date so we don't reset it on re-scan.\n const existingRegistered =\n extractYamlValue(frontmatter, \"registered\") ?? isoDate();\n\n // Preserve existing `last_indexed` value (may be \"null\" string or a date).\n const rawLastIndexed = extractYamlValue(frontmatter, \"last_indexed\");\n const lastIndexed =\n rawLastIndexed === null || rawLastIndexed === \"null\"\n ? null\n : rawLastIndexed;\n\n // Preserve existing `status`.\n const existingStatus = extractYamlValue(frontmatter, \"status\") ?? \"active\";\n\n const newPaiBlock = buildPaiBlock(\n slug,\n existingRegistered,\n lastIndexed,\n existingStatus\n );\n\n const newFrontmatter = replacePaiBlock(frontmatter, newPaiBlock);\n\n // Ensure the frontmatter block ends with exactly one newline before the\n // closing --- delimiter.\n const fmWithNewline = newFrontmatter.endsWith(\"\\n\")\n ? newFrontmatter\n : newFrontmatter + \"\\n\";\n\n // Reconstruct the full file. Preserve whatever separator the body has.\n const newContent = `---\\n${fmWithNewline}---\\n${body}`;\n writeFileSync(markerPath, newContent, \"utf8\");\n}\n\n/**\n * Read PAI marker data from `<projectRoot>/Notes/PAI.md`.\n * Returns null if the file does not exist or contains no `pai:` block.\n */\nexport function readPaiMarker(\n projectRoot: string\n): { slug: string; registered: string; status: string } | null {\n const markerPath = join(projectRoot, \"Notes\", \"PAI.md\");\n if (!existsSync(markerPath)) return null;\n\n const raw = readFileSync(markerPath, \"utf8\");\n const parsed = parseFrontmatter(raw);\n if (!parsed) return null;\n\n const slug = extractYamlValue(parsed.frontmatter, \"slug\");\n if (!slug) return null;\n\n const registered =\n extractYamlValue(parsed.frontmatter, \"registered\") ?? \"\";\n const status =\n extractYamlValue(parsed.frontmatter, \"status\") ?? \"active\";\n\n return { slug, registered, status };\n}\n\n/**\n * Scan a list of parent directories for `<child>/Notes/PAI.md` marker files.\n * Each directory in `searchDirs` is scanned one level deep — its immediate\n * child directories are checked for a `Notes/PAI.md` file.\n *\n * Returns an array of PaiMarker objects for every valid marker found.\n * Invalid or malformed markers are silently skipped.\n *\n * @param searchDirs Absolute paths to parent directories.\n */\nexport function discoverPaiMarkers(searchDirs: string[]): PaiMarker[] {\n const results: PaiMarker[] = [];\n\n for (const dir of searchDirs) {\n if (!existsSync(dir)) continue;\n\n let children: string[];\n try {\n children = readdirSync(dir);\n } catch {\n continue;\n }\n\n for (const child of children) {\n if (child.startsWith(\".\")) continue;\n const childPath = join(dir, child);\n try {\n if (!statSync(childPath).isDirectory()) continue;\n } catch {\n continue;\n }\n\n const markerData = readPaiMarker(childPath);\n if (!markerData) continue;\n\n results.push({\n path: join(childPath, \"Notes\", \"PAI.md\"),\n slug: markerData.slug,\n projectRoot: childPath,\n });\n }\n }\n\n return results;\n}\n"],"mappings":"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"}
|
|
@@ -1,9 +1,12 @@
|
|
|
1
|
-
import "./
|
|
2
|
-
import "./
|
|
3
|
-
import
|
|
4
|
-
import
|
|
5
|
-
import
|
|
6
|
-
import
|
|
1
|
+
import "./helpers-CZsi_49C.mjs";
|
|
2
|
+
import "./postgres-D3xc2RB4.mjs";
|
|
3
|
+
import "./factory-Ypf8r0fK.mjs";
|
|
4
|
+
import "./embeddings-DOLZnT1X.mjs";
|
|
5
|
+
import "./router-BaTbc9VX.mjs";
|
|
6
|
+
import "./config-C-LiGlop.mjs";
|
|
7
|
+
import { D as readWorkersSection } from "./run-env-Bnmf6bRI.mjs";
|
|
8
|
+
import { A as git, F as appendLedger, G as loadStatus, dt as ledgerPath, et as parseRunnerArgs, j as mergeWorker, n as runWorker, pt as workersLogDir, q as newWorkerId, t as printResult, tt as shortText } from "./run-CLYTOm0x.mjs";
|
|
9
|
+
import { n as swapPromptArg } from "./chain-D5876UQX.mjs";
|
|
7
10
|
import { existsSync, mkdirSync, readFileSync } from "node:fs";
|
|
8
11
|
import { join } from "node:path";
|
|
9
12
|
|
|
@@ -297,4 +300,4 @@ function printPlannerSummary(logDir, plannerId, results, merged, conflicted, fmt
|
|
|
297
300
|
|
|
298
301
|
//#endregion
|
|
299
302
|
export { runPlanner };
|
|
300
|
-
//# sourceMappingURL=planner-
|
|
303
|
+
//# sourceMappingURL=planner-D5yt0EfA.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"planner-Dm5Acyo1.mjs","names":[],"sources":["../src/workers/planner.ts"],"sourcesContent":["/**\n * planner.ts — the plan class: plan, spawn, integrate.\n *\n * `pai worker run --class plan -p \"<goal>\"` does not run one worker but a\n * small orchestration:\n *\n * 1. a planner worker (the plan class's provider) reads the repository and\n * writes `<logDir>/plans/<planner id>.json` — 5–50 sub-tasks, fewer only\n * when the goal itself names a smaller count, each with title, brief,\n * class, files and acceptance;\n * 2. the runner validates the plan and spawns the sub-tasks as children of\n * the planner worker (`parent` = the planner id, so `ps` shows the\n * tree), at most `workers.tree.maxChildren` at a time;\n * 3. each child that finishes delivers its structured report to the\n * planner's inbox as a `kind: \"result\"` handoff (see handoff.ts);\n * 4. the run finishes with a summary report and the branches to merge.\n *\n * The planner's prompt carries the prompt rules from the self-driving\n * codebases write-up: domain-specific instructions only, constraints over\n * step lists, explicit quantity ranges, no checkbox style.\n */\n\nimport { existsSync, mkdirSync, readFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { readWorkersSection } from \"./config.js\";\nimport { appendLedger } from \"./ledger.js\";\nimport { ledgerPath, workersLogDir } from \"./paths.js\";\nimport { loadStatus, newWorkerId } from \"./status.js\";\nimport { parseRunnerArgs, shortText } from \"./args.js\";\nimport { swapPromptArg } from \"./chain.js\";\nimport { runWorker, printResult, type RunOptions, type StreamEvent } from \"./run.js\";\nimport { git, mergeWorker } from \"./worktree.js\";\nimport type { WorkerReport } from \"./report.js\";\n\n/** Where a planner run's plan file lives: <logDir>/plans/<planner id>.json. */\nexport function planPathFor(logDir: string, plannerId: string): string {\n return join(logDir, \"plans\", `${plannerId}.json`);\n}\n\nexport interface PlannerTask {\n title: string;\n brief: string;\n /** Task class of the child (implement, research, …); default implement. */\n class?: string;\n /** Files the sub-task likely touches. */\n files?: string[];\n /** How to tell the sub-task is done. */\n acceptance?: string[];\n}\n\nexport const MIN_TASKS = 5;\nexport const MAX_TASKS = 50;\n\n/**\n * Validate a raw plan file into its tasks. Accepts 1–MAX_TASKS tasks: the\n * prompt asks for MIN_TASKS–MAX_TASKS, but a goal may itself name a smaller\n * explicit count (\"one sub-task per file\" with three files), and that\n * explicit range wins. Everything else — missing file, damaged JSON, tasks\n * without a title or brief — throws with a message the operator can act on.\n */\nexport function validatePlan(raw: unknown): PlannerTask[] {\n if (typeof raw !== \"object\" || raw === null || Array.isArray(raw)) {\n throw new Error(\"plan file must be a JSON object\");\n }\n const tasksRaw = (raw as Record<string, unknown>).tasks;\n if (!Array.isArray(tasksRaw)) throw new Error('plan file needs a \"tasks\" array');\n if (tasksRaw.length < 1) throw new Error(\"plan file has no tasks\");\n if (tasksRaw.length > MAX_TASKS) {\n throw new Error(`plan file has ${tasksRaw.length} tasks; the maximum is ${MAX_TASKS}`);\n }\n const tasks: PlannerTask[] = [];\n for (let i = 0; i < tasksRaw.length; i++) {\n const t = tasksRaw[i];\n if (typeof t !== \"object\" || t === null || Array.isArray(t)) {\n throw new Error(`task ${i + 1} must be an object`);\n }\n const o = t as Record<string, unknown>;\n const title = typeof o.title === \"string\" ? o.title.trim() : \"\";\n const brief = typeof o.brief === \"string\" ? o.brief.trim() : \"\";\n if (!title) throw new Error(`task ${i + 1} needs a non-empty \"title\"`);\n if (!brief) throw new Error(`task ${i + 1} needs a non-empty \"brief\"`);\n const strArr = (v: unknown): string[] | undefined =>\n Array.isArray(v) && v.every((x) => typeof x === \"string\") ? (v as string[]) : undefined;\n const files = strArr(o.files);\n const acceptance = strArr(o.acceptance);\n tasks.push({\n title,\n brief,\n ...(typeof o.class === \"string\" && o.class.trim() ? { class: o.class.trim() } : {}),\n ...(files?.length ? { files } : {}),\n ...(acceptance?.length ? { acceptance } : {}),\n });\n }\n return tasks;\n}\n\n/** The planner worker's instructions, goal and prompt rules included. */\nexport function plannerPrompt(goal: string, planFile: string, maxChildren: number): string {\n return [\n \"You are a PLANNER worker. Turn the goal below into a plan file and stop —\",\n \"you do not implement anything, and you do not spawn workers yourself:\",\n \"the runner executes your plan on your behalf (the sub-tasks run as your\",\n \"children and report back to you).\",\n \"\",\n `Write the plan with the Write tool to ${planFile} as one JSON object:`,\n '{\"tasks\":[{\"title\":\"…\",\"brief\":\"…\",\"class\":\"implement\",\"files\":[\"…\"],\"acceptance\":[\"…\"]}]}',\n \"\",\n \"Prompt rules for the plan itself (they are yours too):\",\n \"- Domain-specific instructions only: name real files, commands and\",\n \" constraints of this repository, never generic advice.\",\n \"- Constraints over step lists: state what must hold (and what must not\",\n \" change), not a numbered procedure to follow.\",\n \"- Explicit quantity ranges: how many, how much, how long — never\",\n ' \"several\", \"some\", \"as needed\".',\n \"- No checkbox style: no `[ ]` items, no step numbering theatre.\",\n \"\",\n `Emit between ${MIN_TASKS} and ${MAX_TASKS} sub-tasks — unless the goal itself`,\n 'names a smaller explicit count (\"one sub-task per file\" with three files',\n \"means three); an explicit count in the goal always wins.\",\n `At most ${maxChildren} of them run at a time, so independent tasks are better`,\n \"than long chains. Each task's class picks its provider: implement, draft,\",\n \"review, research, spotcheck, simple, complex, image.\",\n \"\",\n \"Read the repository first (Glob/Grep/Read) so tasks name real files.\",\n \"Every sub-task runs in its own worktree of the same repository: name files\",\n \"relative to the repository root and never an absolute path or a worktree\",\n \"directory in a title, brief, files or acceptance entry.\",\n \"\",\n \"## Goal\",\n \"\",\n goal,\n ].join(\"\\n\");\n}\n\n/** The child's prompt for one planned sub-task. */\nexport function taskPrompt(goal: string, task: PlannerTask, index: number, of: number): string {\n return [\n `You are sub-task ${index + 1} of ${of} of a planned goal. Do exactly this sub-task;`,\n \"the other sub-tasks are other workers' business.\",\n \"\",\n `# ${task.title}`,\n \"\",\n task.brief,\n ...(task.files?.length ? [\"\", \"Files likely touched: \" + task.files.join(\", \")] : []),\n ...(task.acceptance?.length\n ? [\"\", \"Done means:\", ...task.acceptance.map((a) => `- ${a}`)]\n : []),\n \"\",\n \"## The overall goal (context only — your scope is the sub-task above)\",\n \"\",\n goal,\n ].join(\"\\n\");\n}\n\nexport interface PlannerDeps {\n /** Stage runner; tests inject a mock, production uses runWorker. */\n runStage?: (opts: RunOptions) => Promise<number>;\n /** logDir override for tests. */\n logDir?: string;\n /** maxChildren override for tests. */\n maxChildren?: number;\n}\n\n/**\n * The planner orchestration; returns the process exit code. The planner id is\n * minted here and preset on the phase-1 run, so the plan file's name is known\n * before the worker starts and the prompt can name its exact path.\n */\nexport async function runPlanner(opts: RunOptions, deps: PlannerDeps = {}): Promise<number> {\n const runStage = deps.runStage ?? runWorker;\n const { workers: config } = readWorkersSection();\n const logDir = deps.logDir ?? workersLogDir(config);\n const maxChildren = deps.maxChildren ?? config.tree.maxChildren;\n const parsed = parseRunnerArgs(opts.claudeArgs);\n const goal = parsed.prompt ?? \"\";\n const plannerId = newWorkerId();\n const planFile = planPathFor(logDir, plannerId);\n mkdirSync(join(logDir, \"plans\"), { recursive: true });\n\n // --- phase 1: the planner worker writes the plan file\n process.stderr.write(`planner ${plannerId}: writing plan (${planFile})\\n`);\n const rc1 = await runStage({\n ...opts,\n id: plannerId,\n claudeArgs: swapPromptArg(opts.claudeArgs, plannerPrompt(goal, planFile, maxChildren)),\n quiet: true,\n _planner: true,\n });\n if (rc1 !== 0) return rc1;\n if (!existsSync(planFile)) {\n process.stderr.write(\n `planner ${plannerId}: no plan file at ${planFile} — the planner worker did not write one. ` +\n `Re-run, or write the plan yourself and run the tasks with pai worker run.\\n`\n );\n appendLedger(ledgerPath(logDir), \"WORKER-PLAN-END\", { planner: plannerId, rc: 1, failed: \"plan\" });\n return 1;\n }\n let tasks: PlannerTask[];\n try {\n tasks = validatePlan(JSON.parse(readFileSync(planFile, \"utf8\")));\n } catch (e) {\n process.stderr.write(`planner ${plannerId}: invalid plan file ${planFile}: ${(e as Error).message}\\n`);\n appendLedger(ledgerPath(logDir), \"WORKER-PLAN-END\", { planner: plannerId, rc: 1, failed: \"plan\" });\n return 1;\n }\n appendLedger(ledgerPath(logDir), \"WORKER-PLAN\", {\n planner: plannerId,\n tasks: tasks.length,\n plan: planFile,\n });\n\n // --- phase 2: run the sub-tasks as children, maxChildren at a time. Each\n // child is spawned in the planner's own worktree (when it got one) rather\n // than the operator's cwd, so its branch is created off the planner's own\n // HEAD and can later be merged straight into it.\n const plannerStatus = loadStatus(logDir, plannerId);\n const plannerWorktreeDir = plannerStatus?.worktreeDir ?? null;\n const results: { task: PlannerTask; rc: number; id: string }[] = [];\n for (let i = 0; i < tasks.length; i += maxChildren) {\n const wave = tasks.slice(i, i + maxChildren);\n process.stderr.write(\n `planner ${plannerId}: sub-tasks ${i + 1}–${i + wave.length} of ${tasks.length}\\n`\n );\n const waveIds: string[] = new Array(wave.length).fill(\"\");\n const rcs = await Promise.all(\n wave.map((task, w) =>\n runStage({\n className: task.class ?? \"implement\",\n providerFlag: opts.providerFlag,\n modelFlag: opts.modelFlag,\n label: shortText(task.title, 40),\n noPane: opts.noPane,\n mcpFlag: opts.mcpFlag,\n claudeArgs: swapPromptArg(opts.claudeArgs, taskPrompt(goal, task, i + w, tasks.length)),\n cwd: plannerWorktreeDir ?? opts.cwd,\n worktreeFlag: opts.worktreeFlag,\n parent: plannerId,\n quiet: true,\n onWorkerStart: (wid) => {\n waveIds[w] = wid;\n },\n })\n )\n );\n for (let w = 0; w < wave.length; w++) results.push({ task: wave[w], rc: rcs[w], id: waveIds[w] });\n }\n\n // --- phase 3: the planner merges each finished child's branch into its\n // OWN worktree branch, in spawn order — never into the operator's checkout\n // — reusing `pai worker merge`'s own logic (mergeWorker). A conflicting\n // merge is aborted cleanly so the planner's branch stays clean and the\n // next child still gets a try; the child's branch is left intact either way.\n const merged: string[] = [];\n const conflicted: { id: string; branch: string; paths: string[] }[] = [];\n if (plannerWorktreeDir) {\n for (const r of results) {\n if (r.rc !== 0 || !r.id) continue;\n const st = loadStatus(logDir, r.id);\n if (!st?.branch) continue; // ran in place, or no worktree: nothing to merge\n try {\n mergeWorker(logDir, r.id);\n merged.push(r.id);\n } catch {\n let paths: string[] = [];\n try {\n git(plannerWorktreeDir, [\"rev-parse\", \"-q\", \"--verify\", \"MERGE_HEAD\"]);\n paths = git(plannerWorktreeDir, [\"diff\", \"--name-only\", \"--diff-filter=U\"])\n .split(\"\\n\")\n .filter(Boolean);\n git(plannerWorktreeDir, [\"merge\", \"--abort\"]);\n } catch {\n // no merge was left in progress: the failure was not a conflict\n }\n conflicted.push({ id: r.id, branch: st.branch, paths });\n }\n }\n }\n\n // --- phase 4: summary from the children's statuses and inbox handoffs\n const failed = results.filter((r) => r.rc !== 0).length;\n appendLedger(ledgerPath(logDir), \"WORKER-PLAN-END\", {\n planner: plannerId,\n rc: failed || conflicted.length ? 1 : 0,\n failed,\n merged: merged.length,\n conflicted: conflicted.length,\n });\n printPlannerSummary(logDir, plannerId, results, merged, conflicted, parsed.outputFormat, opts.quiet === true);\n return failed || conflicted.length ? 1 : 0;\n}\n\n/** Compose and print the planner's summary report (text or json). */\nfunction printPlannerSummary(\n logDir: string,\n plannerId: string,\n results: { task: PlannerTask; rc: number; id: string }[],\n merged: string[],\n conflicted: { id: string; branch: string; paths: string[] }[],\n fmt: \"text\" | \"json\" | \"stream-json\",\n quiet: boolean\n): void {\n const ok = results.filter((r) => r.rc === 0).length;\n const attempted = new Set([...merged, ...conflicted.map((c) => c.id)]);\n // a branch never attempted (no planner worktree to merge into) still needs\n // the old \"go merge me by hand\" hint\n const stillOpen: { id: string; branch: string }[] = [];\n for (const r of results) {\n if (r.rc !== 0 || !r.id || attempted.has(r.id)) continue;\n const st = loadStatus(logDir, r.id);\n if (st?.branch && !st.merged) stillOpen.push({ id: r.id, branch: st.branch });\n }\n const report: WorkerReport = {\n checks: results.map((r) => ({\n name: r.task.title,\n ok: r.rc === 0,\n detail: r.rc === 0 ? shortText(r.id ? loadStatus(logDir, r.id)?.last : \"\", 80) : `rc=${r.rc}`,\n })),\n ...(merged.length || conflicted.length || stillOpen.length\n ? {\n open: [\n ...merged.map((id) => `merged: ${id}`),\n ...conflicted.map(\n (c) =>\n `conflict: pai worker merge ${c.id} (${c.branch})` +\n (c.paths.length ? ` — ${c.paths.join(\", \")}` : \"\")\n ),\n ...stillOpen.map((c) => `branch to merge: pai worker merge ${c.id} (${c.branch})`),\n ],\n }\n : {}),\n notes:\n `${ok}/${results.length} sub-tasks ok` +\n (merged.length ? `; ${merged.length} merged` : \"\") +\n (conflicted.length ? `; ${conflicted.length} conflict(s)` : \"\") +\n (stillOpen.length ? `; ${stillOpen.length} branch(es) to merge` : \"\"),\n };\n if (quiet || fmt === \"stream-json\") return;\n const allOk = ok === results.length && conflicted.length === 0;\n const resultEvent: StreamEvent = {\n type: \"result\",\n result: [\n `planner ${plannerId}: ${report.notes}`,\n ...(conflicted.length ? [\"conflicts:\"] : []),\n ...conflicted.map(\n (c) =>\n ` pai worker merge ${c.id} # ${c.branch}, conflicting: ${c.paths.join(\", \") || \"unknown\"}`\n ),\n ...(stillOpen.length ? [\"branches to merge:\"] : []),\n ...stillOpen.map((c) => ` pai worker merge ${c.id} # ${c.branch}`),\n ].join(\"\\n\"),\n is_error: !allOk,\n };\n printResult(fmt, resultEvent, allOk ? 0 : 1, logDir, plannerId, report, {\n plan: results.length,\n ...(merged.length ? { mergedBranches: merged } : {}),\n ...(conflicted.length ? { conflictedBranches: conflicted.map((c) => c.branch) } : {}),\n ...(stillOpen.length ? { branches: stillOpen.map((c) => c.branch) } : {}),\n });\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"planner-D5yt0EfA.mjs","names":[],"sources":["../src/workers/planner.ts"],"sourcesContent":["/**\n * planner.ts — the plan class: plan, spawn, integrate.\n *\n * `pai worker run --class plan -p \"<goal>\"` does not run one worker but a\n * small orchestration:\n *\n * 1. a planner worker (the plan class's provider) reads the repository and\n * writes `<logDir>/plans/<planner id>.json` — 5–50 sub-tasks, fewer only\n * when the goal itself names a smaller count, each with title, brief,\n * class, files and acceptance;\n * 2. the runner validates the plan and spawns the sub-tasks as children of\n * the planner worker (`parent` = the planner id, so `ps` shows the\n * tree), at most `workers.tree.maxChildren` at a time;\n * 3. each child that finishes delivers its structured report to the\n * planner's inbox as a `kind: \"result\"` handoff (see handoff.ts);\n * 4. the run finishes with a summary report and the branches to merge.\n *\n * The planner's prompt carries the prompt rules from the self-driving\n * codebases write-up: domain-specific instructions only, constraints over\n * step lists, explicit quantity ranges, no checkbox style.\n */\n\nimport { existsSync, mkdirSync, readFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { readWorkersSection } from \"./config.js\";\nimport { appendLedger } from \"./ledger.js\";\nimport { ledgerPath, workersLogDir } from \"./paths.js\";\nimport { loadStatus, newWorkerId } from \"./status.js\";\nimport { parseRunnerArgs, shortText } from \"./args.js\";\nimport { swapPromptArg } from \"./chain.js\";\nimport { runWorker, printResult, type RunOptions, type StreamEvent } from \"./run.js\";\nimport { git, mergeWorker } from \"./worktree.js\";\nimport type { WorkerReport } from \"./report.js\";\n\n/** Where a planner run's plan file lives: <logDir>/plans/<planner id>.json. */\nexport function planPathFor(logDir: string, plannerId: string): string {\n return join(logDir, \"plans\", `${plannerId}.json`);\n}\n\nexport interface PlannerTask {\n title: string;\n brief: string;\n /** Task class of the child (implement, research, …); default implement. */\n class?: string;\n /** Files the sub-task likely touches. */\n files?: string[];\n /** How to tell the sub-task is done. */\n acceptance?: string[];\n}\n\nexport const MIN_TASKS = 5;\nexport const MAX_TASKS = 50;\n\n/**\n * Validate a raw plan file into its tasks. Accepts 1–MAX_TASKS tasks: the\n * prompt asks for MIN_TASKS–MAX_TASKS, but a goal may itself name a smaller\n * explicit count (\"one sub-task per file\" with three files), and that\n * explicit range wins. Everything else — missing file, damaged JSON, tasks\n * without a title or brief — throws with a message the operator can act on.\n */\nexport function validatePlan(raw: unknown): PlannerTask[] {\n if (typeof raw !== \"object\" || raw === null || Array.isArray(raw)) {\n throw new Error(\"plan file must be a JSON object\");\n }\n const tasksRaw = (raw as Record<string, unknown>).tasks;\n if (!Array.isArray(tasksRaw)) throw new Error('plan file needs a \"tasks\" array');\n if (tasksRaw.length < 1) throw new Error(\"plan file has no tasks\");\n if (tasksRaw.length > MAX_TASKS) {\n throw new Error(`plan file has ${tasksRaw.length} tasks; the maximum is ${MAX_TASKS}`);\n }\n const tasks: PlannerTask[] = [];\n for (let i = 0; i < tasksRaw.length; i++) {\n const t = tasksRaw[i];\n if (typeof t !== \"object\" || t === null || Array.isArray(t)) {\n throw new Error(`task ${i + 1} must be an object`);\n }\n const o = t as Record<string, unknown>;\n const title = typeof o.title === \"string\" ? o.title.trim() : \"\";\n const brief = typeof o.brief === \"string\" ? o.brief.trim() : \"\";\n if (!title) throw new Error(`task ${i + 1} needs a non-empty \"title\"`);\n if (!brief) throw new Error(`task ${i + 1} needs a non-empty \"brief\"`);\n const strArr = (v: unknown): string[] | undefined =>\n Array.isArray(v) && v.every((x) => typeof x === \"string\") ? (v as string[]) : undefined;\n const files = strArr(o.files);\n const acceptance = strArr(o.acceptance);\n tasks.push({\n title,\n brief,\n ...(typeof o.class === \"string\" && o.class.trim() ? { class: o.class.trim() } : {}),\n ...(files?.length ? { files } : {}),\n ...(acceptance?.length ? { acceptance } : {}),\n });\n }\n return tasks;\n}\n\n/** The planner worker's instructions, goal and prompt rules included. */\nexport function plannerPrompt(goal: string, planFile: string, maxChildren: number): string {\n return [\n \"You are a PLANNER worker. Turn the goal below into a plan file and stop —\",\n \"you do not implement anything, and you do not spawn workers yourself:\",\n \"the runner executes your plan on your behalf (the sub-tasks run as your\",\n \"children and report back to you).\",\n \"\",\n `Write the plan with the Write tool to ${planFile} as one JSON object:`,\n '{\"tasks\":[{\"title\":\"…\",\"brief\":\"…\",\"class\":\"implement\",\"files\":[\"…\"],\"acceptance\":[\"…\"]}]}',\n \"\",\n \"Prompt rules for the plan itself (they are yours too):\",\n \"- Domain-specific instructions only: name real files, commands and\",\n \" constraints of this repository, never generic advice.\",\n \"- Constraints over step lists: state what must hold (and what must not\",\n \" change), not a numbered procedure to follow.\",\n \"- Explicit quantity ranges: how many, how much, how long — never\",\n ' \"several\", \"some\", \"as needed\".',\n \"- No checkbox style: no `[ ]` items, no step numbering theatre.\",\n \"\",\n `Emit between ${MIN_TASKS} and ${MAX_TASKS} sub-tasks — unless the goal itself`,\n 'names a smaller explicit count (\"one sub-task per file\" with three files',\n \"means three); an explicit count in the goal always wins.\",\n `At most ${maxChildren} of them run at a time, so independent tasks are better`,\n \"than long chains. Each task's class picks its provider: implement, draft,\",\n \"review, research, spotcheck, simple, complex, image.\",\n \"\",\n \"Read the repository first (Glob/Grep/Read) so tasks name real files.\",\n \"Every sub-task runs in its own worktree of the same repository: name files\",\n \"relative to the repository root and never an absolute path or a worktree\",\n \"directory in a title, brief, files or acceptance entry.\",\n \"\",\n \"## Goal\",\n \"\",\n goal,\n ].join(\"\\n\");\n}\n\n/** The child's prompt for one planned sub-task. */\nexport function taskPrompt(goal: string, task: PlannerTask, index: number, of: number): string {\n return [\n `You are sub-task ${index + 1} of ${of} of a planned goal. Do exactly this sub-task;`,\n \"the other sub-tasks are other workers' business.\",\n \"\",\n `# ${task.title}`,\n \"\",\n task.brief,\n ...(task.files?.length ? [\"\", \"Files likely touched: \" + task.files.join(\", \")] : []),\n ...(task.acceptance?.length\n ? [\"\", \"Done means:\", ...task.acceptance.map((a) => `- ${a}`)]\n : []),\n \"\",\n \"## The overall goal (context only — your scope is the sub-task above)\",\n \"\",\n goal,\n ].join(\"\\n\");\n}\n\nexport interface PlannerDeps {\n /** Stage runner; tests inject a mock, production uses runWorker. */\n runStage?: (opts: RunOptions) => Promise<number>;\n /** logDir override for tests. */\n logDir?: string;\n /** maxChildren override for tests. */\n maxChildren?: number;\n}\n\n/**\n * The planner orchestration; returns the process exit code. The planner id is\n * minted here and preset on the phase-1 run, so the plan file's name is known\n * before the worker starts and the prompt can name its exact path.\n */\nexport async function runPlanner(opts: RunOptions, deps: PlannerDeps = {}): Promise<number> {\n const runStage = deps.runStage ?? runWorker;\n const { workers: config } = readWorkersSection();\n const logDir = deps.logDir ?? workersLogDir(config);\n const maxChildren = deps.maxChildren ?? config.tree.maxChildren;\n const parsed = parseRunnerArgs(opts.claudeArgs);\n const goal = parsed.prompt ?? \"\";\n const plannerId = newWorkerId();\n const planFile = planPathFor(logDir, plannerId);\n mkdirSync(join(logDir, \"plans\"), { recursive: true });\n\n // --- phase 1: the planner worker writes the plan file\n process.stderr.write(`planner ${plannerId}: writing plan (${planFile})\\n`);\n const rc1 = await runStage({\n ...opts,\n id: plannerId,\n claudeArgs: swapPromptArg(opts.claudeArgs, plannerPrompt(goal, planFile, maxChildren)),\n quiet: true,\n _planner: true,\n });\n if (rc1 !== 0) return rc1;\n if (!existsSync(planFile)) {\n process.stderr.write(\n `planner ${plannerId}: no plan file at ${planFile} — the planner worker did not write one. ` +\n `Re-run, or write the plan yourself and run the tasks with pai worker run.\\n`\n );\n appendLedger(ledgerPath(logDir), \"WORKER-PLAN-END\", { planner: plannerId, rc: 1, failed: \"plan\" });\n return 1;\n }\n let tasks: PlannerTask[];\n try {\n tasks = validatePlan(JSON.parse(readFileSync(planFile, \"utf8\")));\n } catch (e) {\n process.stderr.write(`planner ${plannerId}: invalid plan file ${planFile}: ${(e as Error).message}\\n`);\n appendLedger(ledgerPath(logDir), \"WORKER-PLAN-END\", { planner: plannerId, rc: 1, failed: \"plan\" });\n return 1;\n }\n appendLedger(ledgerPath(logDir), \"WORKER-PLAN\", {\n planner: plannerId,\n tasks: tasks.length,\n plan: planFile,\n });\n\n // --- phase 2: run the sub-tasks as children, maxChildren at a time. Each\n // child is spawned in the planner's own worktree (when it got one) rather\n // than the operator's cwd, so its branch is created off the planner's own\n // HEAD and can later be merged straight into it.\n const plannerStatus = loadStatus(logDir, plannerId);\n const plannerWorktreeDir = plannerStatus?.worktreeDir ?? null;\n const results: { task: PlannerTask; rc: number; id: string }[] = [];\n for (let i = 0; i < tasks.length; i += maxChildren) {\n const wave = tasks.slice(i, i + maxChildren);\n process.stderr.write(\n `planner ${plannerId}: sub-tasks ${i + 1}–${i + wave.length} of ${tasks.length}\\n`\n );\n const waveIds: string[] = new Array(wave.length).fill(\"\");\n const rcs = await Promise.all(\n wave.map((task, w) =>\n runStage({\n className: task.class ?? \"implement\",\n providerFlag: opts.providerFlag,\n modelFlag: opts.modelFlag,\n label: shortText(task.title, 40),\n noPane: opts.noPane,\n mcpFlag: opts.mcpFlag,\n claudeArgs: swapPromptArg(opts.claudeArgs, taskPrompt(goal, task, i + w, tasks.length)),\n cwd: plannerWorktreeDir ?? opts.cwd,\n worktreeFlag: opts.worktreeFlag,\n parent: plannerId,\n quiet: true,\n onWorkerStart: (wid) => {\n waveIds[w] = wid;\n },\n })\n )\n );\n for (let w = 0; w < wave.length; w++) results.push({ task: wave[w], rc: rcs[w], id: waveIds[w] });\n }\n\n // --- phase 3: the planner merges each finished child's branch into its\n // OWN worktree branch, in spawn order — never into the operator's checkout\n // — reusing `pai worker merge`'s own logic (mergeWorker). A conflicting\n // merge is aborted cleanly so the planner's branch stays clean and the\n // next child still gets a try; the child's branch is left intact either way.\n const merged: string[] = [];\n const conflicted: { id: string; branch: string; paths: string[] }[] = [];\n if (plannerWorktreeDir) {\n for (const r of results) {\n if (r.rc !== 0 || !r.id) continue;\n const st = loadStatus(logDir, r.id);\n if (!st?.branch) continue; // ran in place, or no worktree: nothing to merge\n try {\n mergeWorker(logDir, r.id);\n merged.push(r.id);\n } catch {\n let paths: string[] = [];\n try {\n git(plannerWorktreeDir, [\"rev-parse\", \"-q\", \"--verify\", \"MERGE_HEAD\"]);\n paths = git(plannerWorktreeDir, [\"diff\", \"--name-only\", \"--diff-filter=U\"])\n .split(\"\\n\")\n .filter(Boolean);\n git(plannerWorktreeDir, [\"merge\", \"--abort\"]);\n } catch {\n // no merge was left in progress: the failure was not a conflict\n }\n conflicted.push({ id: r.id, branch: st.branch, paths });\n }\n }\n }\n\n // --- phase 4: summary from the children's statuses and inbox handoffs\n const failed = results.filter((r) => r.rc !== 0).length;\n appendLedger(ledgerPath(logDir), \"WORKER-PLAN-END\", {\n planner: plannerId,\n rc: failed || conflicted.length ? 1 : 0,\n failed,\n merged: merged.length,\n conflicted: conflicted.length,\n });\n printPlannerSummary(logDir, plannerId, results, merged, conflicted, parsed.outputFormat, opts.quiet === true);\n return failed || conflicted.length ? 1 : 0;\n}\n\n/** Compose and print the planner's summary report (text or json). */\nfunction printPlannerSummary(\n logDir: string,\n plannerId: string,\n results: { task: PlannerTask; rc: number; id: string }[],\n merged: string[],\n conflicted: { id: string; branch: string; paths: string[] }[],\n fmt: \"text\" | \"json\" | \"stream-json\",\n quiet: boolean\n): void {\n const ok = results.filter((r) => r.rc === 0).length;\n const attempted = new Set([...merged, ...conflicted.map((c) => c.id)]);\n // a branch never attempted (no planner worktree to merge into) still needs\n // the old \"go merge me by hand\" hint\n const stillOpen: { id: string; branch: string }[] = [];\n for (const r of results) {\n if (r.rc !== 0 || !r.id || attempted.has(r.id)) continue;\n const st = loadStatus(logDir, r.id);\n if (st?.branch && !st.merged) stillOpen.push({ id: r.id, branch: st.branch });\n }\n const report: WorkerReport = {\n checks: results.map((r) => ({\n name: r.task.title,\n ok: r.rc === 0,\n detail: r.rc === 0 ? shortText(r.id ? loadStatus(logDir, r.id)?.last : \"\", 80) : `rc=${r.rc}`,\n })),\n ...(merged.length || conflicted.length || stillOpen.length\n ? {\n open: [\n ...merged.map((id) => `merged: ${id}`),\n ...conflicted.map(\n (c) =>\n `conflict: pai worker merge ${c.id} (${c.branch})` +\n (c.paths.length ? ` — ${c.paths.join(\", \")}` : \"\")\n ),\n ...stillOpen.map((c) => `branch to merge: pai worker merge ${c.id} (${c.branch})`),\n ],\n }\n : {}),\n notes:\n `${ok}/${results.length} sub-tasks ok` +\n (merged.length ? `; ${merged.length} merged` : \"\") +\n (conflicted.length ? `; ${conflicted.length} conflict(s)` : \"\") +\n (stillOpen.length ? `; ${stillOpen.length} branch(es) to merge` : \"\"),\n };\n if (quiet || fmt === \"stream-json\") return;\n const allOk = ok === results.length && conflicted.length === 0;\n const resultEvent: StreamEvent = {\n type: \"result\",\n result: [\n `planner ${plannerId}: ${report.notes}`,\n ...(conflicted.length ? [\"conflicts:\"] : []),\n ...conflicted.map(\n (c) =>\n ` pai worker merge ${c.id} # ${c.branch}, conflicting: ${c.paths.join(\", \") || \"unknown\"}`\n ),\n ...(stillOpen.length ? [\"branches to merge:\"] : []),\n ...stillOpen.map((c) => ` pai worker merge ${c.id} # ${c.branch}`),\n ].join(\"\\n\"),\n is_error: !allOk,\n };\n printResult(fmt, resultEvent, allOk ? 0 : 1, logDir, plannerId, report, {\n plan: results.length,\n ...(merged.length ? { mergedBranches: merged } : {}),\n ...(conflicted.length ? { conflictedBranches: conflicted.map((c) => c.branch) } : {}),\n ...(stillOpen.length ? { branches: stillOpen.map((c) => c.branch) } : {}),\n });\n}\n"],"mappings":"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"}
|