@uluops/setup 0.8.1 → 0.9.1

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.
package/README.md CHANGED
@@ -19,7 +19,9 @@ npx @uluops/setup
19
19
  | Claude Code | Fully supported (default) | `claude` | `~/.claude.json` |
20
20
  | OpenCode | Fully supported | `oc` | `~/.config/opencode/opencode.json` |
21
21
  | Gemini CLI | Fully supported | `gemini` | `~/.gemini/settings.json` |
22
- | Codex | Experimental (opt-in via `--harness codex`) | — | `~/.codex/config.toml` |
22
+ | Codex | Fully supported | — | `~/.codex/config.toml` |
23
+
24
+ > **Codex note:** the TOML writer seeds `approval_mode = "approve"` for every read-side MCP tool exported by `@uluops/ops-mcp` and `@uluops/registry-mcp` so interactive sessions don't surface an approval prompt on every `list_*`/`get_*`/`query_*` call. Write-side tools (`save_run`, `bulk_update_status`, `publish_definition`, etc.) are intentionally NOT pre-approved — Codex still asks before any state-changing operation. A re-install over a hand-tuned config (any `[mcp_servers.<server>.tools.*]` block present) preserves your customizations verbatim and skips the seed step.
23
25
 
24
26
  ```bash
25
27
  # Install for Claude Code (default)
@@ -45,7 +47,7 @@ If you don't pass `--harness` or `--all-detected`, setup probes your home direct
45
47
  - **Multiple harnesses detected (non-interactive — `--yes`, `--api-key`, piped stdin)** — to keep CI scripts predictable, this preserves earlier behavior: the first detected harness installs and a dimmed notice lists the others. CI users who want multi-install opt in explicitly with `--all-detected`.
46
48
  - **No harnesses detected** — falls back to the default (`claude-code`) so `npx @uluops/setup` always does something useful on a fresh machine.
47
49
 
48
- Passing `--harness <name>` always wins — auto-detection is skipped entirely. Experimental harnesses are excluded from auto-detection (an explicit `--harness` is the only way to opt in).
50
+ Passing `--harness <name>` always wins — auto-detection is skipped entirely. Auto-detection only returns harnesses marked stable; when a future harness ships as experimental, an explicit `--harness <name>` will remain the only way to opt in.
49
51
 
50
52
  ### Multi-harness install
51
53
 
@@ -92,7 +94,7 @@ Each harness gets its own per-section block in the summary:
92
94
  | Pipeline commands | 2 | `~/.claude/commands/pipelines/` |
93
95
  | Agent metrics hook | 1 | `~/.claude/tools/agent-metrics/` |
94
96
 
95
- > Paths shown are for Claude Code (default). Gemini CLI installs agents as `.md` and commands as `.toml` to `~/.gemini/`. OpenCode installs agents to `~/.config/opencode/agents/`. Agent definitions and commands are transformed to the target harness format at install time from a single source.
97
+ > Paths shown are for Claude Code (default). Gemini CLI installs agents as `.md` and commands as `.toml` to `~/.gemini/`. OpenCode installs agents to `~/.config/opencode/agents/`. Codex installs agents as `.toml` to `~/.codex/agents/` and ships a single `uluops-operator` skill under `~/.codex/skills/` instead of slash commands. Agent definitions and commands are transformed to the target harness format at install time from a single source.
96
98
 
97
99
  The installer runs these steps in sequence:
98
100
 
@@ -11,28 +11,136 @@ import { homedir } from "node:os";
11
11
  import { join } from "node:path";
12
12
  import { ULUOPS_SERVERS, } from "./types.js";
13
13
  import { atomicWrite } from "../lib/atomic-write.js";
14
+ import { OPS_MCP_SPEC, REGISTRY_MCP_SPEC } from "../lib/mcp-packages.js";
14
15
  const RAW_TOML = "__rawToml";
16
+ /**
17
+ * Tools to seed with `approval_mode = "approve"` (Codex's auto-allow opt-in).
18
+ *
19
+ * Seeded ONLY when the user has no prior `[mcp_servers.NAME.tools.*]` blocks
20
+ * for the server — a re-install over a hand-tuned config preserves the user's
21
+ * choices verbatim (the merge bails on its own seeds the moment it sees user
22
+ * intent).
23
+ *
24
+ * Only read-side tools are seeded. Writes still prompt — the user retains a
25
+ * choice point on every state-changing operation. Lists mirror the
26
+ * `sideEffects: 'read'` declarations in the respective server's tool-registry
27
+ * (sources of truth: ops-uluops-mcp/src/config/tool-registry.ts and
28
+ * uluops-registry-mcp/src/config/tool-registry.ts). If a new read tool ships
29
+ * there, add it here in the same PR — Codex users will silently get a prompt
30
+ * on first use otherwise.
31
+ */
32
+ const TRACKER_READ_TOOLS = [
33
+ "diff_runs",
34
+ "get_agent_lifecycle",
35
+ "get_agent_matrix",
36
+ "get_agent_reliability",
37
+ "get_agent_runs_analysis",
38
+ "get_analytics",
39
+ "get_burndown",
40
+ "get_discovery",
41
+ "get_full_taxonomy_analytics",
42
+ "get_issue_by_fingerprint",
43
+ "get_issue_details",
44
+ "get_issue_history",
45
+ "get_latest_run",
46
+ "get_project",
47
+ "get_project_analysis",
48
+ "get_project_summary",
49
+ "get_project_trends",
50
+ "get_run",
51
+ "get_run_analysis",
52
+ "get_run_details",
53
+ "get_taxonomy",
54
+ "get_velocity",
55
+ "list_agents",
56
+ "list_projects",
57
+ "list_runs",
58
+ "query_analysis_records",
59
+ "query_issues",
60
+ "search_issues",
61
+ "validate_run",
62
+ ];
63
+ const REGISTRY_READ_TOOLS = [
64
+ "batch_users",
65
+ "compare_effectiveness",
66
+ "diff_versions",
67
+ "get_definition",
68
+ "get_dependencies",
69
+ "get_dependents",
70
+ "get_diff_impact",
71
+ "get_ecosystem_overview",
72
+ "get_effectiveness",
73
+ "get_evolution",
74
+ "get_execution_stats",
75
+ "get_fork_lineage",
76
+ "get_health",
77
+ "get_language",
78
+ "get_lineage",
79
+ "get_model",
80
+ "get_translation_analytics",
81
+ "get_translator_version",
82
+ "get_user",
83
+ "is_forkable",
84
+ "list_aliases",
85
+ "list_definitions",
86
+ "list_forks",
87
+ "list_languages",
88
+ "list_models",
89
+ "list_providers",
90
+ "list_versions",
91
+ "render_definition",
92
+ "resolve_alias",
93
+ "search_definitions",
94
+ "set_default_type",
95
+ "validate_definition",
96
+ ];
15
97
  function tomlString(value) {
16
98
  return JSON.stringify(value);
17
99
  }
18
- function serverBlock(name, pkg, apiKey) {
100
+ function toolBlock(serverName, toolName) {
19
101
  return [
20
- `[mcp_servers.${tomlString(name)}]`,
102
+ `[mcp_servers.${serverName}.tools.${toolName}]`,
103
+ `approval_mode = "approve"`,
104
+ ].join("\n");
105
+ }
106
+ function serverBlock(name, pkg, apiKey, seedTools) {
107
+ const lines = [
108
+ `[mcp_servers.${name}]`,
21
109
  `command = "npx"`,
22
110
  `args = ["-y", ${tomlString(pkg)}]`,
23
111
  ``,
24
- `[mcp_servers.${tomlString(name)}.env]`,
112
+ `[mcp_servers.${name}.env]`,
25
113
  `ULUOPS_API_KEY = ${tomlString(apiKey)}`,
26
- ].join("\n");
114
+ ];
115
+ for (const tool of seedTools) {
116
+ lines.push(``, toolBlock(name, tool));
117
+ }
118
+ return lines.join("\n");
27
119
  }
28
120
  function isServerTableFor(name, table) {
29
121
  return table === `mcp_servers.${name}` || table === `mcp_servers."${name}"`;
30
122
  }
123
+ function isServerEnvTableFor(name, table) {
124
+ return (table === `mcp_servers.${name}.env` ||
125
+ table === `mcp_servers."${name}".env`);
126
+ }
31
127
  function isServerSubtableFor(name, table) {
32
128
  const unquotedPrefix = `mcp_servers.${name}.`;
33
129
  const quotedPrefix = `mcp_servers."${name}".`;
34
130
  return table.startsWith(unquotedPrefix) || table.startsWith(quotedPrefix);
35
131
  }
132
+ function isServerToolTableFor(name, table) {
133
+ const unquotedPrefix = `mcp_servers.${name}.tools.`;
134
+ const quotedPrefix = `mcp_servers."${name}".tools.`;
135
+ return table.startsWith(unquotedPrefix) || table.startsWith(quotedPrefix);
136
+ }
137
+ /**
138
+ * Strip the main `[mcp_servers.NAME]` and `[mcp_servers.NAME.env]` blocks
139
+ * but preserve `[mcp_servers.NAME.tools.*]` blocks. Tool overrides may be
140
+ * user customizations — destroying them on every re-install would punish
141
+ * anyone who hand-edited their Codex config to deny a specific tool or to
142
+ * approve one we don't seed.
143
+ */
36
144
  function removeServerConfigBlocks(raw, name) {
37
145
  const lines = raw.split("\n");
38
146
  const kept = [];
@@ -40,7 +148,7 @@ function removeServerConfigBlocks(raw, name) {
40
148
  for (const line of lines) {
41
149
  const table = line.trim().match(/^\[([^\]]+)\]$/)?.[1];
42
150
  if (table) {
43
- skipping = isServerTableFor(name, table) || table === `mcp_servers.${name}.env` || table === `mcp_servers."${name}".env`;
151
+ skipping = isServerTableFor(name, table) || isServerEnvTableFor(name, table);
44
152
  }
45
153
  if (!skipping)
46
154
  kept.push(line);
@@ -61,6 +169,14 @@ function removeServerSubtree(raw, name) {
61
169
  }
62
170
  return kept.join("\n").replace(/\n{3,}/g, "\n\n").trimEnd();
63
171
  }
172
+ function hasUserToolEntries(raw, name) {
173
+ for (const line of raw.split("\n")) {
174
+ const table = line.trim().match(/^\[([^\]]+)\]$/)?.[1];
175
+ if (table && isServerToolTableFor(name, table))
176
+ return true;
177
+ }
178
+ return false;
179
+ }
64
180
  class CodexMcpConfig {
65
181
  async read(path) {
66
182
  try {
@@ -72,10 +188,17 @@ class CodexMcpConfig {
72
188
  }
73
189
  merge(config, apiKey) {
74
190
  let raw = typeof config[RAW_TOML] === "string" ? config[RAW_TOML] : "";
191
+ // Detect prior user customization BEFORE we strip anything. A hand-tuned
192
+ // config (any `[mcp_servers.NAME.tools.*]` block present) signals user
193
+ // intent — we replay only main + env, leaving the user's per-tool
194
+ // approval choices untouched. A fresh config gets the seeded read-tool
195
+ // approvals so first-launch UX skips the prompt cascade.
196
+ const trackerHasUserTools = hasUserToolEntries(raw, "uluops-tracker");
197
+ const registryHasUserTools = hasUserToolEntries(raw, "uluops-registry");
75
198
  raw = removeServerConfigBlocks(removeServerConfigBlocks(raw, "uluops-tracker"), "uluops-registry");
76
199
  const blocks = [
77
- serverBlock("uluops-tracker", "@uluops/ops-mcp", apiKey),
78
- serverBlock("uluops-registry", "@uluops/registry-mcp", apiKey),
200
+ serverBlock("uluops-tracker", OPS_MCP_SPEC, apiKey, trackerHasUserTools ? [] : TRACKER_READ_TOOLS),
201
+ serverBlock("uluops-registry", REGISTRY_MCP_SPEC, apiKey, registryHasUserTools ? [] : REGISTRY_READ_TOOLS),
79
202
  ].join("\n\n");
80
203
  return { [RAW_TOML]: [raw.trimEnd(), blocks].filter(Boolean).join("\n\n") + "\n" };
81
204
  }
@@ -102,7 +225,7 @@ const home = join(homedir(), ".codex");
102
225
  export const codexProfile = {
103
226
  name: "codex",
104
227
  displayName: "Codex",
105
- status: "experimental",
228
+ status: "stable",
106
229
  homeDir: home,
107
230
  agentFormat: "toml",
108
231
  factoryTarget: "codex",
@@ -16,6 +16,7 @@ import { join, isAbsolute } from "node:path";
16
16
  import { parse as parseJsonc } from "jsonc-parser";
17
17
  import { ULUOPS_SERVERS, ConfigParseError } from "./types.js";
18
18
  import { atomicWrite } from "../lib/atomic-write.js";
19
+ import { OPS_MCP_SPEC, REGISTRY_MCP_SPEC } from "../lib/mcp-packages.js";
19
20
  class OpenCodeMcpConfig {
20
21
  /** Maps requested path → actual resolved path (for .jsonc fallback). */
21
22
  resolvedPaths = new Map();
@@ -51,7 +52,7 @@ class OpenCodeMcpConfig {
51
52
  // their bundled SDKs. See lib/config-merger.ts for rationale.
52
53
  const tracker = {
53
54
  type: "local",
54
- command: ["npx", "-y", "@uluops/ops-mcp"],
55
+ command: ["npx", "-y", OPS_MCP_SPEC],
55
56
  enabled: true,
56
57
  timeout: 30000,
57
58
  environment: {
@@ -60,7 +61,7 @@ class OpenCodeMcpConfig {
60
61
  };
61
62
  const registry = {
62
63
  type: "local",
63
- command: ["npx", "-y", "@uluops/registry-mcp"],
64
+ command: ["npx", "-y", REGISTRY_MCP_SPEC],
64
65
  enabled: true,
65
66
  timeout: 30000,
66
67
  environment: {
@@ -22,9 +22,12 @@ export class HarnessNotTestedError extends Error {
22
22
  constructor(harnessName) {
23
23
  super(
24
24
  // Keep this list in sync with profiles whose `status === "stable"`.
25
- // Today: claude-code, opencode, gemini-cli. When a new stable profile
26
- // lands, add it here so the error stays actionable.
27
- `${harnessName} harness is not yet tested. Use --harness claude-code, --harness opencode, or --harness gemini-cli.`);
25
+ // Today: claude-code, opencode, gemini-cli, codex. When a new stable
26
+ // profile lands, add it here so the error stays actionable. (As of
27
+ // 0.9.0 every shipped profile is stable, so this constructor is
28
+ // currently unreachable — kept as a structural slot for the next
29
+ // experimental harness scaffold.)
30
+ `${harnessName} harness is not yet tested. Use --harness claude-code, --harness opencode, --harness gemini-cli, or --harness codex.`);
28
31
  this.name = "HarnessNotTestedError";
29
32
  }
30
33
  }
@@ -1,6 +1,6 @@
1
1
  import { readFile } from "node:fs/promises";
2
2
  import { atomicWrite } from "./atomic-write.js";
3
- const MCP_PACKAGES = ["@uluops/ops-mcp", "@uluops/registry-mcp"];
3
+ import { MCP_PACKAGES, OPS_MCP_SPEC, REGISTRY_MCP_SPEC, } from "./mcp-packages.js";
4
4
  /**
5
5
  * In-process memoization for the npm availability probe.
6
6
  *
@@ -102,7 +102,7 @@ export function mergeUluopsMcp(config, apiKey, trust = false) {
102
102
  ...existing,
103
103
  "uluops-tracker": {
104
104
  command: "npx",
105
- args: ["-y", "@uluops/ops-mcp"],
105
+ args: ["-y", OPS_MCP_SPEC],
106
106
  env: {
107
107
  ULUOPS_API_KEY: apiKey,
108
108
  },
@@ -110,7 +110,7 @@ export function mergeUluopsMcp(config, apiKey, trust = false) {
110
110
  },
111
111
  "uluops-registry": {
112
112
  command: "npx",
113
- args: ["-y", "@uluops/registry-mcp"],
113
+ args: ["-y", REGISTRY_MCP_SPEC],
114
114
  env: {
115
115
  ULUOPS_API_KEY: apiKey,
116
116
  },
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Pinned MCP server package versions.
3
+ *
4
+ * Every harness's MCP config writer (claude-code/gemini-cli/opencode JSON
5
+ * mergers, codex TOML writer) stamps these spec strings into the harness
6
+ * config so `npx -y <spec>` resolves a known-good version instead of
7
+ * latest. Pinning makes a `@uluops/setup` release self-contained — what
8
+ * users get on first launch is what the package was tested against,
9
+ * regardless of when later MCP server versions ship.
10
+ *
11
+ * **Bump rule:** when an MCP server publishes a new version, bump the
12
+ * version here in the same setup release. A pinned setup install never
13
+ * silently picks up a downstream regression; a missed bump shows up as
14
+ * the next setup release stamping the previous combination.
15
+ *
16
+ * The bare package NAMES (without versions) remain available as
17
+ * `MCP_PACKAGES` for the npm availability probe — that probe asks
18
+ * "does this name exist on the registry" not "does this version exist".
19
+ * Pinning the probe to a specific version would turn a temporary
20
+ * registry blip on an older version into a setup failure even when
21
+ * the latest version was reachable.
22
+ */
23
+ export declare const OPS_MCP_PACKAGE = "@uluops/ops-mcp";
24
+ export declare const OPS_MCP_VERSION = "0.3.1";
25
+ export declare const OPS_MCP_SPEC: "@uluops/ops-mcp@0.3.1";
26
+ export declare const REGISTRY_MCP_PACKAGE = "@uluops/registry-mcp";
27
+ export declare const REGISTRY_MCP_VERSION = "0.2.5";
28
+ export declare const REGISTRY_MCP_SPEC: "@uluops/registry-mcp@0.2.5";
29
+ /**
30
+ * Bare package names for the npm availability probe. The probe checks
31
+ * existence on the registry, not version-specific resolvability, so it
32
+ * uses these unversioned names.
33
+ */
34
+ export declare const MCP_PACKAGES: readonly string[];
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Pinned MCP server package versions.
3
+ *
4
+ * Every harness's MCP config writer (claude-code/gemini-cli/opencode JSON
5
+ * mergers, codex TOML writer) stamps these spec strings into the harness
6
+ * config so `npx -y <spec>` resolves a known-good version instead of
7
+ * latest. Pinning makes a `@uluops/setup` release self-contained — what
8
+ * users get on first launch is what the package was tested against,
9
+ * regardless of when later MCP server versions ship.
10
+ *
11
+ * **Bump rule:** when an MCP server publishes a new version, bump the
12
+ * version here in the same setup release. A pinned setup install never
13
+ * silently picks up a downstream regression; a missed bump shows up as
14
+ * the next setup release stamping the previous combination.
15
+ *
16
+ * The bare package NAMES (without versions) remain available as
17
+ * `MCP_PACKAGES` for the npm availability probe — that probe asks
18
+ * "does this name exist on the registry" not "does this version exist".
19
+ * Pinning the probe to a specific version would turn a temporary
20
+ * registry blip on an older version into a setup failure even when
21
+ * the latest version was reachable.
22
+ */
23
+ export const OPS_MCP_PACKAGE = "@uluops/ops-mcp";
24
+ export const OPS_MCP_VERSION = "0.3.1";
25
+ export const OPS_MCP_SPEC = `${OPS_MCP_PACKAGE}@${OPS_MCP_VERSION}`;
26
+ export const REGISTRY_MCP_PACKAGE = "@uluops/registry-mcp";
27
+ export const REGISTRY_MCP_VERSION = "0.2.5";
28
+ export const REGISTRY_MCP_SPEC = `${REGISTRY_MCP_PACKAGE}@${REGISTRY_MCP_VERSION}`;
29
+ /**
30
+ * Bare package names for the npm availability probe. The probe checks
31
+ * existence on the registry, not version-specific resolvability, so it
32
+ * uses these unversioned names.
33
+ */
34
+ export const MCP_PACKAGES = [
35
+ OPS_MCP_PACKAGE,
36
+ REGISTRY_MCP_PACKAGE,
37
+ ];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uluops/setup",
3
- "version": "0.8.1",
3
+ "version": "0.9.1",
4
4
  "description": "Zero-friction installer for UluOps agentic harnesses",
5
5
  "license": "MIT",
6
6
  "repository": {