@uluops/setup 0.2.0 → 0.4.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 (107) hide show
  1. package/README.md +56 -53
  2. package/assets/agents/anxiety-reader-agent.md +464 -0
  3. package/assets/commands/agents/anxiety-reader.md +160 -0
  4. package/assets/commands/agents/api-contract.md +1 -0
  5. package/assets/commands/agents/architect.md +1 -0
  6. package/assets/commands/agents/aristotle-analyst.md +1 -0
  7. package/assets/commands/agents/aristotle-explorer.md +1 -0
  8. package/assets/commands/agents/aristotle-forecaster.md +1 -0
  9. package/assets/commands/agents/aristotle-validator.md +1 -0
  10. package/assets/commands/agents/assumption-excavator.md +1 -0
  11. package/assets/commands/agents/audit.md +1 -0
  12. package/assets/commands/agents/{validate.md → code-validate.md} +6 -5
  13. package/assets/commands/agents/docs-validate.md +1 -0
  14. package/assets/commands/agents/frontend.md +1 -0
  15. package/assets/commands/agents/mcp-validate.md +1 -0
  16. package/assets/commands/agents/optimize.md +1 -0
  17. package/assets/commands/agents/pattern-analyzer.md +1 -0
  18. package/assets/commands/agents/prompt-quality.md +1 -0
  19. package/assets/commands/agents/prompt-validate.md +1 -0
  20. package/assets/commands/agents/public-interface.md +1 -0
  21. package/assets/commands/agents/release.md +1 -0
  22. package/assets/commands/agents/security.md +1 -0
  23. package/assets/commands/agents/test-review.md +1 -0
  24. package/assets/commands/agents/type-safety.md +1 -0
  25. package/assets/commands/agents/workflow-synthesis.md +1 -0
  26. package/assets/commands/pipelines/aristotle.md +143 -0
  27. package/assets/commands/pipelines/ship.md +188 -0
  28. package/assets/commands/workflows/prompt-audit.md +37 -747
  29. package/dist/cli.js +251 -207
  30. package/dist/harnesses/claude-code.d.ts +8 -0
  31. package/dist/harnesses/claude-code.js +72 -0
  32. package/dist/harnesses/codex.d.ts +15 -0
  33. package/dist/harnesses/codex.js +53 -0
  34. package/dist/harnesses/gemini-cli.d.ts +16 -0
  35. package/dist/harnesses/gemini-cli.js +54 -0
  36. package/dist/harnesses/index.d.ts +18 -0
  37. package/dist/harnesses/index.js +45 -0
  38. package/dist/harnesses/opencode.d.ts +14 -0
  39. package/dist/harnesses/opencode.js +130 -0
  40. package/dist/harnesses/types.d.ts +87 -0
  41. package/dist/harnesses/types.js +24 -0
  42. package/dist/lib/agent-transform.d.ts +12 -0
  43. package/dist/lib/agent-transform.js +129 -0
  44. package/dist/lib/asset-catalog.d.ts +9 -0
  45. package/dist/lib/asset-catalog.js +56 -0
  46. package/dist/lib/atomic-write.d.ts +11 -0
  47. package/dist/lib/atomic-write.js +28 -0
  48. package/dist/lib/config-merger.d.ts +7 -1
  49. package/dist/lib/config-merger.js +34 -5
  50. package/dist/lib/display.d.ts +14 -0
  51. package/dist/lib/display.js +66 -0
  52. package/dist/lib/file-ops.d.ts +6 -0
  53. package/dist/lib/file-ops.js +22 -1
  54. package/dist/lib/hash.d.ts +1 -0
  55. package/dist/lib/hash.js +1 -0
  56. package/dist/lib/health.d.ts +2 -0
  57. package/dist/lib/health.js +10 -0
  58. package/dist/lib/manifest.d.ts +22 -5
  59. package/dist/lib/manifest.js +148 -13
  60. package/dist/lib/paths.d.ts +15 -3
  61. package/dist/lib/paths.js +71 -13
  62. package/dist/lib/settings-merger.d.ts +9 -1
  63. package/dist/lib/settings-merger.js +45 -17
  64. package/dist/steps/agents.d.ts +5 -1
  65. package/dist/steps/agents.js +59 -9
  66. package/dist/steps/auth.js +26 -10
  67. package/dist/steps/commands.d.ts +6 -1
  68. package/dist/steps/commands.js +87 -9
  69. package/dist/steps/detect.d.ts +3 -0
  70. package/dist/steps/detect.js +7 -0
  71. package/dist/steps/mcp.d.ts +6 -2
  72. package/dist/steps/mcp.js +46 -21
  73. package/dist/steps/metrics.d.ts +14 -10
  74. package/dist/steps/metrics.js +59 -89
  75. package/dist/steps/shell.d.ts +2 -0
  76. package/dist/steps/shell.js +16 -9
  77. package/dist/steps/signup.d.ts +6 -3
  78. package/dist/steps/signup.js +26 -14
  79. package/dist/steps/verify.d.ts +2 -2
  80. package/dist/steps/verify.js +84 -117
  81. package/package.json +32 -7
  82. package/assets/commands/workflows/aristotle.md +0 -543
  83. package/assets/commands/workflows/ship.md +0 -721
  84. package/dist/test/auth.test.d.ts +0 -1
  85. package/dist/test/auth.test.js +0 -43
  86. package/dist/test/config-io.test.d.ts +0 -1
  87. package/dist/test/config-io.test.js +0 -56
  88. package/dist/test/config-merger.test.d.ts +0 -1
  89. package/dist/test/config-merger.test.js +0 -94
  90. package/dist/test/detect.test.d.ts +0 -1
  91. package/dist/test/detect.test.js +0 -25
  92. package/dist/test/file-ops.test.d.ts +0 -1
  93. package/dist/test/file-ops.test.js +0 -100
  94. package/dist/test/hash.test.d.ts +0 -1
  95. package/dist/test/hash.test.js +0 -14
  96. package/dist/test/manifest.test.d.ts +0 -1
  97. package/dist/test/manifest.test.js +0 -78
  98. package/dist/test/paths.test.d.ts +0 -1
  99. package/dist/test/paths.test.js +0 -30
  100. package/dist/test/settings-merger.test.d.ts +0 -1
  101. package/dist/test/settings-merger.test.js +0 -167
  102. package/dist/test/shell-profile.test.d.ts +0 -1
  103. package/dist/test/shell-profile.test.js +0 -40
  104. package/dist/test/shell.test.d.ts +0 -1
  105. package/dist/test/shell.test.js +0 -71
  106. package/dist/test/signup.test.d.ts +0 -1
  107. package/dist/test/signup.test.js +0 -83
package/dist/cli.js CHANGED
@@ -13,41 +13,99 @@ import { installCommands, uninstallCommands } from "./steps/commands.js";
13
13
  import { writeShellExport, removeShellExport } from "./steps/shell.js";
14
14
  import { verify } from "./steps/verify.js";
15
15
  import { installMetrics, uninstallMetrics } from "./steps/metrics.js";
16
- import { loadManifest, saveManifest, deleteManifest, } from "./lib/manifest.js";
17
- import { getClaudeHome, getAgentsDir, ASSETS_DIR } from "./lib/paths.js";
16
+ import { probeHookSupport } from "./lib/settings-merger.js";
17
+ import { loadManifest, saveManifest, deleteManifest, validateManifest, } from "./lib/manifest.js";
18
+ import { ASSETS_DIR, findProjectRoot } from "./lib/paths.js";
19
+ import { getHealthTimeout } from "./lib/health.js";
20
+ import { ok, warn, fail, info, printSetupSummary, printAgentList } from "./lib/display.js";
21
+ import { getProfile, resolveHarnessName, listHarnesses, HarnessNotTestedError, } from "./harnesses/index.js";
18
22
  const __dirname = dirname(fileURLToPath(import.meta.url));
19
23
  async function getVersion() {
20
24
  const pkgPath = join(__dirname, "..", "package.json");
21
25
  const pkg = JSON.parse(await readFile(pkgPath, "utf-8"));
22
26
  return pkg.version;
23
27
  }
24
- const ok = (msg) => console.log(` ${chalk.green("✓")} ${msg}`);
25
- const warn = (msg) => console.log(` ${chalk.yellow("⚠")} ${msg}`);
26
- const fail = (msg) => console.log(` ${chalk.red("✗")} ${msg}`);
27
- const info = (msg) => console.log(` ${msg}`);
28
28
  async function runSetup(opts) {
29
29
  const version = await getVersion();
30
+ const profile = getProfile(opts.harness);
30
31
  console.log();
31
32
  console.log(` ${chalk.dim("⟨u⟩")} ${chalk.cyan.bold("ulu")}${chalk.bold("·ops")}`);
32
33
  console.log(` ${chalk.dim("operating intelligence as infrastructure")}`);
33
34
  console.log();
34
- console.log(` Setup v${version}`);
35
+ console.log(` Setup v${version} — ${chalk.bold(profile.displayName)}`);
35
36
  console.log();
36
37
  if (opts.dryRun) {
37
38
  info(chalk.dim("(dry run — no changes will be made)\n"));
38
39
  }
39
- // Detect environment
40
+ const { env, apiKey } = await initContext(opts);
41
+ console.log();
42
+ // Load existing manifest for update detection
43
+ const existingManifest = await loadManifest();
44
+ const existingHarness = existingManifest?.harnesses[profile.name];
45
+ if (existingManifest && existingManifest.version !== version) {
46
+ info(`Updating ${chalk.dim(existingManifest.version)} → ${chalk.green(version)}`);
47
+ console.log();
48
+ }
49
+ else if (existingHarness) {
50
+ info(chalk.dim(`Already at v${version} — checking for changes`));
51
+ console.log();
52
+ }
53
+ // Check for conflicts on first install for this harness
54
+ if (!existingHarness && !opts.yes && !opts.dryRun) {
55
+ await checkConflicts(profile, opts.localDefs);
56
+ }
57
+ const mcpResult = await configureMcpStep(profile, apiKey, opts);
58
+ const agentsResult = await installAgentsDefs(profile, opts, existingHarness?.agents);
59
+ const commandsResult = await installCommandsDefs(profile, opts, existingHarness?.commands);
60
+ const metricsResult = await configureMetricsStep(profile, opts);
61
+ await runHealthCheck(opts);
62
+ const shellModified = await configureShell(env, apiKey, opts);
63
+ // Save manifest
64
+ if (!opts.dryRun) {
65
+ const now = new Date().toISOString();
66
+ const harnessEntry = {
67
+ installedAt: now,
68
+ setupVersion: version,
69
+ mcpScope: opts.scope,
70
+ mcpConfigPath: mcpResult.configPath,
71
+ defsScope: opts.localDefs ? "local" : "global",
72
+ defsPath: opts.localDefs
73
+ ? join(await findProjectRoot(), "uluops")
74
+ : profile.paths.home,
75
+ agents: agentsResult.files,
76
+ commands: commandsResult.files,
77
+ hooksInstalled: metricsResult.hookConfigured,
78
+ };
79
+ const manifest = existingManifest ?? {
80
+ version,
81
+ installedAt: now,
82
+ shellModified: false,
83
+ harnesses: {},
84
+ };
85
+ manifest.version = version;
86
+ manifest.installedAt = now;
87
+ manifest.shellModified = shellModified || manifest.shellModified;
88
+ manifest.harnesses[profile.name] = harnessEntry;
89
+ await saveManifest(manifest);
90
+ }
91
+ await printSetupSummary({
92
+ profile,
93
+ agentCount: agentsResult.files.length,
94
+ commandCount: commandsResult.files.length,
95
+ apiKey,
96
+ });
97
+ }
98
+ // --- extracted helpers ---
99
+ /** Resolve API key via flag, env, file, signup, or interactive prompt. Returns env detection + key. */
100
+ async function initContext(opts) {
40
101
  const env = await detect();
41
- // Resolve API key — via signup or existing key
42
102
  let apiKey;
43
- let email = null;
44
103
  try {
45
104
  if (opts.signup) {
46
105
  info("Create your UluOps account\n");
47
106
  const auth = await signup();
48
107
  apiKey = auth.apiKey;
49
- email = auth.email;
50
- ok(`Account created (${email})`);
108
+ ok(`Account created (${auth.email})`);
51
109
  ok(`API key generated`);
52
110
  }
53
111
  else {
@@ -57,193 +115,137 @@ async function runSetup(opts) {
57
115
  interactive: !opts.yes && !opts.apiKey && !process.env["ULUOPS_API_KEY"],
58
116
  });
59
117
  apiKey = auth.apiKey;
60
- email = auth.email;
61
- if (email) {
62
- ok(`Key validated (${email})`);
63
- }
64
- else if (opts.skipValidation) {
118
+ if (auth.email)
119
+ ok(`Key validated (${auth.email})`);
120
+ else if (opts.skipValidation)
65
121
  ok("Key accepted (validation skipped)");
66
- }
67
- else {
122
+ else
68
123
  ok("Key validated");
69
- }
70
124
  }
71
125
  }
72
126
  catch (err) {
73
127
  fail(err instanceof Error ? err.message : String(err));
74
128
  process.exit(1);
75
129
  }
76
- console.log();
77
- // Load existing manifest for update detection
78
- const existingManifest = await loadManifest();
79
- // Show update info if re-running with newer version
80
- if (existingManifest && existingManifest.version !== version) {
81
- info(`Updating ${chalk.dim(existingManifest.version)} → ${chalk.green(version)}`);
82
- console.log();
83
- }
84
- else if (existingManifest) {
85
- info(chalk.dim(`Already at v${version} — checking for changes`));
86
- console.log();
87
- }
88
- // Check for conflicts on first install
89
- if (!existingManifest && !opts.yes && !opts.dryRun) {
90
- await checkConflicts(opts.localDefs);
91
- }
92
- // MCP config
93
- const mcpResult = await installMcp(apiKey, opts.scope, opts.dryRun);
94
- ok(`MCP config → ${mcpResult.configPath} (2 servers)`);
95
- // Agents
96
- const agentsResult = await installAgents(opts.localDefs, opts.dryRun, existingManifest?.agents);
97
- const agentParts = [];
98
- if (agentsResult.copied > 0)
99
- agentParts.push(`${agentsResult.copied} copied`);
100
- if (agentsResult.skipped > 0)
101
- agentParts.push(`${agentsResult.skipped} unchanged`);
102
- if (agentsResult.removed > 0)
103
- agentParts.push(`${agentsResult.removed} removed`);
104
- ok(`${agentsResult.files.length} agents ${opts.localDefs ? "./uluops/agents/" : "~/.claude/agents/"}${agentParts.length ? ` (${agentParts.join(", ")})` : ""}`);
105
- // Commands
106
- const commandsResult = await installCommands(opts.localDefs, opts.dryRun, existingManifest?.commands);
107
- const totalCommands = commandsResult.agentCommands + commandsResult.workflowCommands;
108
- const cmdSkipped = commandsResult.skipped;
109
- const cmdTotal = commandsResult.files.length;
110
- const cmdParts = [];
111
- if (totalCommands > 0)
112
- cmdParts.push(`${totalCommands} copied`);
113
- if (cmdSkipped > 0)
114
- cmdParts.push(`${cmdSkipped} unchanged`);
115
- if (commandsResult.removed > 0)
116
- cmdParts.push(`${commandsResult.removed} removed`);
117
- ok(`${cmdTotal} commands ${opts.localDefs ? "./uluops/commands/" : "~/.claude/commands/"}${cmdParts.length ? ` (${cmdParts.join(", ")})` : ""}`);
118
- // Agent metrics (SubagentStop hook for auto-capture)
119
- const metricsResult = await installMetrics(opts.dryRun);
120
- if (metricsResult.hookConfigured) {
130
+ return { env, apiKey };
131
+ }
132
+ /** Write MCP server entries to harness config and report warnings. */
133
+ async function configureMcpStep(profile, apiKey, opts) {
134
+ const res = await installMcp(profile, apiKey, opts.scope, opts.dryRun);
135
+ ok(`MCP config → ${res.configPath} (2 servers)`);
136
+ for (const w of res.packageWarnings)
137
+ warn(w);
138
+ return res;
139
+ }
140
+ /** Copy agent definitions from assets to harness directory. */
141
+ async function installAgentsDefs(profile, opts, prev) {
142
+ const res = await installAgents(profile, opts.localDefs, opts.dryRun, prev);
143
+ const parts = [];
144
+ if (res.copied > 0)
145
+ parts.push(`${res.copied} copied`);
146
+ if (res.skipped > 0)
147
+ parts.push(`${res.skipped} unchanged`);
148
+ if (res.removed > 0)
149
+ parts.push(`${res.removed} removed`);
150
+ const dest = opts.localDefs
151
+ ? "./uluops/agents/"
152
+ : `${profile.paths.agentsDir.replace(process.env["HOME"] ?? "", "~")}/`;
153
+ ok(`${res.files.length} agents → ${dest}${parts.length ? ` (${parts.join(", ")})` : ""}`);
154
+ return res;
155
+ }
156
+ /** Copy slash-command definitions from assets (Claude Code only). */
157
+ async function installCommandsDefs(profile, opts, prev) {
158
+ const res = await installCommands(profile, opts.localDefs, opts.dryRun, prev);
159
+ if (res.skippedReason === "not-supported") {
160
+ info(chalk.dim(`Commands not yet supported for ${profile.displayName} (coming soon)`));
161
+ return res;
162
+ }
163
+ const total = res.agentCommands + res.workflowCommands + res.pipelineCommands;
164
+ const parts = [];
165
+ if (total > 0)
166
+ parts.push(`${total} copied`);
167
+ if (res.skipped > 0)
168
+ parts.push(`${res.skipped} unchanged`);
169
+ if (res.removed > 0)
170
+ parts.push(`${res.removed} removed`);
171
+ const dest = opts.localDefs
172
+ ? "./uluops/commands/"
173
+ : `${profile.paths.commandsDir.replace(process.env["HOME"] ?? "", "~")}/`;
174
+ ok(`${res.files.length} commands → ${dest}${parts.length ? ` (${parts.join(", ")})` : ""}`);
175
+ return res;
176
+ }
177
+ /** Install agent-metrics hook and tool files (Claude Code only). */
178
+ async function configureMetricsStep(profile, opts) {
179
+ if (!profile.hooks) {
180
+ info(chalk.dim(`Metrics hooks not supported for ${profile.displayName}`));
181
+ return { toolFilesCopied: 0, hookConfigured: false };
182
+ }
183
+ const probe = probeHookSupport();
184
+ if (probe.warning)
185
+ warn(probe.warning);
186
+ const res = await installMetrics(profile, opts.dryRun);
187
+ if (res.hookConfigured) {
121
188
  const parts = [];
122
- if (metricsResult.toolFilesCopied > 0)
123
- parts.push(`${metricsResult.toolFilesCopied} files`);
189
+ if (res.toolFilesCopied > 0)
190
+ parts.push(`${res.toolFilesCopied} files`);
124
191
  parts.push("hook configured");
125
- ok(`Agent metrics ~/.claude/tools/agent-metrics/ (${parts.join(", ")})`);
192
+ const toolPath = profile.paths.toolsDir?.replace(process.env["HOME"] ?? "", "~");
193
+ ok(`Agent metrics → ${toolPath}/ (${parts.join(", ")})`);
126
194
  }
127
195
  else {
128
196
  warn("Agent metrics hook not configured (tool files not found)");
129
197
  }
130
- // Health check
198
+ return res;
199
+ }
200
+ /** Ping tracker and registry health endpoints. */
201
+ async function runHealthCheck(opts) {
131
202
  if (!opts.skipValidation && !opts.dryRun) {
132
203
  try {
133
204
  const [trackerOk, registryOk] = await Promise.all([
134
205
  checkEndpoint("https://api.uluops.ai/api/v1/health"),
135
206
  checkEndpoint("https://api.uluops.ai/api/v1/registry/health"),
136
207
  ]);
137
- if (trackerOk && registryOk) {
208
+ if (trackerOk && registryOk)
138
209
  ok("Health check passed — both APIs reachable");
139
- }
140
- else {
210
+ else
141
211
  warn("Some APIs unreachable (MCP tools may have limited functionality)");
142
- }
143
212
  }
144
213
  catch {
145
214
  warn("Health check skipped (network issue)");
146
215
  }
147
216
  }
148
- // Shell export
149
- let shellModified = false;
217
+ }
218
+ /** Optionally write ULUOPS_API_KEY export to shell profile. */
219
+ async function configureShell(env, apiKey, opts) {
220
+ let modified = false;
150
221
  if (opts.shell && env.shellProfile) {
222
+ if (!opts.yes && !opts.dryRun) {
223
+ const confirmed = await confirmShellWrite(env.shellProfile);
224
+ if (!confirmed) {
225
+ warn("Skipped writing API key to shell profile");
226
+ return false;
227
+ }
228
+ }
151
229
  await writeShellExport(env.shellProfile, apiKey, opts.dryRun);
152
230
  ok(`ULUOPS_API_KEY added to ${env.shellProfile}`);
153
- shellModified = true;
231
+ warn("API key stored in plaintext in shell profile. Consider rotating if shared machine.");
232
+ modified = true;
154
233
  }
155
234
  else if (opts.shell) {
156
235
  warn("--shell requested but no supported shell detected ($SHELL). Skipping.");
157
236
  }
158
- // Save manifest
159
- if (!opts.dryRun) {
160
- await saveManifest({
161
- version,
162
- installedAt: new Date().toISOString(),
163
- mcpScope: opts.scope,
164
- mcpConfigPath: mcpResult.configPath,
165
- defsScope: opts.localDefs ? "local" : "global",
166
- defsPath: opts.localDefs
167
- ? join(process.cwd(), "uluops")
168
- : getClaudeHome(),
169
- shellModified,
170
- agents: agentsResult.files,
171
- commands: commandsResult.files,
172
- metricsHookInstalled: metricsResult.hookConfigured,
173
- });
174
- }
175
- printSetupSummary({
176
- agentCount: agentsResult.files.length,
177
- commandCount: cmdTotal,
178
- apiKey,
179
- });
180
- }
181
- // MCP tool count across both servers. Update when server toolsets change.
182
- const TOOL_COUNT = 73;
183
- const AGENT_LIST = [
184
- ["/agents:validate", "Code quality", "sonnet"],
185
- ["/agents:type-safety", "TypeScript", "sonnet"],
186
- ["/agents:test-review", "Test quality", "sonnet"],
187
- ["/agents:optimize", "Performance", "sonnet"],
188
- ["/agents:frontend", "React/a11y", "sonnet"],
189
- ["/agents:mcp-validate", "MCP compliance", "sonnet"],
190
- ["/agents:architect", "Design review", "sonnet"],
191
- ["/agents:audit", "Runtime bugs", "opus"],
192
- ["/agents:security", "OWASP", "sonnet"],
193
- ["/agents:api-contract", "API alignment", "sonnet"],
194
- ["/agents:release", "Publish ready", "sonnet"],
195
- ["/agents:public-interface", "README/exports", "sonnet"],
196
- ["/agents:docs-validate", "Documentation", "sonnet"],
197
- ["/agents:prompt-validate", "Prompt review", "sonnet"],
198
- ["/agents:prompt-quality", "Prompt quality", "sonnet"],
199
- ["/agents:pattern-analyzer", "Patterns", "sonnet"],
200
- ["/agents:aristotle-explorer", "Categories", "opus"],
201
- ["/agents:aristotle-analyst", "Four causes", "opus"],
202
- ["/agents:aristotle-validator", "Teleology", "opus"],
203
- ["/agents:aristotle-forecaster", "Potentiality", "opus"],
204
- ["/agents:assumption-excavator", "Assumptions", "sonnet"],
205
- ["/agents:workflow-synthesis", "Cross-agent synthesis", "opus"],
206
- ];
207
- function printSetupSummary(opts) {
208
- console.log();
209
- console.log(` ${chalk.dim("━".repeat(46))}`);
210
- console.log();
211
- console.log(` ${chalk.bold("Setup complete!")} ${TOOL_COUNT} MCP tools · ${opts.agentCount} agents · ${opts.commandCount} slash commands · metrics`);
212
- console.log();
213
- printAgentList();
214
- info("For SDK/CLI usage, add to your shell profile:");
215
- info(` ${chalk.cyan(`export ULUOPS_API_KEY="${opts.apiKey}"`)}`);
216
- console.log();
217
- info(`Run again to update: ${chalk.cyan("npx @uluops/setup")}`);
218
- console.log();
219
- // Restart warning — last and prominent
220
- console.log(` ${chalk.dim("━".repeat(46))}`);
221
- console.log();
222
- console.log(` ${chalk.yellow.bold("Restart Claude Code to load agents.")}`);
223
- console.log();
224
- info("After restart, verify with:");
225
- info(` ${chalk.cyan("/agents:validate --help")}`);
226
- console.log();
227
- info("Then try:");
228
- info(` ${chalk.cyan("/workflows:post-implementation .")}`);
229
- console.log();
237
+ return modified;
230
238
  }
231
- function printAgentList() {
232
- info(chalk.bold("WORKFLOWS"));
233
- info(` ${chalk.cyan("/workflows:pre-implementation")} Design review before coding`);
234
- info(` ${chalk.cyan("/workflows:post-implementation")} Iterative validation loop`);
235
- info(` ${chalk.cyan("/workflows:ship")} Final gate before shipping`);
236
- info(` ${chalk.cyan("/workflows:prompt-audit")} Audit agent prompts`);
237
- console.log();
238
- info(` ${chalk.cyan("/workflows:aristotle")} Four-cause teleological analysis`);
239
- console.log();
240
- info(`${chalk.bold("AGENTS")} (run individually)${" ".repeat(26)}${chalk.dim("MODEL")}`);
241
- for (const [cmd, desc, model] of AGENT_LIST) {
242
- info(` ${chalk.cyan(cmd.padEnd(34))}${desc.padEnd(17)}${chalk.dim(model)}`);
243
- }
244
- console.log();
245
- info(chalk.dim(` This is the starter set. Browse 135+ agents at registry.uluops.ai`));
246
- console.log();
239
+ /** Interactive y/N confirmation before writing API key to shell profile. */
240
+ async function confirmShellWrite(profilePath) {
241
+ const readline = await import("node:readline/promises");
242
+ const rl = readline.createInterface({
243
+ input: process.stdin,
244
+ output: process.stdout,
245
+ });
246
+ const answer = await rl.question(`Write ULUOPS_API_KEY to ${profilePath}? (y/N) `);
247
+ rl.close();
248
+ return answer.trim().toLowerCase() === "y";
247
249
  }
248
250
  async function runUninstall(opts) {
249
251
  const version = await getVersion();
@@ -258,59 +260,87 @@ async function runUninstall(opts) {
258
260
  warn("No manifest found — nothing to uninstall.");
259
261
  return;
260
262
  }
261
- // Remove agents
262
- if (!opts.dryRun) {
263
- const removed = await uninstallAgents(manifest.agents, manifest.defsPath);
264
- ok(`Removed ${removed} agent(s)`);
265
- }
266
- else {
267
- ok(`Would remove ${manifest.agents.length} agent(s)`);
268
- }
269
- // Remove commands
270
- if (!opts.dryRun) {
271
- const removed = await uninstallCommands(manifest.commands, manifest.defsPath);
272
- ok(`Removed ${removed} command(s)`);
273
- }
274
- else {
275
- ok(`Would remove ${manifest.commands.length} command(s)`);
276
- }
277
- // Remove MCP config
278
- if (!opts.dryRun) {
279
- await uninstallMcp(manifest.mcpConfigPath);
280
- ok(`Removed MCP servers from ${manifest.mcpConfigPath}`);
263
+ const validation = await validateManifest(manifest);
264
+ if (!validation.valid) {
265
+ fail("Manifest references paths that no longer exist:");
266
+ for (const err of validation.errors)
267
+ info(` ${err}`);
268
+ console.log();
269
+ info("Uninstall may be incomplete. Proceeding with what's available.");
270
+ console.log();
281
271
  }
282
- else {
283
- ok(`Would remove MCP servers from ${manifest.mcpConfigPath}`);
272
+ if (validation.warnings.length > 0) {
273
+ for (const w of validation.warnings)
274
+ warn(w);
275
+ console.log();
284
276
  }
285
- // Remove agent metrics hook and tool files
286
- if (manifest.metricsHookInstalled) {
277
+ for (const [harnessName, hm] of Object.entries(manifest.harnesses)) {
278
+ let profile;
279
+ try {
280
+ profile = getProfile(harnessName);
281
+ }
282
+ catch {
283
+ warn(`Unknown harness "${harnessName}" in manifest — skipping`);
284
+ continue;
285
+ }
286
+ info(chalk.bold(profile.displayName));
287
+ if (!opts.dryRun) {
288
+ const agentCount = await uninstallAgents(hm.agents, hm.defsPath);
289
+ ok(`Removed ${agentCount} agent(s)`);
290
+ }
291
+ else {
292
+ ok(`Would remove ${hm.agents.length} agent(s)`);
293
+ }
294
+ if (hm.commands.length > 0) {
295
+ if (!opts.dryRun) {
296
+ const cmdCount = await uninstallCommands(hm.commands, hm.defsPath);
297
+ ok(`Removed ${cmdCount} command(s)`);
298
+ }
299
+ else {
300
+ ok(`Would remove ${hm.commands.length} command(s)`);
301
+ }
302
+ }
287
303
  if (!opts.dryRun) {
288
- await uninstallMetrics(false);
289
- ok("Removed agent-metrics hook and tool files");
304
+ try {
305
+ await uninstallMcp(profile, hm.mcpConfigPath);
306
+ ok(`Removed MCP servers from ${hm.mcpConfigPath}`);
307
+ }
308
+ catch {
309
+ warn(`Could not remove MCP servers from ${hm.mcpConfigPath}`);
310
+ }
290
311
  }
291
312
  else {
292
- ok("Would remove agent-metrics hook and tool files");
313
+ ok(`Would remove MCP servers from ${hm.mcpConfigPath}`);
314
+ }
315
+ if (hm.hooksInstalled) {
316
+ if (!opts.dryRun) {
317
+ await uninstallMetrics(profile, false);
318
+ ok("Removed agent-metrics hook and tool files");
319
+ }
320
+ else {
321
+ ok("Would remove agent-metrics hook and tool files");
322
+ }
293
323
  }
324
+ console.log();
294
325
  }
295
326
  // Remove shell export
296
327
  if (manifest.shellModified) {
297
328
  const { getShellProfile } = await import("./lib/paths.js");
298
- const profile = getShellProfile();
299
- if (profile && !opts.dryRun) {
300
- await removeShellExport(profile.path);
301
- ok(`Removed export from ${profile.path}`);
329
+ const shellProfile = getShellProfile();
330
+ if (shellProfile && !opts.dryRun) {
331
+ await removeShellExport(shellProfile.path);
332
+ ok(`Removed export from ${shellProfile.path}`);
302
333
  }
303
- else if (profile) {
304
- ok(`Would remove export from ${profile.path}`);
334
+ else if (shellProfile) {
335
+ ok(`Would remove export from ${shellProfile.path}`);
305
336
  }
306
337
  }
307
- // Delete manifest
308
338
  if (!opts.dryRun) {
309
339
  await deleteManifest();
310
340
  ok("Manifest deleted");
311
341
  }
312
342
  console.log();
313
- info("UluOps has been removed. Restart Claude Code to complete.");
343
+ info("UluOps has been removed. Restart your harness to complete.");
314
344
  console.log();
315
345
  }
316
346
  async function runVerify() {
@@ -337,9 +367,12 @@ async function runVerify() {
337
367
  console.log();
338
368
  process.exit(result.ok ? 0 : 1);
339
369
  }
340
- async function checkConflicts(localDefs) {
341
- const destDir = getAgentsDir(localDefs);
342
- const srcDir = join(ASSETS_DIR, "agents");
370
+ /** Warn if existing agent files will be overwritten and prompt for confirmation. */
371
+ async function checkConflicts(profile, localDefs) {
372
+ const destDir = localDefs
373
+ ? join(await findProjectRoot(), "uluops", "agents")
374
+ : profile.paths.agentsDir;
375
+ const srcDir = join(ASSETS_DIR, "agents", profile.name);
343
376
  let existingFiles;
344
377
  let assetFiles;
345
378
  try {
@@ -347,7 +380,7 @@ async function checkConflicts(localDefs) {
347
380
  assetFiles = await readdir(srcDir);
348
381
  }
349
382
  catch {
350
- return; // Directory doesn't exist yet
383
+ return;
351
384
  }
352
385
  const conflicts = assetFiles.filter((f) => existingFiles.includes(f));
353
386
  if (conflicts.length === 0)
@@ -368,9 +401,12 @@ async function checkConflicts(localDefs) {
368
401
  process.exit(0);
369
402
  }
370
403
  }
404
+ /** Fetch a URL and return true if the response is OK, false on any failure. */
371
405
  async function checkEndpoint(url) {
372
406
  try {
373
- const res = await fetch(url, { signal: AbortSignal.timeout(5000) });
407
+ const res = await fetch(url, {
408
+ signal: AbortSignal.timeout(getHealthTimeout()),
409
+ });
374
410
  return res.ok;
375
411
  }
376
412
  catch {
@@ -381,12 +417,13 @@ async function main() {
381
417
  const version = await getVersion();
382
418
  const program = new Command()
383
419
  .name("uluops-setup")
384
- .description("Zero-friction installer for UluOps + Claude Code")
420
+ .description("Zero-friction installer for UluOps agentic harnesses")
385
421
  .version(version)
386
422
  .option("--api-key <key>", "API key (skip prompt)")
387
423
  .option("--signup", "Create a new account (email + password, no browser)")
424
+ .option("--harness <name>", `Target harness: ${listHarnesses().join(", ")} (aliases: claude, oc, gemini)`, "claude-code")
388
425
  .option("--scope <mode>", 'MCP config scope: "global" or "local"', "global")
389
- .option("--local-defs", "Save agents/commands locally instead of ~/.claude/", false)
426
+ .option("--local-defs", "Save agents/commands locally instead of harness global dir", false)
390
427
  .option("--shell", "Write API key export to shell profile", false)
391
428
  .option("--skip-validation", "Accept API key without verifying", false)
392
429
  .option("--list", "Show available agents and workflows without installing")
@@ -400,7 +437,7 @@ async function main() {
400
437
  console.log();
401
438
  console.log(` ${chalk.dim("⟨u⟩")} ${chalk.cyan.bold("ulu")}${chalk.bold("·ops")} v${version} — available agents and workflows`);
402
439
  console.log();
403
- printAgentList();
440
+ await printAgentList();
404
441
  info(`Install with: ${chalk.cyan("npx @uluops/setup")}`);
405
442
  console.log();
406
443
  return;
@@ -418,6 +455,8 @@ async function main() {
418
455
  process.exit(1);
419
456
  }
420
457
  const scope = opts.scope === "local" ? "local" : "global";
458
+ // Resolve harness name (supports aliases)
459
+ const harnessName = resolveHarnessName(opts.harness);
421
460
  await runSetup({
422
461
  apiKey: opts.apiKey,
423
462
  signup: opts.signup ?? false,
@@ -427,9 +466,14 @@ async function main() {
427
466
  skipValidation: opts.skipValidation,
428
467
  dryRun: opts.dryRun,
429
468
  yes: opts.yes,
469
+ harness: harnessName,
430
470
  });
431
471
  }
432
472
  main().catch((err) => {
473
+ if (err instanceof HarnessNotTestedError) {
474
+ console.error(chalk.yellow(`\n ${err.message}\n`));
475
+ process.exit(1);
476
+ }
433
477
  const msg = err instanceof Error ? err.message : String(err);
434
478
  console.error(chalk.red(`\n Error: ${msg}\n`));
435
479
  process.exit(1);
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Claude Code Harness Profile
3
+ *
4
+ * Wraps existing config-merger.ts and settings-merger.ts logic
5
+ * behind the HarnessProfile abstraction.
6
+ */
7
+ import type { HarnessProfile } from "./types.js";
8
+ export declare const claudeCodeProfile: HarnessProfile;