@uluops/setup 0.2.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (253) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +109 -89
  3. package/assets/auto-tracker-save.mjs +142 -0
  4. package/assets/claude-code/agents/anxiety-reader-agent.md +464 -0
  5. package/assets/{agents → claude-code/agents}/api-contract-validator-agent.md +9 -228
  6. package/assets/{agents → claude-code/agents}/aristotle-analyst-agent.md +51 -4
  7. package/assets/{agents → claude-code/agents}/aristotle-explorer-agent.md +6 -2
  8. package/assets/{agents → claude-code/agents}/aristotle-forecaster-agent.md +15 -230
  9. package/assets/{agents → claude-code/agents}/aristotle-validator-agent.md +12 -252
  10. package/assets/{agents → claude-code/agents}/assumption-excavator-agent.md +21 -247
  11. package/assets/{agents → claude-code/agents}/code-auditor-agent.md +12 -255
  12. package/assets/{agents → claude-code/agents}/code-optimizer-agent.md +15 -236
  13. package/assets/{agents → claude-code/agents}/code-validator-agent.md +31 -300
  14. package/assets/claude-code/agents/docs-validator-agent.md +472 -0
  15. package/assets/{agents → claude-code/agents}/frontend-validator-agent.md +15 -258
  16. package/assets/{agents → claude-code/agents}/mcp-validator-agent.md +8 -252
  17. package/assets/{agents → claude-code/agents}/pre-implementation-architect-agent.md +8 -224
  18. package/assets/{agents → claude-code/agents}/prompt-engineer-agent.md +57 -290
  19. package/assets/{agents → claude-code/agents}/prompt-pattern-analyzer-agent.md +10 -225
  20. package/assets/{agents → claude-code/agents}/prompt-quality-validator-agent.md +11 -249
  21. package/assets/{agents → claude-code/agents}/public-interface-validator-agent.md +15 -268
  22. package/assets/claude-code/agents/release-readiness-agent.md +495 -0
  23. package/assets/{agents → claude-code/agents}/security-analyst-agent.md +236 -480
  24. package/assets/{agents → claude-code/agents}/test-architect-agent.md +16 -259
  25. package/assets/{agents → claude-code/agents}/type-safety-validator-agent.md +23 -266
  26. package/assets/{agents → claude-code/agents}/workflow-synthesis-agent.md +23 -226
  27. package/assets/claude-code/commands/agents/anxiety-reader.md +157 -0
  28. package/assets/{commands → claude-code/commands}/agents/api-contract.md +156 -135
  29. package/assets/{commands → claude-code/commands}/agents/architect.md +156 -135
  30. package/assets/claude-code/commands/agents/aristotle-analyst.md +157 -0
  31. package/assets/claude-code/commands/agents/aristotle-explorer.md +157 -0
  32. package/assets/claude-code/commands/agents/aristotle-forecaster.md +157 -0
  33. package/assets/claude-code/commands/agents/aristotle-validator.md +157 -0
  34. package/assets/{commands → claude-code/commands}/agents/assumption-excavator.md +49 -6
  35. package/assets/{commands → claude-code/commands}/agents/audit.md +156 -136
  36. package/assets/{commands → claude-code/commands}/agents/docs-validate.md +156 -133
  37. package/assets/{commands → claude-code/commands}/agents/frontend.md +156 -135
  38. package/assets/{commands → claude-code/commands}/agents/mcp-validate.md +156 -136
  39. package/assets/{commands → claude-code/commands}/agents/optimize.md +156 -133
  40. package/assets/{commands → claude-code/commands}/agents/pattern-analyzer.md +150 -126
  41. package/assets/{commands → claude-code/commands}/agents/prompt-quality.md +155 -134
  42. package/assets/claude-code/commands/agents/prompt-validate.md +155 -0
  43. package/assets/{commands → claude-code/commands}/agents/public-interface.md +156 -134
  44. package/assets/{commands → claude-code/commands}/agents/release.md +156 -135
  45. package/assets/{commands → claude-code/commands}/agents/security.md +156 -137
  46. package/assets/{commands → claude-code/commands}/agents/test-review.md +156 -136
  47. package/assets/{commands → claude-code/commands}/agents/type-safety.md +156 -135
  48. package/assets/{commands → claude-code/commands}/agents/validate.md +156 -134
  49. package/assets/claude-code/commands/agents/workflow-synthesis.md +157 -0
  50. package/assets/claude-code/commands/pipelines/aristotle.md +143 -0
  51. package/assets/claude-code/commands/pipelines/ship.md +188 -0
  52. package/assets/claude-code/commands/workflows/post-implementation.md +60 -0
  53. package/assets/claude-code/commands/workflows/pre-implementation.md +46 -0
  54. package/assets/claude-code/commands/workflows/prompt-audit.md +44 -0
  55. package/assets/codex/agents/anxiety-reader-agent.toml +462 -0
  56. package/assets/codex/agents/api-contract-validator-agent.toml +738 -0
  57. package/assets/codex/agents/aristotle-analyst-agent.toml +750 -0
  58. package/assets/codex/agents/aristotle-explorer-agent.toml +155 -0
  59. package/assets/codex/agents/aristotle-forecaster-agent.toml +449 -0
  60. package/assets/codex/agents/aristotle-validator-agent.toml +424 -0
  61. package/assets/codex/agents/assumption-excavator-agent.toml +1126 -0
  62. package/assets/codex/agents/code-auditor-agent.toml +815 -0
  63. package/assets/codex/agents/code-optimizer-agent.toml +652 -0
  64. package/assets/codex/agents/code-validator-agent.toml +573 -0
  65. package/assets/codex/agents/docs-validator-agent.toml +468 -0
  66. package/assets/codex/agents/frontend-validator-agent.toml +598 -0
  67. package/assets/codex/agents/mcp-validator-agent.toml +580 -0
  68. package/assets/codex/agents/pre-implementation-architect-agent.toml +817 -0
  69. package/assets/codex/agents/prompt-engineer-agent.toml +922 -0
  70. package/assets/codex/agents/prompt-pattern-analyzer-agent.toml +689 -0
  71. package/assets/codex/agents/prompt-quality-validator-agent.toml +777 -0
  72. package/assets/codex/agents/public-interface-validator-agent.toml +695 -0
  73. package/assets/codex/agents/release-readiness-agent.toml +491 -0
  74. package/assets/codex/agents/security-analyst-agent.toml +847 -0
  75. package/assets/codex/agents/test-architect-agent.toml +615 -0
  76. package/assets/codex/agents/type-safety-validator-agent.toml +686 -0
  77. package/assets/codex/agents/workflow-synthesis-agent.toml +631 -0
  78. package/assets/gemini-cli/agents/anxiety-reader-agent.md +470 -0
  79. package/assets/gemini-cli/agents/api-contract-validator-agent.md +747 -0
  80. package/assets/gemini-cli/agents/aristotle-analyst-agent.md +758 -0
  81. package/assets/gemini-cli/agents/aristotle-explorer-agent.md +163 -0
  82. package/assets/gemini-cli/agents/aristotle-forecaster-agent.md +457 -0
  83. package/assets/gemini-cli/agents/aristotle-validator-agent.md +432 -0
  84. package/assets/gemini-cli/agents/assumption-excavator-agent.md +1134 -0
  85. package/assets/gemini-cli/agents/code-auditor-agent.md +827 -0
  86. package/assets/gemini-cli/agents/code-optimizer-agent.md +661 -0
  87. package/assets/gemini-cli/agents/code-validator-agent.md +582 -0
  88. package/assets/gemini-cli/agents/docs-validator-agent.md +477 -0
  89. package/assets/gemini-cli/agents/frontend-validator-agent.md +610 -0
  90. package/assets/gemini-cli/agents/mcp-validator-agent.md +589 -0
  91. package/assets/gemini-cli/agents/pre-implementation-architect-agent.md +826 -0
  92. package/assets/gemini-cli/agents/prompt-engineer-agent.md +931 -0
  93. package/assets/gemini-cli/agents/prompt-pattern-analyzer-agent.md +698 -0
  94. package/assets/gemini-cli/agents/prompt-quality-validator-agent.md +786 -0
  95. package/assets/gemini-cli/agents/public-interface-validator-agent.md +707 -0
  96. package/assets/gemini-cli/agents/release-readiness-agent.md +500 -0
  97. package/assets/gemini-cli/agents/security-analyst-agent.md +859 -0
  98. package/assets/gemini-cli/agents/test-architect-agent.md +624 -0
  99. package/assets/gemini-cli/agents/type-safety-validator-agent.md +695 -0
  100. package/assets/gemini-cli/agents/workflow-synthesis-agent.md +639 -0
  101. package/assets/gemini-cli/commands/agents/anxiety-reader.toml +155 -0
  102. package/assets/gemini-cli/commands/agents/api-contract.toml +154 -0
  103. package/assets/gemini-cli/commands/agents/architect.toml +154 -0
  104. package/assets/gemini-cli/commands/agents/aristotle-analyst.toml +155 -0
  105. package/assets/gemini-cli/commands/agents/aristotle-explorer.toml +155 -0
  106. package/assets/gemini-cli/commands/agents/aristotle-forecaster.toml +155 -0
  107. package/assets/gemini-cli/commands/agents/aristotle-validator.toml +155 -0
  108. package/assets/gemini-cli/commands/agents/assumption-excavator.toml +155 -0
  109. package/assets/gemini-cli/commands/agents/audit.toml +154 -0
  110. package/assets/gemini-cli/commands/agents/docs-validate.toml +154 -0
  111. package/assets/gemini-cli/commands/agents/frontend.toml +154 -0
  112. package/assets/gemini-cli/commands/agents/mcp-validate.toml +154 -0
  113. package/assets/gemini-cli/commands/agents/optimize.toml +154 -0
  114. package/assets/gemini-cli/commands/agents/pattern-analyzer.toml +148 -0
  115. package/assets/gemini-cli/commands/agents/prompt-quality.toml +153 -0
  116. package/assets/gemini-cli/commands/agents/prompt-validate.toml +153 -0
  117. package/assets/gemini-cli/commands/agents/public-interface.toml +154 -0
  118. package/assets/gemini-cli/commands/agents/release.toml +154 -0
  119. package/assets/gemini-cli/commands/agents/security.toml +154 -0
  120. package/assets/gemini-cli/commands/agents/test-review.toml +154 -0
  121. package/assets/gemini-cli/commands/agents/type-safety.toml +154 -0
  122. package/assets/gemini-cli/commands/agents/validate.toml +154 -0
  123. package/assets/gemini-cli/commands/agents/workflow-synthesis.toml +155 -0
  124. package/assets/gemini-cli/commands/pipelines/aristotle.toml +139 -0
  125. package/assets/gemini-cli/commands/pipelines/ship.toml +184 -0
  126. package/assets/gemini-cli/commands/workflows/post-implementation.toml +56 -0
  127. package/assets/gemini-cli/commands/workflows/pre-implementation.toml +42 -0
  128. package/assets/gemini-cli/commands/workflows/prompt-audit.toml +40 -0
  129. package/assets/opencode/agents/anxiety-reader-agent.md +472 -0
  130. package/assets/opencode/agents/api-contract-validator-agent.md +749 -0
  131. package/assets/opencode/agents/aristotle-analyst-agent.md +760 -0
  132. package/assets/opencode/agents/aristotle-explorer-agent.md +164 -0
  133. package/assets/opencode/agents/aristotle-forecaster-agent.md +459 -0
  134. package/assets/opencode/agents/aristotle-validator-agent.md +434 -0
  135. package/assets/opencode/agents/assumption-excavator-agent.md +1136 -0
  136. package/assets/opencode/agents/code-auditor-agent.md +826 -0
  137. package/assets/opencode/agents/code-optimizer-agent.md +663 -0
  138. package/assets/opencode/agents/code-validator-agent.md +584 -0
  139. package/assets/opencode/agents/docs-validator-agent.md +479 -0
  140. package/assets/opencode/agents/frontend-validator-agent.md +609 -0
  141. package/assets/opencode/agents/mcp-validator-agent.md +591 -0
  142. package/assets/opencode/agents/pre-implementation-architect-agent.md +828 -0
  143. package/assets/opencode/agents/prompt-engineer-agent.md +933 -0
  144. package/assets/opencode/agents/prompt-pattern-analyzer-agent.md +700 -0
  145. package/assets/opencode/agents/prompt-quality-validator-agent.md +788 -0
  146. package/assets/opencode/agents/public-interface-validator-agent.md +706 -0
  147. package/assets/opencode/agents/release-readiness-agent.md +502 -0
  148. package/assets/opencode/agents/security-analyst-agent.md +858 -0
  149. package/assets/opencode/agents/test-architect-agent.md +626 -0
  150. package/assets/opencode/agents/type-safety-validator-agent.md +697 -0
  151. package/assets/opencode/agents/workflow-synthesis-agent.md +641 -0
  152. package/dist/cli.js +22 -380
  153. package/dist/commands/helpers.d.ts +73 -0
  154. package/dist/commands/helpers.js +274 -0
  155. package/dist/commands/setup.d.ts +13 -0
  156. package/dist/commands/setup.js +93 -0
  157. package/dist/commands/uninstall.d.ts +3 -0
  158. package/dist/commands/uninstall.js +126 -0
  159. package/dist/commands/verify.d.ts +1 -0
  160. package/dist/commands/verify.js +28 -0
  161. package/dist/harnesses/claude-code.d.ts +8 -0
  162. package/dist/harnesses/claude-code.js +74 -0
  163. package/dist/harnesses/codex.d.ts +15 -0
  164. package/dist/harnesses/codex.js +54 -0
  165. package/dist/harnesses/gemini-cli.d.ts +12 -0
  166. package/dist/harnesses/gemini-cli.js +80 -0
  167. package/dist/harnesses/index.d.ts +27 -0
  168. package/dist/harnesses/index.js +54 -0
  169. package/dist/harnesses/opencode.d.ts +14 -0
  170. package/dist/harnesses/opencode.js +139 -0
  171. package/dist/harnesses/types.d.ts +106 -0
  172. package/dist/harnesses/types.js +26 -0
  173. package/dist/lib/agent-transform.d.ts +12 -0
  174. package/dist/lib/agent-transform.js +129 -0
  175. package/dist/lib/asset-catalog.d.ts +9 -0
  176. package/dist/lib/asset-catalog.js +56 -0
  177. package/dist/lib/atomic-write.d.ts +11 -0
  178. package/dist/lib/atomic-write.js +28 -0
  179. package/dist/lib/config-merger.d.ts +9 -2
  180. package/dist/lib/config-merger.js +44 -7
  181. package/dist/lib/display.d.ts +14 -0
  182. package/dist/lib/display.js +66 -0
  183. package/dist/lib/file-ops.d.ts +11 -0
  184. package/dist/lib/file-ops.js +40 -4
  185. package/dist/lib/hash.d.ts +1 -0
  186. package/dist/lib/hash.js +2 -1
  187. package/dist/lib/health.d.ts +2 -0
  188. package/dist/lib/health.js +10 -0
  189. package/dist/lib/manifest.d.ts +51 -5
  190. package/dist/lib/manifest.js +146 -13
  191. package/dist/lib/paths.d.ts +30 -3
  192. package/dist/lib/paths.js +98 -12
  193. package/dist/lib/settings-merger.d.ts +31 -8
  194. package/dist/lib/settings-merger.js +87 -24
  195. package/dist/lib/version.d.ts +2 -0
  196. package/dist/lib/version.js +10 -0
  197. package/dist/steps/agents.d.ts +4 -1
  198. package/dist/steps/agents.js +48 -9
  199. package/dist/steps/auth.js +26 -10
  200. package/dist/steps/cli.d.ts +53 -0
  201. package/dist/steps/cli.js +90 -0
  202. package/dist/steps/commands.d.ts +6 -1
  203. package/dist/steps/commands.js +36 -9
  204. package/dist/steps/detect.d.ts +3 -0
  205. package/dist/steps/detect.js +11 -0
  206. package/dist/steps/mcp.d.ts +6 -2
  207. package/dist/steps/mcp.js +39 -22
  208. package/dist/steps/metrics.d.ts +26 -10
  209. package/dist/steps/metrics.js +108 -108
  210. package/dist/steps/shell.d.ts +2 -0
  211. package/dist/steps/shell.js +26 -9
  212. package/dist/steps/signup.d.ts +7 -4
  213. package/dist/steps/signup.js +29 -20
  214. package/dist/steps/verify.d.ts +2 -2
  215. package/dist/steps/verify.js +118 -112
  216. package/package.json +40 -14
  217. package/assets/agents/docs-validator-agent.md +0 -490
  218. package/assets/agents/release-readiness-agent.md +0 -482
  219. package/assets/commands/agents/aristotle-analyst.md +0 -115
  220. package/assets/commands/agents/aristotle-explorer.md +0 -92
  221. package/assets/commands/agents/aristotle-forecaster.md +0 -114
  222. package/assets/commands/agents/aristotle-validator.md +0 -114
  223. package/assets/commands/agents/prompt-validate.md +0 -135
  224. package/assets/commands/agents/workflow-synthesis.md +0 -101
  225. package/assets/commands/workflows/aristotle.md +0 -543
  226. package/assets/commands/workflows/post-implementation.md +0 -577
  227. package/assets/commands/workflows/pre-implementation.md +0 -670
  228. package/assets/commands/workflows/prompt-audit.md +0 -754
  229. package/assets/commands/workflows/ship.md +0 -721
  230. package/dist/test/auth.test.d.ts +0 -1
  231. package/dist/test/auth.test.js +0 -43
  232. package/dist/test/config-io.test.d.ts +0 -1
  233. package/dist/test/config-io.test.js +0 -56
  234. package/dist/test/config-merger.test.d.ts +0 -1
  235. package/dist/test/config-merger.test.js +0 -94
  236. package/dist/test/detect.test.d.ts +0 -1
  237. package/dist/test/detect.test.js +0 -25
  238. package/dist/test/file-ops.test.d.ts +0 -1
  239. package/dist/test/file-ops.test.js +0 -100
  240. package/dist/test/hash.test.d.ts +0 -1
  241. package/dist/test/hash.test.js +0 -14
  242. package/dist/test/manifest.test.d.ts +0 -1
  243. package/dist/test/manifest.test.js +0 -78
  244. package/dist/test/paths.test.d.ts +0 -1
  245. package/dist/test/paths.test.js +0 -30
  246. package/dist/test/settings-merger.test.d.ts +0 -1
  247. package/dist/test/settings-merger.test.js +0 -167
  248. package/dist/test/shell-profile.test.d.ts +0 -1
  249. package/dist/test/shell-profile.test.js +0 -40
  250. package/dist/test/shell.test.d.ts +0 -1
  251. package/dist/test/shell.test.js +0 -71
  252. package/dist/test/signup.test.d.ts +0 -1
  253. package/dist/test/signup.test.js +0 -83
@@ -2,12 +2,39 @@
2
2
  export declare const PACKAGE_ROOT: string;
3
3
  /** Assets directory containing pre-rendered .md files */
4
4
  export declare const ASSETS_DIR: string;
5
+ export declare function setProjectRoot(path: string | null): void;
6
+ /** Walk upward from cwd to find the nearest directory containing .git or package.json. Falls back to cwd. */
7
+ export declare function findProjectRoot(): Promise<string>;
8
+ /** Return the Claude config home directory (~/.claude by default, or CLAUDE_HOME env override). */
5
9
  export declare function getClaudeHome(): string;
10
+ /** Return the path to Claude's global config file (~/.claude.json by default, or CLAUDE_JSON_PATH env override). */
6
11
  export declare function getClaudeJsonPath(): string;
7
- export declare function getLocalMcpPath(): string;
12
+ /** Return the path to the project-local MCP config file (.mcp.json in project root). */
13
+ export declare function getLocalMcpPath(): Promise<string>;
14
+ /** Return the UluOps state directory (~/.uluops/). Harness-neutral. */
15
+ export declare function getUluopsDir(): string;
16
+ /** Return the path to the UluOps install manifest file (new location). */
8
17
  export declare function getManifestPath(): string;
9
- export declare function getAgentsDir(localDefs: boolean): string;
10
- export declare function getCommandsDir(localDefs: boolean): string;
18
+ /** Return the legacy manifest path for migration. */
19
+ export declare function getLegacyManifestPath(): string;
20
+ /**
21
+ * Return the backup directory for a harness's **config** files.
22
+ *
23
+ * Scope is deliberately narrow: this directory holds copies of mutable
24
+ * user-owned config surfaces (the MCP config file, the shell profile),
25
+ * NOT vendor-owned tool files in `~/.claude/tools/agent-metrics/`. Tool
26
+ * files are treated as disposable — they can be regenerated by re-running
27
+ * setup, and the source of truth lives in the npm-installed
28
+ * `@uluops/agent-metrics` package. Backing them up would require a
29
+ * different ritual (versioned snapshots tied to the manifest's
30
+ * hooksInstalledVersion) and is not provided here.
31
+ *
32
+ * Renaming this to `getConfigBackupDir` would be more honest but is a
33
+ * public surface change deferred until we have a tool-file backup to
34
+ * disambiguate against.
35
+ */
36
+ export declare function getBackupDir(harnessName: string): string;
37
+ /** Detect the user's shell and return its name and profile path, or null if unsupported. */
11
38
  export declare function getShellProfile(): {
12
39
  shell: string;
13
40
  path: string;
package/dist/lib/paths.js CHANGED
@@ -1,33 +1,119 @@
1
1
  import { homedir, platform } from "node:os";
2
- import { join, dirname } from "node:path";
2
+ import { join, dirname, isAbsolute } from "node:path";
3
3
  import { fileURLToPath } from "node:url";
4
+ import { access } from "node:fs/promises";
4
5
  const __dirname = dirname(fileURLToPath(import.meta.url));
5
6
  /** Root of the npm package (where assets/ lives) */
6
7
  export const PACKAGE_ROOT = join(__dirname, "..", "..");
7
8
  /** Assets directory containing pre-rendered .md files */
8
9
  export const ASSETS_DIR = join(PACKAGE_ROOT, "assets");
10
+ /** Explicit project root override via --project-root flag or env var. */
11
+ let projectRootOverride = null;
12
+ export function setProjectRoot(path) {
13
+ projectRootOverride = path;
14
+ }
15
+ /** Walk upward from cwd to find the nearest directory containing .git or package.json. Falls back to cwd. */
16
+ export async function findProjectRoot() {
17
+ if (projectRootOverride)
18
+ return projectRootOverride;
19
+ const envRoot = process.env["ULUOPS_PROJECT_ROOT"];
20
+ if (envRoot) {
21
+ if (!isAbsolute(envRoot)) {
22
+ throw new Error(`ULUOPS_PROJECT_ROOT must be an absolute path, got: ${envRoot}`);
23
+ }
24
+ if (envRoot.includes("..")) {
25
+ throw new Error(`ULUOPS_PROJECT_ROOT must not contain traversal sequences: ${envRoot}`);
26
+ }
27
+ return envRoot;
28
+ }
29
+ let dir = process.cwd();
30
+ const root = dirname(dir);
31
+ while (dir !== root) {
32
+ if (await isProjectMarker(dir))
33
+ return dir;
34
+ dir = dirname(dir);
35
+ }
36
+ if (await isProjectMarker(dir))
37
+ return dir;
38
+ return process.cwd();
39
+ }
40
+ async function isProjectMarker(dir) {
41
+ try {
42
+ await access(join(dir, ".git"));
43
+ return true;
44
+ }
45
+ catch {
46
+ // continue
47
+ }
48
+ try {
49
+ await access(join(dir, "package.json"));
50
+ return true;
51
+ }
52
+ catch {
53
+ // continue
54
+ }
55
+ return false;
56
+ }
57
+ /** Validate an env-var-supplied path: must be absolute and contain no traversal sequences. */
58
+ function validateEnvPath(value, varName) {
59
+ if (!isAbsolute(value)) {
60
+ throw new Error(`${varName} must be an absolute path, got: ${value}`);
61
+ }
62
+ if (value.includes("..")) {
63
+ throw new Error(`${varName} must not contain traversal sequences: ${value}`);
64
+ }
65
+ return value;
66
+ }
67
+ /** Return the Claude config home directory (~/.claude by default, or CLAUDE_HOME env override). */
9
68
  export function getClaudeHome() {
69
+ const envHome = process.env["CLAUDE_HOME"];
70
+ if (envHome)
71
+ return validateEnvPath(envHome, "CLAUDE_HOME");
10
72
  return join(homedir(), ".claude");
11
73
  }
74
+ /** Return the path to Claude's global config file (~/.claude.json by default, or CLAUDE_JSON_PATH env override). */
12
75
  export function getClaudeJsonPath() {
76
+ const envPath = process.env["CLAUDE_JSON_PATH"];
77
+ if (envPath)
78
+ return validateEnvPath(envPath, "CLAUDE_JSON_PATH");
13
79
  return join(homedir(), ".claude.json");
14
80
  }
15
- export function getLocalMcpPath() {
16
- return join(process.cwd(), ".mcp.json");
81
+ /** Return the path to the project-local MCP config file (.mcp.json in project root). */
82
+ export async function getLocalMcpPath() {
83
+ return join(await findProjectRoot(), ".mcp.json");
17
84
  }
85
+ /** Return the UluOps state directory (~/.uluops/). Harness-neutral. */
86
+ export function getUluopsDir() {
87
+ return join(homedir(), ".uluops");
88
+ }
89
+ /** Return the path to the UluOps install manifest file (new location). */
18
90
  export function getManifestPath() {
19
- return join(getClaudeHome(), "uluops-manifest.json");
91
+ return join(getUluopsDir(), "manifest.json");
20
92
  }
21
- export function getAgentsDir(localDefs) {
22
- if (localDefs)
23
- return join(process.cwd(), "uluops", "agents");
24
- return join(getClaudeHome(), "agents");
93
+ /** Return the legacy manifest path for migration. */
94
+ export function getLegacyManifestPath() {
95
+ return join(getClaudeHome(), "uluops-manifest.json");
25
96
  }
26
- export function getCommandsDir(localDefs) {
27
- if (localDefs)
28
- return join(process.cwd(), "uluops", "commands");
29
- return join(getClaudeHome(), "commands");
97
+ /**
98
+ * Return the backup directory for a harness's **config** files.
99
+ *
100
+ * Scope is deliberately narrow: this directory holds copies of mutable
101
+ * user-owned config surfaces (the MCP config file, the shell profile),
102
+ * NOT vendor-owned tool files in `~/.claude/tools/agent-metrics/`. Tool
103
+ * files are treated as disposable — they can be regenerated by re-running
104
+ * setup, and the source of truth lives in the npm-installed
105
+ * `@uluops/agent-metrics` package. Backing them up would require a
106
+ * different ritual (versioned snapshots tied to the manifest's
107
+ * hooksInstalledVersion) and is not provided here.
108
+ *
109
+ * Renaming this to `getConfigBackupDir` would be more honest but is a
110
+ * public surface change deferred until we have a tool-file backup to
111
+ * disambiguate against.
112
+ */
113
+ export function getBackupDir(harnessName) {
114
+ return join(getUluopsDir(), "backups", harnessName);
30
115
  }
116
+ /** Detect the user's shell and return its name and profile path, or null if unsupported. */
31
117
  export function getShellProfile() {
32
118
  const shell = process.env["SHELL"] ?? "";
33
119
  const home = homedir();
@@ -13,31 +13,54 @@ interface HookMatcher {
13
13
  matcher?: string;
14
14
  hooks: HookEntry[];
15
15
  }
16
- interface ClaudeSettings {
16
+ export interface HarnessSettings {
17
17
  permissions?: Record<string, unknown>;
18
18
  hooks?: Record<string, HookMatcher[]>;
19
19
  [key: string]: unknown;
20
20
  }
21
+ /**
22
+ * Supported hook event types in Claude Code's settings.json schema.
23
+ *
24
+ * This set is a snapshot of the harness's vocabulary. When Claude Code
25
+ * adds, renames, or removes hook types, this set rots — the `probeHookSupport`
26
+ * warning will fire on legitimate user configs and (worse) train users to
27
+ * ignore it. The snapshot test in `settings-merger.test.ts` exists to make
28
+ * any change to the set visible in PR review so the warning logic can be
29
+ * re-evaluated.
30
+ *
31
+ * Exported for that test only — runtime callers should use `probeHookSupport`.
32
+ */
33
+ export declare const CLAUDE_HOOK_TYPES: Set<string>;
34
+ /** Default Claude Code hook event used when no override is configured. */
35
+ export declare const DEFAULT_CLAUDE_HOOK_TYPE = "SubagentStop";
36
+ export interface HookProbeResult {
37
+ hookType: string;
38
+ supported: boolean;
39
+ warning?: string;
40
+ }
41
+ /** Check whether the configured hook event type is in the known supported set. Returns the resolved hook type and a warning if unsupported. */
42
+ export declare function probeHookSupport(hookTypeOverride?: string): HookProbeResult;
21
43
  /**
22
44
  * Read an existing settings.json, or return empty object if it doesn't exist.
45
+ * Throws on malformed JSON to prevent silent data loss during merge+write.
23
46
  */
24
- export declare function readSettings(path: string): Promise<ClaudeSettings>;
47
+ export declare function readSettings(path: string): Promise<HarnessSettings>;
25
48
  /**
26
49
  * Write settings back to file with stable formatting.
27
50
  */
28
- export declare function writeSettings(path: string, settings: ClaudeSettings): Promise<void>;
51
+ export declare function writeSettings(path: string, settings: HarnessSettings): Promise<void>;
29
52
  /**
30
- * Merge the UluOps SubagentStop hook into settings, preserving all other
53
+ * Merge the UluOps hook into settings, preserving all other
31
54
  * hooks and settings. If a UluOps hook already exists, it is replaced.
32
55
  */
33
- export declare function mergeUluopsHook(settings: ClaudeSettings, hookCommand: string): ClaudeSettings;
56
+ export declare function mergeUluopsHook(settings: HarnessSettings, hookCommand: string, hookTypeOverride?: string, matcher?: string): HarnessSettings;
34
57
  /**
35
- * Remove UluOps hook entries from settings. If SubagentStop becomes empty,
58
+ * Remove UluOps hook entries from settings. If a hook type becomes empty,
36
59
  * the key is removed. If hooks becomes empty, the key is removed.
37
60
  */
38
- export declare function removeUluopsHook(settings: ClaudeSettings): ClaudeSettings;
61
+ export declare function removeUluopsHook(settings: HarnessSettings, hookTypeOverride?: string): HarnessSettings;
39
62
  /**
40
63
  * Check if a UluOps hook is configured in settings.
41
64
  */
42
- export declare function hasUluopsHook(settings: ClaudeSettings): boolean;
65
+ export declare function hasUluopsHook(settings: HarnessSettings, hookTypeOverride?: string): boolean;
43
66
  export {};
@@ -4,37 +4,95 @@
4
4
  * Safe read/merge/remove for Claude Code's settings.json.
5
5
  * Only touches UluOps-managed hook entries — all other settings preserved.
6
6
  */
7
- import { readFile, writeFile } from "node:fs/promises";
8
- /** Marker embedded in hook commands to identify UluOps-managed entries */
9
- const ULUOPS_HOOK_MARKER = "tools/agent-metrics";
7
+ import { readFile } from "node:fs/promises";
8
+ import { atomicWrite } from "./atomic-write.js";
9
+ /**
10
+ * Substring used purely as an *ownership sentinel* — present in every hook
11
+ * command we install, so we can identify our own entries in `settings.json`
12
+ * without false-positives on the user's hooks.
13
+ *
14
+ * This is intentionally NOT a path constant. The path where the hook lives
15
+ * is derived from each profile's `paths.toolsDir`; if those move (a harness
16
+ * restructure, a custom toolsDir, a future per-instance layout), the
17
+ * signature must remain stable so existing user settings.json entries
18
+ * keep being recognized as UluOps-managed.
19
+ *
20
+ * The current value `agent-metrics/dist/hook.js` is the suffix of every
21
+ * hook command we emit — discriminating enough to avoid colliding with
22
+ * user-named tools while surviving moves of the parent directory.
23
+ */
24
+ const HOOK_OWNERSHIP_SIGNATURE = "agent-metrics/dist/hook.js";
25
+ /**
26
+ * Supported hook event types in Claude Code's settings.json schema.
27
+ *
28
+ * This set is a snapshot of the harness's vocabulary. When Claude Code
29
+ * adds, renames, or removes hook types, this set rots — the `probeHookSupport`
30
+ * warning will fire on legitimate user configs and (worse) train users to
31
+ * ignore it. The snapshot test in `settings-merger.test.ts` exists to make
32
+ * any change to the set visible in PR review so the warning logic can be
33
+ * re-evaluated.
34
+ *
35
+ * Exported for that test only — runtime callers should use `probeHookSupport`.
36
+ */
37
+ export const CLAUDE_HOOK_TYPES = new Set([
38
+ "SubagentStop",
39
+ "PreToolUse",
40
+ "PostToolUse",
41
+ "Notification",
42
+ "Stop",
43
+ ]);
44
+ /** Default Claude Code hook event used when no override is configured. */
45
+ export const DEFAULT_CLAUDE_HOOK_TYPE = "SubagentStop";
46
+ /** Configurable hook type via env var. Falls back to SubagentStop. */
47
+ function getDefaultHookEventType() {
48
+ return process.env["ULUOPS_HOOK_TYPE"] ?? DEFAULT_CLAUDE_HOOK_TYPE;
49
+ }
50
+ /** Check whether the configured hook event type is in the known supported set. Returns the resolved hook type and a warning if unsupported. */
51
+ export function probeHookSupport(hookTypeOverride) {
52
+ const hookType = hookTypeOverride ?? getDefaultHookEventType();
53
+ if (CLAUDE_HOOK_TYPES.has(hookType) || hookType === "AfterTool") {
54
+ return { hookType, supported: true };
55
+ }
56
+ return {
57
+ hookType,
58
+ supported: false,
59
+ warning: `Hook type "${hookType}" is not in the known supported set {${[...CLAUDE_HOOK_TYPES].join(", ")}, AfterTool}. Metrics may silently fail if this hook type does not exist in the harness.`,
60
+ };
61
+ }
10
62
  /**
11
63
  * Read an existing settings.json, or return empty object if it doesn't exist.
64
+ * Throws on malformed JSON to prevent silent data loss during merge+write.
12
65
  */
13
66
  export async function readSettings(path) {
67
+ let raw;
68
+ try {
69
+ raw = await readFile(path, "utf-8");
70
+ }
71
+ catch {
72
+ return {}; // File doesn't exist — fresh config
73
+ }
14
74
  try {
15
- const raw = await readFile(path, "utf-8");
16
75
  return JSON.parse(raw);
17
76
  }
18
77
  catch {
19
- return {};
78
+ throw new Error(`Failed to parse settings at ${path} — file contains invalid JSON`);
20
79
  }
21
80
  }
22
81
  /**
23
82
  * Write settings back to file with stable formatting.
24
83
  */
25
84
  export async function writeSettings(path, settings) {
26
- await writeFile(path, JSON.stringify(settings, null, 2) + "\n");
85
+ await atomicWrite(path, JSON.stringify(settings, null, 2) + "\n");
27
86
  }
28
87
  /**
29
- * Merge the UluOps SubagentStop hook into settings, preserving all other
88
+ * Merge the UluOps hook into settings, preserving all other
30
89
  * hooks and settings. If a UluOps hook already exists, it is replaced.
31
90
  */
32
- export function mergeUluopsHook(settings, hookCommand) {
91
+ export function mergeUluopsHook(settings, hookCommand, hookTypeOverride, matcher) {
92
+ const hookType = hookTypeOverride ?? getDefaultHookEventType();
33
93
  const hooks = settings.hooks ?? {};
34
- const existing = hooks["SubagentStop"] ?? [];
35
- // Remove any existing UluOps hook entries
36
- const filtered = existing.filter((m) => !m.hooks.some((h) => h.command.includes(ULUOPS_HOOK_MARKER)));
37
- // Add the new UluOps hook
94
+ const existing = hooks[hookType] ?? [];
95
+ const filtered = existing.filter((m) => !m.hooks.some((h) => h.command.includes(HOOK_OWNERSHIP_SIGNATURE)));
38
96
  const uluopsHook = {
39
97
  hooks: [
40
98
  {
@@ -44,32 +102,36 @@ export function mergeUluopsHook(settings, hookCommand) {
44
102
  },
45
103
  ],
46
104
  };
105
+ if (matcher) {
106
+ uluopsHook.matcher = matcher;
107
+ }
47
108
  return {
48
109
  ...settings,
49
110
  hooks: {
50
111
  ...hooks,
51
- SubagentStop: [...filtered, uluopsHook],
112
+ [hookType]: [...filtered, uluopsHook],
52
113
  },
53
114
  };
54
115
  }
55
116
  /**
56
- * Remove UluOps hook entries from settings. If SubagentStop becomes empty,
117
+ * Remove UluOps hook entries from settings. If a hook type becomes empty,
57
118
  * the key is removed. If hooks becomes empty, the key is removed.
58
119
  */
59
- export function removeUluopsHook(settings) {
120
+ export function removeUluopsHook(settings, hookTypeOverride) {
121
+ const hookType = hookTypeOverride ?? getDefaultHookEventType();
60
122
  const hooks = settings.hooks;
61
123
  if (!hooks)
62
124
  return settings;
63
- const subagentStop = hooks["SubagentStop"];
64
- if (!subagentStop)
125
+ const hookEntries = hooks[hookType];
126
+ if (!hookEntries)
65
127
  return settings;
66
- const filtered = subagentStop.filter((m) => !m.hooks.some((h) => h.command.includes(ULUOPS_HOOK_MARKER)));
128
+ const filtered = hookEntries.filter((m) => !m.hooks.some((h) => h.command.includes(HOOK_OWNERSHIP_SIGNATURE)));
67
129
  const updatedHooks = { ...hooks };
68
130
  if (filtered.length === 0) {
69
- delete updatedHooks["SubagentStop"];
131
+ delete updatedHooks[hookType];
70
132
  }
71
133
  else {
72
- updatedHooks["SubagentStop"] = filtered;
134
+ updatedHooks[hookType] = filtered;
73
135
  }
74
136
  const result = { ...settings };
75
137
  if (Object.keys(updatedHooks).length === 0) {
@@ -83,9 +145,10 @@ export function removeUluopsHook(settings) {
83
145
  /**
84
146
  * Check if a UluOps hook is configured in settings.
85
147
  */
86
- export function hasUluopsHook(settings) {
87
- const subagentStop = settings.hooks?.["SubagentStop"];
88
- if (!subagentStop)
148
+ export function hasUluopsHook(settings, hookTypeOverride) {
149
+ const hookType = hookTypeOverride ?? getDefaultHookEventType();
150
+ const hookEntries = settings.hooks?.[hookType];
151
+ if (!hookEntries)
89
152
  return false;
90
- return subagentStop.some((m) => m.hooks.some((h) => h.command.includes(ULUOPS_HOOK_MARKER)));
153
+ return hookEntries.some((m) => m.hooks.some((h) => h.command.includes(HOOK_OWNERSHIP_SIGNATURE)));
91
154
  }
@@ -0,0 +1,2 @@
1
+ /** Read the package version from package.json. */
2
+ export declare function getVersion(): Promise<string>;
@@ -0,0 +1,10 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { join, dirname } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ const __dirname = dirname(fileURLToPath(import.meta.url));
5
+ /** Read the package version from package.json. */
6
+ export async function getVersion() {
7
+ const pkgPath = join(__dirname, "..", "..", "package.json");
8
+ const pkg = JSON.parse(await readFile(pkgPath, "utf-8"));
9
+ return pkg.version;
10
+ }
@@ -1,8 +1,11 @@
1
+ import type { HarnessProfile } from "../harnesses/index.js";
1
2
  export interface AgentsResult {
2
3
  copied: number;
3
4
  skipped: number;
4
5
  removed: number;
5
6
  files: string[];
6
7
  }
7
- export declare function installAgents(localDefs: boolean, dryRun: boolean, existingManifestAgents?: string[]): Promise<AgentsResult>;
8
+ /** Copy pre-rendered agent definitions from harness-specific assets to the target directory. */
9
+ export declare function installAgents(profile: HarnessProfile, localDefs: boolean, dryRun: boolean, existingManifestAgents?: string[]): Promise<AgentsResult>;
10
+ /** Remove previously installed agent files by name. */
8
11
  export declare function uninstallAgents(files: string[], defsPath: string): Promise<number>;
@@ -1,14 +1,53 @@
1
+ import { readdir, mkdir, unlink } from "node:fs/promises";
1
2
  import { join } from "node:path";
2
- import { ASSETS_DIR, getAgentsDir } from "../lib/paths.js";
3
- import { syncAssets, unlinkFiles } from "../lib/file-ops.js";
4
- export async function installAgents(localDefs, dryRun, existingManifestAgents) {
5
- return syncAssets({
6
- srcDir: join(ASSETS_DIR, "agents"),
7
- destDir: getAgentsDir(localDefs),
8
- dryRun,
9
- oldManifestFiles: existingManifestAgents,
10
- });
3
+ import { ASSETS_DIR, findProjectRoot } from "../lib/paths.js";
4
+ import { copyIfChanged, unlinkFiles } from "../lib/file-ops.js";
5
+ /** Copy pre-rendered agent definitions from harness-specific assets to the target directory. */
6
+ export async function installAgents(profile, localDefs, dryRun, existingManifestAgents) {
7
+ const srcDir = join(ASSETS_DIR, profile.name, "agents");
8
+ const destDir = localDefs
9
+ ? join(await findProjectRoot(), "uluops", "agents")
10
+ : profile.paths.agentsDir;
11
+ if (!dryRun) {
12
+ await mkdir(destDir, { recursive: true });
13
+ }
14
+ const ext = profile.agentExtension;
15
+ let files;
16
+ try {
17
+ files = (await readdir(srcDir)).filter((f) => f.endsWith(ext));
18
+ }
19
+ catch {
20
+ return { copied: 0, skipped: 0, removed: 0, files: [] };
21
+ }
22
+ let copied = 0;
23
+ let skipped = 0;
24
+ for (const file of files) {
25
+ const result = await copyIfChanged(join(srcDir, file), join(destDir, file), dryRun);
26
+ if (result === "copied")
27
+ copied++;
28
+ else
29
+ skipped++;
30
+ }
31
+ // Remove files that were in the old manifest but no longer in the package
32
+ let removed = 0;
33
+ if (existingManifestAgents) {
34
+ for (const oldFile of existingManifestAgents) {
35
+ if (!files.includes(oldFile)) {
36
+ if (!dryRun) {
37
+ try {
38
+ await unlink(join(destDir, oldFile));
39
+ }
40
+ catch {
41
+ // Already gone
42
+ }
43
+ }
44
+ removed++;
45
+ }
46
+ }
47
+ }
48
+ return { copied, skipped, removed, files };
11
49
  }
50
+ /** Remove previously installed agent files by name. */
12
51
  export async function uninstallAgents(files, defsPath) {
13
52
  return unlinkFiles(join(defsPath, "agents"), files);
14
53
  }
@@ -1,6 +1,9 @@
1
1
  import { readFile } from "node:fs/promises";
2
2
  import { join } from "node:path";
3
3
  import { homedir } from "node:os";
4
+ function getKeyPrefix() {
5
+ return process.env["ULUOPS_KEY_PREFIX"] ?? "ulr_";
6
+ }
4
7
  /**
5
8
  * Resolve API key from flags, env, credentials file, or interactive prompt.
6
9
  */
@@ -20,20 +23,22 @@ export async function resolveApiKey(options) {
20
23
  if (!options.interactive) {
21
24
  throw new Error("No API key found. Pass --api-key or set ULUOPS_API_KEY. Get one at app.uluops.ai/settings/api-keys");
22
25
  }
26
+ const prefix = getKeyPrefix();
23
27
  const { input } = await import("@inquirer/prompts");
24
28
  apiKey = await input({
25
29
  message: "Enter your UluOps API key",
26
30
  validate: (val) => {
27
31
  if (!val.trim())
28
32
  return "Get a key at app.uluops.ai/settings/api-keys";
29
- if (!val.startsWith("ulr_"))
30
- return "API keys start with ulr_get one at app.uluops.ai/settings/api-keys";
33
+ if (!val.startsWith(prefix))
34
+ return `API keys typically start with ${prefix}if your key has a different format, just paste it and server validation will check it`;
31
35
  return true;
32
36
  },
33
37
  });
34
38
  }
35
- if (!apiKey?.startsWith("ulr_")) {
36
- throw new Error("API keys start with ulr_ — get one at app.uluops.ai/settings/api-keys");
39
+ const prefix = getKeyPrefix();
40
+ if (!apiKey.startsWith(prefix) && !options.skipValidation) {
41
+ process.stderr.write(` ⚠ Key does not start with expected prefix "${prefix}" — proceeding with server validation\n`);
37
42
  }
38
43
  // Validate against server
39
44
  if (!options.skipValidation) {
@@ -43,22 +48,33 @@ export async function resolveApiKey(options) {
43
48
  return { apiKey, email: null };
44
49
  }
45
50
  async function readCredentialsFile() {
51
+ const credsPath = join(homedir(), ".uluops", "credentials.json");
52
+ let raw;
46
53
  try {
47
- const credsPath = join(homedir(), ".uluops", "credentials.json");
48
- const raw = await readFile(credsPath, "utf-8");
49
- const creds = JSON.parse(raw);
50
- const defaultProfile = creds["default"];
51
- return defaultProfile?.apiKey ?? defaultProfile?.api_key;
54
+ raw = await readFile(credsPath, "utf-8");
52
55
  }
53
56
  catch {
54
- return undefined;
57
+ return undefined; // File doesn't exist
58
+ }
59
+ let creds;
60
+ try {
61
+ creds = JSON.parse(raw);
55
62
  }
63
+ catch (err) {
64
+ throw new Error(`Malformed credentials file at ${credsPath}: ${err instanceof Error ? err.message : "invalid JSON"}`);
65
+ }
66
+ if (typeof creds !== "object" || creds === null)
67
+ return undefined;
68
+ const profiles = creds;
69
+ const defaultProfile = profiles["default"];
70
+ return defaultProfile?.apiKey ?? defaultProfile?.api_key;
56
71
  }
57
72
  async function validateKey(apiKey) {
58
73
  const url = "https://api.uluops.ai/api/v1/registry/users/me";
59
74
  try {
60
75
  const res = await fetch(url, {
61
76
  headers: { Authorization: `Bearer ${apiKey}` },
77
+ signal: AbortSignal.timeout(15000),
62
78
  });
63
79
  if (res.status === 401) {
64
80
  throw new Error("Invalid API key — generate a new one at app.uluops.ai/settings/api-keys");
@@ -0,0 +1,53 @@
1
+ export declare const CLI_PACKAGE = "@uluops/cli";
2
+ export declare const CLI_BIN = "ulu";
3
+ export interface CliExecutor {
4
+ /** Returns the installed CLI version, or null if `ulu` is not on PATH or fails to run. */
5
+ detect: () => string | null;
6
+ /** Best-effort `npm install -g`. Returns ok + captured error for surface display. */
7
+ install: () => {
8
+ ok: boolean;
9
+ error?: string;
10
+ };
11
+ /** Best-effort `npm uninstall -g`. Returns ok + captured error. */
12
+ uninstall: () => {
13
+ ok: boolean;
14
+ error?: string;
15
+ };
16
+ }
17
+ /** Default executor — shells out to `ulu` and `npm`. */
18
+ export declare const defaultExecutor: CliExecutor;
19
+ export interface CliInstallResult {
20
+ /** `ulu` is on PATH after this step, regardless of how it got there. */
21
+ installed: boolean;
22
+ /** Version string from `ulu --version`, if detectable. */
23
+ version: string | null;
24
+ /** True when `ulu` was already on PATH before we did anything. */
25
+ alreadyPresent: boolean;
26
+ /** Set when our `npm install -g` attempt failed; caller decides how to surface. */
27
+ error?: string;
28
+ }
29
+ /**
30
+ * Install `@uluops/cli` globally if not already present.
31
+ *
32
+ * Designed to never abort the parent setup flow:
33
+ * - If `ulu` is already on PATH, returns `{ installed: true, alreadyPresent: true }` without touching npm.
34
+ * - If `npm install -g` fails (permissions, network, nvm prefix surprise), returns
35
+ * `{ installed: false, error }` so the caller can warn-and-continue.
36
+ * - In dryRun mode, no executor calls happen.
37
+ */
38
+ export declare function installCli(opts: {
39
+ dryRun: boolean;
40
+ executor?: CliExecutor;
41
+ }): Promise<CliInstallResult>;
42
+ export interface CliUninstallResult {
43
+ removed: boolean;
44
+ error?: string;
45
+ }
46
+ /**
47
+ * Uninstall `@uluops/cli` globally. Best-effort: if the package isn't there,
48
+ * npm exits non-zero on some platforms — we treat that as success.
49
+ */
50
+ export declare function uninstallCli(opts: {
51
+ dryRun: boolean;
52
+ executor?: CliExecutor;
53
+ }): Promise<CliUninstallResult>;