@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 +1 -1
- package/bin/ldm.js +178 -4
- package/docs/universal-installer/README.md +1 -1
- package/docs/universal-installer/SPEC.md +189 -16
- package/docs/universal-installer/TECHNICAL.md +84 -29
- package/package.json +1 -1
- package/src/hosted-mcp/nginx/codex-relay.conf +109 -0
- package/src/hosted-mcp/nginx/wip.computer.conf +62 -0
- package/src/hosted-mcp/app/codex-remote-control/index.html +0 -254
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.
|
|
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 ${
|
|
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 ${
|
|
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
|
-
|
|
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
|
-
##
|
|
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.
|
|
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.
|
|
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
|
-
###
|
|
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
|
-
###
|
|
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
|
|
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
|
|
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
|
|
184
|
-
ldm install org/repo
|
|
185
|
-
ldm install
|
|
186
|
-
|
|
187
|
-
|
|
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
|
|
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,
|
|
32
|
-
| **
|
|
33
|
-
| **
|
|
34
|
-
|
|
|
35
|
-
| **Claude Code
|
|
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
|
|
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.
|
|
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.
|
|
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
|
-
###
|
|
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
|
-
###
|
|
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
|
-
###
|
|
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
|
|
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
|
|
264
|
+
Read https://wip.computer/install/<slug>.txt
|
|
213
265
|
|
|
214
|
-
|
|
215
|
-
|
|
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:
|
|
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 |
|
|
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
|
@@ -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) => ({ "&": "&", "<": "<", ">": ">", "\"": """, "'": "'" }[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>
|