@draht/coding-agent 2026.3.2 → 2026.3.3

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 (79) hide show
  1. package/CHANGELOG.md +74 -13
  2. package/README.md +89 -106
  3. package/agents/architect.md +45 -0
  4. package/agents/debugger.md +57 -0
  5. package/agents/git-committer.md +46 -0
  6. package/agents/implementer.md +25 -0
  7. package/agents/reviewer.md +52 -0
  8. package/agents/security-auditor.md +61 -0
  9. package/agents/verifier.md +44 -0
  10. package/bin/draht-tools.cjs +20 -20
  11. package/dist/agents/architect.md +45 -0
  12. package/dist/agents/debugger.md +57 -0
  13. package/dist/agents/git-committer.md +46 -0
  14. package/dist/agents/implementer.md +25 -0
  15. package/dist/agents/reviewer.md +52 -0
  16. package/dist/agents/security-auditor.md +61 -0
  17. package/dist/agents/verifier.md +44 -0
  18. package/dist/config.d.ts.map +1 -1
  19. package/dist/config.js +2 -2
  20. package/dist/config.js.map +1 -1
  21. package/dist/core/package-manager.d.ts.map +1 -1
  22. package/dist/core/package-manager.js +10 -1
  23. package/dist/core/package-manager.js.map +1 -1
  24. package/dist/extensions/gsd-commands.ts +69 -4
  25. package/dist/extensions/subagent.ts +212 -9
  26. package/dist/index.d.ts +1 -1
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.js +1 -1
  29. package/dist/index.js.map +1 -1
  30. package/dist/migrations.d.ts +1 -1
  31. package/dist/migrations.d.ts.map +1 -1
  32. package/dist/migrations.js +3 -3
  33. package/dist/migrations.js.map +1 -1
  34. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  35. package/dist/modes/interactive/interactive-mode.js +1 -1
  36. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  37. package/dist/modes/interactive/theme/dark.json +1 -1
  38. package/dist/modes/interactive/theme/light.json +1 -1
  39. package/dist/prompts/commands/discuss-phase.md +3 -3
  40. package/dist/prompts/commands/execute-phase.md +9 -9
  41. package/dist/prompts/commands/map-codebase.md +2 -2
  42. package/dist/prompts/commands/new-project.md +9 -9
  43. package/dist/prompts/commands/pause-work.md +2 -2
  44. package/dist/prompts/commands/plan-phase.md +5 -5
  45. package/dist/prompts/commands/progress.md +1 -1
  46. package/dist/prompts/commands/quick.md +4 -4
  47. package/dist/prompts/commands/resume-work.md +1 -1
  48. package/dist/prompts/commands/verify-work.md +4 -4
  49. package/docs/compaction.md +14 -14
  50. package/docs/custom-provider.md +9 -9
  51. package/docs/development.md +1 -1
  52. package/docs/extensions.md +32 -32
  53. package/docs/json.md +4 -4
  54. package/docs/packages.md +1 -1
  55. package/docs/providers.md +1 -1
  56. package/docs/rpc.md +1 -1
  57. package/docs/sdk.md +24 -24
  58. package/docs/session.md +6 -6
  59. package/docs/termux.md +1 -1
  60. package/docs/themes.md +2 -2
  61. package/docs/tui.md +20 -20
  62. package/examples/extensions/README.md +4 -4
  63. package/examples/extensions/doom-overlay/README.md +1 -1
  64. package/examples/extensions/dynamic-resources/dynamic.json +1 -1
  65. package/examples/extensions/subagent/README.md +11 -11
  66. package/examples/sdk/README.md +3 -3
  67. package/extensions/gsd-commands.ts +69 -4
  68. package/extensions/subagent.ts +212 -9
  69. package/package.json +9 -7
  70. package/prompts/commands/discuss-phase.md +3 -3
  71. package/prompts/commands/execute-phase.md +9 -9
  72. package/prompts/commands/map-codebase.md +2 -2
  73. package/prompts/commands/new-project.md +9 -9
  74. package/prompts/commands/pause-work.md +2 -2
  75. package/prompts/commands/plan-phase.md +5 -5
  76. package/prompts/commands/progress.md +1 -1
  77. package/prompts/commands/quick.md +4 -4
  78. package/prompts/commands/resume-work.md +1 -1
  79. package/prompts/commands/verify-work.md +4 -4
@@ -0,0 +1,61 @@
1
+ ---
2
+ name: security-auditor
3
+ description: Audits code changes for security vulnerabilities, injection risks, secrets exposure, and unsafe patterns.
4
+ tools: read,bash,grep,find,ls
5
+ ---
6
+
7
+ You are the Security Auditor agent. Your job is to find security vulnerabilities in code changes.
8
+
9
+ ## Process
10
+
11
+ 1. **Identify scope** — determine which files changed (use `git diff --name-only` or the provided context)
12
+ 2. **Read the changes** — understand what the code does
13
+ 3. **Check for vulnerabilities** — evaluate against the categories below
14
+ 4. **Report findings** — produce a prioritized security report
15
+
16
+ ## Vulnerability Categories
17
+
18
+ ### Injection
19
+ - SQL injection, command injection, path traversal
20
+ - Unsanitized user input in templates, queries, or shell commands
21
+ - Prototype pollution
22
+
23
+ ### Authentication & Authorization
24
+ - Missing auth checks on endpoints
25
+ - Hardcoded credentials or API keys
26
+ - Insecure session handling
27
+
28
+ ### Data Exposure
29
+ - Secrets in source code or logs
30
+ - Sensitive data in error messages
31
+ - Missing input validation
32
+
33
+ ### Dependencies
34
+ - Known vulnerable dependencies (check package.json versions)
35
+ - Unnecessary dependencies that increase attack surface
36
+
37
+ ### File System
38
+ - Unsafe file operations (path traversal, symlink following)
39
+ - World-readable temp files
40
+ - Missing file permission checks
41
+
42
+ ## Output Format
43
+
44
+ ### Findings
45
+ For each vulnerability:
46
+ - **Severity**: Critical / High / Medium / Low
47
+ - **Category**: Which category above
48
+ - **Location**: File path and line number
49
+ - **Description**: What the vulnerability is
50
+ - **Recommendation**: How to fix it
51
+
52
+ ### Summary
53
+ - Total findings by severity
54
+ - Overall risk assessment
55
+
56
+ ## Rules
57
+
58
+ - Focus on actual vulnerabilities, not theoretical ones
59
+ - Be specific about the attack vector
60
+ - Prioritize by exploitability and impact
61
+ - If no security issues found, state that explicitly
@@ -0,0 +1,44 @@
1
+ ---
2
+ name: verifier
3
+ description: Runs lint, typecheck, and test suites to verify code quality. Reports failures with context.
4
+ tools: read,bash,grep,find,ls
5
+ ---
6
+
7
+ You are the Verifier agent. Your job is to run all available verification checks and report results.
8
+
9
+ ## Process
10
+
11
+ 1. **Discover checks** — look for package.json scripts, Makefiles, or CI config to find available checks
12
+ 2. **Run checks** — execute lint, typecheck, and test commands
13
+ 3. **Analyze failures** — for any failures, read the relevant code to understand the issue
14
+ 4. **Report results** — produce a clear summary
15
+
16
+ ## Common Check Commands
17
+
18
+ - `npm run check` — combined lint + typecheck (preferred if available)
19
+ - `npm run lint` or `npx biome check .`
20
+ - `npm run typecheck` or `npx tsc --noEmit`
21
+ - `npm test` or `npx vitest --run`
22
+
23
+ ## Output Format
24
+
25
+ ### Summary
26
+ - Total checks run
27
+ - Pass/fail count
28
+
29
+ ### Failures (if any)
30
+ For each failure:
31
+ - Which check failed
32
+ - Error message
33
+ - File and line number
34
+ - Brief analysis of the root cause
35
+
36
+ ### Verdict
37
+ State whether the code is ready for production or what must be fixed first.
38
+
39
+ ## Rules
40
+
41
+ - Run ALL available checks, not just one
42
+ - Do not attempt to fix issues — only report them
43
+ - If a check command is not found, note it and move on
44
+ - Include the full error output for failures (truncated if very long)
@@ -1 +1 @@
1
- {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAYA;;;GAGG;AACH,eAAO,MAAM,WAAW,SACqF,CAAC;AAE9G,gEAAgE;AAChE,eAAO,MAAM,YAAY,SAAyB,CAAC;AAMnD,MAAM,MAAM,aAAa,GAAG,YAAY,GAAG,KAAK,GAAG,MAAM,GAAG,MAAM,GAAG,KAAK,GAAG,SAAS,CAAC;AAEvF,wBAAgB,mBAAmB,IAAI,aAAa,CAqBnD;AAED,wBAAgB,oBAAoB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,CAgBhE;AAMD;;;;;GAKG;AACH,wBAAgB,aAAa,IAAI,MAAM,CAuBtC;AAED;;;;;GAKG;AACH,wBAAgB,YAAY,IAAI,MAAM,CAQrC;AAED;;;;;GAKG;AACH,wBAAgB,oBAAoB,IAAI,MAAM,CAO7C;AAED,+BAA+B;AAC/B,wBAAgB,kBAAkB,IAAI,MAAM,CAE3C;AAED,4BAA4B;AAC5B,wBAAgB,aAAa,IAAI,MAAM,CAEtC;AAED,iCAAiC;AACjC,wBAAgB,WAAW,IAAI,MAAM,CAEpC;AAED,qCAAqC;AACrC,wBAAgB,eAAe,IAAI,MAAM,CAExC;AAED,+BAA+B;AAC/B,wBAAgB,gBAAgB,IAAI,MAAM,CAEzC;AAQD,eAAO,MAAM,QAAQ,EAAE,MAAyC,CAAC;AACjE,eAAO,MAAM,eAAe,EAAE,MAA+C,CAAC;AAC9E,eAAO,MAAM,OAAO,EAAE,MAAoB,CAAC;AAG3C,eAAO,MAAM,aAAa,QAA+C,CAAC;AAI1E,6CAA6C;AAC7C,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAGxD;AAMD,6DAA6D;AAC7D,wBAAgB,WAAW,IAAI,MAAM,CASpC;AAED,iDAAiD;AACjD,wBAAgB,kBAAkB,IAAI,MAAM,CAE3C;AAED,8BAA8B;AAC9B,wBAAgB,aAAa,IAAI,MAAM,CAEtC;AAED,4BAA4B;AAC5B,wBAAgB,WAAW,IAAI,MAAM,CAEpC;AAED,gCAAgC;AAChC,wBAAgB,eAAe,IAAI,MAAM,CAExC;AAED,kCAAkC;AAClC,wBAAgB,WAAW,IAAI,MAAM,CAEpC;AAED,sDAAsD;AACtD,wBAAgB,SAAS,IAAI,MAAM,CAElC;AAED,6CAA6C;AAC7C,wBAAgB,aAAa,IAAI,MAAM,CAEtC;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,IAAI,MAAM,CAK7C;AAED;;GAEG;AACH,wBAAgB,kBAAkB,IAAI,MAAM,CAK3C;AAED;;;;;GAKG;AACH,wBAAgB,uBAAuB,IAAI,MAAM,CAKhD;AAED,qCAAqC;AACrC,wBAAgB,cAAc,IAAI,MAAM,CAEvC;AAED,iCAAiC;AACjC,wBAAgB,eAAe,IAAI,MAAM,CAExC","sourcesContent":["import { existsSync, readFileSync } from \"fs\";\nimport { homedir } from \"os\";\nimport { dirname, join, resolve } from \"path\";\nimport { fileURLToPath } from \"url\";\n\n// =============================================================================\n// Package Detection\n// =============================================================================\n\nconst __filename = fileURLToPath(import.meta.url);\nconst __dirname = dirname(__filename);\n\n/**\n * Detect if we're running as a Bun compiled binary.\n * Bun binaries have import.meta.url containing \"$bunfs\", \"~BUN\", or \"%7EBUN\" (Bun's virtual filesystem path)\n */\nexport const isBunBinary =\n\timport.meta.url.includes(\"$bunfs\") || import.meta.url.includes(\"~BUN\") || import.meta.url.includes(\"%7EBUN\");\n\n/** Detect if Bun is the runtime (compiled binary or bun run) */\nexport const isBunRuntime = !!process.versions.bun;\n\n// =============================================================================\n// Install Method Detection\n// =============================================================================\n\nexport type InstallMethod = \"bun-binary\" | \"npm\" | \"pnpm\" | \"yarn\" | \"bun\" | \"unknown\";\n\nexport function detectInstallMethod(): InstallMethod {\n\tif (isBunBinary) {\n\t\treturn \"bun-binary\";\n\t}\n\n\tconst resolvedPath = `${__dirname}\\0${process.execPath || \"\"}`.toLowerCase();\n\n\tif (resolvedPath.includes(\"/pnpm/\") || resolvedPath.includes(\"/.pnpm/\") || resolvedPath.includes(\"\\\\pnpm\\\\\")) {\n\t\treturn \"pnpm\";\n\t}\n\tif (resolvedPath.includes(\"/yarn/\") || resolvedPath.includes(\"/.yarn/\") || resolvedPath.includes(\"\\\\yarn\\\\\")) {\n\t\treturn \"yarn\";\n\t}\n\tif (isBunRuntime) {\n\t\treturn \"bun\";\n\t}\n\tif (resolvedPath.includes(\"/npm/\") || resolvedPath.includes(\"/node_modules/\") || resolvedPath.includes(\"\\\\npm\\\\\")) {\n\t\treturn \"npm\";\n\t}\n\n\treturn \"unknown\";\n}\n\nexport function getUpdateInstruction(packageName: string): string {\n\tconst method = detectInstallMethod();\n\tswitch (method) {\n\t\tcase \"bun-binary\":\n\t\t\treturn `Download from: https://github.com/badlogic/pi-mono/releases/latest`;\n\t\tcase \"pnpm\":\n\t\t\treturn `Run: pnpm install -g ${packageName}`;\n\t\tcase \"yarn\":\n\t\t\treturn `Run: yarn global add ${packageName}`;\n\t\tcase \"bun\":\n\t\t\treturn `Run: bun install -g ${packageName}`;\n\t\tcase \"npm\":\n\t\t\treturn `Run: npm install -g ${packageName}`;\n\t\tdefault:\n\t\t\treturn `Run: npm install -g ${packageName}`;\n\t}\n}\n\n// =============================================================================\n// Package Asset Paths (shipped with executable)\n// =============================================================================\n\n/**\n * Get the base directory for resolving package assets (themes, package.json, README.md, CHANGELOG.md).\n * - For Bun binary: returns the directory containing the executable\n * - For Node.js (dist/): returns __dirname (the dist/ directory)\n * - For tsx (src/): returns parent directory (the package root)\n */\nexport function getPackageDir(): string {\n\t// Allow override via environment variable (useful for Nix/Guix where store paths tokenize poorly)\n\tconst envDir = process.env.DRAHT_PACKAGE_DIR;\n\tif (envDir) {\n\t\tif (envDir === \"~\") return homedir();\n\t\tif (envDir.startsWith(\"~/\")) return homedir() + envDir.slice(1);\n\t\treturn envDir;\n\t}\n\n\tif (isBunBinary) {\n\t\t// Bun binary: process.execPath points to the compiled executable\n\t\treturn dirname(process.execPath);\n\t}\n\t// Node.js: walk up from __dirname until we find package.json\n\tlet dir = __dirname;\n\twhile (dir !== dirname(dir)) {\n\t\tif (existsSync(join(dir, \"package.json\"))) {\n\t\t\treturn dir;\n\t\t}\n\t\tdir = dirname(dir);\n\t}\n\t// Fallback (shouldn't happen)\n\treturn __dirname;\n}\n\n/**\n * Get path to built-in themes directory (shipped with package)\n * - For Bun binary: theme/ next to executable\n * - For Node.js (dist/): dist/modes/interactive/theme/\n * - For tsx (src/): src/modes/interactive/theme/\n */\nexport function getThemesDir(): string {\n\tif (isBunBinary) {\n\t\treturn join(dirname(process.execPath), \"theme\");\n\t}\n\t// Theme is in modes/interactive/theme/ relative to src/ or dist/\n\tconst packageDir = getPackageDir();\n\tconst srcOrDist = existsSync(join(packageDir, \"src\")) ? \"src\" : \"dist\";\n\treturn join(packageDir, srcOrDist, \"modes\", \"interactive\", \"theme\");\n}\n\n/**\n * Get path to HTML export template directory (shipped with package)\n * - For Bun binary: export-html/ next to executable\n * - For Node.js (dist/): dist/core/export-html/\n * - For tsx (src/): src/core/export-html/\n */\nexport function getExportTemplateDir(): string {\n\tif (isBunBinary) {\n\t\treturn join(dirname(process.execPath), \"export-html\");\n\t}\n\tconst packageDir = getPackageDir();\n\tconst srcOrDist = existsSync(join(packageDir, \"src\")) ? \"src\" : \"dist\";\n\treturn join(packageDir, srcOrDist, \"core\", \"export-html\");\n}\n\n/** Get path to package.json */\nexport function getPackageJsonPath(): string {\n\treturn join(getPackageDir(), \"package.json\");\n}\n\n/** Get path to README.md */\nexport function getReadmePath(): string {\n\treturn resolve(join(getPackageDir(), \"README.md\"));\n}\n\n/** Get path to docs directory */\nexport function getDocsPath(): string {\n\treturn resolve(join(getPackageDir(), \"docs\"));\n}\n\n/** Get path to examples directory */\nexport function getExamplesPath(): string {\n\treturn resolve(join(getPackageDir(), \"examples\"));\n}\n\n/** Get path to CHANGELOG.md */\nexport function getChangelogPath(): string {\n\treturn resolve(join(getPackageDir(), \"CHANGELOG.md\"));\n}\n\n// =============================================================================\n// App Config (from package.json drahtConfig)\n// =============================================================================\n\nconst pkg = JSON.parse(readFileSync(getPackageJsonPath(), \"utf-8\"));\n\nexport const APP_NAME: string = pkg.drahtConfig?.name || \"draht\";\nexport const CONFIG_DIR_NAME: string = pkg.drahtConfig?.configDir || \".draht\";\nexport const VERSION: string = pkg.version;\n\n// e.g., DRAHT_CODING_AGENT_DIR or TAU_CODING_AGENT_DIR\nexport const ENV_AGENT_DIR = `${APP_NAME.toUpperCase()}_CODING_AGENT_DIR`;\n\nconst DEFAULT_SHARE_VIEWER_URL = \"https://draht.dev/session/\";\n\n/** Get the share viewer URL for a gist ID */\nexport function getShareViewerUrl(gistId: string): string {\n\tconst baseUrl = process.env.DRAHT_SHARE_VIEWER_URL || DEFAULT_SHARE_VIEWER_URL;\n\treturn `${baseUrl}#${gistId}`;\n}\n\n// =============================================================================\n// User Config Paths (~/.draht/agent/*)\n// =============================================================================\n\n/** Get the agent config directory (e.g., ~/.draht/agent/) */\nexport function getAgentDir(): string {\n\tconst envDir = process.env[ENV_AGENT_DIR];\n\tif (envDir) {\n\t\t// Expand tilde to home directory\n\t\tif (envDir === \"~\") return homedir();\n\t\tif (envDir.startsWith(\"~/\")) return homedir() + envDir.slice(1);\n\t\treturn envDir;\n\t}\n\treturn join(homedir(), CONFIG_DIR_NAME, \"agent\");\n}\n\n/** Get path to user's custom themes directory */\nexport function getCustomThemesDir(): string {\n\treturn join(getAgentDir(), \"themes\");\n}\n\n/** Get path to models.json */\nexport function getModelsPath(): string {\n\treturn join(getAgentDir(), \"models.json\");\n}\n\n/** Get path to auth.json */\nexport function getAuthPath(): string {\n\treturn join(getAgentDir(), \"auth.json\");\n}\n\n/** Get path to settings.json */\nexport function getSettingsPath(): string {\n\treturn join(getAgentDir(), \"settings.json\");\n}\n\n/** Get path to tools directory */\nexport function getToolsDir(): string {\n\treturn join(getAgentDir(), \"tools\");\n}\n\n/** Get path to managed binaries directory (fd, rg) */\nexport function getBinDir(): string {\n\treturn join(getAgentDir(), \"bin\");\n}\n\n/** Get path to prompt templates directory */\nexport function getPromptsDir(): string {\n\treturn join(getAgentDir(), \"prompts\");\n}\n\n/**\n * Get path to shipped prompt templates (bundled with the package).\n * - For Bun binary: prompts/ next to executable\n * - For Node.js: prompts/ at package root\n */\nexport function getShippedPromptsDir(): string {\n\tif (isBunBinary) {\n\t\treturn join(dirname(process.execPath), \"prompts\");\n\t}\n\treturn join(getPackageDir(), \"prompts\");\n}\n\n/**\n * Get path to shipped hooks (bundled with the package).\n */\nexport function getShippedHooksDir(): string {\n\tif (isBunBinary) {\n\t\treturn join(dirname(process.execPath), \"hooks\");\n\t}\n\treturn join(getPackageDir(), \"hooks\");\n}\n\n/**\n * Get path to shipped extensions (bundled with the package).\n * These are loaded automatically for every user — batteries included.\n * - For Bun binary: extensions/ next to executable\n * - For Node.js: extensions/ at package root\n */\nexport function getShippedExtensionsDir(): string {\n\tif (isBunBinary) {\n\t\treturn join(dirname(process.execPath), \"extensions\");\n\t}\n\treturn join(getPackageDir(), \"extensions\");\n}\n\n/** Get path to sessions directory */\nexport function getSessionsDir(): string {\n\treturn join(getAgentDir(), \"sessions\");\n}\n\n/** Get path to debug log file */\nexport function getDebugLogPath(): string {\n\treturn join(getAgentDir(), `${APP_NAME}-debug.log`);\n}\n"]}
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAYA;;;GAGG;AACH,eAAO,MAAM,WAAW,SACqF,CAAC;AAE9G,gEAAgE;AAChE,eAAO,MAAM,YAAY,SAAyB,CAAC;AAMnD,MAAM,MAAM,aAAa,GAAG,YAAY,GAAG,KAAK,GAAG,MAAM,GAAG,MAAM,GAAG,KAAK,GAAG,SAAS,CAAC;AAEvF,wBAAgB,mBAAmB,IAAI,aAAa,CAqBnD;AAED,wBAAgB,oBAAoB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,CAgBhE;AAMD;;;;;GAKG;AACH,wBAAgB,aAAa,IAAI,MAAM,CAuBtC;AAED;;;;;GAKG;AACH,wBAAgB,YAAY,IAAI,MAAM,CAQrC;AAED;;;;;GAKG;AACH,wBAAgB,oBAAoB,IAAI,MAAM,CAO7C;AAED,+BAA+B;AAC/B,wBAAgB,kBAAkB,IAAI,MAAM,CAE3C;AAED,4BAA4B;AAC5B,wBAAgB,aAAa,IAAI,MAAM,CAEtC;AAED,iCAAiC;AACjC,wBAAgB,WAAW,IAAI,MAAM,CAEpC;AAED,qCAAqC;AACrC,wBAAgB,eAAe,IAAI,MAAM,CAExC;AAED,+BAA+B;AAC/B,wBAAgB,gBAAgB,IAAI,MAAM,CAEzC;AAQD,eAAO,MAAM,QAAQ,EAAE,MAAyC,CAAC;AACjE,eAAO,MAAM,eAAe,EAAE,MAA+C,CAAC;AAC9E,eAAO,MAAM,OAAO,EAAE,MAAwE,CAAC;AAG/F,eAAO,MAAM,aAAa,QAA+C,CAAC;AAI1E,6CAA6C;AAC7C,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAGxD;AAMD,6DAA6D;AAC7D,wBAAgB,WAAW,IAAI,MAAM,CASpC;AAED,iDAAiD;AACjD,wBAAgB,kBAAkB,IAAI,MAAM,CAE3C;AAED,8BAA8B;AAC9B,wBAAgB,aAAa,IAAI,MAAM,CAEtC;AAED,4BAA4B;AAC5B,wBAAgB,WAAW,IAAI,MAAM,CAEpC;AAED,gCAAgC;AAChC,wBAAgB,eAAe,IAAI,MAAM,CAExC;AAED,kCAAkC;AAClC,wBAAgB,WAAW,IAAI,MAAM,CAEpC;AAED,sDAAsD;AACtD,wBAAgB,SAAS,IAAI,MAAM,CAElC;AAED,6CAA6C;AAC7C,wBAAgB,aAAa,IAAI,MAAM,CAEtC;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,IAAI,MAAM,CAK7C;AAED;;GAEG;AACH,wBAAgB,kBAAkB,IAAI,MAAM,CAK3C;AAED;;;;;GAKG;AACH,wBAAgB,uBAAuB,IAAI,MAAM,CAKhD;AAED,qCAAqC;AACrC,wBAAgB,cAAc,IAAI,MAAM,CAEvC;AAED,iCAAiC;AACjC,wBAAgB,eAAe,IAAI,MAAM,CAExC","sourcesContent":["import { existsSync, readFileSync } from \"fs\";\nimport { homedir } from \"os\";\nimport { dirname, join, resolve } from \"path\";\nimport { fileURLToPath } from \"url\";\n\n// =============================================================================\n// Package Detection\n// =============================================================================\n\nconst __filename = fileURLToPath(import.meta.url);\nconst __dirname = dirname(__filename);\n\n/**\n * Detect if we're running as a Bun compiled binary.\n * Bun binaries have import.meta.url containing \"$bunfs\", \"~BUN\", or \"%7EBUN\" (Bun's virtual filesystem path)\n */\nexport const isBunBinary =\n\timport.meta.url.includes(\"$bunfs\") || import.meta.url.includes(\"~BUN\") || import.meta.url.includes(\"%7EBUN\");\n\n/** Detect if Bun is the runtime (compiled binary or bun run) */\nexport const isBunRuntime = !!process.versions.bun;\n\n// =============================================================================\n// Install Method Detection\n// =============================================================================\n\nexport type InstallMethod = \"bun-binary\" | \"npm\" | \"pnpm\" | \"yarn\" | \"bun\" | \"unknown\";\n\nexport function detectInstallMethod(): InstallMethod {\n\tif (isBunBinary) {\n\t\treturn \"bun-binary\";\n\t}\n\n\tconst resolvedPath = `${__dirname}\\0${process.execPath || \"\"}`.toLowerCase();\n\n\tif (resolvedPath.includes(\"/pnpm/\") || resolvedPath.includes(\"/.pnpm/\") || resolvedPath.includes(\"\\\\pnpm\\\\\")) {\n\t\treturn \"pnpm\";\n\t}\n\tif (resolvedPath.includes(\"/yarn/\") || resolvedPath.includes(\"/.yarn/\") || resolvedPath.includes(\"\\\\yarn\\\\\")) {\n\t\treturn \"yarn\";\n\t}\n\tif (isBunRuntime) {\n\t\treturn \"bun\";\n\t}\n\tif (resolvedPath.includes(\"/npm/\") || resolvedPath.includes(\"/node_modules/\") || resolvedPath.includes(\"\\\\npm\\\\\")) {\n\t\treturn \"npm\";\n\t}\n\n\treturn \"unknown\";\n}\n\nexport function getUpdateInstruction(packageName: string): string {\n\tconst method = detectInstallMethod();\n\tswitch (method) {\n\t\tcase \"bun-binary\":\n\t\t\treturn `Download from: https://github.com/draht-dev/draht/releases/latest`;\n\t\tcase \"pnpm\":\n\t\t\treturn `Run: pnpm install -g ${packageName}`;\n\t\tcase \"yarn\":\n\t\t\treturn `Run: yarn global add ${packageName}`;\n\t\tcase \"bun\":\n\t\t\treturn `Run: bun install -g ${packageName}`;\n\t\tcase \"npm\":\n\t\t\treturn `Run: npm install -g ${packageName}`;\n\t\tdefault:\n\t\t\treturn `Run: npm install -g ${packageName}`;\n\t}\n}\n\n// =============================================================================\n// Package Asset Paths (shipped with executable)\n// =============================================================================\n\n/**\n * Get the base directory for resolving package assets (themes, package.json, README.md, CHANGELOG.md).\n * - For Bun binary: returns the directory containing the executable\n * - For Node.js (dist/): returns __dirname (the dist/ directory)\n * - For tsx (src/): returns parent directory (the package root)\n */\nexport function getPackageDir(): string {\n\t// Allow override via environment variable (useful for Nix/Guix where store paths tokenize poorly)\n\tconst envDir = process.env.DRAHT_PACKAGE_DIR;\n\tif (envDir) {\n\t\tif (envDir === \"~\") return homedir();\n\t\tif (envDir.startsWith(\"~/\")) return homedir() + envDir.slice(1);\n\t\treturn envDir;\n\t}\n\n\tif (isBunBinary) {\n\t\t// Bun binary: process.execPath points to the compiled executable\n\t\treturn dirname(process.execPath);\n\t}\n\t// Node.js: walk up from __dirname until we find package.json\n\tlet dir = __dirname;\n\twhile (dir !== dirname(dir)) {\n\t\tif (existsSync(join(dir, \"package.json\"))) {\n\t\t\treturn dir;\n\t\t}\n\t\tdir = dirname(dir);\n\t}\n\t// Fallback (shouldn't happen)\n\treturn __dirname;\n}\n\n/**\n * Get path to built-in themes directory (shipped with package)\n * - For Bun binary: theme/ next to executable\n * - For Node.js (dist/): dist/modes/interactive/theme/\n * - For tsx (src/): src/modes/interactive/theme/\n */\nexport function getThemesDir(): string {\n\tif (isBunBinary) {\n\t\treturn join(dirname(process.execPath), \"theme\");\n\t}\n\t// Theme is in modes/interactive/theme/ relative to src/ or dist/\n\tconst packageDir = getPackageDir();\n\tconst srcOrDist = existsSync(join(packageDir, \"src\")) ? \"src\" : \"dist\";\n\treturn join(packageDir, srcOrDist, \"modes\", \"interactive\", \"theme\");\n}\n\n/**\n * Get path to HTML export template directory (shipped with package)\n * - For Bun binary: export-html/ next to executable\n * - For Node.js (dist/): dist/core/export-html/\n * - For tsx (src/): src/core/export-html/\n */\nexport function getExportTemplateDir(): string {\n\tif (isBunBinary) {\n\t\treturn join(dirname(process.execPath), \"export-html\");\n\t}\n\tconst packageDir = getPackageDir();\n\tconst srcOrDist = existsSync(join(packageDir, \"src\")) ? \"src\" : \"dist\";\n\treturn join(packageDir, srcOrDist, \"core\", \"export-html\");\n}\n\n/** Get path to package.json */\nexport function getPackageJsonPath(): string {\n\treturn join(getPackageDir(), \"package.json\");\n}\n\n/** Get path to README.md */\nexport function getReadmePath(): string {\n\treturn resolve(join(getPackageDir(), \"README.md\"));\n}\n\n/** Get path to docs directory */\nexport function getDocsPath(): string {\n\treturn resolve(join(getPackageDir(), \"docs\"));\n}\n\n/** Get path to examples directory */\nexport function getExamplesPath(): string {\n\treturn resolve(join(getPackageDir(), \"examples\"));\n}\n\n/** Get path to CHANGELOG.md */\nexport function getChangelogPath(): string {\n\treturn resolve(join(getPackageDir(), \"CHANGELOG.md\"));\n}\n\n// =============================================================================\n// App Config (from package.json drahtConfig)\n// =============================================================================\n\nconst pkg = JSON.parse(readFileSync(getPackageJsonPath(), \"utf-8\"));\n\nexport const APP_NAME: string = pkg.drahtConfig?.name || \"draht\";\nexport const CONFIG_DIR_NAME: string = pkg.drahtConfig?.configDir || \".draht\";\nexport const VERSION: string = existsSync(join(getPackageDir(), \".dev\")) ? \"dev\" : pkg.version;\n\n// e.g., DRAHT_CODING_AGENT_DIR or TAU_CODING_AGENT_DIR\nexport const ENV_AGENT_DIR = `${APP_NAME.toUpperCase()}_CODING_AGENT_DIR`;\n\nconst DEFAULT_SHARE_VIEWER_URL = \"https://draht.dev/session/\";\n\n/** Get the share viewer URL for a gist ID */\nexport function getShareViewerUrl(gistId: string): string {\n\tconst baseUrl = process.env.DRAHT_SHARE_VIEWER_URL || DEFAULT_SHARE_VIEWER_URL;\n\treturn `${baseUrl}#${gistId}`;\n}\n\n// =============================================================================\n// User Config Paths (~/.draht/agent/*)\n// =============================================================================\n\n/** Get the agent config directory (e.g., ~/.draht/agent/) */\nexport function getAgentDir(): string {\n\tconst envDir = process.env[ENV_AGENT_DIR];\n\tif (envDir) {\n\t\t// Expand tilde to home directory\n\t\tif (envDir === \"~\") return homedir();\n\t\tif (envDir.startsWith(\"~/\")) return homedir() + envDir.slice(1);\n\t\treturn envDir;\n\t}\n\treturn join(homedir(), CONFIG_DIR_NAME, \"agent\");\n}\n\n/** Get path to user's custom themes directory */\nexport function getCustomThemesDir(): string {\n\treturn join(getAgentDir(), \"themes\");\n}\n\n/** Get path to models.json */\nexport function getModelsPath(): string {\n\treturn join(getAgentDir(), \"models.json\");\n}\n\n/** Get path to auth.json */\nexport function getAuthPath(): string {\n\treturn join(getAgentDir(), \"auth.json\");\n}\n\n/** Get path to settings.json */\nexport function getSettingsPath(): string {\n\treturn join(getAgentDir(), \"settings.json\");\n}\n\n/** Get path to tools directory */\nexport function getToolsDir(): string {\n\treturn join(getAgentDir(), \"tools\");\n}\n\n/** Get path to managed binaries directory (fd, rg) */\nexport function getBinDir(): string {\n\treturn join(getAgentDir(), \"bin\");\n}\n\n/** Get path to prompt templates directory */\nexport function getPromptsDir(): string {\n\treturn join(getAgentDir(), \"prompts\");\n}\n\n/**\n * Get path to shipped prompt templates (bundled with the package).\n * - For Bun binary: prompts/ next to executable\n * - For Node.js: prompts/ at package root\n */\nexport function getShippedPromptsDir(): string {\n\tif (isBunBinary) {\n\t\treturn join(dirname(process.execPath), \"prompts\");\n\t}\n\treturn join(getPackageDir(), \"prompts\");\n}\n\n/**\n * Get path to shipped hooks (bundled with the package).\n */\nexport function getShippedHooksDir(): string {\n\tif (isBunBinary) {\n\t\treturn join(dirname(process.execPath), \"hooks\");\n\t}\n\treturn join(getPackageDir(), \"hooks\");\n}\n\n/**\n * Get path to shipped extensions (bundled with the package).\n * These are loaded automatically for every user — batteries included.\n * - For Bun binary: extensions/ next to executable\n * - For Node.js: extensions/ at package root\n */\nexport function getShippedExtensionsDir(): string {\n\tif (isBunBinary) {\n\t\treturn join(dirname(process.execPath), \"extensions\");\n\t}\n\treturn join(getPackageDir(), \"extensions\");\n}\n\n/** Get path to sessions directory */\nexport function getSessionsDir(): string {\n\treturn join(getAgentDir(), \"sessions\");\n}\n\n/** Get path to debug log file */\nexport function getDebugLogPath(): string {\n\treturn join(getAgentDir(), `${APP_NAME}-debug.log`);\n}\n"]}
package/dist/config.js CHANGED
@@ -37,7 +37,7 @@ export function getUpdateInstruction(packageName) {
37
37
  const method = detectInstallMethod();
38
38
  switch (method) {
39
39
  case "bun-binary":
40
- return `Download from: https://github.com/badlogic/pi-mono/releases/latest`;
40
+ return `Download from: https://github.com/draht-dev/draht/releases/latest`;
41
41
  case "pnpm":
42
42
  return `Run: pnpm install -g ${packageName}`;
43
43
  case "yarn":
@@ -139,7 +139,7 @@ export function getChangelogPath() {
139
139
  const pkg = JSON.parse(readFileSync(getPackageJsonPath(), "utf-8"));
140
140
  export const APP_NAME = pkg.drahtConfig?.name || "draht";
141
141
  export const CONFIG_DIR_NAME = pkg.drahtConfig?.configDir || ".draht";
142
- export const VERSION = pkg.version;
142
+ export const VERSION = existsSync(join(getPackageDir(), ".dev")) ? "dev" : pkg.version;
143
143
  // e.g., DRAHT_CODING_AGENT_DIR or TAU_CODING_AGENT_DIR
144
144
  export const ENV_AGENT_DIR = `${APP_NAME.toUpperCase()}_CODING_AGENT_DIR`;
145
145
  const DEFAULT_SHARE_VIEWER_URL = "https://draht.dev/session/";
@@ -1 +1 @@
1
- {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"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","sourcesContent":["import { existsSync, readFileSync } from \"fs\";\nimport { homedir } from \"os\";\nimport { dirname, join, resolve } from \"path\";\nimport { fileURLToPath } from \"url\";\n\n// =============================================================================\n// Package Detection\n// =============================================================================\n\nconst __filename = fileURLToPath(import.meta.url);\nconst __dirname = dirname(__filename);\n\n/**\n * Detect if we're running as a Bun compiled binary.\n * Bun binaries have import.meta.url containing \"$bunfs\", \"~BUN\", or \"%7EBUN\" (Bun's virtual filesystem path)\n */\nexport const isBunBinary =\n\timport.meta.url.includes(\"$bunfs\") || import.meta.url.includes(\"~BUN\") || import.meta.url.includes(\"%7EBUN\");\n\n/** Detect if Bun is the runtime (compiled binary or bun run) */\nexport const isBunRuntime = !!process.versions.bun;\n\n// =============================================================================\n// Install Method Detection\n// =============================================================================\n\nexport type InstallMethod = \"bun-binary\" | \"npm\" | \"pnpm\" | \"yarn\" | \"bun\" | \"unknown\";\n\nexport function detectInstallMethod(): InstallMethod {\n\tif (isBunBinary) {\n\t\treturn \"bun-binary\";\n\t}\n\n\tconst resolvedPath = `${__dirname}\\0${process.execPath || \"\"}`.toLowerCase();\n\n\tif (resolvedPath.includes(\"/pnpm/\") || resolvedPath.includes(\"/.pnpm/\") || resolvedPath.includes(\"\\\\pnpm\\\\\")) {\n\t\treturn \"pnpm\";\n\t}\n\tif (resolvedPath.includes(\"/yarn/\") || resolvedPath.includes(\"/.yarn/\") || resolvedPath.includes(\"\\\\yarn\\\\\")) {\n\t\treturn \"yarn\";\n\t}\n\tif (isBunRuntime) {\n\t\treturn \"bun\";\n\t}\n\tif (resolvedPath.includes(\"/npm/\") || resolvedPath.includes(\"/node_modules/\") || resolvedPath.includes(\"\\\\npm\\\\\")) {\n\t\treturn \"npm\";\n\t}\n\n\treturn \"unknown\";\n}\n\nexport function getUpdateInstruction(packageName: string): string {\n\tconst method = detectInstallMethod();\n\tswitch (method) {\n\t\tcase \"bun-binary\":\n\t\t\treturn `Download from: https://github.com/badlogic/pi-mono/releases/latest`;\n\t\tcase \"pnpm\":\n\t\t\treturn `Run: pnpm install -g ${packageName}`;\n\t\tcase \"yarn\":\n\t\t\treturn `Run: yarn global add ${packageName}`;\n\t\tcase \"bun\":\n\t\t\treturn `Run: bun install -g ${packageName}`;\n\t\tcase \"npm\":\n\t\t\treturn `Run: npm install -g ${packageName}`;\n\t\tdefault:\n\t\t\treturn `Run: npm install -g ${packageName}`;\n\t}\n}\n\n// =============================================================================\n// Package Asset Paths (shipped with executable)\n// =============================================================================\n\n/**\n * Get the base directory for resolving package assets (themes, package.json, README.md, CHANGELOG.md).\n * - For Bun binary: returns the directory containing the executable\n * - For Node.js (dist/): returns __dirname (the dist/ directory)\n * - For tsx (src/): returns parent directory (the package root)\n */\nexport function getPackageDir(): string {\n\t// Allow override via environment variable (useful for Nix/Guix where store paths tokenize poorly)\n\tconst envDir = process.env.DRAHT_PACKAGE_DIR;\n\tif (envDir) {\n\t\tif (envDir === \"~\") return homedir();\n\t\tif (envDir.startsWith(\"~/\")) return homedir() + envDir.slice(1);\n\t\treturn envDir;\n\t}\n\n\tif (isBunBinary) {\n\t\t// Bun binary: process.execPath points to the compiled executable\n\t\treturn dirname(process.execPath);\n\t}\n\t// Node.js: walk up from __dirname until we find package.json\n\tlet dir = __dirname;\n\twhile (dir !== dirname(dir)) {\n\t\tif (existsSync(join(dir, \"package.json\"))) {\n\t\t\treturn dir;\n\t\t}\n\t\tdir = dirname(dir);\n\t}\n\t// Fallback (shouldn't happen)\n\treturn __dirname;\n}\n\n/**\n * Get path to built-in themes directory (shipped with package)\n * - For Bun binary: theme/ next to executable\n * - For Node.js (dist/): dist/modes/interactive/theme/\n * - For tsx (src/): src/modes/interactive/theme/\n */\nexport function getThemesDir(): string {\n\tif (isBunBinary) {\n\t\treturn join(dirname(process.execPath), \"theme\");\n\t}\n\t// Theme is in modes/interactive/theme/ relative to src/ or dist/\n\tconst packageDir = getPackageDir();\n\tconst srcOrDist = existsSync(join(packageDir, \"src\")) ? \"src\" : \"dist\";\n\treturn join(packageDir, srcOrDist, \"modes\", \"interactive\", \"theme\");\n}\n\n/**\n * Get path to HTML export template directory (shipped with package)\n * - For Bun binary: export-html/ next to executable\n * - For Node.js (dist/): dist/core/export-html/\n * - For tsx (src/): src/core/export-html/\n */\nexport function getExportTemplateDir(): string {\n\tif (isBunBinary) {\n\t\treturn join(dirname(process.execPath), \"export-html\");\n\t}\n\tconst packageDir = getPackageDir();\n\tconst srcOrDist = existsSync(join(packageDir, \"src\")) ? \"src\" : \"dist\";\n\treturn join(packageDir, srcOrDist, \"core\", \"export-html\");\n}\n\n/** Get path to package.json */\nexport function getPackageJsonPath(): string {\n\treturn join(getPackageDir(), \"package.json\");\n}\n\n/** Get path to README.md */\nexport function getReadmePath(): string {\n\treturn resolve(join(getPackageDir(), \"README.md\"));\n}\n\n/** Get path to docs directory */\nexport function getDocsPath(): string {\n\treturn resolve(join(getPackageDir(), \"docs\"));\n}\n\n/** Get path to examples directory */\nexport function getExamplesPath(): string {\n\treturn resolve(join(getPackageDir(), \"examples\"));\n}\n\n/** Get path to CHANGELOG.md */\nexport function getChangelogPath(): string {\n\treturn resolve(join(getPackageDir(), \"CHANGELOG.md\"));\n}\n\n// =============================================================================\n// App Config (from package.json drahtConfig)\n// =============================================================================\n\nconst pkg = JSON.parse(readFileSync(getPackageJsonPath(), \"utf-8\"));\n\nexport const APP_NAME: string = pkg.drahtConfig?.name || \"draht\";\nexport const CONFIG_DIR_NAME: string = pkg.drahtConfig?.configDir || \".draht\";\nexport const VERSION: string = pkg.version;\n\n// e.g., DRAHT_CODING_AGENT_DIR or TAU_CODING_AGENT_DIR\nexport const ENV_AGENT_DIR = `${APP_NAME.toUpperCase()}_CODING_AGENT_DIR`;\n\nconst DEFAULT_SHARE_VIEWER_URL = \"https://draht.dev/session/\";\n\n/** Get the share viewer URL for a gist ID */\nexport function getShareViewerUrl(gistId: string): string {\n\tconst baseUrl = process.env.DRAHT_SHARE_VIEWER_URL || DEFAULT_SHARE_VIEWER_URL;\n\treturn `${baseUrl}#${gistId}`;\n}\n\n// =============================================================================\n// User Config Paths (~/.draht/agent/*)\n// =============================================================================\n\n/** Get the agent config directory (e.g., ~/.draht/agent/) */\nexport function getAgentDir(): string {\n\tconst envDir = process.env[ENV_AGENT_DIR];\n\tif (envDir) {\n\t\t// Expand tilde to home directory\n\t\tif (envDir === \"~\") return homedir();\n\t\tif (envDir.startsWith(\"~/\")) return homedir() + envDir.slice(1);\n\t\treturn envDir;\n\t}\n\treturn join(homedir(), CONFIG_DIR_NAME, \"agent\");\n}\n\n/** Get path to user's custom themes directory */\nexport function getCustomThemesDir(): string {\n\treturn join(getAgentDir(), \"themes\");\n}\n\n/** Get path to models.json */\nexport function getModelsPath(): string {\n\treturn join(getAgentDir(), \"models.json\");\n}\n\n/** Get path to auth.json */\nexport function getAuthPath(): string {\n\treturn join(getAgentDir(), \"auth.json\");\n}\n\n/** Get path to settings.json */\nexport function getSettingsPath(): string {\n\treturn join(getAgentDir(), \"settings.json\");\n}\n\n/** Get path to tools directory */\nexport function getToolsDir(): string {\n\treturn join(getAgentDir(), \"tools\");\n}\n\n/** Get path to managed binaries directory (fd, rg) */\nexport function getBinDir(): string {\n\treturn join(getAgentDir(), \"bin\");\n}\n\n/** Get path to prompt templates directory */\nexport function getPromptsDir(): string {\n\treturn join(getAgentDir(), \"prompts\");\n}\n\n/**\n * Get path to shipped prompt templates (bundled with the package).\n * - For Bun binary: prompts/ next to executable\n * - For Node.js: prompts/ at package root\n */\nexport function getShippedPromptsDir(): string {\n\tif (isBunBinary) {\n\t\treturn join(dirname(process.execPath), \"prompts\");\n\t}\n\treturn join(getPackageDir(), \"prompts\");\n}\n\n/**\n * Get path to shipped hooks (bundled with the package).\n */\nexport function getShippedHooksDir(): string {\n\tif (isBunBinary) {\n\t\treturn join(dirname(process.execPath), \"hooks\");\n\t}\n\treturn join(getPackageDir(), \"hooks\");\n}\n\n/**\n * Get path to shipped extensions (bundled with the package).\n * These are loaded automatically for every user — batteries included.\n * - For Bun binary: extensions/ next to executable\n * - For Node.js: extensions/ at package root\n */\nexport function getShippedExtensionsDir(): string {\n\tif (isBunBinary) {\n\t\treturn join(dirname(process.execPath), \"extensions\");\n\t}\n\treturn join(getPackageDir(), \"extensions\");\n}\n\n/** Get path to sessions directory */\nexport function getSessionsDir(): string {\n\treturn join(getAgentDir(), \"sessions\");\n}\n\n/** Get path to debug log file */\nexport function getDebugLogPath(): string {\n\treturn join(getAgentDir(), `${APP_NAME}-debug.log`);\n}\n"]}
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"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","sourcesContent":["import { existsSync, readFileSync } from \"fs\";\nimport { homedir } from \"os\";\nimport { dirname, join, resolve } from \"path\";\nimport { fileURLToPath } from \"url\";\n\n// =============================================================================\n// Package Detection\n// =============================================================================\n\nconst __filename = fileURLToPath(import.meta.url);\nconst __dirname = dirname(__filename);\n\n/**\n * Detect if we're running as a Bun compiled binary.\n * Bun binaries have import.meta.url containing \"$bunfs\", \"~BUN\", or \"%7EBUN\" (Bun's virtual filesystem path)\n */\nexport const isBunBinary =\n\timport.meta.url.includes(\"$bunfs\") || import.meta.url.includes(\"~BUN\") || import.meta.url.includes(\"%7EBUN\");\n\n/** Detect if Bun is the runtime (compiled binary or bun run) */\nexport const isBunRuntime = !!process.versions.bun;\n\n// =============================================================================\n// Install Method Detection\n// =============================================================================\n\nexport type InstallMethod = \"bun-binary\" | \"npm\" | \"pnpm\" | \"yarn\" | \"bun\" | \"unknown\";\n\nexport function detectInstallMethod(): InstallMethod {\n\tif (isBunBinary) {\n\t\treturn \"bun-binary\";\n\t}\n\n\tconst resolvedPath = `${__dirname}\\0${process.execPath || \"\"}`.toLowerCase();\n\n\tif (resolvedPath.includes(\"/pnpm/\") || resolvedPath.includes(\"/.pnpm/\") || resolvedPath.includes(\"\\\\pnpm\\\\\")) {\n\t\treturn \"pnpm\";\n\t}\n\tif (resolvedPath.includes(\"/yarn/\") || resolvedPath.includes(\"/.yarn/\") || resolvedPath.includes(\"\\\\yarn\\\\\")) {\n\t\treturn \"yarn\";\n\t}\n\tif (isBunRuntime) {\n\t\treturn \"bun\";\n\t}\n\tif (resolvedPath.includes(\"/npm/\") || resolvedPath.includes(\"/node_modules/\") || resolvedPath.includes(\"\\\\npm\\\\\")) {\n\t\treturn \"npm\";\n\t}\n\n\treturn \"unknown\";\n}\n\nexport function getUpdateInstruction(packageName: string): string {\n\tconst method = detectInstallMethod();\n\tswitch (method) {\n\t\tcase \"bun-binary\":\n\t\t\treturn `Download from: https://github.com/draht-dev/draht/releases/latest`;\n\t\tcase \"pnpm\":\n\t\t\treturn `Run: pnpm install -g ${packageName}`;\n\t\tcase \"yarn\":\n\t\t\treturn `Run: yarn global add ${packageName}`;\n\t\tcase \"bun\":\n\t\t\treturn `Run: bun install -g ${packageName}`;\n\t\tcase \"npm\":\n\t\t\treturn `Run: npm install -g ${packageName}`;\n\t\tdefault:\n\t\t\treturn `Run: npm install -g ${packageName}`;\n\t}\n}\n\n// =============================================================================\n// Package Asset Paths (shipped with executable)\n// =============================================================================\n\n/**\n * Get the base directory for resolving package assets (themes, package.json, README.md, CHANGELOG.md).\n * - For Bun binary: returns the directory containing the executable\n * - For Node.js (dist/): returns __dirname (the dist/ directory)\n * - For tsx (src/): returns parent directory (the package root)\n */\nexport function getPackageDir(): string {\n\t// Allow override via environment variable (useful for Nix/Guix where store paths tokenize poorly)\n\tconst envDir = process.env.DRAHT_PACKAGE_DIR;\n\tif (envDir) {\n\t\tif (envDir === \"~\") return homedir();\n\t\tif (envDir.startsWith(\"~/\")) return homedir() + envDir.slice(1);\n\t\treturn envDir;\n\t}\n\n\tif (isBunBinary) {\n\t\t// Bun binary: process.execPath points to the compiled executable\n\t\treturn dirname(process.execPath);\n\t}\n\t// Node.js: walk up from __dirname until we find package.json\n\tlet dir = __dirname;\n\twhile (dir !== dirname(dir)) {\n\t\tif (existsSync(join(dir, \"package.json\"))) {\n\t\t\treturn dir;\n\t\t}\n\t\tdir = dirname(dir);\n\t}\n\t// Fallback (shouldn't happen)\n\treturn __dirname;\n}\n\n/**\n * Get path to built-in themes directory (shipped with package)\n * - For Bun binary: theme/ next to executable\n * - For Node.js (dist/): dist/modes/interactive/theme/\n * - For tsx (src/): src/modes/interactive/theme/\n */\nexport function getThemesDir(): string {\n\tif (isBunBinary) {\n\t\treturn join(dirname(process.execPath), \"theme\");\n\t}\n\t// Theme is in modes/interactive/theme/ relative to src/ or dist/\n\tconst packageDir = getPackageDir();\n\tconst srcOrDist = existsSync(join(packageDir, \"src\")) ? \"src\" : \"dist\";\n\treturn join(packageDir, srcOrDist, \"modes\", \"interactive\", \"theme\");\n}\n\n/**\n * Get path to HTML export template directory (shipped with package)\n * - For Bun binary: export-html/ next to executable\n * - For Node.js (dist/): dist/core/export-html/\n * - For tsx (src/): src/core/export-html/\n */\nexport function getExportTemplateDir(): string {\n\tif (isBunBinary) {\n\t\treturn join(dirname(process.execPath), \"export-html\");\n\t}\n\tconst packageDir = getPackageDir();\n\tconst srcOrDist = existsSync(join(packageDir, \"src\")) ? \"src\" : \"dist\";\n\treturn join(packageDir, srcOrDist, \"core\", \"export-html\");\n}\n\n/** Get path to package.json */\nexport function getPackageJsonPath(): string {\n\treturn join(getPackageDir(), \"package.json\");\n}\n\n/** Get path to README.md */\nexport function getReadmePath(): string {\n\treturn resolve(join(getPackageDir(), \"README.md\"));\n}\n\n/** Get path to docs directory */\nexport function getDocsPath(): string {\n\treturn resolve(join(getPackageDir(), \"docs\"));\n}\n\n/** Get path to examples directory */\nexport function getExamplesPath(): string {\n\treturn resolve(join(getPackageDir(), \"examples\"));\n}\n\n/** Get path to CHANGELOG.md */\nexport function getChangelogPath(): string {\n\treturn resolve(join(getPackageDir(), \"CHANGELOG.md\"));\n}\n\n// =============================================================================\n// App Config (from package.json drahtConfig)\n// =============================================================================\n\nconst pkg = JSON.parse(readFileSync(getPackageJsonPath(), \"utf-8\"));\n\nexport const APP_NAME: string = pkg.drahtConfig?.name || \"draht\";\nexport const CONFIG_DIR_NAME: string = pkg.drahtConfig?.configDir || \".draht\";\nexport const VERSION: string = existsSync(join(getPackageDir(), \".dev\")) ? \"dev\" : pkg.version;\n\n// e.g., DRAHT_CODING_AGENT_DIR or TAU_CODING_AGENT_DIR\nexport const ENV_AGENT_DIR = `${APP_NAME.toUpperCase()}_CODING_AGENT_DIR`;\n\nconst DEFAULT_SHARE_VIEWER_URL = \"https://draht.dev/session/\";\n\n/** Get the share viewer URL for a gist ID */\nexport function getShareViewerUrl(gistId: string): string {\n\tconst baseUrl = process.env.DRAHT_SHARE_VIEWER_URL || DEFAULT_SHARE_VIEWER_URL;\n\treturn `${baseUrl}#${gistId}`;\n}\n\n// =============================================================================\n// User Config Paths (~/.draht/agent/*)\n// =============================================================================\n\n/** Get the agent config directory (e.g., ~/.draht/agent/) */\nexport function getAgentDir(): string {\n\tconst envDir = process.env[ENV_AGENT_DIR];\n\tif (envDir) {\n\t\t// Expand tilde to home directory\n\t\tif (envDir === \"~\") return homedir();\n\t\tif (envDir.startsWith(\"~/\")) return homedir() + envDir.slice(1);\n\t\treturn envDir;\n\t}\n\treturn join(homedir(), CONFIG_DIR_NAME, \"agent\");\n}\n\n/** Get path to user's custom themes directory */\nexport function getCustomThemesDir(): string {\n\treturn join(getAgentDir(), \"themes\");\n}\n\n/** Get path to models.json */\nexport function getModelsPath(): string {\n\treturn join(getAgentDir(), \"models.json\");\n}\n\n/** Get path to auth.json */\nexport function getAuthPath(): string {\n\treturn join(getAgentDir(), \"auth.json\");\n}\n\n/** Get path to settings.json */\nexport function getSettingsPath(): string {\n\treturn join(getAgentDir(), \"settings.json\");\n}\n\n/** Get path to tools directory */\nexport function getToolsDir(): string {\n\treturn join(getAgentDir(), \"tools\");\n}\n\n/** Get path to managed binaries directory (fd, rg) */\nexport function getBinDir(): string {\n\treturn join(getAgentDir(), \"bin\");\n}\n\n/** Get path to prompt templates directory */\nexport function getPromptsDir(): string {\n\treturn join(getAgentDir(), \"prompts\");\n}\n\n/**\n * Get path to shipped prompt templates (bundled with the package).\n * - For Bun binary: prompts/ next to executable\n * - For Node.js: prompts/ at package root\n */\nexport function getShippedPromptsDir(): string {\n\tif (isBunBinary) {\n\t\treturn join(dirname(process.execPath), \"prompts\");\n\t}\n\treturn join(getPackageDir(), \"prompts\");\n}\n\n/**\n * Get path to shipped hooks (bundled with the package).\n */\nexport function getShippedHooksDir(): string {\n\tif (isBunBinary) {\n\t\treturn join(dirname(process.execPath), \"hooks\");\n\t}\n\treturn join(getPackageDir(), \"hooks\");\n}\n\n/**\n * Get path to shipped extensions (bundled with the package).\n * These are loaded automatically for every user — batteries included.\n * - For Bun binary: extensions/ next to executable\n * - For Node.js: extensions/ at package root\n */\nexport function getShippedExtensionsDir(): string {\n\tif (isBunBinary) {\n\t\treturn join(dirname(process.execPath), \"extensions\");\n\t}\n\treturn join(getPackageDir(), \"extensions\");\n}\n\n/** Get path to sessions directory */\nexport function getSessionsDir(): string {\n\treturn join(getAgentDir(), \"sessions\");\n}\n\n/** Get path to debug log file */\nexport function getDebugLogPath(): string {\n\treturn join(getAgentDir(), `${APP_NAME}-debug.log`);\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"package-manager.d.ts","sourceRoot":"","sources":["../../src/core/package-manager.ts"],"names":[],"mappings":"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","sourcesContent":["import { spawn, spawnSync } from \"node:child_process\";\nimport { createHash } from \"node:crypto\";\nimport { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from \"node:fs\";\nimport { homedir, tmpdir } from \"node:os\";\nimport { basename, dirname, join, relative, resolve, sep } from \"node:path\";\nimport ignore from \"ignore\";\nimport { minimatch } from \"minimatch\";\nimport { CONFIG_DIR_NAME } from \"../config.js\";\nimport { type GitSource, parseGitUrl } from \"../utils/git.js\";\nimport type { PackageSource, SettingsManager } from \"./settings-manager.js\";\n\nconst NETWORK_TIMEOUT_MS = 10000;\n\nfunction isOfflineModeEnabled(): boolean {\n\tconst value = process.env.DRAHT_OFFLINE;\n\tif (!value) return false;\n\treturn value === \"1\" || value.toLowerCase() === \"true\" || value.toLowerCase() === \"yes\";\n}\n\nexport interface PathMetadata {\n\tsource: string;\n\tscope: SourceScope;\n\torigin: \"package\" | \"top-level\";\n\tbaseDir?: string;\n}\n\nexport interface ResolvedResource {\n\tpath: string;\n\tenabled: boolean;\n\tmetadata: PathMetadata;\n}\n\nexport interface ResolvedPaths {\n\textensions: ResolvedResource[];\n\tskills: ResolvedResource[];\n\tprompts: ResolvedResource[];\n\tthemes: ResolvedResource[];\n}\n\nexport type MissingSourceAction = \"install\" | \"skip\" | \"error\";\n\nexport interface ProgressEvent {\n\ttype: \"start\" | \"progress\" | \"complete\" | \"error\";\n\taction: \"install\" | \"remove\" | \"update\" | \"clone\" | \"pull\";\n\tsource: string;\n\tmessage?: string;\n}\n\nexport type ProgressCallback = (event: ProgressEvent) => void;\n\nexport interface PackageManager {\n\tresolve(onMissing?: (source: string) => Promise<MissingSourceAction>): Promise<ResolvedPaths>;\n\tinstall(source: string, options?: { local?: boolean }): Promise<void>;\n\tremove(source: string, options?: { local?: boolean }): Promise<void>;\n\tupdate(source?: string): Promise<void>;\n\tresolveExtensionSources(\n\t\tsources: string[],\n\t\toptions?: { local?: boolean; temporary?: boolean },\n\t): Promise<ResolvedPaths>;\n\taddSourceToSettings(source: string, options?: { local?: boolean }): boolean;\n\tremoveSourceFromSettings(source: string, options?: { local?: boolean }): boolean;\n\tsetProgressCallback(callback: ProgressCallback | undefined): void;\n\tgetInstalledPath(source: string, scope: \"user\" | \"project\"): string | undefined;\n}\n\ninterface PackageManagerOptions {\n\tcwd: string;\n\tagentDir: string;\n\tsettingsManager: SettingsManager;\n}\n\ntype SourceScope = \"user\" | \"project\" | \"temporary\";\n\ntype NpmSource = {\n\ttype: \"npm\";\n\tspec: string;\n\tname: string;\n\tpinned: boolean;\n};\n\ntype LocalSource = {\n\ttype: \"local\";\n\tpath: string;\n};\n\ntype ParsedSource = NpmSource | GitSource | LocalSource;\n\ninterface DrahtManifest {\n\textensions?: string[];\n\tskills?: string[];\n\tprompts?: string[];\n\tthemes?: string[];\n}\n\ninterface ResourceAccumulator {\n\textensions: Map<string, { metadata: PathMetadata; enabled: boolean }>;\n\tskills: Map<string, { metadata: PathMetadata; enabled: boolean }>;\n\tprompts: Map<string, { metadata: PathMetadata; enabled: boolean }>;\n\tthemes: Map<string, { metadata: PathMetadata; enabled: boolean }>;\n}\n\ninterface PackageFilter {\n\textensions?: string[];\n\tskills?: string[];\n\tprompts?: string[];\n\tthemes?: string[];\n}\n\ntype ResourceType = \"extensions\" | \"skills\" | \"prompts\" | \"themes\";\n\nconst RESOURCE_TYPES: ResourceType[] = [\"extensions\", \"skills\", \"prompts\", \"themes\"];\n\nconst FILE_PATTERNS: Record<ResourceType, RegExp> = {\n\textensions: /\\.(ts|js)$/,\n\tskills: /\\.md$/,\n\tprompts: /\\.md$/,\n\tthemes: /\\.json$/,\n};\n\nconst IGNORE_FILE_NAMES = [\".gitignore\", \".ignore\", \".fdignore\"];\n\ntype IgnoreMatcher = ReturnType<typeof ignore>;\n\nfunction toPosixPath(p: string): string {\n\treturn p.split(sep).join(\"/\");\n}\n\nfunction prefixIgnorePattern(line: string, prefix: string): string | null {\n\tconst trimmed = line.trim();\n\tif (!trimmed) return null;\n\tif (trimmed.startsWith(\"#\") && !trimmed.startsWith(\"\\\\#\")) return null;\n\n\tlet pattern = line;\n\tlet negated = false;\n\n\tif (pattern.startsWith(\"!\")) {\n\t\tnegated = true;\n\t\tpattern = pattern.slice(1);\n\t} else if (pattern.startsWith(\"\\\\!\")) {\n\t\tpattern = pattern.slice(1);\n\t}\n\n\tif (pattern.startsWith(\"/\")) {\n\t\tpattern = pattern.slice(1);\n\t}\n\n\tconst prefixed = prefix ? `${prefix}${pattern}` : pattern;\n\treturn negated ? `!${prefixed}` : prefixed;\n}\n\nfunction addIgnoreRules(ig: IgnoreMatcher, dir: string, rootDir: string): void {\n\tconst relativeDir = relative(rootDir, dir);\n\tconst prefix = relativeDir ? `${toPosixPath(relativeDir)}/` : \"\";\n\n\tfor (const filename of IGNORE_FILE_NAMES) {\n\t\tconst ignorePath = join(dir, filename);\n\t\tif (!existsSync(ignorePath)) continue;\n\t\ttry {\n\t\t\tconst content = readFileSync(ignorePath, \"utf-8\");\n\t\t\tconst patterns = content\n\t\t\t\t.split(/\\r?\\n/)\n\t\t\t\t.map((line) => prefixIgnorePattern(line, prefix))\n\t\t\t\t.filter((line): line is string => Boolean(line));\n\t\t\tif (patterns.length > 0) {\n\t\t\t\tig.add(patterns);\n\t\t\t}\n\t\t} catch {}\n\t}\n}\n\nfunction isPattern(s: string): boolean {\n\treturn s.startsWith(\"!\") || s.startsWith(\"+\") || s.startsWith(\"-\") || s.includes(\"*\") || s.includes(\"?\");\n}\n\nfunction splitPatterns(entries: string[]): { plain: string[]; patterns: string[] } {\n\tconst plain: string[] = [];\n\tconst patterns: string[] = [];\n\tfor (const entry of entries) {\n\t\tif (isPattern(entry)) {\n\t\t\tpatterns.push(entry);\n\t\t} else {\n\t\t\tplain.push(entry);\n\t\t}\n\t}\n\treturn { plain, patterns };\n}\n\nfunction collectFiles(\n\tdir: string,\n\tfilePattern: RegExp,\n\tskipNodeModules = true,\n\tignoreMatcher?: IgnoreMatcher,\n\trootDir?: string,\n): string[] {\n\tconst files: string[] = [];\n\tif (!existsSync(dir)) return files;\n\n\tconst root = rootDir ?? dir;\n\tconst ig = ignoreMatcher ?? ignore();\n\taddIgnoreRules(ig, dir, root);\n\n\ttry {\n\t\tconst entries = readdirSync(dir, { withFileTypes: true });\n\t\tfor (const entry of entries) {\n\t\t\tif (entry.name.startsWith(\".\")) continue;\n\t\t\tif (skipNodeModules && entry.name === \"node_modules\") continue;\n\n\t\t\tconst fullPath = join(dir, entry.name);\n\t\t\tlet isDir = entry.isDirectory();\n\t\t\tlet isFile = entry.isFile();\n\n\t\t\tif (entry.isSymbolicLink()) {\n\t\t\t\ttry {\n\t\t\t\t\tconst stats = statSync(fullPath);\n\t\t\t\t\tisDir = stats.isDirectory();\n\t\t\t\t\tisFile = stats.isFile();\n\t\t\t\t} catch {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tconst relPath = toPosixPath(relative(root, fullPath));\n\t\t\tconst ignorePath = isDir ? `${relPath}/` : relPath;\n\t\t\tif (ig.ignores(ignorePath)) continue;\n\n\t\t\tif (isDir) {\n\t\t\t\tfiles.push(...collectFiles(fullPath, filePattern, skipNodeModules, ig, root));\n\t\t\t} else if (isFile && filePattern.test(entry.name)) {\n\t\t\t\tfiles.push(fullPath);\n\t\t\t}\n\t\t}\n\t} catch {\n\t\t// Ignore errors\n\t}\n\n\treturn files;\n}\n\nfunction collectSkillEntries(\n\tdir: string,\n\tincludeRootFiles = true,\n\tignoreMatcher?: IgnoreMatcher,\n\trootDir?: string,\n): string[] {\n\tconst entries: string[] = [];\n\tif (!existsSync(dir)) return entries;\n\n\tconst root = rootDir ?? dir;\n\tconst ig = ignoreMatcher ?? ignore();\n\taddIgnoreRules(ig, dir, root);\n\n\ttry {\n\t\tconst dirEntries = readdirSync(dir, { withFileTypes: true });\n\t\tfor (const entry of dirEntries) {\n\t\t\tif (entry.name.startsWith(\".\")) continue;\n\t\t\tif (entry.name === \"node_modules\") continue;\n\n\t\t\tconst fullPath = join(dir, entry.name);\n\t\t\tlet isDir = entry.isDirectory();\n\t\t\tlet isFile = entry.isFile();\n\n\t\t\tif (entry.isSymbolicLink()) {\n\t\t\t\ttry {\n\t\t\t\t\tconst stats = statSync(fullPath);\n\t\t\t\t\tisDir = stats.isDirectory();\n\t\t\t\t\tisFile = stats.isFile();\n\t\t\t\t} catch {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tconst relPath = toPosixPath(relative(root, fullPath));\n\t\t\tconst ignorePath = isDir ? `${relPath}/` : relPath;\n\t\t\tif (ig.ignores(ignorePath)) continue;\n\n\t\t\tif (isDir) {\n\t\t\t\tentries.push(...collectSkillEntries(fullPath, false, ig, root));\n\t\t\t} else if (isFile) {\n\t\t\t\tconst isRootMd = includeRootFiles && entry.name.endsWith(\".md\");\n\t\t\t\tconst isSkillMd = !includeRootFiles && entry.name === \"SKILL.md\";\n\t\t\t\tif (isRootMd || isSkillMd) {\n\t\t\t\t\tentries.push(fullPath);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t} catch {\n\t\t// Ignore errors\n\t}\n\n\treturn entries;\n}\n\nfunction collectAutoSkillEntries(dir: string, includeRootFiles = true): string[] {\n\treturn collectSkillEntries(dir, includeRootFiles);\n}\n\nfunction findGitRepoRoot(startDir: string): string | null {\n\tlet dir = resolve(startDir);\n\twhile (true) {\n\t\tif (existsSync(join(dir, \".git\"))) {\n\t\t\treturn dir;\n\t\t}\n\t\tconst parent = dirname(dir);\n\t\tif (parent === dir) {\n\t\t\treturn null;\n\t\t}\n\t\tdir = parent;\n\t}\n}\n\nfunction collectAncestorAgentsSkillDirs(startDir: string): string[] {\n\tconst skillDirs: string[] = [];\n\tconst resolvedStartDir = resolve(startDir);\n\tconst gitRepoRoot = findGitRepoRoot(resolvedStartDir);\n\n\tlet dir = resolvedStartDir;\n\twhile (true) {\n\t\tskillDirs.push(join(dir, \".agents\", \"skills\"));\n\t\tif (gitRepoRoot && dir === gitRepoRoot) {\n\t\t\tbreak;\n\t\t}\n\t\tconst parent = dirname(dir);\n\t\tif (parent === dir) {\n\t\t\tbreak;\n\t\t}\n\t\tdir = parent;\n\t}\n\n\treturn skillDirs;\n}\n\nfunction collectAutoPromptEntries(dir: string): string[] {\n\tconst entries: string[] = [];\n\tif (!existsSync(dir)) return entries;\n\n\tconst ig = ignore();\n\taddIgnoreRules(ig, dir, dir);\n\n\ttry {\n\t\tconst dirEntries = readdirSync(dir, { withFileTypes: true });\n\t\tfor (const entry of dirEntries) {\n\t\t\tif (entry.name.startsWith(\".\")) continue;\n\t\t\tif (entry.name === \"node_modules\") continue;\n\n\t\t\tconst fullPath = join(dir, entry.name);\n\t\t\tlet isFile = entry.isFile();\n\t\t\tif (entry.isSymbolicLink()) {\n\t\t\t\ttry {\n\t\t\t\t\tisFile = statSync(fullPath).isFile();\n\t\t\t\t} catch {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tconst relPath = toPosixPath(relative(dir, fullPath));\n\t\t\tif (ig.ignores(relPath)) continue;\n\n\t\t\tif (isFile && entry.name.endsWith(\".md\")) {\n\t\t\t\tentries.push(fullPath);\n\t\t\t}\n\t\t}\n\t} catch {\n\t\t// Ignore errors\n\t}\n\n\treturn entries;\n}\n\nfunction collectAutoThemeEntries(dir: string): string[] {\n\tconst entries: string[] = [];\n\tif (!existsSync(dir)) return entries;\n\n\tconst ig = ignore();\n\taddIgnoreRules(ig, dir, dir);\n\n\ttry {\n\t\tconst dirEntries = readdirSync(dir, { withFileTypes: true });\n\t\tfor (const entry of dirEntries) {\n\t\t\tif (entry.name.startsWith(\".\")) continue;\n\t\t\tif (entry.name === \"node_modules\") continue;\n\n\t\t\tconst fullPath = join(dir, entry.name);\n\t\t\tlet isFile = entry.isFile();\n\t\t\tif (entry.isSymbolicLink()) {\n\t\t\t\ttry {\n\t\t\t\t\tisFile = statSync(fullPath).isFile();\n\t\t\t\t} catch {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tconst relPath = toPosixPath(relative(dir, fullPath));\n\t\t\tif (ig.ignores(relPath)) continue;\n\n\t\t\tif (isFile && entry.name.endsWith(\".json\")) {\n\t\t\t\tentries.push(fullPath);\n\t\t\t}\n\t\t}\n\t} catch {\n\t\t// Ignore errors\n\t}\n\n\treturn entries;\n}\n\nfunction readDrahtManifestFile(packageJsonPath: string): DrahtManifest | null {\n\ttry {\n\t\tconst content = readFileSync(packageJsonPath, \"utf-8\");\n\t\tconst pkg = JSON.parse(content) as { draht?: DrahtManifest };\n\t\treturn pkg.draht ?? null;\n\t} catch {\n\t\treturn null;\n\t}\n}\n\nfunction resolveExtensionEntries(dir: string): string[] | null {\n\tconst packageJsonPath = join(dir, \"package.json\");\n\tif (existsSync(packageJsonPath)) {\n\t\tconst manifest = readDrahtManifestFile(packageJsonPath);\n\t\tif (manifest?.extensions?.length) {\n\t\t\tconst entries: string[] = [];\n\t\t\tfor (const extPath of manifest.extensions) {\n\t\t\t\tconst resolvedExtPath = resolve(dir, extPath);\n\t\t\t\tif (existsSync(resolvedExtPath)) {\n\t\t\t\t\tentries.push(resolvedExtPath);\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (entries.length > 0) {\n\t\t\t\treturn entries;\n\t\t\t}\n\t\t}\n\t}\n\n\tconst indexTs = join(dir, \"index.ts\");\n\tconst indexJs = join(dir, \"index.js\");\n\tif (existsSync(indexTs)) {\n\t\treturn [indexTs];\n\t}\n\tif (existsSync(indexJs)) {\n\t\treturn [indexJs];\n\t}\n\n\treturn null;\n}\n\nfunction collectAutoExtensionEntries(dir: string): string[] {\n\tconst entries: string[] = [];\n\tif (!existsSync(dir)) return entries;\n\n\t// First check if this directory itself has explicit extension entries (package.json or index)\n\tconst rootEntries = resolveExtensionEntries(dir);\n\tif (rootEntries) {\n\t\treturn rootEntries;\n\t}\n\n\t// Otherwise, discover extensions from directory contents\n\tconst ig = ignore();\n\taddIgnoreRules(ig, dir, dir);\n\n\ttry {\n\t\tconst dirEntries = readdirSync(dir, { withFileTypes: true });\n\t\tfor (const entry of dirEntries) {\n\t\t\tif (entry.name.startsWith(\".\")) continue;\n\t\t\tif (entry.name === \"node_modules\") continue;\n\n\t\t\tconst fullPath = join(dir, entry.name);\n\t\t\tlet isDir = entry.isDirectory();\n\t\t\tlet isFile = entry.isFile();\n\n\t\t\tif (entry.isSymbolicLink()) {\n\t\t\t\ttry {\n\t\t\t\t\tconst stats = statSync(fullPath);\n\t\t\t\t\tisDir = stats.isDirectory();\n\t\t\t\t\tisFile = stats.isFile();\n\t\t\t\t} catch {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tconst relPath = toPosixPath(relative(dir, fullPath));\n\t\t\tconst ignorePath = isDir ? `${relPath}/` : relPath;\n\t\t\tif (ig.ignores(ignorePath)) continue;\n\n\t\t\tif (isFile && (entry.name.endsWith(\".ts\") || entry.name.endsWith(\".js\"))) {\n\t\t\t\tentries.push(fullPath);\n\t\t\t} else if (isDir) {\n\t\t\t\tconst resolvedEntries = resolveExtensionEntries(fullPath);\n\t\t\t\tif (resolvedEntries) {\n\t\t\t\t\tentries.push(...resolvedEntries);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t} catch {\n\t\t// Ignore errors\n\t}\n\n\treturn entries;\n}\n\n/**\n * Collect resource files from a directory based on resource type.\n * Extensions use smart discovery (index.ts in subdirs), others use recursive collection.\n */\nfunction collectResourceFiles(dir: string, resourceType: ResourceType): string[] {\n\tif (resourceType === \"skills\") {\n\t\treturn collectSkillEntries(dir);\n\t}\n\tif (resourceType === \"extensions\") {\n\t\treturn collectAutoExtensionEntries(dir);\n\t}\n\treturn collectFiles(dir, FILE_PATTERNS[resourceType]);\n}\n\nfunction matchesAnyPattern(filePath: string, patterns: string[], baseDir: string): boolean {\n\tconst rel = relative(baseDir, filePath);\n\tconst name = basename(filePath);\n\tconst isSkillFile = name === \"SKILL.md\";\n\tconst parentDir = isSkillFile ? dirname(filePath) : undefined;\n\tconst parentRel = isSkillFile ? relative(baseDir, parentDir!) : undefined;\n\tconst parentName = isSkillFile ? basename(parentDir!) : undefined;\n\n\treturn patterns.some((pattern) => {\n\t\tif (minimatch(rel, pattern) || minimatch(name, pattern) || minimatch(filePath, pattern)) {\n\t\t\treturn true;\n\t\t}\n\t\tif (!isSkillFile) return false;\n\t\treturn minimatch(parentRel!, pattern) || minimatch(parentName!, pattern) || minimatch(parentDir!, pattern);\n\t});\n}\n\nfunction normalizeExactPattern(pattern: string): string {\n\tif (pattern.startsWith(\"./\") || pattern.startsWith(\".\\\\\")) {\n\t\treturn pattern.slice(2);\n\t}\n\treturn pattern;\n}\n\nfunction matchesAnyExactPattern(filePath: string, patterns: string[], baseDir: string): boolean {\n\tif (patterns.length === 0) return false;\n\tconst rel = relative(baseDir, filePath);\n\tconst name = basename(filePath);\n\tconst isSkillFile = name === \"SKILL.md\";\n\tconst parentDir = isSkillFile ? dirname(filePath) : undefined;\n\tconst parentRel = isSkillFile ? relative(baseDir, parentDir!) : undefined;\n\n\treturn patterns.some((pattern) => {\n\t\tconst normalized = normalizeExactPattern(pattern);\n\t\tif (normalized === rel || normalized === filePath) {\n\t\t\treturn true;\n\t\t}\n\t\tif (!isSkillFile) return false;\n\t\treturn normalized === parentRel || normalized === parentDir;\n\t});\n}\n\nfunction getOverridePatterns(entries: string[]): string[] {\n\treturn entries.filter((pattern) => pattern.startsWith(\"!\") || pattern.startsWith(\"+\") || pattern.startsWith(\"-\"));\n}\n\nfunction isEnabledByOverrides(filePath: string, patterns: string[], baseDir: string): boolean {\n\tconst overrides = getOverridePatterns(patterns);\n\tconst excludes = overrides.filter((pattern) => pattern.startsWith(\"!\")).map((pattern) => pattern.slice(1));\n\tconst forceIncludes = overrides.filter((pattern) => pattern.startsWith(\"+\")).map((pattern) => pattern.slice(1));\n\tconst forceExcludes = overrides.filter((pattern) => pattern.startsWith(\"-\")).map((pattern) => pattern.slice(1));\n\n\tlet enabled = true;\n\tif (excludes.length > 0 && matchesAnyPattern(filePath, excludes, baseDir)) {\n\t\tenabled = false;\n\t}\n\tif (forceIncludes.length > 0 && matchesAnyExactPattern(filePath, forceIncludes, baseDir)) {\n\t\tenabled = true;\n\t}\n\tif (forceExcludes.length > 0 && matchesAnyExactPattern(filePath, forceExcludes, baseDir)) {\n\t\tenabled = false;\n\t}\n\treturn enabled;\n}\n\n/**\n * Apply patterns to paths and return a Set of enabled paths.\n * Pattern types:\n * - Plain patterns: include matching paths\n * - `!pattern`: exclude matching paths\n * - `+path`: force-include exact path (overrides exclusions)\n * - `-path`: force-exclude exact path (overrides force-includes)\n */\nfunction applyPatterns(allPaths: string[], patterns: string[], baseDir: string): Set<string> {\n\tconst includes: string[] = [];\n\tconst excludes: string[] = [];\n\tconst forceIncludes: string[] = [];\n\tconst forceExcludes: string[] = [];\n\n\tfor (const p of patterns) {\n\t\tif (p.startsWith(\"+\")) {\n\t\t\tforceIncludes.push(p.slice(1));\n\t\t} else if (p.startsWith(\"-\")) {\n\t\t\tforceExcludes.push(p.slice(1));\n\t\t} else if (p.startsWith(\"!\")) {\n\t\t\texcludes.push(p.slice(1));\n\t\t} else {\n\t\t\tincludes.push(p);\n\t\t}\n\t}\n\n\t// Step 1: Apply includes (or all if no includes)\n\tlet result: string[];\n\tif (includes.length === 0) {\n\t\tresult = [...allPaths];\n\t} else {\n\t\tresult = allPaths.filter((filePath) => matchesAnyPattern(filePath, includes, baseDir));\n\t}\n\n\t// Step 2: Apply excludes\n\tif (excludes.length > 0) {\n\t\tresult = result.filter((filePath) => !matchesAnyPattern(filePath, excludes, baseDir));\n\t}\n\n\t// Step 3: Force-include (add back from allPaths, overriding exclusions)\n\tif (forceIncludes.length > 0) {\n\t\tfor (const filePath of allPaths) {\n\t\t\tif (!result.includes(filePath) && matchesAnyExactPattern(filePath, forceIncludes, baseDir)) {\n\t\t\t\tresult.push(filePath);\n\t\t\t}\n\t\t}\n\t}\n\n\t// Step 4: Force-exclude (remove even if included or force-included)\n\tif (forceExcludes.length > 0) {\n\t\tresult = result.filter((filePath) => !matchesAnyExactPattern(filePath, forceExcludes, baseDir));\n\t}\n\n\treturn new Set(result);\n}\n\nexport class DefaultPackageManager implements PackageManager {\n\tprivate cwd: string;\n\tprivate agentDir: string;\n\tprivate settingsManager: SettingsManager;\n\tprivate globalNpmRoot: string | undefined;\n\tprivate progressCallback: ProgressCallback | undefined;\n\n\tconstructor(options: PackageManagerOptions) {\n\t\tthis.cwd = options.cwd;\n\t\tthis.agentDir = options.agentDir;\n\t\tthis.settingsManager = options.settingsManager;\n\t}\n\n\tsetProgressCallback(callback: ProgressCallback | undefined): void {\n\t\tthis.progressCallback = callback;\n\t}\n\n\taddSourceToSettings(source: string, options?: { local?: boolean }): boolean {\n\t\tconst scope: SourceScope = options?.local ? \"project\" : \"user\";\n\t\tconst currentSettings =\n\t\t\tscope === \"project\" ? this.settingsManager.getProjectSettings() : this.settingsManager.getGlobalSettings();\n\t\tconst currentPackages = currentSettings.packages ?? [];\n\t\tconst normalizedSource = this.normalizePackageSourceForSettings(source, scope);\n\t\tconst exists = currentPackages.some((existing) => this.packageSourcesMatch(existing, source, scope));\n\t\tif (exists) {\n\t\t\treturn false;\n\t\t}\n\t\tconst nextPackages = [...currentPackages, normalizedSource];\n\t\tif (scope === \"project\") {\n\t\t\tthis.settingsManager.setProjectPackages(nextPackages);\n\t\t} else {\n\t\t\tthis.settingsManager.setPackages(nextPackages);\n\t\t}\n\t\treturn true;\n\t}\n\n\tremoveSourceFromSettings(source: string, options?: { local?: boolean }): boolean {\n\t\tconst scope: SourceScope = options?.local ? \"project\" : \"user\";\n\t\tconst currentSettings =\n\t\t\tscope === \"project\" ? this.settingsManager.getProjectSettings() : this.settingsManager.getGlobalSettings();\n\t\tconst currentPackages = currentSettings.packages ?? [];\n\t\tconst nextPackages = currentPackages.filter((existing) => !this.packageSourcesMatch(existing, source, scope));\n\t\tconst changed = nextPackages.length !== currentPackages.length;\n\t\tif (!changed) {\n\t\t\treturn false;\n\t\t}\n\t\tif (scope === \"project\") {\n\t\t\tthis.settingsManager.setProjectPackages(nextPackages);\n\t\t} else {\n\t\t\tthis.settingsManager.setPackages(nextPackages);\n\t\t}\n\t\treturn true;\n\t}\n\n\tgetInstalledPath(source: string, scope: \"user\" | \"project\"): string | undefined {\n\t\tconst parsed = this.parseSource(source);\n\t\tif (parsed.type === \"npm\") {\n\t\t\tconst path = this.getNpmInstallPath(parsed, scope);\n\t\t\treturn existsSync(path) ? path : undefined;\n\t\t}\n\t\tif (parsed.type === \"git\") {\n\t\t\tconst path = this.getGitInstallPath(parsed, scope);\n\t\t\treturn existsSync(path) ? path : undefined;\n\t\t}\n\t\tif (parsed.type === \"local\") {\n\t\t\tconst baseDir = this.getBaseDirForScope(scope);\n\t\t\tconst path = this.resolvePathFromBase(parsed.path, baseDir);\n\t\t\treturn existsSync(path) ? path : undefined;\n\t\t}\n\t\treturn undefined;\n\t}\n\n\tprivate emitProgress(event: ProgressEvent): void {\n\t\tthis.progressCallback?.(event);\n\t}\n\n\tprivate async withProgress(\n\t\taction: ProgressEvent[\"action\"],\n\t\tsource: string,\n\t\tmessage: string,\n\t\toperation: () => Promise<void>,\n\t): Promise<void> {\n\t\tthis.emitProgress({ type: \"start\", action, source, message });\n\t\ttry {\n\t\t\tawait operation();\n\t\t\tthis.emitProgress({ type: \"complete\", action, source });\n\t\t} catch (error) {\n\t\t\tconst errorMessage = error instanceof Error ? error.message : String(error);\n\t\t\tthis.emitProgress({ type: \"error\", action, source, message: errorMessage });\n\t\t\tthrow error;\n\t\t}\n\t}\n\n\tasync resolve(onMissing?: (source: string) => Promise<MissingSourceAction>): Promise<ResolvedPaths> {\n\t\tconst accumulator = this.createAccumulator();\n\t\tconst globalSettings = this.settingsManager.getGlobalSettings();\n\t\tconst projectSettings = this.settingsManager.getProjectSettings();\n\n\t\t// Collect all packages with scope (project first so cwd resources win collisions)\n\t\tconst allPackages: Array<{ pkg: PackageSource; scope: SourceScope }> = [];\n\t\tfor (const pkg of projectSettings.packages ?? []) {\n\t\t\tallPackages.push({ pkg, scope: \"project\" });\n\t\t}\n\t\tfor (const pkg of globalSettings.packages ?? []) {\n\t\t\tallPackages.push({ pkg, scope: \"user\" });\n\t\t}\n\n\t\t// Dedupe: project scope wins over global for same package identity\n\t\tconst packageSources = this.dedupePackages(allPackages);\n\t\tawait this.resolvePackageSources(packageSources, accumulator, onMissing);\n\n\t\tconst globalBaseDir = this.agentDir;\n\t\tconst projectBaseDir = join(this.cwd, CONFIG_DIR_NAME);\n\n\t\tfor (const resourceType of RESOURCE_TYPES) {\n\t\t\tconst target = this.getTargetMap(accumulator, resourceType);\n\t\t\tconst globalEntries = (globalSettings[resourceType] ?? []) as string[];\n\t\t\tconst projectEntries = (projectSettings[resourceType] ?? []) as string[];\n\t\t\tthis.resolveLocalEntries(\n\t\t\t\tprojectEntries,\n\t\t\t\tresourceType,\n\t\t\t\ttarget,\n\t\t\t\t{\n\t\t\t\t\tsource: \"local\",\n\t\t\t\t\tscope: \"project\",\n\t\t\t\t\torigin: \"top-level\",\n\t\t\t\t},\n\t\t\t\tprojectBaseDir,\n\t\t\t);\n\t\t\tthis.resolveLocalEntries(\n\t\t\t\tglobalEntries,\n\t\t\t\tresourceType,\n\t\t\t\ttarget,\n\t\t\t\t{\n\t\t\t\t\tsource: \"local\",\n\t\t\t\t\tscope: \"user\",\n\t\t\t\t\torigin: \"top-level\",\n\t\t\t\t},\n\t\t\t\tglobalBaseDir,\n\t\t\t);\n\t\t}\n\n\t\tthis.addAutoDiscoveredResources(accumulator, globalSettings, projectSettings, globalBaseDir, projectBaseDir);\n\n\t\treturn this.toResolvedPaths(accumulator);\n\t}\n\n\tasync resolveExtensionSources(\n\t\tsources: string[],\n\t\toptions?: { local?: boolean; temporary?: boolean },\n\t): Promise<ResolvedPaths> {\n\t\tconst accumulator = this.createAccumulator();\n\t\tconst scope: SourceScope = options?.temporary ? \"temporary\" : options?.local ? \"project\" : \"user\";\n\t\tconst packageSources = sources.map((source) => ({ pkg: source as PackageSource, scope }));\n\t\tawait this.resolvePackageSources(packageSources, accumulator);\n\t\treturn this.toResolvedPaths(accumulator);\n\t}\n\n\tasync install(source: string, options?: { local?: boolean }): Promise<void> {\n\t\tconst parsed = this.parseSource(source);\n\t\tconst scope: SourceScope = options?.local ? \"project\" : \"user\";\n\t\tawait this.withProgress(\"install\", source, `Installing ${source}...`, async () => {\n\t\t\tif (parsed.type === \"npm\") {\n\t\t\t\tawait this.installNpm(parsed, scope, false);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (parsed.type === \"git\") {\n\t\t\t\tawait this.installGit(parsed, scope);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (parsed.type === \"local\") {\n\t\t\t\tconst resolved = this.resolvePath(parsed.path);\n\t\t\t\tif (!existsSync(resolved)) {\n\t\t\t\t\tthrow new Error(`Path does not exist: ${resolved}`);\n\t\t\t\t}\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tthrow new Error(`Unsupported install source: ${source}`);\n\t\t});\n\t}\n\n\tasync remove(source: string, options?: { local?: boolean }): Promise<void> {\n\t\tconst parsed = this.parseSource(source);\n\t\tconst scope: SourceScope = options?.local ? \"project\" : \"user\";\n\t\tawait this.withProgress(\"remove\", source, `Removing ${source}...`, async () => {\n\t\t\tif (parsed.type === \"npm\") {\n\t\t\t\tawait this.uninstallNpm(parsed, scope);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (parsed.type === \"git\") {\n\t\t\t\tawait this.removeGit(parsed, scope);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (parsed.type === \"local\") {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tthrow new Error(`Unsupported remove source: ${source}`);\n\t\t});\n\t}\n\n\tasync update(source?: string): Promise<void> {\n\t\tconst globalSettings = this.settingsManager.getGlobalSettings();\n\t\tconst projectSettings = this.settingsManager.getProjectSettings();\n\t\tconst identity = source ? this.getPackageIdentity(source) : undefined;\n\n\t\tfor (const pkg of globalSettings.packages ?? []) {\n\t\t\tconst sourceStr = typeof pkg === \"string\" ? pkg : pkg.source;\n\t\t\tif (identity && this.getPackageIdentity(sourceStr, \"user\") !== identity) continue;\n\t\t\tawait this.updateSourceForScope(sourceStr, \"user\");\n\t\t}\n\t\tfor (const pkg of projectSettings.packages ?? []) {\n\t\t\tconst sourceStr = typeof pkg === \"string\" ? pkg : pkg.source;\n\t\t\tif (identity && this.getPackageIdentity(sourceStr, \"project\") !== identity) continue;\n\t\t\tawait this.updateSourceForScope(sourceStr, \"project\");\n\t\t}\n\t}\n\n\tprivate async updateSourceForScope(source: string, scope: SourceScope): Promise<void> {\n\t\tif (isOfflineModeEnabled()) {\n\t\t\treturn;\n\t\t}\n\t\tconst parsed = this.parseSource(source);\n\t\tif (parsed.type === \"npm\") {\n\t\t\tif (parsed.pinned) return;\n\t\t\tawait this.withProgress(\"update\", source, `Updating ${source}...`, async () => {\n\t\t\t\tawait this.installNpm(parsed, scope, false);\n\t\t\t});\n\t\t\treturn;\n\t\t}\n\t\tif (parsed.type === \"git\") {\n\t\t\tif (parsed.pinned) return;\n\t\t\tawait this.withProgress(\"update\", source, `Updating ${source}...`, async () => {\n\t\t\t\tawait this.updateGit(parsed, scope);\n\t\t\t});\n\t\t\treturn;\n\t\t}\n\t}\n\n\tprivate async resolvePackageSources(\n\t\tsources: Array<{ pkg: PackageSource; scope: SourceScope }>,\n\t\taccumulator: ResourceAccumulator,\n\t\tonMissing?: (source: string) => Promise<MissingSourceAction>,\n\t): Promise<void> {\n\t\tfor (const { pkg, scope } of sources) {\n\t\t\tconst sourceStr = typeof pkg === \"string\" ? pkg : pkg.source;\n\t\t\tconst filter = typeof pkg === \"object\" ? pkg : undefined;\n\t\t\tconst parsed = this.parseSource(sourceStr);\n\t\t\tconst metadata: PathMetadata = { source: sourceStr, scope, origin: \"package\" };\n\n\t\t\tif (parsed.type === \"local\") {\n\t\t\t\tconst baseDir = this.getBaseDirForScope(scope);\n\t\t\t\tthis.resolveLocalExtensionSource(parsed, accumulator, filter, metadata, baseDir);\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tconst installMissing = async (): Promise<boolean> => {\n\t\t\t\tif (isOfflineModeEnabled()) {\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\t\t\t\tif (!onMissing) {\n\t\t\t\t\tawait this.installParsedSource(parsed, scope);\n\t\t\t\t\treturn true;\n\t\t\t\t}\n\t\t\t\tconst action = await onMissing(sourceStr);\n\t\t\t\tif (action === \"skip\") return false;\n\t\t\t\tif (action === \"error\") throw new Error(`Missing source: ${sourceStr}`);\n\t\t\t\tawait this.installParsedSource(parsed, scope);\n\t\t\t\treturn true;\n\t\t\t};\n\n\t\t\tif (parsed.type === \"npm\") {\n\t\t\t\tconst installedPath = this.getNpmInstallPath(parsed, scope);\n\t\t\t\tconst needsInstall = !existsSync(installedPath) || (await this.npmNeedsUpdate(parsed, installedPath));\n\t\t\t\tif (needsInstall) {\n\t\t\t\t\tconst installed = await installMissing();\n\t\t\t\t\tif (!installed) continue;\n\t\t\t\t}\n\t\t\t\tmetadata.baseDir = installedPath;\n\t\t\t\tthis.collectPackageResources(installedPath, accumulator, filter, metadata);\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tif (parsed.type === \"git\") {\n\t\t\t\tconst installedPath = this.getGitInstallPath(parsed, scope);\n\t\t\t\tif (!existsSync(installedPath)) {\n\t\t\t\t\tconst installed = await installMissing();\n\t\t\t\t\tif (!installed) continue;\n\t\t\t\t} else if (scope === \"temporary\" && !parsed.pinned && !isOfflineModeEnabled()) {\n\t\t\t\t\tawait this.refreshTemporaryGitSource(parsed, sourceStr);\n\t\t\t\t}\n\t\t\t\tmetadata.baseDir = installedPath;\n\t\t\t\tthis.collectPackageResources(installedPath, accumulator, filter, metadata);\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate resolveLocalExtensionSource(\n\t\tsource: LocalSource,\n\t\taccumulator: ResourceAccumulator,\n\t\tfilter: PackageFilter | undefined,\n\t\tmetadata: PathMetadata,\n\t\tbaseDir: string,\n\t): void {\n\t\tconst resolved = this.resolvePathFromBase(source.path, baseDir);\n\t\tif (!existsSync(resolved)) {\n\t\t\treturn;\n\t\t}\n\n\t\ttry {\n\t\t\tconst stats = statSync(resolved);\n\t\t\tif (stats.isFile()) {\n\t\t\t\tmetadata.baseDir = dirname(resolved);\n\t\t\t\tthis.addResource(accumulator.extensions, resolved, metadata, true);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (stats.isDirectory()) {\n\t\t\t\tmetadata.baseDir = resolved;\n\t\t\t\tconst resources = this.collectPackageResources(resolved, accumulator, filter, metadata);\n\t\t\t\tif (!resources) {\n\t\t\t\t\tthis.addResource(accumulator.extensions, resolved, metadata, true);\n\t\t\t\t}\n\t\t\t}\n\t\t} catch {\n\t\t\treturn;\n\t\t}\n\t}\n\n\tprivate async installParsedSource(parsed: ParsedSource, scope: SourceScope): Promise<void> {\n\t\tif (parsed.type === \"npm\") {\n\t\t\tawait this.installNpm(parsed, scope, scope === \"temporary\");\n\t\t\treturn;\n\t\t}\n\t\tif (parsed.type === \"git\") {\n\t\t\tawait this.installGit(parsed, scope);\n\t\t\treturn;\n\t\t}\n\t}\n\n\tprivate getPackageSourceString(pkg: PackageSource): string {\n\t\treturn typeof pkg === \"string\" ? pkg : pkg.source;\n\t}\n\n\tprivate getSourceMatchKeyForInput(source: string): string {\n\t\tconst parsed = this.parseSource(source);\n\t\tif (parsed.type === \"npm\") {\n\t\t\treturn `npm:${parsed.name}`;\n\t\t}\n\t\tif (parsed.type === \"git\") {\n\t\t\treturn `git:${parsed.host}/${parsed.path}`;\n\t\t}\n\t\treturn `local:${this.resolvePath(parsed.path)}`;\n\t}\n\n\tprivate getSourceMatchKeyForSettings(source: string, scope: SourceScope): string {\n\t\tconst parsed = this.parseSource(source);\n\t\tif (parsed.type === \"npm\") {\n\t\t\treturn `npm:${parsed.name}`;\n\t\t}\n\t\tif (parsed.type === \"git\") {\n\t\t\treturn `git:${parsed.host}/${parsed.path}`;\n\t\t}\n\t\tconst baseDir = this.getBaseDirForScope(scope);\n\t\treturn `local:${this.resolvePathFromBase(parsed.path, baseDir)}`;\n\t}\n\n\tprivate packageSourcesMatch(existing: PackageSource, inputSource: string, scope: SourceScope): boolean {\n\t\tconst left = this.getSourceMatchKeyForSettings(this.getPackageSourceString(existing), scope);\n\t\tconst right = this.getSourceMatchKeyForInput(inputSource);\n\t\treturn left === right;\n\t}\n\n\tprivate normalizePackageSourceForSettings(source: string, scope: SourceScope): string {\n\t\tconst parsed = this.parseSource(source);\n\t\tif (parsed.type !== \"local\") {\n\t\t\treturn source;\n\t\t}\n\t\tconst baseDir = this.getBaseDirForScope(scope);\n\t\tconst resolved = this.resolvePath(parsed.path);\n\t\tconst rel = relative(baseDir, resolved);\n\t\treturn rel || \".\";\n\t}\n\n\tprivate parseSource(source: string): ParsedSource {\n\t\tif (source.startsWith(\"npm:\")) {\n\t\t\tconst spec = source.slice(\"npm:\".length).trim();\n\t\t\tconst { name, version } = this.parseNpmSpec(spec);\n\t\t\treturn {\n\t\t\t\ttype: \"npm\",\n\t\t\t\tspec,\n\t\t\t\tname,\n\t\t\t\tpinned: Boolean(version),\n\t\t\t};\n\t\t}\n\n\t\tconst trimmed = source.trim();\n\t\tconst isWindowsAbsolutePath = /^[A-Za-z]:[\\\\/]|^\\\\\\\\/.test(trimmed);\n\t\tconst isLocalPathLike =\n\t\t\ttrimmed.startsWith(\".\") ||\n\t\t\ttrimmed.startsWith(\"/\") ||\n\t\t\ttrimmed === \"~\" ||\n\t\t\ttrimmed.startsWith(\"~/\") ||\n\t\t\tisWindowsAbsolutePath;\n\t\tif (isLocalPathLike) {\n\t\t\treturn { type: \"local\", path: source };\n\t\t}\n\n\t\t// Try parsing as git URL\n\t\tconst gitParsed = parseGitUrl(source);\n\t\tif (gitParsed) {\n\t\t\treturn gitParsed;\n\t\t}\n\n\t\treturn { type: \"local\", path: source };\n\t}\n\n\t/**\n\t * Check if an npm package needs to be updated.\n\t * - For unpinned packages: check if registry has a newer version\n\t * - For pinned packages: check if installed version matches the pinned version\n\t */\n\tprivate async npmNeedsUpdate(source: NpmSource, installedPath: string): Promise<boolean> {\n\t\tif (isOfflineModeEnabled()) {\n\t\t\treturn false;\n\t\t}\n\n\t\tconst installedVersion = this.getInstalledNpmVersion(installedPath);\n\t\tif (!installedVersion) return true;\n\n\t\tconst { version: pinnedVersion } = this.parseNpmSpec(source.spec);\n\t\tif (pinnedVersion) {\n\t\t\t// Pinned: check if installed matches pinned (exact match for now)\n\t\t\treturn installedVersion !== pinnedVersion;\n\t\t}\n\n\t\t// Unpinned: check registry for latest version\n\t\ttry {\n\t\t\tconst latestVersion = await this.getLatestNpmVersion(source.name);\n\t\t\treturn latestVersion !== installedVersion;\n\t\t} catch {\n\t\t\t// If we can't check registry, assume it's fine\n\t\t\treturn false;\n\t\t}\n\t}\n\n\tprivate getInstalledNpmVersion(installedPath: string): string | undefined {\n\t\tconst packageJsonPath = join(installedPath, \"package.json\");\n\t\tif (!existsSync(packageJsonPath)) return undefined;\n\t\ttry {\n\t\t\tconst content = readFileSync(packageJsonPath, \"utf-8\");\n\t\t\tconst pkg = JSON.parse(content) as { version?: string };\n\t\t\treturn pkg.version;\n\t\t} catch {\n\t\t\treturn undefined;\n\t\t}\n\t}\n\n\tprivate async getLatestNpmVersion(packageName: string): Promise<string> {\n\t\tconst response = await fetch(`https://registry.npmjs.org/${packageName}/latest`, {\n\t\t\tsignal: AbortSignal.timeout(NETWORK_TIMEOUT_MS),\n\t\t});\n\t\tif (!response.ok) throw new Error(`Failed to fetch npm registry: ${response.status}`);\n\t\tconst data = (await response.json()) as { version: string };\n\t\treturn data.version;\n\t}\n\n\t/**\n\t * Get a unique identity for a package, ignoring version/ref.\n\t * Used to detect when the same package is in both global and project settings.\n\t * For git packages, uses normalized host/path to ensure SSH and HTTPS URLs\n\t * for the same repository are treated as identical.\n\t */\n\tprivate getPackageIdentity(source: string, scope?: SourceScope): string {\n\t\tconst parsed = this.parseSource(source);\n\t\tif (parsed.type === \"npm\") {\n\t\t\treturn `npm:${parsed.name}`;\n\t\t}\n\t\tif (parsed.type === \"git\") {\n\t\t\t// Use host/path for identity to normalize SSH and HTTPS\n\t\t\treturn `git:${parsed.host}/${parsed.path}`;\n\t\t}\n\t\tif (scope) {\n\t\t\tconst baseDir = this.getBaseDirForScope(scope);\n\t\t\treturn `local:${this.resolvePathFromBase(parsed.path, baseDir)}`;\n\t\t}\n\t\treturn `local:${this.resolvePath(parsed.path)}`;\n\t}\n\n\t/**\n\t * Dedupe packages: if same package identity appears in both global and project,\n\t * keep only the project one (project wins).\n\t */\n\tprivate dedupePackages(\n\t\tpackages: Array<{ pkg: PackageSource; scope: SourceScope }>,\n\t): Array<{ pkg: PackageSource; scope: SourceScope }> {\n\t\tconst seen = new Map<string, { pkg: PackageSource; scope: SourceScope }>();\n\n\t\tfor (const entry of packages) {\n\t\t\tconst sourceStr = typeof entry.pkg === \"string\" ? entry.pkg : entry.pkg.source;\n\t\t\tconst identity = this.getPackageIdentity(sourceStr, entry.scope);\n\n\t\t\tconst existing = seen.get(identity);\n\t\t\tif (!existing) {\n\t\t\t\tseen.set(identity, entry);\n\t\t\t} else if (entry.scope === \"project\" && existing.scope === \"user\") {\n\t\t\t\t// Project wins over user\n\t\t\t\tseen.set(identity, entry);\n\t\t\t}\n\t\t\t// If existing is project and new is global, keep existing (project)\n\t\t\t// If both are same scope, keep first one\n\t\t}\n\n\t\treturn Array.from(seen.values());\n\t}\n\n\tprivate parseNpmSpec(spec: string): { name: string; version?: string } {\n\t\tconst match = spec.match(/^(@?[^@]+(?:\\/[^@]+)?)(?:@(.+))?$/);\n\t\tif (!match) {\n\t\t\treturn { name: spec };\n\t\t}\n\t\tconst name = match[1] ?? spec;\n\t\tconst version = match[2];\n\t\treturn { name, version };\n\t}\n\n\tprivate async installNpm(source: NpmSource, scope: SourceScope, temporary: boolean): Promise<void> {\n\t\tif (scope === \"user\" && !temporary) {\n\t\t\tawait this.runCommand(\"npm\", [\"install\", \"-g\", source.spec]);\n\t\t\treturn;\n\t\t}\n\t\tconst installRoot = this.getNpmInstallRoot(scope, temporary);\n\t\tthis.ensureNpmProject(installRoot);\n\t\tawait this.runCommand(\"npm\", [\"install\", source.spec, \"--prefix\", installRoot]);\n\t}\n\n\tprivate async uninstallNpm(source: NpmSource, scope: SourceScope): Promise<void> {\n\t\tif (scope === \"user\") {\n\t\t\tawait this.runCommand(\"npm\", [\"uninstall\", \"-g\", source.name]);\n\t\t\treturn;\n\t\t}\n\t\tconst installRoot = this.getNpmInstallRoot(scope, false);\n\t\tif (!existsSync(installRoot)) {\n\t\t\treturn;\n\t\t}\n\t\tawait this.runCommand(\"npm\", [\"uninstall\", source.name, \"--prefix\", installRoot]);\n\t}\n\n\tprivate async installGit(source: GitSource, scope: SourceScope): Promise<void> {\n\t\tconst targetDir = this.getGitInstallPath(source, scope);\n\t\tif (existsSync(targetDir)) {\n\t\t\treturn;\n\t\t}\n\t\tconst gitRoot = this.getGitInstallRoot(scope);\n\t\tif (gitRoot) {\n\t\t\tthis.ensureGitIgnore(gitRoot);\n\t\t}\n\t\tmkdirSync(dirname(targetDir), { recursive: true });\n\n\t\tawait this.runCommand(\"git\", [\"clone\", source.repo, targetDir]);\n\t\tif (source.ref) {\n\t\t\tawait this.runCommand(\"git\", [\"checkout\", source.ref], { cwd: targetDir });\n\t\t}\n\t\tconst packageJsonPath = join(targetDir, \"package.json\");\n\t\tif (existsSync(packageJsonPath)) {\n\t\t\tawait this.runCommand(\"npm\", [\"install\"], { cwd: targetDir });\n\t\t}\n\t}\n\n\tprivate async updateGit(source: GitSource, scope: SourceScope): Promise<void> {\n\t\tconst targetDir = this.getGitInstallPath(source, scope);\n\t\tif (!existsSync(targetDir)) {\n\t\t\tawait this.installGit(source, scope);\n\t\t\treturn;\n\t\t}\n\n\t\t// Fetch latest from remote (handles force-push by getting new history)\n\t\tawait this.runCommand(\"git\", [\"fetch\", \"--prune\", \"origin\"], { cwd: targetDir });\n\n\t\t// Reset to tracking branch. Fall back to origin/HEAD when no upstream is configured.\n\t\ttry {\n\t\t\tawait this.runCommand(\"git\", [\"reset\", \"--hard\", \"@{upstream}\"], { cwd: targetDir });\n\t\t} catch {\n\t\t\tawait this.runCommand(\"git\", [\"remote\", \"set-head\", \"origin\", \"-a\"], { cwd: targetDir }).catch(() => {});\n\t\t\tawait this.runCommand(\"git\", [\"reset\", \"--hard\", \"origin/HEAD\"], { cwd: targetDir });\n\t\t}\n\n\t\t// Clean untracked files (extensions should be pristine)\n\t\tawait this.runCommand(\"git\", [\"clean\", \"-fdx\"], { cwd: targetDir });\n\n\t\tconst packageJsonPath = join(targetDir, \"package.json\");\n\t\tif (existsSync(packageJsonPath)) {\n\t\t\tawait this.runCommand(\"npm\", [\"install\"], { cwd: targetDir });\n\t\t}\n\t}\n\n\tprivate async refreshTemporaryGitSource(source: GitSource, sourceStr: string): Promise<void> {\n\t\tif (isOfflineModeEnabled()) {\n\t\t\treturn;\n\t\t}\n\t\ttry {\n\t\t\tawait this.withProgress(\"pull\", sourceStr, `Refreshing ${sourceStr}...`, async () => {\n\t\t\t\tawait this.updateGit(source, \"temporary\");\n\t\t\t});\n\t\t} catch {\n\t\t\t// Keep cached temporary checkout if refresh fails.\n\t\t}\n\t}\n\n\tprivate async removeGit(source: GitSource, scope: SourceScope): Promise<void> {\n\t\tconst targetDir = this.getGitInstallPath(source, scope);\n\t\tif (!existsSync(targetDir)) return;\n\t\trmSync(targetDir, { recursive: true, force: true });\n\t\tthis.pruneEmptyGitParents(targetDir, this.getGitInstallRoot(scope));\n\t}\n\n\tprivate pruneEmptyGitParents(targetDir: string, installRoot: string | undefined): void {\n\t\tif (!installRoot) return;\n\t\tconst resolvedRoot = resolve(installRoot);\n\t\tlet current = dirname(targetDir);\n\t\twhile (current.startsWith(resolvedRoot) && current !== resolvedRoot) {\n\t\t\tif (!existsSync(current)) {\n\t\t\t\tcurrent = dirname(current);\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tconst entries = readdirSync(current);\n\t\t\tif (entries.length > 0) {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\ttry {\n\t\t\t\trmSync(current, { recursive: true, force: true });\n\t\t\t} catch {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcurrent = dirname(current);\n\t\t}\n\t}\n\n\tprivate ensureNpmProject(installRoot: string): void {\n\t\tif (!existsSync(installRoot)) {\n\t\t\tmkdirSync(installRoot, { recursive: true });\n\t\t}\n\t\tthis.ensureGitIgnore(installRoot);\n\t\tconst packageJsonPath = join(installRoot, \"package.json\");\n\t\tif (!existsSync(packageJsonPath)) {\n\t\t\tconst pkgJson = { name: \"pi-extensions\", private: true };\n\t\t\twriteFileSync(packageJsonPath, JSON.stringify(pkgJson, null, 2), \"utf-8\");\n\t\t}\n\t}\n\n\tprivate ensureGitIgnore(dir: string): void {\n\t\tif (!existsSync(dir)) {\n\t\t\tmkdirSync(dir, { recursive: true });\n\t\t}\n\t\tconst ignorePath = join(dir, \".gitignore\");\n\t\tif (!existsSync(ignorePath)) {\n\t\t\twriteFileSync(ignorePath, \"*\\n!.gitignore\\n\", \"utf-8\");\n\t\t}\n\t}\n\n\tprivate getNpmInstallRoot(scope: SourceScope, temporary: boolean): string {\n\t\tif (temporary) {\n\t\t\treturn this.getTemporaryDir(\"npm\");\n\t\t}\n\t\tif (scope === \"project\") {\n\t\t\treturn join(this.cwd, CONFIG_DIR_NAME, \"npm\");\n\t\t}\n\t\treturn join(this.getGlobalNpmRoot(), \"..\");\n\t}\n\n\tprivate getGlobalNpmRoot(): string {\n\t\tif (this.globalNpmRoot) {\n\t\t\treturn this.globalNpmRoot;\n\t\t}\n\t\tconst result = this.runCommandSync(\"npm\", [\"root\", \"-g\"]);\n\t\tthis.globalNpmRoot = result.trim();\n\t\treturn this.globalNpmRoot;\n\t}\n\n\tprivate getNpmInstallPath(source: NpmSource, scope: SourceScope): string {\n\t\tif (scope === \"temporary\") {\n\t\t\treturn join(this.getTemporaryDir(\"npm\"), \"node_modules\", source.name);\n\t\t}\n\t\tif (scope === \"project\") {\n\t\t\treturn join(this.cwd, CONFIG_DIR_NAME, \"npm\", \"node_modules\", source.name);\n\t\t}\n\t\treturn join(this.getGlobalNpmRoot(), source.name);\n\t}\n\n\tprivate getGitInstallPath(source: GitSource, scope: SourceScope): string {\n\t\tif (scope === \"temporary\") {\n\t\t\treturn this.getTemporaryDir(`git-${source.host}`, source.path);\n\t\t}\n\t\tif (scope === \"project\") {\n\t\t\treturn join(this.cwd, CONFIG_DIR_NAME, \"git\", source.host, source.path);\n\t\t}\n\t\treturn join(this.agentDir, \"git\", source.host, source.path);\n\t}\n\n\tprivate getGitInstallRoot(scope: SourceScope): string | undefined {\n\t\tif (scope === \"temporary\") {\n\t\t\treturn undefined;\n\t\t}\n\t\tif (scope === \"project\") {\n\t\t\treturn join(this.cwd, CONFIG_DIR_NAME, \"git\");\n\t\t}\n\t\treturn join(this.agentDir, \"git\");\n\t}\n\n\tprivate getTemporaryDir(prefix: string, suffix?: string): string {\n\t\tconst hash = createHash(\"sha256\")\n\t\t\t.update(`${prefix}-${suffix ?? \"\"}`)\n\t\t\t.digest(\"hex\")\n\t\t\t.slice(0, 8);\n\t\treturn join(tmpdir(), \"pi-extensions\", prefix, hash, suffix ?? \"\");\n\t}\n\n\tprivate getBaseDirForScope(scope: SourceScope): string {\n\t\tif (scope === \"project\") {\n\t\t\treturn join(this.cwd, CONFIG_DIR_NAME);\n\t\t}\n\t\tif (scope === \"user\") {\n\t\t\treturn this.agentDir;\n\t\t}\n\t\treturn this.cwd;\n\t}\n\n\tprivate resolvePath(input: string): string {\n\t\tconst trimmed = input.trim();\n\t\tif (trimmed === \"~\") return homedir();\n\t\tif (trimmed.startsWith(\"~/\")) return join(homedir(), trimmed.slice(2));\n\t\tif (trimmed.startsWith(\"~\")) return join(homedir(), trimmed.slice(1));\n\t\treturn resolve(this.cwd, trimmed);\n\t}\n\n\tprivate resolvePathFromBase(input: string, baseDir: string): string {\n\t\tconst trimmed = input.trim();\n\t\tif (trimmed === \"~\") return homedir();\n\t\tif (trimmed.startsWith(\"~/\")) return join(homedir(), trimmed.slice(2));\n\t\tif (trimmed.startsWith(\"~\")) return join(homedir(), trimmed.slice(1));\n\t\treturn resolve(baseDir, trimmed);\n\t}\n\n\tprivate collectPackageResources(\n\t\tpackageRoot: string,\n\t\taccumulator: ResourceAccumulator,\n\t\tfilter: PackageFilter | undefined,\n\t\tmetadata: PathMetadata,\n\t): boolean {\n\t\tif (filter) {\n\t\t\tfor (const resourceType of RESOURCE_TYPES) {\n\t\t\t\tconst patterns = filter[resourceType as keyof PackageFilter];\n\t\t\t\tconst target = this.getTargetMap(accumulator, resourceType);\n\t\t\t\tif (patterns !== undefined) {\n\t\t\t\t\tthis.applyPackageFilter(packageRoot, patterns, resourceType, target, metadata);\n\t\t\t\t} else {\n\t\t\t\t\tthis.collectDefaultResources(packageRoot, resourceType, target, metadata);\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn true;\n\t\t}\n\n\t\tconst manifest = this.readDrahtManifest(packageRoot);\n\t\tif (manifest) {\n\t\t\tfor (const resourceType of RESOURCE_TYPES) {\n\t\t\t\tconst entries = manifest[resourceType as keyof DrahtManifest];\n\t\t\t\tthis.addManifestEntries(\n\t\t\t\t\tentries,\n\t\t\t\t\tpackageRoot,\n\t\t\t\t\tresourceType,\n\t\t\t\t\tthis.getTargetMap(accumulator, resourceType),\n\t\t\t\t\tmetadata,\n\t\t\t\t);\n\t\t\t}\n\t\t\treturn true;\n\t\t}\n\n\t\tlet hasAnyDir = false;\n\t\tfor (const resourceType of RESOURCE_TYPES) {\n\t\t\tconst dir = join(packageRoot, resourceType);\n\t\t\tif (existsSync(dir)) {\n\t\t\t\t// Collect all files from the directory (all enabled by default)\n\t\t\t\tconst files = collectResourceFiles(dir, resourceType);\n\t\t\t\tfor (const f of files) {\n\t\t\t\t\tthis.addResource(this.getTargetMap(accumulator, resourceType), f, metadata, true);\n\t\t\t\t}\n\t\t\t\thasAnyDir = true;\n\t\t\t}\n\t\t}\n\t\treturn hasAnyDir;\n\t}\n\n\tprivate collectDefaultResources(\n\t\tpackageRoot: string,\n\t\tresourceType: ResourceType,\n\t\ttarget: Map<string, { metadata: PathMetadata; enabled: boolean }>,\n\t\tmetadata: PathMetadata,\n\t): void {\n\t\tconst manifest = this.readDrahtManifest(packageRoot);\n\t\tconst entries = manifest?.[resourceType as keyof DrahtManifest];\n\t\tif (entries) {\n\t\t\tthis.addManifestEntries(entries, packageRoot, resourceType, target, metadata);\n\t\t\treturn;\n\t\t}\n\t\tconst dir = join(packageRoot, resourceType);\n\t\tif (existsSync(dir)) {\n\t\t\t// Collect all files from the directory (all enabled by default)\n\t\t\tconst files = collectResourceFiles(dir, resourceType);\n\t\t\tfor (const f of files) {\n\t\t\t\tthis.addResource(target, f, metadata, true);\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate applyPackageFilter(\n\t\tpackageRoot: string,\n\t\tuserPatterns: string[],\n\t\tresourceType: ResourceType,\n\t\ttarget: Map<string, { metadata: PathMetadata; enabled: boolean }>,\n\t\tmetadata: PathMetadata,\n\t): void {\n\t\tconst { allFiles } = this.collectManifestFiles(packageRoot, resourceType);\n\n\t\tif (userPatterns.length === 0) {\n\t\t\t// Empty array explicitly disables all resources of this type\n\t\t\tfor (const f of allFiles) {\n\t\t\t\tthis.addResource(target, f, metadata, false);\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\n\t\t// Apply user patterns\n\t\tconst enabledByUser = applyPatterns(allFiles, userPatterns, packageRoot);\n\n\t\tfor (const f of allFiles) {\n\t\t\tconst enabled = enabledByUser.has(f);\n\t\t\tthis.addResource(target, f, metadata, enabled);\n\t\t}\n\t}\n\n\t/**\n\t * Collect all files from a package for a resource type, applying manifest patterns.\n\t * Returns { allFiles, enabledByManifest } where enabledByManifest is the set of files\n\t * that pass the manifest's own patterns.\n\t */\n\tprivate collectManifestFiles(\n\t\tpackageRoot: string,\n\t\tresourceType: ResourceType,\n\t): { allFiles: string[]; enabledByManifest: Set<string> } {\n\t\tconst manifest = this.readDrahtManifest(packageRoot);\n\t\tconst entries = manifest?.[resourceType as keyof DrahtManifest];\n\t\tif (entries && entries.length > 0) {\n\t\t\tconst allFiles = this.collectFilesFromManifestEntries(entries, packageRoot, resourceType);\n\t\t\tconst manifestPatterns = entries.filter(isPattern);\n\t\t\tconst enabledByManifest =\n\t\t\t\tmanifestPatterns.length > 0 ? applyPatterns(allFiles, manifestPatterns, packageRoot) : new Set(allFiles);\n\t\t\treturn { allFiles: Array.from(enabledByManifest), enabledByManifest };\n\t\t}\n\n\t\tconst conventionDir = join(packageRoot, resourceType);\n\t\tif (!existsSync(conventionDir)) {\n\t\t\treturn { allFiles: [], enabledByManifest: new Set() };\n\t\t}\n\t\tconst allFiles = collectResourceFiles(conventionDir, resourceType);\n\t\treturn { allFiles, enabledByManifest: new Set(allFiles) };\n\t}\n\n\tprivate readDrahtManifest(packageRoot: string): DrahtManifest | null {\n\t\tconst packageJsonPath = join(packageRoot, \"package.json\");\n\t\tif (!existsSync(packageJsonPath)) {\n\t\t\treturn null;\n\t\t}\n\n\t\ttry {\n\t\t\tconst content = readFileSync(packageJsonPath, \"utf-8\");\n\t\t\tconst pkg = JSON.parse(content) as { draht?: DrahtManifest };\n\t\t\treturn pkg.draht ?? null;\n\t\t} catch {\n\t\t\treturn null;\n\t\t}\n\t}\n\n\tprivate addManifestEntries(\n\t\tentries: string[] | undefined,\n\t\troot: string,\n\t\tresourceType: ResourceType,\n\t\ttarget: Map<string, { metadata: PathMetadata; enabled: boolean }>,\n\t\tmetadata: PathMetadata,\n\t): void {\n\t\tif (!entries) return;\n\n\t\tconst allFiles = this.collectFilesFromManifestEntries(entries, root, resourceType);\n\t\tconst patterns = entries.filter(isPattern);\n\t\tconst enabledPaths = applyPatterns(allFiles, patterns, root);\n\n\t\tfor (const f of allFiles) {\n\t\t\tif (enabledPaths.has(f)) {\n\t\t\t\tthis.addResource(target, f, metadata, true);\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate collectFilesFromManifestEntries(entries: string[], root: string, resourceType: ResourceType): string[] {\n\t\tconst plain = entries.filter((entry) => !isPattern(entry));\n\t\tconst resolved = plain.map((entry) => resolve(root, entry));\n\t\treturn this.collectFilesFromPaths(resolved, resourceType);\n\t}\n\n\tprivate resolveLocalEntries(\n\t\tentries: string[],\n\t\tresourceType: ResourceType,\n\t\ttarget: Map<string, { metadata: PathMetadata; enabled: boolean }>,\n\t\tmetadata: PathMetadata,\n\t\tbaseDir: string,\n\t): void {\n\t\tif (entries.length === 0) return;\n\n\t\t// Collect all files from plain entries (non-pattern entries)\n\t\tconst { plain, patterns } = splitPatterns(entries);\n\t\tconst resolvedPlain = plain.map((p) => this.resolvePathFromBase(p, baseDir));\n\t\tconst allFiles = this.collectFilesFromPaths(resolvedPlain, resourceType);\n\n\t\t// Determine which files are enabled based on patterns\n\t\tconst enabledPaths = applyPatterns(allFiles, patterns, baseDir);\n\n\t\t// Add all files with their enabled state\n\t\tfor (const f of allFiles) {\n\t\t\tthis.addResource(target, f, metadata, enabledPaths.has(f));\n\t\t}\n\t}\n\n\tprivate addAutoDiscoveredResources(\n\t\taccumulator: ResourceAccumulator,\n\t\tglobalSettings: ReturnType<SettingsManager[\"getGlobalSettings\"]>,\n\t\tprojectSettings: ReturnType<SettingsManager[\"getProjectSettings\"]>,\n\t\tglobalBaseDir: string,\n\t\tprojectBaseDir: string,\n\t): void {\n\t\tconst userMetadata: PathMetadata = {\n\t\t\tsource: \"auto\",\n\t\t\tscope: \"user\",\n\t\t\torigin: \"top-level\",\n\t\t\tbaseDir: globalBaseDir,\n\t\t};\n\t\tconst projectMetadata: PathMetadata = {\n\t\t\tsource: \"auto\",\n\t\t\tscope: \"project\",\n\t\t\torigin: \"top-level\",\n\t\t\tbaseDir: projectBaseDir,\n\t\t};\n\n\t\tconst userOverrides = {\n\t\t\textensions: (globalSettings.extensions ?? []) as string[],\n\t\t\tskills: (globalSettings.skills ?? []) as string[],\n\t\t\tprompts: (globalSettings.prompts ?? []) as string[],\n\t\t\tthemes: (globalSettings.themes ?? []) as string[],\n\t\t};\n\t\tconst projectOverrides = {\n\t\t\textensions: (projectSettings.extensions ?? []) as string[],\n\t\t\tskills: (projectSettings.skills ?? []) as string[],\n\t\t\tprompts: (projectSettings.prompts ?? []) as string[],\n\t\t\tthemes: (projectSettings.themes ?? []) as string[],\n\t\t};\n\n\t\tconst userDirs = {\n\t\t\textensions: join(globalBaseDir, \"extensions\"),\n\t\t\tskills: join(globalBaseDir, \"skills\"),\n\t\t\tprompts: join(globalBaseDir, \"prompts\"),\n\t\t\tthemes: join(globalBaseDir, \"themes\"),\n\t\t};\n\t\tconst projectDirs = {\n\t\t\textensions: join(projectBaseDir, \"extensions\"),\n\t\t\tskills: join(projectBaseDir, \"skills\"),\n\t\t\tprompts: join(projectBaseDir, \"prompts\"),\n\t\t\tthemes: join(projectBaseDir, \"themes\"),\n\t\t};\n\t\tconst userAgentsSkillsDir = join(homedir(), \".agents\", \"skills\");\n\t\tconst projectAgentsSkillDirs = collectAncestorAgentsSkillDirs(this.cwd);\n\n\t\tconst addResources = (\n\t\t\tresourceType: ResourceType,\n\t\t\tpaths: string[],\n\t\t\tmetadata: PathMetadata,\n\t\t\toverrides: string[],\n\t\t\tbaseDir: string,\n\t\t) => {\n\t\t\tconst target = this.getTargetMap(accumulator, resourceType);\n\t\t\tfor (const path of paths) {\n\t\t\t\tconst enabled = isEnabledByOverrides(path, overrides, baseDir);\n\t\t\t\tthis.addResource(target, path, metadata, enabled);\n\t\t\t}\n\t\t};\n\n\t\taddResources(\n\t\t\t\"extensions\",\n\t\t\tcollectAutoExtensionEntries(projectDirs.extensions),\n\t\t\tprojectMetadata,\n\t\t\tprojectOverrides.extensions,\n\t\t\tprojectBaseDir,\n\t\t);\n\t\taddResources(\n\t\t\t\"skills\",\n\t\t\t[\n\t\t\t\t...collectAutoSkillEntries(projectDirs.skills),\n\t\t\t\t...projectAgentsSkillDirs.flatMap((dir) => collectAutoSkillEntries(dir)),\n\t\t\t],\n\t\t\tprojectMetadata,\n\t\t\tprojectOverrides.skills,\n\t\t\tprojectBaseDir,\n\t\t);\n\t\taddResources(\n\t\t\t\"prompts\",\n\t\t\tcollectAutoPromptEntries(projectDirs.prompts),\n\t\t\tprojectMetadata,\n\t\t\tprojectOverrides.prompts,\n\t\t\tprojectBaseDir,\n\t\t);\n\t\taddResources(\n\t\t\t\"themes\",\n\t\t\tcollectAutoThemeEntries(projectDirs.themes),\n\t\t\tprojectMetadata,\n\t\t\tprojectOverrides.themes,\n\t\t\tprojectBaseDir,\n\t\t);\n\n\t\taddResources(\n\t\t\t\"extensions\",\n\t\t\tcollectAutoExtensionEntries(userDirs.extensions),\n\t\t\tuserMetadata,\n\t\t\tuserOverrides.extensions,\n\t\t\tglobalBaseDir,\n\t\t);\n\t\taddResources(\n\t\t\t\"skills\",\n\t\t\t[...collectAutoSkillEntries(userDirs.skills), ...collectAutoSkillEntries(userAgentsSkillsDir)],\n\t\t\tuserMetadata,\n\t\t\tuserOverrides.skills,\n\t\t\tglobalBaseDir,\n\t\t);\n\t\taddResources(\n\t\t\t\"prompts\",\n\t\t\tcollectAutoPromptEntries(userDirs.prompts),\n\t\t\tuserMetadata,\n\t\t\tuserOverrides.prompts,\n\t\t\tglobalBaseDir,\n\t\t);\n\t\taddResources(\n\t\t\t\"themes\",\n\t\t\tcollectAutoThemeEntries(userDirs.themes),\n\t\t\tuserMetadata,\n\t\t\tuserOverrides.themes,\n\t\t\tglobalBaseDir,\n\t\t);\n\t}\n\n\tprivate collectFilesFromPaths(paths: string[], resourceType: ResourceType): string[] {\n\t\tconst files: string[] = [];\n\t\tfor (const p of paths) {\n\t\t\tif (!existsSync(p)) continue;\n\n\t\t\ttry {\n\t\t\t\tconst stats = statSync(p);\n\t\t\t\tif (stats.isFile()) {\n\t\t\t\t\tfiles.push(p);\n\t\t\t\t} else if (stats.isDirectory()) {\n\t\t\t\t\tfiles.push(...collectResourceFiles(p, resourceType));\n\t\t\t\t}\n\t\t\t} catch {\n\t\t\t\t// Ignore errors\n\t\t\t}\n\t\t}\n\t\treturn files;\n\t}\n\n\tprivate getTargetMap(\n\t\taccumulator: ResourceAccumulator,\n\t\tresourceType: ResourceType,\n\t): Map<string, { metadata: PathMetadata; enabled: boolean }> {\n\t\tswitch (resourceType) {\n\t\t\tcase \"extensions\":\n\t\t\t\treturn accumulator.extensions;\n\t\t\tcase \"skills\":\n\t\t\t\treturn accumulator.skills;\n\t\t\tcase \"prompts\":\n\t\t\t\treturn accumulator.prompts;\n\t\t\tcase \"themes\":\n\t\t\t\treturn accumulator.themes;\n\t\t\tdefault:\n\t\t\t\tthrow new Error(`Unknown resource type: ${resourceType}`);\n\t\t}\n\t}\n\n\tprivate addResource(\n\t\tmap: Map<string, { metadata: PathMetadata; enabled: boolean }>,\n\t\tpath: string,\n\t\tmetadata: PathMetadata,\n\t\tenabled: boolean,\n\t): void {\n\t\tif (!path) return;\n\t\tif (!map.has(path)) {\n\t\t\tmap.set(path, { metadata, enabled });\n\t\t}\n\t}\n\n\tprivate createAccumulator(): ResourceAccumulator {\n\t\treturn {\n\t\t\textensions: new Map(),\n\t\t\tskills: new Map(),\n\t\t\tprompts: new Map(),\n\t\t\tthemes: new Map(),\n\t\t};\n\t}\n\n\tprivate toResolvedPaths(accumulator: ResourceAccumulator): ResolvedPaths {\n\t\tconst toResolved = (entries: Map<string, { metadata: PathMetadata; enabled: boolean }>): ResolvedResource[] => {\n\t\t\treturn Array.from(entries.entries()).map(([path, { metadata, enabled }]) => ({\n\t\t\t\tpath,\n\t\t\t\tenabled,\n\t\t\t\tmetadata,\n\t\t\t}));\n\t\t};\n\n\t\treturn {\n\t\t\textensions: toResolved(accumulator.extensions),\n\t\t\tskills: toResolved(accumulator.skills),\n\t\t\tprompts: toResolved(accumulator.prompts),\n\t\t\tthemes: toResolved(accumulator.themes),\n\t\t};\n\t}\n\n\tprivate runCommand(command: string, args: string[], options?: { cwd?: string }): Promise<void> {\n\t\treturn new Promise((resolvePromise, reject) => {\n\t\t\tconst child = spawn(command, args, {\n\t\t\t\tcwd: options?.cwd,\n\t\t\t\tstdio: \"inherit\",\n\t\t\t\tshell: process.platform === \"win32\",\n\t\t\t});\n\t\t\tchild.on(\"error\", reject);\n\t\t\tchild.on(\"exit\", (code) => {\n\t\t\t\tif (code === 0) {\n\t\t\t\t\tresolvePromise();\n\t\t\t\t} else {\n\t\t\t\t\treject(new Error(`${command} ${args.join(\" \")} failed with code ${code}`));\n\t\t\t\t}\n\t\t\t});\n\t\t});\n\t}\n\n\tprivate runCommandSync(command: string, args: string[]): string {\n\t\tconst result = spawnSync(command, args, {\n\t\t\tstdio: [\"ignore\", \"pipe\", \"pipe\"],\n\t\t\tencoding: \"utf-8\",\n\t\t\tshell: process.platform === \"win32\",\n\t\t});\n\t\tif (result.status !== 0) {\n\t\t\tthrow new Error(`Failed to run ${command} ${args.join(\" \")}: ${result.stderr || result.stdout}`);\n\t\t}\n\t\treturn (result.stdout || result.stderr || \"\").trim();\n\t}\n}\n"]}
1
+ {"version":3,"file":"package-manager.d.ts","sourceRoot":"","sources":["../../src/core/package-manager.ts"],"names":[],"mappings":"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","sourcesContent":["import { spawn, spawnSync } from \"node:child_process\";\nimport { createHash } from \"node:crypto\";\nimport { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from \"node:fs\";\nimport { homedir, tmpdir } from \"node:os\";\nimport { basename, dirname, join, relative, resolve, sep } from \"node:path\";\nimport ignore from \"ignore\";\nimport { minimatch } from \"minimatch\";\nimport { CONFIG_DIR_NAME, getShippedExtensionsDir } from \"../config.js\";\nimport { type GitSource, parseGitUrl } from \"../utils/git.js\";\nimport type { PackageSource, SettingsManager } from \"./settings-manager.js\";\n\nconst NETWORK_TIMEOUT_MS = 10000;\n\nfunction isOfflineModeEnabled(): boolean {\n\tconst value = process.env.DRAHT_OFFLINE;\n\tif (!value) return false;\n\treturn value === \"1\" || value.toLowerCase() === \"true\" || value.toLowerCase() === \"yes\";\n}\n\nexport interface PathMetadata {\n\tsource: string;\n\tscope: SourceScope;\n\torigin: \"package\" | \"top-level\";\n\tbaseDir?: string;\n}\n\nexport interface ResolvedResource {\n\tpath: string;\n\tenabled: boolean;\n\tmetadata: PathMetadata;\n}\n\nexport interface ResolvedPaths {\n\textensions: ResolvedResource[];\n\tskills: ResolvedResource[];\n\tprompts: ResolvedResource[];\n\tthemes: ResolvedResource[];\n}\n\nexport type MissingSourceAction = \"install\" | \"skip\" | \"error\";\n\nexport interface ProgressEvent {\n\ttype: \"start\" | \"progress\" | \"complete\" | \"error\";\n\taction: \"install\" | \"remove\" | \"update\" | \"clone\" | \"pull\";\n\tsource: string;\n\tmessage?: string;\n}\n\nexport type ProgressCallback = (event: ProgressEvent) => void;\n\nexport interface PackageManager {\n\tresolve(onMissing?: (source: string) => Promise<MissingSourceAction>): Promise<ResolvedPaths>;\n\tinstall(source: string, options?: { local?: boolean }): Promise<void>;\n\tremove(source: string, options?: { local?: boolean }): Promise<void>;\n\tupdate(source?: string): Promise<void>;\n\tresolveExtensionSources(\n\t\tsources: string[],\n\t\toptions?: { local?: boolean; temporary?: boolean },\n\t): Promise<ResolvedPaths>;\n\taddSourceToSettings(source: string, options?: { local?: boolean }): boolean;\n\tremoveSourceFromSettings(source: string, options?: { local?: boolean }): boolean;\n\tsetProgressCallback(callback: ProgressCallback | undefined): void;\n\tgetInstalledPath(source: string, scope: \"user\" | \"project\"): string | undefined;\n}\n\ninterface PackageManagerOptions {\n\tcwd: string;\n\tagentDir: string;\n\tsettingsManager: SettingsManager;\n}\n\ntype SourceScope = \"user\" | \"project\" | \"temporary\";\n\ntype NpmSource = {\n\ttype: \"npm\";\n\tspec: string;\n\tname: string;\n\tpinned: boolean;\n};\n\ntype LocalSource = {\n\ttype: \"local\";\n\tpath: string;\n};\n\ntype ParsedSource = NpmSource | GitSource | LocalSource;\n\ninterface DrahtManifest {\n\textensions?: string[];\n\tskills?: string[];\n\tprompts?: string[];\n\tthemes?: string[];\n}\n\ninterface ResourceAccumulator {\n\textensions: Map<string, { metadata: PathMetadata; enabled: boolean }>;\n\tskills: Map<string, { metadata: PathMetadata; enabled: boolean }>;\n\tprompts: Map<string, { metadata: PathMetadata; enabled: boolean }>;\n\tthemes: Map<string, { metadata: PathMetadata; enabled: boolean }>;\n}\n\ninterface PackageFilter {\n\textensions?: string[];\n\tskills?: string[];\n\tprompts?: string[];\n\tthemes?: string[];\n}\n\ntype ResourceType = \"extensions\" | \"skills\" | \"prompts\" | \"themes\";\n\nconst RESOURCE_TYPES: ResourceType[] = [\"extensions\", \"skills\", \"prompts\", \"themes\"];\n\nconst FILE_PATTERNS: Record<ResourceType, RegExp> = {\n\textensions: /\\.(ts|js)$/,\n\tskills: /\\.md$/,\n\tprompts: /\\.md$/,\n\tthemes: /\\.json$/,\n};\n\nconst IGNORE_FILE_NAMES = [\".gitignore\", \".ignore\", \".fdignore\"];\n\ntype IgnoreMatcher = ReturnType<typeof ignore>;\n\nfunction toPosixPath(p: string): string {\n\treturn p.split(sep).join(\"/\");\n}\n\nfunction prefixIgnorePattern(line: string, prefix: string): string | null {\n\tconst trimmed = line.trim();\n\tif (!trimmed) return null;\n\tif (trimmed.startsWith(\"#\") && !trimmed.startsWith(\"\\\\#\")) return null;\n\n\tlet pattern = line;\n\tlet negated = false;\n\n\tif (pattern.startsWith(\"!\")) {\n\t\tnegated = true;\n\t\tpattern = pattern.slice(1);\n\t} else if (pattern.startsWith(\"\\\\!\")) {\n\t\tpattern = pattern.slice(1);\n\t}\n\n\tif (pattern.startsWith(\"/\")) {\n\t\tpattern = pattern.slice(1);\n\t}\n\n\tconst prefixed = prefix ? `${prefix}${pattern}` : pattern;\n\treturn negated ? `!${prefixed}` : prefixed;\n}\n\nfunction addIgnoreRules(ig: IgnoreMatcher, dir: string, rootDir: string): void {\n\tconst relativeDir = relative(rootDir, dir);\n\tconst prefix = relativeDir ? `${toPosixPath(relativeDir)}/` : \"\";\n\n\tfor (const filename of IGNORE_FILE_NAMES) {\n\t\tconst ignorePath = join(dir, filename);\n\t\tif (!existsSync(ignorePath)) continue;\n\t\ttry {\n\t\t\tconst content = readFileSync(ignorePath, \"utf-8\");\n\t\t\tconst patterns = content\n\t\t\t\t.split(/\\r?\\n/)\n\t\t\t\t.map((line) => prefixIgnorePattern(line, prefix))\n\t\t\t\t.filter((line): line is string => Boolean(line));\n\t\t\tif (patterns.length > 0) {\n\t\t\t\tig.add(patterns);\n\t\t\t}\n\t\t} catch {}\n\t}\n}\n\nfunction isPattern(s: string): boolean {\n\treturn s.startsWith(\"!\") || s.startsWith(\"+\") || s.startsWith(\"-\") || s.includes(\"*\") || s.includes(\"?\");\n}\n\nfunction splitPatterns(entries: string[]): { plain: string[]; patterns: string[] } {\n\tconst plain: string[] = [];\n\tconst patterns: string[] = [];\n\tfor (const entry of entries) {\n\t\tif (isPattern(entry)) {\n\t\t\tpatterns.push(entry);\n\t\t} else {\n\t\t\tplain.push(entry);\n\t\t}\n\t}\n\treturn { plain, patterns };\n}\n\nfunction collectFiles(\n\tdir: string,\n\tfilePattern: RegExp,\n\tskipNodeModules = true,\n\tignoreMatcher?: IgnoreMatcher,\n\trootDir?: string,\n): string[] {\n\tconst files: string[] = [];\n\tif (!existsSync(dir)) return files;\n\n\tconst root = rootDir ?? dir;\n\tconst ig = ignoreMatcher ?? ignore();\n\taddIgnoreRules(ig, dir, root);\n\n\ttry {\n\t\tconst entries = readdirSync(dir, { withFileTypes: true });\n\t\tfor (const entry of entries) {\n\t\t\tif (entry.name.startsWith(\".\")) continue;\n\t\t\tif (skipNodeModules && entry.name === \"node_modules\") continue;\n\n\t\t\tconst fullPath = join(dir, entry.name);\n\t\t\tlet isDir = entry.isDirectory();\n\t\t\tlet isFile = entry.isFile();\n\n\t\t\tif (entry.isSymbolicLink()) {\n\t\t\t\ttry {\n\t\t\t\t\tconst stats = statSync(fullPath);\n\t\t\t\t\tisDir = stats.isDirectory();\n\t\t\t\t\tisFile = stats.isFile();\n\t\t\t\t} catch {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tconst relPath = toPosixPath(relative(root, fullPath));\n\t\t\tconst ignorePath = isDir ? `${relPath}/` : relPath;\n\t\t\tif (ig.ignores(ignorePath)) continue;\n\n\t\t\tif (isDir) {\n\t\t\t\tfiles.push(...collectFiles(fullPath, filePattern, skipNodeModules, ig, root));\n\t\t\t} else if (isFile && filePattern.test(entry.name)) {\n\t\t\t\tfiles.push(fullPath);\n\t\t\t}\n\t\t}\n\t} catch {\n\t\t// Ignore errors\n\t}\n\n\treturn files;\n}\n\nfunction collectSkillEntries(\n\tdir: string,\n\tincludeRootFiles = true,\n\tignoreMatcher?: IgnoreMatcher,\n\trootDir?: string,\n): string[] {\n\tconst entries: string[] = [];\n\tif (!existsSync(dir)) return entries;\n\n\tconst root = rootDir ?? dir;\n\tconst ig = ignoreMatcher ?? ignore();\n\taddIgnoreRules(ig, dir, root);\n\n\ttry {\n\t\tconst dirEntries = readdirSync(dir, { withFileTypes: true });\n\t\tfor (const entry of dirEntries) {\n\t\t\tif (entry.name.startsWith(\".\")) continue;\n\t\t\tif (entry.name === \"node_modules\") continue;\n\n\t\t\tconst fullPath = join(dir, entry.name);\n\t\t\tlet isDir = entry.isDirectory();\n\t\t\tlet isFile = entry.isFile();\n\n\t\t\tif (entry.isSymbolicLink()) {\n\t\t\t\ttry {\n\t\t\t\t\tconst stats = statSync(fullPath);\n\t\t\t\t\tisDir = stats.isDirectory();\n\t\t\t\t\tisFile = stats.isFile();\n\t\t\t\t} catch {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tconst relPath = toPosixPath(relative(root, fullPath));\n\t\t\tconst ignorePath = isDir ? `${relPath}/` : relPath;\n\t\t\tif (ig.ignores(ignorePath)) continue;\n\n\t\t\tif (isDir) {\n\t\t\t\tentries.push(...collectSkillEntries(fullPath, false, ig, root));\n\t\t\t} else if (isFile) {\n\t\t\t\tconst isRootMd = includeRootFiles && entry.name.endsWith(\".md\");\n\t\t\t\tconst isSkillMd = !includeRootFiles && entry.name === \"SKILL.md\";\n\t\t\t\tif (isRootMd || isSkillMd) {\n\t\t\t\t\tentries.push(fullPath);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t} catch {\n\t\t// Ignore errors\n\t}\n\n\treturn entries;\n}\n\nfunction collectAutoSkillEntries(dir: string, includeRootFiles = true): string[] {\n\treturn collectSkillEntries(dir, includeRootFiles);\n}\n\nfunction findGitRepoRoot(startDir: string): string | null {\n\tlet dir = resolve(startDir);\n\twhile (true) {\n\t\tif (existsSync(join(dir, \".git\"))) {\n\t\t\treturn dir;\n\t\t}\n\t\tconst parent = dirname(dir);\n\t\tif (parent === dir) {\n\t\t\treturn null;\n\t\t}\n\t\tdir = parent;\n\t}\n}\n\nfunction collectAncestorAgentsSkillDirs(startDir: string): string[] {\n\tconst skillDirs: string[] = [];\n\tconst resolvedStartDir = resolve(startDir);\n\tconst gitRepoRoot = findGitRepoRoot(resolvedStartDir);\n\n\tlet dir = resolvedStartDir;\n\twhile (true) {\n\t\tskillDirs.push(join(dir, \".agents\", \"skills\"));\n\t\tif (gitRepoRoot && dir === gitRepoRoot) {\n\t\t\tbreak;\n\t\t}\n\t\tconst parent = dirname(dir);\n\t\tif (parent === dir) {\n\t\t\tbreak;\n\t\t}\n\t\tdir = parent;\n\t}\n\n\treturn skillDirs;\n}\n\nfunction collectAutoPromptEntries(dir: string): string[] {\n\tconst entries: string[] = [];\n\tif (!existsSync(dir)) return entries;\n\n\tconst ig = ignore();\n\taddIgnoreRules(ig, dir, dir);\n\n\ttry {\n\t\tconst dirEntries = readdirSync(dir, { withFileTypes: true });\n\t\tfor (const entry of dirEntries) {\n\t\t\tif (entry.name.startsWith(\".\")) continue;\n\t\t\tif (entry.name === \"node_modules\") continue;\n\n\t\t\tconst fullPath = join(dir, entry.name);\n\t\t\tlet isFile = entry.isFile();\n\t\t\tif (entry.isSymbolicLink()) {\n\t\t\t\ttry {\n\t\t\t\t\tisFile = statSync(fullPath).isFile();\n\t\t\t\t} catch {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tconst relPath = toPosixPath(relative(dir, fullPath));\n\t\t\tif (ig.ignores(relPath)) continue;\n\n\t\t\tif (isFile && entry.name.endsWith(\".md\")) {\n\t\t\t\tentries.push(fullPath);\n\t\t\t}\n\t\t}\n\t} catch {\n\t\t// Ignore errors\n\t}\n\n\treturn entries;\n}\n\nfunction collectAutoThemeEntries(dir: string): string[] {\n\tconst entries: string[] = [];\n\tif (!existsSync(dir)) return entries;\n\n\tconst ig = ignore();\n\taddIgnoreRules(ig, dir, dir);\n\n\ttry {\n\t\tconst dirEntries = readdirSync(dir, { withFileTypes: true });\n\t\tfor (const entry of dirEntries) {\n\t\t\tif (entry.name.startsWith(\".\")) continue;\n\t\t\tif (entry.name === \"node_modules\") continue;\n\n\t\t\tconst fullPath = join(dir, entry.name);\n\t\t\tlet isFile = entry.isFile();\n\t\t\tif (entry.isSymbolicLink()) {\n\t\t\t\ttry {\n\t\t\t\t\tisFile = statSync(fullPath).isFile();\n\t\t\t\t} catch {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tconst relPath = toPosixPath(relative(dir, fullPath));\n\t\t\tif (ig.ignores(relPath)) continue;\n\n\t\t\tif (isFile && entry.name.endsWith(\".json\")) {\n\t\t\t\tentries.push(fullPath);\n\t\t\t}\n\t\t}\n\t} catch {\n\t\t// Ignore errors\n\t}\n\n\treturn entries;\n}\n\nfunction readDrahtManifestFile(packageJsonPath: string): DrahtManifest | null {\n\ttry {\n\t\tconst content = readFileSync(packageJsonPath, \"utf-8\");\n\t\tconst pkg = JSON.parse(content) as { draht?: DrahtManifest };\n\t\treturn pkg.draht ?? null;\n\t} catch {\n\t\treturn null;\n\t}\n}\n\nfunction resolveExtensionEntries(dir: string): string[] | null {\n\tconst packageJsonPath = join(dir, \"package.json\");\n\tif (existsSync(packageJsonPath)) {\n\t\tconst manifest = readDrahtManifestFile(packageJsonPath);\n\t\tif (manifest?.extensions?.length) {\n\t\t\tconst entries: string[] = [];\n\t\t\tfor (const extPath of manifest.extensions) {\n\t\t\t\tconst resolvedExtPath = resolve(dir, extPath);\n\t\t\t\tif (existsSync(resolvedExtPath)) {\n\t\t\t\t\tentries.push(resolvedExtPath);\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (entries.length > 0) {\n\t\t\t\treturn entries;\n\t\t\t}\n\t\t}\n\t}\n\n\tconst indexTs = join(dir, \"index.ts\");\n\tconst indexJs = join(dir, \"index.js\");\n\tif (existsSync(indexTs)) {\n\t\treturn [indexTs];\n\t}\n\tif (existsSync(indexJs)) {\n\t\treturn [indexJs];\n\t}\n\n\treturn null;\n}\n\nfunction collectAutoExtensionEntries(dir: string): string[] {\n\tconst entries: string[] = [];\n\tif (!existsSync(dir)) return entries;\n\n\t// First check if this directory itself has explicit extension entries (package.json or index)\n\tconst rootEntries = resolveExtensionEntries(dir);\n\tif (rootEntries) {\n\t\treturn rootEntries;\n\t}\n\n\t// Otherwise, discover extensions from directory contents\n\tconst ig = ignore();\n\taddIgnoreRules(ig, dir, dir);\n\n\ttry {\n\t\tconst dirEntries = readdirSync(dir, { withFileTypes: true });\n\t\tfor (const entry of dirEntries) {\n\t\t\tif (entry.name.startsWith(\".\")) continue;\n\t\t\tif (entry.name === \"node_modules\") continue;\n\n\t\t\tconst fullPath = join(dir, entry.name);\n\t\t\tlet isDir = entry.isDirectory();\n\t\t\tlet isFile = entry.isFile();\n\n\t\t\tif (entry.isSymbolicLink()) {\n\t\t\t\ttry {\n\t\t\t\t\tconst stats = statSync(fullPath);\n\t\t\t\t\tisDir = stats.isDirectory();\n\t\t\t\t\tisFile = stats.isFile();\n\t\t\t\t} catch {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tconst relPath = toPosixPath(relative(dir, fullPath));\n\t\t\tconst ignorePath = isDir ? `${relPath}/` : relPath;\n\t\t\tif (ig.ignores(ignorePath)) continue;\n\n\t\t\tif (isFile && (entry.name.endsWith(\".ts\") || entry.name.endsWith(\".js\"))) {\n\t\t\t\tentries.push(fullPath);\n\t\t\t} else if (isDir) {\n\t\t\t\tconst resolvedEntries = resolveExtensionEntries(fullPath);\n\t\t\t\tif (resolvedEntries) {\n\t\t\t\t\tentries.push(...resolvedEntries);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t} catch {\n\t\t// Ignore errors\n\t}\n\n\treturn entries;\n}\n\n/**\n * Collect resource files from a directory based on resource type.\n * Extensions use smart discovery (index.ts in subdirs), others use recursive collection.\n */\nfunction collectResourceFiles(dir: string, resourceType: ResourceType): string[] {\n\tif (resourceType === \"skills\") {\n\t\treturn collectSkillEntries(dir);\n\t}\n\tif (resourceType === \"extensions\") {\n\t\treturn collectAutoExtensionEntries(dir);\n\t}\n\treturn collectFiles(dir, FILE_PATTERNS[resourceType]);\n}\n\nfunction matchesAnyPattern(filePath: string, patterns: string[], baseDir: string): boolean {\n\tconst rel = relative(baseDir, filePath);\n\tconst name = basename(filePath);\n\tconst isSkillFile = name === \"SKILL.md\";\n\tconst parentDir = isSkillFile ? dirname(filePath) : undefined;\n\tconst parentRel = isSkillFile ? relative(baseDir, parentDir!) : undefined;\n\tconst parentName = isSkillFile ? basename(parentDir!) : undefined;\n\n\treturn patterns.some((pattern) => {\n\t\tif (minimatch(rel, pattern) || minimatch(name, pattern) || minimatch(filePath, pattern)) {\n\t\t\treturn true;\n\t\t}\n\t\tif (!isSkillFile) return false;\n\t\treturn minimatch(parentRel!, pattern) || minimatch(parentName!, pattern) || minimatch(parentDir!, pattern);\n\t});\n}\n\nfunction normalizeExactPattern(pattern: string): string {\n\tif (pattern.startsWith(\"./\") || pattern.startsWith(\".\\\\\")) {\n\t\treturn pattern.slice(2);\n\t}\n\treturn pattern;\n}\n\nfunction matchesAnyExactPattern(filePath: string, patterns: string[], baseDir: string): boolean {\n\tif (patterns.length === 0) return false;\n\tconst rel = relative(baseDir, filePath);\n\tconst name = basename(filePath);\n\tconst isSkillFile = name === \"SKILL.md\";\n\tconst parentDir = isSkillFile ? dirname(filePath) : undefined;\n\tconst parentRel = isSkillFile ? relative(baseDir, parentDir!) : undefined;\n\n\treturn patterns.some((pattern) => {\n\t\tconst normalized = normalizeExactPattern(pattern);\n\t\tif (normalized === rel || normalized === filePath) {\n\t\t\treturn true;\n\t\t}\n\t\tif (!isSkillFile) return false;\n\t\treturn normalized === parentRel || normalized === parentDir;\n\t});\n}\n\nfunction getOverridePatterns(entries: string[]): string[] {\n\treturn entries.filter((pattern) => pattern.startsWith(\"!\") || pattern.startsWith(\"+\") || pattern.startsWith(\"-\"));\n}\n\nfunction isEnabledByOverrides(filePath: string, patterns: string[], baseDir: string): boolean {\n\tconst overrides = getOverridePatterns(patterns);\n\tconst excludes = overrides.filter((pattern) => pattern.startsWith(\"!\")).map((pattern) => pattern.slice(1));\n\tconst forceIncludes = overrides.filter((pattern) => pattern.startsWith(\"+\")).map((pattern) => pattern.slice(1));\n\tconst forceExcludes = overrides.filter((pattern) => pattern.startsWith(\"-\")).map((pattern) => pattern.slice(1));\n\n\tlet enabled = true;\n\tif (excludes.length > 0 && matchesAnyPattern(filePath, excludes, baseDir)) {\n\t\tenabled = false;\n\t}\n\tif (forceIncludes.length > 0 && matchesAnyExactPattern(filePath, forceIncludes, baseDir)) {\n\t\tenabled = true;\n\t}\n\tif (forceExcludes.length > 0 && matchesAnyExactPattern(filePath, forceExcludes, baseDir)) {\n\t\tenabled = false;\n\t}\n\treturn enabled;\n}\n\n/**\n * Apply patterns to paths and return a Set of enabled paths.\n * Pattern types:\n * - Plain patterns: include matching paths\n * - `!pattern`: exclude matching paths\n * - `+path`: force-include exact path (overrides exclusions)\n * - `-path`: force-exclude exact path (overrides force-includes)\n */\nfunction applyPatterns(allPaths: string[], patterns: string[], baseDir: string): Set<string> {\n\tconst includes: string[] = [];\n\tconst excludes: string[] = [];\n\tconst forceIncludes: string[] = [];\n\tconst forceExcludes: string[] = [];\n\n\tfor (const p of patterns) {\n\t\tif (p.startsWith(\"+\")) {\n\t\t\tforceIncludes.push(p.slice(1));\n\t\t} else if (p.startsWith(\"-\")) {\n\t\t\tforceExcludes.push(p.slice(1));\n\t\t} else if (p.startsWith(\"!\")) {\n\t\t\texcludes.push(p.slice(1));\n\t\t} else {\n\t\t\tincludes.push(p);\n\t\t}\n\t}\n\n\t// Step 1: Apply includes (or all if no includes)\n\tlet result: string[];\n\tif (includes.length === 0) {\n\t\tresult = [...allPaths];\n\t} else {\n\t\tresult = allPaths.filter((filePath) => matchesAnyPattern(filePath, includes, baseDir));\n\t}\n\n\t// Step 2: Apply excludes\n\tif (excludes.length > 0) {\n\t\tresult = result.filter((filePath) => !matchesAnyPattern(filePath, excludes, baseDir));\n\t}\n\n\t// Step 3: Force-include (add back from allPaths, overriding exclusions)\n\tif (forceIncludes.length > 0) {\n\t\tfor (const filePath of allPaths) {\n\t\t\tif (!result.includes(filePath) && matchesAnyExactPattern(filePath, forceIncludes, baseDir)) {\n\t\t\t\tresult.push(filePath);\n\t\t\t}\n\t\t}\n\t}\n\n\t// Step 4: Force-exclude (remove even if included or force-included)\n\tif (forceExcludes.length > 0) {\n\t\tresult = result.filter((filePath) => !matchesAnyExactPattern(filePath, forceExcludes, baseDir));\n\t}\n\n\treturn new Set(result);\n}\n\nexport class DefaultPackageManager implements PackageManager {\n\tprivate cwd: string;\n\tprivate agentDir: string;\n\tprivate settingsManager: SettingsManager;\n\tprivate globalNpmRoot: string | undefined;\n\tprivate progressCallback: ProgressCallback | undefined;\n\n\tconstructor(options: PackageManagerOptions) {\n\t\tthis.cwd = options.cwd;\n\t\tthis.agentDir = options.agentDir;\n\t\tthis.settingsManager = options.settingsManager;\n\t}\n\n\tsetProgressCallback(callback: ProgressCallback | undefined): void {\n\t\tthis.progressCallback = callback;\n\t}\n\n\taddSourceToSettings(source: string, options?: { local?: boolean }): boolean {\n\t\tconst scope: SourceScope = options?.local ? \"project\" : \"user\";\n\t\tconst currentSettings =\n\t\t\tscope === \"project\" ? this.settingsManager.getProjectSettings() : this.settingsManager.getGlobalSettings();\n\t\tconst currentPackages = currentSettings.packages ?? [];\n\t\tconst normalizedSource = this.normalizePackageSourceForSettings(source, scope);\n\t\tconst exists = currentPackages.some((existing) => this.packageSourcesMatch(existing, source, scope));\n\t\tif (exists) {\n\t\t\treturn false;\n\t\t}\n\t\tconst nextPackages = [...currentPackages, normalizedSource];\n\t\tif (scope === \"project\") {\n\t\t\tthis.settingsManager.setProjectPackages(nextPackages);\n\t\t} else {\n\t\t\tthis.settingsManager.setPackages(nextPackages);\n\t\t}\n\t\treturn true;\n\t}\n\n\tremoveSourceFromSettings(source: string, options?: { local?: boolean }): boolean {\n\t\tconst scope: SourceScope = options?.local ? \"project\" : \"user\";\n\t\tconst currentSettings =\n\t\t\tscope === \"project\" ? this.settingsManager.getProjectSettings() : this.settingsManager.getGlobalSettings();\n\t\tconst currentPackages = currentSettings.packages ?? [];\n\t\tconst nextPackages = currentPackages.filter((existing) => !this.packageSourcesMatch(existing, source, scope));\n\t\tconst changed = nextPackages.length !== currentPackages.length;\n\t\tif (!changed) {\n\t\t\treturn false;\n\t\t}\n\t\tif (scope === \"project\") {\n\t\t\tthis.settingsManager.setProjectPackages(nextPackages);\n\t\t} else {\n\t\t\tthis.settingsManager.setPackages(nextPackages);\n\t\t}\n\t\treturn true;\n\t}\n\n\tgetInstalledPath(source: string, scope: \"user\" | \"project\"): string | undefined {\n\t\tconst parsed = this.parseSource(source);\n\t\tif (parsed.type === \"npm\") {\n\t\t\tconst path = this.getNpmInstallPath(parsed, scope);\n\t\t\treturn existsSync(path) ? path : undefined;\n\t\t}\n\t\tif (parsed.type === \"git\") {\n\t\t\tconst path = this.getGitInstallPath(parsed, scope);\n\t\t\treturn existsSync(path) ? path : undefined;\n\t\t}\n\t\tif (parsed.type === \"local\") {\n\t\t\tconst baseDir = this.getBaseDirForScope(scope);\n\t\t\tconst path = this.resolvePathFromBase(parsed.path, baseDir);\n\t\t\treturn existsSync(path) ? path : undefined;\n\t\t}\n\t\treturn undefined;\n\t}\n\n\tprivate emitProgress(event: ProgressEvent): void {\n\t\tthis.progressCallback?.(event);\n\t}\n\n\tprivate async withProgress(\n\t\taction: ProgressEvent[\"action\"],\n\t\tsource: string,\n\t\tmessage: string,\n\t\toperation: () => Promise<void>,\n\t): Promise<void> {\n\t\tthis.emitProgress({ type: \"start\", action, source, message });\n\t\ttry {\n\t\t\tawait operation();\n\t\t\tthis.emitProgress({ type: \"complete\", action, source });\n\t\t} catch (error) {\n\t\t\tconst errorMessage = error instanceof Error ? error.message : String(error);\n\t\t\tthis.emitProgress({ type: \"error\", action, source, message: errorMessage });\n\t\t\tthrow error;\n\t\t}\n\t}\n\n\tasync resolve(onMissing?: (source: string) => Promise<MissingSourceAction>): Promise<ResolvedPaths> {\n\t\tconst accumulator = this.createAccumulator();\n\t\tconst globalSettings = this.settingsManager.getGlobalSettings();\n\t\tconst projectSettings = this.settingsManager.getProjectSettings();\n\n\t\t// Collect all packages with scope (project first so cwd resources win collisions)\n\t\tconst allPackages: Array<{ pkg: PackageSource; scope: SourceScope }> = [];\n\t\tfor (const pkg of projectSettings.packages ?? []) {\n\t\t\tallPackages.push({ pkg, scope: \"project\" });\n\t\t}\n\t\tfor (const pkg of globalSettings.packages ?? []) {\n\t\t\tallPackages.push({ pkg, scope: \"user\" });\n\t\t}\n\n\t\t// Dedupe: project scope wins over global for same package identity\n\t\tconst packageSources = this.dedupePackages(allPackages);\n\t\tawait this.resolvePackageSources(packageSources, accumulator, onMissing);\n\n\t\tconst globalBaseDir = this.agentDir;\n\t\tconst projectBaseDir = join(this.cwd, CONFIG_DIR_NAME);\n\n\t\tfor (const resourceType of RESOURCE_TYPES) {\n\t\t\tconst target = this.getTargetMap(accumulator, resourceType);\n\t\t\tconst globalEntries = (globalSettings[resourceType] ?? []) as string[];\n\t\t\tconst projectEntries = (projectSettings[resourceType] ?? []) as string[];\n\t\t\tthis.resolveLocalEntries(\n\t\t\t\tprojectEntries,\n\t\t\t\tresourceType,\n\t\t\t\ttarget,\n\t\t\t\t{\n\t\t\t\t\tsource: \"local\",\n\t\t\t\t\tscope: \"project\",\n\t\t\t\t\torigin: \"top-level\",\n\t\t\t\t},\n\t\t\t\tprojectBaseDir,\n\t\t\t);\n\t\t\tthis.resolveLocalEntries(\n\t\t\t\tglobalEntries,\n\t\t\t\tresourceType,\n\t\t\t\ttarget,\n\t\t\t\t{\n\t\t\t\t\tsource: \"local\",\n\t\t\t\t\tscope: \"user\",\n\t\t\t\t\torigin: \"top-level\",\n\t\t\t\t},\n\t\t\t\tglobalBaseDir,\n\t\t\t);\n\t\t}\n\n\t\tthis.addAutoDiscoveredResources(accumulator, globalSettings, projectSettings, globalBaseDir, projectBaseDir);\n\n\t\treturn this.toResolvedPaths(accumulator);\n\t}\n\n\tasync resolveExtensionSources(\n\t\tsources: string[],\n\t\toptions?: { local?: boolean; temporary?: boolean },\n\t): Promise<ResolvedPaths> {\n\t\tconst accumulator = this.createAccumulator();\n\t\tconst scope: SourceScope = options?.temporary ? \"temporary\" : options?.local ? \"project\" : \"user\";\n\t\tconst packageSources = sources.map((source) => ({ pkg: source as PackageSource, scope }));\n\t\tawait this.resolvePackageSources(packageSources, accumulator);\n\t\treturn this.toResolvedPaths(accumulator);\n\t}\n\n\tasync install(source: string, options?: { local?: boolean }): Promise<void> {\n\t\tconst parsed = this.parseSource(source);\n\t\tconst scope: SourceScope = options?.local ? \"project\" : \"user\";\n\t\tawait this.withProgress(\"install\", source, `Installing ${source}...`, async () => {\n\t\t\tif (parsed.type === \"npm\") {\n\t\t\t\tawait this.installNpm(parsed, scope, false);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (parsed.type === \"git\") {\n\t\t\t\tawait this.installGit(parsed, scope);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (parsed.type === \"local\") {\n\t\t\t\tconst resolved = this.resolvePath(parsed.path);\n\t\t\t\tif (!existsSync(resolved)) {\n\t\t\t\t\tthrow new Error(`Path does not exist: ${resolved}`);\n\t\t\t\t}\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tthrow new Error(`Unsupported install source: ${source}`);\n\t\t});\n\t}\n\n\tasync remove(source: string, options?: { local?: boolean }): Promise<void> {\n\t\tconst parsed = this.parseSource(source);\n\t\tconst scope: SourceScope = options?.local ? \"project\" : \"user\";\n\t\tawait this.withProgress(\"remove\", source, `Removing ${source}...`, async () => {\n\t\t\tif (parsed.type === \"npm\") {\n\t\t\t\tawait this.uninstallNpm(parsed, scope);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (parsed.type === \"git\") {\n\t\t\t\tawait this.removeGit(parsed, scope);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (parsed.type === \"local\") {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tthrow new Error(`Unsupported remove source: ${source}`);\n\t\t});\n\t}\n\n\tasync update(source?: string): Promise<void> {\n\t\tconst globalSettings = this.settingsManager.getGlobalSettings();\n\t\tconst projectSettings = this.settingsManager.getProjectSettings();\n\t\tconst identity = source ? this.getPackageIdentity(source) : undefined;\n\n\t\tfor (const pkg of globalSettings.packages ?? []) {\n\t\t\tconst sourceStr = typeof pkg === \"string\" ? pkg : pkg.source;\n\t\t\tif (identity && this.getPackageIdentity(sourceStr, \"user\") !== identity) continue;\n\t\t\tawait this.updateSourceForScope(sourceStr, \"user\");\n\t\t}\n\t\tfor (const pkg of projectSettings.packages ?? []) {\n\t\t\tconst sourceStr = typeof pkg === \"string\" ? pkg : pkg.source;\n\t\t\tif (identity && this.getPackageIdentity(sourceStr, \"project\") !== identity) continue;\n\t\t\tawait this.updateSourceForScope(sourceStr, \"project\");\n\t\t}\n\t}\n\n\tprivate async updateSourceForScope(source: string, scope: SourceScope): Promise<void> {\n\t\tif (isOfflineModeEnabled()) {\n\t\t\treturn;\n\t\t}\n\t\tconst parsed = this.parseSource(source);\n\t\tif (parsed.type === \"npm\") {\n\t\t\tif (parsed.pinned) return;\n\t\t\tawait this.withProgress(\"update\", source, `Updating ${source}...`, async () => {\n\t\t\t\tawait this.installNpm(parsed, scope, false);\n\t\t\t});\n\t\t\treturn;\n\t\t}\n\t\tif (parsed.type === \"git\") {\n\t\t\tif (parsed.pinned) return;\n\t\t\tawait this.withProgress(\"update\", source, `Updating ${source}...`, async () => {\n\t\t\t\tawait this.updateGit(parsed, scope);\n\t\t\t});\n\t\t\treturn;\n\t\t}\n\t}\n\n\tprivate async resolvePackageSources(\n\t\tsources: Array<{ pkg: PackageSource; scope: SourceScope }>,\n\t\taccumulator: ResourceAccumulator,\n\t\tonMissing?: (source: string) => Promise<MissingSourceAction>,\n\t): Promise<void> {\n\t\tfor (const { pkg, scope } of sources) {\n\t\t\tconst sourceStr = typeof pkg === \"string\" ? pkg : pkg.source;\n\t\t\tconst filter = typeof pkg === \"object\" ? pkg : undefined;\n\t\t\tconst parsed = this.parseSource(sourceStr);\n\t\t\tconst metadata: PathMetadata = { source: sourceStr, scope, origin: \"package\" };\n\n\t\t\tif (parsed.type === \"local\") {\n\t\t\t\tconst baseDir = this.getBaseDirForScope(scope);\n\t\t\t\tthis.resolveLocalExtensionSource(parsed, accumulator, filter, metadata, baseDir);\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tconst installMissing = async (): Promise<boolean> => {\n\t\t\t\tif (isOfflineModeEnabled()) {\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\t\t\t\tif (!onMissing) {\n\t\t\t\t\tawait this.installParsedSource(parsed, scope);\n\t\t\t\t\treturn true;\n\t\t\t\t}\n\t\t\t\tconst action = await onMissing(sourceStr);\n\t\t\t\tif (action === \"skip\") return false;\n\t\t\t\tif (action === \"error\") throw new Error(`Missing source: ${sourceStr}`);\n\t\t\t\tawait this.installParsedSource(parsed, scope);\n\t\t\t\treturn true;\n\t\t\t};\n\n\t\t\tif (parsed.type === \"npm\") {\n\t\t\t\tconst installedPath = this.getNpmInstallPath(parsed, scope);\n\t\t\t\tconst needsInstall = !existsSync(installedPath) || (await this.npmNeedsUpdate(parsed, installedPath));\n\t\t\t\tif (needsInstall) {\n\t\t\t\t\tconst installed = await installMissing();\n\t\t\t\t\tif (!installed) continue;\n\t\t\t\t}\n\t\t\t\tmetadata.baseDir = installedPath;\n\t\t\t\tthis.collectPackageResources(installedPath, accumulator, filter, metadata);\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tif (parsed.type === \"git\") {\n\t\t\t\tconst installedPath = this.getGitInstallPath(parsed, scope);\n\t\t\t\tif (!existsSync(installedPath)) {\n\t\t\t\t\tconst installed = await installMissing();\n\t\t\t\t\tif (!installed) continue;\n\t\t\t\t} else if (scope === \"temporary\" && !parsed.pinned && !isOfflineModeEnabled()) {\n\t\t\t\t\tawait this.refreshTemporaryGitSource(parsed, sourceStr);\n\t\t\t\t}\n\t\t\t\tmetadata.baseDir = installedPath;\n\t\t\t\tthis.collectPackageResources(installedPath, accumulator, filter, metadata);\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate resolveLocalExtensionSource(\n\t\tsource: LocalSource,\n\t\taccumulator: ResourceAccumulator,\n\t\tfilter: PackageFilter | undefined,\n\t\tmetadata: PathMetadata,\n\t\tbaseDir: string,\n\t): void {\n\t\tconst resolved = this.resolvePathFromBase(source.path, baseDir);\n\t\tif (!existsSync(resolved)) {\n\t\t\treturn;\n\t\t}\n\n\t\ttry {\n\t\t\tconst stats = statSync(resolved);\n\t\t\tif (stats.isFile()) {\n\t\t\t\tmetadata.baseDir = dirname(resolved);\n\t\t\t\tthis.addResource(accumulator.extensions, resolved, metadata, true);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (stats.isDirectory()) {\n\t\t\t\tmetadata.baseDir = resolved;\n\t\t\t\tconst resources = this.collectPackageResources(resolved, accumulator, filter, metadata);\n\t\t\t\tif (!resources) {\n\t\t\t\t\tthis.addResource(accumulator.extensions, resolved, metadata, true);\n\t\t\t\t}\n\t\t\t}\n\t\t} catch {\n\t\t\treturn;\n\t\t}\n\t}\n\n\tprivate async installParsedSource(parsed: ParsedSource, scope: SourceScope): Promise<void> {\n\t\tif (parsed.type === \"npm\") {\n\t\t\tawait this.installNpm(parsed, scope, scope === \"temporary\");\n\t\t\treturn;\n\t\t}\n\t\tif (parsed.type === \"git\") {\n\t\t\tawait this.installGit(parsed, scope);\n\t\t\treturn;\n\t\t}\n\t}\n\n\tprivate getPackageSourceString(pkg: PackageSource): string {\n\t\treturn typeof pkg === \"string\" ? pkg : pkg.source;\n\t}\n\n\tprivate getSourceMatchKeyForInput(source: string): string {\n\t\tconst parsed = this.parseSource(source);\n\t\tif (parsed.type === \"npm\") {\n\t\t\treturn `npm:${parsed.name}`;\n\t\t}\n\t\tif (parsed.type === \"git\") {\n\t\t\treturn `git:${parsed.host}/${parsed.path}`;\n\t\t}\n\t\treturn `local:${this.resolvePath(parsed.path)}`;\n\t}\n\n\tprivate getSourceMatchKeyForSettings(source: string, scope: SourceScope): string {\n\t\tconst parsed = this.parseSource(source);\n\t\tif (parsed.type === \"npm\") {\n\t\t\treturn `npm:${parsed.name}`;\n\t\t}\n\t\tif (parsed.type === \"git\") {\n\t\t\treturn `git:${parsed.host}/${parsed.path}`;\n\t\t}\n\t\tconst baseDir = this.getBaseDirForScope(scope);\n\t\treturn `local:${this.resolvePathFromBase(parsed.path, baseDir)}`;\n\t}\n\n\tprivate packageSourcesMatch(existing: PackageSource, inputSource: string, scope: SourceScope): boolean {\n\t\tconst left = this.getSourceMatchKeyForSettings(this.getPackageSourceString(existing), scope);\n\t\tconst right = this.getSourceMatchKeyForInput(inputSource);\n\t\treturn left === right;\n\t}\n\n\tprivate normalizePackageSourceForSettings(source: string, scope: SourceScope): string {\n\t\tconst parsed = this.parseSource(source);\n\t\tif (parsed.type !== \"local\") {\n\t\t\treturn source;\n\t\t}\n\t\tconst baseDir = this.getBaseDirForScope(scope);\n\t\tconst resolved = this.resolvePath(parsed.path);\n\t\tconst rel = relative(baseDir, resolved);\n\t\treturn rel || \".\";\n\t}\n\n\tprivate parseSource(source: string): ParsedSource {\n\t\tif (source.startsWith(\"npm:\")) {\n\t\t\tconst spec = source.slice(\"npm:\".length).trim();\n\t\t\tconst { name, version } = this.parseNpmSpec(spec);\n\t\t\treturn {\n\t\t\t\ttype: \"npm\",\n\t\t\t\tspec,\n\t\t\t\tname,\n\t\t\t\tpinned: Boolean(version),\n\t\t\t};\n\t\t}\n\n\t\tconst trimmed = source.trim();\n\t\tconst isWindowsAbsolutePath = /^[A-Za-z]:[\\\\/]|^\\\\\\\\/.test(trimmed);\n\t\tconst isLocalPathLike =\n\t\t\ttrimmed.startsWith(\".\") ||\n\t\t\ttrimmed.startsWith(\"/\") ||\n\t\t\ttrimmed === \"~\" ||\n\t\t\ttrimmed.startsWith(\"~/\") ||\n\t\t\tisWindowsAbsolutePath;\n\t\tif (isLocalPathLike) {\n\t\t\treturn { type: \"local\", path: source };\n\t\t}\n\n\t\t// Try parsing as git URL\n\t\tconst gitParsed = parseGitUrl(source);\n\t\tif (gitParsed) {\n\t\t\treturn gitParsed;\n\t\t}\n\n\t\treturn { type: \"local\", path: source };\n\t}\n\n\t/**\n\t * Check if an npm package needs to be updated.\n\t * - For unpinned packages: check if registry has a newer version\n\t * - For pinned packages: check if installed version matches the pinned version\n\t */\n\tprivate async npmNeedsUpdate(source: NpmSource, installedPath: string): Promise<boolean> {\n\t\tif (isOfflineModeEnabled()) {\n\t\t\treturn false;\n\t\t}\n\n\t\tconst installedVersion = this.getInstalledNpmVersion(installedPath);\n\t\tif (!installedVersion) return true;\n\n\t\tconst { version: pinnedVersion } = this.parseNpmSpec(source.spec);\n\t\tif (pinnedVersion) {\n\t\t\t// Pinned: check if installed matches pinned (exact match for now)\n\t\t\treturn installedVersion !== pinnedVersion;\n\t\t}\n\n\t\t// Unpinned: check registry for latest version\n\t\ttry {\n\t\t\tconst latestVersion = await this.getLatestNpmVersion(source.name);\n\t\t\treturn latestVersion !== installedVersion;\n\t\t} catch {\n\t\t\t// If we can't check registry, assume it's fine\n\t\t\treturn false;\n\t\t}\n\t}\n\n\tprivate getInstalledNpmVersion(installedPath: string): string | undefined {\n\t\tconst packageJsonPath = join(installedPath, \"package.json\");\n\t\tif (!existsSync(packageJsonPath)) return undefined;\n\t\ttry {\n\t\t\tconst content = readFileSync(packageJsonPath, \"utf-8\");\n\t\t\tconst pkg = JSON.parse(content) as { version?: string };\n\t\t\treturn pkg.version;\n\t\t} catch {\n\t\t\treturn undefined;\n\t\t}\n\t}\n\n\tprivate async getLatestNpmVersion(packageName: string): Promise<string> {\n\t\tconst response = await fetch(`https://registry.npmjs.org/${packageName}/latest`, {\n\t\t\tsignal: AbortSignal.timeout(NETWORK_TIMEOUT_MS),\n\t\t});\n\t\tif (!response.ok) throw new Error(`Failed to fetch npm registry: ${response.status}`);\n\t\tconst data = (await response.json()) as { version: string };\n\t\treturn data.version;\n\t}\n\n\t/**\n\t * Get a unique identity for a package, ignoring version/ref.\n\t * Used to detect when the same package is in both global and project settings.\n\t * For git packages, uses normalized host/path to ensure SSH and HTTPS URLs\n\t * for the same repository are treated as identical.\n\t */\n\tprivate getPackageIdentity(source: string, scope?: SourceScope): string {\n\t\tconst parsed = this.parseSource(source);\n\t\tif (parsed.type === \"npm\") {\n\t\t\treturn `npm:${parsed.name}`;\n\t\t}\n\t\tif (parsed.type === \"git\") {\n\t\t\t// Use host/path for identity to normalize SSH and HTTPS\n\t\t\treturn `git:${parsed.host}/${parsed.path}`;\n\t\t}\n\t\tif (scope) {\n\t\t\tconst baseDir = this.getBaseDirForScope(scope);\n\t\t\treturn `local:${this.resolvePathFromBase(parsed.path, baseDir)}`;\n\t\t}\n\t\treturn `local:${this.resolvePath(parsed.path)}`;\n\t}\n\n\t/**\n\t * Dedupe packages: if same package identity appears in both global and project,\n\t * keep only the project one (project wins).\n\t */\n\tprivate dedupePackages(\n\t\tpackages: Array<{ pkg: PackageSource; scope: SourceScope }>,\n\t): Array<{ pkg: PackageSource; scope: SourceScope }> {\n\t\tconst seen = new Map<string, { pkg: PackageSource; scope: SourceScope }>();\n\n\t\tfor (const entry of packages) {\n\t\t\tconst sourceStr = typeof entry.pkg === \"string\" ? entry.pkg : entry.pkg.source;\n\t\t\tconst identity = this.getPackageIdentity(sourceStr, entry.scope);\n\n\t\t\tconst existing = seen.get(identity);\n\t\t\tif (!existing) {\n\t\t\t\tseen.set(identity, entry);\n\t\t\t} else if (entry.scope === \"project\" && existing.scope === \"user\") {\n\t\t\t\t// Project wins over user\n\t\t\t\tseen.set(identity, entry);\n\t\t\t}\n\t\t\t// If existing is project and new is global, keep existing (project)\n\t\t\t// If both are same scope, keep first one\n\t\t}\n\n\t\treturn Array.from(seen.values());\n\t}\n\n\tprivate parseNpmSpec(spec: string): { name: string; version?: string } {\n\t\tconst match = spec.match(/^(@?[^@]+(?:\\/[^@]+)?)(?:@(.+))?$/);\n\t\tif (!match) {\n\t\t\treturn { name: spec };\n\t\t}\n\t\tconst name = match[1] ?? spec;\n\t\tconst version = match[2];\n\t\treturn { name, version };\n\t}\n\n\tprivate async installNpm(source: NpmSource, scope: SourceScope, temporary: boolean): Promise<void> {\n\t\tif (scope === \"user\" && !temporary) {\n\t\t\tawait this.runCommand(\"npm\", [\"install\", \"-g\", source.spec]);\n\t\t\treturn;\n\t\t}\n\t\tconst installRoot = this.getNpmInstallRoot(scope, temporary);\n\t\tthis.ensureNpmProject(installRoot);\n\t\tawait this.runCommand(\"npm\", [\"install\", source.spec, \"--prefix\", installRoot]);\n\t}\n\n\tprivate async uninstallNpm(source: NpmSource, scope: SourceScope): Promise<void> {\n\t\tif (scope === \"user\") {\n\t\t\tawait this.runCommand(\"npm\", [\"uninstall\", \"-g\", source.name]);\n\t\t\treturn;\n\t\t}\n\t\tconst installRoot = this.getNpmInstallRoot(scope, false);\n\t\tif (!existsSync(installRoot)) {\n\t\t\treturn;\n\t\t}\n\t\tawait this.runCommand(\"npm\", [\"uninstall\", source.name, \"--prefix\", installRoot]);\n\t}\n\n\tprivate async installGit(source: GitSource, scope: SourceScope): Promise<void> {\n\t\tconst targetDir = this.getGitInstallPath(source, scope);\n\t\tif (existsSync(targetDir)) {\n\t\t\treturn;\n\t\t}\n\t\tconst gitRoot = this.getGitInstallRoot(scope);\n\t\tif (gitRoot) {\n\t\t\tthis.ensureGitIgnore(gitRoot);\n\t\t}\n\t\tmkdirSync(dirname(targetDir), { recursive: true });\n\n\t\tawait this.runCommand(\"git\", [\"clone\", source.repo, targetDir]);\n\t\tif (source.ref) {\n\t\t\tawait this.runCommand(\"git\", [\"checkout\", source.ref], { cwd: targetDir });\n\t\t}\n\t\tconst packageJsonPath = join(targetDir, \"package.json\");\n\t\tif (existsSync(packageJsonPath)) {\n\t\t\tawait this.runCommand(\"npm\", [\"install\"], { cwd: targetDir });\n\t\t}\n\t}\n\n\tprivate async updateGit(source: GitSource, scope: SourceScope): Promise<void> {\n\t\tconst targetDir = this.getGitInstallPath(source, scope);\n\t\tif (!existsSync(targetDir)) {\n\t\t\tawait this.installGit(source, scope);\n\t\t\treturn;\n\t\t}\n\n\t\t// Fetch latest from remote (handles force-push by getting new history)\n\t\tawait this.runCommand(\"git\", [\"fetch\", \"--prune\", \"origin\"], { cwd: targetDir });\n\n\t\t// Reset to tracking branch. Fall back to origin/HEAD when no upstream is configured.\n\t\ttry {\n\t\t\tawait this.runCommand(\"git\", [\"reset\", \"--hard\", \"@{upstream}\"], { cwd: targetDir });\n\t\t} catch {\n\t\t\tawait this.runCommand(\"git\", [\"remote\", \"set-head\", \"origin\", \"-a\"], { cwd: targetDir }).catch(() => {});\n\t\t\tawait this.runCommand(\"git\", [\"reset\", \"--hard\", \"origin/HEAD\"], { cwd: targetDir });\n\t\t}\n\n\t\t// Clean untracked files (extensions should be pristine)\n\t\tawait this.runCommand(\"git\", [\"clean\", \"-fdx\"], { cwd: targetDir });\n\n\t\tconst packageJsonPath = join(targetDir, \"package.json\");\n\t\tif (existsSync(packageJsonPath)) {\n\t\t\tawait this.runCommand(\"npm\", [\"install\"], { cwd: targetDir });\n\t\t}\n\t}\n\n\tprivate async refreshTemporaryGitSource(source: GitSource, sourceStr: string): Promise<void> {\n\t\tif (isOfflineModeEnabled()) {\n\t\t\treturn;\n\t\t}\n\t\ttry {\n\t\t\tawait this.withProgress(\"pull\", sourceStr, `Refreshing ${sourceStr}...`, async () => {\n\t\t\t\tawait this.updateGit(source, \"temporary\");\n\t\t\t});\n\t\t} catch {\n\t\t\t// Keep cached temporary checkout if refresh fails.\n\t\t}\n\t}\n\n\tprivate async removeGit(source: GitSource, scope: SourceScope): Promise<void> {\n\t\tconst targetDir = this.getGitInstallPath(source, scope);\n\t\tif (!existsSync(targetDir)) return;\n\t\trmSync(targetDir, { recursive: true, force: true });\n\t\tthis.pruneEmptyGitParents(targetDir, this.getGitInstallRoot(scope));\n\t}\n\n\tprivate pruneEmptyGitParents(targetDir: string, installRoot: string | undefined): void {\n\t\tif (!installRoot) return;\n\t\tconst resolvedRoot = resolve(installRoot);\n\t\tlet current = dirname(targetDir);\n\t\twhile (current.startsWith(resolvedRoot) && current !== resolvedRoot) {\n\t\t\tif (!existsSync(current)) {\n\t\t\t\tcurrent = dirname(current);\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tconst entries = readdirSync(current);\n\t\t\tif (entries.length > 0) {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\ttry {\n\t\t\t\trmSync(current, { recursive: true, force: true });\n\t\t\t} catch {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcurrent = dirname(current);\n\t\t}\n\t}\n\n\tprivate ensureNpmProject(installRoot: string): void {\n\t\tif (!existsSync(installRoot)) {\n\t\t\tmkdirSync(installRoot, { recursive: true });\n\t\t}\n\t\tthis.ensureGitIgnore(installRoot);\n\t\tconst packageJsonPath = join(installRoot, \"package.json\");\n\t\tif (!existsSync(packageJsonPath)) {\n\t\t\tconst pkgJson = { name: \"pi-extensions\", private: true };\n\t\t\twriteFileSync(packageJsonPath, JSON.stringify(pkgJson, null, 2), \"utf-8\");\n\t\t}\n\t}\n\n\tprivate ensureGitIgnore(dir: string): void {\n\t\tif (!existsSync(dir)) {\n\t\t\tmkdirSync(dir, { recursive: true });\n\t\t}\n\t\tconst ignorePath = join(dir, \".gitignore\");\n\t\tif (!existsSync(ignorePath)) {\n\t\t\twriteFileSync(ignorePath, \"*\\n!.gitignore\\n\", \"utf-8\");\n\t\t}\n\t}\n\n\tprivate getNpmInstallRoot(scope: SourceScope, temporary: boolean): string {\n\t\tif (temporary) {\n\t\t\treturn this.getTemporaryDir(\"npm\");\n\t\t}\n\t\tif (scope === \"project\") {\n\t\t\treturn join(this.cwd, CONFIG_DIR_NAME, \"npm\");\n\t\t}\n\t\treturn join(this.getGlobalNpmRoot(), \"..\");\n\t}\n\n\tprivate getGlobalNpmRoot(): string {\n\t\tif (this.globalNpmRoot) {\n\t\t\treturn this.globalNpmRoot;\n\t\t}\n\t\tconst result = this.runCommandSync(\"npm\", [\"root\", \"-g\"]);\n\t\tthis.globalNpmRoot = result.trim();\n\t\treturn this.globalNpmRoot;\n\t}\n\n\tprivate getNpmInstallPath(source: NpmSource, scope: SourceScope): string {\n\t\tif (scope === \"temporary\") {\n\t\t\treturn join(this.getTemporaryDir(\"npm\"), \"node_modules\", source.name);\n\t\t}\n\t\tif (scope === \"project\") {\n\t\t\treturn join(this.cwd, CONFIG_DIR_NAME, \"npm\", \"node_modules\", source.name);\n\t\t}\n\t\treturn join(this.getGlobalNpmRoot(), source.name);\n\t}\n\n\tprivate getGitInstallPath(source: GitSource, scope: SourceScope): string {\n\t\tif (scope === \"temporary\") {\n\t\t\treturn this.getTemporaryDir(`git-${source.host}`, source.path);\n\t\t}\n\t\tif (scope === \"project\") {\n\t\t\treturn join(this.cwd, CONFIG_DIR_NAME, \"git\", source.host, source.path);\n\t\t}\n\t\treturn join(this.agentDir, \"git\", source.host, source.path);\n\t}\n\n\tprivate getGitInstallRoot(scope: SourceScope): string | undefined {\n\t\tif (scope === \"temporary\") {\n\t\t\treturn undefined;\n\t\t}\n\t\tif (scope === \"project\") {\n\t\t\treturn join(this.cwd, CONFIG_DIR_NAME, \"git\");\n\t\t}\n\t\treturn join(this.agentDir, \"git\");\n\t}\n\n\tprivate getTemporaryDir(prefix: string, suffix?: string): string {\n\t\tconst hash = createHash(\"sha256\")\n\t\t\t.update(`${prefix}-${suffix ?? \"\"}`)\n\t\t\t.digest(\"hex\")\n\t\t\t.slice(0, 8);\n\t\treturn join(tmpdir(), \"pi-extensions\", prefix, hash, suffix ?? \"\");\n\t}\n\n\tprivate getBaseDirForScope(scope: SourceScope): string {\n\t\tif (scope === \"project\") {\n\t\t\treturn join(this.cwd, CONFIG_DIR_NAME);\n\t\t}\n\t\tif (scope === \"user\") {\n\t\t\treturn this.agentDir;\n\t\t}\n\t\treturn this.cwd;\n\t}\n\n\tprivate resolvePath(input: string): string {\n\t\tconst trimmed = input.trim();\n\t\tif (trimmed === \"~\") return homedir();\n\t\tif (trimmed.startsWith(\"~/\")) return join(homedir(), trimmed.slice(2));\n\t\tif (trimmed.startsWith(\"~\")) return join(homedir(), trimmed.slice(1));\n\t\treturn resolve(this.cwd, trimmed);\n\t}\n\n\tprivate resolvePathFromBase(input: string, baseDir: string): string {\n\t\tconst trimmed = input.trim();\n\t\tif (trimmed === \"~\") return homedir();\n\t\tif (trimmed.startsWith(\"~/\")) return join(homedir(), trimmed.slice(2));\n\t\tif (trimmed.startsWith(\"~\")) return join(homedir(), trimmed.slice(1));\n\t\treturn resolve(baseDir, trimmed);\n\t}\n\n\tprivate collectPackageResources(\n\t\tpackageRoot: string,\n\t\taccumulator: ResourceAccumulator,\n\t\tfilter: PackageFilter | undefined,\n\t\tmetadata: PathMetadata,\n\t): boolean {\n\t\tif (filter) {\n\t\t\tfor (const resourceType of RESOURCE_TYPES) {\n\t\t\t\tconst patterns = filter[resourceType as keyof PackageFilter];\n\t\t\t\tconst target = this.getTargetMap(accumulator, resourceType);\n\t\t\t\tif (patterns !== undefined) {\n\t\t\t\t\tthis.applyPackageFilter(packageRoot, patterns, resourceType, target, metadata);\n\t\t\t\t} else {\n\t\t\t\t\tthis.collectDefaultResources(packageRoot, resourceType, target, metadata);\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn true;\n\t\t}\n\n\t\tconst manifest = this.readDrahtManifest(packageRoot);\n\t\tif (manifest) {\n\t\t\tfor (const resourceType of RESOURCE_TYPES) {\n\t\t\t\tconst entries = manifest[resourceType as keyof DrahtManifest];\n\t\t\t\tthis.addManifestEntries(\n\t\t\t\t\tentries,\n\t\t\t\t\tpackageRoot,\n\t\t\t\t\tresourceType,\n\t\t\t\t\tthis.getTargetMap(accumulator, resourceType),\n\t\t\t\t\tmetadata,\n\t\t\t\t);\n\t\t\t}\n\t\t\treturn true;\n\t\t}\n\n\t\tlet hasAnyDir = false;\n\t\tfor (const resourceType of RESOURCE_TYPES) {\n\t\t\tconst dir = join(packageRoot, resourceType);\n\t\t\tif (existsSync(dir)) {\n\t\t\t\t// Collect all files from the directory (all enabled by default)\n\t\t\t\tconst files = collectResourceFiles(dir, resourceType);\n\t\t\t\tfor (const f of files) {\n\t\t\t\t\tthis.addResource(this.getTargetMap(accumulator, resourceType), f, metadata, true);\n\t\t\t\t}\n\t\t\t\thasAnyDir = true;\n\t\t\t}\n\t\t}\n\t\treturn hasAnyDir;\n\t}\n\n\tprivate collectDefaultResources(\n\t\tpackageRoot: string,\n\t\tresourceType: ResourceType,\n\t\ttarget: Map<string, { metadata: PathMetadata; enabled: boolean }>,\n\t\tmetadata: PathMetadata,\n\t): void {\n\t\tconst manifest = this.readDrahtManifest(packageRoot);\n\t\tconst entries = manifest?.[resourceType as keyof DrahtManifest];\n\t\tif (entries) {\n\t\t\tthis.addManifestEntries(entries, packageRoot, resourceType, target, metadata);\n\t\t\treturn;\n\t\t}\n\t\tconst dir = join(packageRoot, resourceType);\n\t\tif (existsSync(dir)) {\n\t\t\t// Collect all files from the directory (all enabled by default)\n\t\t\tconst files = collectResourceFiles(dir, resourceType);\n\t\t\tfor (const f of files) {\n\t\t\t\tthis.addResource(target, f, metadata, true);\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate applyPackageFilter(\n\t\tpackageRoot: string,\n\t\tuserPatterns: string[],\n\t\tresourceType: ResourceType,\n\t\ttarget: Map<string, { metadata: PathMetadata; enabled: boolean }>,\n\t\tmetadata: PathMetadata,\n\t): void {\n\t\tconst { allFiles } = this.collectManifestFiles(packageRoot, resourceType);\n\n\t\tif (userPatterns.length === 0) {\n\t\t\t// Empty array explicitly disables all resources of this type\n\t\t\tfor (const f of allFiles) {\n\t\t\t\tthis.addResource(target, f, metadata, false);\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\n\t\t// Apply user patterns\n\t\tconst enabledByUser = applyPatterns(allFiles, userPatterns, packageRoot);\n\n\t\tfor (const f of allFiles) {\n\t\t\tconst enabled = enabledByUser.has(f);\n\t\t\tthis.addResource(target, f, metadata, enabled);\n\t\t}\n\t}\n\n\t/**\n\t * Collect all files from a package for a resource type, applying manifest patterns.\n\t * Returns { allFiles, enabledByManifest } where enabledByManifest is the set of files\n\t * that pass the manifest's own patterns.\n\t */\n\tprivate collectManifestFiles(\n\t\tpackageRoot: string,\n\t\tresourceType: ResourceType,\n\t): { allFiles: string[]; enabledByManifest: Set<string> } {\n\t\tconst manifest = this.readDrahtManifest(packageRoot);\n\t\tconst entries = manifest?.[resourceType as keyof DrahtManifest];\n\t\tif (entries && entries.length > 0) {\n\t\t\tconst allFiles = this.collectFilesFromManifestEntries(entries, packageRoot, resourceType);\n\t\t\tconst manifestPatterns = entries.filter(isPattern);\n\t\t\tconst enabledByManifest =\n\t\t\t\tmanifestPatterns.length > 0 ? applyPatterns(allFiles, manifestPatterns, packageRoot) : new Set(allFiles);\n\t\t\treturn { allFiles: Array.from(enabledByManifest), enabledByManifest };\n\t\t}\n\n\t\tconst conventionDir = join(packageRoot, resourceType);\n\t\tif (!existsSync(conventionDir)) {\n\t\t\treturn { allFiles: [], enabledByManifest: new Set() };\n\t\t}\n\t\tconst allFiles = collectResourceFiles(conventionDir, resourceType);\n\t\treturn { allFiles, enabledByManifest: new Set(allFiles) };\n\t}\n\n\tprivate readDrahtManifest(packageRoot: string): DrahtManifest | null {\n\t\tconst packageJsonPath = join(packageRoot, \"package.json\");\n\t\tif (!existsSync(packageJsonPath)) {\n\t\t\treturn null;\n\t\t}\n\n\t\ttry {\n\t\t\tconst content = readFileSync(packageJsonPath, \"utf-8\");\n\t\t\tconst pkg = JSON.parse(content) as { draht?: DrahtManifest };\n\t\t\treturn pkg.draht ?? null;\n\t\t} catch {\n\t\t\treturn null;\n\t\t}\n\t}\n\n\tprivate addManifestEntries(\n\t\tentries: string[] | undefined,\n\t\troot: string,\n\t\tresourceType: ResourceType,\n\t\ttarget: Map<string, { metadata: PathMetadata; enabled: boolean }>,\n\t\tmetadata: PathMetadata,\n\t): void {\n\t\tif (!entries) return;\n\n\t\tconst allFiles = this.collectFilesFromManifestEntries(entries, root, resourceType);\n\t\tconst patterns = entries.filter(isPattern);\n\t\tconst enabledPaths = applyPatterns(allFiles, patterns, root);\n\n\t\tfor (const f of allFiles) {\n\t\t\tif (enabledPaths.has(f)) {\n\t\t\t\tthis.addResource(target, f, metadata, true);\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate collectFilesFromManifestEntries(entries: string[], root: string, resourceType: ResourceType): string[] {\n\t\tconst plain = entries.filter((entry) => !isPattern(entry));\n\t\tconst resolved = plain.map((entry) => resolve(root, entry));\n\t\treturn this.collectFilesFromPaths(resolved, resourceType);\n\t}\n\n\tprivate resolveLocalEntries(\n\t\tentries: string[],\n\t\tresourceType: ResourceType,\n\t\ttarget: Map<string, { metadata: PathMetadata; enabled: boolean }>,\n\t\tmetadata: PathMetadata,\n\t\tbaseDir: string,\n\t): void {\n\t\tif (entries.length === 0) return;\n\n\t\t// Collect all files from plain entries (non-pattern entries)\n\t\tconst { plain, patterns } = splitPatterns(entries);\n\t\tconst resolvedPlain = plain.map((p) => this.resolvePathFromBase(p, baseDir));\n\t\tconst allFiles = this.collectFilesFromPaths(resolvedPlain, resourceType);\n\n\t\t// Determine which files are enabled based on patterns\n\t\tconst enabledPaths = applyPatterns(allFiles, patterns, baseDir);\n\n\t\t// Add all files with their enabled state\n\t\tfor (const f of allFiles) {\n\t\t\tthis.addResource(target, f, metadata, enabledPaths.has(f));\n\t\t}\n\t}\n\n\tprivate addAutoDiscoveredResources(\n\t\taccumulator: ResourceAccumulator,\n\t\tglobalSettings: ReturnType<SettingsManager[\"getGlobalSettings\"]>,\n\t\tprojectSettings: ReturnType<SettingsManager[\"getProjectSettings\"]>,\n\t\tglobalBaseDir: string,\n\t\tprojectBaseDir: string,\n\t): void {\n\t\tconst userMetadata: PathMetadata = {\n\t\t\tsource: \"auto\",\n\t\t\tscope: \"user\",\n\t\t\torigin: \"top-level\",\n\t\t\tbaseDir: globalBaseDir,\n\t\t};\n\t\tconst projectMetadata: PathMetadata = {\n\t\t\tsource: \"auto\",\n\t\t\tscope: \"project\",\n\t\t\torigin: \"top-level\",\n\t\t\tbaseDir: projectBaseDir,\n\t\t};\n\n\t\tconst userOverrides = {\n\t\t\textensions: (globalSettings.extensions ?? []) as string[],\n\t\t\tskills: (globalSettings.skills ?? []) as string[],\n\t\t\tprompts: (globalSettings.prompts ?? []) as string[],\n\t\t\tthemes: (globalSettings.themes ?? []) as string[],\n\t\t};\n\t\tconst projectOverrides = {\n\t\t\textensions: (projectSettings.extensions ?? []) as string[],\n\t\t\tskills: (projectSettings.skills ?? []) as string[],\n\t\t\tprompts: (projectSettings.prompts ?? []) as string[],\n\t\t\tthemes: (projectSettings.themes ?? []) as string[],\n\t\t};\n\n\t\tconst userDirs = {\n\t\t\textensions: join(globalBaseDir, \"extensions\"),\n\t\t\tskills: join(globalBaseDir, \"skills\"),\n\t\t\tprompts: join(globalBaseDir, \"prompts\"),\n\t\t\tthemes: join(globalBaseDir, \"themes\"),\n\t\t};\n\t\tconst projectDirs = {\n\t\t\textensions: join(projectBaseDir, \"extensions\"),\n\t\t\tskills: join(projectBaseDir, \"skills\"),\n\t\t\tprompts: join(projectBaseDir, \"prompts\"),\n\t\t\tthemes: join(projectBaseDir, \"themes\"),\n\t\t};\n\t\tconst userAgentsSkillsDir = join(homedir(), \".agents\", \"skills\");\n\t\tconst projectAgentsSkillDirs = collectAncestorAgentsSkillDirs(this.cwd);\n\n\t\tconst addResources = (\n\t\t\tresourceType: ResourceType,\n\t\t\tpaths: string[],\n\t\t\tmetadata: PathMetadata,\n\t\t\toverrides: string[],\n\t\t\tbaseDir: string,\n\t\t) => {\n\t\t\tconst target = this.getTargetMap(accumulator, resourceType);\n\t\t\tfor (const path of paths) {\n\t\t\t\tconst enabled = isEnabledByOverrides(path, overrides, baseDir);\n\t\t\t\tthis.addResource(target, path, metadata, enabled);\n\t\t\t}\n\t\t};\n\n\t\taddResources(\n\t\t\t\"extensions\",\n\t\t\tcollectAutoExtensionEntries(projectDirs.extensions),\n\t\t\tprojectMetadata,\n\t\t\tprojectOverrides.extensions,\n\t\t\tprojectBaseDir,\n\t\t);\n\t\taddResources(\n\t\t\t\"skills\",\n\t\t\t[\n\t\t\t\t...collectAutoSkillEntries(projectDirs.skills),\n\t\t\t\t...projectAgentsSkillDirs.flatMap((dir) => collectAutoSkillEntries(dir)),\n\t\t\t],\n\t\t\tprojectMetadata,\n\t\t\tprojectOverrides.skills,\n\t\t\tprojectBaseDir,\n\t\t);\n\t\taddResources(\n\t\t\t\"prompts\",\n\t\t\tcollectAutoPromptEntries(projectDirs.prompts),\n\t\t\tprojectMetadata,\n\t\t\tprojectOverrides.prompts,\n\t\t\tprojectBaseDir,\n\t\t);\n\t\taddResources(\n\t\t\t\"themes\",\n\t\t\tcollectAutoThemeEntries(projectDirs.themes),\n\t\t\tprojectMetadata,\n\t\t\tprojectOverrides.themes,\n\t\t\tprojectBaseDir,\n\t\t);\n\n\t\taddResources(\n\t\t\t\"extensions\",\n\t\t\tcollectAutoExtensionEntries(userDirs.extensions),\n\t\t\tuserMetadata,\n\t\t\tuserOverrides.extensions,\n\t\t\tglobalBaseDir,\n\t\t);\n\t\taddResources(\n\t\t\t\"skills\",\n\t\t\t[...collectAutoSkillEntries(userDirs.skills), ...collectAutoSkillEntries(userAgentsSkillsDir)],\n\t\t\tuserMetadata,\n\t\t\tuserOverrides.skills,\n\t\t\tglobalBaseDir,\n\t\t);\n\t\taddResources(\n\t\t\t\"prompts\",\n\t\t\tcollectAutoPromptEntries(userDirs.prompts),\n\t\t\tuserMetadata,\n\t\t\tuserOverrides.prompts,\n\t\t\tglobalBaseDir,\n\t\t);\n\t\taddResources(\n\t\t\t\"themes\",\n\t\t\tcollectAutoThemeEntries(userDirs.themes),\n\t\t\tuserMetadata,\n\t\t\tuserOverrides.themes,\n\t\t\tglobalBaseDir,\n\t\t);\n\n\t\t// Shipped extensions: bundled with the package (batteries included).\n\t\t// Added last so project/user extensions take precedence (addResource is first-write-wins).\n\t\tconst shippedMetadata: PathMetadata = {\n\t\t\tsource: \"auto\",\n\t\t\tscope: \"user\",\n\t\t\torigin: \"top-level\",\n\t\t\tbaseDir: getShippedExtensionsDir(),\n\t\t};\n\t\taddResources(\n\t\t\t\"extensions\",\n\t\t\tcollectAutoExtensionEntries(getShippedExtensionsDir()),\n\t\t\tshippedMetadata,\n\t\t\t[],\n\t\t\tgetShippedExtensionsDir(),\n\t\t);\n\t}\n\n\tprivate collectFilesFromPaths(paths: string[], resourceType: ResourceType): string[] {\n\t\tconst files: string[] = [];\n\t\tfor (const p of paths) {\n\t\t\tif (!existsSync(p)) continue;\n\n\t\t\ttry {\n\t\t\t\tconst stats = statSync(p);\n\t\t\t\tif (stats.isFile()) {\n\t\t\t\t\tfiles.push(p);\n\t\t\t\t} else if (stats.isDirectory()) {\n\t\t\t\t\tfiles.push(...collectResourceFiles(p, resourceType));\n\t\t\t\t}\n\t\t\t} catch {\n\t\t\t\t// Ignore errors\n\t\t\t}\n\t\t}\n\t\treturn files;\n\t}\n\n\tprivate getTargetMap(\n\t\taccumulator: ResourceAccumulator,\n\t\tresourceType: ResourceType,\n\t): Map<string, { metadata: PathMetadata; enabled: boolean }> {\n\t\tswitch (resourceType) {\n\t\t\tcase \"extensions\":\n\t\t\t\treturn accumulator.extensions;\n\t\t\tcase \"skills\":\n\t\t\t\treturn accumulator.skills;\n\t\t\tcase \"prompts\":\n\t\t\t\treturn accumulator.prompts;\n\t\t\tcase \"themes\":\n\t\t\t\treturn accumulator.themes;\n\t\t\tdefault:\n\t\t\t\tthrow new Error(`Unknown resource type: ${resourceType}`);\n\t\t}\n\t}\n\n\tprivate addResource(\n\t\tmap: Map<string, { metadata: PathMetadata; enabled: boolean }>,\n\t\tpath: string,\n\t\tmetadata: PathMetadata,\n\t\tenabled: boolean,\n\t): void {\n\t\tif (!path) return;\n\t\tif (!map.has(path)) {\n\t\t\tmap.set(path, { metadata, enabled });\n\t\t}\n\t}\n\n\tprivate createAccumulator(): ResourceAccumulator {\n\t\treturn {\n\t\t\textensions: new Map(),\n\t\t\tskills: new Map(),\n\t\t\tprompts: new Map(),\n\t\t\tthemes: new Map(),\n\t\t};\n\t}\n\n\tprivate toResolvedPaths(accumulator: ResourceAccumulator): ResolvedPaths {\n\t\tconst toResolved = (entries: Map<string, { metadata: PathMetadata; enabled: boolean }>): ResolvedResource[] => {\n\t\t\treturn Array.from(entries.entries()).map(([path, { metadata, enabled }]) => ({\n\t\t\t\tpath,\n\t\t\t\tenabled,\n\t\t\t\tmetadata,\n\t\t\t}));\n\t\t};\n\n\t\treturn {\n\t\t\textensions: toResolved(accumulator.extensions),\n\t\t\tskills: toResolved(accumulator.skills),\n\t\t\tprompts: toResolved(accumulator.prompts),\n\t\t\tthemes: toResolved(accumulator.themes),\n\t\t};\n\t}\n\n\tprivate runCommand(command: string, args: string[], options?: { cwd?: string }): Promise<void> {\n\t\treturn new Promise((resolvePromise, reject) => {\n\t\t\tconst child = spawn(command, args, {\n\t\t\t\tcwd: options?.cwd,\n\t\t\t\tstdio: \"inherit\",\n\t\t\t\tshell: process.platform === \"win32\",\n\t\t\t});\n\t\t\tchild.on(\"error\", reject);\n\t\t\tchild.on(\"exit\", (code) => {\n\t\t\t\tif (code === 0) {\n\t\t\t\t\tresolvePromise();\n\t\t\t\t} else {\n\t\t\t\t\treject(new Error(`${command} ${args.join(\" \")} failed with code ${code}`));\n\t\t\t\t}\n\t\t\t});\n\t\t});\n\t}\n\n\tprivate runCommandSync(command: string, args: string[]): string {\n\t\tconst result = spawnSync(command, args, {\n\t\t\tstdio: [\"ignore\", \"pipe\", \"pipe\"],\n\t\t\tencoding: \"utf-8\",\n\t\t\tshell: process.platform === \"win32\",\n\t\t});\n\t\tif (result.status !== 0) {\n\t\t\tthrow new Error(`Failed to run ${command} ${args.join(\" \")}: ${result.stderr || result.stdout}`);\n\t\t}\n\t\treturn (result.stdout || result.stderr || \"\").trim();\n\t}\n}\n"]}