@diologue/local-agent 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,288 @@
1
+ # `@diologue/local-agent`
2
+
3
+ Local helper that bridges the Diologue **Coding Agent** web UI to an
4
+ [OpenCode](https://opencode.ai/) process running against a user-selected
5
+ git repo on the same machine.
6
+
7
+ ## Quickstart (recommended)
8
+
9
+ Visit `/coding-agent/quickstart` on your Diologue deployment in a
10
+ browser. It will issue a one-time setup token and hand you a single
11
+ command to paste in a terminal:
12
+
13
+ ```bash
14
+ npx @diologue/local-agent --cloud=<your-cloud> --token=<setup-token>
15
+ ```
16
+
17
+ The helper starts, calls back to the cloud, the browser tab redirects
18
+ itself, and the coding agent is ready. No env vars, no copy-paste of
19
+ URLs or tokens into the UI.
20
+
21
+ ## Standalone / power-user mode
22
+
23
+ ```bash
24
+ # Run the helper directly, get a random pairing token to paste in
25
+ diologue-local-agent start --origin=https://your-app.com
26
+ ```
27
+
28
+ **This is v1 of a hybrid architecture.** It binds to `127.0.0.1` only and
29
+ is intended to be reached by the browser running on the same machine.
30
+ A future version will open an authenticated tunnel back to the cloud so
31
+ the same UI can drive a remote machine — the HTTP surface is designed to
32
+ be compatible with that evolution.
33
+
34
+ ---
35
+
36
+ ## Architecture
37
+
38
+ ```
39
+ ┌──────────────────┐ HTTPS ┌──────────────────┐
40
+ │ Diologue web UI │ ──────────────▶ │ Diologue cloud │ Existing app:
41
+ │ (your browser) │ ◀────────────── │ (Express + LLMs) │ auth, billing,
42
+ └──────────────────┘ └──────────────────┘ session storage
43
+ │ ▲
44
+ │ http://127.0.0.1:4099 │ llm_request brokered
45
+ │ (this helper) │ back through the
46
+ ▼ │ browser → cloud LLM
47
+ ┌──────────────────┐ @opencode-ai/sdk │ proxy → usage_ledger
48
+ │ Local Agent │ ─────────────────────────┘
49
+ │ (this package) │
50
+ └──────────────────┘
51
+ │ spawns/talks to
52
+
53
+ ┌──────────────────┐
54
+ │ opencode binary │ Installed separately — see Prerequisites.
55
+ │ on your machine │
56
+ └──────────────────┘
57
+ ```
58
+
59
+ ---
60
+
61
+ ## Prerequisites
62
+
63
+ 1. **Node 20+** (matches the parent app).
64
+ 2. **OpenCode binary** on `PATH`. The official installer:
65
+ ```bash
66
+ curl -fsSL https://opencode.ai/install | bash
67
+ ```
68
+ Verify: `opencode --version`.
69
+
70
+ You can run the helper without opencode by forcing the mock adapter
71
+ (see "Run" below) — useful for demos.
72
+
73
+ 3. **A provider key for opencode** — opencode reads its own config (env
74
+ vars / `~/.config/opencode/...`). Today the helper uses opencode's
75
+ native provider config for the actual model calls; the cloud LLM
76
+ proxy + usage ledger are wired but not yet plumbed through opencode
77
+ itself (see "Roadmap").
78
+
79
+ ---
80
+
81
+ ## Install
82
+
83
+ ```bash
84
+ # For end users — no clone required
85
+ npx @diologue/local-agent --help
86
+
87
+ # For development on this repo
88
+ cd local-agent && npm install
89
+ ```
90
+
91
+ The published npm package is a single-file bundle produced by
92
+ `npm run build`. End users never need to clone the repo.
93
+
94
+ ---
95
+
96
+ ## Run
97
+
98
+ ```bash
99
+ cd local-agent
100
+ npm run dev # tsx watch — auto-reloads on edits
101
+ # or
102
+ npm start
103
+ ```
104
+
105
+ The helper listens on `http://127.0.0.1:4099` and prints a one-time
106
+ pairing token to the console. Paste that token into the Coding Agent
107
+ page in the web UI to connect.
108
+
109
+ ```
110
+ [local-agent] Listening on http://127.0.0.1:4099
111
+ [local-agent] Adapter: opencode
112
+ [local-agent] Pairing token (paste into the web UI):
113
+ [local-agent] <random-token-here>
114
+ [local-agent] Allowed browser origin: http://localhost:5000
115
+ [local-agent] Press Ctrl+C to stop. The token is regenerated on each restart.
116
+ ```
117
+
118
+ ### Force the mock adapter
119
+
120
+ For demos or offline development:
121
+
122
+ ```bash
123
+ LOCAL_AGENT_ADAPTER=mock npm run dev
124
+ ```
125
+
126
+ The mock streams a canned response with a synthetic tool call and a
127
+ synthetic diff that creates `MOCK_NOTES.md` — safe to "Apply" against
128
+ any real repo.
129
+
130
+ ---
131
+
132
+ ## Verify the SDK dependency
133
+
134
+ A non-network sanity check that the OpenCode SDK installed correctly:
135
+
136
+ ```bash
137
+ npm run verify-sdk
138
+ ```
139
+
140
+ Does **not** spawn opencode and does **not** call any network APIs.
141
+
142
+ ---
143
+
144
+ ## HTTP surface
145
+
146
+ All endpoints except `GET /health` require the
147
+ `x-local-agent-token` header.
148
+
149
+ | Method | Path | Purpose |
150
+ |--------|-------------------------------------|--------------------------------------------------------------------------------------|
151
+ | GET | `/health` | Liveness + version. Unauthenticated. |
152
+ | POST | `/repo/select` | Validate + bind a repo path. Body: `{ path }`. |
153
+ | GET | `/repo/status` | Current selected repo (or `{ repo: null }`). |
154
+ | GET | `/repo/diff` | Unified `git diff HEAD` + `sizeBytes`. |
155
+ | GET | `/repo/changed-files` | Parsed `git status --short -uall`. |
156
+ | POST | `/repo/apply` | Apply a unified diff. Two-phase: `git apply --check` then `git apply`. Body: `{ unified, baselineHash? }`. |
157
+ | POST | `/agent/message` | SSE: stream `AgentStreamEvent` envelopes for one turn. Body: `{ sessionId, prompt, history? }`. |
158
+ | POST | `/agent/llm-response/:requestId` | Browser fulfils a brokered LLM call. Body: `{ sessionId, text, provider, model, tokenUsage?, error? }`. |
159
+
160
+ ---
161
+
162
+ ## Brokered LLM calls
163
+
164
+ When an adapter calls `request.broker(payload)`, the helper:
165
+
166
+ 1. Generates a `requestId` and stores a pending promise.
167
+ 2. Emits an `llm_request` envelope on the open `/agent/message` SSE
168
+ stream.
169
+ 3. The browser hits `POST /api/coding/sessions/:id/llm-proxy` on the
170
+ Diologue cloud, which calls the user's configured LLM via the
171
+ existing provider stack and writes a `usage_ledger` row.
172
+ 4. The browser POSTs the response to
173
+ `/agent/llm-response/:requestId`, which fulfils the broker's
174
+ promise. The adapter receives the text and continues iterating.
175
+
176
+ The broker registry is indexed by `sessionId`. A reconnecting stream
177
+ supersedes the prior broker (its pending requests reject with
178
+ `superseded_by_new_stream`).
179
+
180
+ > The **real `OpenCodeProcessAdapter` does not yet route through the
181
+ > broker** — opencode uses its own provider config. See "Roadmap".
182
+
183
+ ---
184
+
185
+ ## Security posture
186
+
187
+ - Bound to `127.0.0.1` only — never reachable from another host.
188
+ - CORS allowlist is your web app origin (default
189
+ `http://localhost:5000`, override via env). Anything else gets no
190
+ CORS headers; the browser drops the response.
191
+ - Every endpoint except `GET /health` requires the
192
+ `x-local-agent-token` header. Comparison is constant-time.
193
+ - No shell-passthrough endpoints. Git operations use
194
+ `execFile('git', [args], { cwd: validatedRepoPath })`.
195
+ - Repo paths are absolute, must exist, must contain `.git`, and are
196
+ `realpath`-resolved to defeat symlink confusion. Stored verbatim is
197
+ never trusted on subsequent calls.
198
+ - `POST /repo/apply` runs `git apply --check` before writing. A failing
199
+ check returns 409 with stderr; no filesystem changes.
200
+ - The helper holds no cloud credentials. LLM calls are brokered back
201
+ through the authenticated browser session.
202
+
203
+ > **TODO(tunnel-cloud-auth):** the static pairing token is an MVP
204
+ > shortcut. When the cloud tunnel ships, this is replaced by a
205
+ > cloud-issued, rotated device credential.
206
+
207
+ ---
208
+
209
+ ## Environment variables
210
+
211
+ | Var | Default | Purpose |
212
+ |----------------------------------|--------------------------|----------------------------------------------------------------------|
213
+ | `LOCAL_AGENT_PORT` | `4099` | TCP port to bind on `127.0.0.1`. |
214
+ | `LOCAL_AGENT_ALLOWED_ORIGIN` | `http://localhost:5000` | Single allowed CORS origin (the web app URL). |
215
+ | `LOCAL_AGENT_TOKEN` | _(generated)_ | Override the auto-generated pairing token. **For tests only.** |
216
+ | `LOCAL_AGENT_ADAPTER` | _(opencode)_ | Set to `mock` to force the mock adapter. Default uses opencode. |
217
+ | `LOCAL_AGENT_ENGINE` | `local` | Engine source. `local` (default, V5+) = bundled diologue-engine that postinstall fetches from GitHub Releases. `npm` = legacy opt-out (@opencode-ai/sdk + system `opencode` binary). |
218
+ | `LOCAL_AGENT_ENGINE_BUNDLE` | _(auto-discovered)_ | Absolute path to a diologue-engine binary, overriding the postinstall-fetched one. For dev / CI. |
219
+ | `SKIP_DIOLOGUE_POSTINSTALL` | _(unset)_ | Set to `1` to skip the postinstall bundle download (offline installs, CI). Helper still works via `LOCAL_AGENT_ENGINE=npm`. |
220
+
221
+ ---
222
+
223
+ ## Testing
224
+
225
+ ```bash
226
+ npm test
227
+ ```
228
+
229
+ Uses Node's built-in test runner via `tsx`. Coverage:
230
+
231
+ - `lib/git.test.ts` — parses `git status --short` output (pure
232
+ function, no shell).
233
+ - `lib/paths.test.ts` — repo-path validation against an on-disk
234
+ scratch dir.
235
+ - `routes/repo.test.ts` — end-to-end `/repo/*` against a real git
236
+ repository created in `mkdtemp`. Covers `/repo/apply` happy path,
237
+ rejection on a diff that doesn't apply, and 400 on invalid body.
238
+ - `routes/agent.test.ts` — SSE happy path with the mock adapter.
239
+ - `routes/agent-broker.test.ts` — end-to-end brokered LLM round-trip
240
+ (the test code plays the role of the browser).
241
+ - `broker.test.ts` — unit tests for `LlmBroker` + `BrokerRegistry`.
242
+ - `adapters/mock-adapter.test.ts` — 10 conformance tests for the
243
+ `OpenCodeAdapter` contract. Real adapters MUST pass these.
244
+ - `adapters/opencode-event-mapper.test.ts` — pure-function event
245
+ translator tests.
246
+ - `adapters/opencode-adapter.test.ts` — adapter shape + binary-check
247
+ error path (skipped when opencode IS installed).
248
+
249
+ Today: **71/71 passing**.
250
+
251
+ ---
252
+
253
+ ## Roadmap
254
+
255
+ - [ ] Plumb opencode's outbound LLM calls through `request.broker` so
256
+ coding sessions hit the cloud usage ledger.
257
+ - [ ] Persistent opencode session ↔ Diologue session mapping across
258
+ helper restarts.
259
+ - [ ] Synthesise full-file additions for untracked files into the
260
+ `/repo/diff` output.
261
+ - [ ] Outbound tunnel to cloud relay for remote-machine support.
262
+ - [ ] Replace static pairing token with cloud-issued device credential.
263
+ - [ ] Deeper opencode rebrand — only if/when we vendor source rather
264
+ than depend on the SDK.
265
+
266
+ ---
267
+
268
+ ## Troubleshooting
269
+
270
+ **"Helper isn't responding"** in the web UI → the helper isn't
271
+ running, the URL is wrong, or the port is in use. Start the helper and
272
+ verify with:
273
+ ```bash
274
+ curl http://localhost:4099/health
275
+ ```
276
+
277
+ **"opencode binary not found on PATH"** in chat error envelopes →
278
+ install via `curl -fsSL https://opencode.ai/install | bash` and confirm
279
+ `opencode --version` works in the same shell you're starting the helper
280
+ from. Or run with `LOCAL_AGENT_ADAPTER=mock` to bypass.
281
+
282
+ **"Pairing token is missing or doesn't match"** → the helper
283
+ regenerates the token on every restart. Copy the new one from the
284
+ console after each restart.
285
+
286
+ **Diff "doesn't apply"** → the working tree drifted between when the
287
+ diff was proposed and when you clicked Apply. Refresh the diff
288
+ (right-hand panel button) and re-prompt the agent.