@uluops/setup 0.4.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 (211) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +67 -50
  3. package/assets/auto-tracker-save.mjs +142 -0
  4. package/assets/{agents → claude-code/agents}/api-contract-validator-agent.md +9 -228
  5. package/assets/{agents → claude-code/agents}/aristotle-analyst-agent.md +51 -4
  6. package/assets/{agents → claude-code/agents}/aristotle-explorer-agent.md +6 -2
  7. package/assets/{agents → claude-code/agents}/aristotle-forecaster-agent.md +15 -230
  8. package/assets/{agents → claude-code/agents}/aristotle-validator-agent.md +12 -252
  9. package/assets/{agents → claude-code/agents}/assumption-excavator-agent.md +21 -247
  10. package/assets/{agents → claude-code/agents}/code-auditor-agent.md +12 -255
  11. package/assets/{agents → claude-code/agents}/code-optimizer-agent.md +15 -236
  12. package/assets/{agents → claude-code/agents}/code-validator-agent.md +31 -300
  13. package/assets/claude-code/agents/docs-validator-agent.md +472 -0
  14. package/assets/{agents → claude-code/agents}/frontend-validator-agent.md +15 -258
  15. package/assets/{agents → claude-code/agents}/mcp-validator-agent.md +8 -252
  16. package/assets/{agents → claude-code/agents}/pre-implementation-architect-agent.md +8 -224
  17. package/assets/{agents → claude-code/agents}/prompt-engineer-agent.md +57 -290
  18. package/assets/{agents → claude-code/agents}/prompt-pattern-analyzer-agent.md +10 -225
  19. package/assets/{agents → claude-code/agents}/prompt-quality-validator-agent.md +11 -249
  20. package/assets/{agents → claude-code/agents}/public-interface-validator-agent.md +15 -268
  21. package/assets/claude-code/agents/release-readiness-agent.md +495 -0
  22. package/assets/{agents → claude-code/agents}/security-analyst-agent.md +236 -480
  23. package/assets/{agents → claude-code/agents}/test-architect-agent.md +16 -259
  24. package/assets/{agents → claude-code/agents}/type-safety-validator-agent.md +23 -266
  25. package/assets/{agents → claude-code/agents}/workflow-synthesis-agent.md +23 -226
  26. package/assets/{commands → claude-code/commands}/agents/anxiety-reader.md +12 -15
  27. package/assets/{commands → claude-code/commands}/agents/api-contract.md +156 -136
  28. package/assets/{commands → claude-code/commands}/agents/architect.md +156 -136
  29. package/assets/claude-code/commands/agents/aristotle-analyst.md +157 -0
  30. package/assets/claude-code/commands/agents/aristotle-explorer.md +157 -0
  31. package/assets/claude-code/commands/agents/aristotle-forecaster.md +157 -0
  32. package/assets/claude-code/commands/agents/aristotle-validator.md +157 -0
  33. package/assets/{commands → claude-code/commands}/agents/assumption-excavator.md +49 -7
  34. package/assets/{commands → claude-code/commands}/agents/audit.md +156 -137
  35. package/assets/{commands → claude-code/commands}/agents/docs-validate.md +156 -134
  36. package/assets/{commands → claude-code/commands}/agents/frontend.md +156 -136
  37. package/assets/{commands → claude-code/commands}/agents/mcp-validate.md +156 -137
  38. package/assets/{commands → claude-code/commands}/agents/optimize.md +156 -134
  39. package/assets/{commands → claude-code/commands}/agents/pattern-analyzer.md +150 -127
  40. package/assets/{commands → claude-code/commands}/agents/prompt-quality.md +155 -135
  41. package/assets/claude-code/commands/agents/prompt-validate.md +155 -0
  42. package/assets/{commands → claude-code/commands}/agents/public-interface.md +156 -135
  43. package/assets/{commands → claude-code/commands}/agents/release.md +156 -136
  44. package/assets/{commands → claude-code/commands}/agents/security.md +156 -138
  45. package/assets/{commands → claude-code/commands}/agents/test-review.md +156 -137
  46. package/assets/{commands → claude-code/commands}/agents/type-safety.md +156 -136
  47. package/assets/{commands/agents/code-validate.md → claude-code/commands/agents/validate.md} +156 -135
  48. package/assets/claude-code/commands/agents/workflow-synthesis.md +157 -0
  49. package/assets/{commands → claude-code/commands}/pipelines/aristotle.md +8 -8
  50. package/assets/{commands → claude-code/commands}/pipelines/ship.md +8 -8
  51. package/assets/claude-code/commands/workflows/post-implementation.md +60 -0
  52. package/assets/claude-code/commands/workflows/pre-implementation.md +46 -0
  53. package/assets/{commands → claude-code/commands}/workflows/prompt-audit.md +2 -2
  54. package/assets/codex/agents/anxiety-reader-agent.toml +462 -0
  55. package/assets/codex/agents/api-contract-validator-agent.toml +738 -0
  56. package/assets/codex/agents/aristotle-analyst-agent.toml +750 -0
  57. package/assets/codex/agents/aristotle-explorer-agent.toml +155 -0
  58. package/assets/codex/agents/aristotle-forecaster-agent.toml +449 -0
  59. package/assets/codex/agents/aristotle-validator-agent.toml +424 -0
  60. package/assets/codex/agents/assumption-excavator-agent.toml +1126 -0
  61. package/assets/codex/agents/code-auditor-agent.toml +815 -0
  62. package/assets/codex/agents/code-optimizer-agent.toml +652 -0
  63. package/assets/codex/agents/code-validator-agent.toml +573 -0
  64. package/assets/codex/agents/docs-validator-agent.toml +468 -0
  65. package/assets/codex/agents/frontend-validator-agent.toml +598 -0
  66. package/assets/codex/agents/mcp-validator-agent.toml +580 -0
  67. package/assets/codex/agents/pre-implementation-architect-agent.toml +817 -0
  68. package/assets/codex/agents/prompt-engineer-agent.toml +922 -0
  69. package/assets/codex/agents/prompt-pattern-analyzer-agent.toml +689 -0
  70. package/assets/codex/agents/prompt-quality-validator-agent.toml +777 -0
  71. package/assets/codex/agents/public-interface-validator-agent.toml +695 -0
  72. package/assets/codex/agents/release-readiness-agent.toml +491 -0
  73. package/assets/codex/agents/security-analyst-agent.toml +847 -0
  74. package/assets/codex/agents/test-architect-agent.toml +615 -0
  75. package/assets/codex/agents/type-safety-validator-agent.toml +686 -0
  76. package/assets/codex/agents/workflow-synthesis-agent.toml +631 -0
  77. package/assets/gemini-cli/agents/anxiety-reader-agent.md +470 -0
  78. package/assets/gemini-cli/agents/api-contract-validator-agent.md +747 -0
  79. package/assets/gemini-cli/agents/aristotle-analyst-agent.md +758 -0
  80. package/assets/gemini-cli/agents/aristotle-explorer-agent.md +163 -0
  81. package/assets/gemini-cli/agents/aristotle-forecaster-agent.md +457 -0
  82. package/assets/gemini-cli/agents/aristotle-validator-agent.md +432 -0
  83. package/assets/gemini-cli/agents/assumption-excavator-agent.md +1134 -0
  84. package/assets/gemini-cli/agents/code-auditor-agent.md +827 -0
  85. package/assets/gemini-cli/agents/code-optimizer-agent.md +661 -0
  86. package/assets/gemini-cli/agents/code-validator-agent.md +582 -0
  87. package/assets/gemini-cli/agents/docs-validator-agent.md +477 -0
  88. package/assets/gemini-cli/agents/frontend-validator-agent.md +610 -0
  89. package/assets/gemini-cli/agents/mcp-validator-agent.md +589 -0
  90. package/assets/gemini-cli/agents/pre-implementation-architect-agent.md +826 -0
  91. package/assets/gemini-cli/agents/prompt-engineer-agent.md +931 -0
  92. package/assets/gemini-cli/agents/prompt-pattern-analyzer-agent.md +698 -0
  93. package/assets/gemini-cli/agents/prompt-quality-validator-agent.md +786 -0
  94. package/assets/gemini-cli/agents/public-interface-validator-agent.md +707 -0
  95. package/assets/gemini-cli/agents/release-readiness-agent.md +500 -0
  96. package/assets/gemini-cli/agents/security-analyst-agent.md +859 -0
  97. package/assets/gemini-cli/agents/test-architect-agent.md +624 -0
  98. package/assets/gemini-cli/agents/type-safety-validator-agent.md +695 -0
  99. package/assets/gemini-cli/agents/workflow-synthesis-agent.md +639 -0
  100. package/assets/gemini-cli/commands/agents/anxiety-reader.toml +155 -0
  101. package/assets/gemini-cli/commands/agents/api-contract.toml +154 -0
  102. package/assets/gemini-cli/commands/agents/architect.toml +154 -0
  103. package/assets/gemini-cli/commands/agents/aristotle-analyst.toml +155 -0
  104. package/assets/gemini-cli/commands/agents/aristotle-explorer.toml +155 -0
  105. package/assets/gemini-cli/commands/agents/aristotle-forecaster.toml +155 -0
  106. package/assets/gemini-cli/commands/agents/aristotle-validator.toml +155 -0
  107. package/assets/gemini-cli/commands/agents/assumption-excavator.toml +155 -0
  108. package/assets/gemini-cli/commands/agents/audit.toml +154 -0
  109. package/assets/gemini-cli/commands/agents/docs-validate.toml +154 -0
  110. package/assets/gemini-cli/commands/agents/frontend.toml +154 -0
  111. package/assets/gemini-cli/commands/agents/mcp-validate.toml +154 -0
  112. package/assets/gemini-cli/commands/agents/optimize.toml +154 -0
  113. package/assets/gemini-cli/commands/agents/pattern-analyzer.toml +148 -0
  114. package/assets/gemini-cli/commands/agents/prompt-quality.toml +153 -0
  115. package/assets/gemini-cli/commands/agents/prompt-validate.toml +153 -0
  116. package/assets/gemini-cli/commands/agents/public-interface.toml +154 -0
  117. package/assets/gemini-cli/commands/agents/release.toml +154 -0
  118. package/assets/gemini-cli/commands/agents/security.toml +154 -0
  119. package/assets/gemini-cli/commands/agents/test-review.toml +154 -0
  120. package/assets/gemini-cli/commands/agents/type-safety.toml +154 -0
  121. package/assets/gemini-cli/commands/agents/validate.toml +154 -0
  122. package/assets/gemini-cli/commands/agents/workflow-synthesis.toml +155 -0
  123. package/assets/gemini-cli/commands/pipelines/aristotle.toml +139 -0
  124. package/assets/gemini-cli/commands/pipelines/ship.toml +184 -0
  125. package/assets/gemini-cli/commands/workflows/post-implementation.toml +56 -0
  126. package/assets/gemini-cli/commands/workflows/pre-implementation.toml +42 -0
  127. package/assets/gemini-cli/commands/workflows/prompt-audit.toml +40 -0
  128. package/assets/opencode/agents/anxiety-reader-agent.md +472 -0
  129. package/assets/opencode/agents/api-contract-validator-agent.md +749 -0
  130. package/assets/opencode/agents/aristotle-analyst-agent.md +760 -0
  131. package/assets/opencode/agents/aristotle-explorer-agent.md +164 -0
  132. package/assets/opencode/agents/aristotle-forecaster-agent.md +459 -0
  133. package/assets/opencode/agents/aristotle-validator-agent.md +434 -0
  134. package/assets/opencode/agents/assumption-excavator-agent.md +1136 -0
  135. package/assets/opencode/agents/code-auditor-agent.md +826 -0
  136. package/assets/opencode/agents/code-optimizer-agent.md +663 -0
  137. package/assets/opencode/agents/code-validator-agent.md +584 -0
  138. package/assets/opencode/agents/docs-validator-agent.md +479 -0
  139. package/assets/opencode/agents/frontend-validator-agent.md +609 -0
  140. package/assets/opencode/agents/mcp-validator-agent.md +591 -0
  141. package/assets/opencode/agents/pre-implementation-architect-agent.md +828 -0
  142. package/assets/opencode/agents/prompt-engineer-agent.md +933 -0
  143. package/assets/opencode/agents/prompt-pattern-analyzer-agent.md +700 -0
  144. package/assets/opencode/agents/prompt-quality-validator-agent.md +788 -0
  145. package/assets/opencode/agents/public-interface-validator-agent.md +706 -0
  146. package/assets/opencode/agents/release-readiness-agent.md +502 -0
  147. package/assets/opencode/agents/security-analyst-agent.md +858 -0
  148. package/assets/opencode/agents/test-architect-agent.md +626 -0
  149. package/assets/opencode/agents/type-safety-validator-agent.md +697 -0
  150. package/assets/opencode/agents/workflow-synthesis-agent.md +641 -0
  151. package/dist/cli.js +12 -414
  152. package/dist/commands/helpers.d.ts +73 -0
  153. package/dist/commands/helpers.js +274 -0
  154. package/dist/commands/setup.d.ts +13 -0
  155. package/dist/commands/setup.js +93 -0
  156. package/dist/commands/uninstall.d.ts +3 -0
  157. package/dist/commands/uninstall.js +126 -0
  158. package/dist/commands/verify.d.ts +1 -0
  159. package/dist/commands/verify.js +28 -0
  160. package/dist/harnesses/claude-code.d.ts +1 -1
  161. package/dist/harnesses/claude-code.js +3 -1
  162. package/dist/harnesses/codex.js +6 -5
  163. package/dist/harnesses/gemini-cli.d.ts +4 -8
  164. package/dist/harnesses/gemini-cli.js +47 -21
  165. package/dist/harnesses/index.d.ts +10 -1
  166. package/dist/harnesses/index.js +11 -2
  167. package/dist/harnesses/opencode.d.ts +1 -1
  168. package/dist/harnesses/opencode.js +15 -6
  169. package/dist/harnesses/types.d.ts +19 -0
  170. package/dist/harnesses/types.js +2 -0
  171. package/dist/lib/asset-catalog.js +2 -2
  172. package/dist/lib/config-merger.d.ts +2 -1
  173. package/dist/lib/config-merger.js +12 -4
  174. package/dist/lib/file-ops.d.ts +5 -0
  175. package/dist/lib/file-ops.js +18 -3
  176. package/dist/lib/hash.d.ts +1 -1
  177. package/dist/lib/hash.js +2 -2
  178. package/dist/lib/manifest.d.ts +30 -1
  179. package/dist/lib/manifest.js +5 -7
  180. package/dist/lib/paths.d.ts +16 -1
  181. package/dist/lib/paths.js +31 -3
  182. package/dist/lib/settings-merger.d.ts +24 -9
  183. package/dist/lib/settings-merger.js +57 -22
  184. package/dist/lib/version.d.ts +2 -0
  185. package/dist/lib/version.js +10 -0
  186. package/dist/steps/agents.d.ts +1 -2
  187. package/dist/steps/agents.js +7 -18
  188. package/dist/steps/cli.d.ts +53 -0
  189. package/dist/steps/cli.js +90 -0
  190. package/dist/steps/commands.d.ts +1 -1
  191. package/dist/steps/commands.js +20 -71
  192. package/dist/steps/detect.js +4 -0
  193. package/dist/steps/mcp.js +7 -15
  194. package/dist/steps/metrics.d.ts +12 -0
  195. package/dist/steps/metrics.js +52 -22
  196. package/dist/steps/shell.js +11 -1
  197. package/dist/steps/signup.d.ts +2 -2
  198. package/dist/steps/signup.js +9 -12
  199. package/dist/steps/verify.js +47 -8
  200. package/package.json +12 -11
  201. package/assets/agents/docs-validator-agent.md +0 -490
  202. package/assets/agents/release-readiness-agent.md +0 -482
  203. package/assets/commands/agents/aristotle-analyst.md +0 -116
  204. package/assets/commands/agents/aristotle-explorer.md +0 -93
  205. package/assets/commands/agents/aristotle-forecaster.md +0 -115
  206. package/assets/commands/agents/aristotle-validator.md +0 -115
  207. package/assets/commands/agents/prompt-validate.md +0 -136
  208. package/assets/commands/agents/workflow-synthesis.md +0 -102
  209. package/assets/commands/workflows/post-implementation.md +0 -577
  210. package/assets/commands/workflows/pre-implementation.md +0 -670
  211. /package/assets/{agents → claude-code/agents}/anxiety-reader-agent.md +0 -0
@@ -1,41 +1,67 @@
1
1
  /**
2
- * Gemini CLI Harness Profile (Scaffold)
2
+ * Gemini CLI Harness Profile
3
3
  *
4
4
  * Paths and metadata are verified from vendor docs.
5
5
  * MCP config uses `mcpServers` key (same shape as Claude Code)
6
6
  * but at a different file path (~/.gemini/settings.json).
7
7
  *
8
- * NOT YET TESTED with UluOps agents. McpConfigStrategy throws
9
- * until integration testing is complete.
10
- *
11
- * Note: Gemini CLI cannot have underscores in MCP server names
12
- * (FQN format mcp_serverName_toolName uses underscore as delimiter).
13
- * Our names use hyphens — safe.
8
+ * Gemini CLI requires `trust: true` in MCP server config to
9
+ * allow automatic tool execution without confirmation prompts.
14
10
  */
15
11
  import { homedir } from "node:os";
16
12
  import { join } from "node:path";
17
- import { HarnessNotTestedError } from "./types.js";
13
+ import { ULUOPS_SERVERS, } from "./types.js";
14
+ import { readConfig, mergeUluopsMcp, removeUluopsMcp, writeConfig, } from "../lib/config-merger.js";
15
+ import { readSettings, writeSettings, mergeUluopsHook, removeUluopsHook, hasUluopsHook, } from "../lib/settings-merger.js";
18
16
  class GeminiMcpConfig {
19
- async read() {
20
- throw new HarnessNotTestedError("Gemini CLI");
17
+ async read(path) {
18
+ return readConfig(path);
19
+ }
20
+ merge(config, apiKey) {
21
+ // Gemini CLI requires trust: true for a smooth experience
22
+ return mergeUluopsMcp(config, apiKey, true);
23
+ }
24
+ remove(config) {
25
+ return removeUluopsMcp(config);
21
26
  }
22
- merge() {
23
- throw new HarnessNotTestedError("Gemini CLI");
27
+ async write(path, config) {
28
+ await writeConfig(path, config);
24
29
  }
25
- remove() {
26
- throw new HarnessNotTestedError("Gemini CLI");
30
+ check(config) {
31
+ const servers = config["mcpServers"];
32
+ if (!servers)
33
+ return false;
34
+ return ULUOPS_SERVERS.every((name) => name in servers);
35
+ }
36
+ }
37
+ class GeminiHooks {
38
+ static HOOK_TYPE = "AfterTool";
39
+ static MATCHER = "invoke_agent";
40
+ async install(settingsPath, hookCommand, dryRun) {
41
+ if (dryRun)
42
+ return true;
43
+ const settings = await readSettings(settingsPath);
44
+ const merged = mergeUluopsHook(settings, hookCommand, GeminiHooks.HOOK_TYPE, GeminiHooks.MATCHER);
45
+ await writeSettings(settingsPath, merged);
46
+ return true;
27
47
  }
28
- async write() {
29
- throw new HarnessNotTestedError("Gemini CLI");
48
+ async remove(settingsPath, dryRun) {
49
+ if (dryRun)
50
+ return;
51
+ const settings = await readSettings(settingsPath);
52
+ const cleaned = removeUluopsHook(settings, GeminiHooks.HOOK_TYPE);
53
+ await writeSettings(settingsPath, cleaned);
30
54
  }
31
- check() {
32
- return false;
55
+ async check(settingsPath) {
56
+ const settings = await readSettings(settingsPath);
57
+ return hasUluopsHook(settings, GeminiHooks.HOOK_TYPE);
33
58
  }
34
59
  }
35
60
  const home = join(homedir(), ".gemini");
36
61
  export const geminiCliProfile = {
37
62
  name: "gemini-cli",
38
63
  displayName: "Gemini CLI",
64
+ status: "stable",
39
65
  homeDir: home,
40
66
  agentFormat: "markdown",
41
67
  factoryTarget: "gemini-cli",
@@ -46,9 +72,9 @@ export const geminiCliProfile = {
46
72
  localMcpConfig: ".gemini/settings.json",
47
73
  agentsDir: join(home, "agents"),
48
74
  commandsDir: join(home, "commands"),
49
- settingsPath: null,
50
- toolsDir: null,
75
+ settingsPath: join(home, "settings.json"),
76
+ toolsDir: join(home, "tools", "agent-metrics"),
51
77
  },
52
78
  mcpConfig: new GeminiMcpConfig(),
53
- hooks: null,
79
+ hooks: new GeminiHooks(),
54
80
  };
@@ -12,7 +12,16 @@ export declare const ALL_PROFILES: readonly HarnessProfile[];
12
12
  export declare function resolveHarnessName(input: string): string;
13
13
  /** Get a harness profile by name or alias. Throws if not found. */
14
14
  export declare function getProfile(name: string): HarnessProfile;
15
- /** Detect which harnesses are installed by probing home directories. */
15
+ /**
16
+ * Detect which harnesses are installed by probing home directories.
17
+ *
18
+ * Excludes experimental profiles even when their home dir is present:
19
+ * auto-detection should never return a profile that will throw
20
+ * HarnessNotTestedError on use. Users can still opt in with
21
+ * `--harness <name>`, which goes through getProfile() and surfaces the
22
+ * explicit error message — the relational promise "in the detected
23
+ * list = safe to install" is preserved.
24
+ */
16
25
  export declare function detectHarnesses(): HarnessProfile[];
17
26
  /** List all available harness names (not aliases). */
18
27
  export declare function listHarnesses(): string[];
@@ -35,9 +35,18 @@ export function getProfile(name) {
35
35
  }
36
36
  return profile;
37
37
  }
38
- /** Detect which harnesses are installed by probing home directories. */
38
+ /**
39
+ * Detect which harnesses are installed by probing home directories.
40
+ *
41
+ * Excludes experimental profiles even when their home dir is present:
42
+ * auto-detection should never return a profile that will throw
43
+ * HarnessNotTestedError on use. Users can still opt in with
44
+ * `--harness <name>`, which goes through getProfile() and surfaces the
45
+ * explicit error message — the relational promise "in the detected
46
+ * list = safe to install" is preserved.
47
+ */
39
48
  export function detectHarnesses() {
40
- return ALL_PROFILES.filter((p) => existsSync(p.paths.home));
49
+ return ALL_PROFILES.filter((p) => p.status === "stable" && existsSync(p.paths.home));
41
50
  }
42
51
  /** List all available harness names (not aliases). */
43
52
  export function listHarnesses() {
@@ -10,5 +10,5 @@
10
10
  *
11
11
  * Verified working shape from ~/opencode.jsonc (2026-04-30).
12
12
  */
13
- import type { HarnessProfile } from "./types.js";
13
+ import { type HarnessProfile } from "./types.js";
14
14
  export declare const opencodeProfile: HarnessProfile;
@@ -12,11 +12,10 @@
12
12
  */
13
13
  import { readFile } from "node:fs/promises";
14
14
  import { homedir } from "node:os";
15
- import { join } from "node:path";
15
+ import { join, isAbsolute } from "node:path";
16
16
  import { parse as parseJsonc } from "jsonc-parser";
17
- import { ConfigParseError } from "./types.js";
17
+ import { ULUOPS_SERVERS, ConfigParseError } from "./types.js";
18
18
  import { atomicWrite } from "../lib/atomic-write.js";
19
- const ULUOPS_SERVERS = ["uluops-tracker", "uluops-registry"];
20
19
  class OpenCodeMcpConfig {
21
20
  /** Maps requested path → actual resolved path (for .jsonc fallback). */
22
21
  resolvedPaths = new Map();
@@ -54,8 +53,8 @@ class OpenCodeMcpConfig {
54
53
  enabled: true,
55
54
  timeout: 30000,
56
55
  environment: {
57
- ULUOPS_TRACKER_API_URL: "https://api.uluops.ai/api/v1",
58
- ULUOPS_TRACKER_API_KEY: apiKey,
56
+ ULUOPS_BASE_URL: "https://api.uluops.ai/api/v1",
57
+ ULUOPS_API_KEY: apiKey,
59
58
  },
60
59
  };
61
60
  const registry = {
@@ -107,11 +106,21 @@ class OpenCodeMcpConfig {
107
106
  return ULUOPS_SERVERS.every((name) => name in mcp);
108
107
  }
109
108
  }
110
- const xdgConfig = process.env["XDG_CONFIG_HOME"] ?? join(homedir(), ".config");
109
+ const xdgConfig = (() => {
110
+ const env = process.env["XDG_CONFIG_HOME"];
111
+ if (env) {
112
+ if (!isAbsolute(env) || env.includes("..")) {
113
+ throw new Error(`XDG_CONFIG_HOME must be an absolute path without traversal: ${env}`);
114
+ }
115
+ return env;
116
+ }
117
+ return join(homedir(), ".config");
118
+ })();
111
119
  const home = join(xdgConfig, "opencode");
112
120
  export const opencodeProfile = {
113
121
  name: "opencode",
114
122
  displayName: "OpenCode",
123
+ status: "stable",
115
124
  homeDir: home,
116
125
  agentFormat: "markdown",
117
126
  factoryTarget: "opencode",
@@ -5,12 +5,29 @@
5
5
  * Each harness (Claude Code, OpenCode, Codex, Gemini CLI) implements
6
6
  * these interfaces to encapsulate its config format, paths, and capabilities.
7
7
  */
8
+ /**
9
+ * Maturity status of a harness profile.
10
+ *
11
+ * - `"stable"` — implementation is tested end-to-end and safe to auto-detect.
12
+ * detectHarnesses() returns these.
13
+ * - `"experimental"` — scaffold only; throws HarnessNotTestedError on most
14
+ * operations. getProfile() still resolves these (so `--harness <name>`
15
+ * surfaces the explicit error), but detectHarnesses() filters them out so
16
+ * automatic install never picks an unusable profile.
17
+ */
18
+ export type HarnessStatus = "stable" | "experimental";
8
19
  /** Static definition of a harness — no runtime state. */
9
20
  export interface HarnessProfile {
10
21
  /** Canonical name (e.g., 'claude-code', 'opencode', 'codex', 'gemini-cli') */
11
22
  readonly name: string;
12
23
  /** Display name for CLI output (e.g., 'Claude Code', 'OpenCode') */
13
24
  readonly displayName: string;
25
+ /**
26
+ * Maturity status. Experimental profiles are excluded from auto-detection
27
+ * but still accept an explicit `--harness` flag so users can opt in to
28
+ * the not-yet-tested error surface.
29
+ */
30
+ readonly status: HarnessStatus;
14
31
  /** Home directory for this harness */
15
32
  readonly homeDir: string;
16
33
  /** Agent definition format */
@@ -76,6 +93,8 @@ export interface HookStrategy {
76
93
  /** Check if hook is currently installed. */
77
94
  check(settingsPath: string): Promise<boolean>;
78
95
  }
96
+ /** MCP server names installed by UluOps setup. Shared across all harnesses. */
97
+ export declare const ULUOPS_SERVERS: readonly ["uluops-tracker", "uluops-registry"];
79
98
  /** Thrown when a harness config file cannot be parsed. */
80
99
  export declare class ConfigParseError extends Error {
81
100
  readonly path: string;
@@ -5,6 +5,8 @@
5
5
  * Each harness (Claude Code, OpenCode, Codex, Gemini CLI) implements
6
6
  * these interfaces to encapsulate its config format, paths, and capabilities.
7
7
  */
8
+ /** MCP server names installed by UluOps setup. Shared across all harnesses. */
9
+ export const ULUOPS_SERVERS = ["uluops-tracker", "uluops-registry"];
8
10
  /** Thrown when a harness config file cannot be parsed. */
9
11
  export class ConfigParseError extends Error {
10
12
  path;
@@ -48,9 +48,9 @@ async function scanCommandDir(dir) {
48
48
  }
49
49
  /** Get all agent command entries from assets. */
50
50
  export async function getAgentCommands() {
51
- return scanCommandDir(join(ASSETS_DIR, "commands", "agents"));
51
+ return scanCommandDir(join(ASSETS_DIR, "claude-code", "commands", "agents"));
52
52
  }
53
53
  /** Get all workflow command entries from assets. */
54
54
  export async function getWorkflowCommands() {
55
- return scanCommandDir(join(ASSETS_DIR, "commands", "workflows"));
55
+ return scanCommandDir(join(ASSETS_DIR, "claude-code", "commands", "workflows"));
56
56
  }
@@ -2,6 +2,7 @@ interface McpServerConfig {
2
2
  command: string;
3
3
  args: string[];
4
4
  env: Record<string, string>;
5
+ trust?: boolean;
5
6
  }
6
7
  export interface ClaudeConfig {
7
8
  mcpServers?: Record<string, McpServerConfig>;
@@ -20,7 +21,7 @@ export declare function readConfig(path: string): Promise<ClaudeConfig>;
20
21
  /**
21
22
  * Merge UluOps MCP server entries into a config, preserving all other keys.
22
23
  */
23
- export declare function mergeUluopsMcp(config: ClaudeConfig, apiKey: string): ClaudeConfig;
24
+ export declare function mergeUluopsMcp(config: ClaudeConfig, apiKey: string, trust?: boolean): ClaudeConfig;
24
25
  /**
25
26
  * Remove UluOps MCP server entries from a config.
26
27
  */
@@ -36,13 +36,19 @@ export async function readConfig(path) {
36
36
  catch {
37
37
  return {}; // File doesn't exist — fresh config
38
38
  }
39
- return JSON.parse(raw);
39
+ try {
40
+ return JSON.parse(raw);
41
+ }
42
+ catch {
43
+ throw new Error(`Failed to parse config at ${path} — file contains invalid JSON`);
44
+ }
40
45
  }
41
46
  /**
42
47
  * Merge UluOps MCP server entries into a config, preserving all other keys.
43
48
  */
44
- export function mergeUluopsMcp(config, apiKey) {
49
+ export function mergeUluopsMcp(config, apiKey, trust = false) {
45
50
  const existing = config.mcpServers ?? {};
51
+ const trustField = trust ? { trust: true } : {};
46
52
  return {
47
53
  ...config,
48
54
  mcpServers: {
@@ -51,9 +57,10 @@ export function mergeUluopsMcp(config, apiKey) {
51
57
  command: "npx",
52
58
  args: ["-y", "uluops-tracker-mcp-client"],
53
59
  env: {
54
- ULUOPS_TRACKER_API_URL: "https://api.uluops.ai/api/v1",
55
- ULUOPS_TRACKER_API_KEY: apiKey,
60
+ ULUOPS_BASE_URL: "https://api.uluops.ai/api/v1",
61
+ ULUOPS_API_KEY: apiKey,
56
62
  },
63
+ ...trustField,
57
64
  },
58
65
  "uluops-registry": {
59
66
  command: "npx",
@@ -62,6 +69,7 @@ export function mergeUluopsMcp(config, apiKey) {
62
69
  ULUOPS_REGISTRY_URL: "https://api.uluops.ai/api/v1/registry",
63
70
  ULUOPS_API_KEY: apiKey,
64
71
  },
72
+ ...trustField,
65
73
  },
66
74
  },
67
75
  };
@@ -27,3 +27,8 @@ export declare function syncAssets(opts: {
27
27
  removed: number;
28
28
  files: string[];
29
29
  }>;
30
+ /**
31
+ * Back up a file to the UluOps backup directory before modifying it.
32
+ * No-op if the source file doesn't exist.
33
+ */
34
+ export declare function backupFile(srcPath: string, backupDir: string): Promise<void>;
@@ -1,5 +1,5 @@
1
- import { readFile, writeFile, mkdir, unlink } from "node:fs/promises";
2
- import { join } from "node:path";
1
+ import { readFile, writeFile, mkdir, unlink, access, copyFile, readdir } from "node:fs/promises";
2
+ import { join, basename } from "node:path";
3
3
  import { fileHash } from "./hash.js";
4
4
  /**
5
5
  * Copy a file if its content has changed (hash comparison). Returns "copied" or "skipped".
@@ -62,7 +62,6 @@ export async function unlinkFiles(dir, files) {
62
62
  * Returns list of copied files, skipped count, and removed count (for old manifest entries).
63
63
  */
64
64
  export async function syncAssets(opts) {
65
- const { readdir } = await import("node:fs/promises");
66
65
  if (!opts.dryRun) {
67
66
  await mkdir(opts.destDir, { recursive: true });
68
67
  }
@@ -105,3 +104,19 @@ export async function syncAssets(opts) {
105
104
  }
106
105
  return { copied, skipped, removed, files: assetFiles };
107
106
  }
107
+ /**
108
+ * Back up a file to the UluOps backup directory before modifying it.
109
+ * No-op if the source file doesn't exist.
110
+ */
111
+ export async function backupFile(srcPath, backupDir) {
112
+ try {
113
+ await access(srcPath);
114
+ }
115
+ catch {
116
+ return; // Nothing to back up
117
+ }
118
+ await mkdir(backupDir, { recursive: true });
119
+ const filename = basename(srcPath);
120
+ const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
121
+ await copyFile(srcPath, join(backupDir, `${filename}.${timestamp}.bak`));
122
+ }
@@ -1,2 +1,2 @@
1
- /** Return a truncated SHA-256 hash (12 hex chars) of the given string content. */
1
+ /** Return the full SHA-256 hash (64 hex chars) of the given string content. */
2
2
  export declare function fileHash(content: string): string;
package/dist/lib/hash.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { createHash } from "node:crypto";
2
- /** Return a truncated SHA-256 hash (12 hex chars) of the given string content. */
2
+ /** Return the full SHA-256 hash (64 hex chars) of the given string content. */
3
3
  export function fileHash(content) {
4
- return createHash("sha256").update(content).digest("hex").slice(0, 12);
4
+ return createHash("sha256").update(content).digest("hex");
5
5
  }
@@ -1,3 +1,16 @@
1
+ /**
2
+ * Identifier for a single harness installation in the manifest.
3
+ *
4
+ * Today this is the profile name (e.g. `"claude-code"`), assuming a
5
+ * one-to-one mapping between profiles and installations. The day that
6
+ * assumption breaks — multiple Claude Code installs (project vs global,
7
+ * NVM versions, parallel beta channels) — this becomes the seam where
8
+ * fracture surfaces. When that happens, the right migration is to extend
9
+ * keys to `{profile.name}@{stable-instance-id}` (e.g. a hash of toolsDir)
10
+ * rather than overwriting silently. The type alias exists so the
11
+ * assumption is documented in the schema, not the comments.
12
+ */
13
+ export type HarnessInstanceKey = string;
1
14
  /** Per-harness installation state. */
2
15
  export interface HarnessManifest {
3
16
  installedAt: string;
@@ -9,13 +22,29 @@ export interface HarnessManifest {
9
22
  agents: string[];
10
23
  commands: string[];
11
24
  hooksInstalled: boolean;
25
+ /**
26
+ * Version of @uluops/agent-metrics whose dist/ was copied into the harness tree.
27
+ * Null when hooks are not installed or when the manifest predates this field.
28
+ * Verify uses this to detect drift between installed and currently-resolvable versions —
29
+ * the shared version ledger across the setup↔agent-metrics seam.
30
+ */
31
+ hooksInstalledVersion?: string | null;
12
32
  }
13
33
  /** Top-level manifest with per-harness entries. */
14
34
  export interface Manifest {
15
35
  version: string;
16
36
  installedAt: string;
17
37
  shellModified: boolean;
18
- harnesses: Record<string, HarnessManifest>;
38
+ harnesses: Record<HarnessInstanceKey, HarnessManifest>;
39
+ /**
40
+ * Tracks whether `@uluops/cli` was installed globally by this setup run.
41
+ * Null/false when not installed by setup (user-installed or never installed).
42
+ * Uninstall only removes the global package when this is true — we don't
43
+ * remove what we didn't install.
44
+ */
45
+ cliInstalled?: boolean;
46
+ /** Version reported by `ulu --version` at install time, for drift detection. */
47
+ cliInstalledVersion?: string | null;
19
48
  contentHash?: string;
20
49
  }
21
50
  export interface ManifestValidationResult {
@@ -110,13 +110,11 @@ async function pathExists(p) {
110
110
  }
111
111
  }
112
112
  async function findMissingFiles(baseDir, subDir, files) {
113
- const missing = [];
114
- for (const file of files) {
115
- if (!(await pathExists(join(baseDir, subDir, file)))) {
116
- missing.push(file);
117
- }
118
- }
119
- return missing;
113
+ const results = await Promise.all(files.map(async (file) => ({
114
+ file,
115
+ exists: await pathExists(join(baseDir, subDir, file)),
116
+ })));
117
+ return results.filter((r) => !r.exists).map((r) => r.file);
120
118
  }
121
119
  async function readManifestFile(path) {
122
120
  try {
@@ -17,7 +17,22 @@ export declare function getUluopsDir(): string;
17
17
  export declare function getManifestPath(): string;
18
18
  /** Return the legacy manifest path for migration. */
19
19
  export declare function getLegacyManifestPath(): string;
20
- /** Return the backup directory for a harness's config files. */
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
+ */
21
36
  export declare function getBackupDir(harnessName: string): string;
22
37
  /** Detect the user's shell and return its name and profile path, or null if unsupported. */
23
38
  export declare function getShellProfile(): {
package/dist/lib/paths.js CHANGED
@@ -54,15 +54,28 @@ async function isProjectMarker(dir) {
54
54
  }
55
55
  return false;
56
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
+ }
57
67
  /** Return the Claude config home directory (~/.claude by default, or CLAUDE_HOME env override). */
58
68
  export function getClaudeHome() {
59
- return process.env["CLAUDE_HOME"] ?? join(homedir(), ".claude");
69
+ const envHome = process.env["CLAUDE_HOME"];
70
+ if (envHome)
71
+ return validateEnvPath(envHome, "CLAUDE_HOME");
72
+ return join(homedir(), ".claude");
60
73
  }
61
74
  /** Return the path to Claude's global config file (~/.claude.json by default, or CLAUDE_JSON_PATH env override). */
62
75
  export function getClaudeJsonPath() {
63
76
  const envPath = process.env["CLAUDE_JSON_PATH"];
64
77
  if (envPath)
65
- return envPath;
78
+ return validateEnvPath(envPath, "CLAUDE_JSON_PATH");
66
79
  return join(homedir(), ".claude.json");
67
80
  }
68
81
  /** Return the path to the project-local MCP config file (.mcp.json in project root). */
@@ -81,7 +94,22 @@ export function getManifestPath() {
81
94
  export function getLegacyManifestPath() {
82
95
  return join(getClaudeHome(), "uluops-manifest.json");
83
96
  }
84
- /** Return the backup directory for a harness's config files. */
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
+ */
85
113
  export function getBackupDir(harnessName) {
86
114
  return join(getUluopsDir(), "backups", harnessName);
87
115
  }
@@ -13,39 +13,54 @@ interface HookMatcher {
13
13
  matcher?: string;
14
14
  hooks: HookEntry[];
15
15
  }
16
- export 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";
21
36
  export interface HookProbeResult {
22
37
  hookType: string;
23
38
  supported: boolean;
24
39
  warning?: string;
25
40
  }
26
41
  /** Check whether the configured hook event type is in the known supported set. Returns the resolved hook type and a warning if unsupported. */
27
- export declare function probeHookSupport(): HookProbeResult;
42
+ export declare function probeHookSupport(hookTypeOverride?: string): HookProbeResult;
28
43
  /**
29
44
  * Read an existing settings.json, or return empty object if it doesn't exist.
30
45
  * Throws on malformed JSON to prevent silent data loss during merge+write.
31
46
  */
32
- export declare function readSettings(path: string): Promise<ClaudeSettings>;
47
+ export declare function readSettings(path: string): Promise<HarnessSettings>;
33
48
  /**
34
49
  * Write settings back to file with stable formatting.
35
50
  */
36
- export declare function writeSettings(path: string, settings: ClaudeSettings): Promise<void>;
51
+ export declare function writeSettings(path: string, settings: HarnessSettings): Promise<void>;
37
52
  /**
38
- * Merge the UluOps SubagentStop hook into settings, preserving all other
53
+ * Merge the UluOps hook into settings, preserving all other
39
54
  * hooks and settings. If a UluOps hook already exists, it is replaced.
40
55
  */
41
- export declare function mergeUluopsHook(settings: ClaudeSettings, hookCommand: string): ClaudeSettings;
56
+ export declare function mergeUluopsHook(settings: HarnessSettings, hookCommand: string, hookTypeOverride?: string, matcher?: string): HarnessSettings;
42
57
  /**
43
- * Remove UluOps hook entries from settings. If SubagentStop becomes empty,
58
+ * Remove UluOps hook entries from settings. If a hook type becomes empty,
44
59
  * the key is removed. If hooks becomes empty, the key is removed.
45
60
  */
46
- export declare function removeUluopsHook(settings: ClaudeSettings): ClaudeSettings;
61
+ export declare function removeUluopsHook(settings: HarnessSettings, hookTypeOverride?: string): HarnessSettings;
47
62
  /**
48
63
  * Check if a UluOps hook is configured in settings.
49
64
  */
50
- export declare function hasUluopsHook(settings: ClaudeSettings): boolean;
65
+ export declare function hasUluopsHook(settings: HarnessSettings, hookTypeOverride?: string): boolean;
51
66
  export {};