@tekmidian/pai 0.1.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 (66) hide show
  1. package/ARCHITECTURE.md +567 -0
  2. package/FEATURE.md +108 -0
  3. package/LICENSE +21 -0
  4. package/README.md +101 -0
  5. package/dist/auto-route-D7W6RE06.mjs +86 -0
  6. package/dist/auto-route-D7W6RE06.mjs.map +1 -0
  7. package/dist/cli/index.d.mts +1 -0
  8. package/dist/cli/index.mjs +5927 -0
  9. package/dist/cli/index.mjs.map +1 -0
  10. package/dist/config-DBh1bYM2.mjs +151 -0
  11. package/dist/config-DBh1bYM2.mjs.map +1 -0
  12. package/dist/daemon/index.d.mts +1 -0
  13. package/dist/daemon/index.mjs +56 -0
  14. package/dist/daemon/index.mjs.map +1 -0
  15. package/dist/daemon-mcp/index.d.mts +1 -0
  16. package/dist/daemon-mcp/index.mjs +185 -0
  17. package/dist/daemon-mcp/index.mjs.map +1 -0
  18. package/dist/daemon-v5O897D4.mjs +773 -0
  19. package/dist/daemon-v5O897D4.mjs.map +1 -0
  20. package/dist/db-4lSqLFb8.mjs +199 -0
  21. package/dist/db-4lSqLFb8.mjs.map +1 -0
  22. package/dist/db-BcDxXVBu.mjs +110 -0
  23. package/dist/db-BcDxXVBu.mjs.map +1 -0
  24. package/dist/detect-BHqYcjJ1.mjs +86 -0
  25. package/dist/detect-BHqYcjJ1.mjs.map +1 -0
  26. package/dist/detector-DKA83aTZ.mjs +74 -0
  27. package/dist/detector-DKA83aTZ.mjs.map +1 -0
  28. package/dist/embeddings-mfqv-jFu.mjs +91 -0
  29. package/dist/embeddings-mfqv-jFu.mjs.map +1 -0
  30. package/dist/factory-BDAiKtYR.mjs +42 -0
  31. package/dist/factory-BDAiKtYR.mjs.map +1 -0
  32. package/dist/index.d.mts +307 -0
  33. package/dist/index.d.mts.map +1 -0
  34. package/dist/index.mjs +11 -0
  35. package/dist/indexer-B20bPHL-.mjs +677 -0
  36. package/dist/indexer-B20bPHL-.mjs.map +1 -0
  37. package/dist/indexer-backend-BXaocO5r.mjs +360 -0
  38. package/dist/indexer-backend-BXaocO5r.mjs.map +1 -0
  39. package/dist/ipc-client-DPy7s3iu.mjs +156 -0
  40. package/dist/ipc-client-DPy7s3iu.mjs.map +1 -0
  41. package/dist/mcp/index.d.mts +1 -0
  42. package/dist/mcp/index.mjs +373 -0
  43. package/dist/mcp/index.mjs.map +1 -0
  44. package/dist/migrate-Bwj7qPaE.mjs +241 -0
  45. package/dist/migrate-Bwj7qPaE.mjs.map +1 -0
  46. package/dist/pai-marker-DX_mFLum.mjs +186 -0
  47. package/dist/pai-marker-DX_mFLum.mjs.map +1 -0
  48. package/dist/postgres-Ccvpc6fC.mjs +335 -0
  49. package/dist/postgres-Ccvpc6fC.mjs.map +1 -0
  50. package/dist/rolldown-runtime-95iHPtFO.mjs +18 -0
  51. package/dist/schemas-DjdwzIQ8.mjs +3405 -0
  52. package/dist/schemas-DjdwzIQ8.mjs.map +1 -0
  53. package/dist/search-PjftDxxs.mjs +282 -0
  54. package/dist/search-PjftDxxs.mjs.map +1 -0
  55. package/dist/sqlite-CHUrNtbI.mjs +90 -0
  56. package/dist/sqlite-CHUrNtbI.mjs.map +1 -0
  57. package/dist/tools-CLK4080-.mjs +805 -0
  58. package/dist/tools-CLK4080-.mjs.map +1 -0
  59. package/dist/utils-DEWdIFQ0.mjs +160 -0
  60. package/dist/utils-DEWdIFQ0.mjs.map +1 -0
  61. package/package.json +72 -0
  62. package/templates/README.md +181 -0
  63. package/templates/agent-prefs.example.md +362 -0
  64. package/templates/claude-md.template.md +733 -0
  65. package/templates/pai-project.template.md +13 -0
  66. package/templates/voices.example.json +251 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.mjs","names":["slugify","getProject","upsertTag","cmdList","cmdInfo","cmdTag","_homedir","getProject","toTitleCase","findClaudeNotesDir","cmdMigrate","loadConfig","CLAUDE_JSON_PATH","readClaudeJson","writeClaudeJson","cmdInstall","cmdStatus","HOME","cmdStatus","loadConfig","HOME","REGISTRY_DB","CONFIG_FILE","BACKUPS_DIR","DOCKER_CONTAINER","PG_DATABASE","PG_USER","line","CONFIG_FILE","getTemplatesDir","loadConfig","CONFIG_FILE","cmdStatus","makeClient","loadConfig","loadConfig"],"sources":["../../src/session/promote.ts","../../src/cli/commands/project.ts","../../src/session/slug-generator.ts","../../src/cli/commands/session.ts","../../src/cli/commands/session-cleanup.ts","../../src/cli/commands/registry.ts","../../src/cli/commands/memory.ts","../../src/cli/commands/mcp.ts","../../src/cli/commands/daemon.ts","../../src/cli/commands/backup.ts","../../src/cli/commands/restore.ts","../../src/cli/commands/setup.ts","../../src/obsidian/sync.ts","../../src/obsidian/status.ts","../../src/cli/commands/obsidian.ts","../../src/cli/commands/update.ts","../../src/cli/commands/notify.ts","../../src/cli/commands/topic.ts","../../src/cli/index.ts"],"sourcesContent":["/**\n * pai project promote --from-session <path> --to <new-project-path> [--name \"Name\"]\n *\n * Promotes a session note into a new standalone project:\n * 1. Validates inputs\n * 2. Scaffolds the new project directory\n * 3. Copies the session note and creates a MEMORY.md\n * 4. Registers the new project in the PAI registry\n * 5. Optionally backlinks from the source project's TODO.md\n */\n\nimport type { Database } from \"better-sqlite3\";\nimport {\n existsSync,\n mkdirSync,\n copyFileSync,\n readFileSync,\n appendFileSync,\n writeFileSync,\n} from \"node:fs\";\nimport { join, basename, dirname, resolve } from \"node:path\";\nimport {\n ok,\n warn,\n err,\n dim,\n bold,\n slugify,\n encodeDir,\n resolvePath,\n scaffoldProjectDirs,\n now,\n} from \"../cli/utils.js\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface PromoteOptions {\n fromSession: string;\n to: string;\n name?: string;\n}\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Derive a human-readable project name from a session note filename.\n * \"0042 - 2026-02-24 - Dark Mode Implementation.md\" → \"Dark Mode Implementation\"\n */\nfunction deriveNameFromFilename(filename: string): string {\n const base = basename(filename, \".md\");\n // Strip leading \"NNNN - YYYY-MM-DD - \" prefix if present\n const match = base.match(/^\\d+ - \\d{4}-\\d{2}-\\d{2} - (.+)$/);\n return match ? match[1] : base;\n}\n\n/**\n * Extract the first N H2 (##) section headings from markdown content.\n */\nfunction extractH2Headings(content: string, limit = 2): string[] {\n const headings: string[] = [];\n for (const m of content.matchAll(/^## .+$/gm)) {\n headings.push(m[0]);\n if (headings.length >= limit) break;\n }\n return headings;\n}\n\n/**\n * Find the source project root from the session note path.\n * Convention: session note lives at {project-root}/Notes/{filename}\n * So: dirname(dirname(sessionPath)) = project root.\n */\nfunction findSourceProjectRoot(sessionPath: string): string {\n const notesDir = dirname(resolve(sessionPath));\n return dirname(notesDir);\n}\n\n// ---------------------------------------------------------------------------\n// Main implementation\n// ---------------------------------------------------------------------------\n\nexport function cmdPromote(db: Database, opts: PromoteOptions): void {\n const sessionPath = resolvePath(opts.fromSession);\n const targetPath = resolvePath(opts.to);\n\n // ---- Validate inputs ----\n\n if (!existsSync(sessionPath)) {\n console.error(err(`Session note not found: ${sessionPath}`));\n process.exit(1);\n }\n if (!sessionPath.endsWith(\".md\")) {\n console.error(err(`--from-session must be a markdown (.md) file: ${sessionPath}`));\n process.exit(1);\n }\n if (existsSync(targetPath)) {\n console.error(err(`Target path already exists: ${targetPath}`));\n process.exit(1);\n }\n\n // ---- Derive project name and slug ----\n\n const displayName = opts.name ?? deriveNameFromFilename(sessionPath);\n const slug = slugify(displayName);\n\n if (!slug) {\n console.error(err(`Could not derive a valid slug from name: \"${displayName}\"`));\n process.exit(1);\n }\n\n // ---- Check registry for conflicts ----\n\n const encodedDir = encodeDir(targetPath);\n const existing = db\n .prepare(\"SELECT id FROM projects WHERE slug = ? OR root_path = ? OR encoded_dir = ?\")\n .get(slug, targetPath, encodedDir);\n if (existing) {\n console.error(\n err(`A project with slug \"${slug}\" or path \"${targetPath}\" is already registered.`)\n );\n process.exit(1);\n }\n\n // ---- Scaffold new project ----\n\n scaffoldProjectDirs(targetPath);\n\n // ---- Copy session note ----\n\n const today = new Date().toISOString().slice(0, 10);\n const sourceBasename = basename(sessionPath);\n const sourceProjectRoot = findSourceProjectRoot(sessionPath);\n const sourceProjectName = basename(sourceProjectRoot);\n\n const destNoteName = `0001 - ${today} - Promoted from ${sourceProjectName}.md`;\n const destNotePath = join(targetPath, \"Notes\", destNoteName);\n\n copyFileSync(sessionPath, destNotePath);\n\n // ---- Create MEMORY.md ----\n\n const sessionContent = readFileSync(sessionPath, \"utf-8\");\n const headings = extractH2Headings(sessionContent);\n\n const memoryContent = [\n `# Project Memory`,\n ``,\n `## Origin`,\n ``,\n `- **Promoted from:** ${sessionPath}`,\n `- **Source project:** ${sourceProjectRoot}`,\n `- **Date of promotion:** ${today}`,\n `- **Original session note:** ${sourceBasename}`,\n ``,\n `## Initial Context`,\n ``,\n headings.length > 0\n ? `Key topics from the source session:\\n\\n${headings.map((h) => `- ${h.replace(/^## /, \"\")}`).join(\"\\n\")}`\n : `(Extracted from session note — see ${destNoteName})`,\n ``,\n ].join(\"\\n\");\n\n writeFileSync(join(targetPath, \"Notes\", \"MEMORY.md\"), memoryContent, \"utf-8\");\n\n // ---- Register in PAI registry ----\n\n const ts = now();\n db.prepare(\n `INSERT INTO projects\n (slug, display_name, root_path, encoded_dir, type, status, created_at, updated_at)\n VALUES (?, ?, ?, ?, 'local', 'active', ?, ?)`\n ).run(slug, displayName, targetPath, encodedDir, ts, ts);\n\n // ---- Backlink in source TODO.md ----\n\n const sourceTodoPath = join(sourceProjectRoot, \"Notes\", \"TODO.md\");\n if (existsSync(sourceTodoPath)) {\n const backlink = `\\n- Promoted to project: [${slug}](${targetPath})\\n`;\n appendFileSync(sourceTodoPath, backlink, \"utf-8\");\n console.log(dim(` Backlink added to: ${sourceTodoPath}`));\n }\n\n // ---- Success output ----\n\n console.log();\n console.log(ok(`Project promoted: ${bold(slug)}`));\n console.log(dim(` Display name: ${displayName}`));\n console.log(dim(` Path: ${targetPath}`));\n console.log(dim(` Slug: ${slug}`));\n console.log(dim(` Session note: ${destNoteName}`));\n console.log(dim(` Memory: Notes/MEMORY.md created`));\n console.log();\n console.log(dim(` Next: cd ${targetPath} && pai session list ${slug}`));\n}\n","/**\n * pai project <sub-command>\n *\n * add, list, info, archive, unarchive, move, tag, alias, edit,\n * detect, health, consolidate, promote\n */\n\nimport type { Command } from \"commander\";\nimport type { Database } from \"better-sqlite3\";\nimport { existsSync, readdirSync, statSync, mkdirSync, renameSync } from \"node:fs\";\nimport { join, basename } from \"node:path\";\nimport { homedir as _homedir } from \"node:os\";\nimport {\n ok,\n warn,\n err,\n dim,\n bold,\n header,\n slugFromPath,\n encodeDir,\n resolvePath,\n scaffoldProjectDirs,\n renderTable,\n shortenPath,\n fmtDate,\n now,\n} from \"../utils.js\";\nimport {\n detectProject,\n formatDetection,\n formatDetectionJson,\n} from \"./detect.js\";\nimport { cmdPromote } from \"../../session/promote.js\";\nimport { ensurePaiMarker } from \"../../registry/pai-marker.js\";\nimport chalk from \"chalk\";\n\n// ---------------------------------------------------------------------------\n// Row types (mirrors the SQLite schema)\n// ---------------------------------------------------------------------------\n\ninterface ProjectRow {\n id: number;\n slug: string;\n display_name: string;\n root_path: string;\n encoded_dir: string;\n type: string;\n status: string;\n created_at: number;\n updated_at: number;\n archived_at: number | null;\n}\n\ninterface SessionRow {\n id: number;\n project_id: number;\n number: number;\n date: string;\n title: string;\n status: string;\n closed_at: number | null;\n}\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\nfunction getProject(db: Database, slug: string): ProjectRow | undefined {\n // Check direct slug first, then aliases\n const direct = db\n .prepare(\"SELECT * FROM projects WHERE slug = ?\")\n .get(slug) as ProjectRow | undefined;\n if (direct) return direct;\n\n const alias = db\n .prepare(\n `SELECT p.* FROM projects p\n JOIN aliases a ON a.project_id = p.id\n WHERE a.alias = ?`\n )\n .get(slug) as ProjectRow | undefined;\n return alias;\n}\n\nfunction requireProject(db: Database, slug: string): ProjectRow {\n const project = getProject(db, slug);\n if (!project) {\n console.error(err(`Project not found: ${slug}`));\n process.exit(1);\n }\n return project;\n}\n\n/**\n * Resolve an identifier that may be a list index number or a slug.\n */\nfunction resolveIdentifier(db: Database, identifier: string): ProjectRow | undefined {\n const num = parseInt(identifier, 10);\n if (!isNaN(num) && num > 0 && String(num) === identifier) {\n const rows = db.prepare(\n \"SELECT * FROM projects ORDER BY status ASC, updated_at DESC\"\n ).all() as ProjectRow[];\n if (num <= rows.length) return rows[num - 1];\n }\n return getProject(db, identifier);\n}\n\nfunction getProjectTags(db: Database, projectId: number): string[] {\n const rows = db\n .prepare(\n `SELECT t.name FROM tags t\n JOIN project_tags pt ON pt.tag_id = t.id\n WHERE pt.project_id = ?\n ORDER BY t.name`\n )\n .all(projectId) as { name: string }[];\n return rows.map((r) => r.name);\n}\n\nfunction getProjectAliases(db: Database, projectId: number): string[] {\n const rows = db\n .prepare(\"SELECT alias FROM aliases WHERE project_id = ? ORDER BY alias\")\n .all(projectId) as { alias: string }[];\n return rows.map((r) => r.alias);\n}\n\nfunction getSessionCount(db: Database, projectId: number): number {\n const row = db\n .prepare(\"SELECT COUNT(*) AS cnt FROM sessions WHERE project_id = ?\")\n .get(projectId) as { cnt: number };\n return row.cnt;\n}\n\nfunction getLastSessionDate(db: Database, projectId: number): number | null {\n const row = db\n .prepare(\n `SELECT created_at FROM sessions WHERE project_id = ?\n ORDER BY created_at DESC LIMIT 1`\n )\n .get(projectId) as { created_at: number } | undefined;\n return row ? row.created_at : null;\n}\n\nfunction upsertTag(db: Database, tagName: string): number {\n db.prepare(\"INSERT OR IGNORE INTO tags (name) VALUES (?)\").run(tagName);\n const row = db.prepare(\"SELECT id FROM tags WHERE name = ?\").get(tagName) as {\n id: number;\n };\n return row.id;\n}\n\n// ---------------------------------------------------------------------------\n// Command implementations\n// ---------------------------------------------------------------------------\n\nfunction cmdAdd(\n db: Database,\n rawPath: string,\n opts: {\n slug?: string;\n type?: string;\n displayName?: string;\n }\n): void {\n const rootPath = resolvePath(rawPath);\n const slug = opts.slug ?? slugFromPath(rootPath);\n const encodedDir = encodeDir(rootPath);\n const displayName = opts.displayName ?? slug;\n const type = opts.type ?? \"local\";\n\n // Validate type\n const validTypes = [\"local\", \"central\", \"obsidian-linked\", \"external\"];\n if (!validTypes.includes(type)) {\n console.error(err(`Invalid type \"${type}\". Valid: ${validTypes.join(\", \")}`));\n process.exit(1);\n }\n\n // Check for duplicate slug\n const existing = db\n .prepare(\"SELECT id FROM projects WHERE slug = ? OR root_path = ?\")\n .get(slug, rootPath);\n if (existing) {\n console.error(err(`Project already registered (slug: ${slug} or path: ${rootPath})`));\n process.exit(1);\n }\n\n const ts = now();\n db.prepare(\n `INSERT INTO projects\n (slug, display_name, root_path, encoded_dir, type, status, created_at, updated_at)\n VALUES (?, ?, ?, ?, ?, 'active', ?, ?)`\n ).run(slug, displayName, rootPath, encodedDir, type, ts, ts);\n\n // Create directory scaffolding\n scaffoldProjectDirs(rootPath);\n\n // Create PAI.md marker file in Notes/\n try {\n ensurePaiMarker(rootPath, slug, displayName);\n } catch {\n // Non-fatal — warn but don't fail the add command.\n }\n\n console.log(ok(`Project added: ${bold(slug)}`));\n console.log(dim(` Path: ${rootPath}`));\n console.log(dim(` Encoded dir: ${encodedDir}`));\n console.log(dim(` Type: ${type}`));\n}\n\nfunction cmdList(\n db: Database,\n opts: {\n status?: string;\n tag?: string;\n type?: string;\n }\n): void {\n let query = `\n SELECT p.*,\n (SELECT COUNT(*) FROM sessions s WHERE s.project_id = p.id) AS session_count,\n (SELECT MAX(s.created_at) FROM sessions s WHERE s.project_id = p.id) AS last_active\n FROM projects p\n `;\n const params: unknown[] = [];\n\n const where: string[] = [];\n if (opts.status) {\n where.push(\"p.status = ?\");\n params.push(opts.status);\n }\n if (opts.type) {\n where.push(\"p.type = ?\");\n params.push(opts.type);\n }\n if (opts.tag) {\n where.push(`p.id IN (\n SELECT pt.project_id FROM project_tags pt\n JOIN tags t ON t.id = pt.tag_id WHERE t.name = ?\n )`);\n params.push(opts.tag);\n }\n\n if (where.length) {\n query += \" WHERE \" + where.join(\" AND \");\n }\n query += \" ORDER BY p.status ASC, p.updated_at DESC\";\n\n const rows = db.prepare(query).all(...params) as (ProjectRow & {\n session_count: number;\n last_active: number | null;\n })[];\n\n if (!rows.length) {\n console.log(warn(\"No projects found.\"));\n return;\n }\n\n const tableRows = rows.map((r, i) => [\n dim(String(i + 1)),\n bold(r.slug),\n dim(shortenPath(r.root_path, 50)),\n r.status === \"active\" ? chalk.green(r.status) : chalk.yellow(r.status),\n dim(r.type),\n String(r.session_count),\n fmtDate(r.last_active),\n ]);\n\n console.log(\n renderTable(\n [\"#\", \"Slug\", \"Path\", \"Status\", \"Type\", \"Sessions\", \"Last Active\"],\n tableRows\n )\n );\n console.log();\n console.log(dim(` ${rows.length} project(s)`));\n}\n\nfunction cmdInfo(db: Database, identifier: string): void {\n const project = resolveIdentifier(db, identifier) ?? requireProject(db, identifier);\n const tags = getProjectTags(db, project.id);\n const aliases = getProjectAliases(db, project.id);\n const sessionCount = getSessionCount(db, project.id);\n const lastSession = getLastSessionDate(db, project.id);\n\n const recentSessions = db\n .prepare(\n `SELECT * FROM sessions WHERE project_id = ?\n ORDER BY created_at DESC LIMIT 5`\n )\n .all(project.id) as SessionRow[];\n\n console.log();\n console.log(header(` ${project.display_name}`));\n console.log();\n console.log(` ${bold(\"Slug:\")} ${project.slug}`);\n console.log(` ${bold(\"Path:\")} ${project.root_path}`);\n console.log(` ${bold(\"Encoded dir:\")} ${project.encoded_dir}`);\n console.log(` ${bold(\"Type:\")} ${project.type}`);\n console.log(\n ` ${bold(\"Status:\")} ${project.status === \"active\" ? chalk.green(project.status) : chalk.yellow(project.status)}`\n );\n console.log(\n ` ${bold(\"Tags:\")} ${tags.length ? tags.map((t) => chalk.cyan(t)).join(\", \") : dim(\"none\")}`\n );\n console.log(\n ` ${bold(\"Aliases:\")} ${aliases.length ? aliases.join(\", \") : dim(\"none\")}`\n );\n console.log(` ${bold(\"Sessions:\")} ${sessionCount}`);\n console.log(` ${bold(\"Last active:\")} ${fmtDate(lastSession)}`);\n console.log(` ${bold(\"Created:\")} ${fmtDate(project.created_at)}`);\n if (project.archived_at) {\n console.log(` ${bold(\"Archived:\")} ${fmtDate(project.archived_at)}`);\n }\n\n if (recentSessions.length) {\n console.log();\n console.log(` ${bold(\"Recent sessions:\")}`);\n const sessionRows = recentSessions.map((s) => [\n dim(`#${s.number}`),\n s.date,\n s.title.length > 50 ? s.title.slice(0, 47) + \"...\" : s.title,\n s.status === \"completed\"\n ? chalk.green(s.status)\n : chalk.yellow(s.status),\n ]);\n console.log(\n renderTable([\"#\", \"Date\", \"Title\", \"Status\"], sessionRows)\n .split(\"\\n\")\n .map((l) => \" \" + l)\n .join(\"\\n\")\n );\n }\n console.log();\n}\n\nfunction cmdArchive(db: Database, slug: string): void {\n const project = requireProject(db, slug);\n if (project.status === \"archived\") {\n console.log(warn(`Project ${slug} is already archived.`));\n return;\n }\n const ts = now();\n db.prepare(\n \"UPDATE projects SET status = 'archived', archived_at = ?, updated_at = ? WHERE id = ?\"\n ).run(ts, ts, project.id);\n console.log(ok(`Archived: ${bold(slug)}`));\n}\n\nfunction cmdUnarchive(db: Database, slug: string): void {\n const project = requireProject(db, slug);\n if (project.status !== \"archived\") {\n console.log(warn(`Project ${slug} is not archived (status: ${project.status}).`));\n return;\n }\n const ts = now();\n db.prepare(\n \"UPDATE projects SET status = 'active', archived_at = NULL, updated_at = ? WHERE id = ?\"\n ).run(ts, project.id);\n console.log(ok(`Unarchived: ${bold(slug)}`));\n}\n\nfunction cmdMove(db: Database, slug: string, newPath: string): void {\n const project = requireProject(db, slug);\n const resolvedNew = resolvePath(newPath);\n const newEncoded = encodeDir(resolvedNew);\n const ts = now();\n\n db.prepare(\n \"UPDATE projects SET root_path = ?, encoded_dir = ?, updated_at = ? WHERE id = ?\"\n ).run(resolvedNew, newEncoded, ts, project.id);\n\n console.log(ok(`Moved: ${bold(slug)}`));\n console.log(dim(` Old path: ${project.root_path}`));\n console.log(dim(` New path: ${resolvedNew}`));\n}\n\nfunction cmdTag(db: Database, slug: string, tags: string[]): void {\n const project = requireProject(db, slug);\n\n const added: string[] = [];\n const skipped: string[] = [];\n\n for (const tagName of tags) {\n const tagId = upsertTag(db, tagName);\n const exists = db\n .prepare(\n \"SELECT 1 FROM project_tags WHERE project_id = ? AND tag_id = ?\"\n )\n .get(project.id, tagId);\n if (exists) {\n skipped.push(tagName);\n } else {\n db.prepare(\n \"INSERT INTO project_tags (project_id, tag_id) VALUES (?, ?)\"\n ).run(project.id, tagId);\n added.push(tagName);\n }\n }\n\n if (added.length) {\n console.log(ok(`Tagged ${bold(slug)}: ${added.map((t) => chalk.cyan(t)).join(\", \")}`));\n }\n if (skipped.length) {\n console.log(dim(` Already present: ${skipped.join(\", \")}`));\n }\n}\n\nfunction cmdAlias(db: Database, slug: string, alias: string): void {\n requireProject(db, slug);\n\n // Make sure alias isn't already a slug for another project\n const conflict = db\n .prepare(\"SELECT id FROM projects WHERE slug = ?\")\n .get(alias);\n if (conflict) {\n console.error(err(`\"${alias}\" is already a project slug — cannot use as alias.`));\n process.exit(1);\n }\n\n const project = getProject(db, slug)!;\n try {\n db.prepare(\n \"INSERT INTO aliases (alias, project_id) VALUES (?, ?)\"\n ).run(alias, project.id);\n console.log(ok(`Alias added: ${bold(alias)} → ${slug}`));\n } catch {\n console.error(err(`Alias \"${alias}\" is already registered.`));\n process.exit(1);\n }\n}\n\nfunction cmdEdit(\n db: Database,\n slug: string,\n opts: { displayName?: string; type?: string }\n): void {\n const project = requireProject(db, slug);\n\n if (!opts.displayName && !opts.type) {\n console.log(warn(\"Nothing to update. Use --display-name or --type.\"));\n return;\n }\n\n const validTypes = [\"local\", \"central\", \"obsidian-linked\", \"external\"];\n if (opts.type && !validTypes.includes(opts.type)) {\n console.error(err(`Invalid type \"${opts.type}\". Valid: ${validTypes.join(\", \")}`));\n process.exit(1);\n }\n\n const ts = now();\n if (opts.displayName) {\n db.prepare(\n \"UPDATE projects SET display_name = ?, updated_at = ? WHERE id = ?\"\n ).run(opts.displayName, ts, project.id);\n console.log(ok(`Display name updated: ${bold(opts.displayName)}`));\n }\n if (opts.type) {\n db.prepare(\n \"UPDATE projects SET type = ?, updated_at = ? WHERE id = ?\"\n ).run(opts.type, ts, project.id);\n console.log(ok(`Type updated: ${bold(opts.type)}`));\n }\n}\n\n// ---------------------------------------------------------------------------\n// Health command\n// ---------------------------------------------------------------------------\n\ninterface HealthRow extends ProjectRow {\n session_count: number;\n}\n\ntype HealthCategory = \"active\" | \"stale\" | \"dead\";\n\ninterface ProjectHealth {\n project: HealthRow;\n category: HealthCategory;\n /** For stale: a similar directory found on disk near the recorded path */\n suggestedPath?: string;\n claudeNotesExists: boolean;\n orphanedNotesDirs: string[];\n}\n\n/**\n * Find Claude project dirs (~/.claude/projects/) that look like they belong\n * to a project based on encoded_dir prefix matching.\n */\nfunction findOrphanedNotesDirs(project: ProjectRow): string[] {\n const claudeProjects = join(_homedir(), \".claude\", \"projects\");\n if (!existsSync(claudeProjects)) return [];\n\n const expected = encodeDir(project.root_path);\n const results: string[] = [];\n\n try {\n for (const entry of readdirSync(claudeProjects)) {\n const full = join(claudeProjects, entry);\n try {\n if (!statSync(full).isDirectory()) continue;\n } catch {\n continue;\n }\n // Look for encoded dirs that match this project's encoded_dir\n if (entry === expected || entry === project.encoded_dir) {\n const notesDir = join(full, \"Notes\");\n if (existsSync(notesDir)) {\n results.push(notesDir);\n }\n }\n }\n } catch {\n // Unreadable — ignore\n }\n return results;\n}\n\n/**\n * Try to find a moved project by looking for a directory with the same name\n * as the last path component in common nearby locations.\n */\nfunction suggestMovedPath(project: ProjectRow): string | undefined {\n const name = basename(project.root_path);\n // Common parent patterns to check\n const candidates = [\n join(_homedir(), \"dev\", name),\n join(_homedir(), \"dev\", \"ai\", name),\n join(_homedir(), \"Desktop\", name),\n join(_homedir(), \"Projects\", name),\n ];\n for (const c of candidates) {\n if (existsSync(c)) return c;\n }\n return undefined;\n}\n\nfunction cmdHealth(\n db: Database,\n opts: { fix?: boolean; json?: boolean; status?: string }\n): void {\n const rows = db\n .prepare(\n `SELECT p.*,\n (SELECT COUNT(*) FROM sessions s WHERE s.project_id = p.id) AS session_count\n FROM projects p\n ORDER BY p.status ASC, p.updated_at DESC`\n )\n .all() as HealthRow[];\n\n const results: ProjectHealth[] = rows.map((project) => {\n const pathExists = existsSync(project.root_path);\n const orphaned = findOrphanedNotesDirs(project);\n\n let category: HealthCategory;\n let suggestedPath: string | undefined;\n\n if (pathExists) {\n category = \"active\";\n } else {\n suggestedPath = suggestMovedPath(project);\n category = suggestedPath ? \"stale\" : \"dead\";\n }\n\n const claudeNotesExists = orphaned.length > 0;\n\n return {\n project,\n category,\n suggestedPath,\n claudeNotesExists,\n orphanedNotesDirs: orphaned,\n };\n });\n\n // Filter by status if requested\n const filtered =\n opts.status\n ? results.filter((r) => r.category === opts.status)\n : results;\n\n if (opts.json) {\n console.log(\n JSON.stringify(\n filtered.map((r) => ({\n slug: r.project.slug,\n root_path: r.project.root_path,\n status: r.project.status,\n health: r.category,\n session_count: r.project.session_count,\n suggested_path: r.suggestedPath ?? null,\n claude_notes_exists: r.claudeNotesExists,\n orphaned_notes_dirs: r.orphanedNotesDirs,\n })),\n null,\n 2\n )\n );\n return;\n }\n\n // Human-readable output\n const active = filtered.filter((r) => r.category === \"active\");\n const stale = filtered.filter((r) => r.category === \"stale\");\n const dead = filtered.filter((r) => r.category === \"dead\");\n\n console.log();\n console.log(header(\" PAI Project Health Report\"));\n console.log();\n console.log(\n ` ${chalk.green(\"Active:\")} ${active.length} ${chalk.yellow(\"Stale (moved?):\")} ${stale.length} ${chalk.red(\"Dead (missing):\")} ${dead.length}`\n );\n console.log();\n\n if (active.length) {\n console.log(bold(\" Active projects (path exists):\"));\n const tableRows = active.map((r) => [\n bold(r.project.slug),\n dim(shortenPath(r.project.root_path, 50)),\n String(r.project.session_count),\n r.claudeNotesExists ? chalk.green(\"yes\") : dim(\"no\"),\n ]);\n console.log(\n renderTable(\n [\"Slug\", \"Path\", \"Sessions\", \"Claude Notes\"],\n tableRows\n )\n .split(\"\\n\")\n .map((l) => \" \" + l)\n .join(\"\\n\")\n );\n console.log();\n }\n\n if (stale.length) {\n console.log(warn(\" Stale projects (path missing, possible new location found):\"));\n for (const r of stale) {\n console.log(` ${bold(r.project.slug)}`);\n console.log(dim(` Old path: ${r.project.root_path}`));\n console.log(chalk.cyan(` Found at: ${r.suggestedPath}`));\n if (r.claudeNotesExists) {\n console.log(chalk.green(` Notes: ${r.orphanedNotesDirs.join(\", \")}`));\n }\n if (opts.fix && r.suggestedPath) {\n const ts = now();\n const newEncoded = encodeDir(r.suggestedPath);\n db.prepare(\n \"UPDATE projects SET root_path = ?, encoded_dir = ?, updated_at = ? WHERE id = ?\"\n ).run(r.suggestedPath, newEncoded, ts, r.project.id);\n console.log(ok(` Auto-fixed: updated path to ${r.suggestedPath}`));\n } else if (r.suggestedPath) {\n console.log(dim(` Fix: pai project move ${r.project.slug} ${r.suggestedPath}`));\n }\n }\n console.log();\n }\n\n if (dead.length) {\n console.log(err(\" Dead projects (path missing, no match found):\"));\n for (const r of dead) {\n console.log(` ${bold(r.project.slug)} ${dim(r.project.root_path)}`);\n if (r.claudeNotesExists) {\n console.log(\n chalk.yellow(` Notes: ${r.orphanedNotesDirs.join(\", \")}`)\n );\n }\n if (r.project.session_count === 0 && opts.fix) {\n db.prepare(\n \"UPDATE projects SET status = 'archived', archived_at = ?, updated_at = ? WHERE id = ?\"\n ).run(now(), now(), r.project.id);\n console.log(ok(\" Auto-fixed: archived (0 sessions, path gone)\"));\n } else {\n console.log(\n dim(\n ` Fix: pai project archive ${r.project.slug} (or pai project move ...)`\n )\n );\n }\n }\n console.log();\n }\n\n const summary = ` ${rows.length} total: ${active.length} active, ${stale.length} stale, ${dead.length} dead`;\n console.log(dim(summary));\n\n if (!opts.fix && (stale.length > 0 || dead.length > 0)) {\n console.log();\n console.log(warn(\" Run with --fix to auto-remediate where possible.\"));\n }\n}\n\n// ---------------------------------------------------------------------------\n// Detect command\n// ---------------------------------------------------------------------------\n\nfunction cmdDetect(\n db: Database,\n pathArg: string | undefined,\n opts: { json?: boolean }\n): void {\n const cwd = pathArg ? resolvePath(pathArg) : process.cwd();\n const detection = detectProject(db, cwd);\n\n if (!detection) {\n if (opts.json) {\n console.log(JSON.stringify({ error: \"no_match\", cwd }, null, 2));\n } else {\n console.log(warn(`No registered project found for: ${cwd}`));\n console.log(dim(\" Run 'pai project add .' to register this directory.\"));\n }\n process.exit(0);\n return;\n }\n\n if (opts.json) {\n console.log(formatDetectionJson(detection));\n return;\n }\n\n console.log();\n console.log(header(\" Project Detection Result\"));\n console.log();\n console.log(\n formatDetection(detection)\n .split(\"\\n\")\n .map((l) => \" \" + l)\n .join(\"\\n\")\n );\n console.log();\n}\n\n// ---------------------------------------------------------------------------\n// Consolidate command\n// ---------------------------------------------------------------------------\n\n/**\n * Find all ~/.claude/projects/ encoded dirs whose name encodes to a path\n * that is a child-of or exact-match of the given project's root_path.\n */\nfunction findProjectNotesDirs(project: ProjectRow): {\n encodedDir: string;\n fullPath: string;\n notesPath: string;\n noteCount: number;\n}[] {\n const claudeProjects = join(_homedir(), \".claude\", \"projects\");\n if (!existsSync(claudeProjects)) return [];\n\n const results: { encodedDir: string; fullPath: string; notesPath: string; noteCount: number }[] = [];\n const rootEncoded = encodeDir(project.root_path);\n\n try {\n for (const entry of readdirSync(claudeProjects)) {\n const full = join(claudeProjects, entry);\n try {\n if (!statSync(full).isDirectory()) continue;\n } catch {\n continue;\n }\n\n // Match exact or child (child encoded dirs start with project's encoded prefix)\n if (entry !== rootEncoded && !entry.startsWith(rootEncoded)) continue;\n\n const notesPath = join(full, \"Notes\");\n if (!existsSync(notesPath)) continue;\n\n let noteCount = 0;\n try {\n noteCount = readdirSync(notesPath).filter(\n (f) => f.endsWith(\".md\") || f.endsWith(\".txt\")\n ).length;\n } catch {\n // count stays 0\n }\n\n results.push({ encodedDir: entry, fullPath: full, notesPath, noteCount });\n }\n } catch {\n // Unreadable — ignore\n }\n\n return results;\n}\n\nfunction cmdConsolidate(\n db: Database,\n identifier: string,\n opts: { yes?: boolean; dryRun?: boolean }\n): void {\n const project = resolveIdentifier(db, identifier) ?? requireProject(db, identifier);\n\n console.log();\n console.log(header(` Consolidate: ${project.slug}`));\n console.log(` Target: ${project.root_path}`);\n console.log();\n\n const dirs = findProjectNotesDirs(project);\n\n if (dirs.length === 0) {\n console.log(warn(\" No scattered notes directories found for this project.\"));\n return;\n }\n\n // Canonical notes location\n const canonicalNotes = join(project.root_path, \"Notes\");\n\n // Show what would be consolidated\n const toMerge = dirs.filter((d) => d.notesPath !== canonicalNotes);\n\n if (toMerge.length === 0) {\n console.log(ok(\" All notes are already in the canonical location.\"));\n console.log(dim(` ${canonicalNotes}`));\n return;\n }\n\n console.log(` Found ${toMerge.length} scattered Notes directory(ies) to consolidate:`);\n console.log();\n\n for (const d of toMerge) {\n console.log(` ${bold(d.encodedDir)}`);\n console.log(dim(` Notes: ${d.notesPath} (${d.noteCount} file(s))`));\n }\n\n console.log();\n console.log(` Destination: ${canonicalNotes}`);\n console.log();\n\n if (opts.dryRun) {\n console.log(warn(\" Dry run — no changes made. Remove --dry-run to proceed.\"));\n return;\n }\n\n if (!opts.yes) {\n console.log(\n warn(\n \" Run with --yes to perform consolidation, or --dry-run to preview changes.\"\n )\n );\n return;\n }\n\n // Create canonical Notes dir\n mkdirSync(canonicalNotes, { recursive: true });\n\n let movedCount = 0;\n for (const d of toMerge) {\n try {\n const files = readdirSync(d.notesPath);\n for (const f of files) {\n if (!f.endsWith(\".md\") && !f.endsWith(\".txt\")) continue;\n const src = join(d.notesPath, f);\n const dest = join(canonicalNotes, f);\n if (!existsSync(dest)) {\n renameSync(src, dest);\n console.log(ok(` Moved: ${f}`));\n movedCount++;\n } else {\n console.log(warn(` Skipped (exists): ${f}`));\n }\n }\n } catch (e) {\n console.error(err(` Error reading ${d.notesPath}: ${e}`));\n }\n }\n\n console.log();\n console.log(ok(` Consolidated ${movedCount} file(s) into ${canonicalNotes}`));\n}\n\n// ---------------------------------------------------------------------------\n// Go command — fuzzy project lookup, prints root_path to stdout\n// ---------------------------------------------------------------------------\n\n/**\n * Simple Levenshtein distance for \"did you mean?\" suggestions.\n */\nfunction levenshtein(a: string, b: string): number {\n const m = a.length;\n const n = b.length;\n const dp: number[][] = Array.from({ length: m + 1 }, (_, i) =>\n Array.from({ length: n + 1 }, (_, j) => (i === 0 ? j : j === 0 ? i : 0))\n );\n for (let i = 1; i <= m; i++) {\n for (let j = 1; j <= n; j++) {\n dp[i][j] =\n a[i - 1] === b[j - 1]\n ? dp[i - 1][j - 1]\n : 1 + Math.min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]);\n }\n }\n return dp[m][n];\n}\n\n/**\n * Check if `needle` appears as a substring (case-insensitive) in `haystack`.\n */\nfunction containsIgnoreCase(haystack: string, needle: string): boolean {\n return haystack.toLowerCase().includes(needle.toLowerCase());\n}\n\nexport function cmdGo(db: Database, query: string): void {\n const all = db\n .prepare(\"SELECT * FROM projects WHERE status = 'active' ORDER BY updated_at DESC\")\n .all() as ProjectRow[];\n\n if (!all.length) {\n console.error(err(\"No active projects registered. Run: pai project add <path>\"));\n process.exit(1);\n }\n\n const q = query.trim().toLowerCase();\n\n // 1. Exact slug or alias match (re-use existing helper)\n const exact = getProject(db, query);\n if (exact) {\n process.stdout.write(exact.root_path + \"\\n\");\n return;\n }\n\n // 2. Substring match against slug, display_name, or root_path basename\n const partial = all.filter(\n (p) =>\n containsIgnoreCase(p.slug, q) ||\n containsIgnoreCase(p.display_name, q) ||\n containsIgnoreCase(basename(p.root_path), q)\n );\n\n if (partial.length === 1) {\n // Unique partial match — print path and done\n process.stdout.write(partial[0].root_path + \"\\n\");\n return;\n }\n\n if (partial.length > 1) {\n // Multiple matches — list them so user can narrow down\n console.error(err(`Ambiguous: \"${query}\" matches ${partial.length} projects:\\n`));\n partial.forEach((p, i) => {\n console.error(\n ` ${dim(String(i + 1).padStart(2))} ${bold(p.slug.padEnd(30))} ${dim(shortenPath(p.root_path, 50))}`\n );\n });\n console.error();\n console.error(dim(\" Use a more specific name or the exact slug.\"));\n process.exit(1);\n }\n\n // 3. No match — compute closest slugs by Levenshtein distance for suggestions\n const scored = all\n .map((p) => {\n const distSlug = levenshtein(q, p.slug.toLowerCase());\n const distName = levenshtein(q, p.display_name.toLowerCase());\n return { project: p, dist: Math.min(distSlug, distName) };\n })\n .sort((a, b) => a.dist - b.dist);\n\n // Suggest matches within edit-distance 4, or top 3 if none in threshold\n const threshold = 4;\n const suggestions =\n scored.filter((s) => s.dist <= threshold).length > 0\n ? scored.filter((s) => s.dist <= threshold).slice(0, 3)\n : scored.slice(0, 3);\n\n console.error(err(`Project not found: \"${query}\"\\n`));\n if (suggestions.length) {\n console.error(warn(\" Did you mean?\"));\n for (const s of suggestions) {\n console.error(\n ` ${bold(s.project.slug.padEnd(30))} ${dim(shortenPath(s.project.root_path, 50))}`\n );\n }\n console.error();\n console.error(dim(\" Run: pai project list (to see all projects)\"));\n }\n process.exit(1);\n}\n\n// ---------------------------------------------------------------------------\n// Commander registration\n// ---------------------------------------------------------------------------\n\nexport function registerProjectCommands(\n projectCmd: Command,\n getDb: () => Database\n): void {\n // pai project add <path>\n projectCmd\n .command(\"add <path>\")\n .description(\"Register a project directory in the PAI registry\")\n .option(\"--slug <slug>\", \"Override auto-generated slug\")\n .option(\n \"--type <type>\",\n \"Project type: local | central | obsidian-linked | external\",\n \"local\"\n )\n .option(\"--display-name <name>\", \"Human-readable display name\")\n .action((rawPath: string, opts: { slug?: string; type?: string; displayName?: string }) => {\n cmdAdd(getDb(), rawPath, opts);\n });\n\n // pai project list\n projectCmd\n .command(\"list\")\n .description(\"List registered projects\")\n .option(\"--status <status>\", \"Filter by status: active | archived\")\n .option(\"--tag <tag>\", \"Filter by tag\")\n .option(\"--type <type>\", \"Filter by type\")\n .action((opts: { status?: string; tag?: string; type?: string }) => {\n cmdList(getDb(), opts);\n });\n\n // pai project info <slug>\n projectCmd\n .command(\"info <slug>\")\n .description(\"Show full details for a project\")\n .action((slug: string) => {\n cmdInfo(getDb(), slug);\n });\n\n // pai project archive <slug>\n projectCmd\n .command(\"archive <slug>\")\n .description(\"Archive a project\")\n .action((slug: string) => {\n cmdArchive(getDb(), slug);\n });\n\n // pai project unarchive <slug>\n projectCmd\n .command(\"unarchive <slug>\")\n .description(\"Restore an archived project to active status\")\n .action((slug: string) => {\n cmdUnarchive(getDb(), slug);\n });\n\n // pai project move <slug> <new-path>\n projectCmd\n .command(\"move <slug> <new-path>\")\n .description(\"Update the root path for a project\")\n .action((slug: string, newPath: string) => {\n cmdMove(getDb(), slug, newPath);\n });\n\n // pai project tag <slug> <tags...>\n projectCmd\n .command(\"tag <slug> <tags...>\")\n .description(\"Add one or more tags to a project\")\n .action((slug: string, tags: string[]) => {\n cmdTag(getDb(), slug, tags);\n });\n\n // pai project alias <slug> <alias>\n projectCmd\n .command(\"alias <slug> <alias>\")\n .description(\"Register an alternative slug for a project\")\n .action((slug: string, alias: string) => {\n cmdAlias(getDb(), slug, alias);\n });\n\n // pai project edit <slug>\n projectCmd\n .command(\"edit <slug>\")\n .description(\"Edit project metadata\")\n .option(\"--display-name <name>\", \"New display name\")\n .option(\"--type <type>\", \"New type\")\n .action((slug: string, opts: { displayName?: string; type?: string }) => {\n cmdEdit(getDb(), slug, opts);\n });\n\n // pai project cd <slug-or-number>\n projectCmd\n .command(\"cd <identifier>\")\n .description(\"Print the root path for a project (use with: cd $(pai project cd <id>))\")\n .action((identifier: string) => {\n const project = resolveIdentifier(getDb(), identifier);\n if (!project) {\n console.error(`Project not found: ${identifier}`);\n process.exit(1);\n }\n process.stdout.write(project.root_path + \"\\n\");\n });\n\n // pai project detect [path]\n projectCmd\n .command(\"detect [path]\")\n .description(\n \"Detect which registered project the given path (or CWD) belongs to\"\n )\n .option(\"--json\", \"Output raw JSON instead of human-readable text\")\n .action((pathArg: string | undefined, opts: { json?: boolean }) => {\n cmdDetect(getDb(), pathArg, opts);\n });\n\n // pai project health\n projectCmd\n .command(\"health\")\n .description(\n \"Audit all registered projects: check which paths still exist, find moved/dead projects\"\n )\n .option(\n \"--fix\",\n \"Auto-remediate where possible (update moved paths, archive dead zero-session projects)\"\n )\n .option(\"--json\", \"Output raw JSON report\")\n .option(\n \"--status <category>\",\n \"Filter output to: active | stale | dead\"\n )\n .action(\n (opts: { fix?: boolean; json?: boolean; status?: string }) => {\n cmdHealth(getDb(), opts);\n }\n );\n\n // pai project consolidate <slug-or-number>\n projectCmd\n .command(\"consolidate <identifier>\")\n .description(\n \"Consolidate scattered ~/.claude/projects/.../Notes/ directories for a project into its canonical Notes/ location\"\n )\n .option(\"--yes\", \"Perform consolidation without confirmation prompt\")\n .option(\"--dry-run\", \"Preview what would be moved without making changes\")\n .action(\n (\n identifier: string,\n opts: { yes?: boolean; dryRun?: boolean }\n ) => {\n cmdConsolidate(getDb(), identifier, opts);\n }\n );\n\n // pai project promote\n projectCmd\n .command(\"promote\")\n .description(\"Promote a session note into a new standalone project\")\n .requiredOption(\"--from-session <path>\", \"Path to the session note markdown file\")\n .requiredOption(\"--to <path>\", \"Directory path for the new project (must not exist)\")\n .option(\"--name <name>\", \"Display name for the new project (derived from filename if omitted)\")\n .action((opts: { fromSession: string; to: string; name?: string }) => {\n cmdPromote(getDb(), opts);\n });\n\n // pai project go <query>\n projectCmd\n .command(\"go <query>\")\n .description(\n \"Print the root path for a project by slug, partial name, or fuzzy match.\\n\" +\n \"Designed for shell integration: cd $(pai project go <query>)\\n\" +\n \"Or set a shell alias: alias pcd='cd $(pai project go)'\"\n )\n .action((query: string) => {\n cmdGo(getDb(), query);\n });\n}\n","/**\n * Slug generator for PAI session notes.\n *\n * Extracts a 1-3 word descriptive slug from Claude Code JSONL transcripts\n * using keyword frequency analysis — no LLM required.\n *\n * JSONL format (one JSON object per line):\n * - type \"user\": { type: \"user\", message: { role: \"user\", content: string | [{ type, text }] } }\n * - type \"assistant\": { type: \"assistant\", message: { role: \"assistant\", content: [{ type, text }] } }\n */\n\nimport { readFileSync, existsSync, readdirSync, statSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { homedir } from \"node:os\";\n\n// ---------------------------------------------------------------------------\n// Public API types\n// ---------------------------------------------------------------------------\n\nexport interface SlugOptions {\n /** Maximum number of words in the generated slug (default: 3) */\n maxWords?: number;\n /** Maximum character length of the generated slug (default: 30) */\n maxLength?: number;\n}\n\n// ---------------------------------------------------------------------------\n// Stop words\n// ---------------------------------------------------------------------------\n\nconst STOP_WORDS = new Set([\n \"the\", \"a\", \"an\", \"is\", \"are\", \"was\", \"were\", \"be\", \"been\", \"being\",\n \"have\", \"has\", \"had\", \"do\", \"does\", \"did\", \"will\", \"would\", \"could\",\n \"should\", \"may\", \"might\", \"can\", \"shall\", \"this\", \"that\", \"these\",\n \"those\", \"it\", \"its\", \"i\", \"you\", \"we\", \"they\", \"he\", \"she\", \"my\",\n \"your\", \"our\", \"their\", \"what\", \"which\", \"who\", \"whom\", \"how\", \"when\",\n \"where\", \"why\", \"not\", \"no\", \"yes\", \"just\", \"also\", \"very\", \"really\",\n \"about\", \"after\", \"before\", \"from\", \"into\", \"with\", \"without\", \"for\",\n \"and\", \"or\", \"but\", \"if\", \"then\", \"else\", \"so\", \"because\", \"as\", \"at\",\n \"by\", \"in\", \"on\", \"of\", \"to\", \"up\", \"out\", \"off\", \"over\", \"under\",\n \"more\", \"most\", \"some\", \"any\", \"all\", \"each\", \"every\", \"both\", \"few\",\n \"many\", \"much\", \"other\", \"another\", \"such\", \"only\", \"own\", \"same\",\n \"than\", \"too\", \"let\", \"me\", \"us\", \"ok\", \"okay\", \"sure\",\n \"please\", \"thanks\", \"thank\", \"here\", \"there\", \"now\", \"well\", \"like\",\n \"want\", \"need\", \"know\", \"think\", \"see\", \"look\", \"make\", \"get\", \"go\",\n \"come\", \"take\", \"use\", \"find\", \"give\", \"tell\", \"say\", \"said\", \"try\",\n \"keep\", \"run\", \"set\", \"put\", \"add\", \"show\", \"check\", \"new\", \"file\",\n \"code\", \"going\", \"done\", \"got\", \"https\", \"http\", \"www\", \"com\", \"org\",\n \"net\", \"io\", \"null\", \"undefined\", \"true\", \"false\",\n \"ll\", \"ve\", \"re\", \"don\", \"thats\", \"its\", \"heres\", \"theres\",\n \"youre\", \"theyre\", \"didnt\", \"dont\", \"doesnt\", \"havent\", \"hasnt\",\n \"wont\", \"cant\", \"shouldnt\", \"wouldnt\", \"couldnt\", \"isnt\", \"arent\",\n \"wasnt\", \"werent\", \"never\", \"ever\", \"still\", \"already\", \"yet\", \"back\",\n \"away\", \"down\", \"right\", \"left\", \"next\", \"last\", \"first\", \"second\",\n \"third\", \"one\", \"two\", \"three\", \"four\", \"five\", \"six\", \"seven\",\n \"eight\", \"nine\", \"ten\", \"time\", \"way\", \"thing\", \"something\",\n \"anything\", \"nothing\", \"everything\", \"someone\", \"anyone\", \"everyone\",\n \"then\", \"again\", \"once\", \"twice\", \"since\", \"while\", \"though\",\n \"although\", \"however\", \"therefore\", \"thus\", \"hence\", \"meanwhile\",\n \"moreover\", \"furthermore\", \"otherwise\", \"instead\", \"anyway\",\n \"actually\", \"basically\", \"literally\", \"simply\", \"exactly\", \"probably\",\n \"possibly\", \"maybe\", \"perhaps\", \"certainly\", \"definitely\", \"absolutely\",\n \"completely\", \"totally\", \"quite\", \"rather\", \"fairly\", \"nearly\",\n \"almost\", \"barely\", \"hardly\", \"quickly\", \"slowly\", \"easily\", \"likely\",\n \"unlikely\", \"via\", \"per\", \"etc\", \"ie\", \"eg\", \"vs\",\n]);\n\n// ---------------------------------------------------------------------------\n// Internal helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Extract plain text from a parsed JSONL message object.\n */\nfunction extractText(obj: Record<string, unknown>): string | null {\n const msg = obj.message as Record<string, unknown> | undefined;\n if (!msg) return null;\n\n const content = msg.content;\n\n // Array of content blocks (modern assistant messages)\n if (Array.isArray(content)) {\n const texts: string[] = [];\n for (const block of content) {\n const b = block as Record<string, unknown>;\n if (b.type === \"text\" && typeof b.text === \"string\") {\n texts.push(b.text);\n }\n }\n return texts.join(\" \") || null;\n }\n\n // Plain string content (user messages)\n if (typeof content === \"string\") {\n return content || null;\n }\n\n return null;\n}\n\n/**\n * Tokenize a string into lowercase words, filtering stop words and short tokens.\n */\nfunction tokenize(text: string): string[] {\n return text\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, \" \")\n .split(/\\s+/)\n .filter((w) => {\n if (w.length < 3) return false;\n if (/^\\d+$/.test(w)) return false;\n if (STOP_WORDS.has(w)) return false;\n return true;\n });\n}\n\n// ---------------------------------------------------------------------------\n// Public functions\n// ---------------------------------------------------------------------------\n\n/**\n * Find the latest JSONL transcript for a project's Claude encoded directory.\n *\n * @param encodedDir Claude-encoded project directory name,\n * e.g. \"-Users-alice-dev-ai-PAI\"\n * @returns Absolute path to the most recently modified .jsonl file, or null.\n */\nexport function findLatestTranscript(encodedDir: string): string | null {\n const sessionsDir = join(\n homedir(),\n \".claude\",\n \"projects\",\n encodedDir,\n \"sessions\"\n );\n\n if (!existsSync(sessionsDir)) return null;\n\n let entries: string[];\n try {\n entries = readdirSync(sessionsDir).filter((f) => f.endsWith(\".jsonl\"));\n } catch {\n return null;\n }\n\n if (!entries.length) return null;\n\n // Sort by mtime descending, return the newest\n const sorted = entries\n .map((f) => {\n const fullPath = join(sessionsDir, f);\n try {\n return { path: fullPath, mtime: statSync(fullPath).mtimeMs };\n } catch {\n return { path: fullPath, mtime: 0 };\n }\n })\n .sort((a, b) => b.mtime - a.mtime);\n\n return sorted[0].path;\n}\n\n/**\n * Read the last `count` human/assistant message pairs from a JSONL file.\n *\n * Synchronous — reads the whole file in memory. JSONL transcripts are\n * bounded by context window size so this is safe even for large sessions.\n *\n * @param jsonlPath Absolute path to a Claude Code .jsonl transcript file.\n * @param count Number of conversation pairs to read (default: 15).\n * @returns Array of plain-text message strings (up to count*2 entries).\n */\nexport function readLastMessages(\n jsonlPath: string,\n count: number = 15\n): string[] {\n if (!existsSync(jsonlPath)) return [];\n\n let raw: string;\n try {\n raw = readFileSync(jsonlPath, \"utf8\");\n } catch {\n return [];\n }\n\n const lines = raw.split(\"\\n\").filter((l) => l.trim().length > 0);\n const messages: string[] = [];\n const limit = count * 2;\n\n // Walk from the end to collect the most recent messages first\n for (let i = lines.length - 1; i >= 0 && messages.length < limit; i--) {\n let obj: Record<string, unknown>;\n try {\n obj = JSON.parse(lines[i]) as Record<string, unknown>;\n } catch {\n continue;\n }\n\n const type = obj.type as string | undefined;\n if (type !== \"assistant\" && type !== \"user\") continue;\n\n const text = extractText(obj);\n if (text && text.trim().length > 0) {\n // Prepend to maintain chronological order\n messages.unshift(text);\n }\n }\n\n return messages;\n}\n\n/**\n * Generate a descriptive 1-3 word slug from conversation message strings.\n *\n * Algorithm:\n * 1. Tokenize all messages, remove stop words and short tokens\n * 2. Count word frequency\n * 3. Pick the top `maxWords` most frequent tokens\n * 4. Join with hyphen, lowercase\n *\n * @param messages Array of plain-text conversation messages.\n * @param opts Optional slug configuration.\n * @returns A lowercase hyphen-joined slug, e.g. \"memory-engine-refactor\",\n * or \"unnamed-session\" if no meaningful words are found.\n */\nexport function generateSlug(messages: string[], opts?: SlugOptions): string {\n const maxWords = opts?.maxWords ?? 3;\n const maxLength = opts?.maxLength ?? 30;\n\n if (!messages.length) return \"unnamed-session\";\n\n // Count word frequencies across all messages\n const freq = new Map<string, number>();\n for (const msg of messages) {\n for (const token of tokenize(msg)) {\n freq.set(token, (freq.get(token) ?? 0) + 1);\n }\n }\n\n if (!freq.size) return \"unnamed-session\";\n\n // Sort by frequency descending, take top maxWords\n const topWords = Array.from(freq.entries())\n .sort((a, b) => b[1] - a[1])\n .slice(0, maxWords)\n .map(([word]) => word);\n\n if (!topWords.length) return \"unnamed-session\";\n\n // Join and trim to maxLength (truncate at last hyphen boundary)\n let slug = topWords.join(\"-\");\n if (slug.length > maxLength) {\n slug = slug.slice(0, maxLength).replace(/-[^-]*$/, \"\");\n }\n\n return slug || \"unnamed-session\";\n}\n","/**\n * pai session <sub-command>\n *\n * list [project-slug] — list sessions\n * info <project-slug> <number> — show session detail\n * rename <project-slug> <number> <new-slug> — rename a session note\n * slug <project-slug> <number|latest> — generate/apply a slug\n * tag <project-slug> <number> [tags...] — set/show tags on a session\n * route <session-slug> <target-project> — create cross-reference link\n * auto-route [--cwd path] [--context text] — auto-detect project for session\n */\n\nimport type { Command } from \"commander\";\nimport type { Database } from \"better-sqlite3\";\nimport {\n ok,\n warn,\n err,\n dim,\n bold,\n header,\n renderTable,\n fmtDate,\n} from \"../utils.js\";\nimport chalk from \"chalk\";\nimport {\n existsSync,\n readdirSync,\n renameSync,\n readFileSync,\n writeFileSync,\n statSync,\n mkdirSync,\n} from \"node:fs\";\nimport { join, dirname } from \"node:path\";\nimport { homedir, tmpdir } from \"node:os\";\nimport {\n findLatestTranscript,\n readLastMessages,\n generateSlug,\n} from \"../../session/slug-generator.js\";\n\n// ---------------------------------------------------------------------------\n// Row types\n// ---------------------------------------------------------------------------\n\ninterface SessionRow {\n id: number;\n project_id: number;\n number: number;\n date: string;\n slug: string;\n title: string;\n filename: string;\n status: string;\n claude_session_id: string | null;\n token_count: number | null;\n created_at: number;\n closed_at: number | null;\n}\n\ninterface ProjectRow {\n id: number;\n slug: string;\n display_name: string;\n root_path: string;\n encoded_dir: string;\n}\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\nfunction getProject(db: Database, slug: string): ProjectRow | undefined {\n return db\n .prepare(\n \"SELECT id, slug, display_name, root_path, encoded_dir FROM projects WHERE slug = ?\"\n )\n .get(slug) as ProjectRow | undefined;\n}\n\nfunction statusColor(status: string): string {\n switch (status) {\n case \"completed\":\n return chalk.green(status);\n case \"compacted\":\n return chalk.blue(status);\n default:\n return chalk.yellow(status);\n }\n}\n\n/**\n * Convert a slug to a title-cased display name suitable for filenames.\n * \"memory-engine\" → \"Memory Engine\"\n * \"slug-generator\" → \"Slug Generator\"\n * \"session-slug-fix\" → \"Session Slug Fix\"\n */\nfunction toTitleCase(slug: string): string {\n return slug\n .replace(/-/g, \" \")\n .replace(/\\b\\w/g, (c) => c.toUpperCase());\n}\n\n/**\n * Find the Notes directory for a project.\n *\n * Notes live inside the Claude-managed project directory:\n * ~/.claude/projects/<encoded_dir>/Notes/\n */\nfunction getNotesDir(project: ProjectRow): string {\n return join(\n homedir(),\n \".claude\",\n \"projects\",\n project.encoded_dir,\n \"Notes\"\n );\n}\n\n/**\n * Format a session filename from its parts.\n * number=27, date=\"2026-02-23\", titleSlug=\"Memory Engine\"\n * → \"0027 - 2026-02-23 - Memory Engine.md\"\n */\nfunction formatFilename(number: number, date: string, titleSlug: string): string {\n const n = String(number).padStart(4, \"0\");\n return `${n} - ${date} - ${titleSlug}.md`;\n}\n\n/**\n * Look up a session by project + number OR \"latest\".\n * Returns the session row or exits with an error.\n */\nfunction resolveSession(\n db: Database,\n project: ProjectRow,\n numberOrLatest: string\n): SessionRow {\n let session: SessionRow | undefined;\n\n if (numberOrLatest === \"latest\") {\n session = db\n .prepare(\n \"SELECT * FROM sessions WHERE project_id = ? ORDER BY number DESC LIMIT 1\"\n )\n .get(project.id) as SessionRow | undefined;\n } else {\n const num = parseInt(numberOrLatest, 10);\n if (isNaN(num)) {\n console.error(err(`Invalid session number: ${numberOrLatest}`));\n process.exit(1);\n }\n session = db\n .prepare(\"SELECT * FROM sessions WHERE project_id = ? AND number = ?\")\n .get(project.id, num) as SessionRow | undefined;\n }\n\n if (!session) {\n console.error(\n err(`Session ${numberOrLatest} not found in project ${project.slug}`)\n );\n process.exit(1);\n }\n\n return session;\n}\n\n// ---------------------------------------------------------------------------\n// Command implementations\n// ---------------------------------------------------------------------------\n\nfunction cmdList(\n db: Database,\n projectSlug: string | undefined,\n opts: { limit?: string; status?: string }\n): void {\n const limit = parseInt(opts.limit ?? \"20\", 10);\n const params: unknown[] = [];\n\n let query = `\n SELECT s.*, p.slug AS project_slug, p.display_name AS project_name\n FROM sessions s\n JOIN projects p ON p.id = s.project_id\n `;\n\n const where: string[] = [];\n if (projectSlug) {\n const project = getProject(db, projectSlug);\n if (!project) {\n console.error(err(`Project not found: ${projectSlug}`));\n process.exit(1);\n }\n where.push(\"s.project_id = ?\");\n params.push(project.id);\n }\n if (opts.status) {\n where.push(\"s.status = ?\");\n params.push(opts.status);\n }\n\n if (where.length) {\n query += \" WHERE \" + where.join(\" AND \");\n }\n query += \" ORDER BY s.date DESC, s.number DESC\";\n query += ` LIMIT ${limit}`;\n\n const rows = db.prepare(query).all(...params) as (SessionRow & {\n project_slug: string;\n project_name: string;\n })[];\n\n if (!rows.length) {\n console.log(warn(\"No sessions found.\"));\n return;\n }\n\n const showProject = !projectSlug;\n const headers = showProject\n ? [\"#\", \"Project\", \"Date\", \"Title\", \"Status\", \"Tokens\"]\n : [\"#\", \"Date\", \"Title\", \"Status\", \"Tokens\"];\n\n const tableRows = rows.map((r) => {\n const title =\n r.title.length > 45 ? r.title.slice(0, 42) + \"...\" : r.title;\n const tokens =\n r.token_count != null ? dim(r.token_count.toLocaleString()) : dim(\"—\");\n if (showProject) {\n return [\n dim(`#${r.number}`),\n r.project_slug,\n r.date,\n title,\n statusColor(r.status),\n tokens,\n ];\n }\n return [dim(`#${r.number}`), r.date, title, statusColor(r.status), tokens];\n });\n\n console.log();\n if (projectSlug) {\n const project = getProject(db, projectSlug)!;\n console.log(` ${bold(project.display_name)} sessions:`);\n console.log();\n }\n console.log(renderTable(headers, tableRows));\n console.log();\n console.log(dim(` ${rows.length} session(s) shown (limit: ${limit})`));\n}\n\nfunction cmdInfo(\n db: Database,\n projectSlug: string,\n sessionNumber: string\n): void {\n const project = getProject(db, projectSlug);\n if (!project) {\n console.error(err(`Project not found: ${projectSlug}`));\n process.exit(1);\n }\n\n const session = resolveSession(db, project, sessionNumber);\n\n console.log();\n console.log(header(` Session #${session.number}: ${session.title}`));\n console.log();\n console.log(` ${bold(\"Project:\")} ${project.display_name} (${project.slug})`);\n console.log(` ${bold(\"Date:\")} ${session.date}`);\n console.log(` ${bold(\"Status:\")} ${statusColor(session.status)}`);\n console.log(` ${bold(\"Filename:\")} ${session.filename}`);\n console.log(` ${bold(\"Slug:\")} ${session.slug}`);\n if (session.claude_session_id) {\n console.log(` ${bold(\"Claude ID:\")} ${dim(session.claude_session_id)}`);\n }\n if (session.token_count != null) {\n console.log(` ${bold(\"Tokens:\")} ${session.token_count.toLocaleString()}`);\n }\n console.log(` ${bold(\"Created:\")} ${fmtDate(session.created_at)}`);\n if (session.closed_at) {\n console.log(` ${bold(\"Closed:\")} ${fmtDate(session.closed_at)}`);\n }\n console.log();\n}\n\n/**\n * Rename a session note: updates the database (slug + title), renames the\n * file on disk, and updates the H1 title inside the Markdown file.\n */\nfunction cmdRename(\n db: Database,\n projectSlug: string,\n numberOrLatest: string,\n newSlug: string\n): void {\n const project = getProject(db, projectSlug);\n if (!project) {\n console.error(err(`Project not found: ${projectSlug}`));\n process.exit(1);\n }\n\n const session = resolveSession(db, project, numberOrLatest);\n const notesDir = getNotesDir(project);\n\n if (!existsSync(notesDir)) {\n console.error(err(`Notes directory not found: ${notesDir}`));\n process.exit(1);\n }\n\n // Title-case the slug for the filename and H1\n const titleSlug = toTitleCase(newSlug);\n const newFilename = formatFilename(session.number, session.date, titleSlug);\n const oldPath = join(notesDir, session.filename);\n const newPath = join(notesDir, newFilename);\n\n // Rename file on disk (only if it exists at expected location)\n if (existsSync(oldPath)) {\n if (oldPath !== newPath) {\n try {\n renameSync(oldPath, newPath);\n } catch (e) {\n console.error(err(`Failed to rename file: ${e}`));\n process.exit(1);\n }\n }\n } else {\n // File might already have a different name or be missing — warn but continue\n console.log(\n warn(` Note: file not found at expected path: ${session.filename}`)\n );\n console.log(\n warn(` Skipping disk rename. Database will still be updated.`)\n );\n }\n\n // Update H1 title inside the Markdown file if it exists\n if (existsSync(newPath)) {\n try {\n const content = readFileSync(newPath, \"utf8\");\n const lines = content.split(\"\\n\");\n let h1Updated = false;\n const updated = lines.map((line) => {\n if (!h1Updated && line.startsWith(\"# \")) {\n h1Updated = true;\n return `# ${titleSlug}`;\n }\n return line;\n });\n // If no H1 found, prepend one\n if (!h1Updated) {\n updated.unshift(`# ${titleSlug}`, \"\");\n }\n writeFileSync(newPath, updated.join(\"\\n\"), \"utf8\");\n } catch (e) {\n console.error(err(`Failed to update H1 in file: ${e}`));\n // Non-fatal: continue to update the database\n }\n }\n\n // Update the database\n const normalizedSlug = newSlug\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, \"-\")\n .replace(/^-+|-+$/g, \"\");\n\n db.prepare(\n \"UPDATE sessions SET slug = ?, title = ?, filename = ? WHERE id = ?\"\n ).run(normalizedSlug, titleSlug, newFilename, session.id);\n\n console.log();\n console.log(ok(` Session #${session.number} renamed.`));\n console.log(` ${bold(\"Old:\")} ${session.filename}`);\n console.log(` ${bold(\"New:\")} ${newFilename}`);\n console.log(` ${bold(\"Slug:\")} ${normalizedSlug}`);\n console.log(` ${bold(\"Title:\")} ${titleSlug}`);\n console.log();\n}\n\n/**\n * Generate (and optionally apply) a slug for a session by analysing its\n * Claude Code JSONL transcript.\n */\nfunction cmdSlug(\n db: Database,\n projectSlug: string,\n numberOrLatest: string,\n opts: { apply?: boolean }\n): void {\n const project = getProject(db, projectSlug);\n if (!project) {\n console.error(err(`Project not found: ${projectSlug}`));\n process.exit(1);\n }\n\n const session = resolveSession(db, project, numberOrLatest);\n\n // Find the JSONL transcript\n const transcriptPath = findLatestTranscript(project.encoded_dir);\n\n if (!transcriptPath) {\n console.log(warn(` No JSONL transcripts found for project ${projectSlug}`));\n console.log(\"unnamed-session\");\n return;\n }\n\n // Read the last 15 message pairs\n const messages = readLastMessages(transcriptPath);\n\n if (messages.length < 2) {\n console.log(warn(` Too few messages found (${messages.length}) in transcript`));\n console.log(\"unnamed-session\");\n return;\n }\n\n // Generate the slug\n const generatedSlug = generateSlug(messages);\n\n console.log(generatedSlug);\n\n if (opts.apply) {\n console.log();\n console.log(dim(` Applying slug to session #${session.number}...`));\n cmdRename(db, projectSlug, String(session.number), generatedSlug);\n }\n}\n\n// ---------------------------------------------------------------------------\n// Tag helpers\n// ---------------------------------------------------------------------------\n\nfunction upsertTag(db: Database, tagName: string): number {\n db.prepare(\"INSERT OR IGNORE INTO tags (name) VALUES (?)\").run(tagName);\n const row = db.prepare(\"SELECT id FROM tags WHERE name = ?\").get(tagName) as { id: number };\n return row.id;\n}\n\nfunction getSessionTags(db: Database, sessionId: number): string[] {\n const rows = db\n .prepare(\n `SELECT t.name FROM tags t\n JOIN session_tags st ON st.tag_id = t.id\n WHERE st.session_id = ?\n ORDER BY t.name`\n )\n .all(sessionId) as { name: string }[];\n return rows.map((r) => r.name);\n}\n\n// ---------------------------------------------------------------------------\n// Command: session tag\n// ---------------------------------------------------------------------------\n\n/**\n * Set or show tags on a session.\n *\n * With no tags supplied, prints the current tags.\n * Tags can be supplied as separate args or as a single comma-separated string.\n * pai session tag 20-webseiten 81 — show current tags\n * pai session tag 20-webseiten 81 docker migration server\n * pai session tag 20-webseiten 81 docker,migration,server\n */\nfunction cmdTag(\n db: Database,\n projectSlug: string,\n sessionNumber: string,\n rawTags: string[]\n): void {\n const project = getProject(db, projectSlug);\n if (!project) {\n console.error(err(`Project not found: ${projectSlug}`));\n process.exit(1);\n }\n\n const session = resolveSession(db, project, sessionNumber);\n\n // No tags supplied — show current tags\n if (rawTags.length === 0) {\n const current = getSessionTags(db, session.id);\n console.log();\n if (current.length === 0) {\n console.log(dim(` Session #${session.number} has no tags.`));\n } else {\n console.log(\n ` ${bold(`Session #${session.number}`)} tags: ${current.map((t) => chalk.cyan(t)).join(\", \")}`\n );\n }\n console.log();\n return;\n }\n\n // Expand comma-separated tags within each arg\n const tags = rawTags\n .flatMap((t) => t.split(\",\"))\n .map((t) => t.trim().toLowerCase())\n .filter((t) => t.length > 0);\n\n if (tags.length === 0) {\n console.log(warn(\"No valid tags provided.\"));\n return;\n }\n\n const added: string[] = [];\n const skipped: string[] = [];\n\n for (const tagName of tags) {\n const tagId = upsertTag(db, tagName);\n const exists = db\n .prepare(\"SELECT 1 FROM session_tags WHERE session_id = ? AND tag_id = ?\")\n .get(session.id, tagId);\n if (exists) {\n skipped.push(tagName);\n } else {\n db.prepare(\n \"INSERT INTO session_tags (session_id, tag_id) VALUES (?, ?)\"\n ).run(session.id, tagId);\n added.push(tagName);\n }\n }\n\n console.log();\n if (added.length) {\n console.log(\n ok(` Tagged session #${session.number}: ${added.map((t) => chalk.cyan(t)).join(\", \")}`)\n );\n }\n if (skipped.length) {\n console.log(dim(` Already present: ${skipped.join(\", \")}`));\n }\n\n const allTags = getSessionTags(db, session.id);\n console.log(\n ` ${bold(\"All tags:\")} ${allTags.map((t) => chalk.cyan(t)).join(\", \")}`\n );\n console.log();\n}\n\n// ---------------------------------------------------------------------------\n// Command: session route\n// ---------------------------------------------------------------------------\n\n/**\n * Create a cross-reference link from a session to a target project.\n *\n * pai session route <project-slug> <session-number> <target-project>\n * pai session route <project-slug> <session-number> <target-project> --type follow-up\n */\nfunction cmdRoute(\n db: Database,\n projectSlug: string,\n sessionNumber: string,\n targetProjectSlug: string,\n opts: { type?: string }\n): void {\n const project = getProject(db, projectSlug);\n if (!project) {\n console.error(err(`Project not found: ${projectSlug}`));\n process.exit(1);\n }\n\n const session = resolveSession(db, project, sessionNumber);\n\n // Resolve target project\n const targetProject = db\n .prepare(\"SELECT id, slug, display_name FROM projects WHERE slug = ?\")\n .get(targetProjectSlug) as { id: number; slug: string; display_name: string } | undefined;\n\n if (!targetProject) {\n console.error(err(`Target project not found: ${targetProjectSlug}`));\n process.exit(1);\n }\n\n const validTypes = [\"related\", \"follow-up\", \"reference\"];\n const linkType = opts.type ?? \"related\";\n if (!validTypes.includes(linkType)) {\n console.error(err(`Invalid link type \"${linkType}\". Valid: ${validTypes.join(\", \")}`));\n process.exit(1);\n }\n\n try {\n db.prepare(\n `INSERT INTO links (session_id, target_project_id, link_type, created_at)\n VALUES (?, ?, ?, ?)`\n ).run(session.id, targetProject.id, linkType, Date.now());\n } catch {\n // UNIQUE constraint means this link already exists\n console.log(warn(` Link already exists: session #${session.number} → ${targetProjectSlug}`));\n return;\n }\n\n console.log();\n console.log(\n ok(` Linked session #${session.number} (${project.slug}) → ${targetProject.display_name} (${targetProjectSlug})`)\n );\n console.log(dim(` Link type: ${linkType}`));\n console.log();\n}\n\n// ---------------------------------------------------------------------------\n// Command: session checkpoint\n// ---------------------------------------------------------------------------\n\n/**\n * Find the Notes directory for the current working directory by scanning\n * ~/.claude/projects/ for a matching encoded-dir entry.\n *\n * Returns the Notes dir path if found, or null if the CWD has no Claude\n * project directory yet.\n */\nfunction findNotesDirForCwd(): string | null {\n const cwd = process.cwd();\n const claudeProjectsDir = join(homedir(), \".claude\", \"projects\");\n\n if (!existsSync(claudeProjectsDir)) return null;\n\n // Encode the cwd the same way Claude Code does: every /, space, dot,\n // and hyphen → single dash.\n const expectedEncoded = cwd.replace(/[/\\s.\\-]/g, \"-\");\n\n let encodedDir: string | null = null;\n\n try {\n const entries = readdirSync(claudeProjectsDir);\n\n // Exact match first\n if (entries.includes(expectedEncoded)) {\n encodedDir = expectedEncoded;\n } else {\n // Fallback: look for a CLAUDE.md that contains the cwd path, or a\n // session-registry.json that mentions it. In practice the exact match\n // covers the common case; keep the fallback cheap (no DB needed here\n // since checkpoint is called from hooks where DB may not be available).\n for (const entry of entries) {\n const full = join(claudeProjectsDir, entry);\n try {\n if (!statSync(full).isDirectory()) continue;\n } catch {\n continue;\n }\n // Heuristic: the encoded dir for /a/b/c is \"-a-b-c\". Compare after\n // stripping any trailing slashes from the candidate.\n const candidate = entry.replace(/-+$/, \"\");\n const expected = expectedEncoded.replace(/-+$/, \"\");\n if (candidate === expected) {\n encodedDir = entry;\n break;\n }\n }\n }\n } catch {\n return null;\n }\n\n if (!encodedDir) return null;\n\n const notesDir = join(claudeProjectsDir, encodedDir, \"Notes\");\n return existsSync(notesDir) ? notesDir : null;\n}\n\n/**\n * Find the most recently modified .md file in a directory.\n */\nfunction findLatestNoteFile(notesDir: string): string | null {\n let entries: string[];\n try {\n entries = readdirSync(notesDir);\n } catch {\n return null;\n }\n\n const mdFiles = entries.filter((e) => e.endsWith(\".md\"));\n if (mdFiles.length === 0) return null;\n\n let latestPath: string | null = null;\n let latestMtime = 0;\n\n for (const file of mdFiles) {\n const full = join(notesDir, file);\n try {\n const { mtimeMs } = statSync(full);\n if (mtimeMs > latestMtime) {\n latestMtime = mtimeMs;\n latestPath = full;\n }\n } catch {\n // skip unreadable files\n }\n }\n\n return latestPath;\n}\n\n/**\n * Rate-limit guard: returns true if the last checkpoint was written less\n * than `minGapSeconds` ago, using a temp file keyed to the notes directory.\n */\nfunction checkpointTooRecent(notesDir: string, minGapSeconds: number): boolean {\n // Key the temp file to the notes dir path so different projects don't\n // share a rate-limit bucket.\n const safeKey = notesDir.replace(/[^a-zA-Z0-9]/g, \"-\").slice(-80);\n const tmpFile = join(tmpdir(), `pai-checkpoint-${safeKey}`);\n\n if (!existsSync(tmpFile)) return false;\n\n try {\n const { mtimeMs } = statSync(tmpFile);\n const ageMs = Date.now() - mtimeMs;\n return ageMs < minGapSeconds * 1000;\n } catch {\n return false;\n }\n}\n\n/**\n * Touch the rate-limit sentinel file.\n */\nfunction touchCheckpointSentinel(notesDir: string): void {\n const safeKey = notesDir.replace(/[^a-zA-Z0-9]/g, \"-\").slice(-80);\n const tmpFile = join(tmpdir(), `pai-checkpoint-${safeKey}`);\n try {\n // Write current timestamp as content (mtime is what matters)\n writeFileSync(tmpFile, String(Date.now()), \"utf8\");\n } catch {\n // Non-fatal — rate limiting is best-effort\n }\n}\n\n/**\n * Append a timestamped checkpoint block to the active session note.\n *\n * Designed to be called from Claude Code hooks (PostToolUse,\n * UserPromptSubmit). Fast, silent, exit 0 on success or skip.\n */\nfunction cmdCheckpoint(message: string, opts: { minGap?: string }): void {\n const minGapSeconds = parseInt(opts.minGap ?? \"300\", 10); // default: 5 min\n\n // 1. Locate the Notes directory for the CWD\n const notesDir = findNotesDirForCwd();\n if (!notesDir) {\n // No Claude project for this directory — silently exit\n process.exit(0);\n }\n\n // 2. Rate-limit check\n if (checkpointTooRecent(notesDir, minGapSeconds)) {\n process.exit(0);\n }\n\n // 3. Find the most recent session note\n const notePath = findLatestNoteFile(notesDir);\n if (!notePath) {\n process.exit(0);\n }\n\n // 4. Build the checkpoint block\n const timestamp = new Date().toISOString();\n const block = `\\n## Checkpoint — ${timestamp}\\n${message}\\n`;\n\n // 5. Append atomically: write to .tmp then rename\n const tmpPath = `${notePath}.checkpoint.tmp`;\n try {\n const existing = readFileSync(notePath, \"utf8\");\n writeFileSync(tmpPath, existing + block, \"utf8\");\n renameSync(tmpPath, notePath);\n } catch {\n // Non-fatal: hooks must not crash the Claude Code session\n try {\n // Clean up tmp file if it exists\n if (existsSync(tmpPath)) {\n renameSync(tmpPath, tmpPath + \".dead\");\n }\n } catch { /* ignore */ }\n process.exit(0);\n }\n\n // 6. Update rate-limit sentinel\n touchCheckpointSentinel(notesDir);\n\n // Silent success — hooks should not produce noise\n process.exit(0);\n}\n\n// ---------------------------------------------------------------------------\n// Command: session handover\n// ---------------------------------------------------------------------------\n\n/**\n * TODO candidate locations searched in priority order (mirrors toolProjectTodo).\n */\nconst HANDOVER_TODO_LOCATIONS = [\n \"Notes/TODO.md\",\n \".claude/Notes/TODO.md\",\n \"tasks/todo.md\",\n \"TODO.md\",\n];\n\n/**\n * Find the TODO.md for a given project root path.\n * Returns { path, content } for the first location that exists, or null.\n */\nfunction findProjectTodo(rootPath: string): { path: string; content: string } | null {\n for (const rel of HANDOVER_TODO_LOCATIONS) {\n const full = join(rootPath, rel);\n if (existsSync(full)) {\n try {\n return { path: full, content: readFileSync(full, \"utf8\") };\n } catch {\n // unreadable — try next\n }\n }\n }\n return null;\n}\n\n/**\n * Strip any existing `## Continue` section (up to but not including the\n * first `---` separator or next `##` heading that follows it).\n * Returns the content with that section removed.\n */\nfunction stripContinueSection(content: string): string {\n const lines = content.split(\"\\n\");\n\n const startIdx = lines.findIndex((l) => l.trim() === \"## Continue\");\n if (startIdx === -1) return content;\n\n // Find where the section ends\n let endIdx = lines.length;\n for (let i = startIdx + 1; i < lines.length; i++) {\n const trimmed = lines[i].trim();\n if (trimmed === \"---\" || (trimmed.startsWith(\"##\") && trimmed !== \"## Continue\")) {\n // Keep the separator / next heading as part of the remaining content\n endIdx = i;\n break;\n }\n }\n\n // If the line right after the section is a `---` separator, skip it too\n // so we don't leave a dangling separator with nothing above it.\n let trailingEnd = endIdx;\n if (trailingEnd < lines.length && lines[trailingEnd].trim() === \"---\") {\n trailingEnd += 1;\n }\n\n const before = lines.slice(0, startIdx);\n const after = lines.slice(trailingEnd);\n\n // Collapse any leading blank lines in `after`\n while (after.length > 0 && after[0].trim() === \"\") {\n after.shift();\n }\n\n return [...before, ...after].join(\"\\n\");\n}\n\n/**\n * Write (or overwrite) the `## Continue` section at the TOP of the TODO file.\n *\n * pai session handover [project-slug] [session-id|\"latest\"]\n *\n * Called from hooks (session-stop, pre-compact) with project-slug + \"latest\".\n * Falls back to auto-detecting the project from cwd when no slug is supplied.\n */\nfunction cmdHandover(\n db: Database,\n projectSlug: string | undefined,\n numberOrLatest: string | undefined\n): void {\n // ---- 1. Resolve project ----\n let project: ProjectRow | undefined;\n\n if (projectSlug) {\n project = getProject(db, projectSlug);\n if (!project) {\n // Graceful exit — called from hooks, must not crash\n process.exit(0);\n }\n } else {\n // Auto-detect from cwd: find a project whose root_path is a prefix of cwd\n const cwd = process.cwd();\n const row = db\n .prepare(\n `SELECT id, slug, display_name, root_path, encoded_dir\n FROM projects\n WHERE ? LIKE root_path || '%'\n ORDER BY length(root_path) DESC\n LIMIT 1`\n )\n .get(cwd) as ProjectRow | undefined;\n\n if (!row) {\n process.exit(0);\n }\n project = row;\n }\n\n // ---- 2. Resolve session ----\n let session: SessionRow | undefined;\n const nol = numberOrLatest ?? \"latest\";\n\n if (nol === \"latest\") {\n session = db\n .prepare(\n \"SELECT * FROM sessions WHERE project_id = ? ORDER BY number DESC LIMIT 1\"\n )\n .get(project.id) as SessionRow | undefined;\n } else {\n const num = parseInt(nol, 10);\n if (!isNaN(num)) {\n session = db\n .prepare(\"SELECT * FROM sessions WHERE project_id = ? AND number = ?\")\n .get(project.id, num) as SessionRow | undefined;\n }\n }\n\n // ---- 3. Find the project TODO ----\n const todo = findProjectTodo(project.root_path);\n\n // If no TODO file exists at all, try to create one at the canonical location\n let todoPath: string;\n let existingContent: string;\n\n if (todo) {\n todoPath = todo.path;\n existingContent = todo.content;\n } else {\n // Create Notes/TODO.md as the canonical default\n const notesDir = join(project.root_path, \"Notes\");\n try {\n if (!existsSync(notesDir)) {\n mkdirSync(notesDir, { recursive: true });\n }\n } catch {\n process.exit(0);\n }\n todoPath = join(notesDir, \"TODO.md\");\n existingContent = \"\";\n }\n\n // ---- 4. Build the ## Continue block ----\n const timestamp = new Date().toISOString();\n const cwd = process.cwd();\n\n let sessionLine: string;\n if (session) {\n const num = String(session.number).padStart(4, \"0\");\n const titlePart = session.title || session.slug || \"Session\";\n sessionLine = `${num} - ${session.date} - ${titlePart}`;\n } else {\n sessionLine = \"Unknown session\";\n }\n\n const continueBlock = [\n \"## Continue\",\n \"\",\n `> **Last session:** ${sessionLine}`,\n `> **Paused at:** ${timestamp}`,\n \">\",\n `> Working directory: ${cwd}. Check the latest session note for details.`,\n \"\",\n \"---\",\n \"\",\n ].join(\"\\n\");\n\n // ---- 5. Strip any old ## Continue section and prepend new one ----\n const stripped = stripContinueSection(existingContent).trimStart();\n const newContent = continueBlock + stripped;\n\n // ---- 6. Write atomically ----\n const tmpPath = `${todoPath}.handover.tmp`;\n try {\n writeFileSync(tmpPath, newContent, \"utf8\");\n renameSync(tmpPath, todoPath);\n } catch {\n try {\n if (existsSync(tmpPath)) {\n renameSync(tmpPath, `${tmpPath}.dead`);\n }\n } catch { /* ignore */ }\n process.exit(0);\n }\n\n // Silent success — hooks should not produce noise on stdout\n process.exit(0);\n}\n\n// ---------------------------------------------------------------------------\n// cmd: auto-route\n// ---------------------------------------------------------------------------\n\nasync function cmdAutoRoute(opts: {\n cwd?: string;\n context?: string;\n json?: boolean;\n}): Promise<void> {\n const { autoRoute, formatAutoRoute, formatAutoRouteJson } = await import(\n \"../../session/auto-route.js\"\n );\n const { openRegistry } = await import(\"../../registry/db.js\");\n const { createStorageBackend } = await import(\"../../storage/factory.js\");\n const { loadConfig } = await import(\"../../daemon/config.js\");\n\n const config = loadConfig();\n const registryDb = openRegistry();\n const federation = await createStorageBackend(config);\n\n const targetCwd = opts.cwd ?? process.cwd();\n const result = await autoRoute(registryDb, federation, targetCwd, opts.context);\n\n if (!result) {\n console.log();\n console.log(warn(\" No project match found for: \" + targetCwd));\n console.log();\n console.log(\n dim(\" Tried: path match, PAI.md marker walk\") +\n (opts.context ? dim(\", topic detection\") : \"\")\n );\n console.log();\n console.log(dim(\" Run 'pai project add .' to register this directory.\"));\n console.log();\n return;\n }\n\n if (opts.json) {\n console.log(formatAutoRouteJson(result));\n return;\n }\n\n console.log();\n console.log(header(\" PAI Auto-Route\"));\n console.log();\n console.log(` ${bold(\"Project:\")} ${result.display_name}`);\n console.log(` ${bold(\"Slug:\")} ${result.slug}`);\n console.log(` ${bold(\"Root path:\")} ${result.root_path}`);\n console.log(` ${bold(\"Method:\")} ${result.method}`);\n console.log(\n ` ${bold(\"Confidence:\")} ${(result.confidence * 100).toFixed(0)}%`\n );\n console.log();\n console.log(ok(\" Routed to: \") + bold(result.slug));\n console.log();\n}\n\n// ---------------------------------------------------------------------------\n// Commander registration\n// ---------------------------------------------------------------------------\n\nexport function registerSessionCommands(\n sessionCmd: Command,\n getDb: () => Database\n): void {\n // pai session list [project-slug]\n sessionCmd\n .command(\"list [project-slug]\")\n .description(\"List sessions, optionally filtered to a single project\")\n .option(\"--limit <n>\", \"Maximum number of sessions to show\", \"20\")\n .option(\"--status <status>\", \"Filter by status: open | completed | compacted\")\n .action(\n (\n projectSlug: string | undefined,\n opts: { limit?: string; status?: string }\n ) => {\n cmdList(getDb(), projectSlug, opts);\n }\n );\n\n // pai session info <project-slug> <number>\n sessionCmd\n .command(\"info <project-slug> <number>\")\n .description(\"Show full details for a specific session\")\n .action((projectSlug: string, number: string) => {\n cmdInfo(getDb(), projectSlug, number);\n });\n\n // pai session rename <project-slug> <number> <new-slug>\n sessionCmd\n .command(\"rename <project-slug> <number> <new-slug>\")\n .description(\n \"Rename a session note — updates file on disk, H1 title, and registry\"\n )\n .action((projectSlug: string, number: string, newSlug: string) => {\n cmdRename(getDb(), projectSlug, number, newSlug);\n });\n\n // pai session slug <project-slug> <number|latest>\n sessionCmd\n .command(\"slug <project-slug> <number>\")\n .description(\n \"Generate a descriptive slug from the session JSONL transcript\"\n )\n .option(\"--apply\", \"Rename the session note using the generated slug\")\n .action((projectSlug: string, number: string, opts: { apply?: boolean }) => {\n cmdSlug(getDb(), projectSlug, number, opts);\n });\n\n // pai session tag <project-slug> <number> [tags...]\n sessionCmd\n .command(\"tag <project-slug> <number> [tags...]\")\n .description(\n \"Set or show tags on a session. Tags can be space-separated or comma-separated.\"\n )\n .action((projectSlug: string, number: string, tags: string[]) => {\n cmdTag(getDb(), projectSlug, number, tags);\n });\n\n // pai session route <project-slug> <number> <target-project>\n sessionCmd\n .command(\"route <project-slug> <number> <target-project>\")\n .description(\n \"Create a cross-reference link from a session to a target project\"\n )\n .option(\n \"--type <type>\",\n \"Link type: related | follow-up | reference\",\n \"related\"\n )\n .action(\n (\n projectSlug: string,\n number: string,\n targetProject: string,\n opts: { type?: string }\n ) => {\n cmdRoute(getDb(), projectSlug, number, targetProject, opts);\n }\n );\n\n // pai session handover [project-slug] [session-id]\n sessionCmd\n .command(\"handover [project-slug] [session-id]\")\n .description(\n \"Write a ## Continue section to the project's TODO.md.\\n\" +\n \"Called automatically from session-stop and pre-compact hooks.\\n\" +\n \"Records the last session identifier, timestamp, and working directory\\n\" +\n \"so the next session can resume from the correct context.\"\n )\n .action((projectSlug: string | undefined, sessionId: string | undefined) => {\n cmdHandover(getDb(), projectSlug, sessionId);\n });\n\n // pai session checkpoint <message>\n sessionCmd\n .command(\"checkpoint <message>\")\n .description(\n \"Append a timestamped checkpoint to the active session note.\\n\" +\n \"Designed for hooks (PostToolUse, UserPromptSubmit) — fast and silent.\\n\" +\n \"Rate-limited: skips silently if last checkpoint was < --min-gap seconds ago.\"\n )\n .option(\n \"--min-gap <seconds>\",\n \"Minimum seconds between checkpoints (default: 300 = 5 minutes)\",\n \"300\"\n )\n .action((message: string, opts: { minGap?: string }) => {\n // Note: does NOT call getDb() — checkpoint must work without the registry\n cmdCheckpoint(message, opts);\n });\n\n // pai session auto-route [--cwd path] [--context \"text\"] [--json]\n sessionCmd\n .command(\"auto-route\")\n .description(\n \"Auto-detect which project this session belongs to.\\n\" +\n \"Tries: (1) path match in registry, (2) Notes/PAI.md marker walk, (3) topic detection.\\n\" +\n \"Designed for use in CLAUDE.md session-start hooks.\"\n )\n .option(\"--cwd <path>\", \"Working directory to detect from (default: process.cwd())\")\n .option(\"--context <text>\", \"Conversation context for topic-based fallback routing\")\n .option(\"--json\", \"Output raw JSON instead of formatted display\")\n .action(\n async (opts: { cwd?: string; context?: string; json?: boolean }) => {\n await cmdAutoRoute(opts);\n }\n );\n}\n","/**\n * pai session cleanup\n *\n * Cleans up session notes across all PAI projects:\n *\n * 1. Identify empty sessions (< 500 bytes OR just template content)\n * 2. Delete truly empty sessions (no real work content)\n * 3. Auto-name sessions still called \"New Session\" that have real content\n * 4. Reorganize flat Notes/ into Notes/YYYY/MM/ hierarchy\n * 5. Renumber sessions sequentially after removals\n * 6. Update the registry DB to reflect new filenames and paths\n * 7. Update pai_files and pai_chunks paths in Postgres to preserve embeddings\n *\n * Flags:\n * --dry-run Show what would change (DEFAULT)\n * --execute Actually perform the cleanup\n * --project <slug> Only clean one project\n * --no-renumber Skip renumbering step\n * --no-reindex Skip triggering memory re-index after moves\n */\n\nimport type { Command } from \"commander\";\nimport type { Database } from \"better-sqlite3\";\nimport {\n ok,\n warn,\n err,\n dim,\n bold,\n header,\n} from \"../utils.js\";\nimport chalk from \"chalk\";\nimport {\n existsSync,\n readdirSync,\n readFileSync,\n writeFileSync,\n mkdirSync,\n renameSync,\n unlinkSync,\n statSync,\n} from \"node:fs\";\nimport { join, dirname } from \"node:path\";\nimport { homedir } from \"node:os\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\ninterface ProjectRow {\n id: number;\n slug: string;\n display_name: string;\n root_path: string;\n encoded_dir: string;\n claude_notes_dir: string | null;\n}\n\ninterface SessionRow {\n id: number;\n project_id: number;\n number: number;\n date: string;\n slug: string;\n title: string;\n filename: string;\n status: string;\n}\n\ntype SessionClassification = \"EMPTY\" | \"UNNAMED\" | \"NAMED\" | \"LEGACY_FORMAT\";\n\ninterface SessionCandidate {\n session: SessionRow | null; // null if file exists on disk but not in DB\n filename: string;\n filepath: string;\n sizeBytes: number;\n classification: SessionClassification;\n autoName?: string; // proposed name for UNNAMED sessions\n date: string;\n number: number;\n}\n\ninterface NotesDirPlan {\n notesDir: string;\n toDelete: SessionCandidate[];\n toRename: SessionCandidate[];\n toMove: SessionCandidate[]; // survivors that need moving to YYYY/MM/ within this dir\n}\n\ninterface CleanupPlan {\n project: ProjectRow;\n notesDirs: NotesDirPlan[]; // one entry per discovered Notes/ directory (up to 2)\n renumberMap: Map<number, number>; // old number → new number (global across both dirs)\n}\n\n// ---------------------------------------------------------------------------\n// Constants\n// ---------------------------------------------------------------------------\n\n// Template content indicators — if the file only contains these patterns,\n// it has no real work content and can be deleted.\nconst TEMPLATE_INDICATORS = [\n \"<!-- PAI will add completed work here during session -->\",\n \"<!-- PAI will add completed work here -->\",\n \"Session completed.\",\n \"Session started and ready for your instructions\",\n];\n\n// Session filename patterns\n// Modern: \"0027 - 2026-02-23 - Meaningful Name.md\"\nconst MODERN_PATTERN = /^(\\d{4}) - (\\d{4}-\\d{2}-\\d{2}) - (.+)\\.md$/;\n// Legacy: \"0001_2025-12-24_session-started-and-ready-for-your-instructions.md\"\nconst LEGACY_PATTERN = /^(\\d{4})_(\\d{4}-\\d{2}-\\d{2})_(.+)\\.md$/;\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\nfunction getAllProjects(db: Database): ProjectRow[] {\n return db\n .prepare(\n \"SELECT id, slug, display_name, root_path, encoded_dir, claude_notes_dir FROM projects WHERE status = 'active' ORDER BY slug\"\n )\n .all() as ProjectRow[];\n}\n\nfunction getProject(db: Database, slug: string): ProjectRow | undefined {\n return db\n .prepare(\n \"SELECT id, slug, display_name, root_path, encoded_dir, claude_notes_dir FROM projects WHERE slug = ?\"\n )\n .get(slug) as ProjectRow | undefined;\n}\n\nfunction getProjectSessions(db: Database, projectId: number): SessionRow[] {\n return db\n .prepare(\"SELECT * FROM sessions WHERE project_id = ? ORDER BY number ASC\")\n .all(projectId) as SessionRow[];\n}\n\n/**\n * Find the project-root Notes directory (e.g. {root}/Notes or {root}/.claude/Notes).\n * Returns null if neither exists on disk.\n */\nfunction findRootNotesDir(rootPath: string): string | null {\n const canonical = join(rootPath, \"Notes\");\n if (existsSync(canonical)) return canonical;\n const alt = join(rootPath, \".claude\", \"Notes\");\n if (existsSync(alt)) return alt;\n return null;\n}\n\n/**\n * Find the Claude Code session notes directory for a project.\n * Falls back to computing the path from encoded_dir if claude_notes_dir is not set.\n * Returns null if the directory does not exist on disk, or if it is identical to\n * rootNotesDir (to avoid processing the same directory twice).\n */\nfunction findClaudeNotesDir(project: ProjectRow, rootNotesDir: string | null): string | null {\n // Prefer the registry-stored path; fall back to computing it from encoded_dir\n const candidate =\n project.claude_notes_dir ??\n join(homedir(), \".claude\", \"projects\", project.encoded_dir, \"Notes\");\n\n if (!existsSync(candidate)) return null;\n // Avoid processing the same directory twice\n if (rootNotesDir && candidate === rootNotesDir) return null;\n return candidate;\n}\n\n/**\n * Collect up to two distinct Notes/ directories for a project.\n * Returns an array of existing, distinct paths in the order:\n * 1. Root Notes/ (from project root_path)\n * 2. Claude Code Notes/ (from claude_notes_dir or encoded_dir)\n */\nfunction findAllNotesDirs(project: ProjectRow): string[] {\n const rootDir = findRootNotesDir(project.root_path);\n const claudeDir = findClaudeNotesDir(project, rootDir);\n const dirs: string[] = [];\n if (rootDir) dirs.push(rootDir);\n if (claudeDir) dirs.push(claudeDir);\n return dirs;\n}\n\n/**\n * Determine if a file's content is essentially empty (just the template).\n *\n * A file is template-only if:\n * - It contains a template placeholder marker AND\n * - The \"Work Done\" section has no real content after the placeholder\n * (i.e., no lines with actual text beyond the placeholder comment itself)\n */\nfunction isTemplateOnly(content: string): boolean {\n const hasTemplateMarker = TEMPLATE_INDICATORS.some((ind) => content.includes(ind));\n if (!hasTemplateMarker) return false;\n\n // Look for \"Work Done\" section and check if there's any real content in it.\n // Real content = non-empty lines that are not:\n // - HTML comments (<!-- ... -->)\n // - Section headers (## ...)\n // - The placeholder text itself\n // - \"Session completed.\" alone\n // - \"#Session\" tags\n\n const lines = content.split(\"\\n\");\n let inWorkDone = false;\n\n for (const line of lines) {\n const trimmed = line.trim();\n if (trimmed === \"## Work Done\") {\n inWorkDone = true;\n continue;\n }\n if (trimmed.startsWith(\"## \") && inWorkDone) {\n // Left the Work Done section\n break;\n }\n if (!inWorkDone) continue;\n\n // Skip empty lines, comment lines, section headers\n if (!trimmed) continue;\n if (trimmed.startsWith(\"<!--\") && trimmed.endsWith(\"-->\")) continue;\n if (trimmed.startsWith(\"<!--\")) continue;\n if (trimmed === \"-->\") continue;\n if (trimmed === \"Session completed.\") continue;\n if (trimmed === \"#Session\" || trimmed === \"**Tags:** #Session\") continue;\n\n // Any other non-empty line = real content\n return false;\n }\n\n return true;\n}\n\n// Meta-phrases that indicate template / status text rather than real work descriptions.\n// Lines matching any of these (case-insensitive) are skipped during auto-naming.\nconst META_PHRASE_PATTERNS: RegExp[] = [\n /session initialized and ready for your instructions/i,\n /fresh session with no pending tasks/i,\n /starting new session.*checking for pending work/i,\n /fresh session with empty todo/i,\n /session started and ready/i,\n /^session\\b.*\\bready\\b/i,\n /^session\\b.*\\binitialized\\b/i,\n /^session\\b.*\\bno pending\\b/i,\n /^session\\b.*\\bno prior work\\b/i,\n /^no pending tasks/i,\n /^no prior work/i,\n];\n\n// Articles and prepositions excluded from Title Case capitalisation.\nconst TITLE_CASE_MINOR_WORDS = new Set([\n \"a\", \"an\", \"the\", \"in\", \"on\", \"at\", \"for\", \"to\", \"of\", \"and\", \"or\",\n \"but\", \"via\", \"with\", \"from\", \"by\", \"as\", \"nor\",\n]);\n\n/**\n * Strip markdown checkbox syntax, bullets, and inline formatting from a line.\n * Returns the cleaned plain text, or null if the result is too short to be useful.\n */\nfunction cleanMarkdownLine(raw: string): string | null {\n let s = raw.trim();\n\n // Strip leading bullets with optional checkboxes: \"- [x]\", \"* [ ]\", \"- \", \"* \", \"+ \"\n s = s.replace(/^[-*+]\\s+\\[[ xX]\\]\\s*/, \"\"); // bullet + checkbox\n s = s.replace(/^[-*+]\\s+/, \"\"); // bullet only\n\n // Strip bare checkboxes at the start: \"[x]\", \"[ ]\", \"[X]\"\n s = s.replace(/^\\[[ xX]\\]\\s*/, \"\");\n\n // Strip inline markdown formatting: **bold**, *italic*, `code`\n // Also handles malformed ** text** (leading space inside bold markers)\n s = s.replace(/\\*\\*\\s*([^*]+?)\\s*\\*\\*/g, \"$1\");\n s = s.replace(/\\*\\s*([^*]+?)\\s*\\*/g, \"$1\");\n s = s.replace(/`([^`]+)`/g, \"$1\");\n\n // Strip any leftover bare asterisks (e.g. \"** Restored...\" → \"Restored...\")\n s = s.replace(/^\\*+\\s*/, \"\");\n\n // Strip leading/trailing punctuation that doesn't belong in a title\n s = s.replace(/^[.,;:]+/, \"\").replace(/[.,;:]+$/, \"\");\n\n // Collapse multiple spaces\n s = s.replace(/\\s+/g, \" \").trim();\n\n return s.length >= 4 ? s : null;\n}\n\n/**\n * Return true if the line contains only meta-status text that should not\n * be used as a session title.\n */\nfunction isMetaPhrase(text: string): boolean {\n return META_PHRASE_PATTERNS.some((re) => re.test(text));\n}\n\n/**\n * Convert a string to Title Case, skipping minor words (articles, prepositions)\n * except as the very first word.\n */\nfunction toTitleCase(text: string): string {\n const words = text.split(\" \");\n return words\n .map((word, i) => {\n const lower = word.toLowerCase();\n if (i !== 0 && TITLE_CASE_MINOR_WORDS.has(lower)) return lower;\n return word.charAt(0).toUpperCase() + word.slice(1);\n })\n .join(\" \");\n}\n\n/**\n * Sanitize a string into a valid filename component.\n * Strips chars that don't belong in filenames, collapses spaces, trims to\n * 60 chars at a word boundary, then applies Title Case.\n */\nfunction sanitizeName(raw: string): string {\n // Remove filesystem-unsafe characters (keep hyphens and apostrophes)\n let s = raw.replace(/[\\/\\\\:*?\"<>|#`]/g, \"\");\n s = s.replace(/\\s+/g, \" \").trim();\n\n // Truncate to 60 chars at a word boundary\n if (s.length > 60) {\n const truncated = s.slice(0, 60);\n const lastSpace = truncated.lastIndexOf(\" \");\n s = lastSpace > 20 ? truncated.slice(0, lastSpace) : truncated;\n }\n\n s = s.trim();\n\n // Apply Title Case\n return toTitleCase(s);\n}\n\n/**\n * Extract a meaningful auto-name from session content.\n *\n * Strategy (in priority order):\n * 1. H2 content sections (## Work Done, ## Summary, etc.):\n * look at the CONTENT under them for the first real work bullet.\n * 2. Other descriptive H2 headings that aren't structural section names.\n * 3. H1 heading — only if it is not a plain session-number line.\n * 5. Fallback: \"Unnamed Session\".\n */\nfunction extractAutoName(content: string): string {\n const lines = content.split(\"\\n\");\n\n // Section headings whose *content* we want to mine (not use as the title itself)\n const CONTENT_SECTION_HEADINGS = new Set([\n \"Work Done\", \"Summary\", \"Completed\", \"What Was Done\",\n \"Results\", \"Outcomes\", \"Changes\", \"Progress\",\n ]);\n\n // Section headings to skip entirely (structural, never useful as title)\n const SKIP_SECTION_HEADINGS = new Set([\n \"Next Steps\", \"Tags\", \"TODO\", \"Blockers\", \"Notes\",\n \"Metadata\", \"Context\", \"Background\",\n ]);\n\n let pastH1 = false;\n // After H1 there's usually metadata (Date, Status, etc.) before the first ---\n // We skip lines until we're past the first horizontal rule.\n let pastFirstHr = false;\n let currentSection: string | null = null;\n const contentSectionLines: string[] = [];\n const otherH2Headings: string[] = [];\n\n for (const line of lines) {\n const trimmed = line.trim();\n\n // Track H1\n if (trimmed.startsWith(\"# \")) {\n pastH1 = true;\n continue;\n }\n\n if (!pastH1) continue;\n\n // Skip the metadata block (Date, Status, Completed timestamp, etc.) before the first ---\n if (!pastFirstHr) {\n if (trimmed === \"---\") {\n pastFirstHr = true;\n }\n // Don't process any lines until we're past the first horizontal rule\n continue;\n }\n\n // H2 headings — classify the section\n if (trimmed.startsWith(\"## \")) {\n const headingText = trimmed.slice(3).trim();\n if (CONTENT_SECTION_HEADINGS.has(headingText)) {\n currentSection = \"content\";\n } else if (SKIP_SECTION_HEADINGS.has(headingText)) {\n currentSection = \"skip\";\n } else {\n // Potentially descriptive H2 — save as candidate title\n currentSection = null;\n otherH2Headings.push(headingText);\n }\n continue;\n }\n\n // H3+ — ignore\n if (trimmed.startsWith(\"#\")) continue;\n\n // Skip pure HTML comment lines (nothing after the closing -->).\n // Do NOT skip lines that start with <!-- but have content after -->.\n if (trimmed === \"-->\") continue;\n // A line like \"<!-- comment -->\" with nothing after is pure comment\n if (trimmed.startsWith(\"<!--\") && /^<!--.*-->$/.test(trimmed)) continue;\n\n if (currentSection === \"content\" && trimmed.length > 0) {\n // A line may start with an inline HTML comment followed by content.\n // Strip the comment prefix if present.\n // e.g. \"<!-- PAI will add completed work here during session -->- [x] **Created wlctl...**\"\n const withoutComment = trimmed.replace(/^<!--.*?-->\\s*/, \"\");\n const effective = withoutComment.length > 0 ? withoutComment : trimmed;\n\n // Skip if stripping the comment left nothing (was a pure comment line)\n if (effective.length === 0) continue;\n\n contentSectionLines.push(effective);\n }\n\n // Nothing useful to collect from non-content sections here\n }\n\n // ---- Strategy 1: first real bullet / line under a content section ----\n for (const raw of contentSectionLines) {\n const cleaned = cleanMarkdownLine(raw);\n if (!cleaned) continue;\n if (isMetaPhrase(cleaned)) continue;\n if (cleaned.startsWith(\"<!--\") || cleaned.includes(\"PAI will add\")) continue;\n if (cleaned.length < 5) continue;\n return sanitizeName(cleaned);\n }\n\n // ---- Strategy 2: descriptive H2 headings ----\n for (const heading of otherH2Headings) {\n const cleaned = cleanMarkdownLine(heading);\n if (!cleaned) continue;\n if (isMetaPhrase(cleaned)) continue;\n if (cleaned.length > 3 && cleaned.length < 80) {\n return sanitizeName(cleaned);\n }\n }\n\n // ---- Strategy 3: H1 if it's not just \"Session NNNN: project-name\" ----\n for (const line of lines) {\n const trimmed = line.trim();\n if (trimmed.startsWith(\"# \")) {\n const title = trimmed.slice(2).trim();\n // Skip bare session-number headers\n if (/^Session \\d{4}/i.test(title)) continue;\n const cleaned = cleanMarkdownLine(title);\n if (cleaned && !isMetaPhrase(cleaned) && cleaned.length > 3) {\n return sanitizeName(cleaned);\n }\n }\n }\n\n return \"Unnamed Session\";\n}\n\n/**\n * Format a 4-digit padded session number.\n */\nfunction padNum(n: number): string {\n return String(n).padStart(4, \"0\");\n}\n\n/**\n * Given the sessions still alive after deletion, build a renumber map.\n * Returns a Map of oldNumber → newNumber.\n */\nfunction buildRenumberMap(survivors: SessionCandidate[]): Map<number, number> {\n const map = new Map<number, number>();\n // Sort by existing number to preserve chronological order\n const sorted = [...survivors].sort((a, b) => a.number - b.number);\n sorted.forEach((s, idx) => {\n const newNum = idx + 1;\n if (s.number !== newNum) {\n map.set(s.number, newNum);\n }\n });\n return map;\n}\n\n/**\n * Compute the target path in YYYY/MM/ hierarchy.\n */\nfunction getTargetPath(notesDir: string, date: string, filename: string): string {\n // date is \"YYYY-MM-DD\"\n const [year, month] = date.split(\"-\");\n return join(notesDir, year, month, filename);\n}\n\n// ---------------------------------------------------------------------------\n// Analysis phase — build a CleanupPlan for one project\n// ---------------------------------------------------------------------------\n\n/**\n * Scan a single Notes/ directory and return all session candidates found in it.\n * Looks in both the flat top-level and any YYYY/MM/ sub-directories.\n */\nfunction scanNotesDir(\n notesDir: string,\n dbByFilename: Map<string, SessionRow>\n): SessionCandidate[] {\n const candidates: SessionCandidate[] = [];\n\n // Read flat .md files at the top level\n let flatFiles: string[] = [];\n try {\n flatFiles = readdirSync(notesDir, { withFileTypes: true })\n .filter((d) => d.isFile() && d.name.endsWith(\".md\"))\n .map((d) => d.name);\n } catch {\n // Directory unreadable\n }\n\n // Also find already-moved files in YYYY/MM/ sub-dirs\n const subDirFiles: { filename: string; filepath: string }[] = [];\n try {\n const topEntries = readdirSync(notesDir, { withFileTypes: true });\n for (const entry of topEntries) {\n if (!entry.isDirectory()) continue;\n if (!/^\\d{4}$/.test(entry.name)) continue;\n const yearDir = join(notesDir, entry.name);\n const monthDirs = readdirSync(yearDir, { withFileTypes: true });\n for (const mEntry of monthDirs) {\n if (!mEntry.isDirectory()) continue;\n const monthDir = join(yearDir, mEntry.name);\n const files = readdirSync(monthDir).filter((f) => f.endsWith(\".md\"));\n for (const f of files) {\n subDirFiles.push({ filename: f, filepath: join(monthDir, f) });\n }\n }\n }\n } catch {\n // Ignore errors scanning sub-dirs\n }\n\n // Combine: flat files + already-moved files\n const allFiles: { filename: string; filepath: string }[] = [\n ...flatFiles.map((f) => ({ filename: f, filepath: join(notesDir, f) })),\n ...subDirFiles,\n ];\n\n for (const { filename, filepath } of allFiles) {\n let num: number;\n let date: string;\n let namepart: string;\n let classification: SessionClassification;\n\n const modernMatch = MODERN_PATTERN.exec(filename);\n const legacyMatch = LEGACY_PATTERN.exec(filename);\n\n if (modernMatch) {\n num = parseInt(modernMatch[1], 10);\n date = modernMatch[2];\n namepart = modernMatch[3];\n classification = \"NAMED\";\n } else if (legacyMatch) {\n num = parseInt(legacyMatch[1], 10);\n date = legacyMatch[2];\n namepart = legacyMatch[3];\n classification = \"LEGACY_FORMAT\";\n } else {\n // Not a session file — skip (e.g., TODO.md)\n continue;\n }\n\n let sizeBytes = 0;\n let content = \"\";\n try {\n const stat = statSync(filepath);\n sizeBytes = stat.size;\n content = readFileSync(filepath, \"utf8\");\n } catch {\n continue;\n }\n\n // Look up DB record — sessions table stores filename without YYYY/MM/ prefix\n // for flat files, or with it for already-moved files. Try both.\n const dbSession =\n dbByFilename.get(filename) ??\n dbByFilename.get(filepath.split(`${notesDir}/`)[1] ?? \"\") ??\n null;\n\n if (classification !== \"LEGACY_FORMAT\") {\n if (sizeBytes < 400 || isTemplateOnly(content)) {\n classification = \"EMPTY\";\n } else if (\n namepart === \"New Session\" ||\n namepart === (process.env.USER ?? \"\") ||\n namepart === \"session-started-and-ready-for-your-instructions\"\n ) {\n classification = \"UNNAMED\";\n }\n }\n\n const candidate: SessionCandidate = {\n session: dbSession,\n filename,\n filepath,\n sizeBytes,\n classification,\n date,\n number: num,\n };\n\n if (classification === \"UNNAMED\" || classification === \"LEGACY_FORMAT\") {\n candidate.autoName = extractAutoName(content);\n }\n\n candidates.push(candidate);\n }\n\n return candidates;\n}\n\nfunction analyzeProject(\n db: Database,\n project: ProjectRow\n): CleanupPlan | null {\n const notesDirPaths = findAllNotesDirs(project);\n\n if (notesDirPaths.length === 0) return null;\n\n const dbSessions = getProjectSessions(db, project.id);\n const dbByFilename = new Map<string, SessionRow>();\n for (const s of dbSessions) {\n dbByFilename.set(s.filename, s);\n }\n\n // Scan each Notes/ directory independently, collecting per-dir plan info\n const notesDirPlans: NotesDirPlan[] = [];\n // All survivors across ALL dirs — used for global renumbering\n const allSurvivors: SessionCandidate[] = [];\n\n for (const notesDir of notesDirPaths) {\n const candidates = scanNotesDir(notesDir, dbByFilename);\n if (candidates.length === 0) continue;\n\n const toDelete = candidates.filter((c) => c.classification === \"EMPTY\");\n const toRename = candidates.filter(\n (c) => c.classification === \"UNNAMED\" || c.classification === \"LEGACY_FORMAT\"\n );\n const survivors = candidates.filter((c) => c.classification !== \"EMPTY\");\n\n notesDirPlans.push({ notesDir, toDelete, toRename, toMove: survivors });\n allSurvivors.push(...survivors);\n }\n\n if (notesDirPlans.length === 0) return null;\n\n // Global renumber map across both directories\n const renumberMap = buildRenumberMap(allSurvivors);\n\n return {\n project,\n notesDirs: notesDirPlans,\n renumberMap,\n };\n}\n\n// ---------------------------------------------------------------------------\n// Dry-run display\n// ---------------------------------------------------------------------------\n\nasync function displayDryRun(plans: CleanupPlan[]): Promise<void> {\n let totalDelete = 0;\n let totalRename = 0;\n let totalMove = 0;\n let totalRenumber = 0;\n\n for (const plan of plans) {\n const hasWork =\n plan.notesDirs.some(\n (d) => d.toDelete.length > 0 || d.toRename.length > 0 || d.toMove.length > 0\n ) || plan.renumberMap.size > 0;\n\n if (!hasWork) continue;\n\n console.log();\n console.log(header(` Project: ${plan.project.display_name} (${plan.project.slug})`));\n\n for (const dirPlan of plan.notesDirs) {\n console.log(dim(` Notes: ${dirPlan.notesDir}`));\n\n if (dirPlan.toDelete.length > 0) {\n console.log(bold(\" DELETE (empty/template-only sessions):\"));\n for (const c of dirPlan.toDelete) {\n console.log(\n ` ${chalk.red(\"DEL\")} ${dim(padNum(c.number))} - ${c.date} - ${c.filename.split(\" - \").slice(2).join(\" - \")} ${dim(`(${c.sizeBytes}b)`)}`\n );\n totalDelete++;\n }\n console.log();\n }\n\n if (dirPlan.toRename.length > 0) {\n console.log(bold(\" RENAME (unnamed or legacy-format sessions):\"));\n for (const c of dirPlan.toRename) {\n const autoName = c.autoName ?? \"Unnamed Session\";\n console.log(` ${chalk.yellow(\"REN\")} ${c.filename}`);\n console.log(` → ${padNum(c.number)} - ${c.date} - ${autoName}.md`);\n totalRename++;\n }\n console.log();\n }\n\n if (dirPlan.toMove.length > 0) {\n console.log(bold(\" MOVE TO YYYY/MM/ hierarchy:\"));\n for (const c of dirPlan.toMove) {\n const [year, month] = c.date.split(\"-\");\n console.log(` ${chalk.cyan(\"MOV\")} ${c.filename}`);\n console.log(` → ${year}/${month}/${c.filename}`);\n totalMove++;\n }\n console.log();\n }\n }\n\n if (plan.renumberMap.size > 0) {\n console.log(bold(\" RENUMBER (after deletions, global across all Notes/ dirs):\"));\n for (const [oldN, newN] of plan.renumberMap) {\n console.log(` ${chalk.blue(\"NUM\")} #${padNum(oldN)} → #${padNum(newN)}`);\n totalRenumber++;\n }\n console.log();\n }\n }\n\n // Collect absolute paths of all files that would be moved, to count\n // how many have existing vector DB entries (helps user understand embedding impact).\n const wouldMovePaths: string[] = [];\n for (const plan of plans) {\n for (const dirPlan of plan.notesDirs) {\n for (const c of dirPlan.toMove) {\n const [year, month] = c.date.split(\"-\");\n const targetPath = join(dirPlan.notesDir, year, month, c.filename);\n if (c.filepath !== targetPath) {\n wouldMovePaths.push(c.filepath);\n }\n }\n }\n }\n\n const vectorDbCount = await countVectorDbPaths(wouldMovePaths);\n\n console.log();\n console.log(bold(\" Summary (dry-run):\"));\n console.log(` ${chalk.red(\"DEL\")} ${totalDelete} empty sessions to delete`);\n console.log(` ${chalk.yellow(\"REN\")} ${totalRename} unnamed sessions to rename`);\n console.log(` ${chalk.blue(\"NUM\")} ${totalRenumber} sessions to renumber`);\n console.log(` ${chalk.cyan(\"MOV\")} ${totalMove} sessions to move into YYYY/MM/ dirs`);\n if (vectorDbCount > 0) {\n console.log(` ${chalk.magenta(\"VEC\")} ${vectorDbCount} file path(s) will be updated in the vector DB (embeddings preserved)`);\n } else if (wouldMovePaths.length > 0) {\n console.log(` ${chalk.magenta(\"VEC\")} 0 file path(s) found in vector DB for moved files (no embeddings to preserve)`);\n }\n console.log();\n console.log(warn(\" This is a dry-run. Add --execute to apply changes.\"));\n console.log();\n}\n\n// ---------------------------------------------------------------------------\n// Postgres path update helper\n// ---------------------------------------------------------------------------\n\n/**\n * Count how many files in the vector DB match the given old paths.\n * Used for dry-run reporting. Returns 0 if Postgres is unavailable.\n */\nasync function countVectorDbPaths(oldPaths: string[]): Promise<number> {\n if (oldPaths.length === 0) return 0;\n\n try {\n const { loadConfig } = await import(\"../../daemon/config.js\");\n const { PostgresBackend } = await import(\"../../storage/postgres.js\");\n\n const config = loadConfig();\n if (config.storageBackend !== \"postgres\") return 0;\n\n const pgConfig = config.postgres ?? {};\n const pgBackend = new PostgresBackend(pgConfig);\n\n const connErr = await pgBackend.testConnection();\n if (connErr) {\n await pgBackend.close();\n return 0;\n }\n\n const pool = (pgBackend as unknown as { pool: { query: (sql: string, params: string[]) => Promise<{ rows: Array<{ n: string }> }> } }).pool;\n const placeholders = oldPaths.map((_, i) => `$${i + 1}`).join(\", \");\n const result = await pool.query(\n `SELECT COUNT(*)::text AS n FROM pai_files WHERE path IN (${placeholders})`,\n oldPaths\n );\n\n await pgBackend.close();\n return parseInt(result.rows[0]?.n ?? \"0\", 10);\n } catch {\n return 0;\n }\n}\n\n/**\n * Update file paths in pai_files and pai_chunks for all moved session notes.\n * Returns the number of pai_files rows updated, or -1 on error.\n *\n * Both tables store path directly (no FK between them), so both must be updated.\n */\nasync function updateVectorDbPaths(\n moves: Array<{ oldPath: string; newPath: string }>\n): Promise<number> {\n if (moves.length === 0) return 0;\n\n try {\n const { loadConfig } = await import(\"../../daemon/config.js\");\n const { PostgresBackend } = await import(\"../../storage/postgres.js\");\n\n const config = loadConfig();\n if (config.storageBackend !== \"postgres\") return 0;\n\n const pgConfig = config.postgres ?? {};\n const pgBackend = new PostgresBackend(pgConfig);\n\n const connErr = await pgBackend.testConnection();\n if (connErr) {\n process.stderr.write(`[session-cleanup] Postgres unavailable (${connErr}). Skipping vector DB path update.\\n`);\n await pgBackend.close();\n return 0;\n }\n\n const pool = (pgBackend as unknown as { pool: { connect: () => Promise<{ query: (sql: string, params: string[]) => Promise<{ rowCount: number | null }>; release: () => void }> } }).pool;\n const client = await pool.connect();\n\n let filesUpdated = 0;\n\n try {\n await client.query(\"BEGIN\", []);\n\n for (const { oldPath, newPath } of moves) {\n const filesResult = await client.query(\n \"UPDATE pai_files SET path = $1 WHERE path = $2\",\n [newPath, oldPath]\n );\n filesUpdated += filesResult.rowCount ?? 0;\n\n await client.query(\n \"UPDATE pai_chunks SET path = $1 WHERE path = $2\",\n [newPath, oldPath]\n );\n }\n\n await client.query(\"COMMIT\", []);\n } catch (e) {\n await client.query(\"ROLLBACK\", []);\n throw e;\n } finally {\n client.release();\n }\n\n await pgBackend.close();\n return filesUpdated;\n } catch (e) {\n process.stderr.write(`[session-cleanup] Failed to update vector DB paths: ${e}\\n`);\n return -1;\n }\n}\n\n// ---------------------------------------------------------------------------\n// Execution phase\n// ---------------------------------------------------------------------------\n\nasync function executeCleanup(db: Database, plans: CleanupPlan[], skipReindex: boolean): Promise<void> {\n let deleted = 0;\n let renamed = 0;\n let moved = 0;\n let renumbered = 0;\n let dbUpdated = 0;\n\n // Track all file moves: old absolute path → new absolute path\n // Used to update pai_files and pai_chunks in Postgres after all moves complete.\n const vectorDbMoves: Array<{ oldPath: string; newPath: string }> = [];\n\n for (const plan of plans) {\n console.log();\n console.log(header(` Project: ${plan.project.display_name} (${plan.project.slug})`));\n\n // Process each Notes/ directory independently for delete, rename, and move\n for (const dirPlan of plan.notesDirs) {\n const { notesDir } = dirPlan;\n\n if (plan.notesDirs.length > 1) {\n console.log(dim(` Directory: ${notesDir}`));\n }\n\n // -----------------------------------------------------------------------\n // Step 1: Delete empty sessions\n // -----------------------------------------------------------------------\n for (const c of dirPlan.toDelete) {\n try {\n unlinkSync(c.filepath);\n console.log(ok(` DEL ${c.filename}`));\n deleted++;\n } catch (e) {\n console.log(err(` FAIL to delete ${c.filename}: ${e}`));\n }\n\n // Remove from DB\n if (c.session) {\n try {\n db.prepare(\"DELETE FROM sessions WHERE id = ?\").run(c.session.id);\n dbUpdated++;\n } catch (e) {\n console.log(err(` FAIL to remove session #${c.number} from DB: ${e}`));\n }\n }\n }\n\n // -----------------------------------------------------------------------\n // Step 2: Rename unnamed/legacy sessions (in-place, before moving)\n // -----------------------------------------------------------------------\n for (const c of dirPlan.toRename) {\n const autoName = c.autoName ?? \"Unnamed Session\";\n const newFilename = `${padNum(c.number)} - ${c.date} - ${autoName}.md`;\n const newPath = join(notesDir, newFilename);\n\n if (c.filepath !== newPath) {\n try {\n renameSync(c.filepath, newPath);\n console.log(ok(` REN ${c.filename}`));\n console.log(dim(` → ${newFilename}`));\n renamed++;\n // Update candidate for subsequent move step\n (c as { filename: string }).filename = newFilename;\n (c as { filepath: string }).filepath = newPath;\n } catch (e) {\n console.log(err(` FAIL rename ${c.filename}: ${e}`));\n continue;\n }\n }\n\n // Update content H1\n try {\n const content = readFileSync(newPath, \"utf8\");\n const lines = content.split(\"\\n\");\n let h1Updated = false;\n const updated = lines.map((line) => {\n if (!h1Updated && line.startsWith(\"# \")) {\n h1Updated = true;\n return `# ${autoName}`;\n }\n return line;\n });\n if (!h1Updated) {\n updated.unshift(`# ${autoName}`, \"\");\n }\n writeFileSync(newPath, updated.join(\"\\n\"), \"utf8\");\n } catch {\n // Non-fatal\n }\n\n // Update DB\n if (c.session) {\n const normalizedSlug = autoName\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, \"-\")\n .replace(/^-+|-+$/g, \"\");\n try {\n db.prepare(\n \"UPDATE sessions SET slug = ?, title = ?, filename = ? WHERE id = ?\"\n ).run(normalizedSlug, autoName, newFilename, c.session.id);\n dbUpdated++;\n } catch (e) {\n console.log(err(` FAIL DB update for session #${c.number}: ${e}`));\n }\n }\n }\n\n // -----------------------------------------------------------------------\n // Step 3a: Renumber survivors within this directory (global map)\n // -----------------------------------------------------------------------\n if (plan.renumberMap.size > 0) {\n const toRenumber = dirPlan.toMove.filter((c) => plan.renumberMap.has(c.number));\n\n // Pass 1: rename all candidates to temp files to avoid conflicts\n const tempFiles: { candidate: SessionCandidate; tempPath: string; newNum: number }[] = [];\n for (const c of toRenumber) {\n const newNum = plan.renumberMap.get(c.number)!;\n const tempFilename = `__tmp_${padNum(c.number)}_${c.filename}`;\n const tempPath = join(notesDir, tempFilename);\n try {\n if (existsSync(c.filepath)) {\n renameSync(c.filepath, tempPath);\n tempFiles.push({ candidate: c, tempPath, newNum });\n }\n } catch (e) {\n console.log(err(` FAIL temp-rename #${c.number}: ${e}`));\n }\n }\n\n // Pass 2: rename temp files to final names with new numbers\n for (const { candidate: c, tempPath, newNum } of tempFiles) {\n const newFilename = c.filename.replace(/^\\d{4}/, padNum(newNum));\n const newPath = join(notesDir, newFilename);\n try {\n renameSync(tempPath, newPath);\n console.log(ok(` NUM #${padNum(c.number)} → #${padNum(newNum)}: ${newFilename}`));\n renumbered++;\n // Update candidate for the move step\n (c as { filename: string }).filename = newFilename;\n (c as { filepath: string }).filepath = newPath;\n (c as { number: number }).number = newNum;\n } catch (e) {\n console.log(err(` FAIL final-rename #${newNum}: ${e}`));\n }\n\n // Update content H1 to reflect new number\n if (existsSync(newPath)) {\n try {\n const content = readFileSync(newPath, \"utf8\");\n const lines = content.split(\"\\n\");\n const updated = lines.map((line) => {\n if (line.match(/^# Session \\d{4}:/)) {\n return line.replace(/^# Session \\d{4}:/, `# Session ${padNum(newNum)}:`);\n }\n return line;\n });\n writeFileSync(newPath, updated.join(\"\\n\"), \"utf8\");\n } catch {\n // Non-fatal\n }\n }\n }\n\n // Update DB for sessions in this directory (two-pass for UNIQUE constraint safety)\n const dbRenumbers = tempFiles\n .filter(({ candidate: c }) => c.session != null)\n .map(({ candidate: c, newNum }) => ({ session: c.session!, newNum, newFilename: c.filename }));\n\n if (dbRenumbers.length > 0) {\n const renumberDb = db.transaction(() => {\n for (const { session, newNum } of dbRenumbers) {\n db.prepare(\"UPDATE sessions SET number = ? WHERE id = ?\").run(-newNum, session.id);\n }\n for (const { session, newNum, newFilename } of dbRenumbers) {\n db.prepare(\"UPDATE sessions SET number = ?, filename = ? WHERE id = ?\").run(\n newNum, newFilename, session.id\n );\n }\n });\n try {\n renumberDb();\n dbUpdated += dbRenumbers.length;\n } catch (e) {\n console.log(err(` FAIL DB renumber transaction: ${e}`));\n }\n }\n }\n\n // -----------------------------------------------------------------------\n // Step 3b: Move survivors to YYYY/MM/ hierarchy within this Notes/ dir\n // -----------------------------------------------------------------------\n for (const c of dirPlan.toMove) {\n const [year, month] = c.date.split(\"-\");\n const targetDir = join(notesDir, year, month);\n const targetPath = join(targetDir, c.filename);\n\n // Skip if already in the right place\n if (c.filepath === targetPath) continue;\n\n // Create target dir\n try {\n mkdirSync(targetDir, { recursive: true });\n } catch (e) {\n console.log(err(` FAIL mkdir ${targetDir}: ${e}`));\n continue;\n }\n\n // Move the file\n const oldAbsPath = c.filepath;\n try {\n if (existsSync(c.filepath)) {\n renameSync(c.filepath, targetPath);\n console.log(ok(` MOV ${c.filename}`));\n console.log(dim(` → ${year}/${month}/${c.filename}`));\n moved++;\n vectorDbMoves.push({ oldPath: oldAbsPath, newPath: targetPath });\n }\n } catch (e) {\n console.log(err(` FAIL move ${c.filename}: ${e}`));\n continue;\n }\n\n // Update DB filename to include the YYYY/MM/ prefix\n const newFilenameInDb = `${year}/${month}/${c.filename}`;\n if (c.session) {\n try {\n db.prepare(\"UPDATE sessions SET filename = ? WHERE id = ?\").run(\n newFilenameInDb,\n c.session.id\n );\n dbUpdated++;\n } catch (e) {\n console.log(err(` FAIL DB update path for ${c.filename}: ${e}`));\n }\n }\n }\n }\n }\n\n // -----------------------------------------------------------------------\n // Step 5: Update Postgres vector DB paths to preserve embeddings\n // -----------------------------------------------------------------------\n let vectorDbUpdated = 0;\n if (vectorDbMoves.length > 0) {\n console.log();\n console.log(dim(` Updating ${vectorDbMoves.length} file path(s) in vector DB to preserve embeddings...`));\n const result = await updateVectorDbPaths(vectorDbMoves);\n if (result >= 0) {\n vectorDbUpdated = result;\n console.log(ok(` Updated ${vectorDbUpdated} file path(s) in Postgres (embeddings preserved)`));\n } else {\n console.log(warn(\" Vector DB path update failed — embeddings may be orphaned (check logs)\"));\n }\n }\n\n console.log();\n console.log(bold(\" Cleanup complete:\"));\n console.log(ok(` ${deleted} session(s) deleted`));\n console.log(ok(` ${renamed} session(s) renamed`));\n console.log(ok(` ${renumbered} session(s) renumbered`));\n console.log(ok(` ${moved} session(s) moved to YYYY/MM/ hierarchy`));\n console.log(ok(` ${dbUpdated} registry DB record(s) updated`));\n if (vectorDbMoves.length > 0) {\n console.log(ok(` ${vectorDbUpdated} vector DB file path(s) updated (embeddings preserved)`));\n }\n\n if (!skipReindex) {\n console.log();\n console.log(dim(\" Memory re-index: the PAI daemon will pick up changes within 5 minutes.\"));\n console.log(dim(\" To force immediate re-index: pai memory index --all\"));\n }\n\n console.log();\n}\n\n// ---------------------------------------------------------------------------\n// Commander registration\n// ---------------------------------------------------------------------------\n\nexport function registerSessionCleanupCommand(\n sessionCmd: Command,\n getDb: () => Database\n): void {\n sessionCmd\n .command(\"cleanup [project-slug]\")\n .description(\n \"Clean up session notes: delete empties, auto-name unnamed, move into YYYY/MM/ hierarchy, renumber\"\n )\n .option(\"--execute\", \"Actually perform the cleanup (default is dry-run)\")\n .option(\"--no-renumber\", \"Skip renumbering sessions after deletions\")\n .option(\"--no-reindex\", \"Skip triggering memory re-index after moves\")\n .action(\n async (\n projectSlug: string | undefined,\n opts: { execute?: boolean; renumber?: boolean; reindex?: boolean }\n ) => {\n const db = getDb();\n const dryRun = !opts.execute;\n const skipReindex = opts.reindex === false;\n\n let projects: ProjectRow[];\n if (projectSlug) {\n const p = getProject(db, projectSlug);\n if (!p) {\n console.error(err(`Project not found: ${projectSlug}`));\n process.exit(1);\n }\n projects = [p];\n } else {\n projects = getAllProjects(db);\n }\n\n console.log();\n console.log(\n header(\n dryRun\n ? \" pai session cleanup — DRY RUN (no changes will be made)\"\n : \" pai session cleanup — EXECUTING\"\n )\n );\n console.log(\n dim(` Analyzing ${projects.length} project(s)...`)\n );\n\n const plans: CleanupPlan[] = [];\n for (const project of projects) {\n const plan = analyzeProject(db, project);\n if (plan) {\n plans.push(plan);\n }\n }\n\n const activePlans = plans.filter(\n (p) =>\n p.notesDirs.some(\n (d) => d.toDelete.length > 0 || d.toRename.length > 0 || d.toMove.length > 0\n ) || p.renumberMap.size > 0\n );\n\n if (activePlans.length === 0) {\n console.log();\n console.log(ok(\" Nothing to do — all session notes are clean!\"));\n console.log();\n return;\n }\n\n if (dryRun) {\n await displayDryRun(activePlans);\n } else {\n await executeCleanup(db, activePlans, skipReindex);\n }\n }\n );\n}\n","/**\n * pai registry <sub-command>\n *\n * scan — walk ~/.claude/projects/ and populate the registry\n * migrate — import from ~/.claude/session-registry.json\n * stats — quick summary counts\n * rebuild — drop all data and rescan from filesystem\n */\n\nimport type { Command } from \"commander\";\nimport type { Database } from \"better-sqlite3\";\nimport {\n ok,\n warn,\n err,\n dim,\n bold,\n fmtDate,\n now,\n} from \"../utils.js\";\nimport { decodeEncodedDir, slugify, parseSessionFilename, buildEncodedDirMap } from \"../../registry/migrate.js\";\nimport { ensurePaiMarker, discoverPaiMarkers } from \"../../registry/pai-marker.js\";\nimport { existsSync, readdirSync, statSync, readFileSync, writeFileSync, mkdirSync } from \"node:fs\";\nimport { join, basename, resolve } from \"node:path\";\nimport { homedir } from \"node:os\";\nimport { encodeDir } from \"../utils.js\";\n\n/**\n * Recursively find all .md files in a directory, including YYYY/MM subdirectories\n * created by session cleanup. Returns filenames (basename only).\n */\nfunction findNoteFiles(dir: string): string[] {\n const results: string[] = [];\n if (!existsSync(dir)) return results;\n\n for (const entry of readdirSync(dir, { withFileTypes: true })) {\n if (entry.isFile() && entry.name.endsWith(\".md\")) {\n results.push(entry.name);\n } else if (entry.isDirectory() && /^\\d{4}$/.test(entry.name)) {\n // Year directory (e.g. 2026/) — recurse into month dirs\n const yearDir = join(dir, entry.name);\n for (const monthEntry of readdirSync(yearDir, { withFileTypes: true })) {\n if (monthEntry.isDirectory() && /^\\d{2}$/.test(monthEntry.name)) {\n const monthDir = join(yearDir, monthEntry.name);\n for (const noteEntry of readdirSync(monthDir, { withFileTypes: true })) {\n if (noteEntry.isFile() && noteEntry.name.endsWith(\".md\")) {\n results.push(noteEntry.name);\n }\n }\n }\n }\n }\n }\n return results;\n}\n\n// ---------------------------------------------------------------------------\n// Constants & config\n// ---------------------------------------------------------------------------\n\nconst CLAUDE_PROJECTS_DIR = join(homedir(), \".claude\", \"projects\");\nconst PAI_CONFIG_DIR = join(homedir(), \".pai\");\nconst PAI_CONFIG_FILE = join(PAI_CONFIG_DIR, \"config.json\");\n\ninterface PaiConfig {\n scan_dirs: string[];\n}\n\nfunction loadConfig(): PaiConfig {\n if (!existsSync(PAI_CONFIG_FILE)) return { scan_dirs: [] };\n try {\n return JSON.parse(readFileSync(PAI_CONFIG_FILE, \"utf8\")) as PaiConfig;\n } catch {\n return { scan_dirs: [] };\n }\n}\n\nfunction saveConfig(config: PaiConfig): void {\n mkdirSync(PAI_CONFIG_DIR, { recursive: true });\n writeFileSync(PAI_CONFIG_FILE, JSON.stringify(config, null, 2) + \"\\n\", \"utf8\");\n}\n\nfunction resolveHome(p: string): string {\n if (p.startsWith(\"~/\")) return join(homedir(), p.slice(2));\n return resolve(p);\n}\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Upsert a project row. Returns { id, isNew }.\n *\n * Matching priority:\n * 1. root_path — most reliable; handles slug collisions\n * 2. encoded_dir — Claude project dirs are canonical\n * 3. Insert with suffix-deduplication on slug collision\n */\nfunction upsertProject(\n db: Database,\n slug: string,\n rootPath: string,\n encodedDir: string\n): { id: number; isNew: boolean } {\n const ts = now();\n\n // 1. Match by root_path\n const byPath = db\n .prepare(\"SELECT id FROM projects WHERE root_path = ?\")\n .get(rootPath) as { id: number } | undefined;\n\n if (byPath) {\n // Guard the UPDATE: only set encoded_dir if no OTHER row already owns it.\n const encodedOwner = db\n .prepare(\"SELECT id FROM projects WHERE encoded_dir = ?\")\n .get(encodedDir) as { id: number } | undefined;\n\n if (!encodedOwner || encodedOwner.id === byPath.id) {\n // Safe to update — nobody else owns this encoded_dir.\n db.prepare(\n \"UPDATE projects SET encoded_dir = ?, updated_at = ? WHERE id = ?\"\n ).run(encodedDir, ts, byPath.id);\n }\n // If another row owns the encodedDir, leave both rows as-is; the duplicate\n // encoded_dir situation is resolved by returning the root_path match.\n return { id: byPath.id, isNew: false };\n }\n\n // 2. Match by encoded_dir (same Claude project dir, different root_path record)\n const byEncoded = db\n .prepare(\"SELECT id FROM projects WHERE encoded_dir = ?\")\n .get(encodedDir) as { id: number } | undefined;\n\n if (byEncoded) {\n // Guard the UPDATE: only set root_path if no OTHER row already owns it.\n const pathOwner = db\n .prepare(\"SELECT id FROM projects WHERE root_path = ?\")\n .get(rootPath) as { id: number } | undefined;\n\n if (!pathOwner || pathOwner.id === byEncoded.id) {\n db.prepare(\n \"UPDATE projects SET root_path = ?, updated_at = ? WHERE id = ?\"\n ).run(rootPath, ts, byEncoded.id);\n }\n return { id: byEncoded.id, isNew: false };\n }\n\n // 3. Insert — deduplicate slug with numeric suffix if needed.\n // Use INSERT OR IGNORE so that any remaining UNIQUE collision (e.g. a\n // race condition or an edge case in the checks above) does not throw.\n // We then re-query to return the winning row's id.\n let finalSlug = slug;\n let attempt = 0;\n while (true) {\n const conflict = db\n .prepare(\"SELECT id FROM projects WHERE slug = ?\")\n .get(finalSlug) as { id: number } | undefined;\n if (!conflict) break;\n attempt++;\n finalSlug = `${slug}-${attempt}`;\n }\n\n const result = db\n .prepare(\n `INSERT OR IGNORE INTO projects\n (slug, display_name, root_path, encoded_dir, type, status, created_at, updated_at)\n VALUES (?, ?, ?, ?, 'local', 'active', ?, ?)`\n )\n .run(finalSlug, finalSlug, rootPath, encodedDir, ts, ts);\n\n // If the insert was suppressed by OR IGNORE, fall back to whichever existing\n // row we can find and treat it as an update.\n if (result.changes === 0) {\n const fallback =\n (db.prepare(\"SELECT id FROM projects WHERE encoded_dir = ?\").get(encodedDir) as { id: number } | undefined) ??\n (db.prepare(\"SELECT id FROM projects WHERE root_path = ?\").get(rootPath) as { id: number } | undefined);\n\n if (fallback) {\n return { id: fallback.id, isNew: false };\n }\n\n // Should never happen, but guard against it to avoid crashing.\n throw new Error(\n `upsertProject: INSERT OR IGNORE was suppressed but no matching row found ` +\n `for root_path=${rootPath} encoded_dir=${encodedDir}`\n );\n }\n\n return { id: result.lastInsertRowid as number, isNew: true };\n}\n\n/**\n * Upsert a session note. Returns true if newly inserted.\n */\nfunction upsertSession(\n db: Database,\n projectId: number,\n number: number,\n date: string,\n slug: string,\n title: string,\n filename: string\n): boolean {\n const existing = db\n .prepare(\"SELECT id FROM sessions WHERE project_id = ? AND number = ?\")\n .get(projectId, number);\n\n if (existing) return false;\n\n const ts = now();\n db.prepare(\n `INSERT INTO sessions\n (project_id, number, date, slug, title, filename, status, created_at)\n VALUES (?, ?, ?, ?, ?, ?, 'completed', ?)`\n ).run(projectId, number, date, slug, title, filename, ts);\n\n return true;\n}\n\n// ---------------------------------------------------------------------------\n// Scan command\n// ---------------------------------------------------------------------------\n\ninterface ScanResult {\n projectsScanned: number;\n projectsNew: number;\n projectsUpdated: number;\n sessionsScanned: number;\n sessionsNew: number;\n skipped: string[];\n}\n\nfunction performScan(db: Database): ScanResult {\n const result: ScanResult = {\n projectsScanned: 0,\n projectsNew: 0,\n projectsUpdated: 0,\n sessionsScanned: 0,\n sessionsNew: 0,\n skipped: [],\n };\n\n if (!existsSync(CLAUDE_PROJECTS_DIR)) {\n throw new Error(\n `Claude projects directory not found: ${CLAUDE_PROJECTS_DIR}`\n );\n }\n\n const entries = readdirSync(CLAUDE_PROJECTS_DIR).filter((name) => {\n const full = join(CLAUDE_PROJECTS_DIR, name);\n return statSync(full).isDirectory();\n });\n\n // Build authoritative encoded-dir → original_path lookup from session-registry.json\n // so that decodeEncodedDir uses real paths (the encoding is lossy and ambiguous).\n const lookupMap = buildEncodedDirMap();\n\n for (const encodedDir of entries) {\n // Decode the Claude-encoded directory name back to an absolute path.\n // Uses the authoritative lookup map first, falling back to heuristic decode.\n const rootPath = decodeEncodedDir(encodedDir, lookupMap);\n\n if (!existsSync(rootPath)) {\n result.skipped.push(\n `${encodedDir} (decoded: ${rootPath} — path not found on disk)`\n );\n result.projectsScanned++;\n continue;\n }\n\n const slug = slugify(basename(rootPath) || encodedDir);\n const { id, isNew } = upsertProject(db, slug, rootPath, encodedDir);\n\n result.projectsScanned++;\n if (isNew) result.projectsNew++;\n else result.projectsUpdated++;\n\n // Ensure PAI.md marker exists (or is up-to-date) for this project.\n try {\n ensurePaiMarker(rootPath, slug);\n } catch {\n // Non-fatal — marker creation failure should not abort the scan.\n }\n\n // Scan the Notes/ subdirectory inside the Claude project dir\n const claudeNotesDir = join(CLAUDE_PROJECTS_DIR, encodedDir, \"Notes\");\n\n // Store the Claude notes dir on the project if it exists and differs from\n // {rootPath}/Notes/ — this lets the indexer find notes that live only\n // inside ~/.claude/projects/{encoded}/Notes/.\n if (existsSync(claudeNotesDir)) {\n const rootNotesDir = join(rootPath, \"Notes\");\n if (claudeNotesDir !== rootNotesDir) {\n db.prepare(\n \"UPDATE projects SET claude_notes_dir = ?, updated_at = ? WHERE id = ?\"\n ).run(claudeNotesDir, now(), id);\n }\n }\n\n if (!existsSync(claudeNotesDir)) continue;\n\n const noteFiles = findNoteFiles(claudeNotesDir);\n\n for (const filename of noteFiles) {\n const parsed = parseSessionFilename(filename);\n if (!parsed) continue;\n\n result.sessionsScanned++;\n const isNewSession = upsertSession(\n db,\n id,\n parsed.number,\n parsed.date,\n parsed.slug,\n parsed.title,\n parsed.filename\n );\n if (isNewSession) result.sessionsNew++;\n }\n }\n\n // Phase 2: Scan project-root Notes/ for all registered active projects\n // Many projects store their session notes at {root_path}/Notes/ rather than\n // inside ~/.claude/projects/{encoded}/Notes/ — pick those up here.\n {\n const activeProjects = db\n .prepare(\"SELECT id, slug, root_path FROM projects WHERE status = 'active'\")\n .all() as { id: number; slug: string; root_path: string }[];\n\n for (const project of activeProjects) {\n const notesDir = join(project.root_path, \"Notes\");\n if (!existsSync(notesDir)) continue;\n\n let files: string[];\n try {\n files = findNoteFiles(notesDir);\n } catch {\n continue;\n }\n\n for (const filename of files) {\n const parsed = parseSessionFilename(filename);\n if (!parsed) continue;\n\n result.sessionsScanned++;\n const isNewSession = upsertSession(\n db,\n project.id,\n parsed.number,\n parsed.date,\n parsed.slug,\n parsed.title,\n parsed.filename\n );\n if (isNewSession) result.sessionsNew++;\n }\n }\n }\n\n // Phase 3: Scan extra directories from config\n const config = loadConfig();\n if (config.scan_dirs.length) {\n for (const rawDir of config.scan_dirs) {\n const scanDir = resolveHome(rawDir);\n if (!existsSync(scanDir)) {\n result.skipped.push(`${rawDir} (configured scan_dir not found)`);\n continue;\n }\n\n const children = readdirSync(scanDir).filter((name) => {\n if (name.startsWith(\".\")) return false;\n const full = join(scanDir, name);\n try { return statSync(full).isDirectory(); } catch { return false; }\n });\n\n for (const child of children) {\n const childPath = join(scanDir, child);\n const childSlug = slugify(child);\n const childEncoded = encodeDir(childPath);\n\n // Skip if already registered by path\n const existing = db\n .prepare(\"SELECT id FROM projects WHERE root_path = ?\")\n .get(childPath) as { id: number } | undefined;\n\n if (existing) {\n result.projectsScanned++;\n result.projectsUpdated++;\n\n // Ensure PAI.md marker exists for already-registered project.\n try { ensurePaiMarker(childPath, childSlug); } catch { /* non-fatal */ }\n\n // Still scan Notes/ for new sessions\n const notesDir = join(childPath, \"Notes\");\n if (existsSync(notesDir)) {\n const noteFiles = readdirSync(notesDir).filter((f) => f.endsWith(\".md\"));\n for (const filename of noteFiles) {\n const parsed = parseSessionFilename(filename);\n if (!parsed) continue;\n result.sessionsScanned++;\n if (upsertSession(db, existing.id, parsed.number, parsed.date, parsed.slug, parsed.title, parsed.filename)) {\n result.sessionsNew++;\n }\n }\n }\n continue;\n }\n\n const { id, isNew } = upsertProject(db, childSlug, childPath, childEncoded);\n result.projectsScanned++;\n if (isNew) result.projectsNew++;\n else result.projectsUpdated++;\n\n // Ensure PAI.md marker exists for newly-registered project.\n try { ensurePaiMarker(childPath, childSlug); } catch { /* non-fatal */ }\n\n // Scan Notes/ in the project itself (not Claude's project dir)\n const notesDir = join(childPath, \"Notes\");\n if (existsSync(notesDir)) {\n const noteFiles = readdirSync(notesDir).filter((f) => f.endsWith(\".md\"));\n for (const filename of noteFiles) {\n const parsed = parseSessionFilename(filename);\n if (!parsed) continue;\n result.sessionsScanned++;\n if (upsertSession(db, id, parsed.number, parsed.date, parsed.slug, parsed.title, parsed.filename)) {\n result.sessionsNew++;\n }\n }\n }\n }\n }\n }\n\n // Phase 4: Discover PAI.md markers in scan_dirs.\n // This catches relocated projects: if a project moved but still has a\n // Notes/PAI.md with the original slug, we can find it and auto-update.\n if (config.scan_dirs.length) {\n const resolvedScanDirs = config.scan_dirs.map(resolveHome).filter(existsSync);\n const markers = discoverPaiMarkers(resolvedScanDirs);\n\n for (const marker of markers) {\n // Check if this slug is already registered at the correct path.\n const registeredRow = db\n .prepare(\"SELECT id, root_path, slug FROM projects WHERE slug = ?\")\n .get(marker.slug) as { id: number; root_path: string; slug: string } | undefined;\n\n if (!registeredRow) continue; // Unknown slug — leave it for normal scan.\n\n if (registeredRow.root_path !== marker.projectRoot) {\n // The project moved — update the stored path, but guard all UNIQUE\n // columns so we don't collide with another existing row.\n const newEncoded = encodeDir(marker.projectRoot);\n const now4 = Date.now();\n\n const encodedOwner = db\n .prepare(\"SELECT id FROM projects WHERE encoded_dir = ?\")\n .get(newEncoded) as { id: number } | undefined;\n const pathOwner = db\n .prepare(\"SELECT id FROM projects WHERE root_path = ?\")\n .get(marker.projectRoot) as { id: number } | undefined;\n\n const encodedSafe = !encodedOwner || encodedOwner.id === registeredRow.id;\n const pathSafe = !pathOwner || pathOwner.id === registeredRow.id;\n\n if (encodedSafe && pathSafe) {\n db.prepare(\n \"UPDATE projects SET root_path = ?, encoded_dir = ?, updated_at = ? WHERE id = ?\"\n ).run(marker.projectRoot, newEncoded, now4, registeredRow.id);\n } else if (pathSafe) {\n // encoded_dir is claimed by another row — skip that column.\n db.prepare(\n \"UPDATE projects SET root_path = ?, updated_at = ? WHERE id = ?\"\n ).run(marker.projectRoot, now4, registeredRow.id);\n }\n // If root_path is also claimed, skip the update entirely — the\n // marker points to a path already owned by a different project row.\n }\n }\n }\n\n return result;\n}\n\nfunction cmdScan(db: Database): void {\n const config = loadConfig();\n console.log(dim(\"Scanning ~/.claude/projects/ ...\"));\n if (config.scan_dirs.length) {\n console.log(dim(`Scanning ${config.scan_dirs.length} extra dir(s): ${config.scan_dirs.join(\", \")}`));\n }\n console.log(dim(\"Scanning project-root Notes/ directories ...\"));\n\n let result: ScanResult;\n try {\n result = performScan(db);\n } catch (e) {\n console.error(err(String(e)));\n process.exit(1);\n }\n\n console.log(\n ok(\n `Scanned ${bold(String(result.projectsScanned))} projects, ` +\n `${bold(String(result.sessionsScanned))} session notes.`\n )\n );\n console.log(\n dim(\n ` Projects: ${result.projectsNew} new, ${result.projectsUpdated} updated`\n )\n );\n console.log(dim(` Sessions: ${result.sessionsNew} new`));\n\n if (result.skipped.length) {\n console.log();\n console.log(\n warn(` ${result.skipped.length} project(s) skipped (path not found on disk):`)\n );\n for (const s of result.skipped.slice(0, 10)) {\n console.log(dim(` ${s}`));\n }\n if (result.skipped.length > 10) {\n console.log(dim(` ... and ${result.skipped.length - 10} more`));\n }\n }\n}\n\n// ---------------------------------------------------------------------------\n// Migrate command\n// ---------------------------------------------------------------------------\n\n/**\n * The actual on-disk session-registry.json format used by this PAI instance:\n * {\n * \"version\": 1,\n * \"projects\": [ { \"encoded_dir\", \"original_path\", \"notes\": [...], ... }, ... ],\n * ...other keys ignored...\n * }\n *\n * Note: the migrate.ts module in src/registry/ was written for a different\n * object-keyed format. This function handles the list-based format directly.\n */\n\ninterface LegacyProjectEntry {\n encoded_dir: string;\n original_path: string;\n notes?: string[];\n session_count?: number;\n note_count?: number;\n todo_exists?: boolean;\n last_modified?: string;\n path_exists?: boolean;\n}\n\ninterface LegacyJsonRegistry {\n version?: number;\n projects?: LegacyProjectEntry[];\n [key: string]: unknown;\n}\n\nconst SESSION_REGISTRY_PATH = join(\n homedir(),\n \".claude\",\n \"session-registry.json\"\n);\n\nfunction cmdMigrate(db: Database): void {\n if (!existsSync(SESSION_REGISTRY_PATH)) {\n console.error(\n err(`session-registry.json not found: ${SESSION_REGISTRY_PATH}`)\n );\n process.exit(1);\n }\n\n let registry: LegacyJsonRegistry;\n try {\n const raw = readFileSync(SESSION_REGISTRY_PATH, \"utf8\");\n registry = JSON.parse(raw) as LegacyJsonRegistry;\n } catch (e) {\n console.error(err(`Failed to parse session-registry.json: ${e}`));\n process.exit(1);\n }\n\n const projects = registry.projects ?? [];\n if (!projects.length) {\n console.log(warn(\"No projects found in session-registry.json.\"));\n return;\n }\n\n console.log(\n dim(\n `Migrating ${projects.length} project(s) from session-registry.json ...`\n )\n );\n\n let projectsNew = 0;\n let projectsSkipped = 0;\n let sessionsNew = 0;\n const errors: string[] = [];\n\n for (const entry of projects) {\n if (!entry.original_path || !entry.encoded_dir) {\n errors.push(`Skipping entry with missing path: ${JSON.stringify(entry)}`);\n continue;\n }\n\n const rootPath = entry.original_path;\n const encodedDir = entry.encoded_dir;\n const slug = slugify(rootPath);\n\n try {\n const { isNew, id } = upsertProject(db, slug, rootPath, encodedDir);\n if (isNew) projectsNew++;\n else projectsSkipped++;\n\n // Import session notes from the notes array in the JSON\n const notes = entry.notes ?? [];\n for (const filename of notes) {\n const parsed = parseSessionFilename(filename);\n if (!parsed) continue;\n\n const isNewSession = upsertSession(\n db,\n id,\n parsed.number,\n parsed.date,\n parsed.slug,\n parsed.title,\n parsed.filename\n );\n if (isNewSession) sessionsNew++;\n }\n } catch (e) {\n errors.push(\n `Error processing ${entry.encoded_dir}: ${String(e)}`\n );\n }\n }\n\n console.log(ok(\"Migration complete.\"));\n console.log(\n dim(\n ` Projects: ${projectsNew} new, ${projectsSkipped} already existed`\n )\n );\n console.log(dim(` Sessions: ${sessionsNew} new`));\n\n if (errors.length) {\n console.log();\n console.log(warn(` ${errors.length} error(s) during migration:`));\n for (const e of errors.slice(0, 5)) {\n console.log(dim(` ${e}`));\n }\n if (errors.length > 5) {\n console.log(dim(` ... and ${errors.length - 5} more`));\n }\n }\n}\n\n// ---------------------------------------------------------------------------\n// Stats command\n// ---------------------------------------------------------------------------\n\nfunction cmdStats(db: Database): void {\n const totalProjects = (\n db.prepare(\"SELECT COUNT(*) AS n FROM projects\").get() as { n: number }\n ).n;\n const activeProjects = (\n db\n .prepare(\"SELECT COUNT(*) AS n FROM projects WHERE status = 'active'\")\n .get() as { n: number }\n ).n;\n const archivedProjects = (\n db\n .prepare(\n \"SELECT COUNT(*) AS n FROM projects WHERE status = 'archived'\"\n )\n .get() as { n: number }\n ).n;\n const totalSessions = (\n db.prepare(\"SELECT COUNT(*) AS n FROM sessions\").get() as { n: number }\n ).n;\n const totalTags = (\n db.prepare(\"SELECT COUNT(*) AS n FROM tags\").get() as { n: number }\n ).n;\n\n const lastProject = db\n .prepare(\"SELECT updated_at FROM projects ORDER BY updated_at DESC LIMIT 1\")\n .get() as { updated_at: number } | undefined;\n\n const lastSession = db\n .prepare(\n \"SELECT created_at FROM sessions ORDER BY created_at DESC LIMIT 1\"\n )\n .get() as { created_at: number } | undefined;\n\n console.log();\n console.log(bold(\" PAI Registry Stats\"));\n console.log();\n console.log(` ${bold(\"Projects:\")} ${totalProjects}`);\n console.log(` ${bold(\" Active:\")} ${activeProjects}`);\n console.log(` ${bold(\" Archived:\")} ${archivedProjects}`);\n console.log(` ${bold(\"Sessions:\")} ${totalSessions}`);\n console.log(` ${bold(\"Tags:\")} ${totalTags}`);\n if (lastProject) {\n console.log(\n ` ${bold(\"Last updated:\")} ${fmtDate(lastProject.updated_at)}`\n );\n }\n if (lastSession) {\n console.log(\n ` ${bold(\"Last session:\")} ${fmtDate(lastSession.created_at)}`\n );\n }\n console.log();\n}\n\n// ---------------------------------------------------------------------------\n// Rebuild command\n// ---------------------------------------------------------------------------\n\nfunction cmdRebuild(db: Database): void {\n console.log(warn(\"Rebuilding registry — all existing data will be erased.\"));\n console.log(dim(\"Clearing all tables ...\"));\n\n // Delete in dependency order (FK constraints are ON)\n db.exec(`\n DELETE FROM compaction_log;\n DELETE FROM session_tags;\n DELETE FROM project_tags;\n DELETE FROM aliases;\n DELETE FROM sessions;\n DELETE FROM projects;\n DELETE FROM tags;\n DELETE FROM schema_version;\n `);\n\n console.log(dim(\"Registry cleared. Re-scanning ...\"));\n cmdScan(db);\n}\n\n// ---------------------------------------------------------------------------\n// Lookup command\n// ---------------------------------------------------------------------------\n\n/**\n * Print the project slug whose root_path matches the given filesystem path.\n * Exits 0 on success, 1 if not found. Output is plain (for use in scripts).\n */\nfunction cmdLookup(db: Database, fsPath: string): void {\n // Resolve to an absolute path so relative inputs still match\n const resolved = resolve(fsPath);\n\n const row = db\n .prepare(\"SELECT slug FROM projects WHERE root_path = ?\")\n .get(resolved) as { slug: string } | undefined;\n\n if (!row) {\n process.exit(1);\n }\n\n process.stdout.write(row.slug + \"\\n\");\n}\n\n// ---------------------------------------------------------------------------\n// Commander registration\n// ---------------------------------------------------------------------------\n\nexport function registerRegistryCommands(\n registryCmd: Command,\n getDb: () => Database\n): void {\n // pai registry scan\n registryCmd\n .command(\"scan\")\n .description(\n \"Walk ~/.claude/projects/ and configured scan_dirs, upsert all projects\"\n )\n .option(\"--add-dir <path>\", \"Add a directory to scan_dirs config (immediate children become projects)\")\n .option(\"--remove-dir <path>\", \"Remove a directory from scan_dirs config\")\n .option(\"--show-dirs\", \"Show currently configured scan directories\")\n .action((opts: { addDir?: string; removeDir?: string; showDirs?: boolean }) => {\n if (opts.showDirs) {\n const config = loadConfig();\n if (!config.scan_dirs.length) {\n console.log(dim(\" No extra scan directories configured.\"));\n console.log(dim(\" Use --add-dir <path> to add one.\"));\n } else {\n console.log(bold(\" Configured scan directories:\"));\n for (const d of config.scan_dirs) {\n console.log(` ${d}`);\n }\n }\n return;\n }\n if (opts.addDir) {\n const config = loadConfig();\n const resolved = resolveHome(opts.addDir);\n if (!existsSync(resolved)) {\n console.error(err(`Directory not found: ${resolved}`));\n process.exit(1);\n }\n // Store with ~ prefix for portability\n const display = resolved.startsWith(homedir())\n ? \"~\" + resolved.slice(homedir().length)\n : resolved;\n if (config.scan_dirs.includes(display) || config.scan_dirs.includes(resolved)) {\n console.log(warn(`Already configured: ${display}`));\n } else {\n config.scan_dirs.push(display);\n saveConfig(config);\n console.log(ok(`Added scan directory: ${bold(display)}`));\n }\n }\n if (opts.removeDir) {\n const config = loadConfig();\n const resolved = resolveHome(opts.removeDir);\n const display = resolved.startsWith(homedir())\n ? \"~\" + resolved.slice(homedir().length)\n : resolved;\n const before = config.scan_dirs.length;\n config.scan_dirs = config.scan_dirs.filter(\n (d) => resolveHome(d) !== resolved\n );\n if (config.scan_dirs.length < before) {\n saveConfig(config);\n console.log(ok(`Removed scan directory: ${bold(display)}`));\n } else {\n console.log(warn(`Not found in config: ${display}`));\n }\n }\n if (!opts.addDir && !opts.removeDir) {\n cmdScan(getDb());\n }\n });\n\n // pai registry migrate\n registryCmd\n .command(\"migrate\")\n .description(\"Import data from ~/.claude/session-registry.json\")\n .action(() => {\n cmdMigrate(getDb());\n });\n\n // pai registry stats\n registryCmd\n .command(\"stats\")\n .description(\"Show summary statistics for the registry\")\n .action(() => {\n cmdStats(getDb());\n });\n\n // pai registry rebuild\n registryCmd\n .command(\"rebuild\")\n .description(\n \"Erase all registry data and rebuild from the filesystem (destructive)\"\n )\n .action(() => {\n cmdRebuild(getDb());\n });\n\n // pai registry lookup --path <path>\n registryCmd\n .command(\"lookup\")\n .description(\n \"Find the project slug for a filesystem path (for use in scripts)\"\n )\n .requiredOption(\"--path <path>\", \"Filesystem path to look up\")\n .action((opts: { path: string }) => {\n cmdLookup(getDb(), opts.path);\n });\n}\n","/**\n * CLI commands for the PAI memory engine (Phase 2 / Phase 2.5).\n *\n * Commands:\n * pai memory index [project-slug] — index one or all projects\n * pai memory embed [project-slug] — generate embeddings for un-embedded chunks\n * pai memory search <query> — BM25/semantic/hybrid search across federation.db\n * pai memory status [project-slug] — show index stats\n */\n\nimport type { Command } from \"commander\";\nimport type { Database } from \"better-sqlite3\";\nimport chalk from \"chalk\";\nimport { openFederation } from \"../../memory/db.js\";\nimport { indexProject, indexAll, embedChunks } from \"../../memory/indexer.js\";\nimport { searchMemory, populateSlugs, type SearchResult } from \"../../memory/search.js\";\nimport { renderTable, dim, bold, ok, warn, err, fmtDate } from \"../utils.js\";\nimport { PaiClient } from \"../../daemon/ipc-client.js\";\nimport { loadConfig } from \"../../daemon/config.js\";\nimport { createStorageBackend } from \"../../storage/factory.js\";\n\n// ---------------------------------------------------------------------------\n// Tier colour helper\n// ---------------------------------------------------------------------------\n\nfunction tierColor(tier: string): string {\n switch (tier) {\n case \"evergreen\": return chalk.green(tier);\n case \"daily\": return chalk.yellow(tier);\n case \"topic\": return chalk.blue(tier);\n case \"session\": return chalk.dim(tier);\n default: return tier;\n }\n}\n\n// ---------------------------------------------------------------------------\n// Registration\n// ---------------------------------------------------------------------------\n\n// ---------------------------------------------------------------------------\n// Shared embed runner\n// ---------------------------------------------------------------------------\n\nasync function runEmbed(\n federation: Database,\n projectId?: number,\n projectSlug?: string,\n batchSize = 50,\n): Promise<void> {\n const label = projectSlug ? `project ${projectSlug}` : \"all projects\";\n console.log(dim(`Generating embeddings for ${label} (this may take a while on first run)...`));\n\n const { chunksEmbedded } = await embedChunks(\n federation,\n projectId,\n batchSize,\n (done, total) => {\n process.stdout.write(`\\r ${done} / ${total} chunks embedded...`);\n },\n );\n\n process.stdout.write(\"\\r\");\n console.log(ok(`Done.`) + ` ${bold(String(chunksEmbedded))} chunks embedded`);\n}\n\n// ---------------------------------------------------------------------------\n// Registration\n// ---------------------------------------------------------------------------\n\nexport function registerMemoryCommands(\n memoryCmd: Command,\n getDb: () => Database,\n): void {\n\n // -------------------------------------------------------------------------\n // pai memory index [project-slug]\n // -------------------------------------------------------------------------\n\n memoryCmd\n .command(\"index [project-slug]\")\n .description(\"Index memory files for one project or all projects\")\n .option(\"--all\", \"Index all active projects (default when no slug given)\")\n .option(\"--embed\", \"Also generate embeddings for newly indexed chunks (Phase 2.5)\")\n .option(\"--direct\", \"Skip daemon IPC and run index directly (for debugging)\")\n .action(async (projectSlug: string | undefined, opts: { all?: boolean; embed?: boolean; direct?: boolean }) => {\n const registryDb = getDb();\n\n // If daemon is running and no --direct flag, trigger via IPC (non-blocking)\n if (!opts.direct && !projectSlug) {\n try {\n const config = loadConfig();\n const client = new PaiClient(config.socketPath);\n await client.triggerIndex();\n console.log(ok(\"Index triggered in daemon.\") + dim(\" Check daemon logs for progress.\"));\n console.log(dim(\" Run `pai daemon logs` to watch progress.\"));\n return;\n } catch {\n // Daemon not running — fall through to direct index\n console.log(dim(\"Daemon not running. Running direct index...\"));\n }\n }\n\n let federation: Database;\n try {\n federation = openFederation();\n } catch (e) {\n console.error(err(`Failed to open federation database: ${e}`));\n process.exit(1);\n }\n\n if (projectSlug) {\n // Index a single project\n const project = registryDb\n .prepare(\"SELECT id, slug, display_name, root_path FROM projects WHERE slug = ? AND status = 'active'\")\n .get(projectSlug) as\n | { id: number; slug: string; display_name: string; root_path: string }\n | undefined;\n\n if (!project) {\n console.error(err(`Project not found or not active: ${projectSlug}`));\n process.exit(1);\n }\n\n console.log(dim(`Indexing ${project.display_name} (${project.slug})...`));\n const result = await indexProject(federation, project.id, project.root_path);\n\n console.log(\n ok(`Done.`) +\n ` ${bold(String(result.filesProcessed))} files indexed` +\n `, ${bold(String(result.chunksCreated))} chunks created` +\n `, ${dim(String(result.filesSkipped) + \" skipped (unchanged)\")}`,\n );\n\n if (opts.embed) {\n await runEmbed(federation, project.id, project.slug);\n }\n\n } else if (opts.all || !projectSlug) {\n // Index all active projects\n console.log(dim(\"Indexing all active projects...\"));\n\n const { projects, result } = await indexAll(federation, registryDb);\n\n console.log(\n ok(`Done.`) +\n ` ${bold(String(projects))} projects` +\n `, ${bold(String(result.filesProcessed))} files indexed` +\n `, ${bold(String(result.chunksCreated))} chunks created` +\n `, ${dim(String(result.filesSkipped) + \" skipped (unchanged)\")}`,\n );\n\n if (opts.embed) {\n await runEmbed(federation);\n }\n }\n });\n\n // -------------------------------------------------------------------------\n // pai memory embed [project-slug]\n // -------------------------------------------------------------------------\n\n memoryCmd\n .command(\"embed [project-slug]\")\n .description(\"Generate embeddings for un-embedded chunks (Phase 2.5)\")\n .option(\"--batch-size <n>\", \"Chunks to embed per batch\", \"50\")\n .action(async (projectSlug: string | undefined, opts: { batchSize?: string }) => {\n const registryDb = getDb();\n\n let federation: Database;\n try {\n federation = openFederation();\n } catch (e) {\n console.error(err(`Failed to open federation database: ${e}`));\n process.exit(1);\n }\n\n if (projectSlug) {\n const project = registryDb\n .prepare(\"SELECT id, slug FROM projects WHERE slug = ?\")\n .get(projectSlug) as { id: number; slug: string } | undefined;\n\n if (!project) {\n console.error(err(`Project not found: ${projectSlug}`));\n process.exit(1);\n }\n\n await runEmbed(federation, project.id, project.slug, parseInt(opts.batchSize ?? \"50\", 10));\n } else {\n await runEmbed(federation, undefined, undefined, parseInt(opts.batchSize ?? \"50\", 10));\n }\n });\n\n // -------------------------------------------------------------------------\n // pai memory search <query>\n // -------------------------------------------------------------------------\n\n memoryCmd\n .command(\"search <query>\")\n .description(\"Search indexed memory (BM25 keyword, semantic, or hybrid)\")\n .option(\"--project <slug>\", \"Restrict search to a specific project\")\n .option(\"--source <source>\", \"Restrict to 'memory' or 'notes'\")\n .option(\"--limit <n>\", \"Maximum results to return\", \"10\")\n .option(\n \"--mode <mode>\",\n \"Search mode: keyword (default), semantic, hybrid\",\n \"keyword\",\n )\n .action(\n async (\n query: string,\n opts: { project?: string; source?: string; limit?: string; mode?: string },\n ) => {\n const registryDb = getDb();\n\n let federation: Database;\n try {\n federation = openFederation();\n } catch (e) {\n console.error(err(`Failed to open federation database: ${e}`));\n process.exit(1);\n }\n\n const maxResults = parseInt(opts.limit ?? \"10\", 10);\n const mode = (opts.mode ?? \"keyword\") as \"keyword\" | \"semantic\" | \"hybrid\";\n\n // Validate mode\n if (![\"keyword\", \"semantic\", \"hybrid\"].includes(mode)) {\n console.error(err(`Invalid mode: ${mode}. Use keyword, semantic, or hybrid.`));\n process.exit(1);\n }\n\n // Resolve project slug → ID filter\n let projectIds: number[] | undefined;\n if (opts.project) {\n const project = registryDb\n .prepare(\"SELECT id FROM projects WHERE slug = ?\")\n .get(opts.project) as { id: number } | undefined;\n\n if (!project) {\n console.error(warn(`Project not found: ${opts.project} — searching all projects`));\n } else {\n projectIds = [project.id];\n }\n }\n\n const sources = opts.source ? [opts.source] : undefined;\n const searchOpts = { projectIds, sources, maxResults };\n\n let results: SearchResult[];\n\n if (mode === \"keyword\") {\n results = searchMemory(federation, query, searchOpts);\n\n } else if (mode === \"semantic\" || mode === \"hybrid\") {\n // Use StorageBackend for semantic/hybrid so queries hit Postgres pgvector\n const config = loadConfig();\n const backend = await createStorageBackend(config);\n\n try {\n const { generateEmbedding } = await import(\"../../memory/embeddings.js\");\n\n console.log(dim(\"Generating query embedding...\"));\n const queryEmbedding = await generateEmbedding(query, true);\n\n if (mode === \"semantic\") {\n results = await backend.searchSemantic(queryEmbedding, searchOpts);\n } else {\n // Hybrid: combine keyword (BM25) and semantic results with min-max normalization\n const [keywordResults, semanticResults] = await Promise.all([\n backend.searchKeyword(query, { ...searchOpts, maxResults: 500 }),\n backend.searchSemantic(queryEmbedding, { ...searchOpts, maxResults: 500 }),\n ]);\n\n if (keywordResults.length === 0 && semanticResults.length === 0) {\n results = [];\n } else {\n const keyFor = (r: SearchResult) =>\n `${r.projectId}:${r.path}:${r.startLine}:${r.endLine}`;\n\n function minMaxNormalize(items: SearchResult[]): Map<string, number> {\n if (items.length === 0) return new Map();\n const min = Math.min(...items.map((r) => r.score));\n const max = Math.max(...items.map((r) => r.score));\n const range = max - min;\n const m = new Map<string, number>();\n for (const r of items) {\n m.set(keyFor(r), range === 0 ? 1 : (r.score - min) / range);\n }\n return m;\n }\n\n const kwNorm = minMaxNormalize(keywordResults);\n const semNorm = minMaxNormalize(semanticResults);\n const allKeys = new Set<string>([\n ...keywordResults.map(keyFor),\n ...semanticResults.map(keyFor),\n ]);\n const metaMap = new Map<string, SearchResult>();\n for (const r of [...keywordResults, ...semanticResults]) {\n metaMap.set(keyFor(r), r);\n }\n\n const combined: SearchResult[] = [];\n for (const key of allKeys) {\n const meta = metaMap.get(key)!;\n const combinedScore = 0.5 * (kwNorm.get(key) ?? 0) + 0.5 * (semNorm.get(key) ?? 0);\n combined.push({ ...meta, score: combinedScore });\n }\n\n results = combined\n .sort((a, b) => b.score - a.score)\n .slice(0, maxResults);\n }\n }\n } finally {\n await backend.close();\n }\n } else {\n results = [];\n }\n\n if (!results || results.length === 0) {\n console.log(dim(`No results found for: \"${query}\" (mode: ${mode})`));\n return;\n }\n\n // Populate project slugs for display\n const withSlugs = populateSlugs(results, registryDb);\n const modeLabel = mode !== \"keyword\" ? ` [${mode}]` : \"\";\n\n console.log(\n `\\n ${bold(`Search results for: \"${query}\"`)}${modeLabel} ${dim(`(${withSlugs.length} found)`)}\\n`,\n );\n\n for (const result of withSlugs) {\n const projectLabel = result.projectSlug\n ? chalk.cyan(result.projectSlug)\n : chalk.cyan(String(result.projectId));\n\n const tierLabel = tierColor(result.tier);\n const scoreLabel = dim(`score: ${result.score.toFixed(4)}`);\n const locationLabel = dim(`${result.path}:${result.startLine}-${result.endLine}`);\n\n console.log(\n ` ${projectLabel} ${tierLabel} ${locationLabel} ${scoreLabel}`,\n );\n\n // Display snippet (first 3 lines, trimmed)\n const snippetLines = result.snippet\n .split(\"\\n\")\n .slice(0, 3)\n .map((l) => ` ${l}`);\n console.log(snippetLines.join(\"\\n\"));\n console.log();\n }\n },\n );\n\n // -------------------------------------------------------------------------\n // pai memory status [project-slug]\n // -------------------------------------------------------------------------\n\n memoryCmd\n .command(\"status [project-slug]\")\n .description(\"Show memory index statistics\")\n .action((projectSlug: string | undefined) => {\n const registryDb = getDb();\n\n let federation: Database;\n try {\n federation = openFederation();\n } catch (e) {\n console.error(err(`Failed to open federation database: ${e}`));\n process.exit(1);\n }\n\n if (projectSlug) {\n // Single project stats\n const project = registryDb\n .prepare(\"SELECT id, slug, display_name FROM projects WHERE slug = ?\")\n .get(projectSlug) as\n | { id: number; slug: string; display_name: string }\n | undefined;\n\n if (!project) {\n console.error(err(`Project not found: ${projectSlug}`));\n process.exit(1);\n }\n\n const fileStats = federation\n .prepare(\n \"SELECT COUNT(*) as files FROM memory_files WHERE project_id = ?\",\n )\n .get(project.id) as { files: number };\n\n const chunkStats = federation\n .prepare(\n \"SELECT COUNT(*) as chunks FROM memory_chunks WHERE project_id = ?\",\n )\n .get(project.id) as { chunks: number };\n\n const lastUpdate = federation\n .prepare(\n \"SELECT MAX(updated_at) as last_at FROM memory_chunks WHERE project_id = ?\",\n )\n .get(project.id) as { last_at: number | null };\n\n const tierBreakdown = federation\n .prepare(\n \"SELECT tier, COUNT(*) as n FROM memory_chunks WHERE project_id = ? GROUP BY tier ORDER BY n DESC\",\n )\n .all(project.id) as Array<{ tier: string; n: number }>;\n\n console.log(`\\n ${bold(project.display_name)} ${dim(`(${project.slug})`)}\\n`);\n console.log(` ${bold(\"Files indexed:\")} ${fileStats.files}`);\n console.log(` ${bold(\"Chunks:\")} ${chunkStats.chunks}`);\n console.log(\n ` ${bold(\"Last indexed:\")} ${lastUpdate.last_at ? fmtDate(lastUpdate.last_at) : dim(\"never\")}`,\n );\n\n if (tierBreakdown.length > 0) {\n console.log(`\\n ${bold(\"By tier:\")}`);\n for (const row of tierBreakdown) {\n console.log(` ${tierColor(row.tier).padEnd(20)} ${row.n} chunks`);\n }\n }\n console.log();\n\n } else {\n // Global stats\n const globalFiles = federation\n .prepare(\"SELECT COUNT(*) as n FROM memory_files\")\n .get() as { n: number };\n\n const globalChunks = federation\n .prepare(\"SELECT COUNT(*) as n FROM memory_chunks\")\n .get() as { n: number };\n\n const lastUpdate = federation\n .prepare(\"SELECT MAX(updated_at) as last_at FROM memory_chunks\")\n .get() as { last_at: number | null };\n\n const projectStats = federation\n .prepare(\n `SELECT\n mf.project_id,\n COUNT(DISTINCT mf.path) as files,\n COUNT(mc.id) as chunks,\n MAX(mc.updated_at) as last_at\n FROM memory_files mf\n LEFT JOIN memory_chunks mc ON mc.project_id = mf.project_id AND mc.path = mf.path\n GROUP BY mf.project_id\n ORDER BY chunks DESC`,\n )\n .all() as Array<{\n project_id: number;\n files: number;\n chunks: number;\n last_at: number | null;\n }>;\n\n // Enrich with slugs\n const projectIds = projectStats.map((p) => p.project_id);\n const slugMap = new Map<number, string>();\n if (projectIds.length > 0) {\n const placeholders = projectIds.map(() => \"?\").join(\", \");\n const slugRows = registryDb\n .prepare(\n `SELECT id, slug, display_name FROM projects WHERE id IN (${placeholders})`,\n )\n .all(...projectIds) as Array<{\n id: number;\n slug: string;\n display_name: string;\n }>;\n for (const row of slugRows) {\n slugMap.set(row.id, row.slug);\n }\n }\n\n console.log(`\\n ${bold(\"PAI Memory Index — Global Status\")}\\n`);\n console.log(\n ` ${bold(\"Total files:\")} ${globalFiles.n} ${bold(\"Total chunks:\")} ${globalChunks.n}`,\n );\n console.log(\n ` ${bold(\"Last indexed:\")} ${lastUpdate.last_at ? fmtDate(lastUpdate.last_at) : dim(\"never\")}`,\n );\n\n if (projectStats.length > 0) {\n console.log();\n const rows = projectStats.map((p) => [\n chalk.cyan(slugMap.get(p.project_id) ?? String(p.project_id)),\n String(p.files),\n String(p.chunks),\n p.last_at ? fmtDate(p.last_at) : dim(\"never\"),\n ]);\n console.log(\n renderTable([\"Project\", \"Files\", \"Chunks\", \"Last Indexed\"], rows),\n );\n } else {\n console.log(dim(\"\\n No projects indexed yet. Run `pai memory index --all` to start.\"));\n }\n console.log();\n }\n });\n}\n","/**\n * pai mcp <sub-command>\n *\n * install — Register the PAI MCP server in ~/.claude.json\n * status — Show whether the PAI MCP server is registered and the binary exists\n */\n\nimport type { Command } from \"commander\";\nimport { existsSync, readFileSync, writeFileSync } from \"node:fs\";\nimport { join, dirname } from \"node:path\";\nimport { homedir } from \"node:os\";\nimport { fileURLToPath } from \"node:url\";\nimport { ok, warn, err, dim, bold } from \"../utils.js\";\n\n// ---------------------------------------------------------------------------\n// Paths\n// ---------------------------------------------------------------------------\n\nconst CLAUDE_JSON_PATH = join(homedir(), \".claude.json\");\n\n/**\n * Resolve the absolute path to the built MCP entry point.\n *\n * tsdown bundles all CLI commands into a single dist/cli/index.mjs file, so\n * import.meta.url always resolves to dist/cli/index.mjs at runtime.\n * From dist/cli/ we go up one level to dist/ and then into mcp/index.mjs.\n */\nfunction getMcpBinPath(): string {\n const __filename = fileURLToPath(import.meta.url);\n const __dirname = dirname(__filename);\n // dist/cli/index.mjs → dist/ → dist/mcp/index.mjs\n return join(__dirname, \"../mcp/index.mjs\");\n}\n\n// ---------------------------------------------------------------------------\n// Read / write ~/.claude.json safely\n// ---------------------------------------------------------------------------\n\nfunction readClaudeJson(): Record<string, unknown> {\n if (!existsSync(CLAUDE_JSON_PATH)) return {};\n try {\n const raw = readFileSync(CLAUDE_JSON_PATH, \"utf8\");\n return JSON.parse(raw) as Record<string, unknown>;\n } catch {\n return {};\n }\n}\n\nfunction writeClaudeJson(data: Record<string, unknown>): void {\n writeFileSync(CLAUDE_JSON_PATH, JSON.stringify(data, null, 2) + \"\\n\", \"utf8\");\n}\n\n// ---------------------------------------------------------------------------\n// install\n// ---------------------------------------------------------------------------\n\nfunction cmdInstall(): void {\n const mcpBin = getMcpBinPath();\n\n const config = readClaudeJson();\n\n // Ensure mcpServers key exists\n if (typeof config.mcpServers !== \"object\" || config.mcpServers === null) {\n config.mcpServers = {};\n }\n\n const servers = config.mcpServers as Record<string, unknown>;\n\n if (\"pai\" in servers) {\n console.log(warn(\"PAI MCP server is already registered in ~/.claude.json.\"));\n console.log(dim(` Entry: ${JSON.stringify(servers[\"pai\"])}`));\n console.log(dim(\" Use `pai mcp status` to verify the configuration.\"));\n return;\n }\n\n servers[\"pai\"] = {\n command: \"node\",\n args: [mcpBin],\n };\n\n try {\n writeClaudeJson(config);\n } catch (e) {\n console.error(err(`Failed to write ~/.claude.json: ${e}`));\n process.exit(1);\n }\n\n console.log(ok(\"PAI MCP server registered in ~/.claude.json.\"));\n console.log(dim(` Binary: ${mcpBin}`));\n console.log(dim(\"\"));\n console.log(dim(\" Restart Claude Code to activate the PAI MCP tools:\"));\n console.log(dim(\" memory_search, memory_get, project_info,\"));\n console.log(dim(\" project_list, session_list, registry_search\"));\n\n if (!existsSync(mcpBin)) {\n console.log();\n console.log(\n warn(` Note: MCP binary not found at ${mcpBin}`)\n );\n console.log(dim(\" Run `bun run build` to compile it first.\"));\n }\n}\n\n// ---------------------------------------------------------------------------\n// status\n// ---------------------------------------------------------------------------\n\nfunction cmdStatus(): void {\n const mcpBin = getMcpBinPath();\n const config = readClaudeJson();\n\n const servers =\n typeof config.mcpServers === \"object\" && config.mcpServers !== null\n ? (config.mcpServers as Record<string, unknown>)\n : {};\n\n const registered = \"pai\" in servers;\n const binExists = existsSync(mcpBin);\n\n console.log();\n console.log(bold(\" PAI MCP Server Status\"));\n console.log();\n\n if (registered) {\n const entry = servers[\"pai\"];\n console.log(ok(` Registered in ~/.claude.json`));\n console.log(dim(` Config: ${JSON.stringify(entry)}`));\n } else {\n console.log(warn(` NOT registered in ~/.claude.json`));\n console.log(dim(` Run: pai mcp install`));\n }\n\n console.log();\n\n if (binExists) {\n console.log(ok(` MCP binary found: ${mcpBin}`));\n } else {\n console.log(warn(` MCP binary NOT found: ${mcpBin}`));\n console.log(dim(\" Run: bun run build\"));\n }\n\n console.log();\n\n if (registered && binExists) {\n console.log(dim(\" Status: READY — restart Claude Code to use PAI tools\"));\n } else if (registered && !binExists) {\n console.log(warn(\" Status: NEEDS BUILD — run `bun run build`\"));\n } else {\n console.log(dim(\" Status: NOT INSTALLED — run `pai mcp install`\"));\n }\n\n console.log();\n}\n\n// ---------------------------------------------------------------------------\n// Commander registration\n// ---------------------------------------------------------------------------\n\nexport function registerMcpCommands(mcpCmd: Command): void {\n mcpCmd\n .command(\"install\")\n .description(\n \"Register the PAI MCP server in ~/.claude.json (restart Claude Code to activate)\"\n )\n .action(() => {\n cmdInstall();\n });\n\n mcpCmd\n .command(\"status\")\n .description(\n \"Show whether the PAI MCP server is registered and the binary exists\"\n )\n .action(() => {\n cmdStatus();\n });\n}\n","/**\n * pai daemon <sub-command>\n *\n * serve — Start the PAI daemon in the foreground\n * status — Query daemon status via IPC\n * restart — Send SIGTERM to running daemon (launchd will restart it)\n * install — Write launchd plist + update ~/.claude.json to use the shim\n * uninstall — Remove launchd plist + revert ~/.claude.json to direct MCP\n * logs — Tail the daemon log file\n */\n\nimport type { Command } from \"commander\";\nimport {\n existsSync,\n readFileSync,\n writeFileSync,\n unlinkSync,\n mkdirSync,\n} from \"node:fs\";\nimport { join, dirname } from \"node:path\";\nimport { homedir } from \"node:os\";\nimport { fileURLToPath } from \"node:url\";\nimport { execSync, spawnSync } from \"node:child_process\";\nimport { ok, warn, err, dim, bold } from \"../utils.js\";\nimport { loadConfig } from \"../../daemon/config.js\";\nimport { PaiClient } from \"../../daemon/ipc-client.js\";\n\n// ---------------------------------------------------------------------------\n// Paths\n// ---------------------------------------------------------------------------\n\nconst HOME = homedir();\nconst CLAUDE_JSON_PATH = join(HOME, \".claude.json\");\nconst PLIST_LABEL = \"com.pai.pai-daemon\";\nconst LAUNCH_AGENTS_DIR = join(HOME, \"Library\", \"LaunchAgents\");\nconst PLIST_PATH = join(LAUNCH_AGENTS_DIR, `${PLIST_LABEL}.plist`);\nconst DAEMON_LOG = \"/tmp/pai-daemon.log\";\n\n/**\n * Resolve the absolute path to the built daemon entry point.\n * tsdown bundles into dist/daemon/index.mjs (or similar).\n * From dist/cli/index.mjs → dist/ → dist/daemon/index.mjs\n */\nfunction getDaemonBinPath(): string {\n const __filename = fileURLToPath(import.meta.url);\n const __dirname = dirname(__filename);\n return join(__dirname, \"../daemon/index.mjs\");\n}\n\n/**\n * Resolve the absolute path to the built MCP shim entry point.\n * dist/cli/index.mjs → dist/ → dist/daemon-mcp/index.mjs\n */\nfunction getShimBinPath(): string {\n const __filename = fileURLToPath(import.meta.url);\n const __dirname = dirname(__filename);\n return join(__dirname, \"../daemon-mcp/index.mjs\");\n}\n\n// ---------------------------------------------------------------------------\n// claude.json helpers\n// ---------------------------------------------------------------------------\n\nfunction readClaudeJson(): Record<string, unknown> {\n if (!existsSync(CLAUDE_JSON_PATH)) return {};\n try {\n const raw = readFileSync(CLAUDE_JSON_PATH, \"utf8\");\n return JSON.parse(raw) as Record<string, unknown>;\n } catch {\n return {};\n }\n}\n\nfunction writeClaudeJson(data: Record<string, unknown>): void {\n writeFileSync(CLAUDE_JSON_PATH, JSON.stringify(data, null, 2) + \"\\n\", \"utf8\");\n}\n\n// ---------------------------------------------------------------------------\n// launchd plist generation\n// ---------------------------------------------------------------------------\n\nfunction generatePlist(daemonBin: string): string {\n return `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n <key>Label</key>\n <string>${PLIST_LABEL}</string>\n\n <key>ProgramArguments</key>\n <array>\n <string>/usr/local/bin/node</string>\n <string>${daemonBin}</string>\n <string>serve</string>\n </array>\n\n <key>KeepAlive</key>\n <true/>\n\n <key>ThrottleInterval</key>\n <integer>3</integer>\n\n <key>StandardOutPath</key>\n <string>${DAEMON_LOG}</string>\n\n <key>StandardErrorPath</key>\n <string>${DAEMON_LOG}</string>\n\n <key>RunAtLoad</key>\n <true/>\n\n <key>EnvironmentVariables</key>\n <dict>\n <key>PATH</key>\n <string>/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin</string>\n </dict>\n</dict>\n</plist>\n`;\n}\n\n// ---------------------------------------------------------------------------\n// Command implementations\n// ---------------------------------------------------------------------------\n\nasync function cmdStatus(): Promise<void> {\n const config = loadConfig();\n const client = new PaiClient(config.socketPath);\n\n try {\n const status = await client.status();\n const s = status as Record<string, unknown>;\n\n console.log();\n console.log(bold(\" PAI Daemon Status\"));\n console.log();\n console.log(ok(` Daemon running`));\n console.log(dim(` Uptime: ${s[\"uptime\"]}s`));\n console.log(dim(` Socket: ${s[\"socketPath\"]}`));\n console.log(\n dim(\n ` Index: ${s[\"indexInProgress\"] ? \"in progress\" : \"idle\"} (interval: ${s[\"indexIntervalSecs\"]}s)`\n )\n );\n if (s[\"lastIndexTime\"]) {\n console.log(dim(` Last index: ${s[\"lastIndexTime\"]}`));\n }\n if (s[\"db\"]) {\n const db = s[\"db\"] as Record<string, unknown>;\n console.log(\n dim(\n ` DB: ${db[\"projects\"]} projects, ${db[\"files\"]} files, ${db[\"chunks\"]} chunks`\n )\n );\n }\n console.log();\n } catch (e) {\n const msg = e instanceof Error ? e.message : String(e);\n console.log();\n console.log(warn(\" PAI Daemon Status\"));\n console.log();\n console.log(warn(` Daemon not running: ${msg}`));\n console.log(dim(\" Start with: pai daemon serve\"));\n console.log(dim(\" Or install as service: pai daemon install\"));\n console.log();\n process.exit(1);\n }\n}\n\nfunction cmdRestart(): void {\n // Find and signal the running daemon\n try {\n const result = spawnSync(\"pgrep\", [\"-f\", \"pai-daemon.*serve\"], {\n encoding: \"utf8\",\n });\n\n if (result.status !== 0 || !result.stdout.trim()) {\n console.log(warn(\"No running pai-daemon process found.\"));\n\n // If launchd is managing it, kick it via launchctl\n const unloadResult = spawnSync(\n \"launchctl\",\n [\"kickstart\", \"-k\", `gui/${process.getuid?.() ?? 501}/${PLIST_LABEL}`],\n { encoding: \"utf8\" }\n );\n if (unloadResult.status === 0) {\n console.log(ok(\"Sent kickstart to launchd.\"));\n } else {\n console.log(dim(\"Not managed by launchd either. Run: pai daemon serve\"));\n }\n return;\n }\n\n const pids = result.stdout\n .trim()\n .split(\"\\n\")\n .map((p) => p.trim());\n for (const pid of pids) {\n try {\n process.kill(parseInt(pid, 10), \"SIGTERM\");\n console.log(ok(`Sent SIGTERM to pid ${pid}.`));\n } catch {\n console.log(warn(`Could not signal pid ${pid}.`));\n }\n }\n console.log(dim(\"launchd will restart the daemon automatically.\"));\n } catch (e) {\n const msg = e instanceof Error ? e.message : String(e);\n console.error(err(`restart error: ${msg}`));\n process.exit(1);\n }\n}\n\nfunction cmdInstall(): void {\n const daemonBin = getDaemonBinPath();\n const shimBin = getShimBinPath();\n\n console.log();\n console.log(bold(\" PAI Daemon Install\"));\n console.log();\n\n // 1. Write launchd plist\n if (!existsSync(LAUNCH_AGENTS_DIR)) {\n mkdirSync(LAUNCH_AGENTS_DIR, { recursive: true });\n }\n\n const plistContent = generatePlist(daemonBin);\n try {\n writeFileSync(PLIST_PATH, plistContent, \"utf8\");\n console.log(ok(` Wrote launchd plist: ${PLIST_PATH}`));\n } catch (e) {\n console.error(err(` Failed to write plist: ${e}`));\n process.exit(1);\n }\n\n // 2. Load the plist (unload first in case it was already there)\n try {\n spawnSync(\"launchctl\", [\"unload\", PLIST_PATH], { encoding: \"utf8\" });\n const loadResult = spawnSync(\"launchctl\", [\"load\", PLIST_PATH], {\n encoding: \"utf8\",\n });\n if (loadResult.status === 0) {\n console.log(ok(\" Loaded plist with launchctl.\"));\n } else {\n console.log(warn(` launchctl load: ${loadResult.stderr?.trim() ?? \"unknown error\"}`));\n }\n } catch {\n console.log(warn(\" Could not run launchctl. Load manually:\"));\n console.log(dim(` launchctl load ${PLIST_PATH}`));\n }\n\n // 3. Update ~/.claude.json to use the shim\n const config = readClaudeJson();\n\n if (typeof config.mcpServers !== \"object\" || config.mcpServers === null) {\n config.mcpServers = {};\n }\n\n const servers = config.mcpServers as Record<string, unknown>;\n\n // Check if already pointing to the shim\n const existing = servers[\"pai\"] as Record<string, unknown> | undefined;\n const existingArgs = existing?.args as string[] | undefined;\n const alreadyShim = existingArgs?.includes(shimBin);\n\n if (alreadyShim) {\n console.log(ok(\" ~/.claude.json already points to the shim.\"));\n } else {\n // Back up the existing entry if any\n if (existing) {\n servers[\"pai-legacy\"] = existing;\n console.log(dim(\" Backed up existing 'pai' entry as 'pai-legacy'.\"));\n }\n\n servers[\"pai\"] = {\n command: \"node\",\n args: [shimBin],\n };\n\n try {\n writeClaudeJson(config);\n console.log(ok(\" Updated ~/.claude.json to use daemon shim.\"));\n } catch (e) {\n console.error(err(` Failed to write ~/.claude.json: ${e}`));\n process.exit(1);\n }\n }\n\n // 4. Verify binaries exist\n console.log();\n if (!existsSync(daemonBin)) {\n console.log(warn(` Daemon binary not found: ${daemonBin}`));\n console.log(dim(\" Run: bun run build\"));\n } else {\n console.log(ok(` Daemon binary: ${daemonBin}`));\n }\n\n if (!existsSync(shimBin)) {\n console.log(warn(` Shim binary not found: ${shimBin}`));\n console.log(dim(\" Run: bun run build\"));\n } else {\n console.log(ok(` Shim binary: ${shimBin}`));\n }\n\n console.log();\n console.log(dim(\" Restart Claude Code to activate the daemon-backed PAI tools.\"));\n console.log();\n}\n\nfunction cmdUninstall(): void {\n console.log();\n console.log(bold(\" PAI Daemon Uninstall\"));\n console.log();\n\n // 1. Unload and remove plist\n if (existsSync(PLIST_PATH)) {\n try {\n spawnSync(\"launchctl\", [\"unload\", PLIST_PATH], { encoding: \"utf8\" });\n console.log(ok(\" Unloaded launchd plist.\"));\n } catch {\n console.log(warn(\" Could not unload plist via launchctl.\"));\n }\n try {\n unlinkSync(PLIST_PATH);\n console.log(ok(` Removed plist: ${PLIST_PATH}`));\n } catch (e) {\n console.log(warn(` Could not remove plist: ${e}`));\n }\n } else {\n console.log(dim(\" No launchd plist found.\"));\n }\n\n // 2. Revert ~/.claude.json to legacy direct MCP\n const config = readClaudeJson();\n const servers =\n typeof config.mcpServers === \"object\" && config.mcpServers !== null\n ? (config.mcpServers as Record<string, unknown>)\n : {};\n\n const legacy = servers[\"pai-legacy\"];\n if (legacy) {\n servers[\"pai\"] = legacy;\n delete servers[\"pai-legacy\"];\n try {\n writeClaudeJson(config);\n console.log(ok(\" Reverted ~/.claude.json to legacy direct MCP.\"));\n } catch (e) {\n console.log(warn(` Could not update ~/.claude.json: ${e}`));\n }\n } else {\n console.log(dim(\" No legacy PAI entry found in ~/.claude.json.\"));\n }\n\n console.log();\n console.log(dim(\" Restart Claude Code to deactivate daemon-backed tools.\"));\n console.log();\n}\n\nasync function cmdMigrate(connectionString?: string): Promise<void> {\n console.log();\n console.log(bold(\" PAI Daemon Migrate\"));\n console.log();\n console.log(dim(\" Running SQLite → PostgreSQL migration...\"));\n console.log();\n\n // Resolve the migration script path relative to this built file\n const { fileURLToPath } = await import(\"node:url\");\n const { dirname: pathDirname, join: pathJoin } = await import(\"node:path\");\n const __filename = fileURLToPath(import.meta.url);\n const __dirname = pathDirname(__filename);\n\n // The migration script is at docker/migrate-sqlite.ts in the source tree\n // When built, we look for it relative to the package root\n const migrationScript = pathJoin(__dirname, \"../..\", \"docker\", \"migrate-sqlite.ts\");\n const { spawnSync: spawn } = await import(\"node:child_process\");\n\n // Use npx tsx to run the TypeScript migration script (bun doesn't support better-sqlite3)\n const spawnArgs = [\"tsx\", migrationScript];\n if (connectionString) {\n spawnArgs.push(\"--connection-string\", connectionString);\n }\n\n const result = spawn(\"npx\", spawnArgs, { stdio: \"inherit\", encoding: \"utf8\" });\n\n if (result.status !== 0) {\n console.error(err(\" Migration failed. Check output above.\"));\n process.exit(result.status ?? 1);\n }\n}\n\nfunction cmdLogs(opts: { lines?: string; follow?: boolean }): void {\n const lines = opts.lines ?? \"50\";\n\n if (!existsSync(DAEMON_LOG)) {\n console.log(warn(`No daemon log found at ${DAEMON_LOG}.`));\n console.log(dim(\"The daemon may not have run yet.\"));\n return;\n }\n\n if (opts.follow) {\n // exec stays running\n try {\n execSync(`tail -f -n ${lines} \"${DAEMON_LOG}\"`, { stdio: \"inherit\" });\n } catch {\n // User pressed Ctrl+C — that's fine\n }\n } else {\n try {\n execSync(`tail -n ${lines} \"${DAEMON_LOG}\"`, { stdio: \"inherit\" });\n } catch (e) {\n const msg = e instanceof Error ? e.message : String(e);\n console.error(err(`Could not read log: ${msg}`));\n process.exit(1);\n }\n }\n}\n\n// ---------------------------------------------------------------------------\n// Commander registration\n// ---------------------------------------------------------------------------\n\nexport function registerDaemonCommands(daemonCmd: Command): void {\n daemonCmd\n .command(\"serve\")\n .description(\"Start the PAI daemon in the foreground\")\n .action(async () => {\n const { serve } = await import(\"../../daemon/daemon.js\");\n const { loadConfig: lc, ensureConfigDir } = await import(\n \"../../daemon/config.js\"\n );\n ensureConfigDir();\n const config = lc();\n await serve(config);\n });\n\n daemonCmd\n .command(\"migrate\")\n .description(\"Migrate federation data from SQLite to PostgreSQL\")\n .option(\n \"--connection-string <url>\",\n \"Postgres connection string (default: from config or postgresql://pai:pai@localhost:5432/pai)\"\n )\n .action(async (opts: { connectionString?: string }) => {\n await cmdMigrate(opts.connectionString);\n });\n\n daemonCmd\n .command(\"status\")\n .description(\"Query the running daemon status\")\n .action(async () => {\n await cmdStatus();\n });\n\n daemonCmd\n .command(\"restart\")\n .description(\"Send SIGTERM to the running daemon (launchd will restart it)\")\n .action(() => {\n cmdRestart();\n });\n\n daemonCmd\n .command(\"install\")\n .description(\n \"Install daemon as a launchd service and update ~/.claude.json to use the shim\"\n )\n .action(() => {\n cmdInstall();\n });\n\n daemonCmd\n .command(\"uninstall\")\n .description(\"Remove the launchd service and revert to direct MCP\")\n .action(() => {\n cmdUninstall();\n });\n\n daemonCmd\n .command(\"logs\")\n .description(`Tail the daemon log (${DAEMON_LOG})`)\n .option(\"-n, --lines <n>\", \"Number of lines to show\", \"50\")\n .option(\"-f, --follow\", \"Follow log output (like tail -f)\")\n .action((opts: { lines?: string; follow?: boolean }) => {\n cmdLogs(opts);\n });\n}\n","/**\n * pai backup — snapshot registry, config, and Postgres database.\n *\n * Creates a timestamped backup directory at:\n * ~/.pai/backups/YYYY-MM-DD-HHmmss/\n *\n * Contents:\n * registry.db — SQLite registry database\n * config.json — PAI daemon config\n * postgres-pai.sql — pg_dump of the Postgres \"pai\" database (via docker exec)\n */\n\nimport type { Command } from \"commander\";\nimport {\n existsSync,\n mkdirSync,\n copyFileSync,\n statSync,\n} from \"node:fs\";\nimport { join } from \"node:path\";\nimport { homedir } from \"node:os\";\nimport { execSync } from \"node:child_process\";\nimport { ok, warn, err, dim, bold } from \"../utils.js\";\nimport { loadConfig } from \"../../daemon/config.js\";\n\n// ---------------------------------------------------------------------------\n// Paths\n// ---------------------------------------------------------------------------\n\nconst HOME = homedir();\nconst REGISTRY_DB = join(HOME, \".pai\", \"registry.db\");\nconst CONFIG_FILE = join(HOME, \".config\", \"pai\", \"config.json\");\nconst BACKUPS_DIR = join(HOME, \".pai\", \"backups\");\nconst DOCKER_CONTAINER = \"pai-pgvector\";\nconst PG_DATABASE = \"pai\";\nconst PG_USER = \"pai\";\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\nfunction timestamp(): string {\n const now = new Date();\n const YYYY = now.getFullYear();\n const MM = String(now.getMonth() + 1).padStart(2, \"0\");\n const DD = String(now.getDate()).padStart(2, \"0\");\n const HH = String(now.getHours()).padStart(2, \"0\");\n const mm = String(now.getMinutes()).padStart(2, \"0\");\n const ss = String(now.getSeconds()).padStart(2, \"0\");\n return `${YYYY}-${MM}-${DD}-${HH}${mm}${ss}`;\n}\n\nfunction fmtBytes(bytes: number): string {\n if (bytes < 1024) return `${bytes} B`;\n if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;\n return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;\n}\n\nfunction fileSize(path: string): string {\n try {\n return fmtBytes(statSync(path).size);\n } catch {\n return \"unknown\";\n }\n}\n\n// ---------------------------------------------------------------------------\n// Registration\n// ---------------------------------------------------------------------------\n\nexport function registerBackupCommands(program: Command): void {\n program\n .command(\"backup\")\n .description(\"Backup registry, config, and Postgres database to ~/.pai/backups/\")\n .option(\"--no-postgres\", \"Skip the Postgres pg_dump (faster, registry+config only)\")\n .action(async (opts: { postgres: boolean }) => {\n const ts = timestamp();\n const backupDir = join(BACKUPS_DIR, ts);\n\n console.log(dim(`Creating backup: ${backupDir}`));\n mkdirSync(backupDir, { recursive: true });\n\n const results: { label: string; path: string; size: string; status: string }[] = [];\n\n // ------------------------------------------------------------------\n // 1. Registry SQLite DB\n // ------------------------------------------------------------------\n\n if (existsSync(REGISTRY_DB)) {\n const dest = join(backupDir, \"registry.db\");\n try {\n copyFileSync(REGISTRY_DB, dest);\n results.push({ label: \"Registry DB\", path: dest, size: fileSize(dest), status: ok(\"ok\") });\n } catch (e) {\n results.push({ label: \"Registry DB\", path: dest, size: \"-\", status: err(`failed: ${e}`) });\n }\n } else {\n results.push({ label: \"Registry DB\", path: REGISTRY_DB, size: \"-\", status: warn(\"not found — skipped\") });\n }\n\n // ------------------------------------------------------------------\n // 2. Config file\n // ------------------------------------------------------------------\n\n if (existsSync(CONFIG_FILE)) {\n const dest = join(backupDir, \"config.json\");\n try {\n copyFileSync(CONFIG_FILE, dest);\n results.push({ label: \"Config\", path: dest, size: fileSize(dest), status: ok(\"ok\") });\n } catch (e) {\n results.push({ label: \"Config\", path: dest, size: \"-\", status: err(`failed: ${e}`) });\n }\n } else {\n results.push({ label: \"Config\", path: CONFIG_FILE, size: \"-\", status: warn(\"not found — skipped\") });\n }\n\n // ------------------------------------------------------------------\n // 3. Optional: Federation SQLite (legacy)\n // ------------------------------------------------------------------\n\n const federationDb = join(HOME, \".pai\", \"federation.db\");\n if (existsSync(federationDb)) {\n const dest = join(backupDir, \"federation.db\");\n try {\n copyFileSync(federationDb, dest);\n results.push({ label: \"Federation DB (legacy)\", path: dest, size: fileSize(dest), status: ok(\"ok\") });\n } catch (e) {\n results.push({ label: \"Federation DB (legacy)\", path: dest, size: \"-\", status: warn(`skipped: ${e}`) });\n }\n }\n\n // ------------------------------------------------------------------\n // 4. Postgres pg_dump via docker exec\n // ------------------------------------------------------------------\n\n if (opts.postgres) {\n const sqlDest = join(backupDir, \"postgres-pai.sql\");\n console.log(dim(` Running pg_dump on ${DOCKER_CONTAINER} (this may take a moment)...`));\n try {\n // Check Docker is running and container exists\n execSync(`docker inspect ${DOCKER_CONTAINER} --format='{{.State.Status}}'`, {\n stdio: \"pipe\",\n });\n\n execSync(\n `docker exec ${DOCKER_CONTAINER} pg_dump -U ${PG_USER} ${PG_DATABASE} > \"${sqlDest}\"`,\n { stdio: [\"pipe\", \"pipe\", \"pipe\"], shell: true }\n );\n results.push({ label: \"Postgres DB\", path: sqlDest, size: fileSize(sqlDest), status: ok(\"ok\") });\n } catch (e) {\n const msg = e instanceof Error ? e.message.split(\"\\n\")[0] : String(e);\n results.push({ label: \"Postgres DB\", path: sqlDest, size: \"-\", status: err(`failed: ${msg}`) });\n console.log(warn(` Postgres backup failed. Is Docker running with container '${DOCKER_CONTAINER}'?`));\n }\n } else {\n results.push({ label: \"Postgres DB\", path: \"-\", size: \"-\", status: dim(\"skipped (--no-postgres)\") });\n }\n\n // ------------------------------------------------------------------\n // Summary\n // ------------------------------------------------------------------\n\n console.log(`\\n${bold(\"Backup complete:\")} ${backupDir}\\n`);\n\n const labelWidth = Math.max(...results.map((r) => r.label.length)) + 2;\n for (const r of results) {\n const label = r.label.padEnd(labelWidth);\n console.log(` ${bold(label)} ${r.status} ${dim(r.size)}`);\n }\n\n console.log(`\\n ${dim(\"Path:\")} ${backupDir}`);\n console.log(` ${dim(\"To restore:\")} pai restore ${backupDir}\\n`);\n });\n}\n","/**\n * pai restore [path] — restore from a backup created by `pai backup`.\n *\n * If no path is given, lists available backups and restores the latest one\n * after prompting for confirmation.\n *\n * Restores:\n * registry.db — SQLite registry database\n * config.json — PAI daemon config\n * postgres-pai.sql — Postgres dump (piped into psql via docker exec)\n * federation.db — Legacy SQLite federation DB (if present)\n */\n\nimport type { Command } from \"commander\";\nimport {\n existsSync,\n readdirSync,\n statSync,\n copyFileSync,\n mkdirSync,\n readFileSync,\n} from \"node:fs\";\nimport { join } from \"node:path\";\nimport { homedir } from \"node:os\";\nimport { execSync, spawnSync } from \"node:child_process\";\nimport { createInterface } from \"node:readline\";\nimport { ok, warn, err, dim, bold } from \"../utils.js\";\n\n// ---------------------------------------------------------------------------\n// Paths\n// ---------------------------------------------------------------------------\n\nconst HOME = homedir();\nconst REGISTRY_DB = join(HOME, \".pai\", \"registry.db\");\nconst CONFIG_FILE = join(HOME, \".config\", \"pai\", \"config.json\");\nconst BACKUPS_DIR = join(HOME, \".pai\", \"backups\");\nconst DOCKER_CONTAINER = \"pai-pgvector\";\nconst PG_DATABASE = \"pai\";\nconst PG_USER = \"pai\";\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\nfunction confirm(question: string): Promise<boolean> {\n return new Promise((resolve) => {\n const rl = createInterface({ input: process.stdin, output: process.stdout });\n rl.question(`${question} [y/N] `, (answer) => {\n rl.close();\n resolve(answer.trim().toLowerCase() === \"y\");\n });\n });\n}\n\nfunction listBackups(): string[] {\n if (!existsSync(BACKUPS_DIR)) return [];\n return readdirSync(BACKUPS_DIR)\n .filter((name) => {\n const full = join(BACKUPS_DIR, name);\n return statSync(full).isDirectory() && /^\\d{4}-\\d{2}-\\d{2}-\\d{6}$/.test(name);\n })\n .sort()\n .reverse(); // newest first\n}\n\nfunction formatBackupDate(name: string): string {\n // YYYY-MM-DD-HHmmss → \"YYYY-MM-DD HH:mm:ss\"\n const m = name.match(/^(\\d{4}-\\d{2}-\\d{2})-(\\d{2})(\\d{2})(\\d{2})$/);\n if (!m) return name;\n return `${m[1]} ${m[2]}:${m[3]}:${m[4]}`;\n}\n\nfunction backupContents(backupDir: string): string[] {\n try {\n return readdirSync(backupDir);\n } catch {\n return [];\n }\n}\n\n// ---------------------------------------------------------------------------\n// Registration\n// ---------------------------------------------------------------------------\n\nexport function registerRestoreCommands(program: Command): void {\n program\n .command(\"restore [backup-path]\")\n .description(\"Restore from a backup directory (created by pai backup)\")\n .option(\"--no-postgres\", \"Skip restoring the Postgres database\")\n .option(\"--yes\", \"Skip confirmation prompt (non-interactive)\")\n .action(async (backupPath: string | undefined, opts: { postgres: boolean; yes: boolean }) => {\n\n // ------------------------------------------------------------------\n // 1. Determine backup directory\n // ------------------------------------------------------------------\n\n let resolvedDir: string;\n\n if (backupPath) {\n resolvedDir = backupPath.startsWith(\"~\")\n ? backupPath.replace(/^~/, HOME)\n : backupPath;\n\n if (!existsSync(resolvedDir)) {\n console.error(err(`Backup directory not found: ${resolvedDir}`));\n process.exit(1);\n }\n } else {\n // No path given — pick from available backups\n const backups = listBackups();\n if (backups.length === 0) {\n console.error(err(`No backups found in ${BACKUPS_DIR}`));\n console.log(dim(\" Run 'pai backup' to create one.\"));\n process.exit(1);\n }\n\n console.log(`\\n${bold(\"Available backups\")} (newest first):\\n`);\n backups.forEach((name, i) => {\n const dir = join(BACKUPS_DIR, name);\n const contents = backupContents(dir).join(\", \");\n const marker = i === 0 ? ok(\" ← latest\") : \"\";\n console.log(` ${bold(String(i + 1).padStart(2))}. ${formatBackupDate(name)}${marker}`);\n console.log(` ${dim(dir)}`);\n console.log(` ${dim(\"Contents:\")} ${contents}`);\n });\n\n console.log();\n resolvedDir = join(BACKUPS_DIR, backups[0]);\n console.log(dim(`Using latest backup: ${resolvedDir}\\n`));\n }\n\n // ------------------------------------------------------------------\n // 2. Inventory what's in the backup\n // ------------------------------------------------------------------\n\n const hasRegistry = existsSync(join(resolvedDir, \"registry.db\"));\n const hasConfig = existsSync(join(resolvedDir, \"config.json\"));\n const hasSql = existsSync(join(resolvedDir, \"postgres-pai.sql\"));\n const hasFed = existsSync(join(resolvedDir, \"federation.db\"));\n\n console.log(`${bold(\"Backup contents:\")}`);\n console.log(` registry.db ${hasRegistry ? ok(\"present\") : warn(\"missing\")}`);\n console.log(` config.json ${hasConfig ? ok(\"present\") : warn(\"missing\")}`);\n console.log(` postgres-pai.sql ${hasSql && opts.postgres ? ok(\"present\") : hasSql ? warn(\"present (skipped via --no-postgres)\") : warn(\"missing\")}`);\n if (hasFed) {\n console.log(` federation.db ${ok(\"present\")} ${dim(\"(legacy)\")}`);\n }\n\n // ------------------------------------------------------------------\n // 3. Confirm\n // ------------------------------------------------------------------\n\n console.log(`\\n${warn(\"WARNING:\")} This will OVERWRITE your current PAI data.`);\n if (hasSql && opts.postgres) {\n console.log(warn(\" Postgres database will be dropped and recreated from the backup.\"));\n }\n\n const proceed = opts.yes || await confirm(\"Proceed with restore?\");\n if (!proceed) {\n console.log(dim(\"Restore cancelled.\"));\n process.exit(0);\n }\n\n console.log();\n\n const results: { label: string; status: string }[] = [];\n\n // ------------------------------------------------------------------\n // 4. Restore registry.db\n // ------------------------------------------------------------------\n\n if (hasRegistry) {\n try {\n mkdirSync(join(HOME, \".pai\"), { recursive: true });\n copyFileSync(join(resolvedDir, \"registry.db\"), REGISTRY_DB);\n results.push({ label: \"Registry DB\", status: ok(\"restored\") });\n } catch (e) {\n results.push({ label: \"Registry DB\", status: err(`failed: ${e}`) });\n }\n } else {\n results.push({ label: \"Registry DB\", status: warn(\"missing in backup — skipped\") });\n }\n\n // ------------------------------------------------------------------\n // 5. Restore config.json\n // ------------------------------------------------------------------\n\n if (hasConfig) {\n try {\n mkdirSync(join(HOME, \".config\", \"pai\"), { recursive: true });\n copyFileSync(join(resolvedDir, \"config.json\"), CONFIG_FILE);\n results.push({ label: \"Config\", status: ok(\"restored\") });\n } catch (e) {\n results.push({ label: \"Config\", status: err(`failed: ${e}`) });\n }\n } else {\n results.push({ label: \"Config\", status: warn(\"missing in backup — skipped\") });\n }\n\n // ------------------------------------------------------------------\n // 6. Restore federation.db (legacy)\n // ------------------------------------------------------------------\n\n if (hasFed) {\n try {\n copyFileSync(join(resolvedDir, \"federation.db\"), join(HOME, \".pai\", \"federation.db\"));\n results.push({ label: \"Federation DB (legacy)\", status: ok(\"restored\") });\n } catch (e) {\n results.push({ label: \"Federation DB (legacy)\", status: warn(`skipped: ${e}`) });\n }\n }\n\n // ------------------------------------------------------------------\n // 7. Restore Postgres via docker exec\n // ------------------------------------------------------------------\n\n if (hasSql && opts.postgres) {\n console.log(dim(\" Restoring Postgres database (this may take a while)...\"));\n try {\n // Verify container is running\n execSync(`docker inspect ${DOCKER_CONTAINER} --format='{{.State.Status}}'`, {\n stdio: \"pipe\",\n });\n\n // Drop and recreate the database, then restore\n const dropCreate = `docker exec ${DOCKER_CONTAINER} psql -U ${PG_USER} -c \"DROP DATABASE IF EXISTS ${PG_DATABASE}; CREATE DATABASE ${PG_DATABASE} OWNER ${PG_USER};\"`;\n execSync(dropCreate, { stdio: \"pipe\", shell: true });\n\n // Pipe the SQL dump into psql\n const sqlContent = readFileSync(join(resolvedDir, \"postgres-pai.sql\"), \"utf8\");\n const psqlResult = spawnSync(\n \"docker\",\n [\"exec\", \"-i\", DOCKER_CONTAINER, \"psql\", \"-U\", PG_USER, PG_DATABASE],\n { input: sqlContent, encoding: \"utf8\", stdio: [\"pipe\", \"pipe\", \"pipe\"] }\n );\n\n if (psqlResult.status !== 0) {\n const errMsg = psqlResult.stderr?.split(\"\\n\")[0] ?? \"unknown error\";\n results.push({ label: \"Postgres DB\", status: err(`failed: ${errMsg}`) });\n } else {\n results.push({ label: \"Postgres DB\", status: ok(\"restored\") });\n }\n } catch (e) {\n const msg = e instanceof Error ? e.message.split(\"\\n\")[0] : String(e);\n results.push({ label: \"Postgres DB\", status: err(`failed: ${msg}`) });\n console.log(warn(` Is Docker running with container '${DOCKER_CONTAINER}'?`));\n }\n } else if (!hasSql || !opts.postgres) {\n const reason = !hasSql ? \"no SQL dump in backup\" : \"--no-postgres\";\n results.push({ label: \"Postgres DB\", status: dim(`skipped (${reason})`) });\n }\n\n // ------------------------------------------------------------------\n // Summary\n // ------------------------------------------------------------------\n\n console.log(`\\n${bold(\"Restore complete:\")}\\n`);\n const labelWidth = Math.max(...results.map((r) => r.label.length)) + 2;\n for (const r of results) {\n console.log(` ${bold(r.label.padEnd(labelWidth))} ${r.status}`);\n }\n\n const hasErrors = results.some((r) => r.status.includes(\"\\u001b[31m\"));\n if (!hasErrors) {\n console.log(`\\n ${ok(\"All done.\")} You may need to restart the PAI daemon.\\n`);\n console.log(` ${dim(\"Restart:\")} pai daemon restart\\n`);\n } else {\n console.log(`\\n ${warn(\"Some items failed — check output above.\")}\\n`);\n process.exit(1);\n }\n });\n}\n","/**\n * pai setup — Interactive setup wizard for PAI Knowledge OS\n *\n * Guides new users through:\n * 1. Welcome and overview\n * 2. Storage backend selection (PostgreSQL/SQLite)\n * 3. Embedding model selection\n * 4. Agent configuration (CLAUDE.md generation)\n * 5. Directory scanning configuration\n * 6. Initial index (optional)\n * 7. Summary and next steps\n */\n\nimport type { Command } from \"commander\";\nimport { createInterface } from \"node:readline\";\nimport { existsSync, readFileSync, writeFileSync, mkdirSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { join, resolve } from \"node:path\";\nimport { execSync, spawnSync } from \"node:child_process\";\nimport chalk from \"chalk\";\nimport { CONFIG_DIR, CONFIG_FILE, loadConfig } from \"../../daemon/config.js\";\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\nconst c = {\n bold: (s: string) => chalk.bold(s),\n dim: (s: string) => chalk.dim(s),\n green: (s: string) => chalk.green(s),\n yellow: (s: string) => chalk.yellow(s),\n cyan: (s: string) => chalk.cyan(s),\n red: (s: string) => chalk.red(s),\n blue: (s: string) => chalk.blue(s),\n ok: (s: string) => chalk.green(\" \" + s),\n warn: (s: string) => chalk.yellow(\" \" + s),\n err: (s: string) => chalk.red(\" \" + s),\n};\n\nfunction line(text = \"\") {\n console.log(text);\n}\n\nfunction section(title: string) {\n line();\n console.log(chalk.bold.cyan(\" \" + title));\n console.log(chalk.dim(\" \" + \"─\".repeat(title.length)));\n}\n\n// ---------------------------------------------------------------------------\n// Readline prompt helper\n// ---------------------------------------------------------------------------\n\nfunction createRl() {\n const rl = createInterface({\n input: process.stdin,\n output: process.stdout,\n });\n\n // Handle Ctrl+C gracefully\n rl.on(\"SIGINT\", () => {\n line();\n line(c.dim(\" Setup cancelled. Run `pai setup` again to restart.\"));\n line();\n process.exit(0);\n });\n\n return rl;\n}\n\nasync function prompt(rl: ReturnType<typeof createRl>, question: string): Promise<string> {\n return new Promise((resolve) => {\n rl.question(question, (answer) => {\n resolve(answer.trim());\n });\n });\n}\n\n/**\n * Prompt for a numbered menu selection.\n * Returns the 0-based index of the selected option, or defaultIdx if empty input.\n */\nasync function promptMenu(\n rl: ReturnType<typeof createRl>,\n options: Array<{ label: string; description?: string }>,\n defaultIdx = 0,\n): Promise<number> {\n for (let i = 0; i < options.length; i++) {\n const num = chalk.bold(` ${i + 1}.`);\n const label = i === defaultIdx ? chalk.cyan(options[i].label) : options[i].label;\n const marker = i === defaultIdx ? chalk.dim(\" (recommended)\") : \"\";\n console.log(`${num} ${label}${marker}`);\n if (options[i].description) {\n console.log(chalk.dim(` ${options[i].description}`));\n }\n }\n line();\n\n while (true) {\n const answer = await prompt(\n rl,\n chalk.bold(` Enter number [1-${options.length}] (default: ${defaultIdx + 1}): `),\n );\n\n if (answer === \"\") return defaultIdx;\n\n const n = parseInt(answer, 10);\n if (!isNaN(n) && n >= 1 && n <= options.length) {\n return n - 1;\n }\n\n console.log(c.warn(`Please enter a number between 1 and ${options.length}.`));\n }\n}\n\n/**\n * Prompt for a yes/no answer. Returns true for yes.\n */\nasync function promptYesNo(\n rl: ReturnType<typeof createRl>,\n question: string,\n defaultYes = true,\n): Promise<boolean> {\n const hint = defaultYes ? \"[Y/n]\" : \"[y/N]\";\n const answer = await prompt(rl, ` ${question} ${chalk.dim(hint)}: `);\n\n if (answer === \"\") return defaultYes;\n return answer.toLowerCase().startsWith(\"y\");\n}\n\n// ---------------------------------------------------------------------------\n// Config read/write helpers\n// ---------------------------------------------------------------------------\n\nfunction readConfigRaw(): Record<string, unknown> {\n if (!existsSync(CONFIG_FILE)) return {};\n try {\n return JSON.parse(readFileSync(CONFIG_FILE, \"utf-8\")) as Record<string, unknown>;\n } catch {\n return {};\n }\n}\n\nfunction writeConfigRaw(data: Record<string, unknown>): void {\n if (!existsSync(CONFIG_DIR)) {\n mkdirSync(CONFIG_DIR, { recursive: true });\n }\n writeFileSync(CONFIG_FILE, JSON.stringify(data, null, 2) + \"\\n\", \"utf-8\");\n}\n\nfunction mergeConfig(updates: Record<string, unknown>): void {\n const current = readConfigRaw();\n const merged = { ...current, ...updates };\n // Merge nested postgres object if present\n if (updates.postgres && typeof current.postgres === \"object\" && current.postgres !== null) {\n merged.postgres = { ...(current.postgres as object), ...(updates.postgres as object) };\n }\n writeConfigRaw(merged);\n}\n\n// ---------------------------------------------------------------------------\n// Docker helpers\n// ---------------------------------------------------------------------------\n\nfunction hasDocker(): boolean {\n try {\n const result = spawnSync(\"docker\", [\"--version\"], { stdio: \"pipe\" });\n return result.status === 0;\n } catch {\n return false;\n }\n}\n\nfunction getDockerDir(): string {\n // Find the docker/ directory relative to the installed package\n // When running from dist/cli/index.mjs, go up to find docker/\n // Try common locations: cwd, parent dirs, and npm install paths\n const candidates = [\n join(process.cwd(), \"docker\"),\n join(homedir(), \"dev\", \"ai\", \"PAI\", \"docker\"),\n join(\"/\", \"usr\", \"local\", \"lib\", \"node_modules\", \"@mnott\", \"pai\", \"docker\"),\n ];\n for (const c of candidates) {\n if (existsSync(join(c, \"docker-compose.yml\"))) return c;\n }\n return join(process.cwd(), \"docker\");\n}\n\nfunction getTemplatesDir(): string {\n // Find the templates/ directory relative to the installed package\n const candidates = [\n join(process.cwd(), \"templates\"),\n join(homedir(), \"dev\", \"ai\", \"PAI\", \"templates\"),\n join(\"/\", \"usr\", \"local\", \"lib\", \"node_modules\", \"@mnott\", \"pai\", \"templates\"),\n ];\n for (const c of candidates) {\n if (existsSync(join(c, \"claude-md.template.md\"))) return c;\n }\n return join(process.cwd(), \"templates\");\n}\n\nasync function startDocker(rl: ReturnType<typeof createRl>): Promise<boolean> {\n const dockerDir = getDockerDir();\n const composePath = join(dockerDir, \"docker-compose.yml\");\n\n if (!existsSync(composePath)) {\n console.log(c.warn(`docker-compose.yml not found at ${dockerDir}`));\n console.log(c.dim(\" You can start it manually later:\"));\n console.log(c.dim(\" docker compose up -d\"));\n return false;\n }\n\n console.log(c.dim(` Starting PostgreSQL container from ${dockerDir}...`));\n\n try {\n const result = spawnSync(\"docker\", [\"compose\", \"up\", \"-d\"], {\n cwd: dockerDir,\n stdio: \"inherit\",\n });\n\n if (result.status !== 0) {\n console.log(c.warn(\" Docker compose failed. You can start it manually:\"));\n console.log(c.dim(` cd ${dockerDir} && docker compose up -d`));\n return false;\n }\n\n console.log(c.ok(\"PostgreSQL container started.\"));\n return true;\n } catch (e) {\n console.log(c.warn(` Could not run docker compose: ${e}`));\n return false;\n }\n}\n\nasync function testPostgresConnection(connectionString: string): Promise<boolean> {\n // Quick connection test using pg\n try {\n const { default: pg } = await import(\"pg\") as { default: typeof import(\"pg\") };\n const client = new pg.Client({ connectionString });\n await client.connect();\n await client.end();\n return true;\n } catch {\n return false;\n }\n}\n\n// ---------------------------------------------------------------------------\n// Steps\n// ---------------------------------------------------------------------------\n\n/**\n * Step 1: Welcome banner and overview\n */\nfunction stepWelcome(): void {\n line();\n line(chalk.bold.cyan(\" ╔════════════════════════════════════════╗\"));\n line(chalk.bold.cyan(\" ║ PAI Knowledge OS — Setup Wizard ║\"));\n line(chalk.bold.cyan(\" ╚════════════════════════════════════════╝\"));\n line();\n line(\n \" PAI is a personal knowledge system that indexes your files, generates\",\n );\n line(\n \" semantic embeddings for intelligent search, and stores everything in a\",\n );\n line(\n \" local database so you can search your knowledge base with natural language.\",\n );\n line();\n line(c.dim(\" This wizard will guide you through the initial configuration.\"));\n line(c.dim(\" Press Ctrl+C at any time to cancel.\"));\n}\n\n/**\n * Step 2: Storage backend selection\n */\nasync function stepStorage(rl: ReturnType<typeof createRl>): Promise<Record<string, unknown>> {\n section(\"Step 2: Storage Backend\");\n line();\n line(\" Choose how PAI stores your indexed knowledge:\");\n line();\n\n const choice = await promptMenu(rl, [\n {\n label: \"PostgreSQL with pgvector\",\n description:\n \"Best for large collections, semantic search, and production use. Requires Docker or a Postgres server.\",\n },\n {\n label: \"SQLite\",\n description:\n \"Simple, no dependencies, zero configuration. Good for trying PAI out. Keyword search only.\",\n },\n ]);\n\n if (choice === 1) {\n // SQLite\n line();\n console.log(c.ok(\"SQLite selected. No additional setup needed.\"));\n return { storageBackend: \"sqlite\" };\n }\n\n // PostgreSQL\n line();\n line(\" PostgreSQL requires a running Postgres server with the pgvector extension.\");\n line();\n\n if (hasDocker()) {\n console.log(c.ok(\"Docker is installed.\"));\n line();\n\n const useDocker = await promptYesNo(\n rl,\n \"Start the PAI PostgreSQL container with Docker? (recommended)\",\n true,\n );\n\n if (useDocker) {\n line();\n await startDocker(rl);\n line();\n\n // Brief wait for container to start\n console.log(c.dim(\" Waiting 3 seconds for container to be ready...\"));\n await new Promise((r) => setTimeout(r, 3000));\n\n const connStr = \"postgresql://pai:pai@localhost:5432/pai\";\n console.log(c.dim(` Testing connection to ${connStr}...`));\n\n const ok2 = await testPostgresConnection(connStr);\n if (ok2) {\n console.log(c.ok(\"Connection successful!\"));\n return {\n storageBackend: \"postgres\",\n postgres: { connectionString: connStr },\n };\n } else {\n console.log(c.warn(\"Connection test failed. The container may still be starting.\"));\n console.log(c.dim(\" Using default connection string — you can verify with `pai daemon status`.\"));\n return {\n storageBackend: \"postgres\",\n postgres: { connectionString: connStr },\n };\n }\n }\n } else {\n console.log(c.dim(\" Docker not found. Using manual connection string entry.\"));\n }\n\n // Manual connection string entry\n line();\n line(\" Enter your PostgreSQL connection details:\");\n line();\n\n const useConnStr = await promptYesNo(\n rl,\n \"Use a full connection string? (e.g. postgresql://user:pass@host:5432/dbname)\",\n true,\n );\n\n if (useConnStr) {\n const connStr = await prompt(\n rl,\n chalk.bold(\" Connection string: \"),\n );\n\n if (connStr) {\n console.log(c.dim(\" Testing connection...\"));\n const connected = await testPostgresConnection(connStr);\n if (connected) {\n console.log(c.ok(\"Connection successful!\"));\n } else {\n console.log(c.warn(\"Connection test failed — check credentials and try again later.\"));\n }\n return {\n storageBackend: \"postgres\",\n postgres: { connectionString: connStr },\n };\n }\n }\n\n // Field-by-field entry\n const host = await prompt(rl, chalk.bold(\" Host [localhost]: \")) || \"localhost\";\n const portStr = await prompt(rl, chalk.bold(\" Port [5432]: \")) || \"5432\";\n const database = await prompt(rl, chalk.bold(\" Database [pai]: \")) || \"pai\";\n const user = await prompt(rl, chalk.bold(\" User [pai]: \")) || \"pai\";\n const password = await prompt(rl, chalk.bold(\" Password [pai]: \")) || \"pai\";\n\n const connStr = `postgresql://${user}:${password}@${host}:${portStr}/${database}`;\n\n console.log(c.dim(` Connection string: ${connStr}`));\n console.log(c.dim(\" Testing connection...\"));\n\n const connected = await testPostgresConnection(connStr);\n if (connected) {\n console.log(c.ok(\"Connection successful!\"));\n } else {\n console.log(c.warn(\"Connection test failed — check credentials and try again later.\"));\n }\n\n return {\n storageBackend: \"postgres\",\n postgres: { connectionString: connStr },\n };\n}\n\n/**\n * Step 3: Embedding model selection\n */\nasync function stepEmbedding(rl: ReturnType<typeof createRl>): Promise<Record<string, unknown>> {\n section(\"Step 3: Embedding Model\");\n line();\n line(\n \" An embedding model converts your text into vectors for semantic search.\",\n );\n line(\n \" Models are downloaded from HuggingFace on first use.\",\n );\n line();\n\n const choice = await promptMenu(rl, [\n {\n label: \"Snowflake Arctic Embed m v1.5\",\n description:\n \"768 dims, ~118MB download. Best retrieval quality per MB (MTEB score 55.14). \" +\n \"Asymmetric retrieval — different handling for queries vs documents. Best for most users.\",\n },\n {\n label: \"BGE Small EN v1.5\",\n description:\n \"384 dims, ~32MB download. Lightweight and fast. Good for limited disk space \" +\n \"or when faster embedding is more important than maximum quality. English only.\",\n },\n {\n label: \"Nomic Embed Text v1.5\",\n description:\n \"768 dims, ~100MB download. 8K token context window — excellent for long documents. \" +\n \"Matryoshka dimensions (can truncate for speed/size tradeoffs).\",\n },\n {\n label: \"None — skip embeddings\",\n description:\n \"BM25/keyword search only. No model download needed. You can add embeddings later \" +\n \"by running `pai memory embed` after selecting a model.\",\n },\n ]);\n\n const models: Record<number, string | null> = {\n 0: \"Snowflake/snowflake-arctic-embed-m-v1.5\",\n 1: \"BAAI/bge-small-en-v1.5\",\n 2: \"nomic-ai/nomic-embed-text-v1.5\",\n 3: null,\n };\n\n const selectedModel = models[choice];\n\n line();\n if (selectedModel) {\n console.log(c.ok(`Model selected: ${selectedModel}`));\n console.log(c.dim(\" The model will be downloaded on first use of `pai memory embed`.\"));\n } else {\n console.log(c.ok(\"Skipping embeddings. Keyword search will still work.\"));\n console.log(c.dim(\" Add later: update embeddingModel in ~/.config/pai/config.json\"));\n }\n\n return {\n embeddingModel: selectedModel ?? \"none\",\n };\n}\n\n/**\n * Step 4: Agent configuration — generate ~/.claude/CLAUDE.md from template\n */\nasync function stepClaudeMd(rl: ReturnType<typeof createRl>): Promise<boolean> {\n section(\"Step 4: Agent Configuration (CLAUDE.md)\");\n line();\n line(\n \" PAI ships a CLAUDE.md template with agent orchestration patterns:\",\n );\n line(\n \" swarm mode, model escalation, parallel execution, quality standards.\",\n );\n line();\n\n const claudeDir = join(homedir(), \".claude\");\n const claudeMd = join(claudeDir, \"CLAUDE.md\");\n const agentPrefs = join(CONFIG_DIR, \"agent-prefs.md\");\n const templatesDir = getTemplatesDir();\n const templatePath = join(templatesDir, \"claude-md.template.md\");\n\n if (!existsSync(templatePath)) {\n console.log(c.warn(\"Template not found: \" + templatePath));\n console.log(c.dim(\" Skipping CLAUDE.md generation. You can copy it manually from templates/.\"));\n return false;\n }\n\n // Check existing CLAUDE.md\n if (existsSync(claudeMd)) {\n const content = readFileSync(claudeMd, \"utf-8\");\n const isGenerated = content.includes(\"Generated by PAI Setup\");\n if (isGenerated) {\n console.log(c.dim(\" Found existing PAI-generated CLAUDE.md.\"));\n } else {\n console.log(c.yellow(\" Found existing CLAUDE.md (custom, not PAI-generated).\"));\n console.log(c.dim(\" A backup will be created before overwriting.\"));\n }\n line();\n\n const overwrite = await promptYesNo(\n rl,\n \"Update ~/.claude/CLAUDE.md with the latest PAI template?\",\n isGenerated, // default yes if already generated, no if custom\n );\n\n if (!overwrite) {\n console.log(c.dim(\" Keeping existing CLAUDE.md unchanged.\"));\n return false;\n }\n\n // Backup custom CLAUDE.md\n if (!isGenerated) {\n const backupPath = claudeMd + \".backup\";\n writeFileSync(backupPath, content, \"utf-8\");\n console.log(c.ok(`Backed up existing CLAUDE.md to ${backupPath}`));\n }\n }\n\n // Read template\n let template = readFileSync(templatePath, \"utf-8\");\n\n // Substitute ${HOME} with actual home directory\n template = template.replace(/\\$\\{HOME\\}/g, homedir());\n\n // Write CLAUDE.md\n if (!existsSync(claudeDir)) {\n mkdirSync(claudeDir, { recursive: true });\n }\n writeFileSync(claudeMd, template, \"utf-8\");\n\n line();\n console.log(c.ok(\"Generated ~/.claude/CLAUDE.md from PAI template.\"));\n\n // Check for agent-prefs.md\n if (!existsSync(agentPrefs)) {\n line();\n line(\" For personal settings (project mappings, notification preferences),\");\n line(\" copy the example and customize:\");\n line();\n console.log(chalk.cyan(` cp ${templatesDir}/agent-prefs.example.md ${agentPrefs}`));\n line();\n\n const createPrefs = await promptYesNo(\n rl,\n \"Copy the example agent-prefs.md now?\",\n true,\n );\n\n if (createPrefs) {\n const examplePath = join(templatesDir, \"agent-prefs.example.md\");\n if (existsSync(examplePath)) {\n if (!existsSync(CONFIG_DIR)) {\n mkdirSync(CONFIG_DIR, { recursive: true });\n }\n writeFileSync(agentPrefs, readFileSync(examplePath, \"utf-8\"), \"utf-8\");\n console.log(c.ok(`Created ${agentPrefs}`));\n console.log(c.dim(\" Edit this file to add your personal preferences and project mappings.\"));\n } else {\n console.log(c.warn(\"Example file not found. Create agent-prefs.md manually.\"));\n }\n }\n } else {\n console.log(c.dim(\" Personal preferences: \" + agentPrefs));\n }\n\n return true;\n}\n\n/**\n * Step 5: Directory scanning configuration\n */\nasync function stepDirectories(rl: ReturnType<typeof createRl>): Promise<void> {\n section(\"Step 5: Directories to Index\");\n line();\n line(\n \" PAI indexes files in your registered projects. You can register projects\",\n );\n line(\n \" individually with `pai project add <path>`, or let the registry scanner\",\n );\n line(\n \" discover them automatically with `pai registry scan`.\",\n );\n line();\n\n const defaults = [\n join(homedir(), \"Projects\"),\n join(homedir(), \"Documents\"),\n join(homedir(), \"dev\"),\n ].filter(existsSync);\n\n if (defaults.length > 0) {\n line(\" These directories exist on your system:\");\n for (const d of defaults) {\n console.log(chalk.dim(` ${d}`));\n }\n line();\n }\n\n const runScan = await promptYesNo(\n rl,\n \"Run `pai registry scan` to auto-detect projects after setup?\",\n false,\n );\n\n if (runScan) {\n line();\n console.log(c.dim(\" Registry scan will run after setup completes.\"));\n } else {\n console.log(c.dim(\" Add projects manually: pai project add <path>\"));\n console.log(c.dim(\" Or discover them later: pai registry scan\"));\n }\n\n // Store whether scan was requested for use in step 5\n (stepDirectories as { _runScan?: boolean })._runScan = runScan;\n}\n\n/**\n * Step 6: Initial index\n */\nasync function stepInitialIndex(rl: ReturnType<typeof createRl>): Promise<void> {\n section(\"Step 6: Initial Index\");\n line();\n line(\n \" Indexing scans your registered projects and builds the search index.\",\n );\n line(\n \" The daemon runs indexing automatically every 5 minutes once started.\",\n );\n line();\n\n const willScan = (stepDirectories as { _runScan?: boolean })._runScan;\n\n if (willScan) {\n const startDaemon = await promptYesNo(\n rl,\n \"Start the PAI daemon now? (enables background indexing)\",\n true,\n );\n\n if (startDaemon) {\n line();\n console.log(c.dim(\" Starting daemon...\"));\n\n try {\n const result = spawnSync(\"pai\", [\"daemon\", \"serve\", \"--background\"], {\n stdio: \"pipe\",\n timeout: 10000,\n });\n\n if (result.status === 0) {\n console.log(c.ok(\"Daemon started in background.\"));\n } else {\n console.log(c.warn(\"Could not start daemon. Run manually: pai daemon serve\"));\n }\n } catch {\n console.log(c.warn(\"Could not start daemon. Run manually: pai daemon serve\"));\n }\n\n line();\n console.log(c.dim(\" Running registry scan to detect projects...\"));\n\n try {\n const result = spawnSync(\"pai\", [\"registry\", \"scan\"], {\n stdio: \"inherit\",\n timeout: 30000,\n });\n\n if (result.status !== 0) {\n console.log(c.warn(\"Registry scan encountered issues. Run `pai registry scan` manually.\"));\n }\n } catch {\n console.log(c.warn(\"Could not run registry scan. Run manually: pai registry scan\"));\n }\n } else {\n console.log(c.dim(\" Start the daemon later: pai daemon serve\"));\n console.log(c.dim(\" Scan projects later: pai registry scan\"));\n }\n } else {\n console.log(c.dim(\" Register projects with: pai project add <path>\"));\n console.log(c.dim(\" Then index them with: pai memory index --all\"));\n console.log(c.dim(\" Or start the daemon: pai daemon serve\"));\n }\n}\n\n/**\n * Step 7: Summary and next steps\n */\nfunction stepSummary(configUpdates: Record<string, unknown>, claudeMdGenerated: boolean): void {\n section(\"Setup Complete\");\n line();\n console.log(c.ok(\"PAI Knowledge OS is configured!\"));\n line();\n\n // Show what was configured\n const backend = configUpdates.storageBackend as string;\n const model = configUpdates.embeddingModel as string;\n\n line(chalk.bold(\" Configuration saved to: \") + chalk.dim(CONFIG_FILE));\n line();\n console.log(chalk.dim(\" Storage backend: \") + chalk.cyan(backend ?? \"sqlite\"));\n console.log(\n chalk.dim(\" Embedding model: \") +\n chalk.cyan(model && model !== \"none\" ? model : \"(none — keyword search only)\"),\n );\n console.log(\n chalk.dim(\" Agent config: \") +\n chalk.cyan(claudeMdGenerated ? \"~/.claude/CLAUDE.md (generated)\" : \"(unchanged)\"),\n );\n line();\n\n line(chalk.bold(\" Next steps:\"));\n line();\n console.log(chalk.dim(\" # Register a project\"));\n console.log(chalk.cyan(\" pai project add ~/your/project\"));\n line();\n console.log(chalk.dim(\" # Index your files\"));\n console.log(chalk.cyan(\" pai memory index --all\"));\n line();\n console.log(chalk.dim(\" # Search your knowledge\"));\n console.log(chalk.cyan(\" pai memory search \\\"your query\\\"\"));\n line();\n if (model && model !== \"none\") {\n console.log(chalk.dim(\" # Generate embeddings for semantic search\"));\n console.log(chalk.cyan(\" pai memory embed\"));\n line();\n console.log(chalk.dim(\" # Semantic search\"));\n console.log(chalk.cyan(\" pai memory search --mode semantic \\\"your query\\\"\"));\n line();\n }\n console.log(chalk.dim(\" # Start the background daemon\"));\n console.log(chalk.cyan(\" pai daemon serve\"));\n line();\n console.log(chalk.dim(\" # Show all commands\"));\n console.log(chalk.cyan(\" pai --help\"));\n line();\n}\n\n// ---------------------------------------------------------------------------\n// Main setup action\n// ---------------------------------------------------------------------------\n\nasync function runSetup(): Promise<void> {\n const rl = createRl();\n\n try {\n // Check if already configured\n if (existsSync(CONFIG_FILE)) {\n const current = loadConfig();\n line();\n console.log(\n chalk.yellow(\" Note: PAI is already configured.\") +\n chalk.dim(\" Proceeding will update your existing configuration.\"),\n );\n console.log(chalk.dim(` Config: ${CONFIG_FILE}`));\n console.log(chalk.dim(` Current backend: ${current.storageBackend}`));\n line();\n\n const proceed = await promptYesNo(\n rl,\n \"Continue and update configuration?\",\n true,\n );\n\n if (!proceed) {\n rl.close();\n line(c.dim(\" Setup cancelled.\"));\n line();\n return;\n }\n }\n\n // Step 1: Welcome\n stepWelcome();\n line();\n await prompt(rl, chalk.dim(\" Press Enter to begin setup...\"));\n\n // Step 2: Storage\n section(\"Step 2: Storage Backend\");\n const storageConfig = await stepStorage(rl);\n\n // Step 3: Embeddings\n const embeddingConfig = await stepEmbedding(rl);\n\n // Step 4: Agent configuration (CLAUDE.md)\n const claudeMdGenerated = await stepClaudeMd(rl);\n\n // Step 5: Directories (informational — no config written)\n await stepDirectories(rl);\n\n // Write config after gathering all choices\n const allUpdates = { ...storageConfig, ...embeddingConfig };\n mergeConfig(allUpdates);\n\n line();\n console.log(c.ok(\"Configuration saved.\"));\n\n // Step 6: Initial index\n await stepInitialIndex(rl);\n\n // Step 7: Summary\n stepSummary(allUpdates, claudeMdGenerated);\n\n } finally {\n rl.close();\n }\n}\n\n// ---------------------------------------------------------------------------\n// Commander registration\n// ---------------------------------------------------------------------------\n\nexport function registerSetupCommand(program: Command): void {\n program\n .command(\"setup\")\n .description(\n \"Interactive setup wizard — configure storage, embeddings, agent config, and indexing\",\n )\n .action(async () => {\n await runSetup();\n });\n}\n","/**\n * obsidian/sync.ts — Core vault synchronisation for PAI Phase 4\n *\n * Functions:\n * - syncVault(vaultPath, db) — symlink project Notes/ dirs into vault\n * - generateIndex(vaultPath, db) — write _index.md with project table\n * - generateTopicPages(vaultPath, db) — write _topics/{tag}.md pages\n * - defaultVaultPath() — ~/.pai/obsidian-vault\n *\n * Vault structure (per project):\n *\n * {vault}/{slug}/ ← real directory\n * notes → {root}/Notes/ (project root notes, if any)\n * sessions → ~/.claude/projects/{encoded}/Notes/ (Claude Code session notes, if any)\n *\n * Both sub-symlinks are optional; projects with only one source get only that link.\n * Archived projects get a stub .md file in {vault}/_archive/.\n */\n\nimport type { Database } from \"better-sqlite3\";\nimport {\n existsSync,\n mkdirSync,\n symlinkSync,\n readdirSync,\n lstatSync,\n unlinkSync,\n readlinkSync,\n writeFileSync,\n readFileSync,\n rmdirSync,\n} from \"node:fs\";\nimport { join, relative, dirname } from \"node:path\";\nimport { homedir } from \"node:os\";\nimport { fmtDate } from \"../cli/utils.js\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\ninterface ProjectRow {\n id: number;\n slug: string;\n display_name: string;\n root_path: string;\n encoded_dir: string;\n status: string;\n obsidian_link: string | null;\n claude_notes_dir: string | null;\n}\n\ninterface SessionStats {\n session_count: number;\n last_active: number | null;\n}\n\ninterface TagRow {\n name: string;\n}\n\nexport interface SyncStats {\n created: number;\n updated: number;\n removed: number;\n stubbed: number;\n errors: string[];\n}\n\n// ---------------------------------------------------------------------------\n// Internal helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Find the project-root Notes directory for a project.\n * Checks canonical location first, then .claude/Notes.\n */\nfunction findNotesDir(rootPath: string): string | null {\n const canonical = join(rootPath, \"Notes\");\n if (existsSync(canonical)) return canonical;\n const alt = join(rootPath, \".claude\", \"Notes\");\n if (existsSync(alt)) return alt;\n return null;\n}\n\n/**\n * Find the Claude Code session notes directory from the registry-stored value.\n * Returns null if not set or does not exist on disk.\n * Skips the dir if it is identical to the project-root notesDir (avoids double-linking).\n */\nfunction findClaudeNotesDir(\n claudeNotesDirFromRegistry: string | null,\n notesDir: string | null\n): string | null {\n if (!claudeNotesDirFromRegistry) return null;\n if (!existsSync(claudeNotesDirFromRegistry)) return null;\n // Avoid creating a duplicate link when claude_notes_dir IS the project notes dir\n if (notesDir && claudeNotesDirFromRegistry === notesDir) return null;\n return claudeNotesDirFromRegistry;\n}\n\n/**\n * Check whether a path exists via lstat (does not follow symlinks).\n */\nfunction lstatExists(p: string): boolean {\n try {\n lstatSync(p);\n return true;\n } catch {\n return false;\n }\n}\n\n/**\n * Resolve slug collisions by appending -2, -3, etc.\n */\nfunction uniqueSlug(base: string, taken: Set<string>): string {\n if (!taken.has(base)) return base;\n let n = 2;\n while (taken.has(`${base}-${n}`)) n++;\n return `${base}-${n}`;\n}\n\n/**\n * Remove broken symlinks from a directory (one level deep).\n * Returns count of entries removed.\n */\nfunction cleanBrokenSymlinks(dir: string): number {\n let removed = 0;\n if (!existsSync(dir)) return removed;\n for (const entry of readdirSync(dir)) {\n const full = join(dir, entry);\n try {\n const stat = lstatSync(full);\n if (stat.isSymbolicLink()) {\n const target = readlinkSync(full);\n if (!existsSync(target)) {\n unlinkSync(full);\n removed++;\n }\n }\n } catch {\n // Skip unreadable entries\n }\n }\n return removed;\n}\n\n/**\n * Ensure a sub-symlink inside a project directory is correct.\n *\n * If the symlink already points to the right target, nothing changes.\n * If it points somewhere else, it is removed and recreated.\n * If the path is a non-symlink, it is left alone (data-loss prevention).\n *\n * @returns true if a new symlink was created, false otherwise.\n */\nfunction ensureSubSymlink(\n linkPath: string,\n target: string,\n errors: string[],\n label: string\n): boolean {\n if (lstatExists(linkPath)) {\n try {\n const stat = lstatSync(linkPath);\n if (stat.isSymbolicLink()) {\n const current = readlinkSync(linkPath);\n if (current === target) {\n return false; // Already correct\n }\n unlinkSync(linkPath);\n } else {\n // Real file or dir at this path — skip to avoid data loss\n errors.push(`${label}: path exists but is not a symlink — skipped`);\n return false;\n }\n } catch (e) {\n errors.push(`${label}: error checking existing path — ${e}`);\n return false;\n }\n }\n\n try {\n symlinkSync(target, linkPath);\n return true;\n } catch (e) {\n errors.push(`${label}: symlink creation failed — ${e}`);\n return false;\n }\n}\n\n/**\n * Migrate a legacy flat symlink at `slugPath` to a real directory.\n *\n * The old structure was: {vault}/{slug} → {notesDir}\n * The new structure is: {vault}/{slug}/ (real dir)\n * notes → {notesDir}\n * sessions → {claudeNotesDir}\n *\n * If `slugPath` is already a real directory, this is a no-op.\n * If it is a symlink (legacy), it is removed so `mkdirSync` can create the dir.\n */\nfunction migrateToProjectDir(\n slugPath: string,\n errors: string[],\n slug: string\n): boolean {\n if (!lstatExists(slugPath)) {\n return true; // Does not exist yet — mkdirSync will create it\n }\n\n try {\n const stat = lstatSync(slugPath);\n\n if (stat.isDirectory()) {\n return true; // Already a real directory — nothing to migrate\n }\n\n if (stat.isSymbolicLink()) {\n unlinkSync(slugPath);\n return true; // Removed old symlink — now ready for mkdirSync\n }\n\n // Some other filesystem object (file, etc.) — cannot proceed\n errors.push(`${slug}: path exists as non-directory, non-symlink — skipped`);\n return false;\n } catch (e) {\n errors.push(`${slug}: error during migration — ${e}`);\n return false;\n }\n}\n\n/**\n * Remove a project directory from the vault if it is empty (all sub-symlinks gone).\n * Silently ignores errors (non-fatal cleanup).\n */\nfunction removeProjectDirIfEmpty(slugPath: string): void {\n try {\n const entries = readdirSync(slugPath);\n if (entries.length === 0) {\n rmdirSync(slugPath);\n }\n } catch {\n // Non-fatal\n }\n}\n\n// ---------------------------------------------------------------------------\n// Public API\n// ---------------------------------------------------------------------------\n\n/**\n * Sync all active project Notes directories into the Obsidian vault.\n *\n * For each active project that has at least one Notes source, creates:\n *\n * {vault}/{slug}/ — real directory\n * notes → {root}/Notes/ (if project root Notes exists)\n * sessions → ~/.claude/projects/{enc}/Notes/ (if Claude Code session notes exist)\n *\n * Projects with neither source are skipped.\n * Archived projects get a stub markdown file in {vault}/_archive/.\n */\nexport function syncVault(vaultPath: string, db: Database): SyncStats {\n const stats: SyncStats = { created: 0, updated: 0, removed: 0, stubbed: 0, errors: [] };\n\n // Ensure vault root exists\n mkdirSync(vaultPath, { recursive: true });\n\n // Clean up broken symlinks at the vault root level (handles any leftover flat symlinks\n // that are now broken, e.g. from deleted projects or moved notes dirs)\n stats.removed += cleanBrokenSymlinks(vaultPath);\n\n const projects = db\n .prepare(\n `SELECT id, slug, display_name, root_path, encoded_dir, status, obsidian_link, claude_notes_dir\n FROM projects\n ORDER BY status ASC, slug ASC`\n )\n .all() as ProjectRow[];\n\n const takenSlugs = new Set<string>();\n\n for (const project of projects) {\n if (project.status === \"active\") {\n const notesDir = findNotesDir(project.root_path);\n const claudeNotesDir = findClaudeNotesDir(project.claude_notes_dir, notesDir);\n\n // Skip projects with no notes sources at all\n if (!notesDir && !claudeNotesDir) {\n continue;\n }\n\n const slug = uniqueSlug(project.slug, takenSlugs);\n takenSlugs.add(slug);\n const slugPath = join(vaultPath, slug);\n\n // Migrate from legacy flat symlink to real directory\n if (!migrateToProjectDir(slugPath, stats.errors, slug)) {\n continue;\n }\n\n // Ensure the project directory exists\n try {\n mkdirSync(slugPath, { recursive: true });\n } catch (e) {\n stats.errors.push(`${slug}: failed to create project directory — ${e}`);\n continue;\n }\n\n // Create/verify the `notes` sub-symlink\n if (notesDir) {\n const notesLink = join(slugPath, \"notes\");\n const created = ensureSubSymlink(notesLink, notesDir, stats.errors, `${slug}/notes`);\n if (created) {\n stats.created++;\n } else {\n stats.updated++;\n }\n }\n\n // Create/verify the `sessions` sub-symlink\n if (claudeNotesDir) {\n const sessionsLink = join(slugPath, \"sessions\");\n const created = ensureSubSymlink(\n sessionsLink,\n claudeNotesDir,\n stats.errors,\n `${slug}/sessions`\n );\n if (created) {\n stats.created++;\n } else {\n stats.updated++;\n }\n }\n\n // Update obsidian_link in registry to point to the project directory\n try {\n db.prepare(\"UPDATE projects SET obsidian_link = ?, updated_at = ? WHERE id = ?\").run(\n slugPath,\n Date.now(),\n project.id\n );\n } catch {\n // Non-fatal\n }\n } else if (project.status === \"archived\") {\n // Write stub file in _archive/\n const archiveDir = join(vaultPath, \"_archive\");\n mkdirSync(archiveDir, { recursive: true });\n const stubPath = join(archiveDir, `${project.slug}.md`);\n if (!existsSync(stubPath)) {\n const content = [\n `# ${project.display_name}`,\n \"\",\n \"> Archived project — no live notes available.\",\n \"\",\n `- **Slug:** ${project.slug}`,\n `- **Root:** ${project.root_path}`,\n \"\",\n ].join(\"\\n\");\n try {\n writeFileSync(stubPath, content, \"utf-8\");\n stats.stubbed++;\n } catch (e) {\n stats.errors.push(`${project.slug} (archive stub): ${e}`);\n }\n }\n }\n }\n\n return stats;\n}\n\n/**\n * Generate _index.md listing all projects with session counts, tags, and\n * indicators for which note sources are available (notes, sessions, or both).\n */\nexport function generateIndex(vaultPath: string, db: Database): void {\n mkdirSync(vaultPath, { recursive: true });\n\n const rows = db\n .prepare(\n `SELECT p.id, p.slug, p.display_name, p.status, p.root_path,\n p.encoded_dir, p.claude_notes_dir,\n (SELECT COUNT(*) FROM sessions s WHERE s.project_id = p.id) AS session_count,\n (SELECT MAX(s.created_at) FROM sessions s WHERE s.project_id = p.id) AS last_active\n FROM projects p\n ORDER BY p.status ASC, p.updated_at DESC`\n )\n .all() as (ProjectRow & SessionStats)[];\n\n const getTagsForProject = db.prepare(\n `SELECT t.name FROM tags t\n JOIN project_tags pt ON pt.tag_id = t.id\n WHERE pt.project_id = ?\n ORDER BY t.name`\n );\n\n const active = rows.filter((r) => r.status === \"active\");\n const archived = rows.filter((r) => r.status !== \"active\");\n\n const lines: string[] = [\n \"# PAI Project Index\",\n \"\",\n `> Auto-generated by PAI Knowledge OS — ${new Date().toISOString()}`,\n \"\",\n \"## Active Projects\",\n \"\",\n \"| Project | Sessions | Last Active | Sources | Tags |\",\n \"| ------- | -------- | ----------- | ------- | ---- |\",\n ];\n\n for (const row of active) {\n const tags = (getTagsForProject.all(row.id) as TagRow[]).map((t) => `\\`${t.name}\\``).join(\" \");\n const lastActive = fmtDate(row.last_active);\n\n // Determine which note sources are available\n const notesDir = findNotesDir(row.root_path);\n const claudeNotesDir = findClaudeNotesDir(row.claude_notes_dir, notesDir);\n const sources: string[] = [];\n if (notesDir) sources.push(\"notes\");\n if (claudeNotesDir) sources.push(\"sessions\");\n const sourcesLabel = sources.length > 0 ? sources.join(\", \") : \"—\";\n\n lines.push(\n `| [[${row.slug}/|${row.display_name}]] | ${row.session_count} | ${lastActive} | ${sourcesLabel} | ${tags || \"—\"} |`\n );\n }\n\n if (archived.length > 0) {\n lines.push(\n \"\",\n \"## Archived Projects\",\n \"\",\n \"| Project | Sessions | Tags |\",\n \"| ------- | -------- | ---- |\"\n );\n for (const row of archived) {\n const tags = (getTagsForProject.all(row.id) as TagRow[]).map((t) => `\\`${t.name}\\``).join(\" \");\n lines.push(\n `| [${row.display_name}](_archive/${row.slug}.md) | ${row.session_count} | ${tags || \"—\"} |`\n );\n }\n }\n\n lines.push(\n \"\",\n \"---\",\n `*${rows.length} projects total — ${active.length} active, ${archived.length} archived*`\n );\n\n writeFileSync(join(vaultPath, \"_index.md\"), lines.join(\"\\n\") + \"\\n\", \"utf-8\");\n}\n\n/**\n * Generate per-tag topic pages at _topics/{tag}.md.\n * Returns count of pages written.\n */\nexport function generateTopicPages(vaultPath: string, db: Database): number {\n const topicsDir = join(vaultPath, \"_topics\");\n mkdirSync(topicsDir, { recursive: true });\n\n const allTags = db\n .prepare(\"SELECT id, name FROM tags ORDER BY name\")\n .all() as { id: number; name: string }[];\n\n const getProjectsForTag = db.prepare(\n `SELECT p.id, p.slug, p.display_name, p.status,\n (SELECT COUNT(*) FROM sessions s WHERE s.project_id = p.id) AS session_count,\n (SELECT MAX(s.created_at) FROM sessions s WHERE s.project_id = p.id) AS last_active\n FROM projects p\n JOIN project_tags pt ON pt.project_id = p.id\n WHERE pt.tag_id = ?\n ORDER BY p.status ASC, p.updated_at DESC`\n );\n\n let written = 0;\n for (const tag of allTags) {\n const projects = getProjectsForTag.all(tag.id) as (ProjectRow & SessionStats)[];\n if (!projects.length) continue;\n\n const lines: string[] = [\n `# Topic: ${tag.name}`,\n \"\",\n `> Auto-generated by PAI Knowledge OS — ${new Date().toISOString()}`,\n \"\",\n \"## Projects\",\n \"\",\n \"| Project | Status | Sessions | Last Active |\",\n \"| ------- | ------ | -------- | ----------- |\",\n ];\n\n for (const p of projects) {\n const link =\n p.status === \"active\"\n ? `[[${p.slug}/|${p.display_name}]]`\n : `[${p.display_name}](../_archive/${p.slug}.md)`;\n lines.push(`| ${link} | ${p.status} | ${p.session_count} | ${fmtDate(p.last_active)} |`);\n }\n\n lines.push(\"\", \"---\", `*${projects.length} project(s) tagged \\`${tag.name}\\`*`);\n\n writeFileSync(join(topicsDir, `${tag.name}.md`), lines.join(\"\\n\") + \"\\n\", \"utf-8\");\n written++;\n }\n\n return written;\n}\n\n/**\n * Default vault path: ~/.pai/obsidian-vault\n */\nexport function defaultVaultPath(): string {\n return join(homedir(), \".pai\", \"obsidian-vault\");\n}\n\n// ---------------------------------------------------------------------------\n// Master notes generation\n// ---------------------------------------------------------------------------\n\ninterface SessionFile {\n /** Absolute path on disk (inside the symlink target, resolved). */\n absPath: string;\n /** Relative path from the vault project dir (e.g. \"notes/2026/02/0001 - ...\"). */\n vaultRelPath: string;\n /** Wikilink target — relative to the vault project dir, no .md extension. */\n wikilinkTarget: string;\n /** YYYY/MM extracted from path or filename. */\n yearMonth: string;\n /** Basename without .md. */\n basename: string;\n}\n\nconst SESSION_FILENAME_RE = /^(\\d{4}) - (\\d{4}-\\d{2})-\\d{2} - .+\\.md$/;\n\n/** Build the per-project master note filename. */\nfunction masterFilename(slug: string): string {\n return `_${slug}-master.md`;\n}\n\n/**\n * Walk a directory (non-recursive, then one level of YYYY/MM subdirs).\n * Returns absolute paths to all .md files found.\n */\nfunction walkNotesDir(dir: string): string[] {\n const results: string[] = [];\n if (!existsSync(dir)) return results;\n\n let entries: string[];\n try {\n entries = readdirSync(dir);\n } catch {\n return results;\n }\n\n for (const entry of entries) {\n if (entry.startsWith(\".\")) continue;\n // Skip any master note file (pattern: _{slug}-master.md or legacy _master.md)\n if (entry === \"_master.md\" || /^_[^/]+-master\\.md$/.test(entry)) continue;\n const full = join(dir, entry);\n let stat: ReturnType<typeof lstatSync>;\n try {\n // Use stat (follow symlinks) for the dir check\n stat = lstatSync(full);\n } catch {\n continue;\n }\n if (stat.isDirectory()) {\n // Expect YYYY/MM pattern\n if (/^\\d{4}$/.test(entry)) {\n // Year directory — go one deeper\n let monthEntries: string[];\n try {\n monthEntries = readdirSync(full);\n } catch {\n continue;\n }\n for (const month of monthEntries) {\n const monthPath = join(full, month);\n if (/^\\d{2}$/.test(month) && existsSync(monthPath)) {\n let monthFiles: string[];\n try {\n monthFiles = readdirSync(monthPath);\n } catch {\n continue;\n }\n for (const f of monthFiles) {\n if (f.endsWith(\".md\") && !f.startsWith(\".\")) {\n results.push(join(monthPath, f));\n }\n }\n }\n }\n }\n } else if (entry.endsWith(\".md\")) {\n results.push(full);\n }\n }\n return results;\n}\n\n/**\n * Extract YYYY/MM from a session file path.\n * Tries the path first (looks for /YYYY/MM/ pattern), then falls back to filename date.\n */\nfunction extractYearMonth(filePath: string): string {\n const pathMatch = filePath.match(/\\/(\\d{4})\\/(\\d{2})\\//);\n if (pathMatch) return `${pathMatch[1]}/${pathMatch[2]}`;\n\n const basename = filePath.split(\"/\").pop() ?? \"\";\n const nameMatch = basename.match(/^\\d{4} - (\\d{4})-(\\d{2})-\\d{2}/);\n if (nameMatch) return `${nameMatch[1]}/${nameMatch[2]}`;\n\n return \"unknown\";\n}\n\n/**\n * Remove any old/broken backlink footer from a session file.\n * Matches the old [[../_master|...]] pattern as well as any [[_{slug}-master|...]] footer.\n * Returns true if the file was modified.\n */\nfunction removeOldBacklink(filePath: string): boolean {\n let content: string;\n try {\n content = readFileSync(filePath, \"utf-8\");\n } catch {\n return false;\n }\n // Remove old broken pattern: \\n---\\n[[../_master|...]]\n const oldPattern = /\\n---\\n\\[\\[\\.\\.\\/[^\\]]+\\]\\]\\n?$/;\n // Remove any existing PAI-generated master footer (new or old slug)\n const newPattern = /\\n---\\n\\[\\[_[^\\]]*-master\\|[^\\]]*\\]\\]\\n?$/;\n const cleaned = content.replace(oldPattern, \"\").replace(newPattern, \"\");\n if (cleaned === content) return false;\n try {\n writeFileSync(filePath, cleaned, \"utf-8\");\n return true;\n } catch {\n return false;\n }\n}\n\n/**\n * Append the master note backlink footer to a session file, idempotently.\n * Uses Obsidian filename-based wikilink (resolves from anywhere in the vault).\n * Only writes if the sentinel string is not already present in the file.\n */\nfunction appendBacklinkIfMissing(\n filePath: string,\n slug: string,\n displayName: string\n): boolean {\n let content: string;\n try {\n content = readFileSync(filePath, \"utf-8\");\n } catch {\n return false;\n }\n const masterName = masterFilename(slug).replace(/\\.md$/, \"\");\n const sentinel = `[[${masterName}|`;\n if (content.includes(sentinel)) return false;\n\n const footer = `\\n---\\n[[${masterName}|← ${displayName} Master]]\\n`;\n try {\n writeFileSync(filePath, content + footer, \"utf-8\");\n return true;\n } catch {\n return false;\n }\n}\n\n/**\n * Generate _master.md files for projects that have enough sessions.\n *\n * For each project directory in the vault that has >= threshold session files,\n * writes a {vaultPath}/{slug}/_master.md containing:\n * - Project title\n * - Session count + date range\n * - Sessions grouped by YYYY/MM with Obsidian [[wikilinks]]\n *\n * Also appends a backlink footer to each session file (idempotently).\n *\n * @param vaultPath Absolute path to the PAI Obsidian vault\n * @param db Registry SQLite database\n * @param threshold Minimum session count to generate a master note (default: 5)\n * @returns Number of master notes written\n */\nexport function generateMasterNotes(\n vaultPath: string,\n db: Database,\n threshold = 5\n): number {\n if (!existsSync(vaultPath)) return 0;\n\n const projects = db\n .prepare(\n `SELECT id, slug, display_name, root_path, encoded_dir, status, obsidian_link, claude_notes_dir\n FROM projects\n WHERE status = 'active'\n ORDER BY slug ASC`\n )\n .all() as ProjectRow[];\n\n let written = 0;\n\n for (const project of projects) {\n const slugPath = join(vaultPath, project.slug);\n if (!existsSync(slugPath)) continue;\n\n // Remove legacy _master.md if it exists (replaced by _{slug}-master.md)\n const legacyMaster = join(slugPath, \"_master.md\");\n if (existsSync(legacyMaster)) {\n try {\n unlinkSync(legacyMaster);\n } catch {\n // Non-fatal — leave it if we can't delete it\n }\n }\n\n // Collect all session .md files from both symlink sources\n const sessionFiles: SessionFile[] = [];\n\n // Sub-dirs to scan: \"notes\" and \"sessions\" symlinks inside the project dir\n const subLinks = [\"notes\", \"sessions\"];\n for (const subLink of subLinks) {\n const linkPath = join(slugPath, subLink);\n if (!existsSync(linkPath)) continue;\n\n // Resolve symlink target to get real path\n let realDir: string;\n try {\n const stat = lstatSync(linkPath);\n if (stat.isSymbolicLink()) {\n realDir = readlinkSync(linkPath);\n } else if (stat.isDirectory()) {\n realDir = linkPath;\n } else {\n continue;\n }\n } catch {\n continue;\n }\n\n const files = walkNotesDir(realDir);\n for (const absPath of files) {\n const basename = absPath.split(\"/\").pop() ?? \"\";\n if (!SESSION_FILENAME_RE.test(basename)) continue;\n\n // Build the vault-relative wikilink path\n // The file lives under realDir; the link is at slugPath/subLink\n const relFromReal = relative(realDir, absPath);\n const vaultRelPath = `${subLink}/${relFromReal}`;\n // Wikilink: strip .md extension, use path relative to project dir\n const wikilinkTarget = vaultRelPath.replace(/\\.md$/, \"\");\n\n sessionFiles.push({\n absPath,\n vaultRelPath,\n wikilinkTarget,\n yearMonth: extractYearMonth(absPath),\n basename: basename.replace(/\\.md$/, \"\"),\n });\n }\n }\n\n if (sessionFiles.length < threshold) continue;\n\n // Sort by yearMonth then basename (filename order within month)\n sessionFiles.sort((a, b) => {\n if (a.yearMonth !== b.yearMonth) return a.yearMonth.localeCompare(b.yearMonth);\n return a.basename.localeCompare(b.basename);\n });\n\n // Group by yearMonth\n const byMonth = new Map<string, SessionFile[]>();\n for (const sf of sessionFiles) {\n if (!byMonth.has(sf.yearMonth)) byMonth.set(sf.yearMonth, []);\n byMonth.get(sf.yearMonth)!.push(sf);\n }\n\n // Determine date range\n const firstDate = sessionFiles[0]?.basename.match(/\\d{4}-\\d{2}-\\d{2}/)?.[0] ?? \"unknown\";\n const lastDate =\n sessionFiles[sessionFiles.length - 1]?.basename.match(/\\d{4}-\\d{2}-\\d{2}/)?.[0] ?? \"unknown\";\n\n // Build master note content\n const lines: string[] = [\n `# Project: ${project.display_name}`,\n \"\",\n `> Auto-generated by PAI Knowledge OS — ${new Date().toISOString()}`,\n \"\",\n \"## Overview\",\n \"\",\n `- **Sessions:** ${sessionFiles.length}`,\n `- **Date range:** ${firstDate} to ${lastDate}`,\n `- **Tags:** #${project.slug} #project`,\n \"\",\n \"## Sessions by Month\",\n \"\",\n ];\n\n for (const [ym, files] of byMonth) {\n lines.push(`### ${ym}`, \"\");\n for (const sf of files) {\n lines.push(`- [[${sf.wikilinkTarget}|${sf.basename}]]`);\n }\n lines.push(\"\");\n }\n\n lines.push(\"## Topics\", \"\", `#${project.slug} #project`, \"\");\n\n const masterPath = join(slugPath, masterFilename(project.slug));\n try {\n writeFileSync(masterPath, lines.join(\"\\n\"), \"utf-8\");\n written++;\n } catch {\n continue;\n }\n\n // Remove old broken backlinks, then append correct ones (idempotent)\n for (const sf of sessionFiles) {\n removeOldBacklink(sf.absPath);\n appendBacklinkIfMissing(sf.absPath, project.slug, project.display_name);\n }\n }\n\n return written;\n}\n\n// ---------------------------------------------------------------------------\n// Fix session tags\n// ---------------------------------------------------------------------------\n\n/**\n * Remove the generic #Session tag from session note files across all projects.\n *\n * Session notes are written with `**Tags:** #Session ...` but the generic #Session\n * tag is not useful — each note already belongs to a project. This function scans\n * all session and notes directories for every active project and removes #Session\n * (with or without a trailing space) from any `**Tags:**` line.\n *\n * The project-specific tags that follow #Session are preserved untouched.\n *\n * @param db Registry SQLite database\n * @returns Object with counts: { filesScanned, filesModified, errors }\n */\nexport function fixSessionTags(\n db: Database\n): { filesScanned: number; filesModified: number; errors: string[] } {\n const results = { filesScanned: 0, filesModified: 0, errors: [] as string[] };\n\n const projects = db\n .prepare(\n `SELECT id, slug, display_name, root_path, encoded_dir, status, obsidian_link, claude_notes_dir\n FROM projects\n WHERE status = 'active'\n ORDER BY slug ASC`\n )\n .all() as ProjectRow[];\n\n for (const project of projects) {\n // Collect all directories to scan for this project\n const dirsToScan: string[] = [];\n\n const notesDir = findNotesDir(project.root_path);\n if (notesDir) dirsToScan.push(notesDir);\n\n if (\n project.claude_notes_dir &&\n existsSync(project.claude_notes_dir) &&\n project.claude_notes_dir !== notesDir\n ) {\n dirsToScan.push(project.claude_notes_dir);\n }\n\n for (const dir of dirsToScan) {\n const files = walkNotesDir(dir);\n for (const filePath of files) {\n results.filesScanned++;\n let content: string;\n try {\n content = readFileSync(filePath, \"utf-8\");\n } catch (e) {\n results.errors.push(`${filePath}: read error — ${e}`);\n continue;\n }\n\n if (!content.includes(\"#Session\")) continue;\n\n // Remove #Session followed by optional space on any **Tags:** line\n const updated = content.replace(\n /(\\*\\*Tags:\\*\\*[^\\n]*)#Session ?/g,\n \"$1\"\n );\n\n if (updated === content) continue;\n\n // Clean up trailing whitespace on Tags lines that may be left empty\n const cleaned = updated.replace(/(\\*\\*Tags:\\*\\*) *\\n/g, \"$1\\n\");\n\n try {\n writeFileSync(filePath, cleaned, \"utf-8\");\n results.filesModified++;\n } catch (e) {\n results.errors.push(`${filePath}: write error — ${e}`);\n }\n }\n }\n }\n\n return results;\n}\n","/**\n * obsidian/status.ts — Vault health checking for PAI Phase 4\n *\n * Vault structure (per project):\n *\n * {vault}/{slug}/ — real directory\n * notes → {root}/Notes/ (project root notes, optional)\n * sessions → ~/.claude/projects/{enc}/Notes/ (Claude Code session notes, optional)\n */\n\nimport type { Database } from \"better-sqlite3\";\nimport { existsSync, readdirSync, lstatSync, readlinkSync } from \"node:fs\";\nimport { join } from \"node:path\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\ninterface ProjectRow {\n id: number;\n slug: string;\n display_name: string;\n root_path: string;\n status: string;\n claude_notes_dir: string | null;\n obsidian_link: string | null;\n}\n\nexport interface SymlinkHealth {\n slug: string;\n linkPath: string;\n target: string | null;\n state: \"healthy\" | \"broken\" | \"orphaned\" | \"missing\";\n notes: string;\n}\n\nexport interface VaultHealthReport {\n vaultPath: string;\n healthy: SymlinkHealth[];\n broken: SymlinkHealth[];\n orphaned: SymlinkHealth[];\n missing: SymlinkHealth[];\n totalProjects: number;\n}\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Check the health of a sub-symlink inside a project directory.\n * Returns true if the symlink exists and its target exists on disk.\n */\nfunction subSymlinkHealthy(dir: string, name: string): boolean {\n const linkPath = join(dir, name);\n try {\n const stat = lstatSync(linkPath);\n if (!stat.isSymbolicLink()) return false;\n const target = readlinkSync(linkPath);\n return existsSync(target);\n } catch {\n return false;\n }\n}\n\n/**\n * Check if a sub-symlink exists (regardless of target validity).\n */\nfunction subSymlinkExists(dir: string, name: string): boolean {\n try {\n lstatSync(join(dir, name));\n return true;\n } catch {\n return false;\n }\n}\n\n/**\n * Find a project root Notes dir (checks canonical and .claude/Notes fallback).\n */\nfunction findNotesDir(rootPath: string): string | null {\n const canonical = join(rootPath, \"Notes\");\n if (existsSync(canonical)) return canonical;\n const alt = join(rootPath, \".claude\", \"Notes\");\n if (existsSync(alt)) return alt;\n return null;\n}\n\n// ---------------------------------------------------------------------------\n// Public API\n// ---------------------------------------------------------------------------\n\n/**\n * Check health of the Obsidian vault:\n *\n * Project entries are now real directories with sub-symlinks (`notes`, `sessions`).\n *\n * - healthy: project dir exists; at least one sub-symlink is present and resolves\n * - broken: project dir exists but all sub-symlinks have missing targets\n * - orphaned: directory entry in vault has no matching registry project\n * - missing: active project with at least one Notes source but no vault dir\n */\nexport function checkHealth(vaultPath: string, db: Database): VaultHealthReport {\n const report: VaultHealthReport = {\n vaultPath,\n healthy: [],\n broken: [],\n orphaned: [],\n missing: [],\n totalProjects: 0,\n };\n\n if (!existsSync(vaultPath)) {\n return report;\n }\n\n const projects = db\n .prepare(\n `SELECT id, slug, display_name, root_path, status, claude_notes_dir, obsidian_link\n FROM projects WHERE status = 'active'`\n )\n .all() as ProjectRow[];\n\n report.totalProjects = projects.length;\n\n // Build a map of slug → project for quick lookup\n const projectsBySlug = new Map<string, ProjectRow>();\n for (const p of projects) {\n projectsBySlug.set(p.slug, p);\n }\n\n // Scan vault directory\n const seenSlugs = new Set<string>();\n let entries: string[] = [];\n try {\n entries = readdirSync(vaultPath);\n } catch {\n return report;\n }\n\n for (const entry of entries) {\n // Skip internal PAI-generated dirs/files\n if (entry.startsWith(\"_\")) continue;\n\n const fullPath = join(vaultPath, entry);\n let stat;\n try {\n stat = lstatSync(fullPath);\n } catch {\n continue;\n }\n\n const project = projectsBySlug.get(entry);\n\n if (stat.isDirectory()) {\n // New-style project directory\n if (!project) {\n // Directory in vault with no matching registry project\n report.orphaned.push({\n slug: entry,\n linkPath: fullPath,\n target: null,\n state: \"orphaned\",\n notes: \"No matching active project in registry (directory)\",\n });\n continue;\n }\n\n seenSlugs.add(entry);\n\n const notesOk = subSymlinkHealthy(fullPath, \"notes\");\n const sessionsOk = subSymlinkHealthy(fullPath, \"sessions\");\n const notesExists = subSymlinkExists(fullPath, \"notes\");\n const sessionsExists = subSymlinkExists(fullPath, \"sessions\");\n\n if (notesOk || sessionsOk) {\n const parts: string[] = [];\n if (notesOk) parts.push(\"notes\");\n if (sessionsOk) parts.push(\"sessions\");\n report.healthy.push({\n slug: entry,\n linkPath: fullPath,\n target: null,\n state: \"healthy\",\n notes: `Sources: ${parts.join(\", \")}`,\n });\n } else {\n // At least one sub-link exists but none resolves\n const broken: string[] = [];\n if (notesExists) broken.push(\"notes\");\n if (sessionsExists) broken.push(\"sessions\");\n report.broken.push({\n slug: entry,\n linkPath: fullPath,\n target: null,\n state: \"broken\",\n notes: `Broken sub-symlinks: ${broken.join(\", \") || \"(empty directory)\"}`,\n });\n }\n } else if (stat.isSymbolicLink()) {\n // Legacy flat symlink — treat same as before\n let target: string | null = null;\n try {\n target = readlinkSync(fullPath);\n } catch {\n // Can't read symlink\n }\n\n const targetExists = target !== null && existsSync(target);\n\n if (!project) {\n report.orphaned.push({\n slug: entry,\n linkPath: fullPath,\n target,\n state: \"orphaned\",\n notes: \"No matching active project in registry (legacy symlink)\",\n });\n continue;\n }\n\n seenSlugs.add(entry);\n\n if (targetExists) {\n report.healthy.push({\n slug: entry,\n linkPath: fullPath,\n target,\n state: \"healthy\",\n notes: \"(legacy flat symlink — run sync to upgrade)\",\n });\n } else {\n report.broken.push({\n slug: entry,\n linkPath: fullPath,\n target,\n state: \"broken\",\n notes: `Target missing: ${target ?? \"(unknown)\"}`,\n });\n }\n }\n // Non-symlink, non-directory entries are ignored (e.g. stray files)\n }\n\n // Find active projects with Notes/ dirs that have no vault entry\n for (const project of projects) {\n if (seenSlugs.has(project.slug)) continue;\n\n const notesCanonical = join(project.root_path, \"Notes\");\n const notesAlt = join(project.root_path, \".claude\", \"Notes\");\n const hasProjectNotes = existsSync(notesCanonical) || existsSync(notesAlt);\n const hasClaudeNotes =\n project.claude_notes_dir !== null && existsSync(project.claude_notes_dir);\n\n if (hasProjectNotes || hasClaudeNotes) {\n const sources: string[] = [];\n if (hasProjectNotes) sources.push(\"notes\");\n if (hasClaudeNotes) sources.push(\"sessions\");\n report.missing.push({\n slug: project.slug,\n linkPath: join(vaultPath, project.slug),\n target: null,\n state: \"missing\",\n notes: `Has sources (${sources.join(\", \")}) but no vault entry — run 'pai obsidian sync'`,\n });\n }\n }\n\n return report;\n}\n","/**\n * pai obsidian <sub-command>\n *\n * sync — symlink project Notes/ dirs into vault, write _index.md + _topics/\n * status — health report: healthy, broken, orphaned, missing symlinks\n * open — open vault in Obsidian app (macOS)\n */\n\nimport type { Command } from \"commander\";\nimport type { Database } from \"better-sqlite3\";\nimport { execSync } from \"node:child_process\";\nimport { ok, warn, err, dim, bold, header } from \"../utils.js\";\nimport {\n syncVault,\n generateIndex,\n generateTopicPages,\n generateMasterNotes,\n fixSessionTags,\n defaultVaultPath,\n} from \"../../obsidian/sync.js\";\nimport { checkHealth } from \"../../obsidian/status.js\";\nimport {\n loadConfig,\n expandHome,\n CONFIG_FILE,\n} from \"../../daemon/config.js\";\nimport { readFileSync, writeFileSync, existsSync, mkdirSync } from \"node:fs\";\nimport { dirname } from \"node:path\";\nimport chalk from \"chalk\";\n\n// ---------------------------------------------------------------------------\n// Config helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Read obsidianVaultPath from ~/.config/pai/config.json.\n * Falls back to defaultVaultPath() if not set.\n */\nfunction getVaultPath(override?: string): string {\n if (override) return expandHome(override);\n\n try {\n const raw = readFileSync(CONFIG_FILE, \"utf-8\");\n const cfg = JSON.parse(raw) as Record<string, unknown>;\n if (typeof cfg.obsidianVaultPath === \"string\" && cfg.obsidianVaultPath) {\n return expandHome(cfg.obsidianVaultPath);\n }\n } catch {\n // Config missing or unreadable — use default\n }\n return defaultVaultPath();\n}\n\n/**\n * Persist obsidianVaultPath into config.json so future syncs use the same path.\n */\nfunction saveVaultPath(vaultPath: string): void {\n let cfg: Record<string, unknown> = {};\n try {\n cfg = JSON.parse(readFileSync(CONFIG_FILE, \"utf-8\")) as Record<string, unknown>;\n } catch {\n // Start fresh if file is missing/corrupt\n }\n cfg.obsidianVaultPath = vaultPath;\n mkdirSync(dirname(CONFIG_FILE), { recursive: true });\n writeFileSync(CONFIG_FILE, JSON.stringify(cfg, null, 2) + \"\\n\", \"utf-8\");\n}\n\n// ---------------------------------------------------------------------------\n// Command implementations\n// ---------------------------------------------------------------------------\n\nfunction cmdSync(\n db: Database,\n opts: { vault?: string; quiet?: boolean }\n): void {\n const vaultPath = getVaultPath(opts.vault);\n const q = opts.quiet ?? false;\n\n if (!q) {\n console.log();\n console.log(header(\" PAI Obsidian Sync\"));\n console.log(dim(` Vault: ${vaultPath}`));\n console.log();\n }\n\n // --- Symlinks ---\n if (!q) process.stdout.write(\" Syncing symlinks...\");\n const stats = syncVault(vaultPath, db);\n if (!q) {\n process.stdout.write(\n `\\r ${ok(\"Symlinks:\")} created ${stats.created} updated ${stats.updated} removed ${stats.removed} stubs ${stats.stubbed}\\n`\n );\n if (stats.errors.length) {\n for (const e of stats.errors) {\n console.log(warn(` Warning: ${e}`));\n }\n }\n }\n\n // --- Index ---\n if (!q) process.stdout.write(\" Generating index...\");\n generateIndex(vaultPath, db);\n if (!q) console.log(`\\r ${ok(\"Index:\")} _index.md written `);\n\n // --- Topic pages ---\n if (!q) process.stdout.write(\" Generating topic pages...\");\n const topicCount = generateTopicPages(vaultPath, db);\n if (!q) console.log(`\\r ${ok(\"Topics:\")} ${topicCount} topic page(s) written `);\n\n // --- Fix session tags (remove generic #Session) ---\n if (!q) process.stdout.write(\" Fixing session tags...\");\n const tagStats = fixSessionTags(db);\n if (!q) {\n console.log(\n `\\r ${ok(\"Tags:\")} ${tagStats.filesModified} file(s) updated (scanned ${tagStats.filesScanned}) `\n );\n if (tagStats.errors.length) {\n for (const e of tagStats.errors) {\n console.log(warn(` Warning: ${e}`));\n }\n }\n }\n\n // --- Master notes ---\n if (!q) process.stdout.write(\" Generating master notes...\");\n const masterCount = generateMasterNotes(vaultPath, db);\n if (!q) console.log(`\\r ${ok(\"Masters:\")} ${masterCount} master note(s) written `);\n\n // Persist vault path so status/open can use it\n saveVaultPath(vaultPath);\n\n if (!q) {\n console.log();\n console.log(ok(` Done. Vault ready at: ${vaultPath}`));\n console.log();\n }\n}\n\nfunction cmdStatus(db: Database, opts: { vault?: string }): void {\n const vaultPath = getVaultPath(opts.vault);\n const report = checkHealth(vaultPath, db);\n\n console.log();\n console.log(header(\" PAI Obsidian Vault Status\"));\n console.log(dim(` Vault: ${vaultPath}`));\n console.log();\n\n if (!existsSync(vaultPath)) {\n console.log(warn(\" Vault directory does not exist. Run: pai obsidian sync\"));\n console.log();\n return;\n }\n\n // Summary line\n const healthyCount = report.healthy.length;\n const brokenCount = report.broken.length;\n const orphanedCount = report.orphaned.length;\n const missingCount = report.missing.length;\n\n console.log(\n ` ${chalk.green(\"Healthy:\")} ${healthyCount} ` +\n `${chalk.red(\"Broken:\")} ${brokenCount} ` +\n `${chalk.yellow(\"Orphaned:\")} ${orphanedCount} ` +\n `${chalk.cyan(\"Missing:\")} ${missingCount}`\n );\n console.log();\n\n if (report.healthy.length && report.healthy.length <= 10) {\n console.log(bold(\" Healthy symlinks:\"));\n for (const h of report.healthy) {\n console.log(` ${chalk.green(\"✓\")} ${bold(h.slug)} ${dim(\"→ \" + (h.target ?? \"\"))}`);\n }\n console.log();\n } else if (report.healthy.length > 10) {\n console.log(bold(` ${report.healthy.length} healthy symlinks (all good)`));\n console.log();\n }\n\n if (report.broken.length) {\n console.log(err(\" Broken symlinks (target missing):\"));\n for (const b of report.broken) {\n console.log(` ${chalk.red(\"✗\")} ${bold(b.slug)} ${dim(b.notes)}`);\n }\n console.log(dim(\" Fix: pai obsidian sync\"));\n console.log();\n }\n\n if (report.orphaned.length) {\n console.log(warn(\" Orphaned symlinks (no registry project):\"));\n for (const o of report.orphaned) {\n console.log(` ${chalk.yellow(\"?\")} ${bold(o.slug)} ${dim(o.notes)}`);\n }\n console.log();\n }\n\n if (report.missing.length) {\n console.log(warn(\" Projects with Notes/ but no vault symlink:\"));\n for (const m of report.missing) {\n console.log(` ${chalk.cyan(\"→\")} ${bold(m.slug)} ${dim(m.notes)}`);\n }\n console.log();\n }\n\n if (brokenCount === 0 && orphanedCount === 0 && missingCount === 0) {\n console.log(ok(\" Vault is healthy.\"));\n console.log();\n }\n}\n\nfunction cmdOpen(opts: { vault?: string }): void {\n const vaultPath = getVaultPath(opts.vault);\n // Derive vault name from last path component (Obsidian uses folder name as vault name)\n const parts = vaultPath.split(\"/\").filter(Boolean);\n const vaultName = parts[parts.length - 1] ?? \"obsidian-vault\";\n\n if (!existsSync(vaultPath)) {\n console.error(err(`Vault not found at: ${vaultPath}`));\n console.error(dim(\" Run: pai obsidian sync\"));\n process.exit(1);\n }\n\n const url = `obsidian://open?vault=${encodeURIComponent(vaultName)}`;\n console.log(dim(` Opening: ${url}`));\n try {\n execSync(`open \"${url}\"`, { stdio: \"ignore\" });\n console.log(ok(` Opened vault \"${vaultName}\" in Obsidian.`));\n } catch (e) {\n console.error(err(` Failed to open Obsidian: ${e}`));\n console.error(dim(\" Ensure Obsidian is installed and the vault is registered in Obsidian.\"));\n process.exit(1);\n }\n}\n\n// ---------------------------------------------------------------------------\n// Commander registration\n// ---------------------------------------------------------------------------\n\nexport function registerObsidianCommands(\n obsidianCmd: Command,\n getDb: () => Database\n): void {\n // pai obsidian sync\n obsidianCmd\n .command(\"sync\")\n .description(\"Sync project Notes/ dirs into vault, generate _index.md and topic pages\")\n .option(\"--vault <path>\", \"Override vault path (default: ~/.pai/obsidian-vault)\")\n .option(\"--quiet\", \"Minimal output — suitable for cron/hook use\")\n .action((opts: { vault?: string; quiet?: boolean }) => {\n cmdSync(getDb(), opts);\n });\n\n // pai obsidian status\n obsidianCmd\n .command(\"status\")\n .description(\"Show vault health: healthy, broken, orphaned, and missing symlinks\")\n .option(\"--vault <path>\", \"Override vault path\")\n .action((opts: { vault?: string }) => {\n cmdStatus(getDb(), opts);\n });\n\n // pai obsidian open\n obsidianCmd\n .command(\"open\")\n .description(\"Open the vault in Obsidian (macOS)\")\n .option(\"--vault <path>\", \"Override vault path\")\n .action((opts: { vault?: string }) => {\n cmdOpen(opts);\n });\n}\n","/**\n * pai update — Update PAI from the GitHub repository without losing customizations.\n *\n * Steps:\n * 1. Verify we're in a git repo with a remote (origin)\n * 2. Stash any local changes\n * 3. Pull latest from origin/main\n * 4. Pop the stash if there was one (handle conflicts gracefully)\n * 5. Rebuild: bun install && bun run build\n * 6. Restart the daemon (SIGHUP or pai daemon restart)\n * 7. Check if the CLAUDE.md template has changed and offer to refresh\n * 8. Run pai registry scan to update markers\n * 9. Report what changed\n */\n\nimport type { Command } from \"commander\";\nimport {\n existsSync,\n readFileSync,\n writeFileSync,\n} from \"node:fs\";\nimport { join, dirname } from \"node:path\";\nimport { homedir } from \"node:os\";\nimport { fileURLToPath } from \"node:url\";\nimport { execSync, spawnSync } from \"node:child_process\";\nimport chalk from \"chalk\";\nimport { ok, warn, err, dim, bold } from \"../utils.js\";\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\nfunction line(text = \"\") {\n console.log(text);\n}\n\nfunction step(msg: string) {\n console.log(chalk.bold.cyan(`\\n ${msg}`));\n}\n\nfunction info(msg: string) {\n console.log(dim(` ${msg}`));\n}\n\nfunction success(msg: string) {\n console.log(ok(` ${msg}`));\n}\n\nfunction warning(msg: string) {\n console.log(warn(` ${msg}`));\n}\n\nfunction error(msg: string) {\n console.log(err(` ${msg}`));\n}\n\n/**\n * Run a shell command, streaming output to stdout.\n * Returns true on success, false on failure.\n */\nfunction run(cmd: string, cwd?: string): boolean {\n try {\n execSync(cmd, {\n stdio: \"inherit\",\n cwd: cwd ?? process.cwd(),\n env: process.env,\n });\n return true;\n } catch {\n return false;\n }\n}\n\n/**\n * Run a shell command silently, returning stdout or null on failure.\n */\nfunction capture(cmd: string, cwd?: string): string | null {\n try {\n return execSync(cmd, {\n stdio: \"pipe\",\n cwd: cwd ?? process.cwd(),\n env: process.env,\n }).toString().trim();\n } catch {\n return null;\n }\n}\n\n/**\n * Get the PAI source directory: walk up from dist/cli/ or src/cli/ to the\n * package root where package.json lives.\n */\nfunction getPaiSrcDir(): string {\n const __filename = fileURLToPath(import.meta.url);\n const __dirname = dirname(__filename);\n // dist/cli/index.mjs → dist/ → package root\n return join(__dirname, \"../..\");\n}\n\n/**\n * Locate the templates directory relative to the installed package.\n */\nfunction getTemplatesDir(): string {\n const candidates = [\n join(getPaiSrcDir(), \"templates\"),\n join(homedir(), \"dev\", \"ai\", \"PAI\", \"templates\"),\n join(\"/\", \"usr\", \"local\", \"lib\", \"node_modules\", \"@mnott\", \"pai\", \"templates\"),\n ];\n for (const c of candidates) {\n if (existsSync(join(c, \"claude-md.template.md\"))) return c;\n }\n return join(getPaiSrcDir(), \"templates\");\n}\n\n// ---------------------------------------------------------------------------\n// Update steps\n// ---------------------------------------------------------------------------\n\n/**\n * Step 1: Verify git repo + remote.\n * Returns the PAI source directory (repo root) or null if not a git repo.\n */\nfunction stepVerifyRepo(): string | null {\n step(\"Checking repository...\");\n\n const paiDir = getPaiSrcDir();\n\n // Check git repo\n const gitDir = capture(\"git rev-parse --show-toplevel\", paiDir);\n if (!gitDir) {\n error(\"Not a git repository. Cannot update automatically.\");\n info(\"Install PAI via npm to get automatic updates: npm install -g @mnott/pai\");\n return null;\n }\n\n // Check remote\n const remote = capture(\"git remote get-url origin\", gitDir);\n if (!remote) {\n error(\"No 'origin' remote configured. Cannot pull updates.\");\n info(\"Add a remote: git remote add origin https://github.com/mnott/PAI.git\");\n return null;\n }\n\n info(`Repository: ${gitDir}`);\n info(`Remote: ${remote}`);\n return gitDir;\n}\n\n/**\n * Step 2: Stash local changes.\n * Returns true if something was stashed (so we need to pop later).\n */\nfunction stepStash(repoDir: string): boolean {\n step(\"Stashing local changes...\");\n\n // Check if there are any changes to stash\n const status = capture(\"git status --porcelain\", repoDir);\n if (!status) {\n info(\"Working tree is clean — nothing to stash.\");\n return false;\n }\n\n info(\"Uncommitted changes found:\");\n const statusLines = status.split(\"\\n\").slice(0, 5);\n for (const l of statusLines) {\n info(` ${l}`);\n }\n if (status.split(\"\\n\").length > 5) {\n info(` ... and ${status.split(\"\\n\").length - 5} more`);\n }\n\n const result = capture(\"git stash push -m 'pai update: auto-stash'\", repoDir);\n if (result === null) {\n warning(\"Could not stash changes. Proceeding anyway — merge conflicts may occur.\");\n return false;\n }\n\n if (result.includes(\"No local changes to save\")) {\n info(\"Nothing to stash.\");\n return false;\n }\n\n success(\"Changes stashed.\");\n return true;\n}\n\n/**\n * Step 3: Pull latest from origin/main.\n * Returns the git log summary of new commits or null if already up to date.\n */\nfunction stepPull(repoDir: string): string | null {\n step(\"Pulling latest from origin/main...\");\n\n // Get current HEAD before pull\n const headBefore = capture(\"git rev-parse HEAD\", repoDir);\n\n const pulled = run(\"git pull origin main\", repoDir);\n if (!pulled) {\n warning(\"git pull failed. There may be merge conflicts with your stash.\");\n return null;\n }\n\n // Get HEAD after pull\n const headAfter = capture(\"git rev-parse HEAD\", repoDir);\n\n if (headBefore === headAfter) {\n success(\"Already up to date — no new commits.\");\n return null;\n }\n\n // Show what changed\n const log = capture(\n `git log --oneline ${headBefore ?? \"\"}..${headAfter ?? \"HEAD\"}`,\n repoDir,\n );\n if (log) {\n success(\"New commits pulled:\");\n for (const l of log.split(\"\\n\")) {\n info(` ${l}`);\n }\n }\n\n return log;\n}\n\n/**\n * Step 4: Pop stash (if we stashed anything).\n */\nfunction stepPopStash(repoDir: string): void {\n step(\"Restoring local changes (git stash pop)...\");\n\n const result = spawnSync(\"git\", [\"stash\", \"pop\"], {\n cwd: repoDir,\n stdio: \"pipe\",\n encoding: \"utf8\",\n });\n\n if (result.status === 0) {\n success(\"Local changes restored.\");\n } else {\n const stderr = result.stderr ?? \"\";\n if (stderr.includes(\"CONFLICT\")) {\n warning(\"Stash pop caused merge conflicts. Resolve them manually:\");\n info(\" git status\");\n info(\" # Edit conflicting files\");\n info(\" git add <files>\");\n info(\" git stash drop\");\n } else {\n warning(`Stash pop encountered an issue: ${stderr.trim() || \"unknown error\"}`);\n info(\" Run: git stash pop\");\n info(\" Or: git stash list (to see stashed changes)\");\n }\n }\n}\n\n/**\n * Step 5: Rebuild.\n */\nfunction stepBuild(repoDir: string): boolean {\n step(\"Rebuilding PAI (bun install && bun run build)...\");\n\n info(\"Installing dependencies...\");\n const installed = run(\"bun install\", repoDir);\n if (!installed) {\n error(\"bun install failed.\");\n return false;\n }\n\n info(\"Building...\");\n const built = run(\"bun run build\", repoDir);\n if (!built) {\n error(\"bun run build failed.\");\n return false;\n }\n\n success(\"Build complete.\");\n return true;\n}\n\n/**\n * Step 6: Restart the daemon.\n * Tries SIGHUP first (graceful reload); falls back to pai daemon restart.\n */\nfunction stepRestartDaemon(repoDir: string): void {\n step(\"Restarting PAI daemon...\");\n\n // Try to find the daemon PID from the socket/pid file\n const pidFile = \"/tmp/pai-daemon.pid\";\n if (existsSync(pidFile)) {\n try {\n const pid = parseInt(readFileSync(pidFile, \"utf8\").trim(), 10);\n if (!isNaN(pid) && pid > 0) {\n const killed = capture(`kill -HUP ${pid}`);\n if (killed !== null) {\n success(`Sent SIGHUP to daemon (PID ${pid}).`);\n return;\n }\n }\n } catch {\n // fall through to pai daemon restart\n }\n }\n\n // Fall back to pai daemon restart subcommand\n const result = spawnSync(\"pai\", [\"daemon\", \"restart\"], {\n cwd: repoDir,\n stdio: \"pipe\",\n encoding: \"utf8\",\n timeout: 10_000,\n });\n\n if (result.status === 0) {\n success(\"Daemon restarted.\");\n } else {\n warning(\"Could not restart daemon automatically.\");\n info(\" Restart manually: pai daemon restart\");\n info(\" Or: pai daemon serve\");\n }\n}\n\n/**\n * Step 7: Check if CLAUDE.md template changed and offer to refresh.\n */\nfunction stepRefreshClaudeMd(repoDir: string, newCommitsLog: string | null): void {\n step(\"Checking CLAUDE.md template...\");\n\n const templatesDir = getTemplatesDir();\n const templatePath = join(templatesDir, \"claude-md.template.md\");\n const claudeMd = join(homedir(), \".claude\", \"CLAUDE.md\");\n\n if (!existsSync(templatePath)) {\n info(\"Template not found — skipping CLAUDE.md check.\");\n return;\n }\n\n if (!existsSync(claudeMd)) {\n info(\"No ~/.claude/CLAUDE.md found — skipping.\");\n return;\n }\n\n const userContent = readFileSync(claudeMd, \"utf8\");\n const isPaiGenerated = userContent.includes(\"Generated by PAI Setup\");\n\n if (!isPaiGenerated) {\n warning(\"~/.claude/CLAUDE.md appears to be custom (not PAI-generated).\");\n info(\" Skipping auto-update. Review the new template manually:\");\n info(` ${templatePath}`);\n return;\n }\n\n // Check if the template was actually modified in the new commits\n let templateChanged = false;\n if (newCommitsLog) {\n // Check git diff on the template file for newly pulled commits\n const diff = capture(\n \"git diff HEAD~1..HEAD -- templates/claude-md.template.md\",\n repoDir,\n );\n templateChanged = !!(diff && diff.trim().length > 0);\n }\n\n if (!templateChanged) {\n info(\"CLAUDE.md template unchanged in this update.\");\n return;\n }\n\n info(\"CLAUDE.md template was updated in this release.\");\n info(\"Refreshing ~/.claude/CLAUDE.md (PAI-generated — safe to overwrite)...\");\n\n let template = readFileSync(templatePath, \"utf8\");\n // Substitute ${HOME} with actual home directory\n template = template.replace(/\\$\\{HOME\\}/g, homedir());\n writeFileSync(claudeMd, template, \"utf8\");\n\n success(\"~/.claude/CLAUDE.md refreshed from updated template.\");\n}\n\n/**\n * Step 8: Run pai registry scan.\n */\nfunction stepRegistryScan(repoDir: string): void {\n step(\"Running pai registry scan...\");\n\n const result = spawnSync(\"pai\", [\"registry\", \"scan\"], {\n cwd: repoDir,\n stdio: \"inherit\",\n timeout: 60_000,\n });\n\n if (result.status === 0) {\n success(\"Registry scan complete.\");\n } else {\n warning(\"Registry scan encountered issues.\");\n info(\" Run manually: pai registry scan\");\n }\n}\n\n// ---------------------------------------------------------------------------\n// Main update action\n// ---------------------------------------------------------------------------\n\nasync function runUpdate(): Promise<void> {\n line();\n console.log(chalk.bold.cyan(\" ╔═══════════════════════════════════╗\"));\n console.log(chalk.bold.cyan(\" ║ PAI Knowledge OS — Update ║\"));\n console.log(chalk.bold.cyan(\" ╚═══════════════════════════════════╝\"));\n line();\n\n // Step 1: Verify repo\n const repoDir = stepVerifyRepo();\n if (!repoDir) {\n line();\n process.exit(1);\n }\n\n // Step 2: Stash local changes\n const didStash = stepStash(repoDir);\n\n // Step 3: Pull\n const newCommitsLog = stepPull(repoDir);\n\n // Step 4: Pop stash (only if we stashed something)\n if (didStash) {\n stepPopStash(repoDir);\n }\n\n // If no new commits and no stash needed, we can short-circuit after\n // confirming things are current. But we still offer rebuild in case the\n // user's build is stale.\n\n // Step 5: Build\n const buildOk = stepBuild(repoDir);\n if (!buildOk) {\n line();\n error(\"Update failed at build step. Check errors above.\");\n line();\n process.exit(1);\n }\n\n // Step 6: Restart daemon\n stepRestartDaemon(repoDir);\n\n // Step 7: CLAUDE.md refresh\n stepRefreshClaudeMd(repoDir, newCommitsLog);\n\n // Step 8: Registry scan\n stepRegistryScan(repoDir);\n\n // Summary\n line();\n console.log(chalk.bold.cyan(\" ─────────────────────────────────────\"));\n if (newCommitsLog) {\n success(\"PAI updated successfully!\");\n } else {\n success(\"PAI is up to date and rebuilt successfully!\");\n }\n console.log(dim(\" Version: \") + chalk.cyan(capture(\"pai --version\", repoDir) ?? \"unknown\"));\n line();\n}\n\n// ---------------------------------------------------------------------------\n// Commander registration\n// ---------------------------------------------------------------------------\n\nexport function registerUpdateCommand(program: Command): void {\n program\n .command(\"update\")\n .description(\n \"Update PAI from GitHub (git pull + rebuild + daemon restart). Preserves local customizations.\",\n )\n .action(async () => {\n await runUpdate();\n });\n}\n","/**\n * pai notify <sub-command>\n *\n * status — Show current notification mode and active channels\n * get — Alias for status\n * set — Set notification mode or channel/routing config\n * test — Send a test notification through configured channels\n * send — Send a notification (event + message)\n *\n * All sub-commands communicate with the running daemon via IPC.\n */\n\nimport type { Command } from \"commander\";\nimport { ok, warn, err, dim, bold, header } from \"../utils.js\";\nimport { loadConfig } from \"../../daemon/config.js\";\nimport { PaiClient } from \"../../daemon/ipc-client.js\";\nimport type {\n NotificationConfig,\n NotificationMode,\n NotificationEvent,\n} from \"../../notifications/types.js\";\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\nfunction makeClient(): PaiClient {\n const config = loadConfig();\n return new PaiClient(config.socketPath);\n}\n\nfunction modeColor(mode: NotificationMode): string {\n switch (mode) {\n case \"off\":\n return warn(mode);\n case \"voice\":\n return bold(ok(mode));\n case \"auto\":\n return ok(mode);\n default:\n return ok(mode);\n }\n}\n\nfunction printConfig(config: NotificationConfig, activeChannels: string[]): void {\n console.log();\n console.log(header(\" PAI Notification Config\"));\n console.log();\n console.log(` ${bold(\"Mode:\")} ${modeColor(config.mode)}`);\n console.log();\n console.log(` ${bold(\"Channels:\")}`);\n for (const [ch, cfg] of Object.entries(config.channels)) {\n const c = cfg as { enabled: boolean; url?: string; voiceName?: string };\n const status = c.enabled ? ok(\"enabled\") : dim(\"disabled\");\n let extra = \"\";\n if (ch === \"ntfy\" && c.url) extra = dim(` ${c.url}`);\n if (ch === \"voice\" && c.voiceName) extra = dim(` voice: ${c.voiceName}`);\n console.log(` ${bold(ch.padEnd(12))}${status}${extra}`);\n }\n console.log();\n console.log(` ${bold(\"Active:\")} ${activeChannels.length > 0 ? activeChannels.join(\", \") : dim(\"(none)\")}`);\n console.log();\n console.log(` ${bold(\"Routing:\")} ${dim(\"(auto mode only)\")}`);\n for (const [event, channels] of Object.entries(config.routing)) {\n const ch = (channels as string[]).join(\", \") || dim(\"(none)\");\n console.log(` ${event.padEnd(12)}→ ${ch}`);\n }\n console.log();\n}\n\n// ---------------------------------------------------------------------------\n// Command implementations\n// ---------------------------------------------------------------------------\n\nasync function cmdStatus(): Promise<void> {\n const client = makeClient();\n try {\n const { config, activeChannels } = await client.getNotificationConfig();\n printConfig(config, activeChannels);\n } catch (e) {\n const msg = e instanceof Error ? e.message : String(e);\n console.log();\n console.log(warn(\" Cannot reach PAI daemon.\"));\n console.log(dim(` ${msg}`));\n console.log(dim(\" Start it with: pai daemon serve\"));\n console.log();\n process.exit(1);\n }\n}\n\nasync function cmdSet(opts: {\n mode?: string;\n ntfyUrl?: string;\n ntfyPriority?: string;\n whatsappRecipient?: string;\n enableChannel?: string[];\n disableChannel?: string[];\n}): Promise<void> {\n const client = makeClient();\n\n // Build the patch object\n const patch: {\n mode?: NotificationMode;\n channels?: Record<string, unknown>;\n } = {};\n\n if (opts.mode) {\n const validModes: NotificationMode[] = [\n \"auto\", \"voice\", \"whatsapp\", \"ntfy\", \"macos\", \"cli\", \"off\",\n ];\n if (!validModes.includes(opts.mode as NotificationMode)) {\n console.error(err(`Invalid mode: ${opts.mode}. Valid: ${validModes.join(\", \")}`));\n process.exit(1);\n }\n patch.mode = opts.mode as NotificationMode;\n }\n\n // Channel enable/disable\n const channels: Record<string, unknown> = {};\n\n if (opts.enableChannel) {\n for (const ch of opts.enableChannel) {\n channels[ch] = { enabled: true };\n }\n }\n\n if (opts.disableChannel) {\n for (const ch of opts.disableChannel) {\n channels[ch] = { enabled: false };\n }\n }\n\n if (opts.ntfyUrl) {\n channels[\"ntfy\"] = {\n ...(channels[\"ntfy\"] as object ?? {}),\n url: opts.ntfyUrl,\n };\n }\n\n if (opts.ntfyPriority) {\n channels[\"ntfy\"] = {\n ...(channels[\"ntfy\"] as object ?? {}),\n priority: opts.ntfyPriority,\n };\n }\n\n if (opts.whatsappRecipient) {\n channels[\"whatsapp\"] = {\n ...(channels[\"whatsapp\"] as object ?? {}),\n recipient: opts.whatsappRecipient,\n };\n }\n\n if (Object.keys(channels).length > 0) {\n patch.channels = channels;\n }\n\n if (!patch.mode && !patch.channels) {\n console.error(err(\"No changes specified. Use --mode, --enable, --disable, etc.\"));\n console.log(dim(\" Example: pai notify set --mode voice\"));\n console.log(dim(\" Example: pai notify set --mode auto --enable macos --disable ntfy\"));\n process.exit(1);\n }\n\n try {\n const { config } = await client.setNotificationConfig(\n patch as Parameters<typeof client.setNotificationConfig>[0]\n );\n console.log();\n console.log(ok(\" Notification config updated.\"));\n console.log(` ${bold(\"Mode:\")} ${modeColor(config.mode)}`);\n console.log();\n } catch (e) {\n const msg = e instanceof Error ? e.message : String(e);\n console.error(err(` Failed: ${msg}`));\n process.exit(1);\n }\n}\n\nasync function cmdTest(opts: { event?: string }): Promise<void> {\n const client = makeClient();\n const event = (opts.event ?? \"info\") as NotificationEvent;\n\n console.log();\n console.log(dim(` Sending test ${event} notification...`));\n\n try {\n const result = await client.sendNotification({\n event,\n message: `PAI test notification (${event})`,\n title: \"PAI Test\",\n });\n\n if (result.channelsSucceeded.length === 0 && result.channelsAttempted.length > 0) {\n console.log(warn(\" All channels failed.\"));\n } else if (result.channelsAttempted.length === 0) {\n console.log(warn(\" No channels active for this event. Check mode and channel config.\"));\n } else {\n console.log(ok(` Sent to: ${result.channelsSucceeded.join(\", \")}`));\n }\n\n if (result.channelsFailed.length > 0) {\n console.log(warn(` Failed: ${result.channelsFailed.join(\", \")}`));\n }\n\n console.log(dim(` Mode: ${result.mode}`));\n console.log();\n } catch (e) {\n const msg = e instanceof Error ? e.message : String(e);\n console.error(err(` Failed: ${msg}`));\n process.exit(1);\n }\n}\n\nasync function cmdSend(\n event: string,\n message: string,\n opts: { title?: string }\n): Promise<void> {\n const client = makeClient();\n const validEvents: NotificationEvent[] = [\n \"error\", \"progress\", \"completion\", \"info\", \"debug\",\n ];\n\n if (!validEvents.includes(event as NotificationEvent)) {\n console.error(\n err(`Invalid event: ${event}. Valid: ${validEvents.join(\", \")}`)\n );\n process.exit(1);\n }\n\n try {\n const result = await client.sendNotification({\n event: event as NotificationEvent,\n message,\n title: opts.title,\n });\n\n if (result.channelsSucceeded.length > 0) {\n console.log(ok(` Sent to: ${result.channelsSucceeded.join(\", \")}`));\n } else {\n console.log(warn(\" No channels received the notification.\"));\n }\n\n if (result.channelsFailed.length > 0) {\n console.log(warn(` Failed: ${result.channelsFailed.join(\", \")}`));\n }\n } catch (e) {\n const msg = e instanceof Error ? e.message : String(e);\n console.error(err(` Failed: ${msg}`));\n process.exit(1);\n }\n}\n\n// ---------------------------------------------------------------------------\n// Commander registration\n// ---------------------------------------------------------------------------\n\nexport function registerNotifyCommands(notifyCmd: Command): void {\n // pai notify status (default / alias for get)\n notifyCmd\n .command(\"status\")\n .description(\"Show current notification mode and active channels\")\n .action(async () => {\n await cmdStatus();\n });\n\n // pai notify get (alias for status)\n notifyCmd\n .command(\"get\")\n .description(\"Show current notification config (alias for status)\")\n .action(async () => {\n await cmdStatus();\n });\n\n // pai notify set\n notifyCmd\n .command(\"set\")\n .description(\"Update notification mode or channel configuration\")\n .option(\n \"--mode <mode>\",\n \"Notification mode: auto | voice | whatsapp | ntfy | macos | cli | off\"\n )\n .option(\"--enable <channel...>\", \"Enable one or more channels\")\n .option(\"--disable <channel...>\", \"Disable one or more channels\")\n .option(\"--ntfy-url <url>\", \"Set the ntfy.sh topic URL\")\n .option(\n \"--ntfy-priority <level>\",\n \"Set ntfy priority: min | low | default | high | urgent\"\n )\n .option(\"--whatsapp-recipient <contact>\", \"Set WhatsApp recipient\")\n .action(\n async (opts: {\n mode?: string;\n enable?: string[];\n disable?: string[];\n ntfyUrl?: string;\n ntfyPriority?: string;\n whatsappRecipient?: string;\n }) => {\n await cmdSet({\n mode: opts.mode,\n enableChannel: opts.enable,\n disableChannel: opts.disable,\n ntfyUrl: opts.ntfyUrl,\n ntfyPriority: opts.ntfyPriority,\n whatsappRecipient: opts.whatsappRecipient,\n });\n }\n );\n\n // pai notify test\n notifyCmd\n .command(\"test\")\n .description(\"Send a test notification through configured channels\")\n .option(\n \"--event <event>\",\n \"Event type to test: error | progress | completion | info | debug\",\n \"info\"\n )\n .action(async (opts: { event?: string }) => {\n await cmdTest(opts);\n });\n\n // pai notify send <event> <message>\n notifyCmd\n .command(\"send <event> <message>\")\n .description(\"Send a notification with an explicit event type and message\")\n .option(\"--title <title>\", \"Optional notification title\")\n .action(async (event: string, message: string, opts: { title?: string }) => {\n await cmdSend(event, message, opts);\n });\n}\n","/**\n * pai topic <sub-command>\n *\n * check <context> — Check whether context text has drifted to a different project\n *\n * Communicates with the running daemon via IPC.\n * The daemon uses BM25 keyword search to match context against indexed memory.\n *\n * Examples:\n * pai topic check \"working on authentication and JWT tokens\"\n * pai topic check \"fixing the React component\" --current myapp\n * pai topic check \"database schema migration\" --threshold 0.7\n */\n\nimport type { Command } from \"commander\";\nimport { ok, warn, err, dim, bold, header } from \"../utils.js\";\nimport { loadConfig } from \"../../daemon/config.js\";\nimport { PaiClient } from \"../../daemon/ipc-client.js\";\nimport type { TopicCheckResult } from \"../../topics/detector.js\";\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\nfunction makeClient(): PaiClient {\n const config = loadConfig();\n return new PaiClient(config.socketPath);\n}\n\nfunction confidenceBar(confidence: number, width = 20): string {\n const filled = Math.round(confidence * width);\n const empty = width - filled;\n return \"[\" + \"=\".repeat(filled) + \" \".repeat(empty) + \"]\";\n}\n\nfunction printTopicResult(result: TopicCheckResult): void {\n console.log();\n console.log(header(\" PAI Topic Check\"));\n console.log();\n\n console.log(` ${bold(\"Current project:\")} ${result.currentProject ?? dim(\"(none)\")}`);\n console.log(` ${bold(\"Suggested project:\")} ${result.suggestedProject ?? dim(\"(none)\")}`);\n console.log(\n ` ${bold(\"Confidence:\")} ${confidenceBar(result.confidence)} ${(result.confidence * 100).toFixed(1)}%`\n );\n console.log(` ${bold(\"Chunks scored:\")} ${result.chunkCount}`);\n\n if (result.topProjects.length > 0) {\n console.log();\n console.log(` ${bold(\"Top matches:\")}`);\n for (const p of result.topProjects) {\n const bar = confidenceBar(p.score, 15);\n const marker = p.slug === result.currentProject ? dim(\" (current)\") : \"\";\n console.log(` ${p.slug.padEnd(30)} ${bar} ${(p.score * 100).toFixed(1)}%${marker}`);\n }\n }\n\n console.log();\n\n if (result.shifted) {\n console.log(\n warn(\" TOPIC SHIFT DETECTED\") +\n dim(` — conversation appears to be about \"${result.suggestedProject}\", not \"${result.currentProject}\"`)\n );\n console.log();\n } else if (result.suggestedProject && result.suggestedProject === result.currentProject) {\n console.log(ok(\" No shift detected\") + dim(\" — context matches current project\"));\n console.log();\n } else if (!result.currentProject) {\n if (result.suggestedProject) {\n console.log(\n ok(\" Best matching project: \") + bold(result.suggestedProject) +\n dim(` (confidence: ${(result.confidence * 100).toFixed(0)}%)`)\n );\n } else {\n console.log(dim(\" No matching project found in memory index.\"));\n }\n console.log();\n } else {\n console.log(\n dim(` No shift detected (top match \"${result.suggestedProject}\" has confidence ${(result.confidence * 100).toFixed(0)}% — below threshold)`)\n );\n console.log();\n }\n}\n\n// ---------------------------------------------------------------------------\n// Command implementations\n// ---------------------------------------------------------------------------\n\nasync function cmdCheck(\n context: string,\n opts: {\n current?: string;\n threshold?: string;\n json?: boolean;\n }\n): Promise<void> {\n const client = makeClient();\n\n const threshold = opts.threshold ? parseFloat(opts.threshold) : undefined;\n\n if (threshold !== undefined && (isNaN(threshold) || threshold < 0 || threshold > 1)) {\n console.error(err(\" --threshold must be a number between 0 and 1\"));\n process.exit(1);\n }\n\n try {\n const result = await client.topicCheck({\n context,\n currentProject: opts.current,\n threshold,\n });\n\n if (opts.json) {\n console.log(JSON.stringify(result, null, 2));\n return;\n }\n\n printTopicResult(result);\n\n // Exit with code 1 if a shift was detected (useful for scripting / hooks)\n if (result.shifted) {\n process.exit(1);\n }\n } catch (e) {\n const msg = e instanceof Error ? e.message : String(e);\n console.log();\n console.log(warn(\" Cannot reach PAI daemon.\"));\n console.log(dim(` ${msg}`));\n console.log(dim(\" Start it with: pai daemon serve\"));\n console.log();\n process.exit(1);\n }\n}\n\n// ---------------------------------------------------------------------------\n// Commander registration\n// ---------------------------------------------------------------------------\n\nexport function registerTopicCommands(topicCmd: Command): void {\n // pai topic check <context>\n topicCmd\n .command(\"check <context>\")\n .description(\n \"Check whether context text has drifted to a different project.\\n\" +\n 'Example: pai topic check \"working on JWT authentication\"\\n' +\n \"Exit code 1 if a shift is detected (useful for hooks).\"\n )\n .option(\n \"--current <slug>\",\n \"The project this session is currently routed to\"\n )\n .option(\n \"--threshold <n>\",\n \"Confidence threshold [0-1] to declare a shift (default: 0.6)\"\n )\n .option(\"--json\", \"Output raw JSON result\")\n .action(\n async (\n context: string,\n opts: { current?: string; threshold?: string; json?: boolean }\n ) => {\n await cmdCheck(context, opts);\n }\n );\n}\n","#!/usr/bin/env node\n/**\n * PAI Knowledge OS — CLI entry point\n *\n * Command tree:\n * pai project add|list|info|archive|unarchive|move|tag|alias|edit\n * pai session list|info\n * pai registry scan|migrate|stats|rebuild\n * pai memory index|search|status\n * pai search <query> (placeholder — Phase 3)\n * pai update\n * pai version\n */\n\nimport { Command } from \"commander\";\nimport { readFileSync } from \"node:fs\";\nimport { join, dirname } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { openRegistry } from \"../registry/db.js\";\nimport type { Database } from \"better-sqlite3\";\nimport { registerProjectCommands, cmdGo } from \"./commands/project.js\";\nimport { registerSessionCommands } from \"./commands/session.js\";\nimport { registerSessionCleanupCommand } from \"./commands/session-cleanup.js\";\nimport { registerRegistryCommands } from \"./commands/registry.js\";\nimport { registerMemoryCommands } from \"./commands/memory.js\";\nimport { registerMcpCommands } from \"./commands/mcp.js\";\nimport { registerDaemonCommands } from \"./commands/daemon.js\";\nimport { registerBackupCommands } from \"./commands/backup.js\";\nimport { registerRestoreCommands } from \"./commands/restore.js\";\nimport { registerSetupCommand } from \"./commands/setup.js\";\nimport { registerObsidianCommands } from \"./commands/obsidian.js\";\nimport { registerUpdateCommand } from \"./commands/update.js\";\nimport { registerNotifyCommands } from \"./commands/notify.js\";\nimport { registerTopicCommands } from \"./commands/topic.js\";\nimport { err } from \"./utils.js\";\n\n// ---------------------------------------------------------------------------\n// Version resolution\n// ---------------------------------------------------------------------------\n\nfunction getVersion(): string {\n try {\n const __dirname = dirname(fileURLToPath(import.meta.url));\n // Walk up from dist/cli/ or src/cli/ to find package.json\n const pkgPath = join(__dirname, \"../../package.json\");\n const pkg = JSON.parse(readFileSync(pkgPath, \"utf8\")) as { version?: string };\n return pkg.version ?? \"0.0.0\";\n } catch {\n return \"0.0.0\";\n }\n}\n\n// ---------------------------------------------------------------------------\n// Lazy database singleton — opened on first command that needs it\n// ---------------------------------------------------------------------------\n\nlet _db: Database | null = null;\n\nfunction getDb(): Database {\n if (!_db) {\n try {\n _db = openRegistry();\n } catch (e) {\n console.error(err(`Failed to open PAI registry: ${e}`));\n process.exit(1);\n }\n }\n return _db;\n}\n\n// ---------------------------------------------------------------------------\n// Program definition\n// ---------------------------------------------------------------------------\n\nconst program = new Command();\n\nprogram\n .name(\"pai\")\n .description(\"PAI Knowledge OS — Personal AI Infrastructure CLI\")\n .version(getVersion(), \"-V, --version\", \"Print version and exit\");\n\n// ---------------------------------------------------------------------------\n// pai project\n// ---------------------------------------------------------------------------\n\nconst projectCmd = program\n .command(\"project\")\n .description(\"Manage registered projects\");\n\nregisterProjectCommands(projectCmd, getDb);\n\n// ---------------------------------------------------------------------------\n// pai session\n// ---------------------------------------------------------------------------\n\nconst sessionCmd = program\n .command(\"session\")\n .description(\"Browse session notes\");\n\nregisterSessionCommands(sessionCmd, getDb);\nregisterSessionCleanupCommand(sessionCmd, getDb);\n\n// ---------------------------------------------------------------------------\n// pai registry\n// ---------------------------------------------------------------------------\n\nconst registryCmd = program\n .command(\"registry\")\n .description(\"Registry maintenance: scan, migrate, stats, rebuild\");\n\nregisterRegistryCommands(registryCmd, getDb);\n\n// ---------------------------------------------------------------------------\n// pai memory\n// ---------------------------------------------------------------------------\n\nconst memoryCmd = program\n .command(\"memory\")\n .description(\"Memory engine: index, search, and status\");\n\nregisterMemoryCommands(memoryCmd, getDb);\n\n// ---------------------------------------------------------------------------\n// pai mcp\n// ---------------------------------------------------------------------------\n\nconst mcpCmd = program\n .command(\"mcp\")\n .description(\"MCP server management: install and status\");\n\nregisterMcpCommands(mcpCmd);\n\n// ---------------------------------------------------------------------------\n// pai daemon\n// ---------------------------------------------------------------------------\n\nconst daemonCmd = program\n .command(\"daemon\")\n .description(\"PAI daemon management: serve, status, restart, install, uninstall, logs\");\n\nregisterDaemonCommands(daemonCmd);\n\n// ---------------------------------------------------------------------------\n// pai backup / pai restore\n// ---------------------------------------------------------------------------\n\nregisterBackupCommands(program);\nregisterRestoreCommands(program);\n\n// ---------------------------------------------------------------------------\n// pai setup\n// ---------------------------------------------------------------------------\n\nregisterSetupCommand(program);\n\n// ---------------------------------------------------------------------------\n// pai update\n// ---------------------------------------------------------------------------\n\nregisterUpdateCommand(program);\n\n// ---------------------------------------------------------------------------\n// pai notify\n// ---------------------------------------------------------------------------\n\nconst notifyCmd = program\n .command(\"notify\")\n .description(\"Notification config: status, get, set, test, send\");\n\nregisterNotifyCommands(notifyCmd);\n\n// ---------------------------------------------------------------------------\n// pai topic\n// ---------------------------------------------------------------------------\n\nconst topicCmd = program\n .command(\"topic\")\n .description(\"Topic shift detection: check whether context has drifted to a different project\");\n\nregisterTopicCommands(topicCmd);\n\n// ---------------------------------------------------------------------------\n// pai obsidian\n// ---------------------------------------------------------------------------\n\nconst obsidianCmd = program\n .command(\"obsidian\")\n .description(\"Obsidian vault: sync project notes, view status, open in Obsidian\");\n\nregisterObsidianCommands(obsidianCmd, getDb);\n\n// ---------------------------------------------------------------------------\n// pai go <query> — top-level shortcut for pai project go\n// ---------------------------------------------------------------------------\n\nprogram\n .command(\"go <query>\")\n .description(\n \"Jump to a project directory by slug or partial name.\\n\" +\n \"Prints the root path to stdout — use with: cd $(pai go <query>)\\n\" +\n \"Example shell function in ~/.zshrc:\\n\" +\n \" pcd() { cd \\\"$(pai go \\\"$@\\\")\\\" }\"\n )\n .action((query: string) => {\n cmdGo(getDb(), query);\n });\n\n// ---------------------------------------------------------------------------\n// pai search <query> (Phase 3 placeholder)\n// ---------------------------------------------------------------------------\n\nprogram\n .command(\"search <query>\")\n .description(\"Full-text search across sessions and notes (Phase 3)\")\n .option(\"--projects <p1,p2>\", \"Restrict search to these project slugs (comma-separated)\")\n .option(\"--limit <n>\", \"Maximum results to return\", \"10\")\n .action((query: string, opts: { projects?: string; limit?: string }) => {\n console.log(\n `\\n Search is coming in Phase 3.\\n\\n` +\n ` Query: ${query}\\n` +\n ` Projects: ${opts.projects ?? \"(all)\"}\\n` +\n ` Limit: ${opts.limit ?? 10}\\n`\n );\n });\n\n// ---------------------------------------------------------------------------\n// Error handling — show clean message instead of stack trace for user errors\n// ---------------------------------------------------------------------------\n\nprogram.configureOutput({\n writeErr: (str) => process.stderr.write(err(str)),\n});\n\nprogram.exitOverride((error) => {\n if (error.code === \"commander.helpDisplayed\" || error.code === \"commander.version\") {\n process.exit(0);\n }\n if (\n error.code === \"commander.missingArgument\" ||\n error.code === \"commander.unknownOption\" ||\n error.code === \"commander.unknownCommand\"\n ) {\n // Commander has already printed the message\n process.exit(1);\n }\n // For unexpected errors, show message but not stack trace\n console.error(err(error.message));\n process.exit(1);\n});\n\n// ---------------------------------------------------------------------------\n// Parse\n// ---------------------------------------------------------------------------\n\nprogram.parse(process.argv);\n\n// If no sub-command given, print help\nif (process.argv.length <= 2) {\n program.help();\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;AAoDA,SAAS,uBAAuB,UAA0B;CACxD,MAAM,OAAO,SAAS,UAAU,MAAM;CAEtC,MAAM,QAAQ,KAAK,MAAM,mCAAmC;AAC5D,QAAO,QAAQ,MAAM,KAAK;;;;;AAM5B,SAAS,kBAAkB,SAAiB,QAAQ,GAAa;CAC/D,MAAM,WAAqB,EAAE;AAC7B,MAAK,MAAM,KAAK,QAAQ,SAAS,YAAY,EAAE;AAC7C,WAAS,KAAK,EAAE,GAAG;AACnB,MAAI,SAAS,UAAU,MAAO;;AAEhC,QAAO;;;;;;;AAQT,SAAS,sBAAsB,aAA6B;AAE1D,QAAO,QADU,QAAQ,QAAQ,YAAY,CAAC,CACtB;;AAO1B,SAAgB,WAAW,IAAc,MAA4B;CACnE,MAAM,cAAc,YAAY,KAAK,YAAY;CACjD,MAAM,aAAa,YAAY,KAAK,GAAG;AAIvC,KAAI,CAAC,WAAW,YAAY,EAAE;AAC5B,UAAQ,MAAM,IAAI,2BAA2B,cAAc,CAAC;AAC5D,UAAQ,KAAK,EAAE;;AAEjB,KAAI,CAAC,YAAY,SAAS,MAAM,EAAE;AAChC,UAAQ,MAAM,IAAI,iDAAiD,cAAc,CAAC;AAClF,UAAQ,KAAK,EAAE;;AAEjB,KAAI,WAAW,WAAW,EAAE;AAC1B,UAAQ,MAAM,IAAI,+BAA+B,aAAa,CAAC;AAC/D,UAAQ,KAAK,EAAE;;CAKjB,MAAM,cAAc,KAAK,QAAQ,uBAAuB,YAAY;CACpE,MAAM,OAAOA,UAAQ,YAAY;AAEjC,KAAI,CAAC,MAAM;AACT,UAAQ,MAAM,IAAI,6CAA6C,YAAY,GAAG,CAAC;AAC/E,UAAQ,KAAK,EAAE;;CAKjB,MAAM,aAAa,UAAU,WAAW;AAIxC,KAHiB,GACd,QAAQ,6EAA6E,CACrF,IAAI,MAAM,YAAY,WAAW,EACtB;AACZ,UAAQ,MACN,IAAI,wBAAwB,KAAK,aAAa,WAAW,0BAA0B,CACpF;AACD,UAAQ,KAAK,EAAE;;AAKjB,qBAAoB,WAAW;CAI/B,MAAM,yBAAQ,IAAI,MAAM,EAAC,aAAa,CAAC,MAAM,GAAG,GAAG;CACnD,MAAM,iBAAiB,SAAS,YAAY;CAC5C,MAAM,oBAAoB,sBAAsB,YAAY;CAG5D,MAAM,eAAe,UAAU,MAAM,mBAFX,SAAS,kBAAkB,CAEqB;AAG1E,cAAa,aAFQ,KAAK,YAAY,SAAS,aAAa,CAErB;CAKvC,MAAM,WAAW,kBADM,aAAa,aAAa,QAAQ,CACP;CAElD,MAAM,gBAAgB;EACpB;EACA;EACA;EACA;EACA,wBAAwB;EACxB,yBAAyB;EACzB,4BAA4B;EAC5B,gCAAgC;EAChC;EACA;EACA;EACA,SAAS,SAAS,IACd,0CAA0C,SAAS,KAAK,MAAM,KAAK,EAAE,QAAQ,QAAQ,GAAG,GAAG,CAAC,KAAK,KAAK,KACtG,sCAAsC,aAAa;EACvD;EACD,CAAC,KAAK,KAAK;AAEZ,eAAc,KAAK,YAAY,SAAS,YAAY,EAAE,eAAe,QAAQ;CAI7E,MAAM,KAAK,KAAK;AAChB,IAAG,QACD;;mDAGD,CAAC,IAAI,MAAM,aAAa,YAAY,YAAY,IAAI,GAAG;CAIxD,MAAM,iBAAiB,KAAK,mBAAmB,SAAS,UAAU;AAClE,KAAI,WAAW,eAAe,EAAE;AAE9B,iBAAe,gBADE,6BAA6B,KAAK,IAAI,WAAW,MACzB,QAAQ;AACjD,UAAQ,IAAI,IAAI,wBAAwB,iBAAiB,CAAC;;AAK5D,SAAQ,KAAK;AACb,SAAQ,IAAI,GAAG,qBAAqB,KAAK,KAAK,GAAG,CAAC;AAClD,SAAQ,IAAI,IAAI,mBAAmB,cAAc,CAAC;AAClD,SAAQ,IAAI,IAAI,mBAAmB,aAAa,CAAC;AACjD,SAAQ,IAAI,IAAI,mBAAmB,OAAO,CAAC;AAC3C,SAAQ,IAAI,IAAI,mBAAmB,eAAe,CAAC;AACnD,SAAQ,IAAI,IAAI,0CAA0C,CAAC;AAC3D,SAAQ,KAAK;AACb,SAAQ,IAAI,IAAI,cAAc,WAAW,uBAAuB,OAAO,CAAC;;;;;AChI1E,SAASC,aAAW,IAAc,MAAsC;CAEtE,MAAM,SAAS,GACZ,QAAQ,wCAAwC,CAChD,IAAI,KAAK;AACZ,KAAI,OAAQ,QAAO;AASnB,QAPc,GACX,QACC;;0BAGD,CACA,IAAI,KAAK;;AAId,SAAS,eAAe,IAAc,MAA0B;CAC9D,MAAM,UAAUA,aAAW,IAAI,KAAK;AACpC,KAAI,CAAC,SAAS;AACZ,UAAQ,MAAM,IAAI,sBAAsB,OAAO,CAAC;AAChD,UAAQ,KAAK,EAAE;;AAEjB,QAAO;;;;;AAMT,SAAS,kBAAkB,IAAc,YAA4C;CACnF,MAAM,MAAM,SAAS,YAAY,GAAG;AACpC,KAAI,CAAC,MAAM,IAAI,IAAI,MAAM,KAAK,OAAO,IAAI,KAAK,YAAY;EACxD,MAAM,OAAO,GAAG,QACd,8DACD,CAAC,KAAK;AACP,MAAI,OAAO,KAAK,OAAQ,QAAO,KAAK,MAAM;;AAE5C,QAAOA,aAAW,IAAI,WAAW;;AAGnC,SAAS,eAAe,IAAc,WAA6B;AASjE,QARa,GACV,QACC;;;wBAID,CACA,IAAI,UAAU,CACL,KAAK,MAAM,EAAE,KAAK;;AAGhC,SAAS,kBAAkB,IAAc,WAA6B;AAIpE,QAHa,GACV,QAAQ,gEAAgE,CACxE,IAAI,UAAU,CACL,KAAK,MAAM,EAAE,MAAM;;AAGjC,SAAS,gBAAgB,IAAc,WAA2B;AAIhE,QAHY,GACT,QAAQ,4DAA4D,CACpE,IAAI,UAAU,CACN;;AAGb,SAAS,mBAAmB,IAAc,WAAkC;CAC1E,MAAM,MAAM,GACT,QACC;yCAED,CACA,IAAI,UAAU;AACjB,QAAO,MAAM,IAAI,aAAa;;AAGhC,SAASC,YAAU,IAAc,SAAyB;AACxD,IAAG,QAAQ,+CAA+C,CAAC,IAAI,QAAQ;AAIvE,QAHY,GAAG,QAAQ,qCAAqC,CAAC,IAAI,QAAQ,CAG9D;;AAOb,SAAS,OACP,IACA,SACA,MAKM;CACN,MAAM,WAAW,YAAY,QAAQ;CACrC,MAAM,OAAO,KAAK,QAAQ,aAAa,SAAS;CAChD,MAAM,aAAa,UAAU,SAAS;CACtC,MAAM,cAAc,KAAK,eAAe;CACxC,MAAM,OAAO,KAAK,QAAQ;CAG1B,MAAM,aAAa;EAAC;EAAS;EAAW;EAAmB;EAAW;AACtE,KAAI,CAAC,WAAW,SAAS,KAAK,EAAE;AAC9B,UAAQ,MAAM,IAAI,iBAAiB,KAAK,YAAY,WAAW,KAAK,KAAK,GAAG,CAAC;AAC7E,UAAQ,KAAK,EAAE;;AAOjB,KAHiB,GACd,QAAQ,0DAA0D,CAClE,IAAI,MAAM,SAAS,EACR;AACZ,UAAQ,MAAM,IAAI,qCAAqC,KAAK,YAAY,SAAS,GAAG,CAAC;AACrF,UAAQ,KAAK,EAAE;;CAGjB,MAAM,KAAK,KAAK;AAChB,IAAG,QACD;;6CAGD,CAAC,IAAI,MAAM,aAAa,UAAU,YAAY,MAAM,IAAI,GAAG;AAG5D,qBAAoB,SAAS;AAG7B,KAAI;AACF,kBAAgB,UAAU,MAAM,YAAY;SACtC;AAIR,SAAQ,IAAI,GAAG,kBAAkB,KAAK,KAAK,GAAG,CAAC;AAC/C,SAAQ,IAAI,IAAI,mBAAmB,WAAW,CAAC;AAC/C,SAAQ,IAAI,IAAI,mBAAmB,aAAa,CAAC;AACjD,SAAQ,IAAI,IAAI,mBAAmB,OAAO,CAAC;;AAG7C,SAASC,UACP,IACA,MAKM;CACN,IAAI,QAAQ;;;;;;CAMZ,MAAM,SAAoB,EAAE;CAE5B,MAAM,QAAkB,EAAE;AAC1B,KAAI,KAAK,QAAQ;AACf,QAAM,KAAK,eAAe;AAC1B,SAAO,KAAK,KAAK,OAAO;;AAE1B,KAAI,KAAK,MAAM;AACb,QAAM,KAAK,aAAa;AACxB,SAAO,KAAK,KAAK,KAAK;;AAExB,KAAI,KAAK,KAAK;AACZ,QAAM,KAAK;;;OAGR;AACH,SAAO,KAAK,KAAK,IAAI;;AAGvB,KAAI,MAAM,OACR,UAAS,YAAY,MAAM,KAAK,QAAQ;AAE1C,UAAS;CAET,MAAM,OAAO,GAAG,QAAQ,MAAM,CAAC,IAAI,GAAG,OAAO;AAK7C,KAAI,CAAC,KAAK,QAAQ;AAChB,UAAQ,IAAI,KAAK,qBAAqB,CAAC;AACvC;;CAGF,MAAM,YAAY,KAAK,KAAK,GAAG,MAAM;EACnC,IAAI,OAAO,IAAI,EAAE,CAAC;EAClB,KAAK,EAAE,KAAK;EACZ,IAAI,YAAY,EAAE,WAAW,GAAG,CAAC;EACjC,EAAE,WAAW,WAAW,MAAM,MAAM,EAAE,OAAO,GAAG,MAAM,OAAO,EAAE,OAAO;EACtE,IAAI,EAAE,KAAK;EACX,OAAO,EAAE,cAAc;EACvB,QAAQ,EAAE,YAAY;EACvB,CAAC;AAEF,SAAQ,IACN,YACE;EAAC;EAAK;EAAQ;EAAQ;EAAU;EAAQ;EAAY;EAAc,EAClE,UACD,CACF;AACD,SAAQ,KAAK;AACb,SAAQ,IAAI,IAAI,KAAK,KAAK,OAAO,aAAa,CAAC;;AAGjD,SAASC,UAAQ,IAAc,YAA0B;CACvD,MAAM,UAAU,kBAAkB,IAAI,WAAW,IAAI,eAAe,IAAI,WAAW;CACnF,MAAM,OAAO,eAAe,IAAI,QAAQ,GAAG;CAC3C,MAAM,UAAU,kBAAkB,IAAI,QAAQ,GAAG;CACjD,MAAM,eAAe,gBAAgB,IAAI,QAAQ,GAAG;CACpD,MAAM,cAAc,mBAAmB,IAAI,QAAQ,GAAG;CAEtD,MAAM,iBAAiB,GACpB,QACC;yCAED,CACA,IAAI,QAAQ,GAAG;AAElB,SAAQ,KAAK;AACb,SAAQ,IAAI,OAAO,KAAK,QAAQ,eAAe,CAAC;AAChD,SAAQ,KAAK;AACb,SAAQ,IAAI,KAAK,KAAK,QAAQ,CAAC,WAAW,QAAQ,OAAO;AACzD,SAAQ,IAAI,KAAK,KAAK,QAAQ,CAAC,WAAW,QAAQ,YAAY;AAC9D,SAAQ,IAAI,KAAK,KAAK,eAAe,CAAC,IAAI,QAAQ,cAAc;AAChE,SAAQ,IAAI,KAAK,KAAK,QAAQ,CAAC,WAAW,QAAQ,OAAO;AACzD,SAAQ,IACN,KAAK,KAAK,UAAU,CAAC,SAAS,QAAQ,WAAW,WAAW,MAAM,MAAM,QAAQ,OAAO,GAAG,MAAM,OAAO,QAAQ,OAAO,GACvH;AACD,SAAQ,IACN,KAAK,KAAK,QAAQ,CAAC,WAAW,KAAK,SAAS,KAAK,KAAK,MAAM,MAAM,KAAK,EAAE,CAAC,CAAC,KAAK,KAAK,GAAG,IAAI,OAAO,GACpG;AACD,SAAQ,IACN,KAAK,KAAK,WAAW,CAAC,QAAQ,QAAQ,SAAS,QAAQ,KAAK,KAAK,GAAG,IAAI,OAAO,GAChF;AACD,SAAQ,IAAI,KAAK,KAAK,YAAY,CAAC,OAAO,eAAe;AACzD,SAAQ,IAAI,KAAK,KAAK,eAAe,CAAC,IAAI,QAAQ,YAAY,GAAG;AACjE,SAAQ,IAAI,KAAK,KAAK,WAAW,CAAC,QAAQ,QAAQ,QAAQ,WAAW,GAAG;AACxE,KAAI,QAAQ,YACV,SAAQ,IAAI,KAAK,KAAK,YAAY,CAAC,OAAO,QAAQ,QAAQ,YAAY,GAAG;AAG3E,KAAI,eAAe,QAAQ;AACzB,UAAQ,KAAK;AACb,UAAQ,IAAI,KAAK,KAAK,mBAAmB,GAAG;EAC5C,MAAM,cAAc,eAAe,KAAK,MAAM;GAC5C,IAAI,IAAI,EAAE,SAAS;GACnB,EAAE;GACF,EAAE,MAAM,SAAS,KAAK,EAAE,MAAM,MAAM,GAAG,GAAG,GAAG,QAAQ,EAAE;GACvD,EAAE,WAAW,cACT,MAAM,MAAM,EAAE,OAAO,GACrB,MAAM,OAAO,EAAE,OAAO;GAC3B,CAAC;AACF,UAAQ,IACN,YAAY;GAAC;GAAK;GAAQ;GAAS;GAAS,EAAE,YAAY,CACvD,MAAM,KAAK,CACX,KAAK,MAAM,OAAO,EAAE,CACpB,KAAK,KAAK,CACd;;AAEH,SAAQ,KAAK;;AAGf,SAAS,WAAW,IAAc,MAAoB;CACpD,MAAM,UAAU,eAAe,IAAI,KAAK;AACxC,KAAI,QAAQ,WAAW,YAAY;AACjC,UAAQ,IAAI,KAAK,WAAW,KAAK,uBAAuB,CAAC;AACzD;;CAEF,MAAM,KAAK,KAAK;AAChB,IAAG,QACD,wFACD,CAAC,IAAI,IAAI,IAAI,QAAQ,GAAG;AACzB,SAAQ,IAAI,GAAG,aAAa,KAAK,KAAK,GAAG,CAAC;;AAG5C,SAAS,aAAa,IAAc,MAAoB;CACtD,MAAM,UAAU,eAAe,IAAI,KAAK;AACxC,KAAI,QAAQ,WAAW,YAAY;AACjC,UAAQ,IAAI,KAAK,WAAW,KAAK,4BAA4B,QAAQ,OAAO,IAAI,CAAC;AACjF;;CAEF,MAAM,KAAK,KAAK;AAChB,IAAG,QACD,yFACD,CAAC,IAAI,IAAI,QAAQ,GAAG;AACrB,SAAQ,IAAI,GAAG,eAAe,KAAK,KAAK,GAAG,CAAC;;AAG9C,SAAS,QAAQ,IAAc,MAAc,SAAuB;CAClE,MAAM,UAAU,eAAe,IAAI,KAAK;CACxC,MAAM,cAAc,YAAY,QAAQ;CACxC,MAAM,aAAa,UAAU,YAAY;CACzC,MAAM,KAAK,KAAK;AAEhB,IAAG,QACD,kFACD,CAAC,IAAI,aAAa,YAAY,IAAI,QAAQ,GAAG;AAE9C,SAAQ,IAAI,GAAG,UAAU,KAAK,KAAK,GAAG,CAAC;AACvC,SAAQ,IAAI,IAAI,eAAe,QAAQ,YAAY,CAAC;AACpD,SAAQ,IAAI,IAAI,eAAe,cAAc,CAAC;;AAGhD,SAASC,SAAO,IAAc,MAAc,MAAsB;CAChE,MAAM,UAAU,eAAe,IAAI,KAAK;CAExC,MAAM,QAAkB,EAAE;CAC1B,MAAM,UAAoB,EAAE;AAE5B,MAAK,MAAM,WAAW,MAAM;EAC1B,MAAM,QAAQH,YAAU,IAAI,QAAQ;AAMpC,MALe,GACZ,QACC,iEACD,CACA,IAAI,QAAQ,IAAI,MAAM,CAEvB,SAAQ,KAAK,QAAQ;OAChB;AACL,MAAG,QACD,8DACD,CAAC,IAAI,QAAQ,IAAI,MAAM;AACxB,SAAM,KAAK,QAAQ;;;AAIvB,KAAI,MAAM,OACR,SAAQ,IAAI,GAAG,UAAU,KAAK,KAAK,CAAC,IAAI,MAAM,KAAK,MAAM,MAAM,KAAK,EAAE,CAAC,CAAC,KAAK,KAAK,GAAG,CAAC;AAExF,KAAI,QAAQ,OACV,SAAQ,IAAI,IAAI,sBAAsB,QAAQ,KAAK,KAAK,GAAG,CAAC;;AAIhE,SAAS,SAAS,IAAc,MAAc,OAAqB;AACjE,gBAAe,IAAI,KAAK;AAMxB,KAHiB,GACd,QAAQ,yCAAyC,CACjD,IAAI,MAAM,EACC;AACZ,UAAQ,MAAM,IAAI,IAAI,MAAM,oDAAoD,CAAC;AACjF,UAAQ,KAAK,EAAE;;CAGjB,MAAM,UAAUD,aAAW,IAAI,KAAK;AACpC,KAAI;AACF,KAAG,QACD,wDACD,CAAC,IAAI,OAAO,QAAQ,GAAG;AACxB,UAAQ,IAAI,GAAG,gBAAgB,KAAK,MAAM,CAAC,KAAK,OAAO,CAAC;SAClD;AACN,UAAQ,MAAM,IAAI,UAAU,MAAM,0BAA0B,CAAC;AAC7D,UAAQ,KAAK,EAAE;;;AAInB,SAAS,QACP,IACA,MACA,MACM;CACN,MAAM,UAAU,eAAe,IAAI,KAAK;AAExC,KAAI,CAAC,KAAK,eAAe,CAAC,KAAK,MAAM;AACnC,UAAQ,IAAI,KAAK,mDAAmD,CAAC;AACrE;;CAGF,MAAM,aAAa;EAAC;EAAS;EAAW;EAAmB;EAAW;AACtE,KAAI,KAAK,QAAQ,CAAC,WAAW,SAAS,KAAK,KAAK,EAAE;AAChD,UAAQ,MAAM,IAAI,iBAAiB,KAAK,KAAK,YAAY,WAAW,KAAK,KAAK,GAAG,CAAC;AAClF,UAAQ,KAAK,EAAE;;CAGjB,MAAM,KAAK,KAAK;AAChB,KAAI,KAAK,aAAa;AACpB,KAAG,QACD,oEACD,CAAC,IAAI,KAAK,aAAa,IAAI,QAAQ,GAAG;AACvC,UAAQ,IAAI,GAAG,yBAAyB,KAAK,KAAK,YAAY,GAAG,CAAC;;AAEpE,KAAI,KAAK,MAAM;AACb,KAAG,QACD,4DACD,CAAC,IAAI,KAAK,MAAM,IAAI,QAAQ,GAAG;AAChC,UAAQ,IAAI,GAAG,iBAAiB,KAAK,KAAK,KAAK,GAAG,CAAC;;;;;;;AA2BvD,SAAS,sBAAsB,SAA+B;CAC5D,MAAM,iBAAiB,KAAKK,SAAU,EAAE,WAAW,WAAW;AAC9D,KAAI,CAAC,WAAW,eAAe,CAAE,QAAO,EAAE;CAE1C,MAAM,WAAW,UAAU,QAAQ,UAAU;CAC7C,MAAM,UAAoB,EAAE;AAE5B,KAAI;AACF,OAAK,MAAM,SAAS,YAAY,eAAe,EAAE;GAC/C,MAAM,OAAO,KAAK,gBAAgB,MAAM;AACxC,OAAI;AACF,QAAI,CAAC,SAAS,KAAK,CAAC,aAAa,CAAE;WAC7B;AACN;;AAGF,OAAI,UAAU,YAAY,UAAU,QAAQ,aAAa;IACvD,MAAM,WAAW,KAAK,MAAM,QAAQ;AACpC,QAAI,WAAW,SAAS,CACtB,SAAQ,KAAK,SAAS;;;SAItB;AAGR,QAAO;;;;;;AAOT,SAAS,iBAAiB,SAAyC;CACjE,MAAM,OAAO,SAAS,QAAQ,UAAU;CAExC,MAAM,aAAa;EACjB,KAAKA,SAAU,EAAE,OAAO,KAAK;EAC7B,KAAKA,SAAU,EAAE,OAAO,MAAM,KAAK;EACnC,KAAKA,SAAU,EAAE,WAAW,KAAK;EACjC,KAAKA,SAAU,EAAE,YAAY,KAAK;EACnC;AACD,MAAK,MAAM,KAAK,WACd,KAAI,WAAW,EAAE,CAAE,QAAO;;AAK9B,SAAS,UACP,IACA,MACM;CACN,MAAM,OAAO,GACV,QACC;;;iDAID,CACA,KAAK;CAER,MAAM,UAA2B,KAAK,KAAK,YAAY;EACrD,MAAM,aAAa,WAAW,QAAQ,UAAU;EAChD,MAAM,WAAW,sBAAsB,QAAQ;EAE/C,IAAI;EACJ,IAAI;AAEJ,MAAI,WACF,YAAW;OACN;AACL,mBAAgB,iBAAiB,QAAQ;AACzC,cAAW,gBAAgB,UAAU;;EAGvC,MAAM,oBAAoB,SAAS,SAAS;AAE5C,SAAO;GACL;GACA;GACA;GACA;GACA,mBAAmB;GACpB;GACD;CAGF,MAAM,WACJ,KAAK,SACD,QAAQ,QAAQ,MAAM,EAAE,aAAa,KAAK,OAAO,GACjD;AAEN,KAAI,KAAK,MAAM;AACb,UAAQ,IACN,KAAK,UACH,SAAS,KAAK,OAAO;GACnB,MAAM,EAAE,QAAQ;GAChB,WAAW,EAAE,QAAQ;GACrB,QAAQ,EAAE,QAAQ;GAClB,QAAQ,EAAE;GACV,eAAe,EAAE,QAAQ;GACzB,gBAAgB,EAAE,iBAAiB;GACnC,qBAAqB,EAAE;GACvB,qBAAqB,EAAE;GACxB,EAAE,EACH,MACA,EACD,CACF;AACD;;CAIF,MAAM,SAAS,SAAS,QAAQ,MAAM,EAAE,aAAa,SAAS;CAC9D,MAAM,QAAQ,SAAS,QAAQ,MAAM,EAAE,aAAa,QAAQ;CAC5D,MAAM,OAAO,SAAS,QAAQ,MAAM,EAAE,aAAa,OAAO;AAE1D,SAAQ,KAAK;AACb,SAAQ,IAAI,OAAO,8BAA8B,CAAC;AAClD,SAAQ,KAAK;AACb,SAAQ,IACN,KAAK,MAAM,MAAM,UAAU,CAAC,GAAG,OAAO,OAAO,KAAK,MAAM,OAAO,kBAAkB,CAAC,GAAG,MAAM,OAAO,KAAK,MAAM,IAAI,kBAAkB,CAAC,GAAG,KAAK,SAC7I;AACD,SAAQ,KAAK;AAEb,KAAI,OAAO,QAAQ;AACjB,UAAQ,IAAI,KAAK,mCAAmC,CAAC;EACrD,MAAM,YAAY,OAAO,KAAK,MAAM;GAClC,KAAK,EAAE,QAAQ,KAAK;GACpB,IAAI,YAAY,EAAE,QAAQ,WAAW,GAAG,CAAC;GACzC,OAAO,EAAE,QAAQ,cAAc;GAC/B,EAAE,oBAAoB,MAAM,MAAM,MAAM,GAAG,IAAI,KAAK;GACrD,CAAC;AACF,UAAQ,IACN,YACE;GAAC;GAAQ;GAAQ;GAAY;GAAe,EAC5C,UACD,CACE,MAAM,KAAK,CACX,KAAK,MAAM,OAAO,EAAE,CACpB,KAAK,KAAK,CACd;AACD,UAAQ,KAAK;;AAGf,KAAI,MAAM,QAAQ;AAChB,UAAQ,IAAI,KAAK,gEAAgE,CAAC;AAClF,OAAK,MAAM,KAAK,OAAO;AACrB,WAAQ,IAAI,OAAO,KAAK,EAAE,QAAQ,KAAK,GAAG;AAC1C,WAAQ,IAAI,IAAI,qBAAqB,EAAE,QAAQ,YAAY,CAAC;AAC5D,WAAQ,IAAI,MAAM,KAAK,qBAAqB,EAAE,gBAAgB,CAAC;AAC/D,OAAI,EAAE,kBACJ,SAAQ,IAAI,MAAM,MAAM,qBAAqB,EAAE,kBAAkB,KAAK,KAAK,GAAG,CAAC;AAEjF,OAAI,KAAK,OAAO,EAAE,eAAe;IAC/B,MAAM,KAAK,KAAK;IAChB,MAAM,aAAa,UAAU,EAAE,cAAc;AAC7C,OAAG,QACD,kFACD,CAAC,IAAI,EAAE,eAAe,YAAY,IAAI,EAAE,QAAQ,GAAG;AACpD,YAAQ,IAAI,GAAG,qCAAqC,EAAE,gBAAgB,CAAC;cAC9D,EAAE,cACX,SAAQ,IAAI,IAAI,sCAAsC,EAAE,QAAQ,KAAK,GAAG,EAAE,gBAAgB,CAAC;;AAG/F,UAAQ,KAAK;;AAGf,KAAI,KAAK,QAAQ;AACf,UAAQ,IAAI,IAAI,kDAAkD,CAAC;AACnE,OAAK,MAAM,KAAK,MAAM;AACpB,WAAQ,IAAI,OAAO,KAAK,EAAE,QAAQ,KAAK,CAAC,KAAK,IAAI,EAAE,QAAQ,UAAU,GAAG;AACxE,OAAI,EAAE,kBACJ,SAAQ,IACN,MAAM,OAAO,iBAAiB,EAAE,kBAAkB,KAAK,KAAK,GAAG,CAChE;AAEH,OAAI,EAAE,QAAQ,kBAAkB,KAAK,KAAK,KAAK;AAC7C,OAAG,QACD,wFACD,CAAC,IAAI,KAAK,EAAE,KAAK,EAAE,EAAE,QAAQ,GAAG;AACjC,YAAQ,IAAI,GAAG,qDAAqD,CAAC;SAErE,SAAQ,IACN,IACE,qCAAqC,EAAE,QAAQ,KAAK,8BACrD,CACF;;AAGL,UAAQ,KAAK;;CAGf,MAAM,UAAU,KAAK,KAAK,OAAO,UAAU,OAAO,OAAO,WAAW,MAAM,OAAO,UAAU,KAAK,OAAO;AACvG,SAAQ,IAAI,IAAI,QAAQ,CAAC;AAEzB,KAAI,CAAC,KAAK,QAAQ,MAAM,SAAS,KAAK,KAAK,SAAS,IAAI;AACtD,UAAQ,KAAK;AACb,UAAQ,IAAI,KAAK,qDAAqD,CAAC;;;AAQ3E,SAAS,UACP,IACA,SACA,MACM;CACN,MAAM,MAAM,UAAU,YAAY,QAAQ,GAAG,QAAQ,KAAK;CAC1D,MAAM,YAAY,cAAc,IAAI,IAAI;AAExC,KAAI,CAAC,WAAW;AACd,MAAI,KAAK,KACP,SAAQ,IAAI,KAAK,UAAU;GAAE,OAAO;GAAY;GAAK,EAAE,MAAM,EAAE,CAAC;OAC3D;AACL,WAAQ,IAAI,KAAK,oCAAoC,MAAM,CAAC;AAC5D,WAAQ,IAAI,IAAI,wDAAwD,CAAC;;AAE3E,UAAQ,KAAK,EAAE;AACf;;AAGF,KAAI,KAAK,MAAM;AACb,UAAQ,IAAI,oBAAoB,UAAU,CAAC;AAC3C;;AAGF,SAAQ,KAAK;AACb,SAAQ,IAAI,OAAO,6BAA6B,CAAC;AACjD,SAAQ,KAAK;AACb,SAAQ,IACN,gBAAgB,UAAU,CACvB,MAAM,KAAK,CACX,KAAK,MAAM,OAAO,EAAE,CACpB,KAAK,KAAK,CACd;AACD,SAAQ,KAAK;;;;;;AAWf,SAAS,qBAAqB,SAK1B;CACF,MAAM,iBAAiB,KAAKA,SAAU,EAAE,WAAW,WAAW;AAC9D,KAAI,CAAC,WAAW,eAAe,CAAE,QAAO,EAAE;CAE1C,MAAM,UAA4F,EAAE;CACpG,MAAM,cAAc,UAAU,QAAQ,UAAU;AAEhD,KAAI;AACF,OAAK,MAAM,SAAS,YAAY,eAAe,EAAE;GAC/C,MAAM,OAAO,KAAK,gBAAgB,MAAM;AACxC,OAAI;AACF,QAAI,CAAC,SAAS,KAAK,CAAC,aAAa,CAAE;WAC7B;AACN;;AAIF,OAAI,UAAU,eAAe,CAAC,MAAM,WAAW,YAAY,CAAE;GAE7D,MAAM,YAAY,KAAK,MAAM,QAAQ;AACrC,OAAI,CAAC,WAAW,UAAU,CAAE;GAE5B,IAAI,YAAY;AAChB,OAAI;AACF,gBAAY,YAAY,UAAU,CAAC,QAChC,MAAM,EAAE,SAAS,MAAM,IAAI,EAAE,SAAS,OAAO,CAC/C,CAAC;WACI;AAIR,WAAQ,KAAK;IAAE,YAAY;IAAO,UAAU;IAAM;IAAW;IAAW,CAAC;;SAErE;AAIR,QAAO;;AAGT,SAAS,eACP,IACA,YACA,MACM;CACN,MAAM,UAAU,kBAAkB,IAAI,WAAW,IAAI,eAAe,IAAI,WAAW;AAEnF,SAAQ,KAAK;AACb,SAAQ,IAAI,OAAO,kBAAkB,QAAQ,OAAO,CAAC;AACrD,SAAQ,IAAI,cAAc,QAAQ,YAAY;AAC9C,SAAQ,KAAK;CAEb,MAAM,OAAO,qBAAqB,QAAQ;AAE1C,KAAI,KAAK,WAAW,GAAG;AACrB,UAAQ,IAAI,KAAK,2DAA2D,CAAC;AAC7E;;CAIF,MAAM,iBAAiB,KAAK,QAAQ,WAAW,QAAQ;CAGvD,MAAM,UAAU,KAAK,QAAQ,MAAM,EAAE,cAAc,eAAe;AAElE,KAAI,QAAQ,WAAW,GAAG;AACxB,UAAQ,IAAI,GAAG,qDAAqD,CAAC;AACrE,UAAQ,IAAI,IAAI,KAAK,iBAAiB,CAAC;AACvC;;AAGF,SAAQ,IAAI,WAAW,QAAQ,OAAO,iDAAiD;AACvF,SAAQ,KAAK;AAEb,MAAK,MAAM,KAAK,SAAS;AACvB,UAAQ,IAAI,OAAO,KAAK,EAAE,WAAW,GAAG;AACxC,UAAQ,IAAI,IAAI,gBAAgB,EAAE,UAAU,IAAI,EAAE,UAAU,WAAW,CAAC;;AAG1E,SAAQ,KAAK;AACb,SAAQ,IAAI,kBAAkB,iBAAiB;AAC/C,SAAQ,KAAK;AAEb,KAAI,KAAK,QAAQ;AACf,UAAQ,IAAI,KAAK,4DAA4D,CAAC;AAC9E;;AAGF,KAAI,CAAC,KAAK,KAAK;AACb,UAAQ,IACN,KACE,8EACD,CACF;AACD;;AAIF,WAAU,gBAAgB,EAAE,WAAW,MAAM,CAAC;CAE9C,IAAI,aAAa;AACjB,MAAK,MAAM,KAAK,QACd,KAAI;EACF,MAAM,QAAQ,YAAY,EAAE,UAAU;AACtC,OAAK,MAAM,KAAK,OAAO;AACrB,OAAI,CAAC,EAAE,SAAS,MAAM,IAAI,CAAC,EAAE,SAAS,OAAO,CAAE;GAC/C,MAAM,MAAM,KAAK,EAAE,WAAW,EAAE;GAChC,MAAM,OAAO,KAAK,gBAAgB,EAAE;AACpC,OAAI,CAAC,WAAW,KAAK,EAAE;AACrB,eAAW,KAAK,KAAK;AACrB,YAAQ,IAAI,GAAG,cAAc,IAAI,CAAC;AAClC;SAEA,SAAQ,IAAI,KAAK,yBAAyB,IAAI,CAAC;;UAG5C,GAAG;AACV,UAAQ,MAAM,IAAI,qBAAqB,EAAE,UAAU,IAAI,IAAI,CAAC;;AAIhE,SAAQ,KAAK;AACb,SAAQ,IAAI,GAAG,kBAAkB,WAAW,gBAAgB,iBAAiB,CAAC;;;;;AAUhF,SAAS,YAAY,GAAW,GAAmB;CACjD,MAAM,IAAI,EAAE;CACZ,MAAM,IAAI,EAAE;CACZ,MAAM,KAAiB,MAAM,KAAK,EAAE,QAAQ,IAAI,GAAG,GAAG,GAAG,MACvD,MAAM,KAAK,EAAE,QAAQ,IAAI,GAAG,GAAG,GAAG,MAAO,MAAM,IAAI,IAAI,MAAM,IAAI,IAAI,EAAG,CACzE;AACD,MAAK,IAAI,IAAI,GAAG,KAAK,GAAG,IACtB,MAAK,IAAI,IAAI,GAAG,KAAK,GAAG,IACtB,IAAG,GAAG,KACJ,EAAE,IAAI,OAAO,EAAE,IAAI,KACf,GAAG,IAAI,GAAG,IAAI,KACd,IAAI,KAAK,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,GAAG,IAAI,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG;AAGpE,QAAO,GAAG,GAAG;;;;;AAMf,SAAS,mBAAmB,UAAkB,QAAyB;AACrE,QAAO,SAAS,aAAa,CAAC,SAAS,OAAO,aAAa,CAAC;;AAG9D,SAAgB,MAAM,IAAc,OAAqB;CACvD,MAAM,MAAM,GACT,QAAQ,0EAA0E,CAClF,KAAK;AAER,KAAI,CAAC,IAAI,QAAQ;AACf,UAAQ,MAAM,IAAI,6DAA6D,CAAC;AAChF,UAAQ,KAAK,EAAE;;CAGjB,MAAM,IAAI,MAAM,MAAM,CAAC,aAAa;CAGpC,MAAM,QAAQL,aAAW,IAAI,MAAM;AACnC,KAAI,OAAO;AACT,UAAQ,OAAO,MAAM,MAAM,YAAY,KAAK;AAC5C;;CAIF,MAAM,UAAU,IAAI,QACjB,MACC,mBAAmB,EAAE,MAAM,EAAE,IAC7B,mBAAmB,EAAE,cAAc,EAAE,IACrC,mBAAmB,SAAS,EAAE,UAAU,EAAE,EAAE,CAC/C;AAED,KAAI,QAAQ,WAAW,GAAG;AAExB,UAAQ,OAAO,MAAM,QAAQ,GAAG,YAAY,KAAK;AACjD;;AAGF,KAAI,QAAQ,SAAS,GAAG;AAEtB,UAAQ,MAAM,IAAI,eAAe,MAAM,YAAY,QAAQ,OAAO,cAAc,CAAC;AACjF,UAAQ,SAAS,GAAG,MAAM;AACxB,WAAQ,MACN,KAAK,IAAI,OAAO,IAAI,EAAE,CAAC,SAAS,EAAE,CAAC,CAAC,IAAI,KAAK,EAAE,KAAK,OAAO,GAAG,CAAC,CAAC,IAAI,IAAI,YAAY,EAAE,WAAW,GAAG,CAAC,GACtG;IACD;AACF,UAAQ,OAAO;AACf,UAAQ,MAAM,IAAI,gDAAgD,CAAC;AACnE,UAAQ,KAAK,EAAE;;CAIjB,MAAM,SAAS,IACZ,KAAK,MAAM;EACV,MAAM,WAAW,YAAY,GAAG,EAAE,KAAK,aAAa,CAAC;EACrD,MAAM,WAAW,YAAY,GAAG,EAAE,aAAa,aAAa,CAAC;AAC7D,SAAO;GAAE,SAAS;GAAG,MAAM,KAAK,IAAI,UAAU,SAAS;GAAE;GACzD,CACD,MAAM,GAAG,MAAM,EAAE,OAAO,EAAE,KAAK;CAGlC,MAAM,YAAY;CAClB,MAAM,cACJ,OAAO,QAAQ,MAAM,EAAE,QAAQ,UAAU,CAAC,SAAS,IAC/C,OAAO,QAAQ,MAAM,EAAE,QAAQ,UAAU,CAAC,MAAM,GAAG,EAAE,GACrD,OAAO,MAAM,GAAG,EAAE;AAExB,SAAQ,MAAM,IAAI,uBAAuB,MAAM,KAAK,CAAC;AACrD,KAAI,YAAY,QAAQ;AACtB,UAAQ,MAAM,KAAK,kBAAkB,CAAC;AACtC,OAAK,MAAM,KAAK,YACd,SAAQ,MACN,OAAO,KAAK,EAAE,QAAQ,KAAK,OAAO,GAAG,CAAC,CAAC,IAAI,IAAI,YAAY,EAAE,QAAQ,WAAW,GAAG,CAAC,GACrF;AAEH,UAAQ,OAAO;AACf,UAAQ,MAAM,IAAI,iDAAiD,CAAC;;AAEtE,SAAQ,KAAK,EAAE;;AAOjB,SAAgB,wBACd,YACA,OACM;AAEN,YACG,QAAQ,aAAa,CACrB,YAAY,mDAAmD,CAC/D,OAAO,iBAAiB,+BAA+B,CACvD,OACC,iBACA,8DACA,QACD,CACA,OAAO,yBAAyB,8BAA8B,CAC9D,QAAQ,SAAiB,SAAiE;AACzF,SAAO,OAAO,EAAE,SAAS,KAAK;GAC9B;AAGJ,YACG,QAAQ,OAAO,CACf,YAAY,2BAA2B,CACvC,OAAO,qBAAqB,sCAAsC,CAClE,OAAO,eAAe,gBAAgB,CACtC,OAAO,iBAAiB,iBAAiB,CACzC,QAAQ,SAA2D;AAClE,YAAQ,OAAO,EAAE,KAAK;GACtB;AAGJ,YACG,QAAQ,cAAc,CACtB,YAAY,kCAAkC,CAC9C,QAAQ,SAAiB;AACxB,YAAQ,OAAO,EAAE,KAAK;GACtB;AAGJ,YACG,QAAQ,iBAAiB,CACzB,YAAY,oBAAoB,CAChC,QAAQ,SAAiB;AACxB,aAAW,OAAO,EAAE,KAAK;GACzB;AAGJ,YACG,QAAQ,mBAAmB,CAC3B,YAAY,+CAA+C,CAC3D,QAAQ,SAAiB;AACxB,eAAa,OAAO,EAAE,KAAK;GAC3B;AAGJ,YACG,QAAQ,yBAAyB,CACjC,YAAY,qCAAqC,CACjD,QAAQ,MAAc,YAAoB;AACzC,UAAQ,OAAO,EAAE,MAAM,QAAQ;GAC/B;AAGJ,YACG,QAAQ,uBAAuB,CAC/B,YAAY,oCAAoC,CAChD,QAAQ,MAAc,SAAmB;AACxC,WAAO,OAAO,EAAE,MAAM,KAAK;GAC3B;AAGJ,YACG,QAAQ,uBAAuB,CAC/B,YAAY,6CAA6C,CACzD,QAAQ,MAAc,UAAkB;AACvC,WAAS,OAAO,EAAE,MAAM,MAAM;GAC9B;AAGJ,YACG,QAAQ,cAAc,CACtB,YAAY,wBAAwB,CACpC,OAAO,yBAAyB,mBAAmB,CACnD,OAAO,iBAAiB,WAAW,CACnC,QAAQ,MAAc,SAAkD;AACvE,UAAQ,OAAO,EAAE,MAAM,KAAK;GAC5B;AAGJ,YACG,QAAQ,kBAAkB,CAC1B,YAAY,0EAA0E,CACtF,QAAQ,eAAuB;EAC9B,MAAM,UAAU,kBAAkB,OAAO,EAAE,WAAW;AACtD,MAAI,CAAC,SAAS;AACZ,WAAQ,MAAM,sBAAsB,aAAa;AACjD,WAAQ,KAAK,EAAE;;AAEjB,UAAQ,OAAO,MAAM,QAAQ,YAAY,KAAK;GAC9C;AAGJ,YACG,QAAQ,gBAAgB,CACxB,YACC,qEACD,CACA,OAAO,UAAU,iDAAiD,CAClE,QAAQ,SAA6B,SAA6B;AACjE,YAAU,OAAO,EAAE,SAAS,KAAK;GACjC;AAGJ,YACG,QAAQ,SAAS,CACjB,YACC,yFACD,CACA,OACC,SACA,yFACD,CACA,OAAO,UAAU,yBAAyB,CAC1C,OACC,uBACA,0CACD,CACA,QACE,SAA6D;AAC5D,YAAU,OAAO,EAAE,KAAK;GAE3B;AAGH,YACG,QAAQ,2BAA2B,CACnC,YACC,mHACD,CACA,OAAO,SAAS,oDAAoD,CACpE,OAAO,aAAa,qDAAqD,CACzE,QAEG,YACA,SACG;AACH,iBAAe,OAAO,EAAE,YAAY,KAAK;GAE5C;AAGH,YACG,QAAQ,UAAU,CAClB,YAAY,uDAAuD,CACnE,eAAe,yBAAyB,yCAAyC,CACjF,eAAe,eAAe,sDAAsD,CACpF,OAAO,iBAAiB,sEAAsE,CAC9F,QAAQ,SAA6D;AACpE,aAAW,OAAO,EAAE,KAAK;GACzB;AAGJ,YACG,QAAQ,aAAa,CACrB,YACC,iMAGD,CACA,QAAQ,UAAkB;AACzB,QAAM,OAAO,EAAE,MAAM;GACrB;;;;;;;;;;;;;;;AChmCN,MAAM,aAAa,IAAI,IAAI;CACzB;CAAO;CAAK;CAAM;CAAM;CAAO;CAAO;CAAQ;CAAM;CAAQ;CAC5D;CAAQ;CAAO;CAAO;CAAM;CAAQ;CAAO;CAAQ;CAAS;CAC5D;CAAU;CAAO;CAAS;CAAO;CAAS;CAAQ;CAAQ;CAC1D;CAAS;CAAM;CAAO;CAAK;CAAO;CAAM;CAAQ;CAAM;CAAO;CAC7D;CAAQ;CAAO;CAAS;CAAQ;CAAS;CAAO;CAAQ;CAAO;CAC/D;CAAS;CAAO;CAAO;CAAM;CAAO;CAAQ;CAAQ;CAAQ;CAC5D;CAAS;CAAS;CAAU;CAAQ;CAAQ;CAAQ;CAAW;CAC/D;CAAO;CAAM;CAAO;CAAM;CAAQ;CAAQ;CAAM;CAAW;CAAM;CACjE;CAAM;CAAM;CAAM;CAAM;CAAM;CAAM;CAAO;CAAO;CAAQ;CAC1D;CAAQ;CAAQ;CAAQ;CAAO;CAAO;CAAQ;CAAS;CAAQ;CAC/D;CAAQ;CAAQ;CAAS;CAAW;CAAQ;CAAQ;CAAO;CAC3D;CAAQ;CAAO;CAAO;CAAM;CAAM;CAAM;CAAQ;CAChD;CAAU;CAAU;CAAS;CAAQ;CAAS;CAAO;CAAQ;CAC7D;CAAQ;CAAQ;CAAQ;CAAS;CAAO;CAAQ;CAAQ;CAAO;CAC/D;CAAQ;CAAQ;CAAO;CAAQ;CAAQ;CAAQ;CAAO;CAAQ;CAC9D;CAAQ;CAAO;CAAO;CAAO;CAAO;CAAQ;CAAS;CAAO;CAC5D;CAAQ;CAAS;CAAQ;CAAO;CAAS;CAAQ;CAAO;CAAO;CAC/D;CAAO;CAAM;CAAQ;CAAa;CAAQ;CAC1C;CAAM;CAAM;CAAM;CAAO;CAAS;CAAO;CAAS;CAClD;CAAS;CAAU;CAAS;CAAQ;CAAU;CAAU;CACxD;CAAQ;CAAQ;CAAY;CAAW;CAAW;CAAQ;CAC1D;CAAS;CAAU;CAAS;CAAQ;CAAS;CAAW;CAAO;CAC/D;CAAQ;CAAQ;CAAS;CAAQ;CAAQ;CAAQ;CAAS;CAC1D;CAAS;CAAO;CAAO;CAAS;CAAQ;CAAQ;CAAO;CACvD;CAAS;CAAQ;CAAO;CAAQ;CAAO;CAAS;CAChD;CAAY;CAAW;CAAc;CAAW;CAAU;CAC1D;CAAQ;CAAS;CAAQ;CAAS;CAAS;CAAS;CACpD;CAAY;CAAW;CAAa;CAAQ;CAAS;CACrD;CAAY;CAAe;CAAa;CAAW;CACnD;CAAY;CAAa;CAAa;CAAU;CAAW;CAC3D;CAAY;CAAS;CAAW;CAAa;CAAc;CAC3D;CAAc;CAAW;CAAS;CAAU;CAAU;CACtD;CAAU;CAAU;CAAU;CAAW;CAAU;CAAU;CAC7D;CAAY;CAAO;CAAO;CAAO;CAAM;CAAM;CAC9C,CAAC;;;;AASF,SAAS,YAAY,KAA6C;CAChE,MAAM,MAAM,IAAI;AAChB,KAAI,CAAC,IAAK,QAAO;CAEjB,MAAM,UAAU,IAAI;AAGpB,KAAI,MAAM,QAAQ,QAAQ,EAAE;EAC1B,MAAM,QAAkB,EAAE;AAC1B,OAAK,MAAM,SAAS,SAAS;GAC3B,MAAM,IAAI;AACV,OAAI,EAAE,SAAS,UAAU,OAAO,EAAE,SAAS,SACzC,OAAM,KAAK,EAAE,KAAK;;AAGtB,SAAO,MAAM,KAAK,IAAI,IAAI;;AAI5B,KAAI,OAAO,YAAY,SACrB,QAAO,WAAW;AAGpB,QAAO;;;;;AAMT,SAAS,SAAS,MAAwB;AACxC,QAAO,KACJ,aAAa,CACb,QAAQ,eAAe,IAAI,CAC3B,MAAM,MAAM,CACZ,QAAQ,MAAM;AACb,MAAI,EAAE,SAAS,EAAG,QAAO;AACzB,MAAI,QAAQ,KAAK,EAAE,CAAE,QAAO;AAC5B,MAAI,WAAW,IAAI,EAAE,CAAE,QAAO;AAC9B,SAAO;GACP;;;;;;;;;AAcN,SAAgB,qBAAqB,YAAmC;CACtE,MAAM,cAAc,KAClB,SAAS,EACT,WACA,YACA,YACA,WACD;AAED,KAAI,CAAC,WAAW,YAAY,CAAE,QAAO;CAErC,IAAI;AACJ,KAAI;AACF,YAAU,YAAY,YAAY,CAAC,QAAQ,MAAM,EAAE,SAAS,SAAS,CAAC;SAChE;AACN,SAAO;;AAGT,KAAI,CAAC,QAAQ,OAAQ,QAAO;AAc5B,QAXe,QACZ,KAAK,MAAM;EACV,MAAM,WAAW,KAAK,aAAa,EAAE;AACrC,MAAI;AACF,UAAO;IAAE,MAAM;IAAU,OAAO,SAAS,SAAS,CAAC;IAAS;UACtD;AACN,UAAO;IAAE,MAAM;IAAU,OAAO;IAAG;;GAErC,CACD,MAAM,GAAG,MAAM,EAAE,QAAQ,EAAE,MAAM,CAEtB,GAAG;;;;;;;;;;;;AAanB,SAAgB,iBACd,WACA,QAAgB,IACN;AACV,KAAI,CAAC,WAAW,UAAU,CAAE,QAAO,EAAE;CAErC,IAAI;AACJ,KAAI;AACF,QAAM,aAAa,WAAW,OAAO;SAC/B;AACN,SAAO,EAAE;;CAGX,MAAM,QAAQ,IAAI,MAAM,KAAK,CAAC,QAAQ,MAAM,EAAE,MAAM,CAAC,SAAS,EAAE;CAChE,MAAM,WAAqB,EAAE;CAC7B,MAAM,QAAQ,QAAQ;AAGtB,MAAK,IAAI,IAAI,MAAM,SAAS,GAAG,KAAK,KAAK,SAAS,SAAS,OAAO,KAAK;EACrE,IAAI;AACJ,MAAI;AACF,SAAM,KAAK,MAAM,MAAM,GAAG;UACpB;AACN;;EAGF,MAAM,OAAO,IAAI;AACjB,MAAI,SAAS,eAAe,SAAS,OAAQ;EAE7C,MAAM,OAAO,YAAY,IAAI;AAC7B,MAAI,QAAQ,KAAK,MAAM,CAAC,SAAS,EAE/B,UAAS,QAAQ,KAAK;;AAI1B,QAAO;;;;;;;;;;;;;;;;AAiBT,SAAgB,aAAa,UAAoB,MAA4B;CAC3E,MAAM,WAAW,MAAM,YAAY;CACnC,MAAM,YAAY,MAAM,aAAa;AAErC,KAAI,CAAC,SAAS,OAAQ,QAAO;CAG7B,MAAM,uBAAO,IAAI,KAAqB;AACtC,MAAK,MAAM,OAAO,SAChB,MAAK,MAAM,SAAS,SAAS,IAAI,CAC/B,MAAK,IAAI,QAAQ,KAAK,IAAI,MAAM,IAAI,KAAK,EAAE;AAI/C,KAAI,CAAC,KAAK,KAAM,QAAO;CAGvB,MAAM,WAAW,MAAM,KAAK,KAAK,SAAS,CAAC,CACxC,MAAM,GAAG,MAAM,EAAE,KAAK,EAAE,GAAG,CAC3B,MAAM,GAAG,SAAS,CAClB,KAAK,CAAC,UAAU,KAAK;AAExB,KAAI,CAAC,SAAS,OAAQ,QAAO;CAG7B,IAAI,OAAO,SAAS,KAAK,IAAI;AAC7B,KAAI,KAAK,SAAS,UAChB,QAAO,KAAK,MAAM,GAAG,UAAU,CAAC,QAAQ,WAAW,GAAG;AAGxD,QAAO,QAAQ;;;;;ACtLjB,SAASM,aAAW,IAAc,MAAsC;AACtE,QAAO,GACJ,QACC,qFACD,CACA,IAAI,KAAK;;AAGd,SAAS,YAAY,QAAwB;AAC3C,SAAQ,QAAR;EACE,KAAK,YACH,QAAO,MAAM,MAAM,OAAO;EAC5B,KAAK,YACH,QAAO,MAAM,KAAK,OAAO;EAC3B,QACE,QAAO,MAAM,OAAO,OAAO;;;;;;;;;AAUjC,SAASC,cAAY,MAAsB;AACzC,QAAO,KACJ,QAAQ,MAAM,IAAI,CAClB,QAAQ,UAAU,MAAM,EAAE,aAAa,CAAC;;;;;;;;AAS7C,SAAS,YAAY,SAA6B;AAChD,QAAO,KACL,SAAS,EACT,WACA,YACA,QAAQ,aACR,QACD;;;;;;;AAQH,SAAS,eAAe,QAAgB,MAAc,WAA2B;AAE/E,QAAO,GADG,OAAO,OAAO,CAAC,SAAS,GAAG,IAAI,CAC7B,KAAK,KAAK,KAAK,UAAU;;;;;;AAOvC,SAAS,eACP,IACA,SACA,gBACY;CACZ,IAAI;AAEJ,KAAI,mBAAmB,SACrB,WAAU,GACP,QACC,2EACD,CACA,IAAI,QAAQ,GAAG;MACb;EACL,MAAM,MAAM,SAAS,gBAAgB,GAAG;AACxC,MAAI,MAAM,IAAI,EAAE;AACd,WAAQ,MAAM,IAAI,2BAA2B,iBAAiB,CAAC;AAC/D,WAAQ,KAAK,EAAE;;AAEjB,YAAU,GACP,QAAQ,6DAA6D,CACrE,IAAI,QAAQ,IAAI,IAAI;;AAGzB,KAAI,CAAC,SAAS;AACZ,UAAQ,MACN,IAAI,WAAW,eAAe,wBAAwB,QAAQ,OAAO,CACtE;AACD,UAAQ,KAAK,EAAE;;AAGjB,QAAO;;AAOT,SAAS,QACP,IACA,aACA,MACM;CACN,MAAM,QAAQ,SAAS,KAAK,SAAS,MAAM,GAAG;CAC9C,MAAM,SAAoB,EAAE;CAE5B,IAAI,QAAQ;;;;;CAMZ,MAAM,QAAkB,EAAE;AAC1B,KAAI,aAAa;EACf,MAAM,UAAUD,aAAW,IAAI,YAAY;AAC3C,MAAI,CAAC,SAAS;AACZ,WAAQ,MAAM,IAAI,sBAAsB,cAAc,CAAC;AACvD,WAAQ,KAAK,EAAE;;AAEjB,QAAM,KAAK,mBAAmB;AAC9B,SAAO,KAAK,QAAQ,GAAG;;AAEzB,KAAI,KAAK,QAAQ;AACf,QAAM,KAAK,eAAe;AAC1B,SAAO,KAAK,KAAK,OAAO;;AAG1B,KAAI,MAAM,OACR,UAAS,YAAY,MAAM,KAAK,QAAQ;AAE1C,UAAS;AACT,UAAS,UAAU;CAEnB,MAAM,OAAO,GAAG,QAAQ,MAAM,CAAC,IAAI,GAAG,OAAO;AAK7C,KAAI,CAAC,KAAK,QAAQ;AAChB,UAAQ,IAAI,KAAK,qBAAqB,CAAC;AACvC;;CAGF,MAAM,cAAc,CAAC;CACrB,MAAM,UAAU,cACZ;EAAC;EAAK;EAAW;EAAQ;EAAS;EAAU;EAAS,GACrD;EAAC;EAAK;EAAQ;EAAS;EAAU;EAAS;CAE9C,MAAM,YAAY,KAAK,KAAK,MAAM;EAChC,MAAM,QACJ,EAAE,MAAM,SAAS,KAAK,EAAE,MAAM,MAAM,GAAG,GAAG,GAAG,QAAQ,EAAE;EACzD,MAAM,SACJ,EAAE,eAAe,OAAO,IAAI,EAAE,YAAY,gBAAgB,CAAC,GAAG,IAAI,IAAI;AACxE,MAAI,YACF,QAAO;GACL,IAAI,IAAI,EAAE,SAAS;GACnB,EAAE;GACF,EAAE;GACF;GACA,YAAY,EAAE,OAAO;GACrB;GACD;AAEH,SAAO;GAAC,IAAI,IAAI,EAAE,SAAS;GAAE,EAAE;GAAM;GAAO,YAAY,EAAE,OAAO;GAAE;GAAO;GAC1E;AAEF,SAAQ,KAAK;AACb,KAAI,aAAa;EACf,MAAM,UAAUA,aAAW,IAAI,YAAY;AAC3C,UAAQ,IAAI,KAAK,KAAK,QAAQ,aAAa,CAAC,YAAY;AACxD,UAAQ,KAAK;;AAEf,SAAQ,IAAI,YAAY,SAAS,UAAU,CAAC;AAC5C,SAAQ,KAAK;AACb,SAAQ,IAAI,IAAI,KAAK,KAAK,OAAO,4BAA4B,MAAM,GAAG,CAAC;;AAGzE,SAAS,QACP,IACA,aACA,eACM;CACN,MAAM,UAAUA,aAAW,IAAI,YAAY;AAC3C,KAAI,CAAC,SAAS;AACZ,UAAQ,MAAM,IAAI,sBAAsB,cAAc,CAAC;AACvD,UAAQ,KAAK,EAAE;;CAGjB,MAAM,UAAU,eAAe,IAAI,SAAS,cAAc;AAE1D,SAAQ,KAAK;AACb,SAAQ,IAAI,OAAO,cAAc,QAAQ,OAAO,IAAI,QAAQ,QAAQ,CAAC;AACrE,SAAQ,KAAK;AACb,SAAQ,IAAI,KAAK,KAAK,WAAW,CAAC,OAAO,QAAQ,aAAa,IAAI,QAAQ,KAAK,GAAG;AAClF,SAAQ,IAAI,KAAK,KAAK,QAAQ,CAAC,UAAU,QAAQ,OAAO;AACxD,SAAQ,IAAI,KAAK,KAAK,UAAU,CAAC,QAAQ,YAAY,QAAQ,OAAO,GAAG;AACvE,SAAQ,IAAI,KAAK,KAAK,YAAY,CAAC,MAAM,QAAQ,WAAW;AAC5D,SAAQ,IAAI,KAAK,KAAK,QAAQ,CAAC,UAAU,QAAQ,OAAO;AACxD,KAAI,QAAQ,kBACV,SAAQ,IAAI,KAAK,KAAK,aAAa,CAAC,KAAK,IAAI,QAAQ,kBAAkB,GAAG;AAE5E,KAAI,QAAQ,eAAe,KACzB,SAAQ,IAAI,KAAK,KAAK,UAAU,CAAC,QAAQ,QAAQ,YAAY,gBAAgB,GAAG;AAElF,SAAQ,IAAI,KAAK,KAAK,WAAW,CAAC,OAAO,QAAQ,QAAQ,WAAW,GAAG;AACvE,KAAI,QAAQ,UACV,SAAQ,IAAI,KAAK,KAAK,UAAU,CAAC,QAAQ,QAAQ,QAAQ,UAAU,GAAG;AAExE,SAAQ,KAAK;;;;;;AAOf,SAAS,UACP,IACA,aACA,gBACA,SACM;CACN,MAAM,UAAUA,aAAW,IAAI,YAAY;AAC3C,KAAI,CAAC,SAAS;AACZ,UAAQ,MAAM,IAAI,sBAAsB,cAAc,CAAC;AACvD,UAAQ,KAAK,EAAE;;CAGjB,MAAM,UAAU,eAAe,IAAI,SAAS,eAAe;CAC3D,MAAM,WAAW,YAAY,QAAQ;AAErC,KAAI,CAAC,WAAW,SAAS,EAAE;AACzB,UAAQ,MAAM,IAAI,8BAA8B,WAAW,CAAC;AAC5D,UAAQ,KAAK,EAAE;;CAIjB,MAAM,YAAYC,cAAY,QAAQ;CACtC,MAAM,cAAc,eAAe,QAAQ,QAAQ,QAAQ,MAAM,UAAU;CAC3E,MAAM,UAAU,KAAK,UAAU,QAAQ,SAAS;CAChD,MAAM,UAAU,KAAK,UAAU,YAAY;AAG3C,KAAI,WAAW,QAAQ,EACrB;MAAI,YAAY,QACd,KAAI;AACF,cAAW,SAAS,QAAQ;WACrB,GAAG;AACV,WAAQ,MAAM,IAAI,0BAA0B,IAAI,CAAC;AACjD,WAAQ,KAAK,EAAE;;QAGd;AAEL,UAAQ,IACN,KAAK,4CAA4C,QAAQ,WAAW,CACrE;AACD,UAAQ,IACN,KAAK,0DAA0D,CAChE;;AAIH,KAAI,WAAW,QAAQ,CACrB,KAAI;EAEF,MAAM,QADU,aAAa,SAAS,OAAO,CACvB,MAAM,KAAK;EACjC,IAAI,YAAY;EAChB,MAAM,UAAU,MAAM,KAAK,SAAS;AAClC,OAAI,CAAC,aAAa,KAAK,WAAW,KAAK,EAAE;AACvC,gBAAY;AACZ,WAAO,KAAK;;AAEd,UAAO;IACP;AAEF,MAAI,CAAC,UACH,SAAQ,QAAQ,KAAK,aAAa,GAAG;AAEvC,gBAAc,SAAS,QAAQ,KAAK,KAAK,EAAE,OAAO;UAC3C,GAAG;AACV,UAAQ,MAAM,IAAI,gCAAgC,IAAI,CAAC;;CAM3D,MAAM,iBAAiB,QACpB,aAAa,CACb,QAAQ,eAAe,IAAI,CAC3B,QAAQ,YAAY,GAAG;AAE1B,IAAG,QACD,qEACD,CAAC,IAAI,gBAAgB,WAAW,aAAa,QAAQ,GAAG;AAEzD,SAAQ,KAAK;AACb,SAAQ,IAAI,GAAG,cAAc,QAAQ,OAAO,WAAW,CAAC;AACxD,SAAQ,IAAI,KAAK,KAAK,OAAO,CAAC,KAAK,QAAQ,WAAW;AACtD,SAAQ,IAAI,KAAK,KAAK,OAAO,CAAC,KAAK,cAAc;AACjD,SAAQ,IAAI,KAAK,KAAK,QAAQ,CAAC,IAAI,iBAAiB;AACpD,SAAQ,IAAI,KAAK,KAAK,SAAS,CAAC,GAAG,YAAY;AAC/C,SAAQ,KAAK;;;;;;AAOf,SAAS,QACP,IACA,aACA,gBACA,MACM;CACN,MAAM,UAAUD,aAAW,IAAI,YAAY;AAC3C,KAAI,CAAC,SAAS;AACZ,UAAQ,MAAM,IAAI,sBAAsB,cAAc,CAAC;AACvD,UAAQ,KAAK,EAAE;;CAGjB,MAAM,UAAU,eAAe,IAAI,SAAS,eAAe;CAG3D,MAAM,iBAAiB,qBAAqB,QAAQ,YAAY;AAEhE,KAAI,CAAC,gBAAgB;AACnB,UAAQ,IAAI,KAAK,4CAA4C,cAAc,CAAC;AAC5E,UAAQ,IAAI,kBAAkB;AAC9B;;CAIF,MAAM,WAAW,iBAAiB,eAAe;AAEjD,KAAI,SAAS,SAAS,GAAG;AACvB,UAAQ,IAAI,KAAK,6BAA6B,SAAS,OAAO,iBAAiB,CAAC;AAChF,UAAQ,IAAI,kBAAkB;AAC9B;;CAIF,MAAM,gBAAgB,aAAa,SAAS;AAE5C,SAAQ,IAAI,cAAc;AAE1B,KAAI,KAAK,OAAO;AACd,UAAQ,KAAK;AACb,UAAQ,IAAI,IAAI,+BAA+B,QAAQ,OAAO,KAAK,CAAC;AACpE,YAAU,IAAI,aAAa,OAAO,QAAQ,OAAO,EAAE,cAAc;;;AAQrE,SAAS,UAAU,IAAc,SAAyB;AACxD,IAAG,QAAQ,+CAA+C,CAAC,IAAI,QAAQ;AAEvE,QADY,GAAG,QAAQ,qCAAqC,CAAC,IAAI,QAAQ,CAC9D;;AAGb,SAAS,eAAe,IAAc,WAA6B;AASjE,QARa,GACV,QACC;;;wBAID,CACA,IAAI,UAAU,CACL,KAAK,MAAM,EAAE,KAAK;;;;;;;;;;;AAgBhC,SAAS,OACP,IACA,aACA,eACA,SACM;CACN,MAAM,UAAUA,aAAW,IAAI,YAAY;AAC3C,KAAI,CAAC,SAAS;AACZ,UAAQ,MAAM,IAAI,sBAAsB,cAAc,CAAC;AACvD,UAAQ,KAAK,EAAE;;CAGjB,MAAM,UAAU,eAAe,IAAI,SAAS,cAAc;AAG1D,KAAI,QAAQ,WAAW,GAAG;EACxB,MAAM,UAAU,eAAe,IAAI,QAAQ,GAAG;AAC9C,UAAQ,KAAK;AACb,MAAI,QAAQ,WAAW,EACrB,SAAQ,IAAI,IAAI,cAAc,QAAQ,OAAO,eAAe,CAAC;MAE7D,SAAQ,IACN,KAAK,KAAK,YAAY,QAAQ,SAAS,CAAC,SAAS,QAAQ,KAAK,MAAM,MAAM,KAAK,EAAE,CAAC,CAAC,KAAK,KAAK,GAC9F;AAEH,UAAQ,KAAK;AACb;;CAIF,MAAM,OAAO,QACV,SAAS,MAAM,EAAE,MAAM,IAAI,CAAC,CAC5B,KAAK,MAAM,EAAE,MAAM,CAAC,aAAa,CAAC,CAClC,QAAQ,MAAM,EAAE,SAAS,EAAE;AAE9B,KAAI,KAAK,WAAW,GAAG;AACrB,UAAQ,IAAI,KAAK,0BAA0B,CAAC;AAC5C;;CAGF,MAAM,QAAkB,EAAE;CAC1B,MAAM,UAAoB,EAAE;AAE5B,MAAK,MAAM,WAAW,MAAM;EAC1B,MAAM,QAAQ,UAAU,IAAI,QAAQ;AAIpC,MAHe,GACZ,QAAQ,iEAAiE,CACzE,IAAI,QAAQ,IAAI,MAAM,CAEvB,SAAQ,KAAK,QAAQ;OAChB;AACL,MAAG,QACD,8DACD,CAAC,IAAI,QAAQ,IAAI,MAAM;AACxB,SAAM,KAAK,QAAQ;;;AAIvB,SAAQ,KAAK;AACb,KAAI,MAAM,OACR,SAAQ,IACN,GAAG,qBAAqB,QAAQ,OAAO,IAAI,MAAM,KAAK,MAAM,MAAM,KAAK,EAAE,CAAC,CAAC,KAAK,KAAK,GAAG,CACzF;AAEH,KAAI,QAAQ,OACV,SAAQ,IAAI,IAAI,sBAAsB,QAAQ,KAAK,KAAK,GAAG,CAAC;CAG9D,MAAM,UAAU,eAAe,IAAI,QAAQ,GAAG;AAC9C,SAAQ,IACN,KAAK,KAAK,YAAY,CAAC,GAAG,QAAQ,KAAK,MAAM,MAAM,KAAK,EAAE,CAAC,CAAC,KAAK,KAAK,GACvE;AACD,SAAQ,KAAK;;;;;;;;AAaf,SAAS,SACP,IACA,aACA,eACA,mBACA,MACM;CACN,MAAM,UAAUA,aAAW,IAAI,YAAY;AAC3C,KAAI,CAAC,SAAS;AACZ,UAAQ,MAAM,IAAI,sBAAsB,cAAc,CAAC;AACvD,UAAQ,KAAK,EAAE;;CAGjB,MAAM,UAAU,eAAe,IAAI,SAAS,cAAc;CAG1D,MAAM,gBAAgB,GACnB,QAAQ,6DAA6D,CACrE,IAAI,kBAAkB;AAEzB,KAAI,CAAC,eAAe;AAClB,UAAQ,MAAM,IAAI,6BAA6B,oBAAoB,CAAC;AACpE,UAAQ,KAAK,EAAE;;CAGjB,MAAM,aAAa;EAAC;EAAW;EAAa;EAAY;CACxD,MAAM,WAAW,KAAK,QAAQ;AAC9B,KAAI,CAAC,WAAW,SAAS,SAAS,EAAE;AAClC,UAAQ,MAAM,IAAI,sBAAsB,SAAS,YAAY,WAAW,KAAK,KAAK,GAAG,CAAC;AACtF,UAAQ,KAAK,EAAE;;AAGjB,KAAI;AACF,KAAG,QACD;4BAED,CAAC,IAAI,QAAQ,IAAI,cAAc,IAAI,UAAU,KAAK,KAAK,CAAC;SACnD;AAEN,UAAQ,IAAI,KAAK,mCAAmC,QAAQ,OAAO,KAAK,oBAAoB,CAAC;AAC7F;;AAGF,SAAQ,KAAK;AACb,SAAQ,IACN,GAAG,qBAAqB,QAAQ,OAAO,IAAI,QAAQ,KAAK,MAAM,cAAc,aAAa,IAAI,kBAAkB,GAAG,CACnH;AACD,SAAQ,IAAI,IAAI,gBAAgB,WAAW,CAAC;AAC5C,SAAQ,KAAK;;;;;;;;;AAcf,SAAS,qBAAoC;CAC3C,MAAM,MAAM,QAAQ,KAAK;CACzB,MAAM,oBAAoB,KAAK,SAAS,EAAE,WAAW,WAAW;AAEhE,KAAI,CAAC,WAAW,kBAAkB,CAAE,QAAO;CAI3C,MAAM,kBAAkB,IAAI,QAAQ,aAAa,IAAI;CAErD,IAAI,aAA4B;AAEhC,KAAI;EACF,MAAM,UAAU,YAAY,kBAAkB;AAG9C,MAAI,QAAQ,SAAS,gBAAgB,CACnC,cAAa;MAMb,MAAK,MAAM,SAAS,SAAS;GAC3B,MAAM,OAAO,KAAK,mBAAmB,MAAM;AAC3C,OAAI;AACF,QAAI,CAAC,SAAS,KAAK,CAAC,aAAa,CAAE;WAC7B;AACN;;AAMF,OAFkB,MAAM,QAAQ,OAAO,GAAG,KACzB,gBAAgB,QAAQ,OAAO,GAAG,EACvB;AAC1B,iBAAa;AACb;;;SAIA;AACN,SAAO;;AAGT,KAAI,CAAC,WAAY,QAAO;CAExB,MAAM,WAAW,KAAK,mBAAmB,YAAY,QAAQ;AAC7D,QAAO,WAAW,SAAS,GAAG,WAAW;;;;;AAM3C,SAAS,mBAAmB,UAAiC;CAC3D,IAAI;AACJ,KAAI;AACF,YAAU,YAAY,SAAS;SACzB;AACN,SAAO;;CAGT,MAAM,UAAU,QAAQ,QAAQ,MAAM,EAAE,SAAS,MAAM,CAAC;AACxD,KAAI,QAAQ,WAAW,EAAG,QAAO;CAEjC,IAAI,aAA4B;CAChC,IAAI,cAAc;AAElB,MAAK,MAAM,QAAQ,SAAS;EAC1B,MAAM,OAAO,KAAK,UAAU,KAAK;AACjC,MAAI;GACF,MAAM,EAAE,YAAY,SAAS,KAAK;AAClC,OAAI,UAAU,aAAa;AACzB,kBAAc;AACd,iBAAa;;UAET;;AAKV,QAAO;;;;;;AAOT,SAAS,oBAAoB,UAAkB,eAAgC;CAG7E,MAAM,UAAU,SAAS,QAAQ,iBAAiB,IAAI,CAAC,MAAM,IAAI;CACjE,MAAM,UAAU,KAAK,QAAQ,EAAE,kBAAkB,UAAU;AAE3D,KAAI,CAAC,WAAW,QAAQ,CAAE,QAAO;AAEjC,KAAI;EACF,MAAM,EAAE,YAAY,SAAS,QAAQ;AAErC,SADc,KAAK,KAAK,GAAG,UACZ,gBAAgB;SACzB;AACN,SAAO;;;;;;AAOX,SAAS,wBAAwB,UAAwB;CACvD,MAAM,UAAU,SAAS,QAAQ,iBAAiB,IAAI,CAAC,MAAM,IAAI;CACjE,MAAM,UAAU,KAAK,QAAQ,EAAE,kBAAkB,UAAU;AAC3D,KAAI;AAEF,gBAAc,SAAS,OAAO,KAAK,KAAK,CAAC,EAAE,OAAO;SAC5C;;;;;;;;AAWV,SAAS,cAAc,SAAiB,MAAiC;CACvE,MAAM,gBAAgB,SAAS,KAAK,UAAU,OAAO,GAAG;CAGxD,MAAM,WAAW,oBAAoB;AACrC,KAAI,CAAC,SAEH,SAAQ,KAAK,EAAE;AAIjB,KAAI,oBAAoB,UAAU,cAAc,CAC9C,SAAQ,KAAK,EAAE;CAIjB,MAAM,WAAW,mBAAmB,SAAS;AAC7C,KAAI,CAAC,SACH,SAAQ,KAAK,EAAE;CAKjB,MAAM,QAAQ,sCADI,IAAI,MAAM,EAAC,aAAa,CACG,IAAI,QAAQ;CAGzD,MAAM,UAAU,GAAG,SAAS;AAC5B,KAAI;AAEF,gBAAc,SADG,aAAa,UAAU,OAAO,GACb,OAAO,OAAO;AAChD,aAAW,SAAS,SAAS;SACvB;AAEN,MAAI;AAEF,OAAI,WAAW,QAAQ,CACrB,YAAW,SAAS,UAAU,QAAQ;UAElC;AACR,UAAQ,KAAK,EAAE;;AAIjB,yBAAwB,SAAS;AAGjC,SAAQ,KAAK,EAAE;;;;;AAUjB,MAAM,0BAA0B;CAC9B;CACA;CACA;CACA;CACD;;;;;AAMD,SAAS,gBAAgB,UAA4D;AACnF,MAAK,MAAM,OAAO,yBAAyB;EACzC,MAAM,OAAO,KAAK,UAAU,IAAI;AAChC,MAAI,WAAW,KAAK,CAClB,KAAI;AACF,UAAO;IAAE,MAAM;IAAM,SAAS,aAAa,MAAM,OAAO;IAAE;UACpD;;AAKZ,QAAO;;;;;;;AAQT,SAAS,qBAAqB,SAAyB;CACrD,MAAM,QAAQ,QAAQ,MAAM,KAAK;CAEjC,MAAM,WAAW,MAAM,WAAW,MAAM,EAAE,MAAM,KAAK,cAAc;AACnE,KAAI,aAAa,GAAI,QAAO;CAG5B,IAAI,SAAS,MAAM;AACnB,MAAK,IAAI,IAAI,WAAW,GAAG,IAAI,MAAM,QAAQ,KAAK;EAChD,MAAM,UAAU,MAAM,GAAG,MAAM;AAC/B,MAAI,YAAY,SAAU,QAAQ,WAAW,KAAK,IAAI,YAAY,eAAgB;AAEhF,YAAS;AACT;;;CAMJ,IAAI,cAAc;AAClB,KAAI,cAAc,MAAM,UAAU,MAAM,aAAa,MAAM,KAAK,MAC9D,gBAAe;CAGjB,MAAM,SAAS,MAAM,MAAM,GAAG,SAAS;CACvC,MAAM,QAAQ,MAAM,MAAM,YAAY;AAGtC,QAAO,MAAM,SAAS,KAAK,MAAM,GAAG,MAAM,KAAK,GAC7C,OAAM,OAAO;AAGf,QAAO,CAAC,GAAG,QAAQ,GAAG,MAAM,CAAC,KAAK,KAAK;;;;;;;;;;AAWzC,SAAS,YACP,IACA,aACA,gBACM;CAEN,IAAI;AAEJ,KAAI,aAAa;AACf,YAAUA,aAAW,IAAI,YAAY;AACrC,MAAI,CAAC,QAEH,SAAQ,KAAK,EAAE;QAEZ;EAEL,MAAM,MAAM,QAAQ,KAAK;EACzB,MAAM,MAAM,GACT,QACC;;;;mBAKD,CACA,IAAI,IAAI;AAEX,MAAI,CAAC,IACH,SAAQ,KAAK,EAAE;AAEjB,YAAU;;CAIZ,IAAI;CACJ,MAAM,MAAM,kBAAkB;AAE9B,KAAI,QAAQ,SACV,WAAU,GACP,QACC,2EACD,CACA,IAAI,QAAQ,GAAG;MACb;EACL,MAAM,MAAM,SAAS,KAAK,GAAG;AAC7B,MAAI,CAAC,MAAM,IAAI,CACb,WAAU,GACP,QAAQ,6DAA6D,CACrE,IAAI,QAAQ,IAAI,IAAI;;CAK3B,MAAM,OAAO,gBAAgB,QAAQ,UAAU;CAG/C,IAAI;CACJ,IAAI;AAEJ,KAAI,MAAM;AACR,aAAW,KAAK;AAChB,oBAAkB,KAAK;QAClB;EAEL,MAAM,WAAW,KAAK,QAAQ,WAAW,QAAQ;AACjD,MAAI;AACF,OAAI,CAAC,WAAW,SAAS,CACvB,WAAU,UAAU,EAAE,WAAW,MAAM,CAAC;UAEpC;AACN,WAAQ,KAAK,EAAE;;AAEjB,aAAW,KAAK,UAAU,UAAU;AACpC,oBAAkB;;CAIpB,MAAM,6BAAY,IAAI,MAAM,EAAC,aAAa;CAC1C,MAAM,MAAM,QAAQ,KAAK;CAEzB,IAAI;AACJ,KAAI,SAAS;EACX,MAAM,MAAM,OAAO,QAAQ,OAAO,CAAC,SAAS,GAAG,IAAI;EACnD,MAAM,YAAY,QAAQ,SAAS,QAAQ,QAAQ;AACnD,gBAAc,GAAG,IAAI,KAAK,QAAQ,KAAK,KAAK;OAE5C,eAAc;CAiBhB,MAAM,aAdgB;EACpB;EACA;EACA,uBAAuB;EACvB,oBAAoB;EACpB;EACA,wBAAwB,IAAI;EAC5B;EACA;EACA;EACD,CAAC,KAAK,KAAK,GAGK,qBAAqB,gBAAgB,CAAC,WAAW;CAIlE,MAAM,UAAU,GAAG,SAAS;AAC5B,KAAI;AACF,gBAAc,SAAS,YAAY,OAAO;AAC1C,aAAW,SAAS,SAAS;SACvB;AACN,MAAI;AACF,OAAI,WAAW,QAAQ,CACrB,YAAW,SAAS,GAAG,QAAQ,OAAO;UAElC;AACR,UAAQ,KAAK,EAAE;;AAIjB,SAAQ,KAAK,EAAE;;AAOjB,eAAe,aAAa,MAIV;CAChB,MAAM,EAAE,WAAW,iBAAiB,wBAAwB,MAAM,OAChE;CAEF,MAAM,EAAE,iBAAiB,MAAM,OAAO;CACtC,MAAM,EAAE,yBAAyB,MAAM,OAAO;CAC9C,MAAM,EAAE,eAAe,MAAM,OAAO;CAEpC,MAAM,SAAS,YAAY;CAC3B,MAAM,aAAa,cAAc;CACjC,MAAM,aAAa,MAAM,qBAAqB,OAAO;CAErD,MAAM,YAAY,KAAK,OAAO,QAAQ,KAAK;CAC3C,MAAM,SAAS,MAAM,UAAU,YAAY,YAAY,WAAW,KAAK,QAAQ;AAE/E,KAAI,CAAC,QAAQ;AACX,UAAQ,KAAK;AACb,UAAQ,IAAI,KAAK,mCAAmC,UAAU,CAAC;AAC/D,UAAQ,KAAK;AACb,UAAQ,IACN,IAAI,0CAA0C,IAC3C,KAAK,UAAU,IAAI,oBAAoB,GAAG,IAC9C;AACD,UAAQ,KAAK;AACb,UAAQ,IAAI,IAAI,wDAAwD,CAAC;AACzE,UAAQ,KAAK;AACb;;AAGF,KAAI,KAAK,MAAM;AACb,UAAQ,IAAI,oBAAoB,OAAO,CAAC;AACxC;;AAGF,SAAQ,KAAK;AACb,SAAQ,IAAI,OAAO,mBAAmB,CAAC;AACvC,SAAQ,KAAK;AACb,SAAQ,IAAI,KAAK,KAAK,WAAW,CAAC,OAAO,OAAO,eAAe;AAC/D,SAAQ,IAAI,KAAK,KAAK,QAAQ,CAAC,UAAU,OAAO,OAAO;AACvD,SAAQ,IAAI,KAAK,KAAK,aAAa,CAAC,KAAK,OAAO,YAAY;AAC5D,SAAQ,IAAI,KAAK,KAAK,UAAU,CAAC,QAAQ,OAAO,SAAS;AACzD,SAAQ,IACN,KAAK,KAAK,cAAc,CAAC,KAAK,OAAO,aAAa,KAAK,QAAQ,EAAE,CAAC,GACnE;AACD,SAAQ,KAAK;AACb,SAAQ,IAAI,GAAG,gBAAgB,GAAG,KAAK,OAAO,KAAK,CAAC;AACpD,SAAQ,KAAK;;AAOf,SAAgB,wBACd,YACA,OACM;AAEN,YACG,QAAQ,sBAAsB,CAC9B,YAAY,yDAAyD,CACrE,OAAO,eAAe,sCAAsC,KAAK,CACjE,OAAO,qBAAqB,iDAAiD,CAC7E,QAEG,aACA,SACG;AACH,UAAQ,OAAO,EAAE,aAAa,KAAK;GAEtC;AAGH,YACG,QAAQ,+BAA+B,CACvC,YAAY,2CAA2C,CACvD,QAAQ,aAAqB,WAAmB;AAC/C,UAAQ,OAAO,EAAE,aAAa,OAAO;GACrC;AAGJ,YACG,QAAQ,4CAA4C,CACpD,YACC,uEACD,CACA,QAAQ,aAAqB,QAAgB,YAAoB;AAChE,YAAU,OAAO,EAAE,aAAa,QAAQ,QAAQ;GAChD;AAGJ,YACG,QAAQ,+BAA+B,CACvC,YACC,gEACD,CACA,OAAO,WAAW,mDAAmD,CACrE,QAAQ,aAAqB,QAAgB,SAA8B;AAC1E,UAAQ,OAAO,EAAE,aAAa,QAAQ,KAAK;GAC3C;AAGJ,YACG,QAAQ,wCAAwC,CAChD,YACC,iFACD,CACA,QAAQ,aAAqB,QAAgB,SAAmB;AAC/D,SAAO,OAAO,EAAE,aAAa,QAAQ,KAAK;GAC1C;AAGJ,YACG,QAAQ,iDAAiD,CACzD,YACC,mEACD,CACA,OACC,iBACA,8CACA,UACD,CACA,QAEG,aACA,QACA,eACA,SACG;AACH,WAAS,OAAO,EAAE,aAAa,QAAQ,eAAe,KAAK;GAE9D;AAGH,YACG,QAAQ,uCAAuC,CAC/C,YACC,wPAID,CACA,QAAQ,aAAiC,cAAkC;AAC1E,cAAY,OAAO,EAAE,aAAa,UAAU;GAC5C;AAGJ,YACG,QAAQ,uBAAuB,CAC/B,YACC,mNAGD,CACA,OACC,uBACA,kEACA,MACD,CACA,QAAQ,SAAiB,SAA8B;AAEtD,gBAAc,SAAS,KAAK;GAC5B;AAGJ,YACG,QAAQ,aAAa,CACrB,YACC,gMAGD,CACA,OAAO,gBAAgB,4DAA4D,CACnF,OAAO,oBAAoB,wDAAwD,CACnF,OAAO,UAAU,+CAA+C,CAChE,OACC,OAAO,SAA6D;AAClE,QAAM,aAAa,KAAK;GAE3B;;;;;AC9iCL,MAAM,sBAAsB;CAC1B;CACA;CACA;CACA;CACD;AAID,MAAM,iBAAiB;AAEvB,MAAM,iBAAiB;AAMvB,SAAS,eAAe,IAA4B;AAClD,QAAO,GACJ,QACC,8HACD,CACA,KAAK;;AAGV,SAAS,WAAW,IAAc,MAAsC;AACtE,QAAO,GACJ,QACC,uGACD,CACA,IAAI,KAAK;;AAGd,SAAS,mBAAmB,IAAc,WAAiC;AACzE,QAAO,GACJ,QAAQ,kEAAkE,CAC1E,IAAI,UAAU;;;;;;AAOnB,SAAS,iBAAiB,UAAiC;CACzD,MAAM,YAAY,KAAK,UAAU,QAAQ;AACzC,KAAI,WAAW,UAAU,CAAE,QAAO;CAClC,MAAM,MAAM,KAAK,UAAU,WAAW,QAAQ;AAC9C,KAAI,WAAW,IAAI,CAAE,QAAO;AAC5B,QAAO;;;;;;;;AAST,SAASE,qBAAmB,SAAqB,cAA4C;CAE3F,MAAM,YACJ,QAAQ,oBACR,KAAK,SAAS,EAAE,WAAW,YAAY,QAAQ,aAAa,QAAQ;AAEtE,KAAI,CAAC,WAAW,UAAU,CAAE,QAAO;AAEnC,KAAI,gBAAgB,cAAc,aAAc,QAAO;AACvD,QAAO;;;;;;;;AAST,SAAS,iBAAiB,SAA+B;CACvD,MAAM,UAAU,iBAAiB,QAAQ,UAAU;CACnD,MAAM,YAAYA,qBAAmB,SAAS,QAAQ;CACtD,MAAM,OAAiB,EAAE;AACzB,KAAI,QAAS,MAAK,KAAK,QAAQ;AAC/B,KAAI,UAAW,MAAK,KAAK,UAAU;AACnC,QAAO;;;;;;;;;;AAWT,SAAS,eAAe,SAA0B;AAEhD,KAAI,CADsB,oBAAoB,MAAM,QAAQ,QAAQ,SAAS,IAAI,CAAC,CAC1D,QAAO;CAU/B,MAAM,QAAQ,QAAQ,MAAM,KAAK;CACjC,IAAI,aAAa;AAEjB,MAAK,MAAM,QAAQ,OAAO;EACxB,MAAM,UAAU,KAAK,MAAM;AAC3B,MAAI,YAAY,gBAAgB;AAC9B,gBAAa;AACb;;AAEF,MAAI,QAAQ,WAAW,MAAM,IAAI,WAE/B;AAEF,MAAI,CAAC,WAAY;AAGjB,MAAI,CAAC,QAAS;AACd,MAAI,QAAQ,WAAW,OAAO,IAAI,QAAQ,SAAS,MAAM,CAAE;AAC3D,MAAI,QAAQ,WAAW,OAAO,CAAE;AAChC,MAAI,YAAY,MAAO;AACvB,MAAI,YAAY,qBAAsB;AACtC,MAAI,YAAY,cAAc,YAAY,qBAAsB;AAGhE,SAAO;;AAGT,QAAO;;AAKT,MAAM,uBAAiC;CACrC;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACD;AAGD,MAAM,yBAAyB,IAAI,IAAI;CACrC;CAAK;CAAM;CAAO;CAAM;CAAM;CAAM;CAAO;CAAM;CAAM;CAAO;CAC9D;CAAO;CAAO;CAAQ;CAAQ;CAAM;CAAM;CAC3C,CAAC;;;;;AAMF,SAAS,kBAAkB,KAA4B;CACrD,IAAI,IAAI,IAAI,MAAM;AAGlB,KAAI,EAAE,QAAQ,yBAAyB,GAAG;AAC1C,KAAI,EAAE,QAAQ,aAAa,GAAG;AAG9B,KAAI,EAAE,QAAQ,iBAAiB,GAAG;AAIlC,KAAI,EAAE,QAAQ,2BAA2B,KAAK;AAC9C,KAAI,EAAE,QAAQ,uBAAuB,KAAK;AAC1C,KAAI,EAAE,QAAQ,cAAc,KAAK;AAGjC,KAAI,EAAE,QAAQ,WAAW,GAAG;AAG5B,KAAI,EAAE,QAAQ,YAAY,GAAG,CAAC,QAAQ,YAAY,GAAG;AAGrD,KAAI,EAAE,QAAQ,QAAQ,IAAI,CAAC,MAAM;AAEjC,QAAO,EAAE,UAAU,IAAI,IAAI;;;;;;AAO7B,SAAS,aAAa,MAAuB;AAC3C,QAAO,qBAAqB,MAAM,OAAO,GAAG,KAAK,KAAK,CAAC;;;;;;AAOzD,SAAS,YAAY,MAAsB;AAEzC,QADc,KAAK,MAAM,IAAI,CAE1B,KAAK,MAAM,MAAM;EAChB,MAAM,QAAQ,KAAK,aAAa;AAChC,MAAI,MAAM,KAAK,uBAAuB,IAAI,MAAM,CAAE,QAAO;AACzD,SAAO,KAAK,OAAO,EAAE,CAAC,aAAa,GAAG,KAAK,MAAM,EAAE;GACnD,CACD,KAAK,IAAI;;;;;;;AAQd,SAAS,aAAa,KAAqB;CAEzC,IAAI,IAAI,IAAI,QAAQ,oBAAoB,GAAG;AAC3C,KAAI,EAAE,QAAQ,QAAQ,IAAI,CAAC,MAAM;AAGjC,KAAI,EAAE,SAAS,IAAI;EACjB,MAAM,YAAY,EAAE,MAAM,GAAG,GAAG;EAChC,MAAM,YAAY,UAAU,YAAY,IAAI;AAC5C,MAAI,YAAY,KAAK,UAAU,MAAM,GAAG,UAAU,GAAG;;AAGvD,KAAI,EAAE,MAAM;AAGZ,QAAO,YAAY,EAAE;;;;;;;;;;;;AAavB,SAAS,gBAAgB,SAAyB;CAChD,MAAM,QAAQ,QAAQ,MAAM,KAAK;CAGjC,MAAM,2BAA2B,IAAI,IAAI;EACvC;EAAa;EAAW;EAAa;EACrC;EAAW;EAAY;EAAW;EACnC,CAAC;CAGF,MAAM,wBAAwB,IAAI,IAAI;EACpC;EAAc;EAAQ;EAAQ;EAAY;EAC1C;EAAY;EAAW;EACxB,CAAC;CAEF,IAAI,SAAS;CAGb,IAAI,cAAc;CAClB,IAAI,iBAAgC;CACpC,MAAM,sBAAgC,EAAE;CACxC,MAAM,kBAA4B,EAAE;AAEpC,MAAK,MAAM,QAAQ,OAAO;EACxB,MAAM,UAAU,KAAK,MAAM;AAG3B,MAAI,QAAQ,WAAW,KAAK,EAAE;AAC5B,YAAS;AACT;;AAGF,MAAI,CAAC,OAAQ;AAGb,MAAI,CAAC,aAAa;AAChB,OAAI,YAAY,MACd,eAAc;AAGhB;;AAIF,MAAI,QAAQ,WAAW,MAAM,EAAE;GAC7B,MAAM,cAAc,QAAQ,MAAM,EAAE,CAAC,MAAM;AAC3C,OAAI,yBAAyB,IAAI,YAAY,CAC3C,kBAAiB;YACR,sBAAsB,IAAI,YAAY,CAC/C,kBAAiB;QACZ;AAEL,qBAAiB;AACjB,oBAAgB,KAAK,YAAY;;AAEnC;;AAIF,MAAI,QAAQ,WAAW,IAAI,CAAE;AAI7B,MAAI,YAAY,MAAO;AAEvB,MAAI,QAAQ,WAAW,OAAO,IAAI,cAAc,KAAK,QAAQ,CAAE;AAE/D,MAAI,mBAAmB,aAAa,QAAQ,SAAS,GAAG;GAItD,MAAM,iBAAiB,QAAQ,QAAQ,kBAAkB,GAAG;GAC5D,MAAM,YAAY,eAAe,SAAS,IAAI,iBAAiB;AAG/D,OAAI,UAAU,WAAW,EAAG;AAE5B,uBAAoB,KAAK,UAAU;;;AAOvC,MAAK,MAAM,OAAO,qBAAqB;EACrC,MAAM,UAAU,kBAAkB,IAAI;AACtC,MAAI,CAAC,QAAS;AACd,MAAI,aAAa,QAAQ,CAAE;AAC3B,MAAI,QAAQ,WAAW,OAAO,IAAI,QAAQ,SAAS,eAAe,CAAE;AACpE,MAAI,QAAQ,SAAS,EAAG;AACxB,SAAO,aAAa,QAAQ;;AAI9B,MAAK,MAAM,WAAW,iBAAiB;EACrC,MAAM,UAAU,kBAAkB,QAAQ;AAC1C,MAAI,CAAC,QAAS;AACd,MAAI,aAAa,QAAQ,CAAE;AAC3B,MAAI,QAAQ,SAAS,KAAK,QAAQ,SAAS,GACzC,QAAO,aAAa,QAAQ;;AAKhC,MAAK,MAAM,QAAQ,OAAO;EACxB,MAAM,UAAU,KAAK,MAAM;AAC3B,MAAI,QAAQ,WAAW,KAAK,EAAE;GAC5B,MAAM,QAAQ,QAAQ,MAAM,EAAE,CAAC,MAAM;AAErC,OAAI,kBAAkB,KAAK,MAAM,CAAE;GACnC,MAAM,UAAU,kBAAkB,MAAM;AACxC,OAAI,WAAW,CAAC,aAAa,QAAQ,IAAI,QAAQ,SAAS,EACxD,QAAO,aAAa,QAAQ;;;AAKlC,QAAO;;;;;AAMT,SAAS,OAAO,GAAmB;AACjC,QAAO,OAAO,EAAE,CAAC,SAAS,GAAG,IAAI;;;;;;AAOnC,SAAS,iBAAiB,WAAoD;CAC5E,MAAM,sBAAM,IAAI,KAAqB;AAGrC,CADe,CAAC,GAAG,UAAU,CAAC,MAAM,GAAG,MAAM,EAAE,SAAS,EAAE,OAAO,CAC1D,SAAS,GAAG,QAAQ;EACzB,MAAM,SAAS,MAAM;AACrB,MAAI,EAAE,WAAW,OACf,KAAI,IAAI,EAAE,QAAQ,OAAO;GAE3B;AACF,QAAO;;;;;;AAoBT,SAAS,aACP,UACA,cACoB;CACpB,MAAM,aAAiC,EAAE;CAGzC,IAAI,YAAsB,EAAE;AAC5B,KAAI;AACF,cAAY,YAAY,UAAU,EAAE,eAAe,MAAM,CAAC,CACvD,QAAQ,MAAM,EAAE,QAAQ,IAAI,EAAE,KAAK,SAAS,MAAM,CAAC,CACnD,KAAK,MAAM,EAAE,KAAK;SACf;CAKR,MAAM,cAAwD,EAAE;AAChE,KAAI;EACF,MAAM,aAAa,YAAY,UAAU,EAAE,eAAe,MAAM,CAAC;AACjE,OAAK,MAAM,SAAS,YAAY;AAC9B,OAAI,CAAC,MAAM,aAAa,CAAE;AAC1B,OAAI,CAAC,UAAU,KAAK,MAAM,KAAK,CAAE;GACjC,MAAM,UAAU,KAAK,UAAU,MAAM,KAAK;GAC1C,MAAM,YAAY,YAAY,SAAS,EAAE,eAAe,MAAM,CAAC;AAC/D,QAAK,MAAM,UAAU,WAAW;AAC9B,QAAI,CAAC,OAAO,aAAa,CAAE;IAC3B,MAAM,WAAW,KAAK,SAAS,OAAO,KAAK;IAC3C,MAAM,QAAQ,YAAY,SAAS,CAAC,QAAQ,MAAM,EAAE,SAAS,MAAM,CAAC;AACpE,SAAK,MAAM,KAAK,MACd,aAAY,KAAK;KAAE,UAAU;KAAG,UAAU,KAAK,UAAU,EAAE;KAAE,CAAC;;;SAI9D;CAKR,MAAM,WAAqD,CACzD,GAAG,UAAU,KAAK,OAAO;EAAE,UAAU;EAAG,UAAU,KAAK,UAAU,EAAE;EAAE,EAAE,EACvE,GAAG,YACJ;AAED,MAAK,MAAM,EAAE,UAAU,cAAc,UAAU;EAC7C,IAAI;EACJ,IAAI;EACJ,IAAI;EACJ,IAAI;EAEJ,MAAM,cAAc,eAAe,KAAK,SAAS;EACjD,MAAM,cAAc,eAAe,KAAK,SAAS;AAEjD,MAAI,aAAa;AACf,SAAM,SAAS,YAAY,IAAI,GAAG;AAClC,UAAO,YAAY;AACnB,cAAW,YAAY;AACvB,oBAAiB;aACR,aAAa;AACtB,SAAM,SAAS,YAAY,IAAI,GAAG;AAClC,UAAO,YAAY;AACnB,cAAW,YAAY;AACvB,oBAAiB;QAGjB;EAGF,IAAI,YAAY;EAChB,IAAI,UAAU;AACd,MAAI;AAEF,eADa,SAAS,SAAS,CACd;AACjB,aAAU,aAAa,UAAU,OAAO;UAClC;AACN;;EAKF,MAAM,YACJ,aAAa,IAAI,SAAS,IAC1B,aAAa,IAAI,SAAS,MAAM,GAAG,SAAS,GAAG,CAAC,MAAM,GAAG,IACzD;AAEF,MAAI,mBAAmB,iBACrB;OAAI,YAAY,OAAO,eAAe,QAAQ,CAC5C,kBAAiB;YAEjB,aAAa,iBACb,cAAc,QAAQ,IAAI,QAAQ,OAClC,aAAa,kDAEb,kBAAiB;;EAIrB,MAAM,YAA8B;GAClC,SAAS;GACT;GACA;GACA;GACA;GACA;GACA,QAAQ;GACT;AAED,MAAI,mBAAmB,aAAa,mBAAmB,gBACrD,WAAU,WAAW,gBAAgB,QAAQ;AAG/C,aAAW,KAAK,UAAU;;AAG5B,QAAO;;AAGT,SAAS,eACP,IACA,SACoB;CACpB,MAAM,gBAAgB,iBAAiB,QAAQ;AAE/C,KAAI,cAAc,WAAW,EAAG,QAAO;CAEvC,MAAM,aAAa,mBAAmB,IAAI,QAAQ,GAAG;CACrD,MAAM,+BAAe,IAAI,KAAyB;AAClD,MAAK,MAAM,KAAK,WACd,cAAa,IAAI,EAAE,UAAU,EAAE;CAIjC,MAAM,gBAAgC,EAAE;CAExC,MAAM,eAAmC,EAAE;AAE3C,MAAK,MAAM,YAAY,eAAe;EACpC,MAAM,aAAa,aAAa,UAAU,aAAa;AACvD,MAAI,WAAW,WAAW,EAAG;EAE7B,MAAM,WAAW,WAAW,QAAQ,MAAM,EAAE,mBAAmB,QAAQ;EACvE,MAAM,WAAW,WAAW,QACzB,MAAM,EAAE,mBAAmB,aAAa,EAAE,mBAAmB,gBAC/D;EACD,MAAM,YAAY,WAAW,QAAQ,MAAM,EAAE,mBAAmB,QAAQ;AAExE,gBAAc,KAAK;GAAE;GAAU;GAAU;GAAU,QAAQ;GAAW,CAAC;AACvE,eAAa,KAAK,GAAG,UAAU;;AAGjC,KAAI,cAAc,WAAW,EAAG,QAAO;AAKvC,QAAO;EACL;EACA,WAAW;EACX,aALkB,iBAAiB,aAAa;EAMjD;;AAOH,eAAe,cAAc,OAAqC;CAChE,IAAI,cAAc;CAClB,IAAI,cAAc;CAClB,IAAI,YAAY;CAChB,IAAI,gBAAgB;AAEpB,MAAK,MAAM,QAAQ,OAAO;AAMxB,MAAI,EAJF,KAAK,UAAU,MACZ,MAAM,EAAE,SAAS,SAAS,KAAK,EAAE,SAAS,SAAS,KAAK,EAAE,OAAO,SAAS,EAC5E,IAAI,KAAK,YAAY,OAAO,GAEjB;AAEd,UAAQ,KAAK;AACb,UAAQ,IAAI,OAAO,cAAc,KAAK,QAAQ,aAAa,IAAI,KAAK,QAAQ,KAAK,GAAG,CAAC;AAErF,OAAK,MAAM,WAAW,KAAK,WAAW;AACpC,WAAQ,IAAI,IAAI,YAAY,QAAQ,WAAW,CAAC;AAEhD,OAAI,QAAQ,SAAS,SAAS,GAAG;AAC/B,YAAQ,IAAI,KAAK,2CAA2C,CAAC;AAC7D,SAAK,MAAM,KAAK,QAAQ,UAAU;AAChC,aAAQ,IACN,OAAO,MAAM,IAAI,MAAM,CAAC,IAAI,IAAI,OAAO,EAAE,OAAO,CAAC,CAAC,KAAK,EAAE,KAAK,KAAK,EAAE,SAAS,MAAM,MAAM,CAAC,MAAM,EAAE,CAAC,KAAK,MAAM,CAAC,GAAG,IAAI,IAAI,EAAE,UAAU,IAAI,GAC5I;AACD;;AAEF,YAAQ,KAAK;;AAGf,OAAI,QAAQ,SAAS,SAAS,GAAG;AAC/B,YAAQ,IAAI,KAAK,gDAAgD,CAAC;AAClE,SAAK,MAAM,KAAK,QAAQ,UAAU;KAChC,MAAM,WAAW,EAAE,YAAY;AAC/B,aAAQ,IAAI,OAAO,MAAM,OAAO,MAAM,CAAC,IAAI,EAAE,WAAW;AACxD,aAAQ,IAAI,cAAc,OAAO,EAAE,OAAO,CAAC,KAAK,EAAE,KAAK,KAAK,SAAS,KAAK;AAC1E;;AAEF,YAAQ,KAAK;;AAGf,OAAI,QAAQ,OAAO,SAAS,GAAG;AAC7B,YAAQ,IAAI,KAAK,gCAAgC,CAAC;AAClD,SAAK,MAAM,KAAK,QAAQ,QAAQ;KAC9B,MAAM,CAAC,MAAM,SAAS,EAAE,KAAK,MAAM,IAAI;AACvC,aAAQ,IAAI,OAAO,MAAM,KAAK,MAAM,CAAC,IAAI,EAAE,WAAW;AACtD,aAAQ,IAAI,cAAc,KAAK,GAAG,MAAM,GAAG,EAAE,WAAW;AACxD;;AAEF,YAAQ,KAAK;;;AAIjB,MAAI,KAAK,YAAY,OAAO,GAAG;AAC7B,WAAQ,IAAI,KAAK,+DAA+D,CAAC;AACjF,QAAK,MAAM,CAAC,MAAM,SAAS,KAAK,aAAa;AAC3C,YAAQ,IAAI,OAAO,MAAM,KAAK,MAAM,CAAC,KAAK,OAAO,KAAK,CAAC,MAAM,OAAO,KAAK,GAAG;AAC5E;;AAEF,WAAQ,KAAK;;;CAMjB,MAAM,iBAA2B,EAAE;AACnC,MAAK,MAAM,QAAQ,MACjB,MAAK,MAAM,WAAW,KAAK,UACzB,MAAK,MAAM,KAAK,QAAQ,QAAQ;EAC9B,MAAM,CAAC,MAAM,SAAS,EAAE,KAAK,MAAM,IAAI;EACvC,MAAM,aAAa,KAAK,QAAQ,UAAU,MAAM,OAAO,EAAE,SAAS;AAClE,MAAI,EAAE,aAAa,WACjB,gBAAe,KAAK,EAAE,SAAS;;CAMvC,MAAM,gBAAgB,MAAM,mBAAmB,eAAe;AAE9D,SAAQ,KAAK;AACb,SAAQ,IAAI,KAAK,uBAAuB,CAAC;AACzC,SAAQ,IAAI,OAAO,MAAM,IAAI,MAAM,CAAC,IAAI,YAAY,2BAA2B;AAC/E,SAAQ,IAAI,OAAO,MAAM,OAAO,MAAM,CAAC,IAAI,YAAY,6BAA6B;AACpF,SAAQ,IAAI,OAAO,MAAM,KAAK,MAAM,CAAC,IAAI,cAAc,uBAAuB;AAC9E,SAAQ,IAAI,OAAO,MAAM,KAAK,MAAM,CAAC,IAAI,UAAU,sCAAsC;AACzF,KAAI,gBAAgB,EAClB,SAAQ,IAAI,OAAO,MAAM,QAAQ,MAAM,CAAC,IAAI,cAAc,uEAAuE;UACxH,eAAe,SAAS,EACjC,SAAQ,IAAI,OAAO,MAAM,QAAQ,MAAM,CAAC,iFAAiF;AAE3H,SAAQ,KAAK;AACb,SAAQ,IAAI,KAAK,uDAAuD,CAAC;AACzE,SAAQ,KAAK;;;;;;AAWf,eAAe,mBAAmB,UAAqC;AACrE,KAAI,SAAS,WAAW,EAAG,QAAO;AAElC,KAAI;EACF,MAAM,EAAE,eAAe,MAAM,OAAO;EACpC,MAAM,EAAE,oBAAoB,MAAM,OAAO;EAEzC,MAAM,SAAS,YAAY;AAC3B,MAAI,OAAO,mBAAmB,WAAY,QAAO;EAGjD,MAAM,YAAY,IAAI,gBADL,OAAO,YAAY,EAAE,CACS;AAG/C,MADgB,MAAM,UAAU,gBAAgB,EACnC;AACX,SAAM,UAAU,OAAO;AACvB,UAAO;;EAGT,MAAM,OAAQ,UAAyH;EACvI,MAAM,eAAe,SAAS,KAAK,GAAG,MAAM,IAAI,IAAI,IAAI,CAAC,KAAK,KAAK;EACnE,MAAM,SAAS,MAAM,KAAK,MACxB,4DAA4D,aAAa,IACzE,SACD;AAED,QAAM,UAAU,OAAO;AACvB,SAAO,SAAS,OAAO,KAAK,IAAI,KAAK,KAAK,GAAG;SACvC;AACN,SAAO;;;;;;;;;AAUX,eAAe,oBACb,OACiB;AACjB,KAAI,MAAM,WAAW,EAAG,QAAO;AAE/B,KAAI;EACF,MAAM,EAAE,eAAe,MAAM,OAAO;EACpC,MAAM,EAAE,oBAAoB,MAAM,OAAO;EAEzC,MAAM,SAAS,YAAY;AAC3B,MAAI,OAAO,mBAAmB,WAAY,QAAO;EAGjD,MAAM,YAAY,IAAI,gBADL,OAAO,YAAY,EAAE,CACS;EAE/C,MAAM,UAAU,MAAM,UAAU,gBAAgB;AAChD,MAAI,SAAS;AACX,WAAQ,OAAO,MAAM,2CAA2C,QAAQ,sCAAsC;AAC9G,SAAM,UAAU,OAAO;AACvB,UAAO;;EAIT,MAAM,SAAS,MADD,UAAuK,KAC3J,SAAS;EAEnC,IAAI,eAAe;AAEnB,MAAI;AACF,SAAM,OAAO,MAAM,SAAS,EAAE,CAAC;AAE/B,QAAK,MAAM,EAAE,SAAS,aAAa,OAAO;IACxC,MAAM,cAAc,MAAM,OAAO,MAC/B,kDACA,CAAC,SAAS,QAAQ,CACnB;AACD,oBAAgB,YAAY,YAAY;AAExC,UAAM,OAAO,MACX,mDACA,CAAC,SAAS,QAAQ,CACnB;;AAGH,SAAM,OAAO,MAAM,UAAU,EAAE,CAAC;WACzB,GAAG;AACV,SAAM,OAAO,MAAM,YAAY,EAAE,CAAC;AAClC,SAAM;YACE;AACR,UAAO,SAAS;;AAGlB,QAAM,UAAU,OAAO;AACvB,SAAO;UACA,GAAG;AACV,UAAQ,OAAO,MAAM,uDAAuD,EAAE,IAAI;AAClF,SAAO;;;AAQX,eAAe,eAAe,IAAc,OAAsB,aAAqC;CACrG,IAAI,UAAU;CACd,IAAI,UAAU;CACd,IAAI,QAAQ;CACZ,IAAI,aAAa;CACjB,IAAI,YAAY;CAIhB,MAAM,gBAA6D,EAAE;AAErE,MAAK,MAAM,QAAQ,OAAO;AACxB,UAAQ,KAAK;AACb,UAAQ,IAAI,OAAO,cAAc,KAAK,QAAQ,aAAa,IAAI,KAAK,QAAQ,KAAK,GAAG,CAAC;AAGrF,OAAK,MAAM,WAAW,KAAK,WAAW;GACpC,MAAM,EAAE,aAAa;AAErB,OAAI,KAAK,UAAU,SAAS,EAC1B,SAAQ,IAAI,IAAI,gBAAgB,WAAW,CAAC;AAM9C,QAAK,MAAM,KAAK,QAAQ,UAAU;AAChC,QAAI;AACF,gBAAW,EAAE,SAAS;AACtB,aAAQ,IAAI,GAAG,UAAU,EAAE,WAAW,CAAC;AACvC;aACO,GAAG;AACV,aAAQ,IAAI,IAAI,oBAAoB,EAAE,SAAS,IAAI,IAAI,CAAC;;AAI1D,QAAI,EAAE,QACJ,KAAI;AACF,QAAG,QAAQ,oCAAoC,CAAC,IAAI,EAAE,QAAQ,GAAG;AACjE;aACO,GAAG;AACV,aAAQ,IAAI,IAAI,6BAA6B,EAAE,OAAO,YAAY,IAAI,CAAC;;;AAQ7E,QAAK,MAAM,KAAK,QAAQ,UAAU;IAChC,MAAM,WAAW,EAAE,YAAY;IAC/B,MAAM,cAAc,GAAG,OAAO,EAAE,OAAO,CAAC,KAAK,EAAE,KAAK,KAAK,SAAS;IAClE,MAAM,UAAU,KAAK,UAAU,YAAY;AAE3C,QAAI,EAAE,aAAa,QACjB,KAAI;AACF,gBAAW,EAAE,UAAU,QAAQ;AAC/B,aAAQ,IAAI,GAAG,UAAU,EAAE,WAAW,CAAC;AACvC,aAAQ,IAAI,IAAI,YAAY,cAAc,CAAC;AAC3C;AAEA,KAAC,EAA2B,WAAW;AACvC,KAAC,EAA2B,WAAW;aAChC,GAAG;AACV,aAAQ,IAAI,IAAI,iBAAiB,EAAE,SAAS,IAAI,IAAI,CAAC;AACrD;;AAKJ,QAAI;KAEF,MAAM,QADU,aAAa,SAAS,OAAO,CACvB,MAAM,KAAK;KACjC,IAAI,YAAY;KAChB,MAAM,UAAU,MAAM,KAAK,SAAS;AAClC,UAAI,CAAC,aAAa,KAAK,WAAW,KAAK,EAAE;AACvC,mBAAY;AACZ,cAAO,KAAK;;AAEd,aAAO;OACP;AACF,SAAI,CAAC,UACH,SAAQ,QAAQ,KAAK,YAAY,GAAG;AAEtC,mBAAc,SAAS,QAAQ,KAAK,KAAK,EAAE,OAAO;YAC5C;AAKR,QAAI,EAAE,SAAS;KACb,MAAM,iBAAiB,SACpB,aAAa,CACb,QAAQ,eAAe,IAAI,CAC3B,QAAQ,YAAY,GAAG;AAC1B,SAAI;AACF,SAAG,QACD,qEACD,CAAC,IAAI,gBAAgB,UAAU,aAAa,EAAE,QAAQ,GAAG;AAC1D;cACO,GAAG;AACV,cAAQ,IAAI,IAAI,iCAAiC,EAAE,OAAO,IAAI,IAAI,CAAC;;;;AAQzE,OAAI,KAAK,YAAY,OAAO,GAAG;IAC7B,MAAM,aAAa,QAAQ,OAAO,QAAQ,MAAM,KAAK,YAAY,IAAI,EAAE,OAAO,CAAC;IAG/E,MAAM,YAAiF,EAAE;AACzF,SAAK,MAAM,KAAK,YAAY;KAC1B,MAAM,SAAS,KAAK,YAAY,IAAI,EAAE,OAAO;KAE7C,MAAM,WAAW,KAAK,UADD,SAAS,OAAO,EAAE,OAAO,CAAC,GAAG,EAAE,WACP;AAC7C,SAAI;AACF,UAAI,WAAW,EAAE,SAAS,EAAE;AAC1B,kBAAW,EAAE,UAAU,SAAS;AAChC,iBAAU,KAAK;QAAE,WAAW;QAAG;QAAU;QAAQ,CAAC;;cAE7C,GAAG;AACV,cAAQ,IAAI,IAAI,uBAAuB,EAAE,OAAO,IAAI,IAAI,CAAC;;;AAK7D,SAAK,MAAM,EAAE,WAAW,GAAG,UAAU,YAAY,WAAW;KAC1D,MAAM,cAAc,EAAE,SAAS,QAAQ,UAAU,OAAO,OAAO,CAAC;KAChE,MAAM,UAAU,KAAK,UAAU,YAAY;AAC3C,SAAI;AACF,iBAAW,UAAU,QAAQ;AAC7B,cAAQ,IAAI,GAAG,WAAW,OAAO,EAAE,OAAO,CAAC,MAAM,OAAO,OAAO,CAAC,IAAI,cAAc,CAAC;AACnF;AAEA,MAAC,EAA2B,WAAW;AACvC,MAAC,EAA2B,WAAW;AACvC,MAAC,EAAyB,SAAS;cAC5B,GAAG;AACV,cAAQ,IAAI,IAAI,wBAAwB,OAAO,IAAI,IAAI,CAAC;;AAI1D,SAAI,WAAW,QAAQ,CACrB,KAAI;AASF,oBAAc,SARE,aAAa,SAAS,OAAO,CACvB,MAAM,KAAK,CACX,KAAK,SAAS;AAClC,WAAI,KAAK,MAAM,oBAAoB,CACjC,QAAO,KAAK,QAAQ,qBAAqB,aAAa,OAAO,OAAO,CAAC,GAAG;AAE1E,cAAO;QACP,CAC6B,KAAK,KAAK,EAAE,OAAO;aAC5C;;IAOZ,MAAM,cAAc,UACjB,QAAQ,EAAE,WAAW,QAAQ,EAAE,WAAW,KAAK,CAC/C,KAAK,EAAE,WAAW,GAAG,cAAc;KAAE,SAAS,EAAE;KAAU;KAAQ,aAAa,EAAE;KAAU,EAAE;AAEhG,QAAI,YAAY,SAAS,GAAG;KAC1B,MAAM,aAAa,GAAG,kBAAkB;AACtC,WAAK,MAAM,EAAE,SAAS,YAAY,YAChC,IAAG,QAAQ,8CAA8C,CAAC,IAAI,CAAC,QAAQ,QAAQ,GAAG;AAEpF,WAAK,MAAM,EAAE,SAAS,QAAQ,iBAAiB,YAC7C,IAAG,QAAQ,4DAA4D,CAAC,IACtE,QAAQ,aAAa,QAAQ,GAC9B;OAEH;AACF,SAAI;AACF,kBAAY;AACZ,mBAAa,YAAY;cAClB,GAAG;AACV,cAAQ,IAAI,IAAI,mCAAmC,IAAI,CAAC;;;;AAQ9D,QAAK,MAAM,KAAK,QAAQ,QAAQ;IAC9B,MAAM,CAAC,MAAM,SAAS,EAAE,KAAK,MAAM,IAAI;IACvC,MAAM,YAAY,KAAK,UAAU,MAAM,MAAM;IAC7C,MAAM,aAAa,KAAK,WAAW,EAAE,SAAS;AAG9C,QAAI,EAAE,aAAa,WAAY;AAG/B,QAAI;AACF,eAAU,WAAW,EAAE,WAAW,MAAM,CAAC;aAClC,GAAG;AACV,aAAQ,IAAI,IAAI,gBAAgB,UAAU,IAAI,IAAI,CAAC;AACnD;;IAIF,MAAM,aAAa,EAAE;AACrB,QAAI;AACF,SAAI,WAAW,EAAE,SAAS,EAAE;AAC1B,iBAAW,EAAE,UAAU,WAAW;AAClC,cAAQ,IAAI,GAAG,UAAU,EAAE,WAAW,CAAC;AACvC,cAAQ,IAAI,IAAI,YAAY,KAAK,GAAG,MAAM,GAAG,EAAE,WAAW,CAAC;AAC3D;AACA,oBAAc,KAAK;OAAE,SAAS;OAAY,SAAS;OAAY,CAAC;;aAE3D,GAAG;AACV,aAAQ,IAAI,IAAI,eAAe,EAAE,SAAS,IAAI,IAAI,CAAC;AACnD;;IAIF,MAAM,kBAAkB,GAAG,KAAK,GAAG,MAAM,GAAG,EAAE;AAC9C,QAAI,EAAE,QACJ,KAAI;AACF,QAAG,QAAQ,gDAAgD,CAAC,IAC1D,iBACA,EAAE,QAAQ,GACX;AACD;aACO,GAAG;AACV,aAAQ,IAAI,IAAI,6BAA6B,EAAE,SAAS,IAAI,IAAI,CAAC;;;;;CAU3E,IAAI,kBAAkB;AACtB,KAAI,cAAc,SAAS,GAAG;AAC5B,UAAQ,KAAK;AACb,UAAQ,IAAI,IAAI,cAAc,cAAc,OAAO,sDAAsD,CAAC;EAC1G,MAAM,SAAS,MAAM,oBAAoB,cAAc;AACvD,MAAI,UAAU,GAAG;AACf,qBAAkB;AAClB,WAAQ,IAAI,GAAG,aAAa,gBAAgB,kDAAkD,CAAC;QAE/F,SAAQ,IAAI,KAAK,2EAA2E,CAAC;;AAIjG,SAAQ,KAAK;AACb,SAAQ,IAAI,KAAK,sBAAsB,CAAC;AACxC,SAAQ,IAAI,GAAG,OAAO,QAAQ,qBAAqB,CAAC;AACpD,SAAQ,IAAI,GAAG,OAAO,QAAQ,qBAAqB,CAAC;AACpD,SAAQ,IAAI,GAAG,OAAO,WAAW,wBAAwB,CAAC;AAC1D,SAAQ,IAAI,GAAG,OAAO,MAAM,yCAAyC,CAAC;AACtE,SAAQ,IAAI,GAAG,OAAO,UAAU,gCAAgC,CAAC;AACjE,KAAI,cAAc,SAAS,EACzB,SAAQ,IAAI,GAAG,OAAO,gBAAgB,wDAAwD,CAAC;AAGjG,KAAI,CAAC,aAAa;AAChB,UAAQ,KAAK;AACb,UAAQ,IAAI,IAAI,2EAA2E,CAAC;AAC5F,UAAQ,IAAI,IAAI,wDAAwD,CAAC;;AAG3E,SAAQ,KAAK;;AAOf,SAAgB,8BACd,YACA,OACM;AACN,YACG,QAAQ,yBAAyB,CACjC,YACC,oGACD,CACA,OAAO,aAAa,oDAAoD,CACxE,OAAO,iBAAiB,4CAA4C,CACpE,OAAO,gBAAgB,8CAA8C,CACrE,OACC,OACE,aACA,SACG;EACH,MAAM,KAAK,OAAO;EAClB,MAAM,SAAS,CAAC,KAAK;EACrB,MAAM,cAAc,KAAK,YAAY;EAErC,IAAI;AACJ,MAAI,aAAa;GACf,MAAM,IAAI,WAAW,IAAI,YAAY;AACrC,OAAI,CAAC,GAAG;AACN,YAAQ,MAAM,IAAI,sBAAsB,cAAc,CAAC;AACvD,YAAQ,KAAK,EAAE;;AAEjB,cAAW,CAAC,EAAE;QAEd,YAAW,eAAe,GAAG;AAG/B,UAAQ,KAAK;AACb,UAAQ,IACN,OACE,SACI,8DACA,oCACL,CACF;AACD,UAAQ,IACN,IAAI,eAAe,SAAS,OAAO,gBAAgB,CACpD;EAED,MAAM,QAAuB,EAAE;AAC/B,OAAK,MAAM,WAAW,UAAU;GAC9B,MAAM,OAAO,eAAe,IAAI,QAAQ;AACxC,OAAI,KACF,OAAM,KAAK,KAAK;;EAIpB,MAAM,cAAc,MAAM,QACvB,MACC,EAAE,UAAU,MACT,MAAM,EAAE,SAAS,SAAS,KAAK,EAAE,SAAS,SAAS,KAAK,EAAE,OAAO,SAAS,EAC5E,IAAI,EAAE,YAAY,OAAO,EAC7B;AAED,MAAI,YAAY,WAAW,GAAG;AAC5B,WAAQ,KAAK;AACb,WAAQ,IAAI,GAAG,iDAAiD,CAAC;AACjE,WAAQ,KAAK;AACb;;AAGF,MAAI,OACF,OAAM,cAAc,YAAY;MAEhC,OAAM,eAAe,IAAI,aAAa,YAAY;GAGvD;;;;;;;;;AC/qCL,SAAS,cAAc,KAAuB;CAC5C,MAAM,UAAoB,EAAE;AAC5B,KAAI,CAAC,WAAW,IAAI,CAAE,QAAO;AAE7B,MAAK,MAAM,SAAS,YAAY,KAAK,EAAE,eAAe,MAAM,CAAC,CAC3D,KAAI,MAAM,QAAQ,IAAI,MAAM,KAAK,SAAS,MAAM,CAC9C,SAAQ,KAAK,MAAM,KAAK;UACf,MAAM,aAAa,IAAI,UAAU,KAAK,MAAM,KAAK,EAAE;EAE5D,MAAM,UAAU,KAAK,KAAK,MAAM,KAAK;AACrC,OAAK,MAAM,cAAc,YAAY,SAAS,EAAE,eAAe,MAAM,CAAC,CACpE,KAAI,WAAW,aAAa,IAAI,UAAU,KAAK,WAAW,KAAK,EAAE;GAC/D,MAAM,WAAW,KAAK,SAAS,WAAW,KAAK;AAC/C,QAAK,MAAM,aAAa,YAAY,UAAU,EAAE,eAAe,MAAM,CAAC,CACpE,KAAI,UAAU,QAAQ,IAAI,UAAU,KAAK,SAAS,MAAM,CACtD,SAAQ,KAAK,UAAU,KAAK;;;AAOxC,QAAO;;AAOT,MAAM,sBAAsB,KAAK,SAAS,EAAE,WAAW,WAAW;AAClE,MAAM,iBAAiB,KAAK,SAAS,EAAE,OAAO;AAC9C,MAAM,kBAAkB,KAAK,gBAAgB,cAAc;AAM3D,SAAS,aAAwB;AAC/B,KAAI,CAAC,WAAW,gBAAgB,CAAE,QAAO,EAAE,WAAW,EAAE,EAAE;AAC1D,KAAI;AACF,SAAO,KAAK,MAAM,aAAa,iBAAiB,OAAO,CAAC;SAClD;AACN,SAAO,EAAE,WAAW,EAAE,EAAE;;;AAI5B,SAAS,WAAW,QAAyB;AAC3C,WAAU,gBAAgB,EAAE,WAAW,MAAM,CAAC;AAC9C,eAAc,iBAAiB,KAAK,UAAU,QAAQ,MAAM,EAAE,GAAG,MAAM,OAAO;;AAGhF,SAAS,YAAY,GAAmB;AACtC,KAAI,EAAE,WAAW,KAAK,CAAE,QAAO,KAAK,SAAS,EAAE,EAAE,MAAM,EAAE,CAAC;AAC1D,QAAO,QAAQ,EAAE;;;;;;;;;;AAenB,SAAS,cACP,IACA,MACA,UACA,YACgC;CAChC,MAAM,KAAK,KAAK;CAGhB,MAAM,SAAS,GACZ,QAAQ,8CAA8C,CACtD,IAAI,SAAS;AAEhB,KAAI,QAAQ;EAEV,MAAM,eAAe,GAClB,QAAQ,gDAAgD,CACxD,IAAI,WAAW;AAElB,MAAI,CAAC,gBAAgB,aAAa,OAAO,OAAO,GAE9C,IAAG,QACD,mEACD,CAAC,IAAI,YAAY,IAAI,OAAO,GAAG;AAIlC,SAAO;GAAE,IAAI,OAAO;GAAI,OAAO;GAAO;;CAIxC,MAAM,YAAY,GACf,QAAQ,gDAAgD,CACxD,IAAI,WAAW;AAElB,KAAI,WAAW;EAEb,MAAM,YAAY,GACf,QAAQ,8CAA8C,CACtD,IAAI,SAAS;AAEhB,MAAI,CAAC,aAAa,UAAU,OAAO,UAAU,GAC3C,IAAG,QACD,iEACD,CAAC,IAAI,UAAU,IAAI,UAAU,GAAG;AAEnC,SAAO;GAAE,IAAI,UAAU;GAAI,OAAO;GAAO;;CAO3C,IAAI,YAAY;CAChB,IAAI,UAAU;AACd,QAAO,MAAM;AAIX,MAAI,CAHa,GACd,QAAQ,yCAAyC,CACjD,IAAI,UAAU,CACF;AACf;AACA,cAAY,GAAG,KAAK,GAAG;;CAGzB,MAAM,SAAS,GACZ,QACC;;qDAGD,CACA,IAAI,WAAW,WAAW,UAAU,YAAY,IAAI,GAAG;AAI1D,KAAI,OAAO,YAAY,GAAG;EACxB,MAAM,WACH,GAAG,QAAQ,gDAAgD,CAAC,IAAI,WAAW,IAC3E,GAAG,QAAQ,8CAA8C,CAAC,IAAI,SAAS;AAE1E,MAAI,SACF,QAAO;GAAE,IAAI,SAAS;GAAI,OAAO;GAAO;AAI1C,QAAM,IAAI,MACR,0FACiB,SAAS,eAAe,aAC1C;;AAGH,QAAO;EAAE,IAAI,OAAO;EAA2B,OAAO;EAAM;;;;;AAM9D,SAAS,cACP,IACA,WACA,QACA,MACA,MACA,OACA,UACS;AAKT,KAJiB,GACd,QAAQ,8DAA8D,CACtE,IAAI,WAAW,OAAO,CAEX,QAAO;CAErB,MAAM,KAAK,KAAK;AAChB,IAAG,QACD;;gDAGD,CAAC,IAAI,WAAW,QAAQ,MAAM,MAAM,OAAO,UAAU,GAAG;AAEzD,QAAO;;AAgBT,SAAS,YAAY,IAA0B;CAC7C,MAAM,SAAqB;EACzB,iBAAiB;EACjB,aAAa;EACb,iBAAiB;EACjB,iBAAiB;EACjB,aAAa;EACb,SAAS,EAAE;EACZ;AAED,KAAI,CAAC,WAAW,oBAAoB,CAClC,OAAM,IAAI,MACR,wCAAwC,sBACzC;CAGH,MAAM,UAAU,YAAY,oBAAoB,CAAC,QAAQ,SAAS;AAEhE,SAAO,SADM,KAAK,qBAAqB,KAAK,CACvB,CAAC,aAAa;GACnC;CAIF,MAAM,YAAY,oBAAoB;AAEtC,MAAK,MAAM,cAAc,SAAS;EAGhC,MAAM,WAAW,iBAAiB,YAAY,UAAU;AAExD,MAAI,CAAC,WAAW,SAAS,EAAE;AACzB,UAAO,QAAQ,KACb,GAAG,WAAW,aAAa,SAAS,4BACrC;AACD,UAAO;AACP;;EAGF,MAAM,OAAO,QAAQ,SAAS,SAAS,IAAI,WAAW;EACtD,MAAM,EAAE,IAAI,UAAU,cAAc,IAAI,MAAM,UAAU,WAAW;AAEnE,SAAO;AACP,MAAI,MAAO,QAAO;MACb,QAAO;AAGZ,MAAI;AACF,mBAAgB,UAAU,KAAK;UACzB;EAKR,MAAM,iBAAiB,KAAK,qBAAqB,YAAY,QAAQ;AAKrE,MAAI,WAAW,eAAe,EAE5B;OAAI,mBADiB,KAAK,UAAU,QAAQ,CAE1C,IAAG,QACD,wEACD,CAAC,IAAI,gBAAgB,KAAK,EAAE,GAAG;;AAIpC,MAAI,CAAC,WAAW,eAAe,CAAE;EAEjC,MAAM,YAAY,cAAc,eAAe;AAE/C,OAAK,MAAM,YAAY,WAAW;GAChC,MAAM,SAAS,qBAAqB,SAAS;AAC7C,OAAI,CAAC,OAAQ;AAEb,UAAO;AAUP,OATqB,cACnB,IACA,IACA,OAAO,QACP,OAAO,MACP,OAAO,MACP,OAAO,OACP,OAAO,SACR,CACiB,QAAO;;;CAO7B;EACE,MAAM,iBAAiB,GACpB,QAAQ,mEAAmE,CAC3E,KAAK;AAER,OAAK,MAAM,WAAW,gBAAgB;GACpC,MAAM,WAAW,KAAK,QAAQ,WAAW,QAAQ;AACjD,OAAI,CAAC,WAAW,SAAS,CAAE;GAE3B,IAAI;AACJ,OAAI;AACF,YAAQ,cAAc,SAAS;WACzB;AACN;;AAGF,QAAK,MAAM,YAAY,OAAO;IAC5B,MAAM,SAAS,qBAAqB,SAAS;AAC7C,QAAI,CAAC,OAAQ;AAEb,WAAO;AAUP,QATqB,cACnB,IACA,QAAQ,IACR,OAAO,QACP,OAAO,MACP,OAAO,MACP,OAAO,OACP,OAAO,SACR,CACiB,QAAO;;;;CAM/B,MAAM,SAAS,YAAY;AAC3B,KAAI,OAAO,UAAU,OACnB,MAAK,MAAM,UAAU,OAAO,WAAW;EACrC,MAAM,UAAU,YAAY,OAAO;AACnC,MAAI,CAAC,WAAW,QAAQ,EAAE;AACxB,UAAO,QAAQ,KAAK,GAAG,OAAO,kCAAkC;AAChE;;EAGF,MAAM,WAAW,YAAY,QAAQ,CAAC,QAAQ,SAAS;AACrD,OAAI,KAAK,WAAW,IAAI,CAAE,QAAO;GACjC,MAAM,OAAO,KAAK,SAAS,KAAK;AAChC,OAAI;AAAE,WAAO,SAAS,KAAK,CAAC,aAAa;WAAU;AAAE,WAAO;;IAC5D;AAEF,OAAK,MAAM,SAAS,UAAU;GAC5B,MAAM,YAAY,KAAK,SAAS,MAAM;GACtC,MAAM,YAAY,QAAQ,MAAM;GAChC,MAAM,eAAe,UAAU,UAAU;GAGzC,MAAM,WAAW,GACd,QAAQ,8CAA8C,CACtD,IAAI,UAAU;AAEjB,OAAI,UAAU;AACZ,WAAO;AACP,WAAO;AAGP,QAAI;AAAE,qBAAgB,WAAW,UAAU;YAAU;IAGrD,MAAM,WAAW,KAAK,WAAW,QAAQ;AACzC,QAAI,WAAW,SAAS,EAAE;KACxB,MAAM,YAAY,YAAY,SAAS,CAAC,QAAQ,MAAM,EAAE,SAAS,MAAM,CAAC;AACxE,UAAK,MAAM,YAAY,WAAW;MAChC,MAAM,SAAS,qBAAqB,SAAS;AAC7C,UAAI,CAAC,OAAQ;AACb,aAAO;AACP,UAAI,cAAc,IAAI,SAAS,IAAI,OAAO,QAAQ,OAAO,MAAM,OAAO,MAAM,OAAO,OAAO,OAAO,SAAS,CACxG,QAAO;;;AAIb;;GAGF,MAAM,EAAE,IAAI,UAAU,cAAc,IAAI,WAAW,WAAW,aAAa;AAC3E,UAAO;AACP,OAAI,MAAO,QAAO;OACb,QAAO;AAGZ,OAAI;AAAE,oBAAgB,WAAW,UAAU;WAAU;GAGrD,MAAM,WAAW,KAAK,WAAW,QAAQ;AACzC,OAAI,WAAW,SAAS,EAAE;IACxB,MAAM,YAAY,YAAY,SAAS,CAAC,QAAQ,MAAM,EAAE,SAAS,MAAM,CAAC;AACxE,SAAK,MAAM,YAAY,WAAW;KAChC,MAAM,SAAS,qBAAqB,SAAS;AAC7C,SAAI,CAAC,OAAQ;AACb,YAAO;AACP,SAAI,cAAc,IAAI,IAAI,OAAO,QAAQ,OAAO,MAAM,OAAO,MAAM,OAAO,OAAO,OAAO,SAAS,CAC/F,QAAO;;;;;AAWnB,KAAI,OAAO,UAAU,QAAQ;EAE3B,MAAM,UAAU,mBADS,OAAO,UAAU,IAAI,YAAY,CAAC,OAAO,WAAW,CACzB;AAEpD,OAAK,MAAM,UAAU,SAAS;GAE5B,MAAM,gBAAgB,GACnB,QAAQ,0DAA0D,CAClE,IAAI,OAAO,KAAK;AAEnB,OAAI,CAAC,cAAe;AAEpB,OAAI,cAAc,cAAc,OAAO,aAAa;IAGlD,MAAM,aAAa,UAAU,OAAO,YAAY;IAChD,MAAM,OAAO,KAAK,KAAK;IAEvB,MAAM,eAAe,GAClB,QAAQ,gDAAgD,CACxD,IAAI,WAAW;IAClB,MAAM,YAAY,GACf,QAAQ,8CAA8C,CACtD,IAAI,OAAO,YAAY;IAE1B,MAAM,cAAc,CAAC,gBAAgB,aAAa,OAAO,cAAc;IACvE,MAAM,WAAW,CAAC,aAAa,UAAU,OAAO,cAAc;AAE9D,QAAI,eAAe,SACjB,IAAG,QACD,kFACD,CAAC,IAAI,OAAO,aAAa,YAAY,MAAM,cAAc,GAAG;aACpD,SAET,IAAG,QACD,iEACD,CAAC,IAAI,OAAO,aAAa,MAAM,cAAc,GAAG;;;;AAQzD,QAAO;;AAGT,SAAS,QAAQ,IAAoB;CACnC,MAAM,SAAS,YAAY;AAC3B,SAAQ,IAAI,IAAI,mCAAmC,CAAC;AACpD,KAAI,OAAO,UAAU,OACnB,SAAQ,IAAI,IAAI,YAAY,OAAO,UAAU,OAAO,iBAAiB,OAAO,UAAU,KAAK,KAAK,GAAG,CAAC;AAEtG,SAAQ,IAAI,IAAI,+CAA+C,CAAC;CAEhE,IAAI;AACJ,KAAI;AACF,WAAS,YAAY,GAAG;UACjB,GAAG;AACV,UAAQ,MAAM,IAAI,OAAO,EAAE,CAAC,CAAC;AAC7B,UAAQ,KAAK,EAAE;;AAGjB,SAAQ,IACN,GACE,WAAW,KAAK,OAAO,OAAO,gBAAgB,CAAC,CAAC,aAC3C,KAAK,OAAO,OAAO,gBAAgB,CAAC,CAAC,iBAC3C,CACF;AACD,SAAQ,IACN,IACE,eAAe,OAAO,YAAY,QAAQ,OAAO,gBAAgB,UAClE,CACF;AACD,SAAQ,IAAI,IAAI,eAAe,OAAO,YAAY,MAAM,CAAC;AAEzD,KAAI,OAAO,QAAQ,QAAQ;AACzB,UAAQ,KAAK;AACb,UAAQ,IACN,KAAK,KAAK,OAAO,QAAQ,OAAO,+CAA+C,CAChF;AACD,OAAK,MAAM,KAAK,OAAO,QAAQ,MAAM,GAAG,GAAG,CACzC,SAAQ,IAAI,IAAI,OAAO,IAAI,CAAC;AAE9B,MAAI,OAAO,QAAQ,SAAS,GAC1B,SAAQ,IAAI,IAAI,eAAe,OAAO,QAAQ,SAAS,GAAG,OAAO,CAAC;;;AAsCxE,MAAM,wBAAwB,KAC5B,SAAS,EACT,WACA,wBACD;AAED,SAASC,aAAW,IAAoB;AACtC,KAAI,CAAC,WAAW,sBAAsB,EAAE;AACtC,UAAQ,MACN,IAAI,oCAAoC,wBAAwB,CACjE;AACD,UAAQ,KAAK,EAAE;;CAGjB,IAAI;AACJ,KAAI;EACF,MAAM,MAAM,aAAa,uBAAuB,OAAO;AACvD,aAAW,KAAK,MAAM,IAAI;UACnB,GAAG;AACV,UAAQ,MAAM,IAAI,0CAA0C,IAAI,CAAC;AACjE,UAAQ,KAAK,EAAE;;CAGjB,MAAM,WAAW,SAAS,YAAY,EAAE;AACxC,KAAI,CAAC,SAAS,QAAQ;AACpB,UAAQ,IAAI,KAAK,8CAA8C,CAAC;AAChE;;AAGF,SAAQ,IACN,IACE,aAAa,SAAS,OAAO,4CAC9B,CACF;CAED,IAAI,cAAc;CAClB,IAAI,kBAAkB;CACtB,IAAI,cAAc;CAClB,MAAM,SAAmB,EAAE;AAE3B,MAAK,MAAM,SAAS,UAAU;AAC5B,MAAI,CAAC,MAAM,iBAAiB,CAAC,MAAM,aAAa;AAC9C,UAAO,KAAK,qCAAqC,KAAK,UAAU,MAAM,GAAG;AACzE;;EAGF,MAAM,WAAW,MAAM;EACvB,MAAM,aAAa,MAAM;EACzB,MAAM,OAAO,QAAQ,SAAS;AAE9B,MAAI;GACF,MAAM,EAAE,OAAO,OAAO,cAAc,IAAI,MAAM,UAAU,WAAW;AACnE,OAAI,MAAO;OACN;GAGL,MAAM,QAAQ,MAAM,SAAS,EAAE;AAC/B,QAAK,MAAM,YAAY,OAAO;IAC5B,MAAM,SAAS,qBAAqB,SAAS;AAC7C,QAAI,CAAC,OAAQ;AAWb,QATqB,cACnB,IACA,IACA,OAAO,QACP,OAAO,MACP,OAAO,MACP,OAAO,OACP,OAAO,SACR,CACiB;;WAEb,GAAG;AACV,UAAO,KACL,oBAAoB,MAAM,YAAY,IAAI,OAAO,EAAE,GACpD;;;AAIL,SAAQ,IAAI,GAAG,sBAAsB,CAAC;AACtC,SAAQ,IACN,IACE,eAAe,YAAY,QAAQ,gBAAgB,kBACpD,CACF;AACD,SAAQ,IAAI,IAAI,eAAe,YAAY,MAAM,CAAC;AAElD,KAAI,OAAO,QAAQ;AACjB,UAAQ,KAAK;AACb,UAAQ,IAAI,KAAK,KAAK,OAAO,OAAO,6BAA6B,CAAC;AAClE,OAAK,MAAM,KAAK,OAAO,MAAM,GAAG,EAAE,CAChC,SAAQ,IAAI,IAAI,OAAO,IAAI,CAAC;AAE9B,MAAI,OAAO,SAAS,EAClB,SAAQ,IAAI,IAAI,eAAe,OAAO,SAAS,EAAE,OAAO,CAAC;;;AAS/D,SAAS,SAAS,IAAoB;CACpC,MAAM,gBACJ,GAAG,QAAQ,qCAAqC,CAAC,KAAK,CACtD;CACF,MAAM,iBACJ,GACG,QAAQ,6DAA6D,CACrE,KAAK,CACR;CACF,MAAM,mBACJ,GACG,QACC,+DACD,CACA,KAAK,CACR;CACF,MAAM,gBACJ,GAAG,QAAQ,qCAAqC,CAAC,KAAK,CACtD;CACF,MAAM,YACJ,GAAG,QAAQ,iCAAiC,CAAC,KAAK,CAClD;CAEF,MAAM,cAAc,GACjB,QAAQ,mEAAmE,CAC3E,KAAK;CAER,MAAM,cAAc,GACjB,QACC,mEACD,CACA,KAAK;AAER,SAAQ,KAAK;AACb,SAAQ,IAAI,KAAK,uBAAuB,CAAC;AACzC,SAAQ,KAAK;AACb,SAAQ,IAAI,KAAK,KAAK,YAAY,CAAC,OAAO,gBAAgB;AAC1D,SAAQ,IAAI,KAAK,KAAK,YAAY,CAAC,OAAO,iBAAiB;AAC3D,SAAQ,IAAI,KAAK,KAAK,cAAc,CAAC,KAAK,mBAAmB;AAC7D,SAAQ,IAAI,KAAK,KAAK,YAAY,CAAC,OAAO,gBAAgB;AAC1D,SAAQ,IAAI,KAAK,KAAK,QAAQ,CAAC,WAAW,YAAY;AACtD,KAAI,YACF,SAAQ,IACN,KAAK,KAAK,gBAAgB,CAAC,GAAG,QAAQ,YAAY,WAAW,GAC9D;AAEH,KAAI,YACF,SAAQ,IACN,KAAK,KAAK,gBAAgB,CAAC,GAAG,QAAQ,YAAY,WAAW,GAC9D;AAEH,SAAQ,KAAK;;AAOf,SAAS,WAAW,IAAoB;AACtC,SAAQ,IAAI,KAAK,0DAA0D,CAAC;AAC5E,SAAQ,IAAI,IAAI,0BAA0B,CAAC;AAG3C,IAAG,KAAK;;;;;;;;;IASN;AAEF,SAAQ,IAAI,IAAI,oCAAoC,CAAC;AACrD,SAAQ,GAAG;;;;;;AAWb,SAAS,UAAU,IAAc,QAAsB;CAErD,MAAM,WAAW,QAAQ,OAAO;CAEhC,MAAM,MAAM,GACT,QAAQ,gDAAgD,CACxD,IAAI,SAAS;AAEhB,KAAI,CAAC,IACH,SAAQ,KAAK,EAAE;AAGjB,SAAQ,OAAO,MAAM,IAAI,OAAO,KAAK;;AAOvC,SAAgB,yBACd,aACA,OACM;AAEN,aACG,QAAQ,OAAO,CACf,YACC,yEACD,CACA,OAAO,oBAAoB,2EAA2E,CACtG,OAAO,uBAAuB,2CAA2C,CACzE,OAAO,eAAe,6CAA6C,CACnE,QAAQ,SAAsE;AAC7E,MAAI,KAAK,UAAU;GACjB,MAAM,SAAS,YAAY;AAC3B,OAAI,CAAC,OAAO,UAAU,QAAQ;AAC5B,YAAQ,IAAI,IAAI,0CAA0C,CAAC;AAC3D,YAAQ,IAAI,IAAI,qCAAqC,CAAC;UACjD;AACL,YAAQ,IAAI,KAAK,iCAAiC,CAAC;AACnD,SAAK,MAAM,KAAK,OAAO,UACrB,SAAQ,IAAI,OAAO,IAAI;;AAG3B;;AAEF,MAAI,KAAK,QAAQ;GACf,MAAM,SAAS,YAAY;GAC3B,MAAM,WAAW,YAAY,KAAK,OAAO;AACzC,OAAI,CAAC,WAAW,SAAS,EAAE;AACzB,YAAQ,MAAM,IAAI,wBAAwB,WAAW,CAAC;AACtD,YAAQ,KAAK,EAAE;;GAGjB,MAAM,UAAU,SAAS,WAAW,SAAS,CAAC,GAC1C,MAAM,SAAS,MAAM,SAAS,CAAC,OAAO,GACtC;AACJ,OAAI,OAAO,UAAU,SAAS,QAAQ,IAAI,OAAO,UAAU,SAAS,SAAS,CAC3E,SAAQ,IAAI,KAAK,uBAAuB,UAAU,CAAC;QAC9C;AACL,WAAO,UAAU,KAAK,QAAQ;AAC9B,eAAW,OAAO;AAClB,YAAQ,IAAI,GAAG,yBAAyB,KAAK,QAAQ,GAAG,CAAC;;;AAG7D,MAAI,KAAK,WAAW;GAClB,MAAM,SAAS,YAAY;GAC3B,MAAM,WAAW,YAAY,KAAK,UAAU;GAC5C,MAAM,UAAU,SAAS,WAAW,SAAS,CAAC,GAC1C,MAAM,SAAS,MAAM,SAAS,CAAC,OAAO,GACtC;GACJ,MAAM,SAAS,OAAO,UAAU;AAChC,UAAO,YAAY,OAAO,UAAU,QACjC,MAAM,YAAY,EAAE,KAAK,SAC3B;AACD,OAAI,OAAO,UAAU,SAAS,QAAQ;AACpC,eAAW,OAAO;AAClB,YAAQ,IAAI,GAAG,2BAA2B,KAAK,QAAQ,GAAG,CAAC;SAE3D,SAAQ,IAAI,KAAK,wBAAwB,UAAU,CAAC;;AAGxD,MAAI,CAAC,KAAK,UAAU,CAAC,KAAK,UACxB,SAAQ,OAAO,CAAC;GAElB;AAGJ,aACG,QAAQ,UAAU,CAClB,YAAY,mDAAmD,CAC/D,aAAa;AACZ,eAAW,OAAO,CAAC;GACnB;AAGJ,aACG,QAAQ,QAAQ,CAChB,YAAY,2CAA2C,CACvD,aAAa;AACZ,WAAS,OAAO,CAAC;GACjB;AAGJ,aACG,QAAQ,UAAU,CAClB,YACC,wEACD,CACA,aAAa;AACZ,aAAW,OAAO,CAAC;GACnB;AAGJ,aACG,QAAQ,SAAS,CACjB,YACC,mEACD,CACA,eAAe,iBAAiB,6BAA6B,CAC7D,QAAQ,SAA2B;AAClC,YAAU,OAAO,EAAE,KAAK,KAAK;GAC7B;;;;;AC90BN,SAAS,UAAU,MAAsB;AACvC,SAAQ,MAAR;EACE,KAAK,YAAa,QAAO,MAAM,MAAM,KAAK;EAC1C,KAAK,QAAa,QAAO,MAAM,OAAO,KAAK;EAC3C,KAAK,QAAa,QAAO,MAAM,KAAK,KAAK;EACzC,KAAK,UAAa,QAAO,MAAM,IAAI,KAAK;EACxC,QAAkB,QAAO;;;AAY7B,eAAe,SACb,YACA,WACA,aACA,YAAY,IACG;CACf,MAAM,QAAQ,cAAc,WAAW,gBAAgB;AACvD,SAAQ,IAAI,IAAI,6BAA6B,MAAM,0CAA0C,CAAC;CAE9F,MAAM,EAAE,mBAAmB,MAAM,YAC/B,YACA,WACA,YACC,MAAM,UAAU;AACf,UAAQ,OAAO,MAAM,OAAO,KAAK,KAAK,MAAM,qBAAqB;GAEpE;AAED,SAAQ,OAAO,MAAM,KAAK;AAC1B,SAAQ,IAAI,GAAG,QAAQ,GAAG,KAAK,KAAK,OAAO,eAAe,CAAC,CAAC,kBAAkB;;AAOhF,SAAgB,uBACd,WACA,OACM;AAMN,WACG,QAAQ,uBAAuB,CAC/B,YAAY,qDAAqD,CACjE,OAAO,SAAS,yDAAyD,CACzE,OAAO,WAAW,gEAAgE,CAClF,OAAO,YAAY,yDAAyD,CAC5E,OAAO,OAAO,aAAiC,SAA+D;EAC7G,MAAM,aAAa,OAAO;AAG1B,MAAI,CAAC,KAAK,UAAU,CAAC,YACnB,KAAI;AAGF,SADe,IAAI,UADJC,cAAY,CACS,WAAW,CAClC,cAAc;AAC3B,WAAQ,IAAI,GAAG,6BAA6B,GAAG,IAAI,oCAAoC,CAAC;AACxF,WAAQ,IAAI,IAAI,6CAA6C,CAAC;AAC9D;UACM;AAEN,WAAQ,IAAI,IAAI,8CAA8C,CAAC;;EAInE,IAAI;AACJ,MAAI;AACF,gBAAa,gBAAgB;WACtB,GAAG;AACV,WAAQ,MAAM,IAAI,uCAAuC,IAAI,CAAC;AAC9D,WAAQ,KAAK,EAAE;;AAGjB,MAAI,aAAa;GAEf,MAAM,UAAU,WACb,QAAQ,8FAA8F,CACtG,IAAI,YAAY;AAInB,OAAI,CAAC,SAAS;AACZ,YAAQ,MAAM,IAAI,oCAAoC,cAAc,CAAC;AACrE,YAAQ,KAAK,EAAE;;AAGjB,WAAQ,IAAI,IAAI,YAAY,QAAQ,aAAa,IAAI,QAAQ,KAAK,MAAM,CAAC;GACzE,MAAM,SAAS,MAAM,aAAa,YAAY,QAAQ,IAAI,QAAQ,UAAU;AAE5E,WAAQ,IACN,GAAG,QAAQ,GACX,KAAK,KAAK,OAAO,OAAO,eAAe,CAAC,CAAC,kBACpC,KAAK,OAAO,OAAO,cAAc,CAAC,CAAC,mBACnC,IAAI,OAAO,OAAO,aAAa,GAAG,uBAAuB,GAC/D;AAED,OAAI,KAAK,MACP,OAAM,SAAS,YAAY,QAAQ,IAAI,QAAQ,KAAK;aAG7C,KAAK,OAAO,CAAC,aAAa;AAEnC,WAAQ,IAAI,IAAI,kCAAkC,CAAC;GAEnD,MAAM,EAAE,UAAU,WAAW,MAAM,SAAS,YAAY,WAAW;AAEnE,WAAQ,IACN,GAAG,QAAQ,GACX,KAAK,KAAK,OAAO,SAAS,CAAC,CAAC,aACvB,KAAK,OAAO,OAAO,eAAe,CAAC,CAAC,kBACpC,KAAK,OAAO,OAAO,cAAc,CAAC,CAAC,mBACnC,IAAI,OAAO,OAAO,aAAa,GAAG,uBAAuB,GAC/D;AAED,OAAI,KAAK,MACP,OAAM,SAAS,WAAW;;GAG9B;AAMJ,WACG,QAAQ,uBAAuB,CAC/B,YAAY,yDAAyD,CACrE,OAAO,oBAAoB,6BAA6B,KAAK,CAC7D,OAAO,OAAO,aAAiC,SAAiC;EAC/E,MAAM,aAAa,OAAO;EAE1B,IAAI;AACJ,MAAI;AACF,gBAAa,gBAAgB;WACtB,GAAG;AACV,WAAQ,MAAM,IAAI,uCAAuC,IAAI,CAAC;AAC9D,WAAQ,KAAK,EAAE;;AAGjB,MAAI,aAAa;GACf,MAAM,UAAU,WACb,QAAQ,+CAA+C,CACvD,IAAI,YAAY;AAEnB,OAAI,CAAC,SAAS;AACZ,YAAQ,MAAM,IAAI,sBAAsB,cAAc,CAAC;AACvD,YAAQ,KAAK,EAAE;;AAGjB,SAAM,SAAS,YAAY,QAAQ,IAAI,QAAQ,MAAM,SAAS,KAAK,aAAa,MAAM,GAAG,CAAC;QAE1F,OAAM,SAAS,YAAY,QAAW,QAAW,SAAS,KAAK,aAAa,MAAM,GAAG,CAAC;GAExF;AAMJ,WACG,QAAQ,iBAAiB,CACzB,YAAY,4DAA4D,CACxE,OAAO,oBAAoB,wCAAwC,CACnE,OAAO,qBAAqB,kCAAkC,CAC9D,OAAO,eAAe,6BAA6B,KAAK,CACxD,OACC,iBACA,oDACA,UACD,CACA,OACC,OACE,OACA,SACG;EACH,MAAM,aAAa,OAAO;EAE1B,IAAI;AACJ,MAAI;AACF,gBAAa,gBAAgB;WACtB,GAAG;AACV,WAAQ,MAAM,IAAI,uCAAuC,IAAI,CAAC;AAC9D,WAAQ,KAAK,EAAE;;EAGjB,MAAM,aAAa,SAAS,KAAK,SAAS,MAAM,GAAG;EACnD,MAAM,OAAQ,KAAK,QAAQ;AAG3B,MAAI,CAAC;GAAC;GAAW;GAAY;GAAS,CAAC,SAAS,KAAK,EAAE;AACrD,WAAQ,MAAM,IAAI,iBAAiB,KAAK,qCAAqC,CAAC;AAC9E,WAAQ,KAAK,EAAE;;EAIjB,IAAI;AACJ,MAAI,KAAK,SAAS;GAChB,MAAM,UAAU,WACb,QAAQ,yCAAyC,CACjD,IAAI,KAAK,QAAQ;AAEpB,OAAI,CAAC,QACH,SAAQ,MAAM,KAAK,sBAAsB,KAAK,QAAQ,2BAA2B,CAAC;OAElF,cAAa,CAAC,QAAQ,GAAG;;EAI7B,MAAM,UAAU,KAAK,SAAS,CAAC,KAAK,OAAO,GAAG;EAC9C,MAAM,aAAa;GAAE;GAAY;GAAS;GAAY;EAEtD,IAAI;AAEJ,MAAI,SAAS,UACX,WAAU,aAAa,YAAY,OAAO,WAAW;WAE5C,SAAS,cAAc,SAAS,UAAU;GAGnD,MAAM,UAAU,MAAM,qBADPA,cAAY,CACuB;AAElD,OAAI;IACF,MAAM,EAAE,sBAAsB,MAAM,OAAO;AAE3C,YAAQ,IAAI,IAAI,gCAAgC,CAAC;IACjD,MAAM,iBAAiB,MAAM,kBAAkB,OAAO,KAAK;AAE3D,QAAI,SAAS,WACX,WAAU,MAAM,QAAQ,eAAe,gBAAgB,WAAW;SAC7D;KAEL,MAAM,CAAC,gBAAgB,mBAAmB,MAAM,QAAQ,IAAI,CAC1D,QAAQ,cAAc,OAAO;MAAE,GAAG;MAAY,YAAY;MAAK,CAAC,EAChE,QAAQ,eAAe,gBAAgB;MAAE,GAAG;MAAY,YAAY;MAAK,CAAC,CAC3E,CAAC;AAEF,SAAI,eAAe,WAAW,KAAK,gBAAgB,WAAW,EAC5D,WAAU,EAAE;UACP;MACL,MAAM,UAAU,MACd,GAAG,EAAE,UAAU,GAAG,EAAE,KAAK,GAAG,EAAE,UAAU,GAAG,EAAE;MAE/C,SAAS,gBAAgB,OAA4C;AACnE,WAAI,MAAM,WAAW,EAAG,wBAAO,IAAI,KAAK;OACxC,MAAM,MAAM,KAAK,IAAI,GAAG,MAAM,KAAK,MAAM,EAAE,MAAM,CAAC;OAElD,MAAM,QADM,KAAK,IAAI,GAAG,MAAM,KAAK,MAAM,EAAE,MAAM,CAAC,GAC9B;OACpB,MAAM,oBAAI,IAAI,KAAqB;AACnC,YAAK,MAAM,KAAK,MACd,GAAE,IAAI,OAAO,EAAE,EAAE,UAAU,IAAI,KAAK,EAAE,QAAQ,OAAO,MAAM;AAE7D,cAAO;;MAGT,MAAM,SAAS,gBAAgB,eAAe;MAC9C,MAAM,UAAU,gBAAgB,gBAAgB;MAChD,MAAM,UAAU,IAAI,IAAY,CAC9B,GAAG,eAAe,IAAI,OAAO,EAC7B,GAAG,gBAAgB,IAAI,OAAO,CAC/B,CAAC;MACF,MAAM,0BAAU,IAAI,KAA2B;AAC/C,WAAK,MAAM,KAAK,CAAC,GAAG,gBAAgB,GAAG,gBAAgB,CACrD,SAAQ,IAAI,OAAO,EAAE,EAAE,EAAE;MAG3B,MAAM,WAA2B,EAAE;AACnC,WAAK,MAAM,OAAO,SAAS;OACzB,MAAM,OAAO,QAAQ,IAAI,IAAI;OAC7B,MAAM,gBAAgB,MAAO,OAAO,IAAI,IAAI,IAAI,KAAK,MAAO,QAAQ,IAAI,IAAI,IAAI;AAChF,gBAAS,KAAK;QAAE,GAAG;QAAM,OAAO;QAAe,CAAC;;AAGlD,gBAAU,SACP,MAAM,GAAG,MAAM,EAAE,QAAQ,EAAE,MAAM,CACjC,MAAM,GAAG,WAAW;;;aAGnB;AACR,UAAM,QAAQ,OAAO;;QAGvB,WAAU,EAAE;AAGd,MAAI,CAAC,WAAW,QAAQ,WAAW,GAAG;AACpC,WAAQ,IAAI,IAAI,0BAA0B,MAAM,WAAW,KAAK,GAAG,CAAC;AACpE;;EAIF,MAAM,YAAY,cAAc,SAAS,WAAW;EACpD,MAAM,YAAY,SAAS,YAAY,KAAK,KAAK,KAAK;AAEtD,UAAQ,IACN,OAAO,KAAK,wBAAwB,MAAM,GAAG,GAAG,UAAU,IAAI,IAAI,IAAI,UAAU,OAAO,SAAS,CAAC,IAClG;AAED,OAAK,MAAM,UAAU,WAAW;GAC9B,MAAM,eAAe,OAAO,cACxB,MAAM,KAAK,OAAO,YAAY,GAC9B,MAAM,KAAK,OAAO,OAAO,UAAU,CAAC;GAExC,MAAM,YAAY,UAAU,OAAO,KAAK;GACxC,MAAM,aAAa,IAAI,UAAU,OAAO,MAAM,QAAQ,EAAE,GAAG;GAC3D,MAAM,gBAAgB,IAAI,GAAG,OAAO,KAAK,GAAG,OAAO,UAAU,GAAG,OAAO,UAAU;AAEjF,WAAQ,IACN,KAAK,aAAa,IAAI,UAAU,IAAI,cAAc,IAAI,aACvD;GAGD,MAAM,eAAe,OAAO,QACzB,MAAM,KAAK,CACX,MAAM,GAAG,EAAE,CACX,KAAK,MAAM,OAAO,IAAI;AACzB,WAAQ,IAAI,aAAa,KAAK,KAAK,CAAC;AACpC,WAAQ,KAAK;;GAGlB;AAMH,WACG,QAAQ,wBAAwB,CAChC,YAAY,+BAA+B,CAC3C,QAAQ,gBAAoC;EAC3C,MAAM,aAAa,OAAO;EAE1B,IAAI;AACJ,MAAI;AACF,gBAAa,gBAAgB;WACtB,GAAG;AACV,WAAQ,MAAM,IAAI,uCAAuC,IAAI,CAAC;AAC9D,WAAQ,KAAK,EAAE;;AAGjB,MAAI,aAAa;GAEf,MAAM,UAAU,WACb,QAAQ,6DAA6D,CACrE,IAAI,YAAY;AAInB,OAAI,CAAC,SAAS;AACZ,YAAQ,MAAM,IAAI,sBAAsB,cAAc,CAAC;AACvD,YAAQ,KAAK,EAAE;;GAGjB,MAAM,YAAY,WACf,QACC,kEACD,CACA,IAAI,QAAQ,GAAG;GAElB,MAAM,aAAa,WAChB,QACC,oEACD,CACA,IAAI,QAAQ,GAAG;GAElB,MAAM,aAAa,WAChB,QACC,4EACD,CACA,IAAI,QAAQ,GAAG;GAElB,MAAM,gBAAgB,WACnB,QACC,mGACD,CACA,IAAI,QAAQ,GAAG;AAElB,WAAQ,IAAI,OAAO,KAAK,QAAQ,aAAa,CAAC,GAAG,IAAI,IAAI,QAAQ,KAAK,GAAG,CAAC,IAAI;AAC9E,WAAQ,IAAI,KAAK,KAAK,iBAAiB,CAAC,IAAI,UAAU,QAAQ;AAC9D,WAAQ,IAAI,KAAK,KAAK,UAAU,CAAC,WAAW,WAAW,SAAS;AAChE,WAAQ,IACN,KAAK,KAAK,gBAAgB,CAAC,KAAK,WAAW,UAAU,QAAQ,WAAW,QAAQ,GAAG,IAAI,QAAQ,GAChG;AAED,OAAI,cAAc,SAAS,GAAG;AAC5B,YAAQ,IAAI,OAAO,KAAK,WAAW,GAAG;AACtC,SAAK,MAAM,OAAO,cAChB,SAAQ,IAAI,OAAO,UAAU,IAAI,KAAK,CAAC,OAAO,GAAG,CAAC,IAAI,IAAI,EAAE,SAAS;;AAGzE,WAAQ,KAAK;SAER;GAEL,MAAM,cAAc,WACjB,QAAQ,yCAAyC,CACjD,KAAK;GAER,MAAM,eAAe,WAClB,QAAQ,0CAA0C,CAClD,KAAK;GAER,MAAM,aAAa,WAChB,QAAQ,uDAAuD,CAC/D,KAAK;GAER,MAAM,eAAe,WAClB,QACC;;;;;;;;mCASD,CACA,KAAK;GAQR,MAAM,aAAa,aAAa,KAAK,MAAM,EAAE,WAAW;GACxD,MAAM,0BAAU,IAAI,KAAqB;AACzC,OAAI,WAAW,SAAS,GAAG;IACzB,MAAM,eAAe,WAAW,UAAU,IAAI,CAAC,KAAK,KAAK;IACzD,MAAM,WAAW,WACd,QACC,4DAA4D,aAAa,GAC1E,CACA,IAAI,GAAG,WAAW;AAKrB,SAAK,MAAM,OAAO,SAChB,SAAQ,IAAI,IAAI,IAAI,IAAI,KAAK;;AAIjC,WAAQ,IAAI,OAAO,KAAK,mCAAmC,CAAC,IAAI;AAChE,WAAQ,IACN,KAAK,KAAK,eAAe,CAAC,KAAK,YAAY,EAAE,KAAK,KAAK,gBAAgB,CAAC,IAAI,aAAa,IAC1F;AACD,WAAQ,IACN,KAAK,KAAK,gBAAgB,CAAC,IAAI,WAAW,UAAU,QAAQ,WAAW,QAAQ,GAAG,IAAI,QAAQ,GAC/F;AAED,OAAI,aAAa,SAAS,GAAG;AAC3B,YAAQ,KAAK;IACb,MAAM,OAAO,aAAa,KAAK,MAAM;KACnC,MAAM,KAAK,QAAQ,IAAI,EAAE,WAAW,IAAI,OAAO,EAAE,WAAW,CAAC;KAC7D,OAAO,EAAE,MAAM;KACf,OAAO,EAAE,OAAO;KAChB,EAAE,UAAU,QAAQ,EAAE,QAAQ,GAAG,IAAI,QAAQ;KAC9C,CAAC;AACF,YAAQ,IACN,YAAY;KAAC;KAAW;KAAS;KAAU;KAAe,EAAE,KAAK,CAClE;SAED,SAAQ,IAAI,IAAI,sEAAsE,CAAC;AAEzF,WAAQ,KAAK;;GAEf;;;;;ACteN,MAAMC,qBAAmB,KAAK,SAAS,EAAE,eAAe;;;;;;;;AASxD,SAAS,gBAAwB;AAI/B,QAAO,KAFW,QADC,cAAc,OAAO,KAAK,IAAI,CACZ,EAEd,mBAAmB;;AAO5C,SAASC,mBAA0C;AACjD,KAAI,CAAC,WAAWD,mBAAiB,CAAE,QAAO,EAAE;AAC5C,KAAI;EACF,MAAM,MAAM,aAAaA,oBAAkB,OAAO;AAClD,SAAO,KAAK,MAAM,IAAI;SAChB;AACN,SAAO,EAAE;;;AAIb,SAASE,kBAAgB,MAAqC;AAC5D,eAAcF,oBAAkB,KAAK,UAAU,MAAM,MAAM,EAAE,GAAG,MAAM,OAAO;;AAO/E,SAASG,eAAmB;CAC1B,MAAM,SAAS,eAAe;CAE9B,MAAM,SAASF,kBAAgB;AAG/B,KAAI,OAAO,OAAO,eAAe,YAAY,OAAO,eAAe,KACjE,QAAO,aAAa,EAAE;CAGxB,MAAM,UAAU,OAAO;AAEvB,KAAI,SAAS,SAAS;AACpB,UAAQ,IAAI,KAAK,0DAA0D,CAAC;AAC5E,UAAQ,IAAI,IAAI,YAAY,KAAK,UAAU,QAAQ,OAAO,GAAG,CAAC;AAC9D,UAAQ,IAAI,IAAI,sDAAsD,CAAC;AACvE;;AAGF,SAAQ,SAAS;EACf,SAAS;EACT,MAAM,CAAC,OAAO;EACf;AAED,KAAI;AACF,oBAAgB,OAAO;UAChB,GAAG;AACV,UAAQ,MAAM,IAAI,mCAAmC,IAAI,CAAC;AAC1D,UAAQ,KAAK,EAAE;;AAGjB,SAAQ,IAAI,GAAG,+CAA+C,CAAC;AAC/D,SAAQ,IAAI,IAAI,aAAa,SAAS,CAAC;AACvC,SAAQ,IAAI,IAAI,GAAG,CAAC;AACpB,SAAQ,IAAI,IAAI,uDAAuD,CAAC;AACxE,SAAQ,IAAI,IAAI,+CAA+C,CAAC;AAChE,SAAQ,IAAI,IAAI,kDAAkD,CAAC;AAEnE,KAAI,CAAC,WAAW,OAAO,EAAE;AACvB,UAAQ,KAAK;AACb,UAAQ,IACN,KAAK,mCAAmC,SAAS,CAClD;AACD,UAAQ,IAAI,IAAI,6CAA6C,CAAC;;;AAQlE,SAASG,cAAkB;CACzB,MAAM,SAAS,eAAe;CAC9B,MAAM,SAASH,kBAAgB;CAE/B,MAAM,UACJ,OAAO,OAAO,eAAe,YAAY,OAAO,eAAe,OAC1D,OAAO,aACR,EAAE;CAER,MAAM,aAAa,SAAS;CAC5B,MAAM,YAAY,WAAW,OAAO;AAEpC,SAAQ,KAAK;AACb,SAAQ,IAAI,KAAK,0BAA0B,CAAC;AAC5C,SAAQ,KAAK;AAEb,KAAI,YAAY;EACd,MAAM,QAAQ,QAAQ;AACtB,UAAQ,IAAI,GAAG,iCAAiC,CAAC;AACjD,UAAQ,IAAI,IAAI,eAAe,KAAK,UAAU,MAAM,GAAG,CAAC;QACnD;AACL,UAAQ,IAAI,KAAK,qCAAqC,CAAC;AACvD,UAAQ,IAAI,IAAI,2BAA2B,CAAC;;AAG9C,SAAQ,KAAK;AAEb,KAAI,UACF,SAAQ,IAAI,GAAG,uBAAuB,SAAS,CAAC;MAC3C;AACL,UAAQ,IAAI,KAAK,2BAA2B,SAAS,CAAC;AACtD,UAAQ,IAAI,IAAI,yBAAyB,CAAC;;AAG5C,SAAQ,KAAK;AAEb,KAAI,cAAc,UAChB,SAAQ,IAAI,IAAI,yDAAyD,CAAC;UACjE,cAAc,CAAC,UACxB,SAAQ,IAAI,KAAK,8CAA8C,CAAC;KAEhE,SAAQ,IAAI,IAAI,kDAAkD,CAAC;AAGrE,SAAQ,KAAK;;AAOf,SAAgB,oBAAoB,QAAuB;AACzD,QACG,QAAQ,UAAU,CAClB,YACC,kFACD,CACA,aAAa;AACZ,gBAAY;GACZ;AAEJ,QACG,QAAQ,SAAS,CACjB,YACC,sEACD,CACA,aAAa;AACZ,eAAW;GACX;;;;;AChJN,MAAMI,SAAO,SAAS;AACtB,MAAM,mBAAmB,KAAKA,QAAM,eAAe;AACnD,MAAM,cAAc;AACpB,MAAM,oBAAoB,KAAKA,QAAM,WAAW,eAAe;AAC/D,MAAM,aAAa,KAAK,mBAAmB,GAAG,YAAY,QAAQ;AAClE,MAAM,aAAa;;;;;;AAOnB,SAAS,mBAA2B;AAGlC,QAAO,KADW,QADC,cAAc,OAAO,KAAK,IAAI,CACZ,EACd,sBAAsB;;;;;;AAO/C,SAAS,iBAAyB;AAGhC,QAAO,KADW,QADC,cAAc,OAAO,KAAK,IAAI,CACZ,EACd,0BAA0B;;AAOnD,SAAS,iBAA0C;AACjD,KAAI,CAAC,WAAW,iBAAiB,CAAE,QAAO,EAAE;AAC5C,KAAI;EACF,MAAM,MAAM,aAAa,kBAAkB,OAAO;AAClD,SAAO,KAAK,MAAM,IAAI;SAChB;AACN,SAAO,EAAE;;;AAIb,SAAS,gBAAgB,MAAqC;AAC5D,eAAc,kBAAkB,KAAK,UAAU,MAAM,MAAM,EAAE,GAAG,MAAM,OAAO;;AAO/E,SAAS,cAAc,WAA2B;AAChD,QAAO;;;;;cAKK,YAAY;;;;;kBAKR,UAAU;;;;;;;;;;;cAWd,WAAW;;;cAGX,WAAW;;;;;;;;;;;;;;AAmBzB,eAAeC,cAA2B;CAExC,MAAM,SAAS,IAAI,UADJC,cAAY,CACS,WAAW;AAE/C,KAAI;EAEF,MAAM,IADS,MAAM,OAAO,QAAQ;AAGpC,UAAQ,KAAK;AACb,UAAQ,IAAI,KAAK,sBAAsB,CAAC;AACxC,UAAQ,KAAK;AACb,UAAQ,IAAI,GAAG,mBAAmB,CAAC;AACnC,UAAQ,IAAI,IAAI,oBAAoB,EAAE,UAAU,GAAG,CAAC;AACpD,UAAQ,IAAI,IAAI,oBAAoB,EAAE,gBAAgB,CAAC;AACvD,UAAQ,IACN,IACE,oBAAoB,EAAE,qBAAqB,gBAAgB,OAAO,eAAe,EAAE,qBAAqB,IACzG,CACF;AACD,MAAI,EAAE,iBACJ,SAAQ,IAAI,IAAI,oBAAoB,EAAE,mBAAmB,CAAC;AAE5D,MAAI,EAAE,OAAO;GACX,MAAM,KAAK,EAAE;AACb,WAAQ,IACN,IACE,oBAAoB,GAAG,YAAY,aAAa,GAAG,SAAS,UAAU,GAAG,UAAU,SACpF,CACF;;AAEH,UAAQ,KAAK;UACN,GAAG;EACV,MAAM,MAAM,aAAa,QAAQ,EAAE,UAAU,OAAO,EAAE;AACtD,UAAQ,KAAK;AACb,UAAQ,IAAI,KAAK,sBAAsB,CAAC;AACxC,UAAQ,KAAK;AACb,UAAQ,IAAI,KAAK,yBAAyB,MAAM,CAAC;AACjD,UAAQ,IAAI,IAAI,mCAAmC,CAAC;AACpD,UAAQ,IAAI,IAAI,gDAAgD,CAAC;AACjE,UAAQ,KAAK;AACb,UAAQ,KAAK,EAAE;;;AAInB,SAAS,aAAmB;AAE1B,KAAI;EACF,MAAM,SAAS,UAAU,SAAS,CAAC,MAAM,oBAAoB,EAAE,EAC7D,UAAU,QACX,CAAC;AAEF,MAAI,OAAO,WAAW,KAAK,CAAC,OAAO,OAAO,MAAM,EAAE;AAChD,WAAQ,IAAI,KAAK,uCAAuC,CAAC;AAQzD,OALqB,UACnB,aACA;IAAC;IAAa;IAAM,OAAO,QAAQ,UAAU,IAAI,IAAI,GAAG;IAAc,EACtE,EAAE,UAAU,QAAQ,CACrB,CACgB,WAAW,EAC1B,SAAQ,IAAI,GAAG,6BAA6B,CAAC;OAE7C,SAAQ,IAAI,IAAI,uDAAuD,CAAC;AAE1E;;EAGF,MAAM,OAAO,OAAO,OACjB,MAAM,CACN,MAAM,KAAK,CACX,KAAK,MAAM,EAAE,MAAM,CAAC;AACvB,OAAK,MAAM,OAAO,KAChB,KAAI;AACF,WAAQ,KAAK,SAAS,KAAK,GAAG,EAAE,UAAU;AAC1C,WAAQ,IAAI,GAAG,uBAAuB,IAAI,GAAG,CAAC;UACxC;AACN,WAAQ,IAAI,KAAK,wBAAwB,IAAI,GAAG,CAAC;;AAGrD,UAAQ,IAAI,IAAI,iDAAiD,CAAC;UAC3D,GAAG;EACV,MAAM,MAAM,aAAa,QAAQ,EAAE,UAAU,OAAO,EAAE;AACtD,UAAQ,MAAM,IAAI,kBAAkB,MAAM,CAAC;AAC3C,UAAQ,KAAK,EAAE;;;AAInB,SAAS,aAAmB;CAC1B,MAAM,YAAY,kBAAkB;CACpC,MAAM,UAAU,gBAAgB;AAEhC,SAAQ,KAAK;AACb,SAAQ,IAAI,KAAK,uBAAuB,CAAC;AACzC,SAAQ,KAAK;AAGb,KAAI,CAAC,WAAW,kBAAkB,CAChC,WAAU,mBAAmB,EAAE,WAAW,MAAM,CAAC;CAGnD,MAAM,eAAe,cAAc,UAAU;AAC7C,KAAI;AACF,gBAAc,YAAY,cAAc,OAAO;AAC/C,UAAQ,IAAI,GAAG,0BAA0B,aAAa,CAAC;UAChD,GAAG;AACV,UAAQ,MAAM,IAAI,4BAA4B,IAAI,CAAC;AACnD,UAAQ,KAAK,EAAE;;AAIjB,KAAI;AACF,YAAU,aAAa,CAAC,UAAU,WAAW,EAAE,EAAE,UAAU,QAAQ,CAAC;EACpE,MAAM,aAAa,UAAU,aAAa,CAAC,QAAQ,WAAW,EAAE,EAC9D,UAAU,QACX,CAAC;AACF,MAAI,WAAW,WAAW,EACxB,SAAQ,IAAI,GAAG,iCAAiC,CAAC;MAEjD,SAAQ,IAAI,KAAK,qBAAqB,WAAW,QAAQ,MAAM,IAAI,kBAAkB,CAAC;SAElF;AACN,UAAQ,IAAI,KAAK,4CAA4C,CAAC;AAC9D,UAAQ,IAAI,IAAI,sBAAsB,aAAa,CAAC;;CAItD,MAAM,SAAS,gBAAgB;AAE/B,KAAI,OAAO,OAAO,eAAe,YAAY,OAAO,eAAe,KACjE,QAAO,aAAa,EAAE;CAGxB,MAAM,UAAU,OAAO;CAGvB,MAAM,WAAW,QAAQ;AAIzB,MAHqB,UAAU,OACG,SAAS,QAAQ,CAGjD,SAAQ,IAAI,GAAG,+CAA+C,CAAC;MAC1D;AAEL,MAAI,UAAU;AACZ,WAAQ,gBAAgB;AACxB,WAAQ,IAAI,IAAI,oDAAoD,CAAC;;AAGvE,UAAQ,SAAS;GACf,SAAS;GACT,MAAM,CAAC,QAAQ;GAChB;AAED,MAAI;AACF,mBAAgB,OAAO;AACvB,WAAQ,IAAI,GAAG,+CAA+C,CAAC;WACxD,GAAG;AACV,WAAQ,MAAM,IAAI,qCAAqC,IAAI,CAAC;AAC5D,WAAQ,KAAK,EAAE;;;AAKnB,SAAQ,KAAK;AACb,KAAI,CAAC,WAAW,UAAU,EAAE;AAC1B,UAAQ,IAAI,KAAK,8BAA8B,YAAY,CAAC;AAC5D,UAAQ,IAAI,IAAI,uBAAuB,CAAC;OAExC,SAAQ,IAAI,GAAG,oBAAoB,YAAY,CAAC;AAGlD,KAAI,CAAC,WAAW,QAAQ,EAAE;AACxB,UAAQ,IAAI,KAAK,4BAA4B,UAAU,CAAC;AACxD,UAAQ,IAAI,IAAI,uBAAuB,CAAC;OAExC,SAAQ,IAAI,GAAG,kBAAkB,UAAU,CAAC;AAG9C,SAAQ,KAAK;AACb,SAAQ,IAAI,IAAI,iEAAiE,CAAC;AAClF,SAAQ,KAAK;;AAGf,SAAS,eAAqB;AAC5B,SAAQ,KAAK;AACb,SAAQ,IAAI,KAAK,yBAAyB,CAAC;AAC3C,SAAQ,KAAK;AAGb,KAAI,WAAW,WAAW,EAAE;AAC1B,MAAI;AACF,aAAU,aAAa,CAAC,UAAU,WAAW,EAAE,EAAE,UAAU,QAAQ,CAAC;AACpE,WAAQ,IAAI,GAAG,4BAA4B,CAAC;UACtC;AACN,WAAQ,IAAI,KAAK,0CAA0C,CAAC;;AAE9D,MAAI;AACF,cAAW,WAAW;AACtB,WAAQ,IAAI,GAAG,oBAAoB,aAAa,CAAC;WAC1C,GAAG;AACV,WAAQ,IAAI,KAAK,6BAA6B,IAAI,CAAC;;OAGrD,SAAQ,IAAI,IAAI,4BAA4B,CAAC;CAI/C,MAAM,SAAS,gBAAgB;CAC/B,MAAM,UACJ,OAAO,OAAO,eAAe,YAAY,OAAO,eAAe,OAC1D,OAAO,aACR,EAAE;CAER,MAAM,SAAS,QAAQ;AACvB,KAAI,QAAQ;AACV,UAAQ,SAAS;AACjB,SAAO,QAAQ;AACf,MAAI;AACF,mBAAgB,OAAO;AACvB,WAAQ,IAAI,GAAG,kDAAkD,CAAC;WAC3D,GAAG;AACV,WAAQ,IAAI,KAAK,sCAAsC,IAAI,CAAC;;OAG9D,SAAQ,IAAI,IAAI,iDAAiD,CAAC;AAGpE,SAAQ,KAAK;AACb,SAAQ,IAAI,IAAI,2DAA2D,CAAC;AAC5E,SAAQ,KAAK;;AAGf,eAAe,WAAW,kBAA0C;AAClE,SAAQ,KAAK;AACb,SAAQ,IAAI,KAAK,uBAAuB,CAAC;AACzC,SAAQ,KAAK;AACb,SAAQ,IAAI,IAAI,6CAA6C,CAAC;AAC9D,SAAQ,KAAK;CAGb,MAAM,EAAE,kBAAkB,MAAM,OAAO;CACvC,MAAM,EAAE,SAAS,aAAa,MAAM,aAAa,MAAM,OAAO;CAM9D,MAAM,kBAAkB,SAJN,YADC,cAAc,OAAO,KAAK,IAAI,CACR,EAIG,SAAS,UAAU,oBAAoB;CACnF,MAAM,EAAE,WAAW,UAAU,MAAM,OAAO;CAG1C,MAAM,YAAY,CAAC,OAAO,gBAAgB;AAC1C,KAAI,iBACF,WAAU,KAAK,uBAAuB,iBAAiB;CAGzD,MAAM,SAAS,MAAM,OAAO,WAAW;EAAE,OAAO;EAAW,UAAU;EAAQ,CAAC;AAE9E,KAAI,OAAO,WAAW,GAAG;AACvB,UAAQ,MAAM,IAAI,0CAA0C,CAAC;AAC7D,UAAQ,KAAK,OAAO,UAAU,EAAE;;;AAIpC,SAAS,QAAQ,MAAkD;CACjE,MAAM,QAAQ,KAAK,SAAS;AAE5B,KAAI,CAAC,WAAW,WAAW,EAAE;AAC3B,UAAQ,IAAI,KAAK,0BAA0B,WAAW,GAAG,CAAC;AAC1D,UAAQ,IAAI,IAAI,mCAAmC,CAAC;AACpD;;AAGF,KAAI,KAAK,OAEP,KAAI;AACF,WAAS,cAAc,MAAM,IAAI,WAAW,IAAI,EAAE,OAAO,WAAW,CAAC;SAC/D;KAIR,KAAI;AACF,WAAS,WAAW,MAAM,IAAI,WAAW,IAAI,EAAE,OAAO,WAAW,CAAC;UAC3D,GAAG;EACV,MAAM,MAAM,aAAa,QAAQ,EAAE,UAAU,OAAO,EAAE;AACtD,UAAQ,MAAM,IAAI,uBAAuB,MAAM,CAAC;AAChD,UAAQ,KAAK,EAAE;;;AASrB,SAAgB,uBAAuB,WAA0B;AAC/D,WACG,QAAQ,QAAQ,CAChB,YAAY,yCAAyC,CACrD,OAAO,YAAY;EAClB,MAAM,EAAE,UAAU,MAAM,OAAO;EAC/B,MAAM,EAAE,YAAY,IAAI,oBAAoB,MAAM,OAChD;AAEF,mBAAiB;AAEjB,QAAM,MADS,IAAI,CACA;GACnB;AAEJ,WACG,QAAQ,UAAU,CAClB,YAAY,oDAAoD,CAChE,OACC,6BACA,+FACD,CACA,OAAO,OAAO,SAAwC;AACrD,QAAM,WAAW,KAAK,iBAAiB;GACvC;AAEJ,WACG,QAAQ,SAAS,CACjB,YAAY,kCAAkC,CAC9C,OAAO,YAAY;AAClB,QAAMD,aAAW;GACjB;AAEJ,WACG,QAAQ,UAAU,CAClB,YAAY,+DAA+D,CAC3E,aAAa;AACZ,cAAY;GACZ;AAEJ,WACG,QAAQ,UAAU,CAClB,YACC,gFACD,CACA,aAAa;AACZ,cAAY;GACZ;AAEJ,WACG,QAAQ,YAAY,CACpB,YAAY,sDAAsD,CAClE,aAAa;AACZ,gBAAc;GACd;AAEJ,WACG,QAAQ,OAAO,CACf,YAAY,wBAAwB,WAAW,GAAG,CAClD,OAAO,mBAAmB,2BAA2B,KAAK,CAC1D,OAAO,gBAAgB,mCAAmC,CAC1D,QAAQ,SAA+C;AACtD,UAAQ,KAAK;GACb;;;;;ACtcN,MAAME,SAAO,SAAS;AACtB,MAAMC,gBAAc,KAAKD,QAAM,QAAQ,cAAc;AACrD,MAAME,gBAAc,KAAKF,QAAM,WAAW,OAAO,cAAc;AAC/D,MAAMG,gBAAc,KAAKH,QAAM,QAAQ,UAAU;AACjD,MAAMI,qBAAmB;AACzB,MAAMC,gBAAc;AACpB,MAAMC,YAAU;AAMhB,SAAS,YAAoB;CAC3B,MAAM,sBAAM,IAAI,MAAM;AAOtB,QAAO,GANM,IAAI,aAAa,CAMf,GALJ,OAAO,IAAI,UAAU,GAAG,EAAE,CAAC,SAAS,GAAG,IAAI,CAKjC,GAJV,OAAO,IAAI,SAAS,CAAC,CAAC,SAAS,GAAG,IAAI,CAItB,GAHhB,OAAO,IAAI,UAAU,CAAC,CAAC,SAAS,GAAG,IAAI,GACvC,OAAO,IAAI,YAAY,CAAC,CAAC,SAAS,GAAG,IAAI,GACzC,OAAO,IAAI,YAAY,CAAC,CAAC,SAAS,GAAG,IAAI;;AAItD,SAAS,SAAS,OAAuB;AACvC,KAAI,QAAQ,KAAM,QAAO,GAAG,MAAM;AAClC,KAAI,QAAQ,OAAO,KAAM,QAAO,IAAI,QAAQ,MAAM,QAAQ,EAAE,CAAC;AAC7D,QAAO,IAAI,SAAS,OAAO,OAAO,QAAQ,EAAE,CAAC;;AAG/C,SAAS,SAAS,MAAsB;AACtC,KAAI;AACF,SAAO,SAAS,SAAS,KAAK,CAAC,KAAK;SAC9B;AACN,SAAO;;;AAQX,SAAgB,uBAAuB,SAAwB;AAC7D,SACG,QAAQ,SAAS,CACjB,YAAY,oEAAoE,CAChF,OAAO,iBAAiB,2DAA2D,CACnF,OAAO,OAAO,SAAgC;EAE7C,MAAM,YAAY,KAAKH,eADZ,WAAW,CACiB;AAEvC,UAAQ,IAAI,IAAI,oBAAoB,YAAY,CAAC;AACjD,YAAU,WAAW,EAAE,WAAW,MAAM,CAAC;EAEzC,MAAM,UAA2E,EAAE;AAMnF,MAAI,WAAWF,cAAY,EAAE;GAC3B,MAAM,OAAO,KAAK,WAAW,cAAc;AAC3C,OAAI;AACF,iBAAaA,eAAa,KAAK;AAC/B,YAAQ,KAAK;KAAE,OAAO;KAAe,MAAM;KAAM,MAAM,SAAS,KAAK;KAAE,QAAQ,GAAG,KAAK;KAAE,CAAC;YACnF,GAAG;AACV,YAAQ,KAAK;KAAE,OAAO;KAAe,MAAM;KAAM,MAAM;KAAK,QAAQ,IAAI,WAAW,IAAI;KAAE,CAAC;;QAG5F,SAAQ,KAAK;GAAE,OAAO;GAAe,MAAMA;GAAa,MAAM;GAAK,QAAQ,KAAK,sBAAsB;GAAE,CAAC;AAO3G,MAAI,WAAWC,cAAY,EAAE;GAC3B,MAAM,OAAO,KAAK,WAAW,cAAc;AAC3C,OAAI;AACF,iBAAaA,eAAa,KAAK;AAC/B,YAAQ,KAAK;KAAE,OAAO;KAAU,MAAM;KAAM,MAAM,SAAS,KAAK;KAAE,QAAQ,GAAG,KAAK;KAAE,CAAC;YAC9E,GAAG;AACV,YAAQ,KAAK;KAAE,OAAO;KAAU,MAAM;KAAM,MAAM;KAAK,QAAQ,IAAI,WAAW,IAAI;KAAE,CAAC;;QAGvF,SAAQ,KAAK;GAAE,OAAO;GAAU,MAAMA;GAAa,MAAM;GAAK,QAAQ,KAAK,sBAAsB;GAAE,CAAC;EAOtG,MAAM,eAAe,KAAKF,QAAM,QAAQ,gBAAgB;AACxD,MAAI,WAAW,aAAa,EAAE;GAC5B,MAAM,OAAO,KAAK,WAAW,gBAAgB;AAC7C,OAAI;AACF,iBAAa,cAAc,KAAK;AAChC,YAAQ,KAAK;KAAE,OAAO;KAA0B,MAAM;KAAM,MAAM,SAAS,KAAK;KAAE,QAAQ,GAAG,KAAK;KAAE,CAAC;YAC9F,GAAG;AACV,YAAQ,KAAK;KAAE,OAAO;KAA0B,MAAM;KAAM,MAAM;KAAK,QAAQ,KAAK,YAAY,IAAI;KAAE,CAAC;;;AAQ3G,MAAI,KAAK,UAAU;GACjB,MAAM,UAAU,KAAK,WAAW,mBAAmB;AACnD,WAAQ,IAAI,IAAI,wBAAwBI,mBAAiB,8BAA8B,CAAC;AACxF,OAAI;AAEF,aAAS,kBAAkBA,mBAAiB,gCAAgC,EAC1E,OAAO,QACR,CAAC;AAEF,aACE,eAAeA,mBAAiB,cAAcE,UAAQ,GAAGD,cAAY,MAAM,QAAQ,IACnF;KAAE,OAAO;MAAC;MAAQ;MAAQ;MAAO;KAAE,OAAO;KAAM,CACjD;AACD,YAAQ,KAAK;KAAE,OAAO;KAAe,MAAM;KAAS,MAAM,SAAS,QAAQ;KAAE,QAAQ,GAAG,KAAK;KAAE,CAAC;YACzF,GAAG;IACV,MAAM,MAAM,aAAa,QAAQ,EAAE,QAAQ,MAAM,KAAK,CAAC,KAAK,OAAO,EAAE;AACrE,YAAQ,KAAK;KAAE,OAAO;KAAe,MAAM;KAAS,MAAM;KAAK,QAAQ,IAAI,WAAW,MAAM;KAAE,CAAC;AAC/F,YAAQ,IAAI,KAAK,+DAA+DD,mBAAiB,IAAI,CAAC;;QAGxG,SAAQ,KAAK;GAAE,OAAO;GAAe,MAAM;GAAK,MAAM;GAAK,QAAQ,IAAI,0BAA0B;GAAE,CAAC;AAOtG,UAAQ,IAAI,KAAK,KAAK,mBAAmB,CAAC,GAAG,UAAU,IAAI;EAE3D,MAAM,aAAa,KAAK,IAAI,GAAG,QAAQ,KAAK,MAAM,EAAE,MAAM,OAAO,CAAC,GAAG;AACrE,OAAK,MAAM,KAAK,SAAS;GACvB,MAAM,QAAQ,EAAE,MAAM,OAAO,WAAW;AACxC,WAAQ,IAAI,KAAK,KAAK,MAAM,CAAC,GAAG,EAAE,OAAO,IAAI,IAAI,EAAE,KAAK,GAAG;;AAG7D,UAAQ,IAAI,OAAO,IAAI,QAAQ,CAAC,GAAG,YAAY;AAC/C,UAAQ,IAAI,KAAK,IAAI,cAAc,CAAC,eAAe,UAAU,IAAI;GACjE;;;;;AC5IN,MAAM,OAAO,SAAS;AACtB,MAAM,cAAc,KAAK,MAAM,QAAQ,cAAc;AACrD,MAAM,cAAc,KAAK,MAAM,WAAW,OAAO,cAAc;AAC/D,MAAM,cAAc,KAAK,MAAM,QAAQ,UAAU;AACjD,MAAM,mBAAmB;AACzB,MAAM,cAAc;AACpB,MAAM,UAAU;AAMhB,SAAS,QAAQ,UAAoC;AACnD,QAAO,IAAI,SAAS,YAAY;EAC9B,MAAM,KAAK,gBAAgB;GAAE,OAAO,QAAQ;GAAO,QAAQ,QAAQ;GAAQ,CAAC;AAC5E,KAAG,SAAS,GAAG,SAAS,WAAW,WAAW;AAC5C,MAAG,OAAO;AACV,WAAQ,OAAO,MAAM,CAAC,aAAa,KAAK,IAAI;IAC5C;GACF;;AAGJ,SAAS,cAAwB;AAC/B,KAAI,CAAC,WAAW,YAAY,CAAE,QAAO,EAAE;AACvC,QAAO,YAAY,YAAY,CAC5B,QAAQ,SAAS;AAEhB,SAAO,SADM,KAAK,aAAa,KAAK,CACf,CAAC,aAAa,IAAI,4BAA4B,KAAK,KAAK;GAC7E,CACD,MAAM,CACN,SAAS;;AAGd,SAAS,iBAAiB,MAAsB;CAE9C,MAAM,IAAI,KAAK,MAAM,8CAA8C;AACnE,KAAI,CAAC,EAAG,QAAO;AACf,QAAO,GAAG,EAAE,GAAG,GAAG,EAAE,GAAG,GAAG,EAAE,GAAG,GAAG,EAAE;;AAGtC,SAAS,eAAe,WAA6B;AACnD,KAAI;AACF,SAAO,YAAY,UAAU;SACvB;AACN,SAAO,EAAE;;;AAQb,SAAgB,wBAAwB,SAAwB;AAC9D,SACG,QAAQ,wBAAwB,CAChC,YAAY,0DAA0D,CACtE,OAAO,iBAAiB,uCAAuC,CAC/D,OAAO,SAAS,6CAA6C,CAC7D,OAAO,OAAO,YAAgC,SAA8C;EAM3F,IAAI;AAEJ,MAAI,YAAY;AACd,iBAAc,WAAW,WAAW,IAAI,GACpC,WAAW,QAAQ,MAAM,KAAK,GAC9B;AAEJ,OAAI,CAAC,WAAW,YAAY,EAAE;AAC5B,YAAQ,MAAM,IAAI,+BAA+B,cAAc,CAAC;AAChE,YAAQ,KAAK,EAAE;;SAEZ;GAEL,MAAM,UAAU,aAAa;AAC7B,OAAI,QAAQ,WAAW,GAAG;AACxB,YAAQ,MAAM,IAAI,uBAAuB,cAAc,CAAC;AACxD,YAAQ,IAAI,IAAI,oCAAoC,CAAC;AACrD,YAAQ,KAAK,EAAE;;AAGjB,WAAQ,IAAI,KAAK,KAAK,oBAAoB,CAAC,oBAAoB;AAC/D,WAAQ,SAAS,MAAM,MAAM;IAC3B,MAAM,MAAM,KAAK,aAAa,KAAK;IACnC,MAAM,WAAW,eAAe,IAAI,CAAC,KAAK,KAAK;IAC/C,MAAM,SAAS,MAAM,IAAI,GAAG,YAAY,GAAG;AAC3C,YAAQ,IAAI,KAAK,KAAK,OAAO,IAAI,EAAE,CAAC,SAAS,EAAE,CAAC,CAAC,IAAI,iBAAiB,KAAK,GAAG,SAAS;AACvF,YAAQ,IAAI,SAAS,IAAI,IAAI,GAAG;AAChC,YAAQ,IAAI,SAAS,IAAI,YAAY,CAAC,GAAG,WAAW;KACpD;AAEF,WAAQ,KAAK;AACb,iBAAc,KAAK,aAAa,QAAQ,GAAG;AAC3C,WAAQ,IAAI,IAAI,wBAAwB,YAAY,IAAI,CAAC;;EAO3D,MAAM,cAAc,WAAW,KAAK,aAAa,cAAc,CAAC;EAChE,MAAM,YAAc,WAAW,KAAK,aAAa,cAAc,CAAC;EAChE,MAAM,SAAc,WAAW,KAAK,aAAa,mBAAmB,CAAC;EACrE,MAAM,SAAc,WAAW,KAAK,aAAa,gBAAgB,CAAC;AAElE,UAAQ,IAAI,GAAG,KAAK,mBAAmB,GAAG;AAC1C,UAAQ,IAAI,0BAA0B,cAAc,GAAG,UAAU,GAAG,KAAK,UAAU,GAAG;AACtF,UAAQ,IAAI,0BAA0B,YAAc,GAAG,UAAU,GAAG,KAAK,UAAU,GAAG;AACtF,UAAQ,IAAI,0BAA0B,UAAU,KAAK,WAAW,GAAG,UAAU,GAAG,SAAS,KAAK,sCAAsC,GAAG,KAAK,UAAU,GAAG;AACzJ,MAAI,OACF,SAAQ,IAAI,0BAA0B,GAAG,UAAU,CAAC,GAAG,IAAI,WAAW,GAAG;AAO3E,UAAQ,IAAI,KAAK,KAAK,WAAW,CAAC,6CAA6C;AAC/E,MAAI,UAAU,KAAK,SACjB,SAAQ,IAAI,KAAK,qEAAqE,CAAC;AAIzF,MAAI,EADY,KAAK,OAAO,MAAM,QAAQ,wBAAwB,GACpD;AACZ,WAAQ,IAAI,IAAI,qBAAqB,CAAC;AACtC,WAAQ,KAAK,EAAE;;AAGjB,UAAQ,KAAK;EAEb,MAAM,UAA+C,EAAE;AAMvD,MAAI,YACF,KAAI;AACF,aAAU,KAAK,MAAM,OAAO,EAAE,EAAE,WAAW,MAAM,CAAC;AAClD,gBAAa,KAAK,aAAa,cAAc,EAAE,YAAY;AAC3D,WAAQ,KAAK;IAAE,OAAO;IAAe,QAAQ,GAAG,WAAW;IAAE,CAAC;WACvD,GAAG;AACV,WAAQ,KAAK;IAAE,OAAO;IAAe,QAAQ,IAAI,WAAW,IAAI;IAAE,CAAC;;MAGrE,SAAQ,KAAK;GAAE,OAAO;GAAe,QAAQ,KAAK,8BAA8B;GAAE,CAAC;AAOrF,MAAI,UACF,KAAI;AACF,aAAU,KAAK,MAAM,WAAW,MAAM,EAAE,EAAE,WAAW,MAAM,CAAC;AAC5D,gBAAa,KAAK,aAAa,cAAc,EAAE,YAAY;AAC3D,WAAQ,KAAK;IAAE,OAAO;IAAU,QAAQ,GAAG,WAAW;IAAE,CAAC;WAClD,GAAG;AACV,WAAQ,KAAK;IAAE,OAAO;IAAU,QAAQ,IAAI,WAAW,IAAI;IAAE,CAAC;;MAGhE,SAAQ,KAAK;GAAE,OAAO;GAAU,QAAQ,KAAK,8BAA8B;GAAE,CAAC;AAOhF,MAAI,OACF,KAAI;AACF,gBAAa,KAAK,aAAa,gBAAgB,EAAE,KAAK,MAAM,QAAQ,gBAAgB,CAAC;AACrF,WAAQ,KAAK;IAAE,OAAO;IAA0B,QAAQ,GAAG,WAAW;IAAE,CAAC;WAClE,GAAG;AACV,WAAQ,KAAK;IAAE,OAAO;IAA0B,QAAQ,KAAK,YAAY,IAAI;IAAE,CAAC;;AAQpF,MAAI,UAAU,KAAK,UAAU;AAC3B,WAAQ,IAAI,IAAI,2DAA2D,CAAC;AAC5E,OAAI;AAEF,aAAS,kBAAkB,iBAAiB,gCAAgC,EAC1E,OAAO,QACR,CAAC;AAIF,aADmB,eAAe,iBAAiB,WAAW,QAAQ,+BAA+B,YAAY,oBAAoB,YAAY,SAAS,QAAQ,KAC7I;KAAE,OAAO;KAAQ,OAAO;KAAM,CAAC;IAGpD,MAAM,aAAa,aAAa,KAAK,aAAa,mBAAmB,EAAE,OAAO;IAC9E,MAAM,aAAa,UACjB,UACA;KAAC;KAAQ;KAAM;KAAkB;KAAQ;KAAM;KAAS;KAAY,EACpE;KAAE,OAAO;KAAY,UAAU;KAAQ,OAAO;MAAC;MAAQ;MAAQ;MAAO;KAAE,CACzE;AAED,QAAI,WAAW,WAAW,GAAG;KAC3B,MAAM,SAAS,WAAW,QAAQ,MAAM,KAAK,CAAC,MAAM;AACpD,aAAQ,KAAK;MAAE,OAAO;MAAe,QAAQ,IAAI,WAAW,SAAS;MAAE,CAAC;UAExE,SAAQ,KAAK;KAAE,OAAO;KAAe,QAAQ,GAAG,WAAW;KAAE,CAAC;YAEzD,GAAG;IACV,MAAM,MAAM,aAAa,QAAQ,EAAE,QAAQ,MAAM,KAAK,CAAC,KAAK,OAAO,EAAE;AACrE,YAAQ,KAAK;KAAE,OAAO;KAAe,QAAQ,IAAI,WAAW,MAAM;KAAE,CAAC;AACrE,YAAQ,IAAI,KAAK,uCAAuC,iBAAiB,IAAI,CAAC;;aAEvE,CAAC,UAAU,CAAC,KAAK,UAAU;GACpC,MAAM,SAAS,CAAC,SAAS,0BAA0B;AACnD,WAAQ,KAAK;IAAE,OAAO;IAAe,QAAQ,IAAI,YAAY,OAAO,GAAG;IAAE,CAAC;;AAO5E,UAAQ,IAAI,KAAK,KAAK,oBAAoB,CAAC,IAAI;EAC/C,MAAM,aAAa,KAAK,IAAI,GAAG,QAAQ,KAAK,MAAM,EAAE,MAAM,OAAO,CAAC,GAAG;AACrE,OAAK,MAAM,KAAK,QACd,SAAQ,IAAI,KAAK,KAAK,EAAE,MAAM,OAAO,WAAW,CAAC,CAAC,GAAG,EAAE,SAAS;AAIlE,MAAI,CADc,QAAQ,MAAM,MAAM,EAAE,OAAO,SAAS,WAAa,CAAC,EACtD;AACd,WAAQ,IAAI,OAAO,GAAG,YAAY,CAAC,4CAA4C;AAC/E,WAAQ,IAAI,KAAK,IAAI,WAAW,CAAC,uBAAuB;SACnD;AACL,WAAQ,IAAI,OAAO,KAAK,0CAA0C,CAAC,IAAI;AACvE,WAAQ,KAAK,EAAE;;GAEjB;;;;;ACpPN,MAAM,IAAI;CACR,OAAO,MAAc,MAAM,KAAK,EAAE;CAClC,MAAM,MAAc,MAAM,IAAI,EAAE;CAChC,QAAQ,MAAc,MAAM,MAAM,EAAE;CACpC,SAAS,MAAc,MAAM,OAAO,EAAE;CACtC,OAAO,MAAc,MAAM,KAAK,EAAE;CAClC,MAAM,MAAc,MAAM,IAAI,EAAE;CAChC,OAAO,MAAc,MAAM,KAAK,EAAE;CAClC,KAAK,MAAc,MAAM,MAAM,OAAO,EAAE;CACxC,OAAO,MAAc,MAAM,OAAO,OAAO,EAAE;CAC3C,MAAM,MAAc,MAAM,IAAI,OAAO,EAAE;CACxC;AAED,SAASG,OAAK,OAAO,IAAI;AACvB,SAAQ,IAAI,KAAK;;AAGnB,SAAS,QAAQ,OAAe;AAC9B,SAAM;AACN,SAAQ,IAAI,MAAM,KAAK,KAAK,OAAO,MAAM,CAAC;AAC1C,SAAQ,IAAI,MAAM,IAAI,OAAO,IAAI,OAAO,MAAM,OAAO,CAAC,CAAC;;AAOzD,SAAS,WAAW;CAClB,MAAM,KAAK,gBAAgB;EACzB,OAAO,QAAQ;EACf,QAAQ,QAAQ;EACjB,CAAC;AAGF,IAAG,GAAG,gBAAgB;AACpB,UAAM;AACN,SAAK,EAAE,IAAI,uDAAuD,CAAC;AACnE,UAAM;AACN,UAAQ,KAAK,EAAE;GACf;AAEF,QAAO;;AAGT,eAAe,OAAO,IAAiC,UAAmC;AACxF,QAAO,IAAI,SAAS,YAAY;AAC9B,KAAG,SAAS,WAAW,WAAW;AAChC,WAAQ,OAAO,MAAM,CAAC;IACtB;GACF;;;;;;AAOJ,eAAe,WACb,IACA,SACA,aAAa,GACI;AACjB,MAAK,IAAI,IAAI,GAAG,IAAI,QAAQ,QAAQ,KAAK;EACvC,MAAM,MAAM,MAAM,KAAK,KAAK,IAAI,EAAE,GAAG;EACrC,MAAM,QAAQ,MAAM,aAAa,MAAM,KAAK,QAAQ,GAAG,MAAM,GAAG,QAAQ,GAAG;EAC3E,MAAM,SAAS,MAAM,aAAa,MAAM,IAAI,iBAAiB,GAAG;AAChE,UAAQ,IAAI,GAAG,IAAI,GAAG,QAAQ,SAAS;AACvC,MAAI,QAAQ,GAAG,YACb,SAAQ,IAAI,MAAM,IAAI,QAAQ,QAAQ,GAAG,cAAc,CAAC;;AAG5D,SAAM;AAEN,QAAO,MAAM;EACX,MAAM,SAAS,MAAM,OACnB,IACA,MAAM,KAAK,qBAAqB,QAAQ,OAAO,cAAc,aAAa,EAAE,KAAK,CAClF;AAED,MAAI,WAAW,GAAI,QAAO;EAE1B,MAAM,IAAI,SAAS,QAAQ,GAAG;AAC9B,MAAI,CAAC,MAAM,EAAE,IAAI,KAAK,KAAK,KAAK,QAAQ,OACtC,QAAO,IAAI;AAGb,UAAQ,IAAI,EAAE,KAAK,uCAAuC,QAAQ,OAAO,GAAG,CAAC;;;;;;AAOjF,eAAe,YACb,IACA,UACA,aAAa,MACK;CAClB,MAAM,OAAO,aAAa,UAAU;CACpC,MAAM,SAAS,MAAM,OAAO,IAAI,KAAK,SAAS,GAAG,MAAM,IAAI,KAAK,CAAC,IAAI;AAErE,KAAI,WAAW,GAAI,QAAO;AAC1B,QAAO,OAAO,aAAa,CAAC,WAAW,IAAI;;AAO7C,SAAS,gBAAyC;AAChD,KAAI,CAAC,WAAWC,cAAY,CAAE,QAAO,EAAE;AACvC,KAAI;AACF,SAAO,KAAK,MAAM,aAAaA,eAAa,QAAQ,CAAC;SAC/C;AACN,SAAO,EAAE;;;AAIb,SAAS,eAAe,MAAqC;AAC3D,KAAI,CAAC,WAAW,WAAW,CACzB,WAAU,YAAY,EAAE,WAAW,MAAM,CAAC;AAE5C,eAAcA,eAAa,KAAK,UAAU,MAAM,MAAM,EAAE,GAAG,MAAM,QAAQ;;AAG3E,SAAS,YAAY,SAAwC;CAC3D,MAAM,UAAU,eAAe;CAC/B,MAAM,SAAS;EAAE,GAAG;EAAS,GAAG;EAAS;AAEzC,KAAI,QAAQ,YAAY,OAAO,QAAQ,aAAa,YAAY,QAAQ,aAAa,KACnF,QAAO,WAAW;EAAE,GAAI,QAAQ;EAAqB,GAAI,QAAQ;EAAqB;AAExF,gBAAe,OAAO;;AAOxB,SAAS,YAAqB;AAC5B,KAAI;AAEF,SADe,UAAU,UAAU,CAAC,YAAY,EAAE,EAAE,OAAO,QAAQ,CAAC,CACtD,WAAW;SACnB;AACN,SAAO;;;AAIX,SAAS,eAAuB;CAI9B,MAAM,aAAa;EACjB,KAAK,QAAQ,KAAK,EAAE,SAAS;EAC7B,KAAK,SAAS,EAAE,OAAO,MAAM,OAAO,SAAS;EAC7C,KAAK,KAAK,OAAO,SAAS,OAAO,gBAAgB,UAAU,OAAO,SAAS;EAC5E;AACD,MAAK,MAAM,KAAK,WACd,KAAI,WAAW,KAAK,GAAG,qBAAqB,CAAC,CAAE,QAAO;AAExD,QAAO,KAAK,QAAQ,KAAK,EAAE,SAAS;;AAGtC,SAASC,oBAA0B;CAEjC,MAAM,aAAa;EACjB,KAAK,QAAQ,KAAK,EAAE,YAAY;EAChC,KAAK,SAAS,EAAE,OAAO,MAAM,OAAO,YAAY;EAChD,KAAK,KAAK,OAAO,SAAS,OAAO,gBAAgB,UAAU,OAAO,YAAY;EAC/E;AACD,MAAK,MAAM,KAAK,WACd,KAAI,WAAW,KAAK,GAAG,wBAAwB,CAAC,CAAE,QAAO;AAE3D,QAAO,KAAK,QAAQ,KAAK,EAAE,YAAY;;AAGzC,eAAe,YAAY,IAAmD;CAC5E,MAAM,YAAY,cAAc;AAGhC,KAAI,CAAC,WAFe,KAAK,WAAW,qBAAqB,CAE7B,EAAE;AAC5B,UAAQ,IAAI,EAAE,KAAK,mCAAmC,YAAY,CAAC;AACnE,UAAQ,IAAI,EAAE,IAAI,qCAAqC,CAAC;AACxD,UAAQ,IAAI,EAAE,IAAI,2BAA2B,CAAC;AAC9C,SAAO;;AAGT,SAAQ,IAAI,EAAE,IAAI,wCAAwC,UAAU,KAAK,CAAC;AAE1E,KAAI;AAMF,MALe,UAAU,UAAU;GAAC;GAAW;GAAM;GAAK,EAAE;GAC1D,KAAK;GACL,OAAO;GACR,CAAC,CAES,WAAW,GAAG;AACvB,WAAQ,IAAI,EAAE,KAAK,sDAAsD,CAAC;AAC1E,WAAQ,IAAI,EAAE,IAAI,UAAU,UAAU,0BAA0B,CAAC;AACjE,UAAO;;AAGT,UAAQ,IAAI,EAAE,GAAG,gCAAgC,CAAC;AAClD,SAAO;UACA,GAAG;AACV,UAAQ,IAAI,EAAE,KAAK,mCAAmC,IAAI,CAAC;AAC3D,SAAO;;;AAIX,eAAe,uBAAuB,kBAA4C;AAEhF,KAAI;EACF,MAAM,EAAE,SAAS,OAAO,MAAM,OAAO;EACrC,MAAM,SAAS,IAAI,GAAG,OAAO,EAAE,kBAAkB,CAAC;AAClD,QAAM,OAAO,SAAS;AACtB,QAAM,OAAO,KAAK;AAClB,SAAO;SACD;AACN,SAAO;;;;;;AAWX,SAAS,cAAoB;AAC3B,SAAM;AACN,QAAK,MAAM,KAAK,KAAK,+CAA+C,CAAC;AACrE,QAAK,MAAM,KAAK,KAAK,8CAA8C,CAAC;AACpE,QAAK,MAAM,KAAK,KAAK,+CAA+C,CAAC;AACrE,SAAM;AACN,QACE,0EACD;AACD,QACE,2EACD;AACD,QACE,gFACD;AACD,SAAM;AACN,QAAK,EAAE,IAAI,kEAAkE,CAAC;AAC9E,QAAK,EAAE,IAAI,wCAAwC,CAAC;;;;;AAMtD,eAAe,YAAY,IAAmE;AAC5F,SAAQ,0BAA0B;AAClC,SAAM;AACN,QAAK,kDAAkD;AACvD,SAAM;AAeN,KAbe,MAAM,WAAW,IAAI,CAClC;EACE,OAAO;EACP,aACE;EACH,EACD;EACE,OAAO;EACP,aACE;EACH,CACF,CAAC,KAEa,GAAG;AAEhB,UAAM;AACN,UAAQ,IAAI,EAAE,GAAG,+CAA+C,CAAC;AACjE,SAAO,EAAE,gBAAgB,UAAU;;AAIrC,SAAM;AACN,QAAK,+EAA+E;AACpF,SAAM;AAEN,KAAI,WAAW,EAAE;AACf,UAAQ,IAAI,EAAE,GAAG,uBAAuB,CAAC;AACzC,UAAM;AAQN,MANkB,MAAM,YACtB,IACA,iEACA,KACD,EAEc;AACb,WAAM;AACN,SAAM,YAAY,GAAG;AACrB,WAAM;AAGN,WAAQ,IAAI,EAAE,IAAI,mDAAmD,CAAC;AACtE,SAAM,IAAI,SAAS,MAAM,WAAW,GAAG,IAAK,CAAC;GAE7C,MAAM,UAAU;AAChB,WAAQ,IAAI,EAAE,IAAI,2BAA2B,QAAQ,KAAK,CAAC;AAG3D,OADY,MAAM,uBAAuB,QAAQ,EACxC;AACP,YAAQ,IAAI,EAAE,GAAG,yBAAyB,CAAC;AAC3C,WAAO;KACL,gBAAgB;KAChB,UAAU,EAAE,kBAAkB,SAAS;KACxC;UACI;AACL,YAAQ,IAAI,EAAE,KAAK,+DAA+D,CAAC;AACnF,YAAQ,IAAI,EAAE,IAAI,+EAA+E,CAAC;AAClG,WAAO;KACL,gBAAgB;KAChB,UAAU,EAAE,kBAAkB,SAAS;KACxC;;;OAIL,SAAQ,IAAI,EAAE,IAAI,4DAA4D,CAAC;AAIjF,SAAM;AACN,QAAK,8CAA8C;AACnD,SAAM;AAQN,KANmB,MAAM,YACvB,IACA,gFACA,KACD,EAEe;EACd,MAAM,UAAU,MAAM,OACpB,IACA,MAAM,KAAK,wBAAwB,CACpC;AAED,MAAI,SAAS;AACX,WAAQ,IAAI,EAAE,IAAI,0BAA0B,CAAC;AAE7C,OADkB,MAAM,uBAAuB,QAAQ,CAErD,SAAQ,IAAI,EAAE,GAAG,yBAAyB,CAAC;OAE3C,SAAQ,IAAI,EAAE,KAAK,kEAAkE,CAAC;AAExF,UAAO;IACL,gBAAgB;IAChB,UAAU,EAAE,kBAAkB,SAAS;IACxC;;;CAKL,MAAM,OAAO,MAAM,OAAO,IAAI,MAAM,KAAK,uBAAuB,CAAC,IAAI;CACrE,MAAM,UAAU,MAAM,OAAO,IAAI,MAAM,KAAK,kBAAkB,CAAC,IAAI;CACnE,MAAM,WAAW,MAAM,OAAO,IAAI,MAAM,KAAK,qBAAqB,CAAC,IAAI;CAIvE,MAAM,UAAU,gBAHH,MAAM,OAAO,IAAI,MAAM,KAAK,iBAAiB,CAAC,IAAI,MAG1B,GAFpB,MAAM,OAAO,IAAI,MAAM,KAAK,qBAAqB,CAAC,IAAI,MAEtB,GAAG,KAAK,GAAG,QAAQ,GAAG;AAEvE,SAAQ,IAAI,EAAE,IAAI,wBAAwB,UAAU,CAAC;AACrD,SAAQ,IAAI,EAAE,IAAI,0BAA0B,CAAC;AAG7C,KADkB,MAAM,uBAAuB,QAAQ,CAErD,SAAQ,IAAI,EAAE,GAAG,yBAAyB,CAAC;KAE3C,SAAQ,IAAI,EAAE,KAAK,kEAAkE,CAAC;AAGxF,QAAO;EACL,gBAAgB;EAChB,UAAU,EAAE,kBAAkB,SAAS;EACxC;;;;;AAMH,eAAe,cAAc,IAAmE;AAC9F,SAAQ,0BAA0B;AAClC,SAAM;AACN,QACE,4EACD;AACD,QACE,yDACD;AACD,SAAM;CAEN,MAAM,SAAS,MAAM,WAAW,IAAI;EAClC;GACE,OAAO;GACP,aACE;GAEH;EACD;GACE,OAAO;GACP,aACE;GAEH;EACD;GACE,OAAO;GACP,aACE;GAEH;EACD;GACE,OAAO;GACP,aACE;GAEH;EACF,CAAC;CASF,MAAM,gBAPwC;EAC5C,GAAG;EACH,GAAG;EACH,GAAG;EACH,GAAG;EACJ,CAE4B;AAE7B,SAAM;AACN,KAAI,eAAe;AACjB,UAAQ,IAAI,EAAE,GAAG,mBAAmB,gBAAgB,CAAC;AACrD,UAAQ,IAAI,EAAE,IAAI,qEAAqE,CAAC;QACnF;AACL,UAAQ,IAAI,EAAE,GAAG,uDAAuD,CAAC;AACzE,UAAQ,IAAI,EAAE,IAAI,kEAAkE,CAAC;;AAGvF,QAAO,EACL,gBAAgB,iBAAiB,QAClC;;;;;AAMH,eAAe,aAAa,IAAmD;AAC7E,SAAQ,0CAA0C;AAClD,SAAM;AACN,QACE,sEACD;AACD,QACE,yEACD;AACD,SAAM;CAEN,MAAM,YAAY,KAAK,SAAS,EAAE,UAAU;CAC5C,MAAM,WAAW,KAAK,WAAW,YAAY;CAC7C,MAAM,aAAa,KAAK,YAAY,iBAAiB;CACrD,MAAM,eAAeA,mBAAiB;CACtC,MAAM,eAAe,KAAK,cAAc,wBAAwB;AAEhE,KAAI,CAAC,WAAW,aAAa,EAAE;AAC7B,UAAQ,IAAI,EAAE,KAAK,yBAAyB,aAAa,CAAC;AAC1D,UAAQ,IAAI,EAAE,IAAI,6EAA6E,CAAC;AAChG,SAAO;;AAIT,KAAI,WAAW,SAAS,EAAE;EACxB,MAAM,UAAU,aAAa,UAAU,QAAQ;EAC/C,MAAM,cAAc,QAAQ,SAAS,yBAAyB;AAC9D,MAAI,YACF,SAAQ,IAAI,EAAE,IAAI,4CAA4C,CAAC;OAC1D;AACL,WAAQ,IAAI,EAAE,OAAO,0DAA0D,CAAC;AAChF,WAAQ,IAAI,EAAE,IAAI,iDAAiD,CAAC;;AAEtE,UAAM;AAQN,MAAI,CANc,MAAM,YACtB,IACA,4DACA,YACD,EAEe;AACd,WAAQ,IAAI,EAAE,IAAI,0CAA0C,CAAC;AAC7D,UAAO;;AAIT,MAAI,CAAC,aAAa;GAChB,MAAM,aAAa,WAAW;AAC9B,iBAAc,YAAY,SAAS,QAAQ;AAC3C,WAAQ,IAAI,EAAE,GAAG,mCAAmC,aAAa,CAAC;;;CAKtE,IAAI,WAAW,aAAa,cAAc,QAAQ;AAGlD,YAAW,SAAS,QAAQ,eAAe,SAAS,CAAC;AAGrD,KAAI,CAAC,WAAW,UAAU,CACxB,WAAU,WAAW,EAAE,WAAW,MAAM,CAAC;AAE3C,eAAc,UAAU,UAAU,QAAQ;AAE1C,SAAM;AACN,SAAQ,IAAI,EAAE,GAAG,mDAAmD,CAAC;AAGrE,KAAI,CAAC,WAAW,WAAW,EAAE;AAC3B,UAAM;AACN,SAAK,wEAAwE;AAC7E,SAAK,oCAAoC;AACzC,UAAM;AACN,UAAQ,IAAI,MAAM,KAAK,UAAU,aAAa,0BAA0B,aAAa,CAAC;AACtF,UAAM;AAQN,MANoB,MAAM,YACxB,IACA,wCACA,KACD,EAEgB;GACf,MAAM,cAAc,KAAK,cAAc,yBAAyB;AAChE,OAAI,WAAW,YAAY,EAAE;AAC3B,QAAI,CAAC,WAAW,WAAW,CACzB,WAAU,YAAY,EAAE,WAAW,MAAM,CAAC;AAE5C,kBAAc,YAAY,aAAa,aAAa,QAAQ,EAAE,QAAQ;AACtE,YAAQ,IAAI,EAAE,GAAG,WAAW,aAAa,CAAC;AAC1C,YAAQ,IAAI,EAAE,IAAI,0EAA0E,CAAC;SAE7F,SAAQ,IAAI,EAAE,KAAK,0DAA0D,CAAC;;OAIlF,SAAQ,IAAI,EAAE,IAAI,6BAA6B,WAAW,CAAC;AAG7D,QAAO;;;;;AAMT,eAAe,gBAAgB,IAAgD;AAC7E,SAAQ,+BAA+B;AACvC,SAAM;AACN,QACE,6EACD;AACD,QACE,4EACD;AACD,QACE,0DACD;AACD,SAAM;CAEN,MAAM,WAAW;EACf,KAAK,SAAS,EAAE,WAAW;EAC3B,KAAK,SAAS,EAAE,YAAY;EAC5B,KAAK,SAAS,EAAE,MAAM;EACvB,CAAC,OAAO,WAAW;AAEpB,KAAI,SAAS,SAAS,GAAG;AACvB,SAAK,4CAA4C;AACjD,OAAK,MAAM,KAAK,SACd,SAAQ,IAAI,MAAM,IAAI,OAAO,IAAI,CAAC;AAEpC,UAAM;;CAGR,MAAM,UAAU,MAAM,YACpB,IACA,gEACA,MACD;AAED,KAAI,SAAS;AACX,UAAM;AACN,UAAQ,IAAI,EAAE,IAAI,kDAAkD,CAAC;QAChE;AACL,UAAQ,IAAI,EAAE,IAAI,kDAAkD,CAAC;AACrE,UAAQ,IAAI,EAAE,IAAI,8CAA8C,CAAC;;AAInE,CAAC,gBAA2C,WAAW;;;;;AAMzD,eAAe,iBAAiB,IAAgD;AAC9E,SAAQ,wBAAwB;AAChC,SAAM;AACN,QACE,yEACD;AACD,QACE,yEACD;AACD,SAAM;AAIN,KAFkB,gBAA2C,SAS3D,KANoB,MAAM,YACxB,IACA,2DACA,KACD,EAEgB;AACf,UAAM;AACN,UAAQ,IAAI,EAAE,IAAI,uBAAuB,CAAC;AAE1C,MAAI;AAMF,OALe,UAAU,OAAO;IAAC;IAAU;IAAS;IAAe,EAAE;IACnE,OAAO;IACP,SAAS;IACV,CAAC,CAES,WAAW,EACpB,SAAQ,IAAI,EAAE,GAAG,gCAAgC,CAAC;OAElD,SAAQ,IAAI,EAAE,KAAK,yDAAyD,CAAC;UAEzE;AACN,WAAQ,IAAI,EAAE,KAAK,yDAAyD,CAAC;;AAG/E,UAAM;AACN,UAAQ,IAAI,EAAE,IAAI,gDAAgD,CAAC;AAEnE,MAAI;AAMF,OALe,UAAU,OAAO,CAAC,YAAY,OAAO,EAAE;IACpD,OAAO;IACP,SAAS;IACV,CAAC,CAES,WAAW,EACpB,SAAQ,IAAI,EAAE,KAAK,sEAAsE,CAAC;UAEtF;AACN,WAAQ,IAAI,EAAE,KAAK,+DAA+D,CAAC;;QAEhF;AACL,UAAQ,IAAI,EAAE,IAAI,6CAA6C,CAAC;AAChE,UAAQ,IAAI,EAAE,IAAI,2CAA2C,CAAC;;MAE3D;AACL,UAAQ,IAAI,EAAE,IAAI,mDAAmD,CAAC;AACtE,UAAQ,IAAI,EAAE,IAAI,iDAAiD,CAAC;AACpE,UAAQ,IAAI,EAAE,IAAI,0CAA0C,CAAC;;;;;;AAOjE,SAAS,YAAY,eAAwC,mBAAkC;AAC7F,SAAQ,iBAAiB;AACzB,SAAM;AACN,SAAQ,IAAI,EAAE,GAAG,kCAAkC,CAAC;AACpD,SAAM;CAGN,MAAM,UAAU,cAAc;CAC9B,MAAM,QAAQ,cAAc;AAE5B,QAAK,MAAM,KAAK,6BAA6B,GAAG,MAAM,IAAID,cAAY,CAAC;AACvE,SAAM;AACN,SAAQ,IAAI,MAAM,IAAI,uBAAuB,GAAG,MAAM,KAAK,WAAW,SAAS,CAAC;AAChF,SAAQ,IACN,MAAM,IAAI,uBAAuB,GACjC,MAAM,KAAK,SAAS,UAAU,SAAS,QAAQ,+BAA+B,CAC/E;AACD,SAAQ,IACN,MAAM,IAAI,uBAAuB,GACjC,MAAM,KAAK,oBAAoB,oCAAoC,cAAc,CAClF;AACD,SAAM;AAEN,QAAK,MAAM,KAAK,gBAAgB,CAAC;AACjC,SAAM;AACN,SAAQ,IAAI,MAAM,IAAI,2BAA2B,CAAC;AAClD,SAAQ,IAAI,MAAM,KAAK,qCAAqC,CAAC;AAC7D,SAAM;AACN,SAAQ,IAAI,MAAM,IAAI,yBAAyB,CAAC;AAChD,SAAQ,IAAI,MAAM,KAAK,6BAA6B,CAAC;AACrD,SAAM;AACN,SAAQ,IAAI,MAAM,IAAI,8BAA8B,CAAC;AACrD,SAAQ,IAAI,MAAM,KAAK,uCAAuC,CAAC;AAC/D,SAAM;AACN,KAAI,SAAS,UAAU,QAAQ;AAC7B,UAAQ,IAAI,MAAM,IAAI,gDAAgD,CAAC;AACvE,UAAQ,IAAI,MAAM,KAAK,uBAAuB,CAAC;AAC/C,UAAM;AACN,UAAQ,IAAI,MAAM,IAAI,wBAAwB,CAAC;AAC/C,UAAQ,IAAI,MAAM,KAAK,uDAAuD,CAAC;AAC/E,UAAM;;AAER,SAAQ,IAAI,MAAM,IAAI,oCAAoC,CAAC;AAC3D,SAAQ,IAAI,MAAM,KAAK,uBAAuB,CAAC;AAC/C,SAAM;AACN,SAAQ,IAAI,MAAM,IAAI,0BAA0B,CAAC;AACjD,SAAQ,IAAI,MAAM,KAAK,iBAAiB,CAAC;AACzC,SAAM;;AAOR,eAAe,WAA0B;CACvC,MAAM,KAAK,UAAU;AAErB,KAAI;AAEF,MAAI,WAAWA,cAAY,EAAE;GAC3B,MAAM,UAAUE,cAAY;AAC5B,WAAM;AACN,WAAQ,IACN,MAAM,OAAO,qCAAqC,GAClD,MAAM,IAAI,uDAAuD,CAClE;AACD,WAAQ,IAAI,MAAM,IAAI,aAAaF,gBAAc,CAAC;AAClD,WAAQ,IAAI,MAAM,IAAI,sBAAsB,QAAQ,iBAAiB,CAAC;AACtE,WAAM;AAQN,OAAI,CANY,MAAM,YACpB,IACA,sCACA,KACD,EAEa;AACZ,OAAG,OAAO;AACV,WAAK,EAAE,IAAI,qBAAqB,CAAC;AACjC,YAAM;AACN;;;AAKJ,eAAa;AACb,UAAM;AACN,QAAM,OAAO,IAAI,MAAM,IAAI,kCAAkC,CAAC;AAG9D,UAAQ,0BAA0B;EAClC,MAAM,gBAAgB,MAAM,YAAY,GAAG;EAG3C,MAAM,kBAAkB,MAAM,cAAc,GAAG;EAG/C,MAAM,oBAAoB,MAAM,aAAa,GAAG;AAGhD,QAAM,gBAAgB,GAAG;EAGzB,MAAM,aAAa;GAAE,GAAG;GAAe,GAAG;GAAiB;AAC3D,cAAY,WAAW;AAEvB,UAAM;AACN,UAAQ,IAAI,EAAE,GAAG,uBAAuB,CAAC;AAGzC,QAAM,iBAAiB,GAAG;AAG1B,cAAY,YAAY,kBAAkB;WAElC;AACR,KAAG,OAAO;;;AAQd,SAAgB,qBAAqB,SAAwB;AAC3D,SACG,QAAQ,QAAQ,CAChB,YACC,uFACD,CACA,OAAO,YAAY;AAClB,QAAM,UAAU;GAChB;;;;;;;;;AClvBN,SAAS,aAAa,UAAiC;CACrD,MAAM,YAAY,KAAK,UAAU,QAAQ;AACzC,KAAI,WAAW,UAAU,CAAE,QAAO;CAClC,MAAM,MAAM,KAAK,UAAU,WAAW,QAAQ;AAC9C,KAAI,WAAW,IAAI,CAAE,QAAO;AAC5B,QAAO;;;;;;;AAQT,SAAS,mBACP,4BACA,UACe;AACf,KAAI,CAAC,2BAA4B,QAAO;AACxC,KAAI,CAAC,WAAW,2BAA2B,CAAE,QAAO;AAEpD,KAAI,YAAY,+BAA+B,SAAU,QAAO;AAChE,QAAO;;;;;AAMT,SAAS,YAAY,GAAoB;AACvC,KAAI;AACF,YAAU,EAAE;AACZ,SAAO;SACD;AACN,SAAO;;;;;;AAOX,SAAS,WAAW,MAAc,OAA4B;AAC5D,KAAI,CAAC,MAAM,IAAI,KAAK,CAAE,QAAO;CAC7B,IAAI,IAAI;AACR,QAAO,MAAM,IAAI,GAAG,KAAK,GAAG,IAAI,CAAE;AAClC,QAAO,GAAG,KAAK,GAAG;;;;;;AAOpB,SAAS,oBAAoB,KAAqB;CAChD,IAAI,UAAU;AACd,KAAI,CAAC,WAAW,IAAI,CAAE,QAAO;AAC7B,MAAK,MAAM,SAAS,YAAY,IAAI,EAAE;EACpC,MAAM,OAAO,KAAK,KAAK,MAAM;AAC7B,MAAI;AAEF,OADa,UAAU,KAAK,CACnB,gBAAgB,EAEvB;QAAI,CAAC,WADU,aAAa,KAAK,CACV,EAAE;AACvB,gBAAW,KAAK;AAChB;;;UAGE;;AAIV,QAAO;;;;;;;;;;;AAYT,SAAS,iBACP,UACA,QACA,QACA,OACS;AACT,KAAI,YAAY,SAAS,CACvB,KAAI;AAEF,MADa,UAAU,SAAS,CACvB,gBAAgB,EAAE;AAEzB,OADgB,aAAa,SAAS,KACtB,OACd,QAAO;AAET,cAAW,SAAS;SACf;AAEL,UAAO,KAAK,GAAG,MAAM,8CAA8C;AACnE,UAAO;;UAEF,GAAG;AACV,SAAO,KAAK,GAAG,MAAM,mCAAmC,IAAI;AAC5D,SAAO;;AAIX,KAAI;AACF,cAAY,QAAQ,SAAS;AAC7B,SAAO;UACA,GAAG;AACV,SAAO,KAAK,GAAG,MAAM,8BAA8B,IAAI;AACvD,SAAO;;;;;;;;;;;;;;AAeX,SAAS,oBACP,UACA,QACA,MACS;AACT,KAAI,CAAC,YAAY,SAAS,CACxB,QAAO;AAGT,KAAI;EACF,MAAM,OAAO,UAAU,SAAS;AAEhC,MAAI,KAAK,aAAa,CACpB,QAAO;AAGT,MAAI,KAAK,gBAAgB,EAAE;AACzB,cAAW,SAAS;AACpB,UAAO;;AAIT,SAAO,KAAK,GAAG,KAAK,uDAAuD;AAC3E,SAAO;UACA,GAAG;AACV,SAAO,KAAK,GAAG,KAAK,6BAA6B,IAAI;AACrD,SAAO;;;;;;;;;;;;;;;AAmCX,SAAgB,UAAU,WAAmB,IAAyB;CACpE,MAAM,QAAmB;EAAE,SAAS;EAAG,SAAS;EAAG,SAAS;EAAG,SAAS;EAAG,QAAQ,EAAE;EAAE;AAGvF,WAAU,WAAW,EAAE,WAAW,MAAM,CAAC;AAIzC,OAAM,WAAW,oBAAoB,UAAU;CAE/C,MAAM,WAAW,GACd,QACC;;sCAGD,CACA,KAAK;CAER,MAAM,6BAAa,IAAI,KAAa;AAEpC,MAAK,MAAM,WAAW,SACpB,KAAI,QAAQ,WAAW,UAAU;EAC/B,MAAM,WAAW,aAAa,QAAQ,UAAU;EAChD,MAAM,iBAAiB,mBAAmB,QAAQ,kBAAkB,SAAS;AAG7E,MAAI,CAAC,YAAY,CAAC,eAChB;EAGF,MAAM,OAAO,WAAW,QAAQ,MAAM,WAAW;AACjD,aAAW,IAAI,KAAK;EACpB,MAAM,WAAW,KAAK,WAAW,KAAK;AAGtC,MAAI,CAAC,oBAAoB,UAAU,MAAM,QAAQ,KAAK,CACpD;AAIF,MAAI;AACF,aAAU,UAAU,EAAE,WAAW,MAAM,CAAC;WACjC,GAAG;AACV,SAAM,OAAO,KAAK,GAAG,KAAK,yCAAyC,IAAI;AACvE;;AAIF,MAAI,SAGF,KADgB,iBADE,KAAK,UAAU,QAAQ,EACG,UAAU,MAAM,QAAQ,GAAG,KAAK,QAAQ,CAElF,OAAM;MAEN,OAAM;AAKV,MAAI,eAQF,KANgB,iBADK,KAAK,UAAU,WAAW,EAG7C,gBACA,MAAM,QACN,GAAG,KAAK,WACT,CAEC,OAAM;MAEN,OAAM;AAKV,MAAI;AACF,MAAG,QAAQ,qEAAqE,CAAC,IAC/E,UACA,KAAK,KAAK,EACV,QAAQ,GACT;UACK;YAGC,QAAQ,WAAW,YAAY;EAExC,MAAM,aAAa,KAAK,WAAW,WAAW;AAC9C,YAAU,YAAY,EAAE,WAAW,MAAM,CAAC;EAC1C,MAAM,WAAW,KAAK,YAAY,GAAG,QAAQ,KAAK,KAAK;AACvD,MAAI,CAAC,WAAW,SAAS,EAAE;GACzB,MAAM,UAAU;IACd,KAAK,QAAQ;IACb;IACA;IACA;IACA,eAAe,QAAQ;IACvB,eAAe,QAAQ;IACvB;IACD,CAAC,KAAK,KAAK;AACZ,OAAI;AACF,kBAAc,UAAU,SAAS,QAAQ;AACzC,UAAM;YACC,GAAG;AACV,UAAM,OAAO,KAAK,GAAG,QAAQ,KAAK,mBAAmB,IAAI;;;;AAMjE,QAAO;;;;;;AAOT,SAAgB,cAAc,WAAmB,IAAoB;AACnE,WAAU,WAAW,EAAE,WAAW,MAAM,CAAC;CAEzC,MAAM,OAAO,GACV,QACC;;;;;iDAMD,CACA,KAAK;CAER,MAAM,oBAAoB,GAAG,QAC3B;;;sBAID;CAED,MAAM,SAAS,KAAK,QAAQ,MAAM,EAAE,WAAW,SAAS;CACxD,MAAM,WAAW,KAAK,QAAQ,MAAM,EAAE,WAAW,SAAS;CAE1D,MAAM,QAAkB;EACtB;EACA;EACA,2DAA0C,IAAI,MAAM,EAAC,aAAa;EAClE;EACA;EACA;EACA;EACA;EACD;AAED,MAAK,MAAM,OAAO,QAAQ;EACxB,MAAM,OAAQ,kBAAkB,IAAI,IAAI,GAAG,CAAc,KAAK,MAAM,KAAK,EAAE,KAAK,IAAI,CAAC,KAAK,IAAI;EAC9F,MAAM,aAAa,QAAQ,IAAI,YAAY;EAG3C,MAAM,WAAW,aAAa,IAAI,UAAU;EAC5C,MAAM,iBAAiB,mBAAmB,IAAI,kBAAkB,SAAS;EACzE,MAAM,UAAoB,EAAE;AAC5B,MAAI,SAAU,SAAQ,KAAK,QAAQ;AACnC,MAAI,eAAgB,SAAQ,KAAK,WAAW;EAC5C,MAAM,eAAe,QAAQ,SAAS,IAAI,QAAQ,KAAK,KAAK,GAAG;AAE/D,QAAM,KACJ,OAAO,IAAI,KAAK,IAAI,IAAI,aAAa,OAAO,IAAI,cAAc,KAAK,WAAW,KAAK,aAAa,KAAK,QAAQ,IAAI,IAClH;;AAGH,KAAI,SAAS,SAAS,GAAG;AACvB,QAAM,KACJ,IACA,wBACA,IACA,iCACA,gCACD;AACD,OAAK,MAAM,OAAO,UAAU;GAC1B,MAAM,OAAQ,kBAAkB,IAAI,IAAI,GAAG,CAAc,KAAK,MAAM,KAAK,EAAE,KAAK,IAAI,CAAC,KAAK,IAAI;AAC9F,SAAM,KACJ,MAAM,IAAI,aAAa,aAAa,IAAI,KAAK,SAAS,IAAI,cAAc,KAAK,QAAQ,IAAI,IAC1F;;;AAIL,OAAM,KACJ,IACA,OACA,IAAI,KAAK,OAAO,oBAAoB,OAAO,OAAO,WAAW,SAAS,OAAO,YAC9E;AAED,eAAc,KAAK,WAAW,YAAY,EAAE,MAAM,KAAK,KAAK,GAAG,MAAM,QAAQ;;;;;;AAO/E,SAAgB,mBAAmB,WAAmB,IAAsB;CAC1E,MAAM,YAAY,KAAK,WAAW,UAAU;AAC5C,WAAU,WAAW,EAAE,WAAW,MAAM,CAAC;CAEzC,MAAM,UAAU,GACb,QAAQ,0CAA0C,CAClD,KAAK;CAER,MAAM,oBAAoB,GAAG,QAC3B;;;;;;+CAOD;CAED,IAAI,UAAU;AACd,MAAK,MAAM,OAAO,SAAS;EACzB,MAAM,WAAW,kBAAkB,IAAI,IAAI,GAAG;AAC9C,MAAI,CAAC,SAAS,OAAQ;EAEtB,MAAM,QAAkB;GACtB,YAAY,IAAI;GAChB;GACA,2DAA0C,IAAI,MAAM,EAAC,aAAa;GAClE;GACA;GACA;GACA;GACA;GACD;AAED,OAAK,MAAM,KAAK,UAAU;GACxB,MAAM,OACJ,EAAE,WAAW,WACT,KAAK,EAAE,KAAK,IAAI,EAAE,aAAa,MAC/B,IAAI,EAAE,aAAa,gBAAgB,EAAE,KAAK;AAChD,SAAM,KAAK,KAAK,KAAK,KAAK,EAAE,OAAO,KAAK,EAAE,cAAc,KAAK,QAAQ,EAAE,YAAY,CAAC,IAAI;;AAG1F,QAAM,KAAK,IAAI,OAAO,IAAI,SAAS,OAAO,uBAAuB,IAAI,KAAK,KAAK;AAE/E,gBAAc,KAAK,WAAW,GAAG,IAAI,KAAK,KAAK,EAAE,MAAM,KAAK,KAAK,GAAG,MAAM,QAAQ;AAClF;;AAGF,QAAO;;;;;AAMT,SAAgB,mBAA2B;AACzC,QAAO,KAAK,SAAS,EAAE,QAAQ,iBAAiB;;AAoBlD,MAAM,sBAAsB;;AAG5B,SAAS,eAAe,MAAsB;AAC5C,QAAO,IAAI,KAAK;;;;;;AAOlB,SAAS,aAAa,KAAuB;CAC3C,MAAM,UAAoB,EAAE;AAC5B,KAAI,CAAC,WAAW,IAAI,CAAE,QAAO;CAE7B,IAAI;AACJ,KAAI;AACF,YAAU,YAAY,IAAI;SACpB;AACN,SAAO;;AAGT,MAAK,MAAM,SAAS,SAAS;AAC3B,MAAI,MAAM,WAAW,IAAI,CAAE;AAE3B,MAAI,UAAU,gBAAgB,sBAAsB,KAAK,MAAM,CAAE;EACjE,MAAM,OAAO,KAAK,KAAK,MAAM;EAC7B,IAAI;AACJ,MAAI;AAEF,UAAO,UAAU,KAAK;UAChB;AACN;;AAEF,MAAI,KAAK,aAAa,EAEpB;OAAI,UAAU,KAAK,MAAM,EAAE;IAEzB,IAAI;AACJ,QAAI;AACF,oBAAe,YAAY,KAAK;YAC1B;AACN;;AAEF,SAAK,MAAM,SAAS,cAAc;KAChC,MAAM,YAAY,KAAK,MAAM,MAAM;AACnC,SAAI,UAAU,KAAK,MAAM,IAAI,WAAW,UAAU,EAAE;MAClD,IAAI;AACJ,UAAI;AACF,oBAAa,YAAY,UAAU;cAC7B;AACN;;AAEF,WAAK,MAAM,KAAK,WACd,KAAI,EAAE,SAAS,MAAM,IAAI,CAAC,EAAE,WAAW,IAAI,CACzC,SAAQ,KAAK,KAAK,WAAW,EAAE,CAAC;;;;aAMjC,MAAM,SAAS,MAAM,CAC9B,SAAQ,KAAK,KAAK;;AAGtB,QAAO;;;;;;AAOT,SAAS,iBAAiB,UAA0B;CAClD,MAAM,YAAY,SAAS,MAAM,uBAAuB;AACxD,KAAI,UAAW,QAAO,GAAG,UAAU,GAAG,GAAG,UAAU;CAGnD,MAAM,aADW,SAAS,MAAM,IAAI,CAAC,KAAK,IAAI,IACnB,MAAM,iCAAiC;AAClE,KAAI,UAAW,QAAO,GAAG,UAAU,GAAG,GAAG,UAAU;AAEnD,QAAO;;;;;;;AAQT,SAAS,kBAAkB,UAA2B;CACpD,IAAI;AACJ,KAAI;AACF,YAAU,aAAa,UAAU,QAAQ;SACnC;AACN,SAAO;;CAMT,MAAM,UAAU,QAAQ,QAHL,mCAGyB,GAAG,CAAC,QAD7B,6CACiD,GAAG;AACvE,KAAI,YAAY,QAAS,QAAO;AAChC,KAAI;AACF,gBAAc,UAAU,SAAS,QAAQ;AACzC,SAAO;SACD;AACN,SAAO;;;;;;;;AASX,SAAS,wBACP,UACA,MACA,aACS;CACT,IAAI;AACJ,KAAI;AACF,YAAU,aAAa,UAAU,QAAQ;SACnC;AACN,SAAO;;CAET,MAAM,aAAa,eAAe,KAAK,CAAC,QAAQ,SAAS,GAAG;CAC5D,MAAM,WAAW,KAAK,WAAW;AACjC,KAAI,QAAQ,SAAS,SAAS,CAAE,QAAO;CAEvC,MAAM,SAAS,YAAY,WAAW,KAAK,YAAY;AACvD,KAAI;AACF,gBAAc,UAAU,UAAU,QAAQ,QAAQ;AAClD,SAAO;SACD;AACN,SAAO;;;;;;;;;;;;;;;;;;;AAoBX,SAAgB,oBACd,WACA,IACA,YAAY,GACJ;AACR,KAAI,CAAC,WAAW,UAAU,CAAE,QAAO;CAEnC,MAAM,WAAW,GACd,QACC;;;0BAID,CACA,KAAK;CAER,IAAI,UAAU;AAEd,MAAK,MAAM,WAAW,UAAU;EAC9B,MAAM,WAAW,KAAK,WAAW,QAAQ,KAAK;AAC9C,MAAI,CAAC,WAAW,SAAS,CAAE;EAG3B,MAAM,eAAe,KAAK,UAAU,aAAa;AACjD,MAAI,WAAW,aAAa,CAC1B,KAAI;AACF,cAAW,aAAa;UAClB;EAMV,MAAM,eAA8B,EAAE;AAItC,OAAK,MAAM,WADM,CAAC,SAAS,WAAW,EACN;GAC9B,MAAM,WAAW,KAAK,UAAU,QAAQ;AACxC,OAAI,CAAC,WAAW,SAAS,CAAE;GAG3B,IAAI;AACJ,OAAI;IACF,MAAM,OAAO,UAAU,SAAS;AAChC,QAAI,KAAK,gBAAgB,CACvB,WAAU,aAAa,SAAS;aACvB,KAAK,aAAa,CAC3B,WAAU;QAEV;WAEI;AACN;;GAGF,MAAM,QAAQ,aAAa,QAAQ;AACnC,QAAK,MAAM,WAAW,OAAO;IAC3B,MAAM,WAAW,QAAQ,MAAM,IAAI,CAAC,KAAK,IAAI;AAC7C,QAAI,CAAC,oBAAoB,KAAK,SAAS,CAAE;IAKzC,MAAM,eAAe,GAAG,QAAQ,GADZ,SAAS,SAAS,QAAQ;IAG9C,MAAM,iBAAiB,aAAa,QAAQ,SAAS,GAAG;AAExD,iBAAa,KAAK;KAChB;KACA;KACA;KACA,WAAW,iBAAiB,QAAQ;KACpC,UAAU,SAAS,QAAQ,SAAS,GAAG;KACxC,CAAC;;;AAIN,MAAI,aAAa,SAAS,UAAW;AAGrC,eAAa,MAAM,GAAG,MAAM;AAC1B,OAAI,EAAE,cAAc,EAAE,UAAW,QAAO,EAAE,UAAU,cAAc,EAAE,UAAU;AAC9E,UAAO,EAAE,SAAS,cAAc,EAAE,SAAS;IAC3C;EAGF,MAAM,0BAAU,IAAI,KAA4B;AAChD,OAAK,MAAM,MAAM,cAAc;AAC7B,OAAI,CAAC,QAAQ,IAAI,GAAG,UAAU,CAAE,SAAQ,IAAI,GAAG,WAAW,EAAE,CAAC;AAC7D,WAAQ,IAAI,GAAG,UAAU,CAAE,KAAK,GAAG;;EAIrC,MAAM,YAAY,aAAa,IAAI,SAAS,MAAM,oBAAoB,GAAG,MAAM;EAC/E,MAAM,WACJ,aAAa,aAAa,SAAS,IAAI,SAAS,MAAM,oBAAoB,GAAG,MAAM;EAGrF,MAAM,QAAkB;GACtB,cAAc,QAAQ;GACtB;GACA,2DAA0C,IAAI,MAAM,EAAC,aAAa;GAClE;GACA;GACA;GACA,mBAAmB,aAAa;GAChC,qBAAqB,UAAU,MAAM;GACrC,gBAAgB,QAAQ,KAAK;GAC7B;GACA;GACA;GACD;AAED,OAAK,MAAM,CAAC,IAAI,UAAU,SAAS;AACjC,SAAM,KAAK,OAAO,MAAM,GAAG;AAC3B,QAAK,MAAM,MAAM,MACf,OAAM,KAAK,OAAO,GAAG,eAAe,GAAG,GAAG,SAAS,IAAI;AAEzD,SAAM,KAAK,GAAG;;AAGhB,QAAM,KAAK,aAAa,IAAI,IAAI,QAAQ,KAAK,YAAY,GAAG;EAE5D,MAAM,aAAa,KAAK,UAAU,eAAe,QAAQ,KAAK,CAAC;AAC/D,MAAI;AACF,iBAAc,YAAY,MAAM,KAAK,KAAK,EAAE,QAAQ;AACpD;UACM;AACN;;AAIF,OAAK,MAAM,MAAM,cAAc;AAC7B,qBAAkB,GAAG,QAAQ;AAC7B,2BAAwB,GAAG,SAAS,QAAQ,MAAM,QAAQ,aAAa;;;AAI3E,QAAO;;;;;;;;;;;;;;;AAoBT,SAAgB,eACd,IACmE;CACnE,MAAM,UAAU;EAAE,cAAc;EAAG,eAAe;EAAG,QAAQ,EAAE;EAAc;CAE7E,MAAM,WAAW,GACd,QACC;;;0BAID,CACA,KAAK;AAER,MAAK,MAAM,WAAW,UAAU;EAE9B,MAAM,aAAuB,EAAE;EAE/B,MAAM,WAAW,aAAa,QAAQ,UAAU;AAChD,MAAI,SAAU,YAAW,KAAK,SAAS;AAEvC,MACE,QAAQ,oBACR,WAAW,QAAQ,iBAAiB,IACpC,QAAQ,qBAAqB,SAE7B,YAAW,KAAK,QAAQ,iBAAiB;AAG3C,OAAK,MAAM,OAAO,YAAY;GAC5B,MAAM,QAAQ,aAAa,IAAI;AAC/B,QAAK,MAAM,YAAY,OAAO;AAC5B,YAAQ;IACR,IAAI;AACJ,QAAI;AACF,eAAU,aAAa,UAAU,QAAQ;aAClC,GAAG;AACV,aAAQ,OAAO,KAAK,GAAG,SAAS,iBAAiB,IAAI;AACrD;;AAGF,QAAI,CAAC,QAAQ,SAAS,WAAW,CAAE;IAGnC,MAAM,UAAU,QAAQ,QACtB,oCACA,KACD;AAED,QAAI,YAAY,QAAS;IAGzB,MAAM,UAAU,QAAQ,QAAQ,wBAAwB,OAAO;AAE/D,QAAI;AACF,mBAAc,UAAU,SAAS,QAAQ;AACzC,aAAQ;aACD,GAAG;AACV,aAAQ,OAAO,KAAK,GAAG,SAAS,kBAAkB,IAAI;;;;;AAM9D,QAAO;;;;;;;;;AC31BT,SAAS,kBAAkB,KAAa,MAAuB;CAC7D,MAAM,WAAW,KAAK,KAAK,KAAK;AAChC,KAAI;AAEF,MAAI,CADS,UAAU,SAAS,CACtB,gBAAgB,CAAE,QAAO;AAEnC,SAAO,WADQ,aAAa,SAAS,CACZ;SACnB;AACN,SAAO;;;;;;AAOX,SAAS,iBAAiB,KAAa,MAAuB;AAC5D,KAAI;AACF,YAAU,KAAK,KAAK,KAAK,CAAC;AAC1B,SAAO;SACD;AACN,SAAO;;;;;;;;;;;;;AA6BX,SAAgB,YAAY,WAAmB,IAAiC;CAC9E,MAAM,SAA4B;EAChC;EACA,SAAS,EAAE;EACX,QAAQ,EAAE;EACV,UAAU,EAAE;EACZ,SAAS,EAAE;EACX,eAAe;EAChB;AAED,KAAI,CAAC,WAAW,UAAU,CACxB,QAAO;CAGT,MAAM,WAAW,GACd,QACC;8CAED,CACA,KAAK;AAER,QAAO,gBAAgB,SAAS;CAGhC,MAAM,iCAAiB,IAAI,KAAyB;AACpD,MAAK,MAAM,KAAK,SACd,gBAAe,IAAI,EAAE,MAAM,EAAE;CAI/B,MAAM,4BAAY,IAAI,KAAa;CACnC,IAAI,UAAoB,EAAE;AAC1B,KAAI;AACF,YAAU,YAAY,UAAU;SAC1B;AACN,SAAO;;AAGT,MAAK,MAAM,SAAS,SAAS;AAE3B,MAAI,MAAM,WAAW,IAAI,CAAE;EAE3B,MAAM,WAAW,KAAK,WAAW,MAAM;EACvC,IAAI;AACJ,MAAI;AACF,UAAO,UAAU,SAAS;UACpB;AACN;;EAGF,MAAM,UAAU,eAAe,IAAI,MAAM;AAEzC,MAAI,KAAK,aAAa,EAAE;AAEtB,OAAI,CAAC,SAAS;AAEZ,WAAO,SAAS,KAAK;KACnB,MAAM;KACN,UAAU;KACV,QAAQ;KACR,OAAO;KACP,OAAO;KACR,CAAC;AACF;;AAGF,aAAU,IAAI,MAAM;GAEpB,MAAM,UAAU,kBAAkB,UAAU,QAAQ;GACpD,MAAM,aAAa,kBAAkB,UAAU,WAAW;GAC1D,MAAM,cAAc,iBAAiB,UAAU,QAAQ;GACvD,MAAM,iBAAiB,iBAAiB,UAAU,WAAW;AAE7D,OAAI,WAAW,YAAY;IACzB,MAAM,QAAkB,EAAE;AAC1B,QAAI,QAAS,OAAM,KAAK,QAAQ;AAChC,QAAI,WAAY,OAAM,KAAK,WAAW;AACtC,WAAO,QAAQ,KAAK;KAClB,MAAM;KACN,UAAU;KACV,QAAQ;KACR,OAAO;KACP,OAAO,YAAY,MAAM,KAAK,KAAK;KACpC,CAAC;UACG;IAEL,MAAM,SAAmB,EAAE;AAC3B,QAAI,YAAa,QAAO,KAAK,QAAQ;AACrC,QAAI,eAAgB,QAAO,KAAK,WAAW;AAC3C,WAAO,OAAO,KAAK;KACjB,MAAM;KACN,UAAU;KACV,QAAQ;KACR,OAAO;KACP,OAAO,wBAAwB,OAAO,KAAK,KAAK,IAAI;KACrD,CAAC;;aAEK,KAAK,gBAAgB,EAAE;GAEhC,IAAI,SAAwB;AAC5B,OAAI;AACF,aAAS,aAAa,SAAS;WACzB;GAIR,MAAM,eAAe,WAAW,QAAQ,WAAW,OAAO;AAE1D,OAAI,CAAC,SAAS;AACZ,WAAO,SAAS,KAAK;KACnB,MAAM;KACN,UAAU;KACV;KACA,OAAO;KACP,OAAO;KACR,CAAC;AACF;;AAGF,aAAU,IAAI,MAAM;AAEpB,OAAI,aACF,QAAO,QAAQ,KAAK;IAClB,MAAM;IACN,UAAU;IACV;IACA,OAAO;IACP,OAAO;IACR,CAAC;OAEF,QAAO,OAAO,KAAK;IACjB,MAAM;IACN,UAAU;IACV;IACA,OAAO;IACP,OAAO,mBAAmB,UAAU;IACrC,CAAC;;;AAOR,MAAK,MAAM,WAAW,UAAU;AAC9B,MAAI,UAAU,IAAI,QAAQ,KAAK,CAAE;EAEjC,MAAM,iBAAiB,KAAK,QAAQ,WAAW,QAAQ;EACvD,MAAM,WAAW,KAAK,QAAQ,WAAW,WAAW,QAAQ;EAC5D,MAAM,kBAAkB,WAAW,eAAe,IAAI,WAAW,SAAS;EAC1E,MAAM,iBACJ,QAAQ,qBAAqB,QAAQ,WAAW,QAAQ,iBAAiB;AAE3E,MAAI,mBAAmB,gBAAgB;GACrC,MAAM,UAAoB,EAAE;AAC5B,OAAI,gBAAiB,SAAQ,KAAK,QAAQ;AAC1C,OAAI,eAAgB,SAAQ,KAAK,WAAW;AAC5C,UAAO,QAAQ,KAAK;IAClB,MAAM,QAAQ;IACd,UAAU,KAAK,WAAW,QAAQ,KAAK;IACvC,QAAQ;IACR,OAAO;IACP,OAAO,gBAAgB,QAAQ,KAAK,KAAK,CAAC;IAC3C,CAAC;;;AAIN,QAAO;;;;;;;;;ACtOT,SAAS,aAAa,UAA2B;AAC/C,KAAI,SAAU,QAAO,WAAW,SAAS;AAEzC,KAAI;EACF,MAAM,MAAM,aAAaG,eAAa,QAAQ;EAC9C,MAAM,MAAM,KAAK,MAAM,IAAI;AAC3B,MAAI,OAAO,IAAI,sBAAsB,YAAY,IAAI,kBACnD,QAAO,WAAW,IAAI,kBAAkB;SAEpC;AAGR,QAAO,kBAAkB;;;;;AAM3B,SAAS,cAAc,WAAyB;CAC9C,IAAI,MAA+B,EAAE;AACrC,KAAI;AACF,QAAM,KAAK,MAAM,aAAaA,eAAa,QAAQ,CAAC;SAC9C;AAGR,KAAI,oBAAoB;AACxB,WAAU,QAAQA,cAAY,EAAE,EAAE,WAAW,MAAM,CAAC;AACpD,eAAcA,eAAa,KAAK,UAAU,KAAK,MAAM,EAAE,GAAG,MAAM,QAAQ;;AAO1E,SAAS,QACP,IACA,MACM;CACN,MAAM,YAAY,aAAa,KAAK,MAAM;CAC1C,MAAM,IAAI,KAAK,SAAS;AAExB,KAAI,CAAC,GAAG;AACN,UAAQ,KAAK;AACb,UAAQ,IAAI,OAAO,sBAAsB,CAAC;AAC1C,UAAQ,IAAI,IAAI,YAAY,YAAY,CAAC;AACzC,UAAQ,KAAK;;AAIf,KAAI,CAAC,EAAG,SAAQ,OAAO,MAAM,wBAAwB;CACrD,MAAM,QAAQ,UAAU,WAAW,GAAG;AACtC,KAAI,CAAC,GAAG;AACN,UAAQ,OAAO,MACb,OAAO,GAAG,YAAY,CAAC,YAAY,MAAM,QAAQ,YAAY,MAAM,QAAQ,YAAY,MAAM,QAAQ,UAAU,MAAM,QAAQ,IAC9H;AACD,MAAI,MAAM,OAAO,OACf,MAAK,MAAM,KAAK,MAAM,OACpB,SAAQ,IAAI,KAAK,cAAc,IAAI,CAAC;;AAM1C,KAAI,CAAC,EAAG,SAAQ,OAAO,MAAM,wBAAwB;AACrD,eAAc,WAAW,GAAG;AAC5B,KAAI,CAAC,EAAG,SAAQ,IAAI,OAAO,GAAG,SAAS,CAAC,sCAAsC;AAG9E,KAAI,CAAC,EAAG,SAAQ,OAAO,MAAM,8BAA8B;CAC3D,MAAM,aAAa,mBAAmB,WAAW,GAAG;AACpD,KAAI,CAAC,EAAG,SAAQ,IAAI,OAAO,GAAG,UAAU,CAAC,MAAM,WAAW,kCAAkC;AAG5F,KAAI,CAAC,EAAG,SAAQ,OAAO,MAAM,2BAA2B;CACxD,MAAM,WAAW,eAAe,GAAG;AACnC,KAAI,CAAC,GAAG;AACN,UAAQ,IACN,OAAO,GAAG,QAAQ,CAAC,QAAQ,SAAS,cAAc,4BAA4B,SAAS,aAAa,aACrG;AACD,MAAI,SAAS,OAAO,OAClB,MAAK,MAAM,KAAK,SAAS,OACvB,SAAQ,IAAI,KAAK,cAAc,IAAI,CAAC;;AAM1C,KAAI,CAAC,EAAG,SAAQ,OAAO,MAAM,+BAA+B;CAC5D,MAAM,cAAc,oBAAoB,WAAW,GAAG;AACtD,KAAI,CAAC,EAAG,SAAQ,IAAI,OAAO,GAAG,WAAW,CAAC,KAAK,YAAY,kCAAkC;AAG7F,eAAc,UAAU;AAExB,KAAI,CAAC,GAAG;AACN,UAAQ,KAAK;AACb,UAAQ,IAAI,GAAG,2BAA2B,YAAY,CAAC;AACvD,UAAQ,KAAK;;;AAIjB,SAASC,YAAU,IAAc,MAAgC;CAC/D,MAAM,YAAY,aAAa,KAAK,MAAM;CAC1C,MAAM,SAAS,YAAY,WAAW,GAAG;AAEzC,SAAQ,KAAK;AACb,SAAQ,IAAI,OAAO,8BAA8B,CAAC;AAClD,SAAQ,IAAI,IAAI,YAAY,YAAY,CAAC;AACzC,SAAQ,KAAK;AAEb,KAAI,CAAC,WAAW,UAAU,EAAE;AAC1B,UAAQ,IAAI,KAAK,2DAA2D,CAAC;AAC7E,UAAQ,KAAK;AACb;;CAIF,MAAM,eAAe,OAAO,QAAQ;CACpC,MAAM,cAAc,OAAO,OAAO;CAClC,MAAM,gBAAgB,OAAO,SAAS;CACtC,MAAM,eAAe,OAAO,QAAQ;AAEpC,SAAQ,IACN,KAAK,MAAM,MAAM,WAAW,CAAC,GAAG,aAAa,KACxC,MAAM,IAAI,UAAU,CAAC,GAAG,YAAY,KACpC,MAAM,OAAO,YAAY,CAAC,GAAG,cAAc,KAC3C,MAAM,KAAK,WAAW,CAAC,GAAG,eAChC;AACD,SAAQ,KAAK;AAEb,KAAI,OAAO,QAAQ,UAAU,OAAO,QAAQ,UAAU,IAAI;AACxD,UAAQ,IAAI,KAAK,sBAAsB,CAAC;AACxC,OAAK,MAAM,KAAK,OAAO,QACrB,SAAQ,IAAI,OAAO,MAAM,MAAM,IAAI,CAAC,GAAG,KAAK,EAAE,KAAK,CAAC,IAAI,IAAI,QAAQ,EAAE,UAAU,IAAI,GAAG;AAEzF,UAAQ,KAAK;YACJ,OAAO,QAAQ,SAAS,IAAI;AACrC,UAAQ,IAAI,KAAK,KAAK,OAAO,QAAQ,OAAO,8BAA8B,CAAC;AAC3E,UAAQ,KAAK;;AAGf,KAAI,OAAO,OAAO,QAAQ;AACxB,UAAQ,IAAI,IAAI,sCAAsC,CAAC;AACvD,OAAK,MAAM,KAAK,OAAO,OACrB,SAAQ,IAAI,OAAO,MAAM,IAAI,IAAI,CAAC,GAAG,KAAK,EAAE,KAAK,CAAC,IAAI,IAAI,EAAE,MAAM,GAAG;AAEvE,UAAQ,IAAI,IAAI,2BAA2B,CAAC;AAC5C,UAAQ,KAAK;;AAGf,KAAI,OAAO,SAAS,QAAQ;AAC1B,UAAQ,IAAI,KAAK,6CAA6C,CAAC;AAC/D,OAAK,MAAM,KAAK,OAAO,SACrB,SAAQ,IAAI,OAAO,MAAM,OAAO,IAAI,CAAC,GAAG,KAAK,EAAE,KAAK,CAAC,IAAI,IAAI,EAAE,MAAM,GAAG;AAE1E,UAAQ,KAAK;;AAGf,KAAI,OAAO,QAAQ,QAAQ;AACzB,UAAQ,IAAI,KAAK,+CAA+C,CAAC;AACjE,OAAK,MAAM,KAAK,OAAO,QACrB,SAAQ,IAAI,OAAO,MAAM,KAAK,IAAI,CAAC,GAAG,KAAK,EAAE,KAAK,CAAC,IAAI,IAAI,EAAE,MAAM,GAAG;AAExE,UAAQ,KAAK;;AAGf,KAAI,gBAAgB,KAAK,kBAAkB,KAAK,iBAAiB,GAAG;AAClE,UAAQ,IAAI,GAAG,sBAAsB,CAAC;AACtC,UAAQ,KAAK;;;AAIjB,SAAS,QAAQ,MAAgC;CAC/C,MAAM,YAAY,aAAa,KAAK,MAAM;CAE1C,MAAM,QAAQ,UAAU,MAAM,IAAI,CAAC,OAAO,QAAQ;CAClD,MAAM,YAAY,MAAM,MAAM,SAAS,MAAM;AAE7C,KAAI,CAAC,WAAW,UAAU,EAAE;AAC1B,UAAQ,MAAM,IAAI,uBAAuB,YAAY,CAAC;AACtD,UAAQ,MAAM,IAAI,2BAA2B,CAAC;AAC9C,UAAQ,KAAK,EAAE;;CAGjB,MAAM,MAAM,yBAAyB,mBAAmB,UAAU;AAClE,SAAQ,IAAI,IAAI,cAAc,MAAM,CAAC;AACrC,KAAI;AACF,WAAS,SAAS,IAAI,IAAI,EAAE,OAAO,UAAU,CAAC;AAC9C,UAAQ,IAAI,GAAG,mBAAmB,UAAU,gBAAgB,CAAC;UACtD,GAAG;AACV,UAAQ,MAAM,IAAI,8BAA8B,IAAI,CAAC;AACrD,UAAQ,MAAM,IAAI,0EAA0E,CAAC;AAC7F,UAAQ,KAAK,EAAE;;;AAQnB,SAAgB,yBACd,aACA,OACM;AAEN,aACG,QAAQ,OAAO,CACf,YAAY,0EAA0E,CACtF,OAAO,kBAAkB,uDAAuD,CAChF,OAAO,WAAW,8CAA8C,CAChE,QAAQ,SAA8C;AACrD,UAAQ,OAAO,EAAE,KAAK;GACtB;AAGJ,aACG,QAAQ,SAAS,CACjB,YAAY,qEAAqE,CACjF,OAAO,kBAAkB,sBAAsB,CAC/C,QAAQ,SAA6B;AACpC,cAAU,OAAO,EAAE,KAAK;GACxB;AAGJ,aACG,QAAQ,OAAO,CACf,YAAY,qCAAqC,CACjD,OAAO,kBAAkB,sBAAsB,CAC/C,QAAQ,SAA6B;AACpC,UAAQ,KAAK;GACb;;;;;AC5ON,SAAS,KAAK,OAAO,IAAI;AACvB,SAAQ,IAAI,KAAK;;AAGnB,SAAS,KAAK,KAAa;AACzB,SAAQ,IAAI,MAAM,KAAK,KAAK,OAAO,MAAM,CAAC;;AAG5C,SAAS,KAAK,KAAa;AACzB,SAAQ,IAAI,IAAI,KAAK,MAAM,CAAC;;AAG9B,SAAS,QAAQ,KAAa;AAC5B,SAAQ,IAAI,GAAG,KAAK,MAAM,CAAC;;AAG7B,SAAS,QAAQ,KAAa;AAC5B,SAAQ,IAAI,KAAK,KAAK,MAAM,CAAC;;AAG/B,SAAS,MAAM,KAAa;AAC1B,SAAQ,IAAI,IAAI,KAAK,MAAM,CAAC;;;;;;AAO9B,SAAS,IAAI,KAAa,KAAuB;AAC/C,KAAI;AACF,WAAS,KAAK;GACZ,OAAO;GACP,KAAK,OAAO,QAAQ,KAAK;GACzB,KAAK,QAAQ;GACd,CAAC;AACF,SAAO;SACD;AACN,SAAO;;;;;;AAOX,SAAS,QAAQ,KAAa,KAA6B;AACzD,KAAI;AACF,SAAO,SAAS,KAAK;GACnB,OAAO;GACP,KAAK,OAAO,QAAQ,KAAK;GACzB,KAAK,QAAQ;GACd,CAAC,CAAC,UAAU,CAAC,MAAM;SACd;AACN,SAAO;;;;;;;AAQX,SAAS,eAAuB;AAI9B,QAAO,KAFW,QADC,cAAc,OAAO,KAAK,IAAI,CACZ,EAEd,QAAQ;;;;;AAMjC,SAAS,kBAA0B;CACjC,MAAM,aAAa;EACjB,KAAK,cAAc,EAAE,YAAY;EACjC,KAAK,SAAS,EAAE,OAAO,MAAM,OAAO,YAAY;EAChD,KAAK,KAAK,OAAO,SAAS,OAAO,gBAAgB,UAAU,OAAO,YAAY;EAC/E;AACD,MAAK,MAAM,KAAK,WACd,KAAI,WAAW,KAAK,GAAG,wBAAwB,CAAC,CAAE,QAAO;AAE3D,QAAO,KAAK,cAAc,EAAE,YAAY;;;;;;AAW1C,SAAS,iBAAgC;AACvC,MAAK,yBAAyB;CAK9B,MAAM,SAAS,QAAQ,iCAHR,cAAc,CAGkC;AAC/D,KAAI,CAAC,QAAQ;AACX,QAAM,qDAAqD;AAC3D,OAAK,0EAA0E;AAC/E,SAAO;;CAIT,MAAM,SAAS,QAAQ,6BAA6B,OAAO;AAC3D,KAAI,CAAC,QAAQ;AACX,QAAM,sDAAsD;AAC5D,OAAK,uEAAuE;AAC5E,SAAO;;AAGT,MAAK,eAAe,SAAS;AAC7B,MAAK,eAAe,SAAS;AAC7B,QAAO;;;;;;AAOT,SAAS,UAAU,SAA0B;AAC3C,MAAK,4BAA4B;CAGjC,MAAM,SAAS,QAAQ,0BAA0B,QAAQ;AACzD,KAAI,CAAC,QAAQ;AACX,OAAK,4CAA4C;AACjD,SAAO;;AAGT,MAAK,6BAA6B;CAClC,MAAM,cAAc,OAAO,MAAM,KAAK,CAAC,MAAM,GAAG,EAAE;AAClD,MAAK,MAAM,KAAK,YACd,MAAK,KAAK,IAAI;AAEhB,KAAI,OAAO,MAAM,KAAK,CAAC,SAAS,EAC9B,MAAK,aAAa,OAAO,MAAM,KAAK,CAAC,SAAS,EAAE,OAAO;CAGzD,MAAM,SAAS,QAAQ,8CAA8C,QAAQ;AAC7E,KAAI,WAAW,MAAM;AACnB,UAAQ,0EAA0E;AAClF,SAAO;;AAGT,KAAI,OAAO,SAAS,2BAA2B,EAAE;AAC/C,OAAK,oBAAoB;AACzB,SAAO;;AAGT,SAAQ,mBAAmB;AAC3B,QAAO;;;;;;AAOT,SAAS,SAAS,SAAgC;AAChD,MAAK,qCAAqC;CAG1C,MAAM,aAAa,QAAQ,sBAAsB,QAAQ;AAGzD,KAAI,CADW,IAAI,wBAAwB,QAAQ,EACtC;AACX,UAAQ,iEAAiE;AACzE,SAAO;;CAIT,MAAM,YAAY,QAAQ,sBAAsB,QAAQ;AAExD,KAAI,eAAe,WAAW;AAC5B,UAAQ,uCAAuC;AAC/C,SAAO;;CAIT,MAAM,MAAM,QACV,qBAAqB,cAAc,GAAG,IAAI,aAAa,UACvD,QACD;AACD,KAAI,KAAK;AACP,UAAQ,sBAAsB;AAC9B,OAAK,MAAM,KAAK,IAAI,MAAM,KAAK,CAC7B,MAAK,KAAK,IAAI;;AAIlB,QAAO;;;;;AAMT,SAAS,aAAa,SAAuB;AAC3C,MAAK,6CAA6C;CAElD,MAAM,SAAS,UAAU,OAAO,CAAC,SAAS,MAAM,EAAE;EAChD,KAAK;EACL,OAAO;EACP,UAAU;EACX,CAAC;AAEF,KAAI,OAAO,WAAW,EACpB,SAAQ,0BAA0B;MAC7B;EACL,MAAM,SAAS,OAAO,UAAU;AAChC,MAAI,OAAO,SAAS,WAAW,EAAE;AAC/B,WAAQ,2DAA2D;AACnE,QAAK,eAAe;AACpB,QAAK,6BAA6B;AAClC,QAAK,oBAAoB;AACzB,QAAK,mBAAmB;SACnB;AACL,WAAQ,mCAAmC,OAAO,MAAM,IAAI,kBAAkB;AAC9E,QAAK,uBAAuB;AAC5B,QAAK,kDAAkD;;;;;;;AAQ7D,SAAS,UAAU,SAA0B;AAC3C,MAAK,mDAAmD;AAExD,MAAK,6BAA6B;AAElC,KAAI,CADc,IAAI,eAAe,QAAQ,EAC7B;AACd,QAAM,sBAAsB;AAC5B,SAAO;;AAGT,MAAK,cAAc;AAEnB,KAAI,CADU,IAAI,iBAAiB,QAAQ,EAC/B;AACV,QAAM,wBAAwB;AAC9B,SAAO;;AAGT,SAAQ,kBAAkB;AAC1B,QAAO;;;;;;AAOT,SAAS,kBAAkB,SAAuB;AAChD,MAAK,2BAA2B;CAGhC,MAAM,UAAU;AAChB,KAAI,WAAW,QAAQ,CACrB,KAAI;EACF,MAAM,MAAM,SAAS,aAAa,SAAS,OAAO,CAAC,MAAM,EAAE,GAAG;AAC9D,MAAI,CAAC,MAAM,IAAI,IAAI,MAAM,GAEvB;OADe,QAAQ,aAAa,MAAM,KAC3B,MAAM;AACnB,YAAQ,8BAA8B,IAAI,IAAI;AAC9C;;;SAGE;AAaV,KAPe,UAAU,OAAO,CAAC,UAAU,UAAU,EAAE;EACrD,KAAK;EACL,OAAO;EACP,UAAU;EACV,SAAS;EACV,CAAC,CAES,WAAW,EACpB,SAAQ,oBAAoB;MACvB;AACL,UAAQ,0CAA0C;AAClD,OAAK,yCAAyC;AAC9C,OAAK,uCAAuC;;;;;;AAOhD,SAAS,oBAAoB,SAAiB,eAAoC;AAChF,MAAK,iCAAiC;CAGtC,MAAM,eAAe,KADA,iBAAiB,EACE,wBAAwB;CAChE,MAAM,WAAW,KAAK,SAAS,EAAE,WAAW,YAAY;AAExD,KAAI,CAAC,WAAW,aAAa,EAAE;AAC7B,OAAK,iDAAiD;AACtD;;AAGF,KAAI,CAAC,WAAW,SAAS,EAAE;AACzB,OAAK,2CAA2C;AAChD;;AAMF,KAAI,CAHgB,aAAa,UAAU,OAAO,CACf,SAAS,yBAAyB,EAEhD;AACnB,UAAQ,gEAAgE;AACxE,OAAK,4DAA4D;AACjE,OAAK,OAAO,eAAe;AAC3B;;CAIF,IAAI,kBAAkB;AACtB,KAAI,eAAe;EAEjB,MAAM,OAAO,QACX,4DACA,QACD;AACD,oBAAkB,CAAC,EAAE,QAAQ,KAAK,MAAM,CAAC,SAAS;;AAGpD,KAAI,CAAC,iBAAiB;AACpB,OAAK,+CAA+C;AACpD;;AAGF,MAAK,kDAAkD;AACvD,MAAK,wEAAwE;CAE7E,IAAI,WAAW,aAAa,cAAc,OAAO;AAEjD,YAAW,SAAS,QAAQ,eAAe,SAAS,CAAC;AACrD,eAAc,UAAU,UAAU,OAAO;AAEzC,SAAQ,uDAAuD;;;;;AAMjE,SAAS,iBAAiB,SAAuB;AAC/C,MAAK,+BAA+B;AAQpC,KANe,UAAU,OAAO,CAAC,YAAY,OAAO,EAAE;EACpD,KAAK;EACL,OAAO;EACP,SAAS;EACV,CAAC,CAES,WAAW,EACpB,SAAQ,0BAA0B;MAC7B;AACL,UAAQ,oCAAoC;AAC5C,OAAK,oCAAoC;;;AAQ7C,eAAe,YAA2B;AACxC,OAAM;AACN,SAAQ,IAAI,MAAM,KAAK,KAAK,0CAA0C,CAAC;AACvE,SAAQ,IAAI,MAAM,KAAK,KAAK,0CAA0C,CAAC;AACvE,SAAQ,IAAI,MAAM,KAAK,KAAK,0CAA0C,CAAC;AACvE,OAAM;CAGN,MAAM,UAAU,gBAAgB;AAChC,KAAI,CAAC,SAAS;AACZ,QAAM;AACN,UAAQ,KAAK,EAAE;;CAIjB,MAAM,WAAW,UAAU,QAAQ;CAGnC,MAAM,gBAAgB,SAAS,QAAQ;AAGvC,KAAI,SACF,cAAa,QAAQ;AASvB,KAAI,CADY,UAAU,QAAQ,EACpB;AACZ,QAAM;AACN,QAAM,mDAAmD;AACzD,QAAM;AACN,UAAQ,KAAK,EAAE;;AAIjB,mBAAkB,QAAQ;AAG1B,qBAAoB,SAAS,cAAc;AAG3C,kBAAiB,QAAQ;AAGzB,OAAM;AACN,SAAQ,IAAI,MAAM,KAAK,KAAK,0CAA0C,CAAC;AACvE,KAAI,cACF,SAAQ,4BAA4B;KAEpC,SAAQ,8CAA8C;AAExD,SAAQ,IAAI,IAAI,cAAc,GAAG,MAAM,KAAK,QAAQ,iBAAiB,QAAQ,IAAI,UAAU,CAAC;AAC5F,OAAM;;AAOR,SAAgB,sBAAsB,SAAwB;AAC5D,SACG,QAAQ,SAAS,CACjB,YACC,gGACD,CACA,OAAO,YAAY;AAClB,QAAM,WAAW;GACjB;;;;;AC9bN,SAASC,eAAwB;AAE/B,QAAO,IAAI,UADIC,cAAY,CACC,WAAW;;AAGzC,SAAS,UAAU,MAAgC;AACjD,SAAQ,MAAR;EACE,KAAK,MACH,QAAO,KAAK,KAAK;EACnB,KAAK,QACH,QAAO,KAAK,GAAG,KAAK,CAAC;EACvB,KAAK,OACH,QAAO,GAAG,KAAK;EACjB,QACE,QAAO,GAAG,KAAK;;;AAIrB,SAAS,YAAY,QAA4B,gBAAgC;AAC/E,SAAQ,KAAK;AACb,SAAQ,IAAI,OAAO,4BAA4B,CAAC;AAChD,SAAQ,KAAK;AACb,SAAQ,IAAI,KAAK,KAAK,QAAQ,CAAC,MAAM,UAAU,OAAO,KAAK,GAAG;AAC9D,SAAQ,KAAK;AACb,SAAQ,IAAI,KAAK,KAAK,YAAY,GAAG;AACrC,MAAK,MAAM,CAAC,IAAI,QAAQ,OAAO,QAAQ,OAAO,SAAS,EAAE;EACvD,MAAM,IAAI;EACV,MAAM,SAAS,EAAE,UAAU,GAAG,UAAU,GAAG,IAAI,WAAW;EAC1D,IAAI,QAAQ;AACZ,MAAI,OAAO,UAAU,EAAE,IAAK,SAAQ,IAAI,KAAK,EAAE,MAAM;AACrD,MAAI,OAAO,WAAW,EAAE,UAAW,SAAQ,IAAI,YAAY,EAAE,YAAY;AACzE,UAAQ,IAAI,OAAO,KAAK,GAAG,OAAO,GAAG,CAAC,GAAG,SAAS,QAAQ;;AAE5D,SAAQ,KAAK;AACb,SAAQ,IAAI,KAAK,KAAK,UAAU,CAAC,IAAI,eAAe,SAAS,IAAI,eAAe,KAAK,KAAK,GAAG,IAAI,SAAS,GAAG;AAC7G,SAAQ,KAAK;AACb,SAAQ,IAAI,KAAK,KAAK,WAAW,CAAC,GAAG,IAAI,mBAAmB,GAAG;AAC/D,MAAK,MAAM,CAAC,OAAO,aAAa,OAAO,QAAQ,OAAO,QAAQ,EAAE;EAC9D,MAAM,KAAM,SAAsB,KAAK,KAAK,IAAI,IAAI,SAAS;AAC7D,UAAQ,IAAI,OAAO,MAAM,OAAO,GAAG,CAAC,KAAK,KAAK;;AAEhD,SAAQ,KAAK;;AAOf,eAAe,YAA2B;CACxC,MAAM,SAASD,cAAY;AAC3B,KAAI;EACF,MAAM,EAAE,QAAQ,mBAAmB,MAAM,OAAO,uBAAuB;AACvE,cAAY,QAAQ,eAAe;UAC5B,GAAG;EACV,MAAM,MAAM,aAAa,QAAQ,EAAE,UAAU,OAAO,EAAE;AACtD,UAAQ,KAAK;AACb,UAAQ,IAAI,KAAK,6BAA6B,CAAC;AAC/C,UAAQ,IAAI,IAAI,KAAK,MAAM,CAAC;AAC5B,UAAQ,IAAI,IAAI,oCAAoC,CAAC;AACrD,UAAQ,KAAK;AACb,UAAQ,KAAK,EAAE;;;AAInB,eAAe,OAAO,MAOJ;CAChB,MAAM,SAASA,cAAY;CAG3B,MAAM,QAGF,EAAE;AAEN,KAAI,KAAK,MAAM;EACb,MAAM,aAAiC;GACrC;GAAQ;GAAS;GAAY;GAAQ;GAAS;GAAO;GACtD;AACD,MAAI,CAAC,WAAW,SAAS,KAAK,KAAyB,EAAE;AACvD,WAAQ,MAAM,IAAI,iBAAiB,KAAK,KAAK,WAAW,WAAW,KAAK,KAAK,GAAG,CAAC;AACjF,WAAQ,KAAK,EAAE;;AAEjB,QAAM,OAAO,KAAK;;CAIpB,MAAM,WAAoC,EAAE;AAE5C,KAAI,KAAK,cACP,MAAK,MAAM,MAAM,KAAK,cACpB,UAAS,MAAM,EAAE,SAAS,MAAM;AAIpC,KAAI,KAAK,eACP,MAAK,MAAM,MAAM,KAAK,eACpB,UAAS,MAAM,EAAE,SAAS,OAAO;AAIrC,KAAI,KAAK,QACP,UAAS,UAAU;EACjB,GAAI,SAAS,WAAqB,EAAE;EACpC,KAAK,KAAK;EACX;AAGH,KAAI,KAAK,aACP,UAAS,UAAU;EACjB,GAAI,SAAS,WAAqB,EAAE;EACpC,UAAU,KAAK;EAChB;AAGH,KAAI,KAAK,kBACP,UAAS,cAAc;EACrB,GAAI,SAAS,eAAyB,EAAE;EACxC,WAAW,KAAK;EACjB;AAGH,KAAI,OAAO,KAAK,SAAS,CAAC,SAAS,EACjC,OAAM,WAAW;AAGnB,KAAI,CAAC,MAAM,QAAQ,CAAC,MAAM,UAAU;AAClC,UAAQ,MAAM,IAAI,8DAA8D,CAAC;AACjF,UAAQ,IAAI,IAAI,yCAAyC,CAAC;AAC1D,UAAQ,IAAI,IAAI,sEAAsE,CAAC;AACvF,UAAQ,KAAK,EAAE;;AAGjB,KAAI;EACF,MAAM,EAAE,WAAW,MAAM,OAAO,sBAC9B,MACD;AACD,UAAQ,KAAK;AACb,UAAQ,IAAI,GAAG,iCAAiC,CAAC;AACjD,UAAQ,IAAI,KAAK,KAAK,QAAQ,CAAC,GAAG,UAAU,OAAO,KAAK,GAAG;AAC3D,UAAQ,KAAK;UACN,GAAG;EACV,MAAM,MAAM,aAAa,QAAQ,EAAE,UAAU,OAAO,EAAE;AACtD,UAAQ,MAAM,IAAI,aAAa,MAAM,CAAC;AACtC,UAAQ,KAAK,EAAE;;;AAInB,eAAe,QAAQ,MAAyC;CAC9D,MAAM,SAASA,cAAY;CAC3B,MAAM,QAAS,KAAK,SAAS;AAE7B,SAAQ,KAAK;AACb,SAAQ,IAAI,IAAI,kBAAkB,MAAM,kBAAkB,CAAC;AAE3D,KAAI;EACF,MAAM,SAAS,MAAM,OAAO,iBAAiB;GAC3C;GACA,SAAS,0BAA0B,MAAM;GACzC,OAAO;GACR,CAAC;AAEF,MAAI,OAAO,kBAAkB,WAAW,KAAK,OAAO,kBAAkB,SAAS,EAC7E,SAAQ,IAAI,KAAK,yBAAyB,CAAC;WAClC,OAAO,kBAAkB,WAAW,EAC7C,SAAQ,IAAI,KAAK,sEAAsE,CAAC;MAExF,SAAQ,IAAI,GAAG,cAAc,OAAO,kBAAkB,KAAK,KAAK,GAAG,CAAC;AAGtE,MAAI,OAAO,eAAe,SAAS,EACjC,SAAQ,IAAI,KAAK,aAAa,OAAO,eAAe,KAAK,KAAK,GAAG,CAAC;AAGpE,UAAQ,IAAI,IAAI,WAAW,OAAO,OAAO,CAAC;AAC1C,UAAQ,KAAK;UACN,GAAG;EACV,MAAM,MAAM,aAAa,QAAQ,EAAE,UAAU,OAAO,EAAE;AACtD,UAAQ,MAAM,IAAI,aAAa,MAAM,CAAC;AACtC,UAAQ,KAAK,EAAE;;;AAInB,eAAe,QACb,OACA,SACA,MACe;CACf,MAAM,SAASA,cAAY;CAC3B,MAAM,cAAmC;EACvC;EAAS;EAAY;EAAc;EAAQ;EAC5C;AAED,KAAI,CAAC,YAAY,SAAS,MAA2B,EAAE;AACrD,UAAQ,MACN,IAAI,kBAAkB,MAAM,WAAW,YAAY,KAAK,KAAK,GAAG,CACjE;AACD,UAAQ,KAAK,EAAE;;AAGjB,KAAI;EACF,MAAM,SAAS,MAAM,OAAO,iBAAiB;GACpC;GACP;GACA,OAAO,KAAK;GACb,CAAC;AAEF,MAAI,OAAO,kBAAkB,SAAS,EACpC,SAAQ,IAAI,GAAG,cAAc,OAAO,kBAAkB,KAAK,KAAK,GAAG,CAAC;MAEpE,SAAQ,IAAI,KAAK,2CAA2C,CAAC;AAG/D,MAAI,OAAO,eAAe,SAAS,EACjC,SAAQ,IAAI,KAAK,aAAa,OAAO,eAAe,KAAK,KAAK,GAAG,CAAC;UAE7D,GAAG;EACV,MAAM,MAAM,aAAa,QAAQ,EAAE,UAAU,OAAO,EAAE;AACtD,UAAQ,MAAM,IAAI,aAAa,MAAM,CAAC;AACtC,UAAQ,KAAK,EAAE;;;AAQnB,SAAgB,uBAAuB,WAA0B;AAE/D,WACG,QAAQ,SAAS,CACjB,YAAY,qDAAqD,CACjE,OAAO,YAAY;AAClB,QAAM,WAAW;GACjB;AAGJ,WACG,QAAQ,MAAM,CACd,YAAY,sDAAsD,CAClE,OAAO,YAAY;AAClB,QAAM,WAAW;GACjB;AAGJ,WACG,QAAQ,MAAM,CACd,YAAY,oDAAoD,CAChE,OACC,iBACA,wEACD,CACA,OAAO,yBAAyB,8BAA8B,CAC9D,OAAO,0BAA0B,+BAA+B,CAChE,OAAO,oBAAoB,4BAA4B,CACvD,OACC,2BACA,yDACD,CACA,OAAO,kCAAkC,yBAAyB,CAClE,OACC,OAAO,SAOD;AACJ,QAAM,OAAO;GACX,MAAM,KAAK;GACX,eAAe,KAAK;GACpB,gBAAgB,KAAK;GACrB,SAAS,KAAK;GACd,cAAc,KAAK;GACnB,mBAAmB,KAAK;GACzB,CAAC;GAEL;AAGH,WACG,QAAQ,OAAO,CACf,YAAY,uDAAuD,CACnE,OACC,mBACA,oEACA,OACD,CACA,OAAO,OAAO,SAA6B;AAC1C,QAAM,QAAQ,KAAK;GACnB;AAGJ,WACG,QAAQ,yBAAyB,CACjC,YAAY,8DAA8D,CAC1E,OAAO,mBAAmB,8BAA8B,CACxD,OAAO,OAAO,OAAe,SAAiB,SAA6B;AAC1E,QAAM,QAAQ,OAAO,SAAS,KAAK;GACnC;;;;;ACnTN,SAAS,aAAwB;AAE/B,QAAO,IAAI,UADIE,cAAY,CACC,WAAW;;AAGzC,SAAS,cAAc,YAAoB,QAAQ,IAAY;CAC7D,MAAM,SAAS,KAAK,MAAM,aAAa,MAAM;CAC7C,MAAM,QAAQ,QAAQ;AACtB,QAAO,MAAM,IAAI,OAAO,OAAO,GAAG,IAAI,OAAO,MAAM,GAAG;;AAGxD,SAAS,iBAAiB,QAAgC;AACxD,SAAQ,KAAK;AACb,SAAQ,IAAI,OAAO,oBAAoB,CAAC;AACxC,SAAQ,KAAK;AAEb,SAAQ,IAAI,KAAK,KAAK,mBAAmB,CAAC,MAAM,OAAO,kBAAkB,IAAI,SAAS,GAAG;AACzF,SAAQ,IAAI,KAAK,KAAK,qBAAqB,CAAC,IAAI,OAAO,oBAAoB,IAAI,SAAS,GAAG;AAC3F,SAAQ,IACN,KAAK,KAAK,cAAc,CAAC,WAAW,cAAc,OAAO,WAAW,CAAC,IAAI,OAAO,aAAa,KAAK,QAAQ,EAAE,CAAC,GAC9G;AACD,SAAQ,IAAI,KAAK,KAAK,iBAAiB,CAAC,QAAQ,OAAO,aAAa;AAEpE,KAAI,OAAO,YAAY,SAAS,GAAG;AACjC,UAAQ,KAAK;AACb,UAAQ,IAAI,KAAK,KAAK,eAAe,GAAG;AACxC,OAAK,MAAM,KAAK,OAAO,aAAa;GAClC,MAAM,MAAM,cAAc,EAAE,OAAO,GAAG;GACtC,MAAM,SAAS,EAAE,SAAS,OAAO,iBAAiB,IAAI,aAAa,GAAG;AACtE,WAAQ,IAAI,OAAO,EAAE,KAAK,OAAO,GAAG,CAAC,GAAG,IAAI,IAAI,EAAE,QAAQ,KAAK,QAAQ,EAAE,CAAC,GAAG,SAAS;;;AAI1F,SAAQ,KAAK;AAEb,KAAI,OAAO,SAAS;AAClB,UAAQ,IACN,KAAK,yBAAyB,GAC9B,IAAI,wCAAwC,OAAO,iBAAiB,UAAU,OAAO,eAAe,GAAG,CACxG;AACD,UAAQ,KAAK;YACJ,OAAO,oBAAoB,OAAO,qBAAqB,OAAO,gBAAgB;AACvF,UAAQ,IAAI,GAAG,sBAAsB,GAAG,IAAI,qCAAqC,CAAC;AAClF,UAAQ,KAAK;YACJ,CAAC,OAAO,gBAAgB;AACjC,MAAI,OAAO,iBACT,SAAQ,IACN,GAAG,4BAA4B,GAAG,KAAK,OAAO,iBAAiB,GAC/D,IAAI,kBAAkB,OAAO,aAAa,KAAK,QAAQ,EAAE,CAAC,IAAI,CAC/D;MAED,SAAQ,IAAI,IAAI,+CAA+C,CAAC;AAElE,UAAQ,KAAK;QACR;AACL,UAAQ,IACN,IAAI,mCAAmC,OAAO,iBAAiB,oBAAoB,OAAO,aAAa,KAAK,QAAQ,EAAE,CAAC,sBAAsB,CAC9I;AACD,UAAQ,KAAK;;;AAQjB,eAAe,SACb,SACA,MAKe;CACf,MAAM,SAAS,YAAY;CAE3B,MAAM,YAAY,KAAK,YAAY,WAAW,KAAK,UAAU,GAAG;AAEhE,KAAI,cAAc,WAAc,MAAM,UAAU,IAAI,YAAY,KAAK,YAAY,IAAI;AACnF,UAAQ,MAAM,IAAI,iDAAiD,CAAC;AACpE,UAAQ,KAAK,EAAE;;AAGjB,KAAI;EACF,MAAM,SAAS,MAAM,OAAO,WAAW;GACrC;GACA,gBAAgB,KAAK;GACrB;GACD,CAAC;AAEF,MAAI,KAAK,MAAM;AACb,WAAQ,IAAI,KAAK,UAAU,QAAQ,MAAM,EAAE,CAAC;AAC5C;;AAGF,mBAAiB,OAAO;AAGxB,MAAI,OAAO,QACT,SAAQ,KAAK,EAAE;UAEV,GAAG;EACV,MAAM,MAAM,aAAa,QAAQ,EAAE,UAAU,OAAO,EAAE;AACtD,UAAQ,KAAK;AACb,UAAQ,IAAI,KAAK,6BAA6B,CAAC;AAC/C,UAAQ,IAAI,IAAI,KAAK,MAAM,CAAC;AAC5B,UAAQ,IAAI,IAAI,oCAAoC,CAAC;AACrD,UAAQ,KAAK;AACb,UAAQ,KAAK,EAAE;;;AAQnB,SAAgB,sBAAsB,UAAyB;AAE7D,UACG,QAAQ,kBAAkB,CAC1B,YACC,qLAGD,CACA,OACC,oBACA,kDACD,CACA,OACC,mBACA,+DACD,CACA,OAAO,UAAU,yBAAyB,CAC1C,OACC,OACE,SACA,SACG;AACH,QAAM,SAAS,SAAS,KAAK;GAEhC;;;;;;;;;;;;;;;;;AC7HL,SAAS,aAAqB;AAC5B,KAAI;EAGF,MAAM,UAAU,KAFE,QAAQ,cAAc,OAAO,KAAK,IAAI,CAAC,EAEzB,qBAAqB;AAErD,SADY,KAAK,MAAM,aAAa,SAAS,OAAO,CAAC,CAC1C,WAAW;SAChB;AACN,SAAO;;;AAQX,IAAI,MAAuB;AAE3B,SAAS,QAAkB;AACzB,KAAI,CAAC,IACH,KAAI;AACF,QAAM,cAAc;UACb,GAAG;AACV,UAAQ,MAAM,IAAI,gCAAgC,IAAI,CAAC;AACvD,UAAQ,KAAK,EAAE;;AAGnB,QAAO;;AAOT,MAAM,UAAU,IAAI,SAAS;AAE7B,QACG,KAAK,MAAM,CACX,YAAY,oDAAoD,CAChE,QAAQ,YAAY,EAAE,iBAAiB,yBAAyB;AAUnE,wBAJmB,QAChB,QAAQ,UAAU,CAClB,YAAY,6BAA6B,EAER,MAAM;AAM1C,MAAM,aAAa,QAChB,QAAQ,UAAU,CAClB,YAAY,uBAAuB;AAEtC,wBAAwB,YAAY,MAAM;AAC1C,8BAA8B,YAAY,MAAM;AAUhD,yBAJoB,QACjB,QAAQ,WAAW,CACnB,YAAY,sDAAsD,EAE/B,MAAM;AAU5C,uBAJkB,QACf,QAAQ,SAAS,CACjB,YAAY,2CAA2C,EAExB,MAAM;AAUxC,oBAJe,QACZ,QAAQ,MAAM,CACd,YAAY,4CAA4C,CAEhC;AAU3B,uBAJkB,QACf,QAAQ,SAAS,CACjB,YAAY,0EAA0E,CAExD;AAMjC,uBAAuB,QAAQ;AAC/B,wBAAwB,QAAQ;AAMhC,qBAAqB,QAAQ;AAM7B,sBAAsB,QAAQ;AAU9B,uBAJkB,QACf,QAAQ,SAAS,CACjB,YAAY,oDAAoD,CAElC;AAUjC,sBAJiB,QACd,QAAQ,QAAQ,CAChB,YAAY,kFAAkF,CAElE;AAU/B,yBAJoB,QACjB,QAAQ,WAAW,CACnB,YAAY,oEAAoE,EAE7C,MAAM;AAM5C,QACG,QAAQ,aAAa,CACrB,YACC,kMAID,CACA,QAAQ,UAAkB;AACzB,OAAM,OAAO,EAAE,MAAM;EACrB;AAMJ,QACG,QAAQ,iBAAiB,CACzB,YAAY,uDAAuD,CACnE,OAAO,sBAAsB,2DAA2D,CACxF,OAAO,eAAe,6BAA6B,KAAK,CACxD,QAAQ,OAAe,SAAgD;AACtE,SAAQ,IACN,mDACiB,MAAM,gBACN,KAAK,YAAY,QAAQ,gBACzB,KAAK,SAAS,GAAG,IACnC;EACD;AAMJ,QAAQ,gBAAgB,EACtB,WAAW,QAAQ,QAAQ,OAAO,MAAM,IAAI,IAAI,CAAC,EAClD,CAAC;AAEF,QAAQ,cAAc,UAAU;AAC9B,KAAI,MAAM,SAAS,6BAA6B,MAAM,SAAS,oBAC7D,SAAQ,KAAK,EAAE;AAEjB,KACE,MAAM,SAAS,+BACf,MAAM,SAAS,6BACf,MAAM,SAAS,2BAGf,SAAQ,KAAK,EAAE;AAGjB,SAAQ,MAAM,IAAI,MAAM,QAAQ,CAAC;AACjC,SAAQ,KAAK,EAAE;EACf;AAMF,QAAQ,MAAM,QAAQ,KAAK;AAG3B,IAAI,QAAQ,KAAK,UAAU,EACzB,SAAQ,MAAM"}