@wipcomputer/wip-ldm-os 0.4.83 → 0.4.85-alpha.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/SKILL.md CHANGED
@@ -9,7 +9,7 @@ license: MIT
9
9
  compatibility: Requires git, npm, node. Node.js 18+.
10
10
  metadata:
11
11
  display-name: "LDM OS"
12
- version: "0.4.83"
12
+ version: "0.4.84"
13
13
  homepage: "https://github.com/wipcomputer/wip-ldm-os"
14
14
  author: "Parker Todd Brooks"
15
15
  category: infrastructure
package/bin/ldm.js CHANGED
@@ -1537,13 +1537,21 @@ async function cmdInstall() {
1537
1537
  // npm install --prefix silently fails for scoped packages in temp directories...
1538
1538
  // it creates the lock file but doesn't extract files. npm pack is reliable.
1539
1539
  const npmName = resolvedTarget;
1540
+ // --alpha and --beta select the corresponding npm dist-tag instead of @latest.
1541
+ // Without this, `ldm install --alpha <pkg>` was pulling the @latest version
1542
+ // from npm pack and an existing global install would never advance to the
1543
+ // current alpha. Now the tag flows through pack + the downstream
1544
+ // installSingleTool's `npm install -g <pkg>@<version>` step uses the
1545
+ // version baked into the alpha tarball.
1546
+ const npmTag = ALPHA_FLAG ? 'alpha' : (BETA_FLAG ? 'beta' : '');
1547
+ const packTarget = npmTag ? `${npmName}@${npmTag}` : npmName;
1540
1548
  const tempDir = join(LDM_TMP, `npm-${Date.now()}`);
1541
1549
  console.log('');
1542
- console.log(` Installing ${npmName} from npm...`);
1550
+ console.log(` Installing ${packTarget} from npm...`);
1543
1551
  try {
1544
1552
  mkdirSync(tempDir, { recursive: true });
1545
1553
  // Use npm pack + tar instead of npm install --prefix
1546
- const tarball = execSync(`npm pack ${npmName} --pack-destination "${tempDir}" 2>/dev/null`, {
1554
+ const tarball = execSync(`npm pack ${packTarget} --pack-destination "${tempDir}" 2>/dev/null`, {
1547
1555
  encoding: 'utf8', timeout: 60000, cwd: tempDir,
1548
1556
  }).trim();
1549
1557
  const tarPath = join(tempDir, tarball);
@@ -4289,6 +4297,164 @@ async function main() {
4289
4297
  console.log('');
4290
4298
  }
4291
4299
 
4300
+ // ── ldm uninstall <pkg> ──
4301
+ //
4302
+ // Removes a single LDM-installed package. Used to reset a single
4303
+ // extension between dogfood cycles without taking down the rest of
4304
+ // LDM OS. Pairs with the per-package cleanup hook each package may
4305
+ // ship (e.g. `codex-daemon uninstall --purge` for Codex Remote
4306
+ // Control), which the user runs first to clean up product-specific
4307
+ // state. This command then removes the LDM-side install record + the
4308
+ // global npm package + the LDM extension dir.
4309
+ //
4310
+ // Safety:
4311
+ // - Never touches ~/.codex/ or other unrelated user state.
4312
+ // - Never removes ~/.ldm/memory/ or ~/.ldm/agents/.
4313
+ // - Never removes other extensions.
4314
+ // - Idempotent: running twice exits cleanly.
4315
+ // - Refuses to uninstall LDM OS itself (use `ldm uninstall` for that).
4316
+
4317
+ async function cmdUninstallPackage(pkgName) {
4318
+ const isDryRun = args.includes('--dry-run');
4319
+
4320
+ if (pkgName === 'wip-ldm-os' || pkgName === '@wipcomputer/wip-ldm-os') {
4321
+ console.error(' Refusing to uninstall LDM OS itself with `ldm uninstall <pkg>`.');
4322
+ console.error(' To remove all of LDM OS: ldm uninstall');
4323
+ process.exit(1);
4324
+ }
4325
+
4326
+ console.log('');
4327
+ console.log(` ldm uninstall ${pkgName}`);
4328
+ console.log(' ────────────────────────────────────');
4329
+
4330
+ // 1. Look up the package in the registry.
4331
+ const registryPath = join(LDM_EXTENSIONS, 'registry.json');
4332
+ let registry = { _format: 'v1', extensions: {} };
4333
+ try {
4334
+ if (existsSync(registryPath)) {
4335
+ registry = JSON.parse(readFileSync(registryPath, 'utf8'));
4336
+ }
4337
+ } catch (e) {
4338
+ console.error(` ! could not read registry at ${registryPath}: ${e.message}`);
4339
+ }
4340
+ const entry = registry.extensions?.[pkgName] || null;
4341
+
4342
+ // 2. Resolve npm package name.
4343
+ // Registry entries from npm installs put the npm name in `source.npm`
4344
+ // or in the top-level `name` field. Fall back to the user-supplied
4345
+ // pkgName (works for unscoped packages).
4346
+ const npmPkg = entry?.source?.npm || entry?.name || pkgName;
4347
+
4348
+ // 3. Resolve LDM extension dir(s).
4349
+ const ldmExtPath = entry?.paths?.ldm
4350
+ || entry?.ldmPath
4351
+ || join(LDM_EXTENSIONS, pkgName);
4352
+ const ocExtPath = entry?.paths?.openclaw
4353
+ || entry?.ocPath
4354
+ || null;
4355
+
4356
+ // 4. Build the action plan.
4357
+ const actions = [];
4358
+ let pkgInstalledGlobally = false;
4359
+ try {
4360
+ const npmList = execSync(`npm list -g --depth=0 --json 2>/dev/null`, { encoding: 'utf8' });
4361
+ const deps = JSON.parse(npmList).dependencies || {};
4362
+ pkgInstalledGlobally = !!deps[npmPkg];
4363
+ } catch {}
4364
+
4365
+ if (pkgInstalledGlobally) {
4366
+ actions.push({ kind: 'npm-uninstall', npmPkg });
4367
+ } else {
4368
+ actions.push({ kind: 'skip', label: `npm: ${npmPkg} not installed globally` });
4369
+ }
4370
+ if (existsSync(ldmExtPath)) {
4371
+ actions.push({ kind: 'rm-dir', label: 'LDM extension dir', path: ldmExtPath });
4372
+ } else {
4373
+ actions.push({ kind: 'skip', label: `LDM extension dir: ${ldmExtPath} already gone` });
4374
+ }
4375
+ if (ocExtPath && existsSync(ocExtPath)) {
4376
+ actions.push({ kind: 'rm-dir', label: 'OpenClaw extension dir', path: ocExtPath });
4377
+ }
4378
+ if (entry) {
4379
+ actions.push({ kind: 'registry-remove', name: pkgName });
4380
+ } else {
4381
+ actions.push({ kind: 'skip', label: `registry: no entry for ${pkgName}` });
4382
+ }
4383
+
4384
+ const realActions = actions.filter(a => a.kind !== 'skip');
4385
+ const skips = actions.filter(a => a.kind === 'skip');
4386
+
4387
+ console.log('');
4388
+ if (realActions.length === 0) {
4389
+ for (const s of skips) console.log(` - ${s.label}`);
4390
+ console.log('');
4391
+ console.log(' Nothing to do.');
4392
+ console.log('');
4393
+ console.log(' Will NOT touch ~/.codex/ or ~/.ldm/memory/ or other extensions.');
4394
+ return;
4395
+ }
4396
+ console.log(' Will:');
4397
+ for (const a of realActions) {
4398
+ switch (a.kind) {
4399
+ case 'npm-uninstall':
4400
+ console.log(` - npm uninstall -g ${a.npmPkg}`);
4401
+ break;
4402
+ case 'rm-dir':
4403
+ console.log(` - remove ${a.label}: ${a.path}`);
4404
+ break;
4405
+ case 'registry-remove':
4406
+ console.log(` - remove registry entry for ${a.name}`);
4407
+ break;
4408
+ }
4409
+ }
4410
+ for (const s of skips) console.log(` - skipped: ${s.label}`);
4411
+ console.log('');
4412
+ console.log(' Will NOT touch ~/.codex/ or ~/.ldm/memory/ or other extensions.');
4413
+
4414
+ if (isDryRun) {
4415
+ console.log('');
4416
+ console.log(' Dry run. Nothing removed.');
4417
+ console.log('');
4418
+ console.log(' Re-run without --dry-run to apply.');
4419
+ return;
4420
+ }
4421
+
4422
+ console.log('');
4423
+ for (const a of realActions) {
4424
+ switch (a.kind) {
4425
+ case 'npm-uninstall':
4426
+ try {
4427
+ execSync(`npm uninstall -g ${a.npmPkg}`, { stdio: 'pipe', timeout: 60000 });
4428
+ console.log(` + npm uninstall -g ${a.npmPkg}`);
4429
+ } catch (e) {
4430
+ console.error(` ! npm uninstall -g ${a.npmPkg} failed: ${e.message}`);
4431
+ }
4432
+ break;
4433
+ case 'rm-dir':
4434
+ try {
4435
+ execSync(`rm -rf "${a.path}"`, { stdio: 'pipe' });
4436
+ console.log(` + removed ${a.label}: ${a.path}`);
4437
+ } catch (e) {
4438
+ console.error(` ! could not remove ${a.path}: ${e.message}`);
4439
+ }
4440
+ break;
4441
+ case 'registry-remove':
4442
+ try {
4443
+ delete registry.extensions[a.name];
4444
+ writeFileSync(registryPath, JSON.stringify(registry, null, 2) + '\n');
4445
+ console.log(` + removed registry entry for ${a.name}`);
4446
+ } catch (e) {
4447
+ console.error(` ! could not update registry: ${e.message}`);
4448
+ }
4449
+ break;
4450
+ }
4451
+ }
4452
+
4453
+ console.log('');
4454
+ console.log(' Uninstalled.');
4455
+ console.log('');
4456
+ }
4457
+
4292
4458
  // ── ldm worktree ──
4293
4459
 
4294
4460
  async function cmdWorktree() {
@@ -4619,9 +4785,17 @@ async function main() {
4619
4785
  case 'disable':
4620
4786
  await cmdDisable();
4621
4787
  break;
4622
- case 'uninstall':
4623
- await cmdUninstall();
4788
+ case 'uninstall': {
4789
+ // ldm uninstall <pkg> [--dry-run] removes one package
4790
+ // ldm uninstall removes the whole LDM OS install
4791
+ const target = args.slice(1).find(a => !a.startsWith('--'));
4792
+ if (target) {
4793
+ await cmdUninstallPackage(target);
4794
+ } else {
4795
+ await cmdUninstall();
4796
+ }
4624
4797
  break;
4798
+ }
4625
4799
  case 'worktree':
4626
4800
  await cmdWorktree();
4627
4801
  break;
@@ -68,7 +68,7 @@ If I say yes, run: ldm install --dry-run
68
68
  Show me exactly what will change. Don't install anything until I say "install".
69
69
  ```
70
70
 
71
- See [TECHNICAL.md](TECHNICAL.md) for sensors/actuators, the interface table, and real examples.
71
+ See [SPEC.md](SPEC.md) for the architecture layers, the **eight interfaces** (CLI, Module, MCP local stdio, Remote MCP, OpenClaw Plugin, Skill, Claude Code Hook, Claude Code Plugin), the install spec URL convention, track flags (alpha/beta), and the `agent.txt` distinction. See [TECHNICAL.md](TECHNICAL.md) for sensors/actuators, the interface table, and real examples.
72
72
 
73
73
  ---
74
74
 
@@ -4,7 +4,29 @@ Every tool is a sensor, an actuator, or both. Every tool should be accessible th
4
4
 
5
5
  This is the spec.
6
6
 
7
- ## The Six Interfaces
7
+ ## Architecture Layers
8
+
9
+ Five layers. Each one does one job. Together they let any AI safely consume any product.
10
+
11
+ | Layer | What it is | Where it lives |
12
+ |-------|-----------|----------------|
13
+ | **Interface** | What a product exposes (CLI, MCP, Skill, etc.). Eight kinds, listed below in canonical order. | The product repo. |
14
+ | **Installer** | Detects a product's interfaces and installs them all. `ldm install`. Stable, alpha, and beta tracks via flags. | `wip-ldm-os` (`bin/ldm.js`). |
15
+ | **Catalog** | Slug→source resolver (npm package, repo, registry/CLI matches, status) **plus** the trust surface: provenance, version pinning, permission scopes, audits, install/update/revocation. Stays human-readable and browseable as a fallback discovery surface; not the primary steering wheel. | `catalog.json` at the LDM OS root. |
16
+ | **Install Spec** | Agent-readable install runbook published at `wip.computer/install/<slug>.txt`. Track-neutral. Teaches an AI to safely check, explain, dry-run, install, update, and pair the product. | `https://wip.computer/install/<slug>.txt`. See [Install Spec](#install-spec). |
17
+ | **Stacks** | Multi-product bundles. One install brings up several products and their MCP servers. | `catalog.json.stacks`. |
18
+
19
+ Use the install spec URL to learn the safe install flow; use catalog to resolve the slug; use `ldm install` with stable/alpha/beta track flags; installer detects and installs the product's declared interfaces; stacks install bundles.
20
+
21
+ ### Primary flow
22
+
23
+ The user's path is **outcome → agent resolves services → install specs / catalog / auth → bespoke artifact**. Not "browse a plugin store and pick one." The catalog stays browseable for the times a user wants to look around, but it is no longer the steering wheel. The steering wheel is the user's stated outcome and the agent's composition.
24
+
25
+ **Personal context** (goals, preferences, prior experiments, constraints) does not come from this spec. It comes from **Memory Crystal**, a sibling LDM OS component. The universal-installer spec describes how *services* expose themselves; Memory Crystal describes how the *agent* knows you. Both feed the bespoke composition.
26
+
27
+ ## The Eight Interfaces
28
+
29
+ The canonical order is fixed: CLI (1), Module (2), MCP Server local stdio (3), Remote MCP (4), OpenClaw Plugin (5), Skill (6), Claude Code Hook (7), Claude Code Plugin (8). Local and Remote MCP sit next to each other because they are sibling transports of the same protocol. Claude Code Plugin sits last because it bundles the others.
8
30
 
9
31
  ### 1. CLI
10
32
 
@@ -45,15 +67,15 @@ An importable ES module. The programmatic interface. Other tools compose with it
45
67
  }
46
68
  ```
47
69
 
48
- ### 3. MCP Server
70
+ ### 3. MCP Server (local stdio)
49
71
 
50
- A JSON-RPC server implementing the Model Context Protocol. Any MCP-compatible agent can use it.
72
+ A JSON-RPC server implementing the Model Context Protocol over stdio. Spawned as a child process by the agent (Claude Code, Cursor, OpenClaw). For the HTTP/SSE sibling, see [#4 Remote MCP](#4-remote-mcp).
51
73
 
52
74
  **Convention:** `mcp-server.mjs` (or `.js`, `.ts`) at the repo root. Uses `@modelcontextprotocol/sdk`.
53
75
 
54
76
  **Detection:** One of `mcp-server.mjs`, `mcp-server.js`, `mcp-server.ts`, `dist/mcp-server.js` exists.
55
77
 
56
- **Install:** Add to `.mcp.json`:
78
+ **Install:** Add to `.mcp.json` with `command` + `args`:
57
79
 
58
80
  ```json
59
81
  {
@@ -64,7 +86,51 @@ A JSON-RPC server implementing the Model Context Protocol. Any MCP-compatible ag
64
86
  }
65
87
  ```
66
88
 
67
- ### 4. OpenClaw Plugin
89
+ ### 4. Remote MCP
90
+
91
+ The HTTP/SSE (or streamable HTTP) sibling of #3. Hosted at an HTTPS endpoint, not spawned locally. The transport that lights up Claude Desktop connectors, web, and mobile clients.
92
+
93
+ **Contract:** Remote MCP endpoint is **declared by package/catalog metadata** and **registered by `ldm install`**. No filesystem-sniffing fallback.
94
+
95
+ **Convention:** `mcp.remote` field in `package.json`:
96
+
97
+ ```json
98
+ {
99
+ "mcp": {
100
+ "remote": {
101
+ "url": "https://example.com/mcp",
102
+ "transport": "streamable-http",
103
+ "auth": "oauth"
104
+ }
105
+ }
106
+ }
107
+ ```
108
+
109
+ `url` may be a placeholder (`"https://__DEPLOYED_URL__"`) when the repo ships the server code and the URL is supplied by the catalog at install time.
110
+
111
+ **Detection:** `package.json.mcp.remote.url` is a string.
112
+
113
+ **Install:** Add to `.mcp.json` as a remote entry (`url` + `transport` instead of `command` + `args`). Print a one-line Claude Desktop hint so the user can also add it under Connectors. Implementation tracked in [bugs/installer/](../../ai/product/bugs/installer/2026-04-28--cc-mini--installer-remote-mcp-install.md).
114
+
115
+ ```json
116
+ {
117
+ "tool-name": {
118
+ "url": "https://example.com/mcp",
119
+ "transport": "streamable-http"
120
+ }
121
+ }
122
+ ```
123
+
124
+ **How it differs from #3:** sibling transport, not a flag on #3.
125
+
126
+ | | #3 Local stdio | #4 Remote |
127
+ |---|---|---|
128
+ | Transport | stdio (child process) | HTTPS + SSE or streamable HTTP |
129
+ | Process model | Per-session spawn | Long-running, multi-tenant |
130
+ | Auth | Trust the local process | OAuth or shared secret |
131
+ | Surfaces | Claude Code, Cursor, OpenClaw | Claude Desktop, web, mobile |
132
+
133
+ ### 5. OpenClaw Plugin
68
134
 
69
135
  A plugin for OpenClaw agents. Lifecycle hooks, tool registration, settings.
70
136
 
@@ -74,12 +140,14 @@ A plugin for OpenClaw agents. Lifecycle hooks, tool registration, settings.
74
140
 
75
141
  **Install:** Copy to `~/.openclaw/extensions/<name>/`, run `npm install --omit=dev`.
76
142
 
77
- ### 5. Skill (SKILL.md)
143
+ ### 6. Skill (SKILL.md)
78
144
 
79
145
  A markdown file that teaches agents when and how to use the tool. The instruction interface. Follows the [Agent Skills Spec](https://agentskills.io/specification).
80
146
 
81
147
  **Convention:** `SKILL.md` at the repo root. YAML frontmatter with name, description. Optional `references/` directory for context files.
82
148
 
149
+ **Platform variants:** Codex CLI reads `AGENTS.md` instead of `SKILL.md`, with the same role and the same content shape. Treat `AGENTS.md` as the Codex-flavored filename for this same interface, not a separate interface. A repo may ship both (or symlink one to the other) so it works in Codex and SKILL.md-aware agents.
150
+
83
151
  **Detection:** `SKILL.md` exists.
84
152
 
85
153
  **Install:** `SKILL.md` deployed to `~/.openclaw/skills/<name>/`. If `references/` exists, deployed alongside SKILL.md and to `settings/docs/skills/<name>/` in the workspace.
@@ -113,7 +181,7 @@ metadata:
113
181
  ---
114
182
  ```
115
183
 
116
- ### 6. Claude Code Hook
184
+ ### 7. Claude Code Hook
117
185
 
118
186
  A hook that runs during Claude Code's tool lifecycle (PreToolUse, Stop, etc.).
119
187
 
@@ -138,6 +206,58 @@ A hook that runs during Claude Code's tool lifecycle (PreToolUse, Stop, etc.).
138
206
  }
139
207
  ```
140
208
 
209
+ ### 8. Claude Code Plugin
210
+
211
+ A distributable plugin for Claude Code. Bundles skills, agents, hooks, MCP servers, and LSP servers into one installable package. Shareable via marketplaces.
212
+
213
+ **Convention:** `.claude-plugin/plugin.json` at the repo root.
214
+
215
+ **Detection:** `.claude-plugin/plugin.json` exists.
216
+
217
+ **Install:** Registered with Claude Code via `/plugin install` or marketplace.
218
+
219
+ ```
220
+ your-plugin/
221
+ ├── .claude-plugin/
222
+ │ └── plugin.json # manifest (name, version, description)
223
+ ├── skills/ # SKILL.md files
224
+ ├── agents/ # subagent definitions
225
+ ├── hooks/
226
+ │ └── hooks.json # event handlers
227
+ ├── .mcp.json # MCP server configs
228
+ └── .lsp.json # LSP server configs
229
+ ```
230
+
231
+ ```json
232
+ {
233
+ "name": "your-plugin",
234
+ "version": "1.0.0",
235
+ "description": "What it does",
236
+ "author": { "name": "Your Name" }
237
+ }
238
+ ```
239
+
240
+ ### Out of scope by design
241
+
242
+ **Disposable, agent-generated artifacts** (custom dashboards, ephemeral scripts, one-off automations, the 300-line cardio tracker someone vibe-codes in an hour) are out of scope for this spec. They are products of an agent, not Universal Interface products. The eight interfaces describe what the agent has to *compose with*. The composition output is not itself a numbered interface and never will be.
243
+
244
+ ### Worked example (compact sketch)
245
+
246
+ User says: *"Help me track my resting heart rate over the next 8 weeks. Goal: 50 → 45 bpm. Zone 2 cardio + 1 HIIT/week."*
247
+
248
+ The agent:
249
+
250
+ 1. Reads personal context from Memory Crystal (RHR baseline, prior experiments, units preference).
251
+ 2. Resolves the treadmill via catalog → install spec URL → declared Remote MCP (#4) for workout data.
252
+ 3. Pulls calendar/time semantics for the 8-week window.
253
+ 4. Composes a disposable dashboard (~300 lines).
254
+
255
+ The dashboard is **not** a Universal Interface product. It is the agent's output, assembled from agent-native sensors and actuators. Full version of this example is tracked separately ... see [bugs/installer/2026-04-28--cc-mini--installer-cardio-tracker-worked-example.md](../../ai/product/bugs/installer/2026-04-28--cc-mini--installer-cardio-tracker-worked-example.md).
256
+
257
+ ### Future considerations
258
+
259
+ *LSP as a standalone interface (#9).* LSP servers are currently surfaced via Claude Code Plugin bundles (#8) ... `.lsp.json` is part of the plugin shape. If a product ships a standalone LSP server outside a CC Plugin, we will add it as a numbered interface. Not added today because no WIP product ships one yet, and the spec should describe interfaces we install and use, not interfaces software could theoretically have.
260
+
141
261
  ## Architecture
142
262
 
143
263
  Every repo that follows this spec has the same basic structure:
@@ -153,9 +273,9 @@ your-tool/
153
273
  ai/ development process (plans, todos, notes)
154
274
  ```
155
275
 
156
- Not every tool needs all six interfaces. Build the ones that make sense.
276
+ Not every tool needs all eight interfaces. Build the ones that make sense.
157
277
 
158
- The minimum viable agent-native tool has two interfaces: **Module** (importable) and **Skill** (agent instructions). Add CLI for humans. Add MCP for agents that speak MCP. Add OpenClaw/CC Hook for specific platforms.
278
+ The minimum viable agent-native tool has two interfaces: **Module** (importable) and **Skill** (agent instructions). Add CLI for humans. Add local MCP (#3) for agents that speak MCP over stdio. Add Remote MCP (#4) when you want Claude Desktop / web / mobile to reach the same server. Add OpenClaw Plugin / CC Hook / CC Plugin for specific platforms.
159
279
 
160
280
  ## The `ai/` Folder
161
281
 
@@ -175,20 +295,73 @@ The `ai/` folder is the development process. It is not part of the published pro
175
295
 
176
296
  **Public/private split:** If a repo is public, the `ai/` folder should not ship. The recommended pattern is to maintain a private working repo (with `ai/`) and a public repo (everything except `ai/`). The public repo has everything an LLM or human needs to understand and use the tool. The `ai/` folder is operational context for the team building it.
177
297
 
178
- ## The Reference Installer
298
+ ## The Installer
179
299
 
180
- `ldm install` is the primary installer (part of LDM OS). `wip-install` is the standalone fallback. Both scan a repo, detect which interfaces exist, and install them all. One command.
300
+ `ldm install` is the primary installer (part of LDM OS). `wip-install` is the standalone fallback. Both scan a repo or slug, detect which interfaces exist, and install them all. One command.
181
301
 
182
302
  ```bash
183
- ldm install /path/to/repo # local (via LDM OS)
184
- ldm install org/repo # from GitHub
185
- ldm install org/repo --dry-run # detect only
186
- wip-install /path/to/repo # standalone fallback (bootstraps LDM OS if needed)
187
- wip-install --json /path/to/repo # JSON output
303
+ ldm install /path/to/repo # local (via LDM OS)
304
+ ldm install org/repo # from GitHub
305
+ ldm install <slug> # from catalog (stable, default)
306
+ ldm install --alpha <slug> # alpha (validation track)
307
+ ldm install --beta <slug> # beta (validation track)
308
+ ldm install <slug> --dry-run # detect only, no changes
309
+ wip-install /path/to/repo # standalone fallback (bootstraps LDM OS if needed)
310
+ wip-install --json /path/to/repo # JSON output
188
311
  ```
189
312
 
313
+ Tracks select the npm dist-tag (or git ref) the installer pulls from. The same install spec URL covers all three; the AI follows the spec, the user (or releasing agent) picks the track via flag.
314
+
190
315
  For toolbox repos (with a `tools/` directory containing sub-tools), the installer enters toolbox mode and installs each sub-tool.
191
316
 
317
+ ## Install Spec
318
+
319
+ An **install spec** is an agent-readable install runbook published at a stable URL:
320
+
321
+ ```
322
+ https://wip.computer/install/<slug>.txt
323
+ ```
324
+
325
+ The contract is the URL and the behavior, not the file origin. An install spec can be generated from `SKILL.md`, mirrored from it, or live alongside it. What matters is that any AI can fetch it, read it, and walk a user through a safe install.
326
+
327
+ ### Behavior contract
328
+
329
+ The spec teaches an AI to:
330
+
331
+ 1. **Check** whether the product is already installed and at what version.
332
+ 2. **Explain** what the product is, what it installs, and what changes for this AI and the user's other AIs.
333
+ 3. **Dry-run** so the user sees what will change before anything is touched.
334
+ 4. **Install** only after explicit user consent.
335
+ 5. **Update** an existing install (skip steps the user already did).
336
+ 6. **Pair** any post-install steps (passkey, device pairing, gateway start, etc.) with explicit consent at each step.
337
+
338
+ ### Tracks
339
+
340
+ One install spec covers all release tracks. The user picks via flag:
341
+
342
+ | Track | Flag | Audience |
343
+ |-------|------|----------|
344
+ | Stable | (default) `ldm install <slug>` | End users. Owner-dogfooded. |
345
+ | Beta | `ldm install --beta <slug>` | Validation; agents may install. |
346
+ | Alpha | `ldm install --alpha <slug>` | Validation; agents may install. |
347
+
348
+ The spec text itself is track-neutral. Tracks are an installer concern, not a copy concern.
349
+
350
+ ### Install spec vs `agent.txt`
351
+
352
+ These are related, not the same. Both are agent-readable. They sit at different scopes:
353
+
354
+ - **`agent.txt`** ... site- or product-level entrypoint for agents. "What can agents do here? What routes exist? What policies apply?" Lives at the root of a site or product (e.g. `wip.computer/agent.txt`).
355
+ - **`install/<slug>.txt`** ... per-product install runbook. "How should an agent safely check, explain, dry-run, install, update, and pair this product?" Lives under `wip.computer/install/`.
356
+
357
+ `agent.txt` can point agents at install specs. An install spec does not replace `agent.txt`.
358
+
359
+ ### Worked example: Codex Remote Control
360
+
361
+ Install spec: [`https://wip.computer/install/wip-codex-remote-control.txt`](https://wip.computer/install/wip-codex-remote-control.txt).
362
+
363
+ The user pastes one prompt into Codex (or any AI). The AI fetches the install spec, checks installed state, explains the product, runs `ldm install --dry-run wip-codex-remote-control`, and only installs (and starts the daemon, and pairs the phone) after the user says yes at each step. Tracks are selected by flag against the same URL.
364
+
192
365
  ## Examples
193
366
 
194
367
  ### AI DevOps Toolbox (this repo)
@@ -18,23 +18,24 @@ Every tool is a sensor, an actuator, or both. Every tool should be accessible th
18
18
  - Guard a file from edits (wip-file-guard)
19
19
  - Generate a video (wip-grok generate_video)
20
20
 
21
- ## The Seven Interfaces
21
+ ## The Eight Interfaces
22
22
 
23
- Agents don't all speak the same language. Some run shell commands. Some import modules. Some talk MCP. Some read markdown instructions.
23
+ Agents don't all speak the same language. Some run shell commands. Some import modules. Some talk MCP over stdio. Some talk MCP over HTTPS. Some read markdown instructions.
24
24
 
25
25
  So every tool should expose multiple interfaces into the same core logic:
26
26
 
27
- | Interface | What | Who uses it |
28
- |-----------|------|-------------|
29
- | **CLI** | Shell command | Humans, any agent with bash |
30
- | **Module** | ES import | Other tools, scripts |
31
- | **MCP Server** | JSON-RPC over stdio | Claude Code, Cursor, any MCP client |
32
- | **OpenClaw Plugin** | Lifecycle hooks + tools | OpenClaw agents |
33
- | **Skill** | Markdown instructions (SKILL.md) | Any agent that reads files |
34
- | **Claude Code Hook** | PreToolUse/Stop events | Claude Code |
35
- | **Claude Code Plugin** | Distributable package (skills, agents, hooks, MCP, LSP) | Claude Code marketplace |
27
+ | # | Interface | What | Who uses it |
28
+ |---|-----------|------|-------------|
29
+ | 1 | **CLI** | Shell command | Humans, any agent with bash |
30
+ | 2 | **Module** | ES import | Other tools, scripts |
31
+ | 3 | **MCP Server (local stdio)** | JSON-RPC over stdio | Claude Code, Cursor, OpenClaw |
32
+ | 4 | **Remote MCP** | JSON-RPC over HTTPS (SSE / streamable HTTP) | Claude Desktop, web, mobile |
33
+ | 5 | **OpenClaw Plugin** | Lifecycle hooks + tools | OpenClaw agents |
34
+ | 6 | **Skill** | Markdown instructions (SKILL.md) | Any agent that reads files |
35
+ | 7 | **Claude Code Hook** | PreToolUse/Stop events | Claude Code |
36
+ | 8 | **Claude Code Plugin** | Distributable package (skills, agents, hooks, MCP, LSP) | Claude Code marketplace |
36
37
 
37
- Not every tool needs all seven. Build what makes sense. But the more interfaces you expose, the more agents can use your tool.
38
+ Not every tool needs all eight. Build what makes sense. But the more interfaces you expose, the more agents can use your tool. Local and Remote MCP are sibling transports of the same protocol; they sit next to each other so the relationship is obvious.
38
39
 
39
40
  ### 1. CLI
40
41
 
@@ -75,15 +76,15 @@ An importable ES module. The programmatic interface. Other tools compose with it
75
76
  }
76
77
  ```
77
78
 
78
- ### 3. MCP Server
79
+ ### 3. MCP Server (local stdio)
79
80
 
80
- A JSON-RPC server implementing the Model Context Protocol. Any MCP-compatible agent can use it.
81
+ A JSON-RPC server implementing the Model Context Protocol over stdio. Spawned as a child process by the agent. For the HTTP/SSE sibling, see [#4 Remote MCP](#4-remote-mcp).
81
82
 
82
83
  **Convention:** `mcp-server.mjs` (or `.js`, `.ts`) at the repo root. Uses `@modelcontextprotocol/sdk`.
83
84
 
84
85
  **Detection:** One of `mcp-server.mjs`, `mcp-server.js`, `mcp-server.ts`, `dist/mcp-server.js` exists.
85
86
 
86
- **Install:** Add to `.mcp.json`:
87
+ **Install:** Add to `.mcp.json` with `command` + `args`:
87
88
 
88
89
  ```json
89
90
  {
@@ -94,7 +95,46 @@ A JSON-RPC server implementing the Model Context Protocol. Any MCP-compatible ag
94
95
  }
95
96
  ```
96
97
 
97
- ### 4. OpenClaw Plugin
98
+ ### 4. Remote MCP
99
+
100
+ The HTTP/SSE (or streamable HTTP) sibling of #3. Hosted at an HTTPS endpoint, not spawned locally. The transport that lights up Claude Desktop connectors, web, and mobile clients.
101
+
102
+ **Contract:** Remote MCP endpoint is **declared by package/catalog metadata** and **registered by `ldm install`**. No filesystem-sniffing fallback.
103
+
104
+ **Convention:** `mcp.remote` field in `package.json`:
105
+
106
+ ```json
107
+ {
108
+ "mcp": {
109
+ "remote": {
110
+ "url": "https://example.com/mcp",
111
+ "transport": "streamable-http",
112
+ "auth": "oauth"
113
+ }
114
+ }
115
+ }
116
+ ```
117
+
118
+ `url` may be a placeholder (`"https://__DEPLOYED_URL__"`) when the repo ships the server code and the catalog supplies the URL at install time.
119
+
120
+ **Detection:** `package.json.mcp.remote.url` is a string.
121
+
122
+ **Install:** Add to `.mcp.json` as a remote entry, plus print a one-line Claude Desktop hint:
123
+
124
+ ```json
125
+ {
126
+ "tool-name": {
127
+ "url": "https://example.com/mcp",
128
+ "transport": "streamable-http"
129
+ }
130
+ }
131
+ ```
132
+
133
+ **Auth:** `none` writes the URL as-is. `shared-secret` prompts the user once and stores out-of-band. `oauth` is gated behind a follow-up ticket; first cut prints a TODO.
134
+
135
+ **Implementation status:** detection + install action are tracked in `ai/product/bugs/installer/` (see [Remote MCP detection](../../ai/product/bugs/installer/2026-04-28--cc-mini--installer-remote-mcp-detection.md) and [Remote MCP install](../../ai/product/bugs/installer/2026-04-28--cc-mini--installer-remote-mcp-install.md)). The spec is canonical now; the detector and installer catch up next.
136
+
137
+ ### 5. OpenClaw Plugin
98
138
 
99
139
  A plugin for OpenClaw agents. Lifecycle hooks, tool registration, settings.
100
140
 
@@ -104,12 +144,14 @@ A plugin for OpenClaw agents. Lifecycle hooks, tool registration, settings.
104
144
 
105
145
  **Install:** Copy to `~/.openclaw/extensions/<name>/`, run `npm install --omit=dev`.
106
146
 
107
- ### 5. Skill (SKILL.md)
147
+ ### 6. Skill (SKILL.md)
108
148
 
109
149
  A markdown file that teaches agents when and how to use the tool. The instruction interface. Follows the [Agent Skills Spec](https://agentskills.io/specification).
110
150
 
111
151
  **Convention:** `SKILL.md` at the repo root. Optional `references/` directory for context files.
112
152
 
153
+ **Platform variants:** Codex CLI reads `AGENTS.md` with the same role and content shape. Treat as the Codex-flavored filename for this same interface, not a separate one.
154
+
113
155
  **Detection:** `SKILL.md` exists.
114
156
 
115
157
  **Install:** `ldm install` deploys `SKILL.md` to `~/.openclaw/skills/<name>/`. If `references/` exists, it is deployed alongside and also to `settings/docs/skills/<name>/` in the workspace (so all agents can read them).
@@ -132,7 +174,7 @@ metadata:
132
174
  ---
133
175
  ```
134
176
 
135
- ### 6. Claude Code Hook
177
+ ### 7. Claude Code Hook
136
178
 
137
179
  A hook that runs during Claude Code's tool lifecycle (PreToolUse, Stop, etc.).
138
180
 
@@ -157,7 +199,7 @@ A hook that runs during Claude Code's tool lifecycle (PreToolUse, Stop, etc.).
157
199
  }
158
200
  ```
159
201
 
160
- ### 7. Claude Code Plugin
202
+ ### 8. Claude Code Plugin
161
203
 
162
204
  A distributable plugin for Claude Code. Bundles skills, agents, hooks, MCP servers, and LSP servers into one installable package. Shareable via marketplaces.
163
205
 
@@ -188,6 +230,14 @@ your-plugin/
188
230
  }
189
231
  ```
190
232
 
233
+ ### Out of scope by design
234
+
235
+ Disposable, agent-generated artifacts (custom dashboards, ephemeral scripts, one-off automations) are out of scope for this spec. They are products of an agent, not Universal Interface products. The eight interfaces describe what the agent has to *compose with*. Worked example: [SPEC.md ... Worked example (compact sketch)](SPEC.md#worked-example-compact-sketch).
236
+
237
+ ### Future considerations
238
+
239
+ *LSP as a standalone interface (#9).* LSP servers are currently surfaced via Claude Code Plugin bundles (#8) ... `.lsp.json` is part of the plugin shape. If a product ships a standalone LSP server outside a CC Plugin, we will add it as a numbered interface. Not added today.
240
+
191
241
  ## How to Build It
192
242
 
193
243
  The architecture is simple. Four files:
@@ -206,30 +256,33 @@ This means one codebase, one set of tests, multiple interfaces.
206
256
 
207
257
  ## Install Prompt Template
208
258
 
209
- Every product gets an install prompt. Paste it into any AI. The AI reads the spec, explains it, checks what's installed, and walks you through a dry run.
259
+ Every product gets an install prompt. Paste it into any AI. The AI reads the install spec, explains the product, checks what's installed, and walks you through a dry run before touching anything.
260
+
261
+ The install spec URL convention, behavior contract, track flags, and `agent.txt` distinction are defined in [SPEC.md ... Install Spec](SPEC.md#install-spec). This is the canonical paste-into-an-AI form:
210
262
 
211
263
  ```
212
- Read wip.computer/install/{URL}
264
+ Read https://wip.computer/install/<slug>.txt
213
265
 
214
- Then explain:
215
- 1. What is {name of product}?
266
+ Check if <product name> is already installed. If it is, run
267
+ `ldm install --dry-run <slug>` and show me what I have and what's new.
268
+
269
+ If not, walk me through setup and explain:
270
+ 1. What is <product name>?
216
271
  2. What does it install on my system?
217
272
  3. What changes for us? (this AI)
218
273
  4. What changes across all my AIs?
219
274
 
220
- Check if {name of product} is already installed.
221
-
222
- If it is, show me what I have and what's new.
223
-
224
275
  Then ask:
225
276
  - Do you have questions?
226
277
  - Want to see a dry run?
227
278
 
228
- If I say yes, run: {product-init} init --dry-run
279
+ If I say yes, run: ldm install --dry-run <slug>
229
280
 
230
281
  Show me exactly what will change. Don't install anything until I say "install".
231
282
  ```
232
283
 
284
+ Tracks: append `--alpha` or `--beta` to install a prerelease (e.g. `ldm install --beta <slug>`). The same install spec URL covers all tracks.
285
+
233
286
  ## The `ai/` Folder
234
287
 
235
288
  Every repo should have an `ai/` folder. This is where agents and humans collaborate on the project ... plans, todos, dev updates, research notes, conversations.
@@ -308,7 +361,8 @@ ldm install # update all
308
361
  |---------|-----------|---------------|
309
362
  | `package.json` with `bin` | CLI | `npm install -g` |
310
363
  | `main` or `exports` in `package.json` | Module | Reports import path |
311
- | `mcp-server.mjs` | MCP | Prints `.mcp.json` config |
364
+ | `mcp-server.mjs` | MCP (local stdio) | Adds `command` + `args` entry to `.mcp.json` |
365
+ | `mcp.remote.url` in `package.json` | Remote MCP | Adds `url` + `transport` entry to `.mcp.json`; prints Claude Desktop hint. **Implementation in flight ([ticket](../../ai/product/bugs/installer/2026-04-28--cc-mini--installer-remote-mcp-detection.md)).** |
312
366
  | `openclaw.plugin.json` | OpenClaw | Copies to `~/.openclaw/extensions/` |
313
367
  | `SKILL.md` | Skill | Reports path |
314
368
  | `guard.mjs` or `claudeCode.hook` | CC Hook | Adds to `~/.claude/settings.json` |
@@ -323,6 +377,7 @@ ldm install # update all
323
377
  | [wip-file-guard](https://github.com/wipcomputer/wip-ai-devops-toolbox/tree/main/tools/wip-file-guard) | Actuator | CLI + OpenClaw + CC Hook | Protect files from AI edits |
324
378
  | [wip-healthcheck](https://github.com/wipcomputer/wip-healthcheck) | Sensor | CLI + Module | System health monitoring |
325
379
  | [wip-markdown-viewer](https://github.com/wipcomputer/wip-markdown-viewer) | Actuator | CLI + Module | Live markdown viewer |
380
+ | [wip-codex-remote-control](https://wip.computer/install/wip-codex-remote-control.txt) | Sensor + Actuator | CLI + Module + MCP + Skill + Install Spec | Pair phone with Codex; AI-led install via published install spec. Worked example of the install-spec URL pattern. |
326
381
 
327
382
  ## Supported Tools
328
383
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wipcomputer/wip-ldm-os",
3
- "version": "0.4.83",
3
+ "version": "0.4.85-alpha.1",
4
4
  "type": "module",
5
5
  "description": "LDM OS: identity, memory, and sovereignty infrastructure for AI agents",
6
6
  "engines": {
@@ -0,0 +1,109 @@
1
+ # Codex Remote Control relay
2
+ #
3
+ # Routes /api/codex-relay/* to the wip-mcp Node app at 127.0.0.1:18800.
4
+ # Without these blocks nginx falls back to /index.html and the phone-side
5
+ # bootstrap + ws-ticket calls receive HTML, which breaks the E2EE handshake
6
+ # and the relay attach flow.
7
+ #
8
+ # Owners: this snippet pairs with src/hosted-mcp/server.mjs codex-relay
9
+ # routes and the kaleidoscope-private phone surface. See
10
+ # wip-ldm-os-private/ai/product/plans-prds/codex-remote-control/
11
+ # for the full architecture.
12
+ #
13
+ # Include from inside the wip.computer server block.
14
+
15
+ # ── HTTP routes ──────────────────────────────────────────────────────
16
+ # bootstrap (GET) ... phone reads daemon E2EE pubkey + presence
17
+ location /api/codex-relay/bootstrap/ {
18
+ proxy_pass http://127.0.0.1:18800;
19
+ proxy_http_version 1.1;
20
+ proxy_set_header Host $host;
21
+ proxy_set_header X-Real-IP $remote_addr;
22
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
23
+ proxy_set_header X-Forwarded-Proto $scheme;
24
+ }
25
+
26
+ # ws-ticket (POST) ... single-use, route-bound, 60s TTL
27
+ location /api/codex-relay/ws-ticket {
28
+ proxy_pass http://127.0.0.1:18800;
29
+ proxy_http_version 1.1;
30
+ proxy_set_header Host $host;
31
+ proxy_set_header X-Real-IP $remote_addr;
32
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
33
+ proxy_set_header X-Forwarded-Proto $scheme;
34
+ }
35
+
36
+ # state (GET) ... daemon presence diagnostic
37
+ location /api/codex-relay/state {
38
+ proxy_pass http://127.0.0.1:18800;
39
+ proxy_http_version 1.1;
40
+ proxy_set_header Host $host;
41
+ proxy_set_header X-Real-IP $remote_addr;
42
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
43
+ proxy_set_header X-Forwarded-Proto $scheme;
44
+ }
45
+
46
+ # pair-init (POST) ... codex-daemon link starts here
47
+ location /api/codex-relay/pair-init {
48
+ proxy_pass http://127.0.0.1:18800;
49
+ proxy_http_version 1.1;
50
+ proxy_set_header Host $host;
51
+ proxy_set_header X-Real-IP $remote_addr;
52
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
53
+ proxy_set_header X-Forwarded-Proto $scheme;
54
+ }
55
+
56
+ # pair-status (GET) ... daemon polls during pair flow
57
+ location /api/codex-relay/pair-status/ {
58
+ proxy_pass http://127.0.0.1:18800;
59
+ proxy_http_version 1.1;
60
+ proxy_set_header Host $host;
61
+ proxy_set_header X-Real-IP $remote_addr;
62
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
63
+ proxy_set_header X-Forwarded-Proto $scheme;
64
+ }
65
+
66
+ # pair-complete (POST) ... phone -> server after passkey + code
67
+ location /api/codex-relay/pair-complete {
68
+ proxy_pass http://127.0.0.1:18800;
69
+ proxy_http_version 1.1;
70
+ proxy_set_header Host $host;
71
+ proxy_set_header X-Real-IP $remote_addr;
72
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
73
+ proxy_set_header X-Forwarded-Proto $scheme;
74
+ }
75
+
76
+ # ── WebSocket routes ────────────────────────────────────────────────
77
+ # Standard nginx WebSocket pattern: Upgrade + Connection "upgrade", long
78
+ # read/send timeout for daemon's persistent socket, buffering off so
79
+ # streamed Codex events don't pool.
80
+
81
+ # Phone side ... attached with ?ticket=<single-use>
82
+ location /api/codex-relay/web/ {
83
+ proxy_pass http://127.0.0.1:18800;
84
+ proxy_http_version 1.1;
85
+ proxy_set_header Upgrade $http_upgrade;
86
+ proxy_set_header Connection "upgrade";
87
+ proxy_set_header Host $host;
88
+ proxy_set_header X-Real-IP $remote_addr;
89
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
90
+ proxy_set_header X-Forwarded-Proto $scheme;
91
+ proxy_read_timeout 86400;
92
+ proxy_send_timeout 86400;
93
+ proxy_buffering off;
94
+ }
95
+
96
+ # Daemon side ... long-lived presence socket from the user's Mac
97
+ location /api/codex-relay/daemon {
98
+ proxy_pass http://127.0.0.1:18800;
99
+ proxy_http_version 1.1;
100
+ proxy_set_header Upgrade $http_upgrade;
101
+ proxy_set_header Connection "upgrade";
102
+ proxy_set_header Host $host;
103
+ proxy_set_header X-Real-IP $remote_addr;
104
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
105
+ proxy_set_header X-Forwarded-Proto $scheme;
106
+ proxy_read_timeout 86400;
107
+ proxy_send_timeout 86400;
108
+ proxy_buffering off;
109
+ }
@@ -7,6 +7,68 @@ server {
7
7
  access_log /var/log/nginx/wip.computer.access.log;
8
8
  error_log /var/log/nginx/wip.computer.error.log;
9
9
 
10
+ # Docs redirect to docs.wip.computer
11
+ location = /doc {
12
+ return 301 https://docs.wip.computer;
13
+ }
14
+ location = /docs {
15
+ return 301 https://docs.wip.computer;
16
+ }
17
+ location /docs/ {
18
+ return 301 https://docs.wip.computer;
19
+ }
20
+
21
+ # MCP server
22
+ # OAuth 2.0 for Claude iOS connector
23
+ include snippets/mcp-oauth.conf;
24
+ include snippets/mcp-server.conf;
25
+
26
+ # Codex Remote Control relay (HTTP + WSS proxy_pass to wip-mcp Node app
27
+ # at 127.0.0.1:18800; covers /api/codex-relay/bootstrap, ws-ticket, state,
28
+ # pair-init/status/complete, web/<tid>, daemon).
29
+ include snippets/codex-relay.conf;
30
+
31
+ # Codex Remote Control phone surface
32
+ # The Next.js app at kaleidoscope.wip.computer (port 3001) renders
33
+ # /codex-remote-control/[threadId]. The MCP tool's auth URL uses
34
+ # wip.computer as the origin, so wip.computer/codex-remote-control/<tid>
35
+ # must reach the same Next.js app. WebSocket Upgrade headers included
36
+ # so live-reload / RSC streams work cleanly.
37
+ location /codex-remote-control/ {
38
+ proxy_pass http://127.0.0.1:3001;
39
+ proxy_http_version 1.1;
40
+ proxy_set_header Upgrade $http_upgrade;
41
+ proxy_set_header Connection "upgrade";
42
+ proxy_set_header Host $host;
43
+ proxy_set_header X-Real-IP $remote_addr;
44
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
45
+ proxy_set_header X-Forwarded-Proto $scheme;
46
+ }
47
+
48
+ # Health check
49
+ location /health {
50
+ proxy_pass http://127.0.0.1:18800/health;
51
+ proxy_http_version 1.1;
52
+ }
53
+
54
+
55
+ # Demo pages (static files)
56
+ location /demo {
57
+ alias /var/www/wip.computer/app/mcp-server/demo;
58
+ index index.html;
59
+ try_files $uri $uri/ $uri/index.html =404;
60
+ }
61
+
62
+ # Demo API (proxied to MCP server)
63
+ location /demo/api/ {
64
+ proxy_pass http://127.0.0.1:18800/demo/api/;
65
+ proxy_http_version 1.1;
66
+ proxy_set_header Host $host;
67
+ proxy_set_header X-Real-IP $remote_addr;
68
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
69
+ proxy_set_header X-Forwarded-Proto $scheme;
70
+ }
71
+
10
72
  location / {
11
73
  autoindex off;
12
74
  try_files $uri $uri/ /index.html;
@@ -1,254 +0,0 @@
1
- <!DOCTYPE html>
2
- <html lang="en">
3
- <head>
4
- <meta charset="utf-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
6
- <title>Codex remote control</title>
7
- <style>
8
- *, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }
9
- :root {
10
- --bg: #FFFDF5;
11
- --bg-event: #F5F3ED;
12
- --bg-tool: #F0EDE6;
13
- --text: #1a1a1a;
14
- --text-muted: #8a8580;
15
- --accent: #0033FF;
16
- --danger: #b00020;
17
- --border: #E0DDD6;
18
- --user-bubble: #E8F0FE;
19
- --font: -apple-system, BlinkMacSystemFont, "SF Pro Text", system-ui, sans-serif;
20
- --mono: ui-monospace, "SF Mono", Menlo, monospace;
21
- }
22
- html, body { height: 100%; font-family: var(--font); background: var(--bg); color: var(--text); }
23
- body { display: flex; flex-direction: column; }
24
- header { padding: 12px 16px; padding-top: calc(12px + env(safe-area-inset-top, 0px)); border-bottom: 1px solid var(--border); display: flex; align-items: center; gap: 10px; }
25
- header .id { flex: 1; font-size: 13px; color: var(--text-muted); font-family: var(--mono); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
26
- header .dot { width: 8px; height: 8px; border-radius: 50%; background: var(--text-muted); }
27
- header .dot.online { background: #2ea44f; }
28
- header .dot.offline { background: var(--danger); }
29
- main { flex: 1; overflow-y: auto; padding: 16px; padding-bottom: 0; -webkit-overflow-scrolling: touch; }
30
- .event { margin-bottom: 12px; padding: 12px 14px; border-radius: 10px; background: var(--bg-event); font-size: 14px; line-height: 1.45; }
31
- .event .meta { font-size: 11px; color: var(--text-muted); font-family: var(--mono); margin-bottom: 4px; text-transform: uppercase; letter-spacing: 0.05em; }
32
- .event.user { background: var(--user-bubble); }
33
- .event.agent_message { background: var(--bg); border: 1px solid var(--border); }
34
- .event.command_execution { background: var(--bg-tool); font-family: var(--mono); white-space: pre-wrap; word-break: break-all; }
35
- .event.command_execution.failed { border-left: 3px solid var(--danger); }
36
- .event.error { background: #fff0f0; border: 1px solid #f0c0c0; color: var(--danger); }
37
- .event.system { background: transparent; color: var(--text-muted); font-size: 12px; padding: 6px 0; text-align: center; }
38
- .event pre { font-family: var(--mono); white-space: pre-wrap; word-break: break-word; font-size: 13px; }
39
- footer { padding: 12px; padding-bottom: calc(12px + env(safe-area-inset-bottom, 0px)); border-top: 1px solid var(--border); background: var(--bg); }
40
- .composer { display: flex; gap: 8px; align-items: flex-end; }
41
- textarea {
42
- flex: 1; min-height: 44px; max-height: 120px; padding: 12px;
43
- border: 1px solid var(--border); border-radius: 10px;
44
- background: var(--bg); color: var(--text); font-family: var(--font); font-size: 16px;
45
- resize: none;
46
- }
47
- textarea:focus { outline: 2px solid var(--accent); outline-offset: -1px; }
48
- button { padding: 12px 16px; border: none; border-radius: 10px; font-family: var(--font); font-size: 14px; font-weight: 600; cursor: pointer; -webkit-tap-highlight-color: transparent; }
49
- button:active { transform: scale(0.97); }
50
- button:disabled { opacity: 0.4; cursor: not-allowed; }
51
- .btn-send { background: var(--accent); color: white; }
52
- .btn-stop { background: var(--danger); color: white; }
53
- </style>
54
- </head>
55
- <body>
56
- <header>
57
- <div class="dot" id="presence" title="connecting"></div>
58
- <div class="id" id="threadId">...</div>
59
- <button id="stopBtn" class="btn-stop" type="button" disabled>Stop</button>
60
- </header>
61
- <main id="log"></main>
62
- <footer>
63
- <form class="composer" id="composer">
64
- <textarea id="prompt" rows="1" placeholder="Tell Codex what to do..." autocomplete="off"></textarea>
65
- <button type="submit" class="btn-send" id="sendBtn">Send</button>
66
- </form>
67
- </footer>
68
- <script>
69
- function getApiKey() { return sessionStorage.getItem("wip_api_key"); }
70
- function getHandle() { return sessionStorage.getItem("wip_handle") || ""; }
71
-
72
- function ensureSignedIn() {
73
- if (!getApiKey()) {
74
- location.href = "/app/login.html?next=" + encodeURIComponent(location.pathname);
75
- return false;
76
- }
77
- return true;
78
- }
79
-
80
- function parsePath() {
81
- // /:handle/codex-remote-control/:threadId
82
- const m = location.pathname.match(/^\/([^/]+)\/codex-remote-control\/([^/]+)\/?$/);
83
- if (!m) return null;
84
- return { handle: decodeURIComponent(m[1]), threadId: decodeURIComponent(m[2]) };
85
- }
86
-
87
- function setPresence(state) {
88
- const dot = document.getElementById("presence");
89
- dot.classList.remove("online", "offline");
90
- if (state === "online") dot.classList.add("online");
91
- if (state === "offline") dot.classList.add("offline");
92
- dot.title = state;
93
- }
94
-
95
- function appendEvent(html, kind) {
96
- const log = document.getElementById("log");
97
- const div = document.createElement("div");
98
- div.className = "event " + (kind || "");
99
- div.innerHTML = html;
100
- log.appendChild(div);
101
- log.scrollTop = log.scrollHeight;
102
- return div;
103
- }
104
-
105
- function escapeHtml(s) {
106
- return String(s).replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", "\"": "&quot;", "'": "&#39;" }[c]));
107
- }
108
-
109
- function renderItem(item) {
110
- if (item.type === "agent_message") {
111
- return appendEvent('<div class="meta">codex</div>' + escapeHtml(item.text || "").replace(/\n/g, "<br>"), "agent_message");
112
- }
113
- if (item.type === "command_execution") {
114
- const status = (item.status || "").toString();
115
- const out = item.aggregated_output ? '\n\n' + item.aggregated_output : "";
116
- return appendEvent(
117
- '<div class="meta">$ ' + escapeHtml(status) + (item.exit_code != null ? " (exit " + item.exit_code + ")" : "") + '</div>' +
118
- '<pre>' + escapeHtml(item.command || "") + escapeHtml(out) + '</pre>',
119
- "command_execution" + (status === "failed" ? " failed" : ""),
120
- );
121
- }
122
- if (item.type === "reasoning") {
123
- return appendEvent('<div class="meta">reasoning</div>' + escapeHtml(item.text || ""), "reasoning");
124
- }
125
- return appendEvent('<div class="meta">' + escapeHtml(item.type || "item") + '</div><pre>' + escapeHtml(JSON.stringify(item, null, 2)) + '</pre>', "item");
126
- }
127
-
128
- let ws = null;
129
- let pendingId = 1;
130
-
131
- function send(req) {
132
- if (!ws || ws.readyState !== WebSocket.OPEN) return;
133
- ws.send(JSON.stringify(req));
134
- }
135
-
136
- function connect(threadId) {
137
- const apiKey = getApiKey();
138
- const proto = location.protocol === "https:" ? "wss:" : "ws:";
139
- const url = proto + "//" + location.host + "/api/codex-relay/web/" + encodeURIComponent(threadId) + "?token=" + encodeURIComponent(apiKey);
140
- ws = new WebSocket(url);
141
-
142
- ws.addEventListener("open", () => {
143
- setPresence("online");
144
- appendEvent("connected. open this thread in Codex on your Mac if it's not already.", "system");
145
- });
146
-
147
- ws.addEventListener("close", (ev) => {
148
- setPresence("offline");
149
- appendEvent("disconnected (code " + ev.code + ")", "system");
150
- });
151
-
152
- ws.addEventListener("error", () => {
153
- setPresence("offline");
154
- });
155
-
156
- ws.addEventListener("message", (ev) => {
157
- let msg;
158
- try { msg = JSON.parse(ev.data); } catch { return; }
159
- if (msg.type === "session.started") {
160
- // Daemon assigned a temp id; the real thread id will arrive in thread.started.
161
- return;
162
- }
163
- if (msg.type === "session.event") {
164
- const evt = msg.event || {};
165
- if (evt.type === "thread.started") {
166
- // ok
167
- return;
168
- }
169
- if (evt.type === "turn.started") {
170
- document.getElementById("stopBtn").disabled = false;
171
- return;
172
- }
173
- if (evt.type === "item.completed" && evt.item) {
174
- renderItem(evt.item);
175
- return;
176
- }
177
- if (evt.type === "item.started") {
178
- return; // skip; we render on completed for now
179
- }
180
- if (evt.type === "turn.completed") {
181
- document.getElementById("stopBtn").disabled = true;
182
- const u = evt.usage;
183
- if (u) appendEvent("turn complete (" + (u.input_tokens || 0) + " in / " + (u.output_tokens || 0) + " out)", "system");
184
- else appendEvent("turn complete", "system");
185
- return;
186
- }
187
- if (evt.type === "turn.failed") {
188
- document.getElementById("stopBtn").disabled = true;
189
- appendEvent("turn failed: " + (evt.error && evt.error.message ? evt.error.message : "unknown"), "error");
190
- return;
191
- }
192
- return;
193
- }
194
- if (msg.type === "ack") return;
195
- if (msg.type === "error") {
196
- appendEvent("error: " + (msg.message || ""), "error");
197
- return;
198
- }
199
- });
200
- }
201
-
202
- function init() {
203
- if (!ensureSignedIn()) return;
204
- const parsed = parsePath();
205
- if (!parsed) {
206
- appendEvent("Invalid URL. Expected /<handle>/codex-remote-control/<thread-id>.", "error");
207
- return;
208
- }
209
- document.getElementById("threadId").textContent = parsed.threadId;
210
- connect(parsed.threadId);
211
-
212
- // Open or attach to the session on the daemon. session.start returns a temp
213
- // sessionId; the actual thread.id arrives via thread.started in the stream.
214
- setTimeout(() => {
215
- send({ type: "session.start", id: "open-" + (pendingId += 1) });
216
- }, 250);
217
-
218
- document.getElementById("composer").addEventListener("submit", (ev) => {
219
- ev.preventDefault();
220
- const input = document.getElementById("prompt");
221
- const text = input.value.trim();
222
- if (!text) return;
223
- input.value = "";
224
- appendEvent('<div class="meta">you</div>' + escapeHtml(text), "user");
225
- send({
226
- type: "session.send",
227
- id: "send-" + (pendingId += 1),
228
- sessionId: parsed.threadId,
229
- prompt: text,
230
- });
231
- });
232
-
233
- document.getElementById("stopBtn").addEventListener("click", () => {
234
- send({ type: "session.interrupt", id: "stop-" + (pendingId += 1), sessionId: parsed.threadId });
235
- });
236
-
237
- // Submit on Cmd+Enter / Ctrl+Enter; auto-resize.
238
- const ta = document.getElementById("prompt");
239
- ta.addEventListener("input", () => {
240
- ta.style.height = "auto";
241
- ta.style.height = Math.min(120, ta.scrollHeight) + "px";
242
- });
243
- ta.addEventListener("keydown", (ev) => {
244
- if ((ev.metaKey || ev.ctrlKey) && ev.key === "Enter") {
245
- ev.preventDefault();
246
- document.getElementById("composer").requestSubmit();
247
- }
248
- });
249
- }
250
-
251
- init();
252
- </script>
253
- </body>
254
- </html>