@uluops/setup 0.8.1 → 0.9.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.
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
 
@@ -12,27 +12,134 @@ import { join } from "node:path";
12
12
  import { ULUOPS_SERVERS, } from "./types.js";
13
13
  import { atomicWrite } from "../lib/atomic-write.js";
14
14
  const RAW_TOML = "__rawToml";
15
+ /**
16
+ * Tools to seed with `approval_mode = "approve"` (Codex's auto-allow opt-in).
17
+ *
18
+ * Seeded ONLY when the user has no prior `[mcp_servers.NAME.tools.*]` blocks
19
+ * for the server — a re-install over a hand-tuned config preserves the user's
20
+ * choices verbatim (the merge bails on its own seeds the moment it sees user
21
+ * intent).
22
+ *
23
+ * Only read-side tools are seeded. Writes still prompt — the user retains a
24
+ * choice point on every state-changing operation. Lists mirror the
25
+ * `sideEffects: 'read'` declarations in the respective server's tool-registry
26
+ * (sources of truth: ops-uluops-mcp/src/config/tool-registry.ts and
27
+ * uluops-registry-mcp/src/config/tool-registry.ts). If a new read tool ships
28
+ * there, add it here in the same PR — Codex users will silently get a prompt
29
+ * on first use otherwise.
30
+ */
31
+ const TRACKER_READ_TOOLS = [
32
+ "diff_runs",
33
+ "get_agent_lifecycle",
34
+ "get_agent_matrix",
35
+ "get_agent_reliability",
36
+ "get_agent_runs_analysis",
37
+ "get_analytics",
38
+ "get_burndown",
39
+ "get_discovery",
40
+ "get_full_taxonomy_analytics",
41
+ "get_issue_by_fingerprint",
42
+ "get_issue_details",
43
+ "get_issue_history",
44
+ "get_latest_run",
45
+ "get_project",
46
+ "get_project_analysis",
47
+ "get_project_summary",
48
+ "get_project_trends",
49
+ "get_run",
50
+ "get_run_analysis",
51
+ "get_run_details",
52
+ "get_taxonomy",
53
+ "get_velocity",
54
+ "list_agents",
55
+ "list_projects",
56
+ "list_runs",
57
+ "query_analysis_records",
58
+ "query_issues",
59
+ "search_issues",
60
+ "validate_run",
61
+ ];
62
+ const REGISTRY_READ_TOOLS = [
63
+ "batch_users",
64
+ "compare_effectiveness",
65
+ "diff_versions",
66
+ "get_definition",
67
+ "get_dependencies",
68
+ "get_dependents",
69
+ "get_diff_impact",
70
+ "get_ecosystem_overview",
71
+ "get_effectiveness",
72
+ "get_evolution",
73
+ "get_execution_stats",
74
+ "get_fork_lineage",
75
+ "get_health",
76
+ "get_language",
77
+ "get_lineage",
78
+ "get_model",
79
+ "get_translation_analytics",
80
+ "get_translator_version",
81
+ "get_user",
82
+ "is_forkable",
83
+ "list_aliases",
84
+ "list_definitions",
85
+ "list_forks",
86
+ "list_languages",
87
+ "list_models",
88
+ "list_providers",
89
+ "list_versions",
90
+ "render_definition",
91
+ "resolve_alias",
92
+ "search_definitions",
93
+ "set_default_type",
94
+ "validate_definition",
95
+ ];
15
96
  function tomlString(value) {
16
97
  return JSON.stringify(value);
17
98
  }
18
- function serverBlock(name, pkg, apiKey) {
99
+ function toolBlock(serverName, toolName) {
19
100
  return [
20
- `[mcp_servers.${tomlString(name)}]`,
101
+ `[mcp_servers.${serverName}.tools.${toolName}]`,
102
+ `approval_mode = "approve"`,
103
+ ].join("\n");
104
+ }
105
+ function serverBlock(name, pkg, apiKey, seedTools) {
106
+ const lines = [
107
+ `[mcp_servers.${name}]`,
21
108
  `command = "npx"`,
22
109
  `args = ["-y", ${tomlString(pkg)}]`,
23
110
  ``,
24
- `[mcp_servers.${tomlString(name)}.env]`,
111
+ `[mcp_servers.${name}.env]`,
25
112
  `ULUOPS_API_KEY = ${tomlString(apiKey)}`,
26
- ].join("\n");
113
+ ];
114
+ for (const tool of seedTools) {
115
+ lines.push(``, toolBlock(name, tool));
116
+ }
117
+ return lines.join("\n");
27
118
  }
28
119
  function isServerTableFor(name, table) {
29
120
  return table === `mcp_servers.${name}` || table === `mcp_servers."${name}"`;
30
121
  }
122
+ function isServerEnvTableFor(name, table) {
123
+ return (table === `mcp_servers.${name}.env` ||
124
+ table === `mcp_servers."${name}".env`);
125
+ }
31
126
  function isServerSubtableFor(name, table) {
32
127
  const unquotedPrefix = `mcp_servers.${name}.`;
33
128
  const quotedPrefix = `mcp_servers."${name}".`;
34
129
  return table.startsWith(unquotedPrefix) || table.startsWith(quotedPrefix);
35
130
  }
131
+ function isServerToolTableFor(name, table) {
132
+ const unquotedPrefix = `mcp_servers.${name}.tools.`;
133
+ const quotedPrefix = `mcp_servers."${name}".tools.`;
134
+ return table.startsWith(unquotedPrefix) || table.startsWith(quotedPrefix);
135
+ }
136
+ /**
137
+ * Strip the main `[mcp_servers.NAME]` and `[mcp_servers.NAME.env]` blocks
138
+ * but preserve `[mcp_servers.NAME.tools.*]` blocks. Tool overrides may be
139
+ * user customizations — destroying them on every re-install would punish
140
+ * anyone who hand-edited their Codex config to deny a specific tool or to
141
+ * approve one we don't seed.
142
+ */
36
143
  function removeServerConfigBlocks(raw, name) {
37
144
  const lines = raw.split("\n");
38
145
  const kept = [];
@@ -40,7 +147,7 @@ function removeServerConfigBlocks(raw, name) {
40
147
  for (const line of lines) {
41
148
  const table = line.trim().match(/^\[([^\]]+)\]$/)?.[1];
42
149
  if (table) {
43
- skipping = isServerTableFor(name, table) || table === `mcp_servers.${name}.env` || table === `mcp_servers."${name}".env`;
150
+ skipping = isServerTableFor(name, table) || isServerEnvTableFor(name, table);
44
151
  }
45
152
  if (!skipping)
46
153
  kept.push(line);
@@ -61,6 +168,14 @@ function removeServerSubtree(raw, name) {
61
168
  }
62
169
  return kept.join("\n").replace(/\n{3,}/g, "\n\n").trimEnd();
63
170
  }
171
+ function hasUserToolEntries(raw, name) {
172
+ for (const line of raw.split("\n")) {
173
+ const table = line.trim().match(/^\[([^\]]+)\]$/)?.[1];
174
+ if (table && isServerToolTableFor(name, table))
175
+ return true;
176
+ }
177
+ return false;
178
+ }
64
179
  class CodexMcpConfig {
65
180
  async read(path) {
66
181
  try {
@@ -72,10 +187,17 @@ class CodexMcpConfig {
72
187
  }
73
188
  merge(config, apiKey) {
74
189
  let raw = typeof config[RAW_TOML] === "string" ? config[RAW_TOML] : "";
190
+ // Detect prior user customization BEFORE we strip anything. A hand-tuned
191
+ // config (any `[mcp_servers.NAME.tools.*]` block present) signals user
192
+ // intent — we replay only main + env, leaving the user's per-tool
193
+ // approval choices untouched. A fresh config gets the seeded read-tool
194
+ // approvals so first-launch UX skips the prompt cascade.
195
+ const trackerHasUserTools = hasUserToolEntries(raw, "uluops-tracker");
196
+ const registryHasUserTools = hasUserToolEntries(raw, "uluops-registry");
75
197
  raw = removeServerConfigBlocks(removeServerConfigBlocks(raw, "uluops-tracker"), "uluops-registry");
76
198
  const blocks = [
77
- serverBlock("uluops-tracker", "@uluops/ops-mcp", apiKey),
78
- serverBlock("uluops-registry", "@uluops/registry-mcp", apiKey),
199
+ serverBlock("uluops-tracker", "@uluops/ops-mcp", apiKey, trackerHasUserTools ? [] : TRACKER_READ_TOOLS),
200
+ serverBlock("uluops-registry", "@uluops/registry-mcp", apiKey, registryHasUserTools ? [] : REGISTRY_READ_TOOLS),
79
201
  ].join("\n\n");
80
202
  return { [RAW_TOML]: [raw.trimEnd(), blocks].filter(Boolean).join("\n\n") + "\n" };
81
203
  }
@@ -102,7 +224,7 @@ const home = join(homedir(), ".codex");
102
224
  export const codexProfile = {
103
225
  name: "codex",
104
226
  displayName: "Codex",
105
- status: "experimental",
227
+ status: "stable",
106
228
  homeDir: home,
107
229
  agentFormat: "toml",
108
230
  factoryTarget: "codex",
@@ -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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uluops/setup",
3
- "version": "0.8.1",
3
+ "version": "0.9.0",
4
4
  "description": "Zero-friction installer for UluOps agentic harnesses",
5
5
  "license": "MIT",
6
6
  "repository": {