@uluops/setup 0.4.0 → 0.6.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 (213) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +75 -60
  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 +49 -416
  152. package/dist/commands/helpers.d.ts +73 -0
  153. package/dist/commands/helpers.js +311 -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 +17 -8
  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 +15 -7
  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/auth.d.ts +6 -0
  189. package/dist/steps/auth.js +19 -2
  190. package/dist/steps/cli.d.ts +53 -0
  191. package/dist/steps/cli.js +90 -0
  192. package/dist/steps/commands.d.ts +1 -1
  193. package/dist/steps/commands.js +20 -71
  194. package/dist/steps/detect.js +4 -0
  195. package/dist/steps/mcp.js +7 -15
  196. package/dist/steps/metrics.d.ts +12 -0
  197. package/dist/steps/metrics.js +52 -22
  198. package/dist/steps/shell.js +11 -1
  199. package/dist/steps/signup.d.ts +2 -2
  200. package/dist/steps/signup.js +9 -12
  201. package/dist/steps/verify.js +47 -8
  202. package/package.json +12 -11
  203. package/assets/agents/docs-validator-agent.md +0 -490
  204. package/assets/agents/release-readiness-agent.md +0 -482
  205. package/assets/commands/agents/aristotle-analyst.md +0 -116
  206. package/assets/commands/agents/aristotle-explorer.md +0 -93
  207. package/assets/commands/agents/aristotle-forecaster.md +0 -115
  208. package/assets/commands/agents/aristotle-validator.md +0 -115
  209. package/assets/commands/agents/prompt-validate.md +0 -136
  210. package/assets/commands/agents/workflow-synthesis.md +0 -102
  211. package/assets/commands/workflows/post-implementation.md +0 -577
  212. package/assets/commands/workflows/pre-implementation.md +0 -670
  213. /package/assets/{agents → claude-code/agents}/anxiety-reader-agent.md +0 -0
@@ -6,30 +6,57 @@
6
6
  */
7
7
  import { readFile } from "node:fs/promises";
8
8
  import { atomicWrite } from "./atomic-write.js";
9
- /** Marker embedded in hook commands to identify UluOps-managed entries */
10
- const ULUOPS_HOOK_MARKER = "tools/agent-metrics";
11
- /** Supported hook event types in Claude Code. Update when Claude Code adds/renames types. */
12
- const SUPPORTED_HOOK_TYPES = new Set([
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([
13
38
  "SubagentStop",
14
39
  "PreToolUse",
15
40
  "PostToolUse",
16
41
  "Notification",
17
42
  "Stop",
18
43
  ]);
44
+ /** Default Claude Code hook event used when no override is configured. */
45
+ export const DEFAULT_CLAUDE_HOOK_TYPE = "SubagentStop";
19
46
  /** Configurable hook type via env var. Falls back to SubagentStop. */
20
- function getHookEventType() {
21
- return process.env["ULUOPS_HOOK_TYPE"] ?? "SubagentStop";
47
+ function getDefaultHookEventType() {
48
+ return process.env["ULUOPS_HOOK_TYPE"] ?? DEFAULT_CLAUDE_HOOK_TYPE;
22
49
  }
23
50
  /** Check whether the configured hook event type is in the known supported set. Returns the resolved hook type and a warning if unsupported. */
24
- export function probeHookSupport() {
25
- const hookType = getHookEventType();
26
- if (SUPPORTED_HOOK_TYPES.has(hookType)) {
51
+ export function probeHookSupport(hookTypeOverride) {
52
+ const hookType = hookTypeOverride ?? getDefaultHookEventType();
53
+ if (CLAUDE_HOOK_TYPES.has(hookType) || hookType === "AfterTool") {
27
54
  return { hookType, supported: true };
28
55
  }
29
56
  return {
30
57
  hookType,
31
58
  supported: false,
32
- warning: `Hook type "${hookType}" is not in the known supported set {${[...SUPPORTED_HOOK_TYPES].join(", ")}}. Metrics may silently fail if this hook type does not exist in Claude Code.`,
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.`,
33
60
  };
34
61
  }
35
62
  /**
@@ -44,7 +71,12 @@ export async function readSettings(path) {
44
71
  catch {
45
72
  return {}; // File doesn't exist — fresh config
46
73
  }
47
- return JSON.parse(raw);
74
+ try {
75
+ return JSON.parse(raw);
76
+ }
77
+ catch {
78
+ throw new Error(`Failed to parse settings at ${path} — file contains invalid JSON`);
79
+ }
48
80
  }
49
81
  /**
50
82
  * Write settings back to file with stable formatting.
@@ -53,14 +85,14 @@ export async function writeSettings(path, settings) {
53
85
  await atomicWrite(path, JSON.stringify(settings, null, 2) + "\n");
54
86
  }
55
87
  /**
56
- * Merge the UluOps SubagentStop hook into settings, preserving all other
88
+ * Merge the UluOps hook into settings, preserving all other
57
89
  * hooks and settings. If a UluOps hook already exists, it is replaced.
58
90
  */
59
- export function mergeUluopsHook(settings, hookCommand) {
60
- const hookType = getHookEventType();
91
+ export function mergeUluopsHook(settings, hookCommand, hookTypeOverride, matcher) {
92
+ const hookType = hookTypeOverride ?? getDefaultHookEventType();
61
93
  const hooks = settings.hooks ?? {};
62
94
  const existing = hooks[hookType] ?? [];
63
- const filtered = existing.filter((m) => !m.hooks.some((h) => h.command.includes(ULUOPS_HOOK_MARKER)));
95
+ const filtered = existing.filter((m) => !m.hooks.some((h) => h.command.includes(HOOK_OWNERSHIP_SIGNATURE)));
64
96
  const uluopsHook = {
65
97
  hooks: [
66
98
  {
@@ -70,6 +102,9 @@ export function mergeUluopsHook(settings, hookCommand) {
70
102
  },
71
103
  ],
72
104
  };
105
+ if (matcher) {
106
+ uluopsHook.matcher = matcher;
107
+ }
73
108
  return {
74
109
  ...settings,
75
110
  hooks: {
@@ -79,18 +114,18 @@ export function mergeUluopsHook(settings, hookCommand) {
79
114
  };
80
115
  }
81
116
  /**
82
- * Remove UluOps hook entries from settings. If SubagentStop becomes empty,
117
+ * Remove UluOps hook entries from settings. If a hook type becomes empty,
83
118
  * the key is removed. If hooks becomes empty, the key is removed.
84
119
  */
85
- export function removeUluopsHook(settings) {
86
- const hookType = getHookEventType();
120
+ export function removeUluopsHook(settings, hookTypeOverride) {
121
+ const hookType = hookTypeOverride ?? getDefaultHookEventType();
87
122
  const hooks = settings.hooks;
88
123
  if (!hooks)
89
124
  return settings;
90
125
  const hookEntries = hooks[hookType];
91
126
  if (!hookEntries)
92
127
  return settings;
93
- const filtered = hookEntries.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)));
94
129
  const updatedHooks = { ...hooks };
95
130
  if (filtered.length === 0) {
96
131
  delete updatedHooks[hookType];
@@ -110,10 +145,10 @@ export function removeUluopsHook(settings) {
110
145
  /**
111
146
  * Check if a UluOps hook is configured in settings.
112
147
  */
113
- export function hasUluopsHook(settings) {
114
- const hookType = getHookEventType();
148
+ export function hasUluopsHook(settings, hookTypeOverride) {
149
+ const hookType = hookTypeOverride ?? getDefaultHookEventType();
115
150
  const hookEntries = settings.hooks?.[hookType];
116
151
  if (!hookEntries)
117
152
  return false;
118
- return hookEntries.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)));
119
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
+ }
@@ -5,8 +5,7 @@ export interface AgentsResult {
5
5
  removed: number;
6
6
  files: string[];
7
7
  }
8
- /** Copy agent definition files from assets to the harness agents directory,
9
- * transforming frontmatter to match the target harness format. */
8
+ /** Copy pre-rendered agent definitions from harness-specific assets to the target directory. */
10
9
  export declare function installAgents(profile: HarnessProfile, localDefs: boolean, dryRun: boolean, existingManifestAgents?: string[]): Promise<AgentsResult>;
11
10
  /** Remove previously installed agent files by name. */
12
11
  export declare function uninstallAgents(files: string[], defsPath: string): Promise<number>;
@@ -1,23 +1,20 @@
1
- import { readFile, readdir, mkdir, unlink } from "node:fs/promises";
1
+ import { readdir, mkdir, unlink } from "node:fs/promises";
2
2
  import { join } from "node:path";
3
3
  import { ASSETS_DIR, findProjectRoot } from "../lib/paths.js";
4
- import { copyIfChanged, writeIfChanged, unlinkFiles } from "../lib/file-ops.js";
5
- import { transformAgent } from "../lib/agent-transform.js";
6
- /** Source directory for agent assets (single set, Claude Code format). */
7
- const AGENTS_SRC = join(ASSETS_DIR, "agents");
8
- /** Copy agent definition files from assets to the harness agents directory,
9
- * transforming frontmatter to match the target harness format. */
4
+ import { copyIfChanged, unlinkFiles } from "../lib/file-ops.js";
5
+ /** Copy pre-rendered agent definitions from harness-specific assets to the target directory. */
10
6
  export async function installAgents(profile, localDefs, dryRun, existingManifestAgents) {
7
+ const srcDir = join(ASSETS_DIR, profile.name, "agents");
11
8
  const destDir = localDefs
12
9
  ? join(await findProjectRoot(), "uluops", "agents")
13
10
  : profile.paths.agentsDir;
14
11
  if (!dryRun) {
15
12
  await mkdir(destDir, { recursive: true });
16
13
  }
17
- const needsTransform = profile.name !== "claude-code";
14
+ const ext = profile.agentExtension;
18
15
  let files;
19
16
  try {
20
- files = (await readdir(AGENTS_SRC)).filter((f) => f.endsWith(".md"));
17
+ files = (await readdir(srcDir)).filter((f) => f.endsWith(ext));
21
18
  }
22
19
  catch {
23
20
  return { copied: 0, skipped: 0, removed: 0, files: [] };
@@ -25,15 +22,7 @@ export async function installAgents(profile, localDefs, dryRun, existingManifest
25
22
  let copied = 0;
26
23
  let skipped = 0;
27
24
  for (const file of files) {
28
- let result;
29
- if (needsTransform) {
30
- const markdown = await readFile(join(AGENTS_SRC, file), "utf-8");
31
- const transformed = transformAgent(markdown, profile.name);
32
- result = await writeIfChanged(join(destDir, file), transformed, dryRun);
33
- }
34
- else {
35
- result = await copyIfChanged(join(AGENTS_SRC, file), join(destDir, file), dryRun);
36
- }
25
+ const result = await copyIfChanged(join(srcDir, file), join(destDir, file), dryRun);
37
26
  if (result === "copied")
38
27
  copied++;
39
28
  else
@@ -2,6 +2,12 @@ export interface AuthResult {
2
2
  apiKey: string;
3
3
  email: string | null;
4
4
  }
5
+ /**
6
+ * Returns true if a credentials file exists at the default path.
7
+ * Used by the setup flow to skip the "new account?" prompt for returning users.
8
+ * Existence-only — does not validate the file's shape or contents.
9
+ */
10
+ export declare function hasCredentialsFile(): Promise<boolean>;
5
11
  /**
6
12
  * Resolve API key from flags, env, credentials file, or interactive prompt.
7
13
  */
@@ -1,9 +1,26 @@
1
- import { readFile } from "node:fs/promises";
1
+ import { readFile, access } from "node:fs/promises";
2
2
  import { join } from "node:path";
3
3
  import { homedir } from "node:os";
4
4
  function getKeyPrefix() {
5
5
  return process.env["ULUOPS_KEY_PREFIX"] ?? "ulr_";
6
6
  }
7
+ function credentialsPath() {
8
+ return join(homedir(), ".uluops", "credentials.json");
9
+ }
10
+ /**
11
+ * Returns true if a credentials file exists at the default path.
12
+ * Used by the setup flow to skip the "new account?" prompt for returning users.
13
+ * Existence-only — does not validate the file's shape or contents.
14
+ */
15
+ export async function hasCredentialsFile() {
16
+ try {
17
+ await access(credentialsPath());
18
+ return true;
19
+ }
20
+ catch {
21
+ return false;
22
+ }
23
+ }
7
24
  /**
8
25
  * Resolve API key from flags, env, credentials file, or interactive prompt.
9
26
  */
@@ -48,7 +65,7 @@ export async function resolveApiKey(options) {
48
65
  return { apiKey, email: null };
49
66
  }
50
67
  async function readCredentialsFile() {
51
- const credsPath = join(homedir(), ".uluops", "credentials.json");
68
+ const credsPath = credentialsPath();
52
69
  let raw;
53
70
  try {
54
71
  raw = await readFile(credsPath, "utf-8");
@@ -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>;
@@ -0,0 +1,90 @@
1
+ import { spawnSync } from "node:child_process";
2
+ export const CLI_PACKAGE = "@uluops/cli";
3
+ export const CLI_BIN = "ulu";
4
+ /** Default executor — shells out to `ulu` and `npm`. */
5
+ export const defaultExecutor = {
6
+ detect: () => {
7
+ const r = spawnSync(CLI_BIN, ["--version"], {
8
+ encoding: "utf-8",
9
+ stdio: ["ignore", "pipe", "ignore"],
10
+ });
11
+ if (r.status !== 0 || !r.stdout)
12
+ return null;
13
+ return r.stdout.trim() || null;
14
+ },
15
+ install: () => {
16
+ const r = spawnSync("npm", ["install", "-g", CLI_PACKAGE], {
17
+ encoding: "utf-8",
18
+ stdio: ["ignore", "pipe", "pipe"],
19
+ });
20
+ if (r.status === 0)
21
+ return { ok: true };
22
+ const stderr = (r.stderr ?? "").toString().trim();
23
+ const stdout = (r.stdout ?? "").toString().trim();
24
+ return { ok: false, error: stderr || stdout || `exit ${r.status}` };
25
+ },
26
+ uninstall: () => {
27
+ const r = spawnSync("npm", ["uninstall", "-g", CLI_PACKAGE], {
28
+ encoding: "utf-8",
29
+ stdio: ["ignore", "pipe", "pipe"],
30
+ });
31
+ if (r.status === 0)
32
+ return { ok: true };
33
+ const stderr = (r.stderr ?? "").toString().trim();
34
+ const stdout = (r.stdout ?? "").toString().trim();
35
+ return { ok: false, error: stderr || stdout || `exit ${r.status}` };
36
+ },
37
+ };
38
+ /**
39
+ * Install `@uluops/cli` globally if not already present.
40
+ *
41
+ * Designed to never abort the parent setup flow:
42
+ * - If `ulu` is already on PATH, returns `{ installed: true, alreadyPresent: true }` without touching npm.
43
+ * - If `npm install -g` fails (permissions, network, nvm prefix surprise), returns
44
+ * `{ installed: false, error }` so the caller can warn-and-continue.
45
+ * - In dryRun mode, no executor calls happen.
46
+ */
47
+ export async function installCli(opts) {
48
+ const executor = opts.executor ?? defaultExecutor;
49
+ const existing = executor.detect();
50
+ if (existing !== null) {
51
+ return { installed: true, version: existing, alreadyPresent: true };
52
+ }
53
+ if (opts.dryRun) {
54
+ return { installed: false, version: null, alreadyPresent: false };
55
+ }
56
+ const res = executor.install();
57
+ if (!res.ok) {
58
+ return {
59
+ installed: false,
60
+ version: null,
61
+ alreadyPresent: false,
62
+ error: res.error,
63
+ };
64
+ }
65
+ const after = executor.detect();
66
+ return {
67
+ installed: after !== null,
68
+ version: after,
69
+ alreadyPresent: false,
70
+ };
71
+ }
72
+ /**
73
+ * Uninstall `@uluops/cli` globally. Best-effort: if the package isn't there,
74
+ * npm exits non-zero on some platforms — we treat that as success.
75
+ */
76
+ export async function uninstallCli(opts) {
77
+ const executor = opts.executor ?? defaultExecutor;
78
+ if (opts.dryRun)
79
+ return { removed: true };
80
+ const before = executor.detect();
81
+ if (before === null)
82
+ return { removed: true };
83
+ const res = executor.uninstall();
84
+ if (res.ok)
85
+ return { removed: true };
86
+ const after = executor.detect();
87
+ if (after === null)
88
+ return { removed: true };
89
+ return { removed: false, error: res.error };
90
+ }
@@ -8,7 +8,7 @@ export interface CommandsResult {
8
8
  files: string[];
9
9
  skippedReason?: string;
10
10
  }
11
- /** Install slash-command files, transforming to target format as needed. */
11
+ /** Install pre-rendered command files from harness-specific assets. */
12
12
  export declare function installCommands(profile: HarnessProfile, localDefs: boolean, dryRun: boolean, existingManifestCommands?: string[]): Promise<CommandsResult>;
13
13
  /** Remove previously installed command files by relative path. Returns count of successfully removed files. */
14
14
  export declare function uninstallCommands(files: string[], defsPath: string): Promise<number>;
@@ -1,57 +1,24 @@
1
- import { readFile, readdir, mkdir, unlink } from "node:fs/promises";
1
+ import { readdir, mkdir, unlink } from "node:fs/promises";
2
2
  import { join } from "node:path";
3
3
  import { ASSETS_DIR, findProjectRoot } from "../lib/paths.js";
4
- import { copyIfChanged, writeIfChanged } from "../lib/file-ops.js";
4
+ import { copyIfChanged, unlinkFiles } from "../lib/file-ops.js";
5
5
  const SUBDIRS = ["agents", "workflows", "pipelines"];
6
- /** Harnesses that support command installation. */
7
- const SUPPORTED_HARNESSES = new Set(["claude-code", "gemini-cli"]);
8
- // --- Gemini CLI transform ---
9
- /**
10
- * Strip YAML frontmatter from rendered markdown, returning just the body.
11
- */
12
- function stripFrontmatter(markdown) {
13
- const first = markdown.indexOf("---");
14
- if (first === -1)
15
- return markdown;
16
- const second = markdown.indexOf("---", first + 3);
17
- if (second === -1)
18
- return markdown;
19
- return markdown.substring(second + 3);
20
- }
21
- /**
22
- * Escape a string for use in a TOML basic string (double-quoted).
23
- */
24
- function escapeToml(value) {
25
- return value
26
- .replace(/\\/g, "\\\\")
27
- .replace(/"/g, '\\"')
28
- .replace(/\n/g, "\\n")
29
- .replace(/\r/g, "\\r")
30
- .replace(/\t/g, "\\t");
31
- }
32
- /**
33
- * Transform a Claude Code markdown command to a Gemini CLI TOML command.
34
- * Strips frontmatter, substitutes $ARGUMENTS → {{args}}, wraps in TOML.
35
- */
36
- function transformToGeminiToml(markdown, description) {
37
- const body = stripFrontmatter(markdown)
38
- .replace(/\$ARGUMENTS/g, "{{args}}")
39
- .trim();
40
- // Escape """ in body for TOML multi-line strings
41
- const escaped = body.replace(/"""/g, '""\\"');
42
- return `description = "${escapeToml(description)}"\nprompt = """\n${escaped}\n"""\n`;
43
- }
44
- /**
45
- * Extract the description from a markdown command's YAML frontmatter.
46
- */
47
- function extractDescription(markdown) {
48
- const match = markdown.match(/^description:\s*(.+)$/m);
49
- return match?.[1]?.trim() ?? "";
50
- }
51
- // --- Install ---
52
- /** Install slash-command files, transforming to target format as needed. */
6
+ /** Install pre-rendered command files from harness-specific assets. */
53
7
  export async function installCommands(profile, localDefs, dryRun, existingManifestCommands) {
54
- if (!SUPPORTED_HARNESSES.has(profile.name)) {
8
+ const srcBase = join(ASSETS_DIR, profile.name, "commands");
9
+ const destBase = localDefs
10
+ ? join(await findProjectRoot(), "uluops", "commands")
11
+ : profile.paths.commandsDir;
12
+ // If no commands directory exists for this harness, skip gracefully
13
+ let hasSrcDir;
14
+ try {
15
+ await readdir(srcBase);
16
+ hasSrcDir = true;
17
+ }
18
+ catch {
19
+ hasSrcDir = false;
20
+ }
21
+ if (!hasSrcDir) {
55
22
  return {
56
23
  agentCommands: 0,
57
24
  workflowCommands: 0,
@@ -62,11 +29,6 @@ export async function installCommands(profile, localDefs, dryRun, existingManife
62
29
  skippedReason: "not-supported",
63
30
  };
64
31
  }
65
- const srcBase = join(ASSETS_DIR, "commands");
66
- const destBase = localDefs
67
- ? join(await findProjectRoot(), "uluops", "commands")
68
- : profile.paths.commandsDir;
69
- const needsTransform = profile.name === "gemini-cli";
70
32
  let agentCommands = 0;
71
33
  let workflowCommands = 0;
72
34
  let pipelineCommands = 0;
@@ -80,26 +42,14 @@ export async function installCommands(profile, localDefs, dryRun, existingManife
80
42
  }
81
43
  let files;
82
44
  try {
83
- files = (await readdir(srcDir)).filter((f) => f.endsWith(".md"));
45
+ files = (await readdir(srcDir)).filter((f) => f.endsWith(".md") || f.endsWith(".toml"));
84
46
  }
85
47
  catch {
86
48
  continue;
87
49
  }
88
50
  for (const file of files) {
89
- const destFile = needsTransform
90
- ? file.replace(/\.md$/, ".toml")
91
- : file;
92
- const relativePath = `${subdir}/${destFile}`;
93
- let result;
94
- if (needsTransform) {
95
- const markdown = await readFile(join(srcDir, file), "utf-8");
96
- const description = extractDescription(markdown);
97
- const toml = transformToGeminiToml(markdown, description);
98
- result = await writeIfChanged(join(destDir, destFile), toml, dryRun);
99
- }
100
- else {
101
- result = await copyIfChanged(join(srcDir, file), join(destDir, destFile), dryRun);
102
- }
51
+ const relativePath = `${subdir}/${file}`;
52
+ const result = await copyIfChanged(join(srcDir, file), join(destDir, file), dryRun);
103
53
  if (result === "copied") {
104
54
  if (subdir === "agents")
105
55
  agentCommands++;
@@ -142,6 +92,5 @@ export async function installCommands(profile, localDefs, dryRun, existingManife
142
92
  }
143
93
  /** Remove previously installed command files by relative path. Returns count of successfully removed files. */
144
94
  export async function uninstallCommands(files, defsPath) {
145
- const { unlinkFiles } = await import("../lib/file-ops.js");
146
95
  return unlinkFiles(join(defsPath, "commands"), files);
147
96
  }
@@ -16,6 +16,10 @@ export async function detect() {
16
16
  const isWsl = os === "linux" && release().toLowerCase().includes("microsoft");
17
17
  const profile = getShellProfile();
18
18
  const nodeVersion = process.version;
19
+ const majorVersion = parseInt(nodeVersion.slice(1).split(".")[0] ?? "0", 10);
20
+ if (majorVersion < 20) {
21
+ throw new Error(`Unsupported Node.js version: ${nodeVersion}. @uluops/setup requires Node.js 20 or higher.`);
22
+ }
19
23
  let claudeHomeExists = false;
20
24
  try {
21
25
  await access(getClaudeHome());
package/dist/steps/mcp.js CHANGED
@@ -1,8 +1,9 @@
1
- import { readFile, access, mkdir, copyFile } from "node:fs/promises";
2
- import { join, basename } from "node:path";
1
+ import { readFile, access } from "node:fs/promises";
2
+ import { join } from "node:path";
3
3
  import { checkMcpPackageAvailability } from "../lib/config-merger.js";
4
4
  import { findProjectRoot, getBackupDir } from "../lib/paths.js";
5
5
  import { atomicWrite } from "../lib/atomic-write.js";
6
+ import { backupFile } from "../lib/file-ops.js";
6
7
  /** Write UluOps MCP server entries into a harness's config file. */
7
8
  export async function installMcp(profile, apiKey, scope, dryRun) {
8
9
  const configPath = scope === "global"
@@ -33,22 +34,13 @@ export async function uninstallMcp(profile, configPath) {
33
34
  await profile.mcpConfig.write(configPath, cleaned);
34
35
  }
35
36
  async function backupConfig(harnessName, configPath) {
36
- try {
37
- await access(configPath);
38
- }
39
- catch {
40
- return; // Nothing to back up
41
- }
42
- const backupDir = getBackupDir(harnessName);
43
- await mkdir(backupDir, { recursive: true });
44
- const filename = basename(configPath);
45
- const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
46
- await copyFile(configPath, join(backupDir, `${filename}.${timestamp}.bak`));
37
+ await backupFile(configPath, getBackupDir(harnessName));
47
38
  }
48
39
  async function addToGitignore(localConfigFilename) {
49
- const gitignorePath = join(process.cwd(), ".gitignore");
40
+ const root = await findProjectRoot();
41
+ const gitignorePath = join(root, ".gitignore");
50
42
  try {
51
- await access(join(process.cwd(), ".git"));
43
+ await access(join(root, ".git"));
52
44
  }
53
45
  catch {
54
46
  return;
@@ -13,8 +13,20 @@ export declare function getHookCommand(profile: HarnessProfile): string;
13
13
  export interface MetricsResult {
14
14
  toolFilesCopied: number;
15
15
  hookConfigured: boolean;
16
+ /**
17
+ * Version of @uluops/agent-metrics whose dist was installed into the
18
+ * harness tree. Null when the metrics step was skipped or the source
19
+ * package.json could not be read.
20
+ */
21
+ hooksInstalledVersion: string | null;
16
22
  skippedReason?: string;
17
23
  }
24
+ /**
25
+ * Read the installed agent-metrics version from the harness tree.
26
+ * Returns null when the file is missing or unparseable.
27
+ * Exported so verify.ts can detect drift between installed and source.
28
+ */
29
+ export declare function readInstalledMetricsVersion(toolDir: string): Promise<string | null>;
18
30
  /**
19
31
  * Install agent-metrics: copy tool files and configure hook.
20
32
  * Skips entirely if the harness doesn't support hooks.