@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/dist/cli.mjs ADDED
@@ -0,0 +1,2527 @@
1
+ #!/usr/bin/env node
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropNames = Object.getOwnPropertyNames;
4
+ var __esm = (fn, res) => function __init() {
5
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
6
+ };
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, { get: all[name], enumerable: true });
10
+ };
11
+
12
+ // src/adapters/engine-locator-npm.ts
13
+ var engine_locator_npm_exports = {};
14
+ __export(engine_locator_npm_exports, {
15
+ NpmSdkLocator: () => NpmSdkLocator
16
+ });
17
+ import { spawn } from "node:child_process";
18
+ var defaultBinaryCheck, defaultSdkLoader, NpmSdkLocator;
19
+ var init_engine_locator_npm = __esm({
20
+ "src/adapters/engine-locator-npm.ts"() {
21
+ "use strict";
22
+ defaultBinaryCheck = async () => new Promise((resolve) => {
23
+ const child = spawn("opencode", ["--version"], { stdio: "ignore" });
24
+ child.on("error", () => resolve(false));
25
+ child.on("exit", (code) => resolve(code === 0));
26
+ });
27
+ defaultSdkLoader = () => import("@opencode-ai/sdk");
28
+ NpmSdkLocator = class {
29
+ constructor(options = {}) {
30
+ this.options = options;
31
+ }
32
+ name = "npm";
33
+ async start(options) {
34
+ const check = this.options.checkBinary ?? defaultBinaryCheck;
35
+ const ok = await check();
36
+ if (!ok) {
37
+ throw new Error(
38
+ "opencode binary not found on PATH. Install via `curl -fsSL https://opencode.ai/install | bash` and ensure `opencode --version` works in this shell. (Or set LOCAL_AGENT_ENGINE=local once V4 ships.)"
39
+ );
40
+ }
41
+ const loadSdk = this.options.sdkLoader ?? defaultSdkLoader;
42
+ const sdk = await loadSdk();
43
+ const server = await sdk.createOpencodeServer({
44
+ hostname: "127.0.0.1",
45
+ port: 0,
46
+ signal: options.signal,
47
+ config: options.config
48
+ });
49
+ const client = sdk.createOpencodeClient({ baseUrl: server.url });
50
+ return {
51
+ url: server.url,
52
+ client,
53
+ close: () => {
54
+ server.close();
55
+ }
56
+ };
57
+ }
58
+ };
59
+ }
60
+ });
61
+
62
+ // engine-release.json
63
+ var engine_release_default;
64
+ var init_engine_release = __esm({
65
+ "engine-release.json"() {
66
+ engine_release_default = {
67
+ _comment: "Single source of truth for the engine bundle the helper expects. Both scripts/build-engine-bundle.ts and local-agent/scripts/postinstall.ts read this. Bumping `version` here is what triggers a new release.",
68
+ version: "0.1.0",
69
+ releaseTag: "engine-v0.1.0",
70
+ downloadBase: "https://github.com/Deplova-Ltd/AI-Personas-and-Multi-LLM/releases/download",
71
+ targets: [
72
+ { platform: "darwin", arch: "arm64", bunTarget: "bun-darwin-arm64", ext: "" },
73
+ { platform: "darwin", arch: "x64", bunTarget: "bun-darwin-x64", ext: "" },
74
+ { platform: "linux", arch: "x64", bunTarget: "bun-linux-x64", ext: "" },
75
+ { platform: "linux", arch: "arm64", bunTarget: "bun-linux-arm64", ext: "" },
76
+ { platform: "win32", arch: "x64", bunTarget: "bun-windows-x64", ext: ".exe" }
77
+ ]
78
+ };
79
+ }
80
+ });
81
+
82
+ // src/lib/engine-bundle.ts
83
+ import { access as access2, constants } from "node:fs/promises";
84
+ import path3 from "node:path";
85
+ import { fileURLToPath } from "node:url";
86
+ var engineRelease, __filename, __dirname, LOCAL_AGENT_ROOT, REPO_ROOT, BUNDLE_DIR_INSTALLED, BUNDLE_DIR_LOCAL_BUILD, ENGINE_BUNDLE_ENV, exists2, bundleFilename, findEngineBundle;
87
+ var init_engine_bundle = __esm({
88
+ "src/lib/engine-bundle.ts"() {
89
+ "use strict";
90
+ init_engine_release();
91
+ engineRelease = engine_release_default;
92
+ __filename = fileURLToPath(import.meta.url);
93
+ __dirname = path3.dirname(__filename);
94
+ LOCAL_AGENT_ROOT = path3.resolve(__dirname, "../..");
95
+ REPO_ROOT = path3.resolve(LOCAL_AGENT_ROOT, "..");
96
+ BUNDLE_DIR_INSTALLED = path3.join(
97
+ LOCAL_AGENT_ROOT,
98
+ "dist/engine",
99
+ engineRelease.releaseTag
100
+ );
101
+ BUNDLE_DIR_LOCAL_BUILD = path3.join(
102
+ REPO_ROOT,
103
+ "build/diologue-engine-bundles"
104
+ );
105
+ ENGINE_BUNDLE_ENV = "LOCAL_AGENT_ENGINE_BUNDLE";
106
+ exists2 = async (p) => {
107
+ try {
108
+ await access2(p, constants.X_OK);
109
+ return true;
110
+ } catch {
111
+ return false;
112
+ }
113
+ };
114
+ bundleFilename = (platform = process.platform, arch = process.arch) => {
115
+ const target = engineRelease.targets.find(
116
+ (t) => t.platform === platform && t.arch === arch
117
+ );
118
+ const ext = target?.ext ?? "";
119
+ return `diologue-engine-${platform}-${arch}${ext}`;
120
+ };
121
+ findEngineBundle = async () => {
122
+ const fromEnv = process.env[ENGINE_BUNDLE_ENV];
123
+ if (fromEnv) {
124
+ if (await exists2(fromEnv)) {
125
+ return { path: fromEnv, source: "env" };
126
+ }
127
+ }
128
+ const filename = bundleFilename();
129
+ const installed = path3.join(BUNDLE_DIR_INSTALLED, filename);
130
+ if (await exists2(installed)) {
131
+ return { path: installed, source: "installed" };
132
+ }
133
+ const localBuild = path3.join(BUNDLE_DIR_LOCAL_BUILD, filename);
134
+ if (await exists2(localBuild)) {
135
+ return { path: localBuild, source: "local-build" };
136
+ }
137
+ return null;
138
+ };
139
+ }
140
+ });
141
+
142
+ // src/adapters/engine-locator-local.ts
143
+ var engine_locator_local_exports = {};
144
+ __export(engine_locator_local_exports, {
145
+ LocalEngineLocator: () => LocalEngineLocator
146
+ });
147
+ import { access as access3, constants as constants2 } from "node:fs/promises";
148
+ import { spawn as spawn2 } from "node:child_process";
149
+ import path4 from "node:path";
150
+ import { fileURLToPath as fileURLToPath2 } from "node:url";
151
+ var __filename2, __dirname2, REPO_ROOT2, DEFAULT_ENGINE_DIR, ENTRY_REL, DEFAULT_STARTUP_TIMEOUT_MS, LISTENING_REGEX, defaultRuntimeCheck, exists3, defaultClientFactory, LocalEngineLocator, truncate;
152
+ var init_engine_locator_local = __esm({
153
+ "src/adapters/engine-locator-local.ts"() {
154
+ "use strict";
155
+ init_engine_bundle();
156
+ __filename2 = fileURLToPath2(import.meta.url);
157
+ __dirname2 = path4.dirname(__filename2);
158
+ REPO_ROOT2 = path4.resolve(__dirname2, "../../..");
159
+ DEFAULT_ENGINE_DIR = path4.join(REPO_ROOT2, "build/diologue-engine");
160
+ ENTRY_REL = "packages/opencode/src/index.ts";
161
+ DEFAULT_STARTUP_TIMEOUT_MS = 3e4;
162
+ LISTENING_REGEX = /listening on (https?:\/\/[^\s]+)/i;
163
+ defaultRuntimeCheck = async (runtime) => new Promise((resolve) => {
164
+ const child = spawn2(runtime, ["--version"], { stdio: "ignore" });
165
+ child.on("error", () => resolve(false));
166
+ child.on("exit", (code) => resolve(code === 0));
167
+ });
168
+ exists3 = async (p) => {
169
+ try {
170
+ await access3(p, constants2.F_OK);
171
+ return true;
172
+ } catch {
173
+ return false;
174
+ }
175
+ };
176
+ defaultClientFactory = async (baseUrl) => {
177
+ const sdk = await import("@opencode-ai/sdk");
178
+ return sdk.createOpencodeClient({ baseUrl });
179
+ };
180
+ LocalEngineLocator = class {
181
+ constructor(options = {}) {
182
+ this.options = options;
183
+ this.enginePath = options.enginePath ?? DEFAULT_ENGINE_DIR;
184
+ this.runtime = options.runtime ?? "bun";
185
+ this.startupTimeoutMs = options.startupTimeoutMs ?? DEFAULT_STARTUP_TIMEOUT_MS;
186
+ }
187
+ name = "local";
188
+ enginePath;
189
+ runtime;
190
+ startupTimeoutMs;
191
+ /** Decide which engine path we're going to use. Returns the resolved
192
+ * bundle when we'll spawn that directly, or null when we'll fall
193
+ * back to the source-tree path. Test seam. */
194
+ async resolveBundle() {
195
+ if (this.options.forceSource) return null;
196
+ if (this.options.resolveBundle) return this.options.resolveBundle();
197
+ return findEngineBundle();
198
+ }
199
+ /** Run the preflight checks for the path we're about to use. Returns
200
+ * null on success, or a human-readable error string if something's
201
+ * wrong. Exposed so start() can reuse it and tests can assert it
202
+ * directly. */
203
+ async preflight() {
204
+ const bundle = await this.resolveBundle();
205
+ if (bundle) {
206
+ return null;
207
+ }
208
+ if (!await exists3(this.enginePath)) {
209
+ return `[engine-locator/local] No engine bundle found and no source tree at ${this.enginePath}. Either:
210
+ - Reinstall the package so postinstall fetches a bundle, or
211
+ - Run \`npm run rebrand-engine\` to populate the source tree.`;
212
+ }
213
+ const entry = path4.join(this.enginePath, ENTRY_REL);
214
+ if (!await exists3(entry)) {
215
+ return `[engine-locator/local] Engine entry not found at ${entry}. The rebrand may have produced a partial tree \u2014 re-run \`npm run rebrand-engine\`.`;
216
+ }
217
+ const check = this.options.checkRuntime ?? defaultRuntimeCheck;
218
+ const ok = await check(this.runtime);
219
+ if (!ok) {
220
+ return `[engine-locator/local] No engine bundle found and runtime "${this.runtime}" is not on PATH. Either:
221
+ - Reinstall the package so postinstall fetches a bundle, or
222
+ - Install bun (https://bun.sh) for the source-tree fallback.`;
223
+ }
224
+ return null;
225
+ }
226
+ /** Build the spawn command. Tests override via `engineCommand`.
227
+ * Otherwise we prefer a discovered bundle; failing that, we fall
228
+ * back to `<runtime> run <entry>`. */
229
+ async resolveCommand() {
230
+ if (this.options.engineCommand) {
231
+ return { ...this.options.engineCommand, source: "override" };
232
+ }
233
+ const bundle = await this.resolveBundle();
234
+ if (bundle) {
235
+ return {
236
+ command: bundle.path,
237
+ args: ["serve", "--port", "0", "--hostname", "127.0.0.1"],
238
+ source: "bundle",
239
+ bundle
240
+ };
241
+ }
242
+ return {
243
+ command: this.runtime,
244
+ args: [
245
+ "run",
246
+ path4.join(this.enginePath, ENTRY_REL),
247
+ "serve",
248
+ "--port",
249
+ "0",
250
+ "--hostname",
251
+ "127.0.0.1"
252
+ ],
253
+ source: "source"
254
+ };
255
+ }
256
+ async start(options) {
257
+ const preflightErr = await this.preflight();
258
+ if (preflightErr) throw new Error(preflightErr);
259
+ const resolved = await this.resolveCommand();
260
+ const { command, args } = resolved;
261
+ const cwd = resolved.source === "source" && await exists3(this.enginePath) ? this.enginePath : void 0;
262
+ const child = spawn2(command, args, {
263
+ cwd,
264
+ stdio: ["ignore", "pipe", "pipe"],
265
+ // Inherit env so the engine sees any LOCAL_AGENT_* / Provider
266
+ // credentials the parent has, plus inject the shim config via
267
+ // OPENCODE_CONFIG-style env once V2.5 wires that. (For now the
268
+ // config flows through createOpencodeServer in the npm path
269
+ // and is wired post-spawn here — TODO once we cut the npm
270
+ // SDK entirely, we'll pipe the config in via stdin or args.)
271
+ env: process.env
272
+ });
273
+ let url;
274
+ try {
275
+ url = await this.waitForListening(child, options.signal);
276
+ } catch (err) {
277
+ if (!child.killed) {
278
+ try {
279
+ child.kill();
280
+ } catch {
281
+ }
282
+ }
283
+ throw err;
284
+ }
285
+ const factory = this.options.clientFactory ?? defaultClientFactory;
286
+ const client = await factory(url);
287
+ return {
288
+ url,
289
+ client,
290
+ close: () => {
291
+ if (child.exitCode === null && !child.killed) {
292
+ try {
293
+ child.kill();
294
+ } catch {
295
+ }
296
+ }
297
+ }
298
+ };
299
+ }
300
+ /** Watch the subprocess until either:
301
+ * - stdout/stderr emits a "listening on http://..." line — resolve
302
+ * - the process exits before that happens — reject with stderr
303
+ * - the startup timeout fires — reject + (caller kills the child)
304
+ * - the abort signal fires — reject + (caller kills the child)
305
+ */
306
+ waitForListening(child, abort) {
307
+ return new Promise((resolve, reject) => {
308
+ let settled = false;
309
+ let buffered = "";
310
+ const tag = `${this.runtime}/${path4.basename(this.enginePath)}`;
311
+ const settle = (fn) => {
312
+ if (settled) return;
313
+ settled = true;
314
+ cleanup();
315
+ fn();
316
+ };
317
+ const onStdout = (chunk) => {
318
+ const text = chunk.toString("utf-8");
319
+ buffered += text;
320
+ const match = LISTENING_REGEX.exec(buffered);
321
+ if (match) {
322
+ settle(() => resolve(match[1]));
323
+ }
324
+ };
325
+ const onStderr = onStdout;
326
+ const onExit = (code, signal) => {
327
+ settle(
328
+ () => reject(
329
+ new Error(
330
+ `[engine-locator/local] ${tag} exited before becoming ready (code=${code} signal=${signal ?? "-"}). Last output:
331
+ ${truncate(buffered, 4096)}`
332
+ )
333
+ )
334
+ );
335
+ };
336
+ const onError = (err) => {
337
+ settle(
338
+ () => reject(
339
+ new Error(
340
+ `[engine-locator/local] ${tag} failed to spawn: ${err.message}`
341
+ )
342
+ )
343
+ );
344
+ };
345
+ const timer = setTimeout(() => {
346
+ settle(
347
+ () => reject(
348
+ new Error(
349
+ `[engine-locator/local] ${tag} did not become ready within ${this.startupTimeoutMs}ms. Last output:
350
+ ${truncate(buffered, 4096)}`
351
+ )
352
+ )
353
+ );
354
+ }, this.startupTimeoutMs);
355
+ const onAbort = () => {
356
+ settle(
357
+ () => reject(new Error(`[engine-locator/local] startup aborted`))
358
+ );
359
+ };
360
+ const cleanup = () => {
361
+ clearTimeout(timer);
362
+ child.stdout?.off("data", onStdout);
363
+ child.stderr?.off("data", onStderr);
364
+ child.off("exit", onExit);
365
+ child.off("error", onError);
366
+ abort?.removeEventListener("abort", onAbort);
367
+ };
368
+ child.stdout?.on("data", onStdout);
369
+ child.stderr?.on("data", onStderr);
370
+ child.once("exit", onExit);
371
+ child.once("error", onError);
372
+ if (abort) {
373
+ if (abort.aborted) {
374
+ onAbort();
375
+ return;
376
+ }
377
+ abort.addEventListener("abort", onAbort);
378
+ }
379
+ });
380
+ }
381
+ };
382
+ truncate = (s, max) => s.length <= max ? s : s.slice(s.length - max);
383
+ }
384
+ });
385
+
386
+ // src/cli.ts
387
+ import process2 from "node:process";
388
+
389
+ // src/config.ts
390
+ import { randomBytes } from "node:crypto";
391
+ var DEFAULT_PORT = 4099;
392
+ var DEFAULT_ALLOWED_ORIGIN = "http://localhost:5000";
393
+ var parsePort = (raw) => {
394
+ if (!raw) {
395
+ return DEFAULT_PORT;
396
+ }
397
+ const parsed = Number.parseInt(raw, 10);
398
+ if (!Number.isFinite(parsed) || parsed <= 0 || parsed > 65535) {
399
+ throw new Error(
400
+ `LOCAL_AGENT_PORT must be a valid TCP port (1-65535), got: ${raw}`
401
+ );
402
+ }
403
+ return parsed;
404
+ };
405
+ var generateToken = () => {
406
+ return randomBytes(32).toString("hex");
407
+ };
408
+ var loadConfig = (options = {}) => {
409
+ const env = options.env ?? process.env;
410
+ const base = {
411
+ // 127.0.0.1 is intentional and not configurable. Binding to 0.0.0.0
412
+ // would expose the helper to the LAN, which the token alone is not
413
+ // designed to defend against. See TODO(tunnel-cloud-auth).
414
+ host: "127.0.0.1",
415
+ port: parsePort(env.LOCAL_AGENT_PORT),
416
+ allowedOrigin: (env.LOCAL_AGENT_ALLOWED_ORIGIN ?? "").trim() || DEFAULT_ALLOWED_ORIGIN,
417
+ token: (env.LOCAL_AGENT_TOKEN ?? "").trim() || generateToken(),
418
+ // Bumped from package.json when we ship a meaningful change. Kept inline
419
+ // to avoid a JSON import + ESM ergonomics in tests.
420
+ helperVersion: "0.0.1",
421
+ startedAt: (/* @__PURE__ */ new Date()).toISOString()
422
+ };
423
+ return { ...base, ...options.overrides };
424
+ };
425
+
426
+ // src/server.ts
427
+ import express from "express";
428
+
429
+ // src/auth.ts
430
+ import { timingSafeEqual } from "node:crypto";
431
+
432
+ // ../shared/coding-agent-types.ts
433
+ var LOCAL_AGENT_TOKEN_HEADER = "x-local-agent-token";
434
+
435
+ // src/auth.ts
436
+ var DEFAULT_UNAUTHENTICATED = /* @__PURE__ */ new Set(["GET /health"]);
437
+ var DEFAULT_UNAUTHENTICATED_PREFIXES = ["/llm-shim/"];
438
+ var tokensEqual = (a, b) => {
439
+ const ab = Buffer.from(a, "utf8");
440
+ const bb = Buffer.from(b, "utf8");
441
+ if (ab.length !== bb.length) {
442
+ return false;
443
+ }
444
+ return timingSafeEqual(ab, bb);
445
+ };
446
+ var createAuthMiddleware = (options) => {
447
+ const exempt = options.unauthenticated ?? DEFAULT_UNAUTHENTICATED;
448
+ return (req, res, next) => {
449
+ const key = `${req.method} ${req.path}`;
450
+ if (exempt.has(key)) {
451
+ next();
452
+ return;
453
+ }
454
+ for (const prefix of DEFAULT_UNAUTHENTICATED_PREFIXES) {
455
+ if (req.path.startsWith(prefix)) {
456
+ next();
457
+ return;
458
+ }
459
+ }
460
+ if (req.method === "OPTIONS") {
461
+ next();
462
+ return;
463
+ }
464
+ const provided = req.header(LOCAL_AGENT_TOKEN_HEADER);
465
+ if (!provided || !tokensEqual(provided, options.token)) {
466
+ res.status(401).json({ error: "invalid_or_missing_token", header: LOCAL_AGENT_TOKEN_HEADER });
467
+ return;
468
+ }
469
+ next();
470
+ };
471
+ };
472
+
473
+ // src/cors.ts
474
+ var createCorsMiddleware = (options) => {
475
+ const allowed = options.allowedOrigin;
476
+ return (req, res, next) => {
477
+ const origin = req.header("origin");
478
+ if (origin && origin === allowed) {
479
+ res.setHeader("Access-Control-Allow-Origin", origin);
480
+ res.setHeader("Vary", "Origin");
481
+ res.setHeader("Access-Control-Allow-Credentials", "false");
482
+ res.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS");
483
+ res.setHeader(
484
+ "Access-Control-Allow-Headers",
485
+ `Content-Type, ${LOCAL_AGENT_TOKEN_HEADER}`
486
+ );
487
+ res.setHeader("Access-Control-Max-Age", "300");
488
+ }
489
+ if (req.method === "OPTIONS") {
490
+ res.status(204).end();
491
+ return;
492
+ }
493
+ next();
494
+ };
495
+ };
496
+
497
+ // src/state.ts
498
+ var createState = () => {
499
+ let selected = null;
500
+ return {
501
+ getSelectedRepo: () => selected,
502
+ setSelectedRepo: (repo) => {
503
+ selected = repo;
504
+ },
505
+ clearSelectedRepo: () => {
506
+ selected = null;
507
+ }
508
+ };
509
+ };
510
+
511
+ // src/routes/health.ts
512
+ var createHealthHandler = (config) => {
513
+ return (_req, res) => {
514
+ const body = {
515
+ ok: true,
516
+ helperVersion: config.helperVersion,
517
+ boundHost: config.host,
518
+ port: config.port,
519
+ startedAt: config.startedAt
520
+ };
521
+ res.json(body);
522
+ };
523
+ };
524
+
525
+ // src/routes/repo.ts
526
+ import { Router as createRouter } from "express";
527
+ import path2 from "node:path";
528
+ import { z } from "zod";
529
+
530
+ // src/lib/git.ts
531
+ import { execFile } from "node:child_process";
532
+ import { promisify } from "node:util";
533
+ var execFileAsync = promisify(execFile);
534
+ var GitCommandError = class extends Error {
535
+ stderr;
536
+ exitCode;
537
+ constructor(args, exitCode, stderr) {
538
+ super(`git ${args.join(" ")} failed with exit ${exitCode}: ${stderr.trim()}`);
539
+ this.name = "GitCommandError";
540
+ this.stderr = stderr;
541
+ this.exitCode = exitCode;
542
+ }
543
+ };
544
+ var runGit = async (cwd, args) => {
545
+ try {
546
+ const { stdout } = await execFileAsync("git", args, {
547
+ cwd,
548
+ // Generous-ish buffer for diffs of medium-sized changes. We surface
549
+ // a sizeBytes field so the UI can refuse to render extreme cases.
550
+ maxBuffer: 16 * 1024 * 1024,
551
+ // Inherit a stripped env — we don't want PAGER or GIT_* tracing.
552
+ env: { ...process.env, GIT_PAGER: "cat", PAGER: "cat" }
553
+ });
554
+ return stdout;
555
+ } catch (err) {
556
+ if (err && typeof err === "object" && "code" in err) {
557
+ const e = err;
558
+ throw new GitCommandError(args, e.code ?? -1, e.stderr ?? "");
559
+ }
560
+ throw err;
561
+ }
562
+ };
563
+ var getBranch = async (cwd) => {
564
+ try {
565
+ const out = (await runGit(cwd, ["rev-parse", "--abbrev-ref", "HEAD"])).trim();
566
+ if (out === "HEAD") {
567
+ return null;
568
+ }
569
+ return out;
570
+ } catch {
571
+ return null;
572
+ }
573
+ };
574
+ var getHeadShort = async (cwd) => {
575
+ try {
576
+ return (await runGit(cwd, ["rev-parse", "--short", "HEAD"])).trim();
577
+ } catch {
578
+ return null;
579
+ }
580
+ };
581
+ var isDirty = async (cwd) => {
582
+ const out = await runGit(cwd, ["status", "--porcelain"]);
583
+ return out.trim().length > 0;
584
+ };
585
+ var parseShortStatus = (output) => {
586
+ const lines = output.split("\n");
587
+ const files = [];
588
+ for (const line of lines) {
589
+ if (line.length < 4) {
590
+ continue;
591
+ }
592
+ const indexChar = line[0];
593
+ const workChar = line[1];
594
+ if (indexChar === void 0 || workChar === void 0) {
595
+ continue;
596
+ }
597
+ let pathPart = line.slice(3);
598
+ const arrow = pathPart.indexOf(" -> ");
599
+ if (arrow !== -1) {
600
+ pathPart = pathPart.slice(arrow + 4);
601
+ }
602
+ if (pathPart.startsWith('"') && pathPart.endsWith('"') && pathPart.length >= 2) {
603
+ pathPart = pathPart.slice(1, -1);
604
+ }
605
+ files.push({
606
+ path: pathPart,
607
+ index: asGitStatusCode(indexChar),
608
+ workTree: asGitStatusCode(workChar),
609
+ untracked: indexChar === "?" && workChar === "?"
610
+ });
611
+ }
612
+ return files;
613
+ };
614
+ var VALID_STATUS_CODES = /* @__PURE__ */ new Set([" ", "M", "A", "D", "R", "C", "U", "?", "!"]);
615
+ var asGitStatusCode = (ch) => {
616
+ if (VALID_STATUS_CODES.has(ch)) {
617
+ return ch;
618
+ }
619
+ return "M";
620
+ };
621
+ var getStatusShort = async (cwd) => {
622
+ const out = await runGit(cwd, ["status", "--short", "-uall"]);
623
+ return parseShortStatus(out);
624
+ };
625
+ var getDiff = async (cwd) => {
626
+ return runGit(cwd, ["diff", "HEAD"]);
627
+ };
628
+ var runGitWithStdin = async (cwd, args, input) => {
629
+ const { execFile: execFile2 } = await import("node:child_process");
630
+ return new Promise((resolve, reject) => {
631
+ const child = execFile2(
632
+ "git",
633
+ args,
634
+ {
635
+ cwd,
636
+ maxBuffer: 16 * 1024 * 1024,
637
+ env: { ...process.env, GIT_PAGER: "cat", PAGER: "cat" }
638
+ },
639
+ (err, stdout, stderr) => {
640
+ if (err) {
641
+ const e = err;
642
+ reject(new GitCommandError(args, e.code ?? -1, stderr));
643
+ return;
644
+ }
645
+ resolve(stdout);
646
+ }
647
+ );
648
+ child.stdin?.write(input);
649
+ child.stdin?.end();
650
+ });
651
+ };
652
+ var canApplyDiff = async (cwd, unified) => {
653
+ try {
654
+ await runGitWithStdin(cwd, ["apply", "--check"], unified);
655
+ return { ok: true };
656
+ } catch (err) {
657
+ if (err instanceof GitCommandError) {
658
+ return { ok: false, stderr: err.stderr };
659
+ }
660
+ throw err;
661
+ }
662
+ };
663
+ var applyDiff = async (cwd, unified) => {
664
+ await runGitWithStdin(cwd, ["apply"], unified);
665
+ };
666
+ var parseDiffPaths = (unified) => {
667
+ const paths = [];
668
+ for (const line of unified.split("\n")) {
669
+ const match = /^diff --git a\/(.+) b\/(.+)$/.exec(line);
670
+ if (match) {
671
+ paths.push(match[2]);
672
+ }
673
+ }
674
+ return paths;
675
+ };
676
+
677
+ // src/lib/paths.ts
678
+ import { access, lstat, realpath, stat } from "node:fs/promises";
679
+ import path from "node:path";
680
+ var InvalidRepoPathError = class extends Error {
681
+ code;
682
+ constructor(code, message) {
683
+ super(message);
684
+ this.name = "InvalidRepoPathError";
685
+ this.code = code;
686
+ }
687
+ };
688
+ var exists = async (p) => {
689
+ try {
690
+ await access(p);
691
+ return true;
692
+ } catch {
693
+ return false;
694
+ }
695
+ };
696
+ var validateRepoPath = async (raw) => {
697
+ if (typeof raw !== "string") {
698
+ throw new InvalidRepoPathError("not_a_string", "path must be a string");
699
+ }
700
+ const trimmed = raw.trim();
701
+ if (trimmed.length === 0) {
702
+ throw new InvalidRepoPathError("empty", "path must not be empty");
703
+ }
704
+ if (!path.isAbsolute(trimmed)) {
705
+ throw new InvalidRepoPathError(
706
+ "not_absolute",
707
+ `path must be absolute (got: ${trimmed})`
708
+ );
709
+ }
710
+ let resolved;
711
+ try {
712
+ resolved = await realpath(trimmed);
713
+ } catch {
714
+ throw new InvalidRepoPathError(
715
+ "does_not_exist",
716
+ `path does not exist: ${trimmed}`
717
+ );
718
+ }
719
+ const dirStat = await stat(resolved);
720
+ if (!dirStat.isDirectory()) {
721
+ throw new InvalidRepoPathError(
722
+ "not_a_directory",
723
+ `path is not a directory: ${resolved}`
724
+ );
725
+ }
726
+ const gitMarker = path.join(resolved, ".git");
727
+ if (!await exists(gitMarker)) {
728
+ throw new InvalidRepoPathError(
729
+ "not_a_git_repo",
730
+ `path does not contain a .git entry: ${resolved}`
731
+ );
732
+ }
733
+ await lstat(gitMarker);
734
+ return resolved;
735
+ };
736
+
737
+ // src/routes/repo.ts
738
+ var selectRepoSchema = z.object({
739
+ path: z.string().min(1)
740
+ });
741
+ var applyPatchSchema = z.object({
742
+ unified: z.string().min(1).max(8 * 1024 * 1024),
743
+ baselineHash: z.string().optional()
744
+ });
745
+ var buildRepoStatus = async (resolvedPath) => {
746
+ const [branch, head, dirty] = await Promise.all([
747
+ getBranch(resolvedPath),
748
+ getHeadShort(resolvedPath),
749
+ isDirty(resolvedPath)
750
+ ]);
751
+ return {
752
+ path: resolvedPath,
753
+ name: path2.basename(resolvedPath),
754
+ branch,
755
+ head,
756
+ isDirty: dirty
757
+ };
758
+ };
759
+ var sendInvalidRepoPath = (res, err) => {
760
+ res.status(400).json({ error: err.code, message: err.message });
761
+ };
762
+ var sendGitError = (res, err) => {
763
+ res.status(500).json({
764
+ error: "git_command_failed",
765
+ message: err.message,
766
+ exitCode: err.exitCode
767
+ });
768
+ };
769
+ var requireSelectedRepo = (state, res) => {
770
+ const repo = state.getSelectedRepo();
771
+ if (!repo) {
772
+ res.status(409).json({ error: "no_repo_selected" });
773
+ return null;
774
+ }
775
+ return repo;
776
+ };
777
+ var createRepoRouter = (state) => {
778
+ const router = createRouter();
779
+ router.post("/select", async (req, res) => {
780
+ const parsed = selectRepoSchema.safeParse(req.body);
781
+ if (!parsed.success) {
782
+ res.status(400).json({ error: "invalid_body", issues: parsed.error.issues });
783
+ return;
784
+ }
785
+ try {
786
+ const resolved = await validateRepoPath(parsed.data.path);
787
+ const status = await buildRepoStatus(resolved);
788
+ state.setSelectedRepo(status);
789
+ const body = { ok: true, repo: status };
790
+ res.json(body);
791
+ } catch (err) {
792
+ if (err instanceof InvalidRepoPathError) {
793
+ sendInvalidRepoPath(res, err);
794
+ return;
795
+ }
796
+ if (err instanceof GitCommandError) {
797
+ sendGitError(res, err);
798
+ return;
799
+ }
800
+ throw err;
801
+ }
802
+ });
803
+ router.get("/status", async (_req, res) => {
804
+ const repo = state.getSelectedRepo();
805
+ if (!repo) {
806
+ const body = { repo: null };
807
+ res.json(body);
808
+ return;
809
+ }
810
+ try {
811
+ const refreshed = await buildRepoStatus(repo.path);
812
+ state.setSelectedRepo(refreshed);
813
+ const body = { repo: refreshed };
814
+ res.json(body);
815
+ } catch (err) {
816
+ if (err instanceof GitCommandError) {
817
+ sendGitError(res, err);
818
+ return;
819
+ }
820
+ throw err;
821
+ }
822
+ });
823
+ router.get("/diff", async (_req, res) => {
824
+ const repo = requireSelectedRepo(state, res);
825
+ if (!repo) {
826
+ return;
827
+ }
828
+ try {
829
+ const unified = await getDiff(repo.path);
830
+ const body = {
831
+ unified,
832
+ sizeBytes: Buffer.byteLength(unified, "utf8")
833
+ };
834
+ res.json(body);
835
+ } catch (err) {
836
+ if (err instanceof GitCommandError) {
837
+ sendGitError(res, err);
838
+ return;
839
+ }
840
+ throw err;
841
+ }
842
+ });
843
+ router.get("/changed-files", async (_req, res) => {
844
+ const repo = requireSelectedRepo(state, res);
845
+ if (!repo) {
846
+ return;
847
+ }
848
+ try {
849
+ const files = await getStatusShort(repo.path);
850
+ const body = { files };
851
+ res.json(body);
852
+ } catch (err) {
853
+ if (err instanceof GitCommandError) {
854
+ sendGitError(res, err);
855
+ return;
856
+ }
857
+ throw err;
858
+ }
859
+ });
860
+ router.post("/apply", async (req, res) => {
861
+ const repo = requireSelectedRepo(state, res);
862
+ if (!repo) return;
863
+ const parsed = applyPatchSchema.safeParse(req.body);
864
+ if (!parsed.success) {
865
+ res.status(400).json({ error: "invalid_body", issues: parsed.error.issues });
866
+ return;
867
+ }
868
+ try {
869
+ const check = await canApplyDiff(repo.path, parsed.data.unified);
870
+ if (!check.ok) {
871
+ res.status(409).json({
872
+ error: "diff_does_not_apply",
873
+ message: "Diff doesn't apply against the current working tree. The repo may have changed since the diff was proposed.",
874
+ stderr: check.stderr
875
+ });
876
+ return;
877
+ }
878
+ await applyDiff(repo.path, parsed.data.unified);
879
+ const paths = parseDiffPaths(parsed.data.unified);
880
+ const body = {
881
+ ok: true,
882
+ filesChanged: paths.length,
883
+ paths
884
+ };
885
+ res.json(body);
886
+ } catch (err) {
887
+ if (err instanceof GitCommandError) {
888
+ sendGitError(res, err);
889
+ return;
890
+ }
891
+ throw err;
892
+ }
893
+ });
894
+ return router;
895
+ };
896
+
897
+ // src/routes/agent.ts
898
+ import { Router as createRouter2 } from "express";
899
+ import { z as z2 } from "zod";
900
+
901
+ // src/broker.ts
902
+ import { randomUUID } from "node:crypto";
903
+ var LlmBroker = class {
904
+ pending = /* @__PURE__ */ new Map();
905
+ emit;
906
+ timeoutMs;
907
+ overrideProvider;
908
+ overrideModel;
909
+ closed = false;
910
+ constructor(options) {
911
+ this.emit = options.emitToStream;
912
+ this.timeoutMs = options.timeoutMs ?? 9e4;
913
+ this.overrideProvider = options.overrideProvider;
914
+ this.overrideModel = options.overrideModel;
915
+ }
916
+ /** Called by the adapter when it needs an LLM call. Resolves with the
917
+ * full LlmBrokerResponsePayload once the browser fulfils it. Rejects
918
+ * on timeout, broker close, or explicit failure from the browser.
919
+ *
920
+ * Pass `observer` to receive incremental stream chunks if the browser
921
+ * fulfils via the streaming chunk endpoint. Observer is called once
922
+ * per chunk; the Promise still resolves with the final aggregate
923
+ * payload when the stream completes. */
924
+ request(payload, observer) {
925
+ if (this.closed) {
926
+ return Promise.reject(new Error("broker is closed"));
927
+ }
928
+ const requestId = randomUUID();
929
+ return new Promise((resolve, reject) => {
930
+ const timer = setTimeout(() => {
931
+ this.pending.delete(requestId);
932
+ reject(
933
+ new Error(
934
+ `llm_broker_timeout: no response within ${this.timeoutMs}ms`
935
+ )
936
+ );
937
+ }, this.timeoutMs);
938
+ timer.unref?.();
939
+ this.pending.set(requestId, { resolve, reject, timer, observer });
940
+ const effectiveProvider = this.overrideProvider ?? payload.provider;
941
+ const effectiveModel = this.overrideModel ?? payload.model;
942
+ const envelope = {
943
+ type: "llm_request",
944
+ requestId,
945
+ provider: effectiveProvider,
946
+ model: effectiveModel,
947
+ payload: {
948
+ messages: payload.messages,
949
+ tools: payload.tools,
950
+ temperature: payload.temperature,
951
+ systemPrompt: payload.systemPrompt
952
+ }
953
+ };
954
+ try {
955
+ this.emit(envelope);
956
+ } catch (err) {
957
+ clearTimeout(timer);
958
+ this.pending.delete(requestId);
959
+ reject(err instanceof Error ? err : new Error(String(err)));
960
+ }
961
+ });
962
+ }
963
+ /** True iff this broker has a pending request with the given id. Used
964
+ * by the registry to route /agent/llm-response/:requestId without
965
+ * needing the caller to supply sessionId. */
966
+ has(requestId) {
967
+ return this.pending.has(requestId);
968
+ }
969
+ /** Called by the /agent/llm-response route once the browser has POSTed
970
+ * the completion back. Returns true if a pending request matched.
971
+ *
972
+ * If the request was registered with an observer (streaming flow),
973
+ * the observer is also invoked with a synthesised text_delta + a
974
+ * `complete` chunk — this makes the streaming path work even when
975
+ * the browser fulfils via the non-streaming /agent/llm-response
976
+ * route (e.g. for providers that don't support streaming). */
977
+ fulfill(requestId, response) {
978
+ const entry = this.pending.get(requestId);
979
+ if (!entry) return false;
980
+ clearTimeout(entry.timer);
981
+ this.pending.delete(requestId);
982
+ if (entry.observer) {
983
+ try {
984
+ if (response.text) {
985
+ entry.observer({ type: "text_delta", text: response.text });
986
+ }
987
+ entry.observer({ type: "complete", payload: response });
988
+ } catch {
989
+ }
990
+ }
991
+ entry.resolve(response);
992
+ return true;
993
+ }
994
+ /** Called by the /agent/llm-chunk/:requestId route when the browser
995
+ * forwards an incremental stream chunk. The observer (if any) is
996
+ * invoked; a `complete` chunk also resolves the pending Promise. */
997
+ pushChunk(requestId, chunk) {
998
+ const entry = this.pending.get(requestId);
999
+ if (!entry) return false;
1000
+ if (entry.observer) {
1001
+ try {
1002
+ entry.observer(chunk);
1003
+ } catch {
1004
+ }
1005
+ }
1006
+ if (chunk.type === "complete") {
1007
+ clearTimeout(entry.timer);
1008
+ this.pending.delete(requestId);
1009
+ entry.resolve(chunk.payload);
1010
+ } else if (chunk.type === "error") {
1011
+ clearTimeout(entry.timer);
1012
+ this.pending.delete(requestId);
1013
+ entry.reject(new Error(chunk.message));
1014
+ }
1015
+ return true;
1016
+ }
1017
+ /** Called by the /agent/llm-response route when the browser reports a
1018
+ * failure rather than a completion. */
1019
+ fail(requestId, message) {
1020
+ const entry = this.pending.get(requestId);
1021
+ if (!entry) return false;
1022
+ clearTimeout(entry.timer);
1023
+ this.pending.delete(requestId);
1024
+ entry.reject(new Error(message));
1025
+ return true;
1026
+ }
1027
+ /** Reject all in-flight requests. Called when the parent SSE stream
1028
+ * closes so a stranded request doesn't sit forever. */
1029
+ close(reason = "broker_closed") {
1030
+ this.closed = true;
1031
+ for (const [, entry] of this.pending.entries()) {
1032
+ clearTimeout(entry.timer);
1033
+ entry.reject(new Error(reason));
1034
+ }
1035
+ this.pending.clear();
1036
+ }
1037
+ get pendingCount() {
1038
+ return this.pending.size;
1039
+ }
1040
+ };
1041
+ var createBrokerRegistry = () => {
1042
+ const bySession = /* @__PURE__ */ new Map();
1043
+ return {
1044
+ register(sessionId, broker) {
1045
+ const existing = bySession.get(sessionId);
1046
+ if (existing && existing !== broker) {
1047
+ existing.close("superseded_by_new_stream");
1048
+ }
1049
+ bySession.set(sessionId, broker);
1050
+ },
1051
+ unregister(sessionId, broker) {
1052
+ if (bySession.get(sessionId) === broker) {
1053
+ bySession.delete(sessionId);
1054
+ }
1055
+ },
1056
+ get(sessionId) {
1057
+ return bySession.get(sessionId) ?? null;
1058
+ }
1059
+ };
1060
+ };
1061
+
1062
+ // src/routes/agent.ts
1063
+ var historyRoleSchema = z2.enum(["user", "assistant", "system", "tool"]);
1064
+ var agentMessageBodySchema = z2.object({
1065
+ sessionId: z2.string().min(1),
1066
+ prompt: z2.string().min(1).max(2e5),
1067
+ history: z2.array(
1068
+ z2.object({
1069
+ role: historyRoleSchema,
1070
+ content: z2.string()
1071
+ })
1072
+ ).max(200).optional(),
1073
+ /** Optional user-chosen routing for this turn. When absent the cloud
1074
+ * picks the default (CODING_AGENT_DEFAULT_PROVIDER/MODEL). */
1075
+ preferredProvider: z2.string().min(1).max(100).optional(),
1076
+ preferredModel: z2.string().min(1).max(200).optional()
1077
+ });
1078
+ var writeEvent = (res, event) => {
1079
+ res.write(`data: ${JSON.stringify(event)}
1080
+
1081
+ `);
1082
+ };
1083
+ var writeDone = (res) => {
1084
+ res.write("data: [DONE]\n\n");
1085
+ };
1086
+ var createAgentRouter = (deps) => {
1087
+ const router = createRouter2();
1088
+ router.post("/message", async (req, res) => {
1089
+ const parsed = agentMessageBodySchema.safeParse(req.body);
1090
+ if (!parsed.success) {
1091
+ res.status(400).json({ error: "invalid_body", issues: parsed.error.issues });
1092
+ return;
1093
+ }
1094
+ const repo = deps.state.getSelectedRepo();
1095
+ if (!repo) {
1096
+ res.status(409).json({ error: "no_repo_selected" });
1097
+ return;
1098
+ }
1099
+ res.setHeader("Content-Type", "text/event-stream");
1100
+ res.setHeader("Cache-Control", "no-cache, no-transform");
1101
+ res.setHeader("Connection", "keep-alive");
1102
+ res.setHeader("X-Accel-Buffering", "no");
1103
+ res.flushHeaders?.();
1104
+ const controller = new AbortController();
1105
+ res.on("close", () => {
1106
+ if (!res.writableEnded) {
1107
+ controller.abort();
1108
+ }
1109
+ });
1110
+ const broker = new LlmBroker({
1111
+ emitToStream: (event) => writeEvent(res, event),
1112
+ overrideProvider: parsed.data.preferredProvider,
1113
+ overrideModel: parsed.data.preferredModel
1114
+ });
1115
+ deps.brokerRegistry.register(parsed.data.sessionId, broker);
1116
+ try {
1117
+ const history = parsed.data.history ? parsed.data.history.map((m) => ({ role: m.role, content: m.content })) : void 0;
1118
+ for await (const event of deps.adapter.streamMessage({
1119
+ sessionId: parsed.data.sessionId,
1120
+ repoPath: repo.path,
1121
+ prompt: parsed.data.prompt,
1122
+ history,
1123
+ signal: controller.signal,
1124
+ broker: (payload) => broker.request(payload),
1125
+ preferredProvider: parsed.data.preferredProvider,
1126
+ preferredModel: parsed.data.preferredModel
1127
+ })) {
1128
+ if (controller.signal.aborted) {
1129
+ break;
1130
+ }
1131
+ writeEvent(res, event);
1132
+ }
1133
+ writeDone(res);
1134
+ } catch (err) {
1135
+ if (err && typeof err === "object" && "name" in err && err.name === "AbortError") {
1136
+ writeDone(res);
1137
+ } else {
1138
+ const message = err instanceof Error ? err.message : "Adapter stream failed";
1139
+ writeEvent(res, { type: "error", message });
1140
+ writeDone(res);
1141
+ }
1142
+ } finally {
1143
+ broker.close("stream_closed");
1144
+ deps.brokerRegistry.unregister(parsed.data.sessionId, broker);
1145
+ res.end();
1146
+ }
1147
+ });
1148
+ return router;
1149
+ };
1150
+ var llmResponseBodySchema = z2.object({
1151
+ sessionId: z2.string().min(1),
1152
+ text: z2.string(),
1153
+ provider: z2.string().min(1),
1154
+ model: z2.string().min(1),
1155
+ tokenUsage: z2.object({ input: z2.number().nonnegative(), output: z2.number().nonnegative() }).optional(),
1156
+ error: z2.string().optional(),
1157
+ toolCalls: z2.array(
1158
+ z2.object({
1159
+ id: z2.string().min(1),
1160
+ type: z2.literal("function"),
1161
+ function: z2.object({
1162
+ name: z2.string().min(1),
1163
+ arguments: z2.string()
1164
+ })
1165
+ })
1166
+ ).optional(),
1167
+ finishReason: z2.enum(["stop", "tool_calls", "length", "content_filter"]).optional()
1168
+ });
1169
+ var llmChunkBodySchema = z2.object({
1170
+ sessionId: z2.string().min(1),
1171
+ chunk: z2.union([
1172
+ z2.object({
1173
+ type: z2.literal("text_delta"),
1174
+ text: z2.string()
1175
+ }),
1176
+ z2.object({
1177
+ type: z2.literal("tool_call_delta"),
1178
+ index: z2.number().int().nonnegative(),
1179
+ id: z2.string().optional(),
1180
+ name: z2.string().optional(),
1181
+ argumentsDelta: z2.string().optional()
1182
+ }),
1183
+ z2.object({
1184
+ type: z2.literal("complete"),
1185
+ payload: z2.object({
1186
+ text: z2.string(),
1187
+ provider: z2.string(),
1188
+ model: z2.string(),
1189
+ tokenUsage: z2.object({
1190
+ input: z2.number().nonnegative(),
1191
+ output: z2.number().nonnegative()
1192
+ }).optional(),
1193
+ finishReason: z2.enum(["stop", "tool_calls", "length", "content_filter"]).optional(),
1194
+ toolCalls: z2.array(
1195
+ z2.object({
1196
+ id: z2.string(),
1197
+ type: z2.literal("function"),
1198
+ function: z2.object({
1199
+ name: z2.string(),
1200
+ arguments: z2.string()
1201
+ })
1202
+ })
1203
+ ).optional()
1204
+ })
1205
+ }),
1206
+ z2.object({
1207
+ type: z2.literal("error"),
1208
+ message: z2.string()
1209
+ })
1210
+ ])
1211
+ });
1212
+ var createLlmResponseRouter = (deps) => {
1213
+ const router = createRouter2();
1214
+ router.post("/:requestId", (req, res) => {
1215
+ const requestId = req.params.requestId;
1216
+ if (!requestId) {
1217
+ res.status(400).json({ error: "missing_request_id" });
1218
+ return;
1219
+ }
1220
+ const parsed = llmResponseBodySchema.safeParse(req.body);
1221
+ if (!parsed.success) {
1222
+ res.status(400).json({ error: "invalid_body", issues: parsed.error.issues });
1223
+ return;
1224
+ }
1225
+ const broker = deps.brokerRegistry.get(parsed.data.sessionId);
1226
+ if (!broker) {
1227
+ res.status(404).json({ error: "no_active_stream_for_session" });
1228
+ return;
1229
+ }
1230
+ if (!broker.has(requestId)) {
1231
+ res.status(404).json({ error: "unknown_request_id" });
1232
+ return;
1233
+ }
1234
+ if (parsed.data.error) {
1235
+ broker.fail(requestId, parsed.data.error);
1236
+ res.json({ ok: true });
1237
+ return;
1238
+ }
1239
+ const payload = {
1240
+ text: parsed.data.text,
1241
+ provider: parsed.data.provider,
1242
+ model: parsed.data.model,
1243
+ tokenUsage: parsed.data.tokenUsage,
1244
+ ...parsed.data.toolCalls ? { toolCalls: parsed.data.toolCalls } : {},
1245
+ ...parsed.data.finishReason ? { finishReason: parsed.data.finishReason } : {}
1246
+ };
1247
+ broker.fulfill(requestId, payload);
1248
+ res.json({ ok: true });
1249
+ });
1250
+ return router;
1251
+ };
1252
+ var createLlmChunkRouter = (deps) => {
1253
+ const router = createRouter2();
1254
+ router.post("/:requestId", (req, res) => {
1255
+ const requestId = req.params.requestId;
1256
+ if (!requestId) {
1257
+ res.status(400).json({ error: "missing_request_id" });
1258
+ return;
1259
+ }
1260
+ const parsed = llmChunkBodySchema.safeParse(req.body);
1261
+ if (!parsed.success) {
1262
+ res.status(400).json({ error: "invalid_body", issues: parsed.error.issues });
1263
+ return;
1264
+ }
1265
+ const broker = deps.brokerRegistry.get(parsed.data.sessionId);
1266
+ if (!broker) {
1267
+ res.status(404).json({ error: "no_active_stream_for_session" });
1268
+ return;
1269
+ }
1270
+ const accepted = broker.pushChunk(
1271
+ requestId,
1272
+ parsed.data.chunk
1273
+ );
1274
+ if (!accepted) {
1275
+ res.status(404).json({ error: "unknown_request_id" });
1276
+ return;
1277
+ }
1278
+ res.json({ ok: true });
1279
+ });
1280
+ return router;
1281
+ };
1282
+
1283
+ // src/routes/llm-shim.ts
1284
+ import { Router as createRouter3 } from "express";
1285
+
1286
+ // src/shim/openai-translator.ts
1287
+ var translateRequest = (req) => {
1288
+ let systemPrompt;
1289
+ const messages = [];
1290
+ for (const msg of req.messages) {
1291
+ if (msg.role === "system") {
1292
+ const text = typeof msg.content === "string" ? msg.content : "";
1293
+ systemPrompt = systemPrompt === void 0 ? text : `${systemPrompt}
1294
+
1295
+ ${text}`;
1296
+ continue;
1297
+ }
1298
+ let content = typeof msg.content === "string" ? msg.content : "";
1299
+ if (msg.role === "assistant" && msg.tool_calls && msg.tool_calls.length > 0) {
1300
+ const callsLine = msg.tool_calls.map(
1301
+ (c) => `[tool_call ${c.function.name} args=${c.function.arguments}]`
1302
+ ).join("\n");
1303
+ content = content ? `${content}
1304
+ ${callsLine}` : callsLine;
1305
+ }
1306
+ if (msg.role === "tool" && msg.tool_call_id) {
1307
+ content = `[tool_result id=${msg.tool_call_id}] ${content}`;
1308
+ }
1309
+ messages.push({ role: msg.role, content });
1310
+ }
1311
+ return {
1312
+ provider: void 0,
1313
+ // cloud picks the default — see CODING_AGENT_DEFAULT_PROVIDER
1314
+ model: req.model,
1315
+ // pass through for telemetry; cloud may override
1316
+ systemPrompt,
1317
+ temperature: req.temperature,
1318
+ messages,
1319
+ tools: req.tools
1320
+ };
1321
+ };
1322
+ var translateResponse = (response, id, modelEcho) => {
1323
+ const hasToolCalls = response.toolCalls && response.toolCalls.length > 0;
1324
+ const finish_reason = response.finishReason ?? (hasToolCalls ? "tool_calls" : "stop");
1325
+ return {
1326
+ id,
1327
+ object: "chat.completion",
1328
+ created: Math.floor(Date.now() / 1e3),
1329
+ // Echo the model the client requested. The cloud may have routed to
1330
+ // a different actual model; we surface that in `response.model` but
1331
+ // OpenAI clients expect the echoed model id to match their request.
1332
+ model: modelEcho,
1333
+ choices: [
1334
+ {
1335
+ index: 0,
1336
+ message: {
1337
+ role: "assistant",
1338
+ content: response.text || null,
1339
+ ...hasToolCalls ? { tool_calls: response.toolCalls } : {}
1340
+ },
1341
+ finish_reason
1342
+ }
1343
+ ],
1344
+ ...response.tokenUsage ? {
1345
+ usage: {
1346
+ prompt_tokens: response.tokenUsage.input,
1347
+ completion_tokens: response.tokenUsage.output,
1348
+ total_tokens: response.tokenUsage.input + response.tokenUsage.output
1349
+ }
1350
+ } : {}
1351
+ };
1352
+ };
1353
+
1354
+ // src/shim/active-broker.ts
1355
+ import { randomUUID as randomUUID2 } from "node:crypto";
1356
+ var byToken = /* @__PURE__ */ new Map();
1357
+ var bindActiveBroker = (broker) => {
1358
+ const token = `dlg-${randomUUID2()}`;
1359
+ byToken.set(token, { token, broker });
1360
+ return {
1361
+ token,
1362
+ release() {
1363
+ const current = byToken.get(token);
1364
+ if (current && current.broker === broker) {
1365
+ byToken.delete(token);
1366
+ }
1367
+ }
1368
+ };
1369
+ };
1370
+ var getActiveBrokerByToken = (token) => {
1371
+ return byToken.get(token)?.broker ?? null;
1372
+ };
1373
+
1374
+ // src/routes/llm-shim.ts
1375
+ var extractBearer = (req) => {
1376
+ const header = req.header("authorization");
1377
+ if (!header) return null;
1378
+ if (!header.toLowerCase().startsWith("bearer ")) return null;
1379
+ return header.slice(7).trim() || null;
1380
+ };
1381
+ var sendOpenAIError = (res, status, message, type = "invalid_request_error") => {
1382
+ res.status(status).json({ error: { message, type } });
1383
+ };
1384
+ var createLlmShimRouter = () => {
1385
+ const router = createRouter3();
1386
+ router.post(
1387
+ "/v1/chat/completions",
1388
+ async (req, res) => {
1389
+ const token = extractBearer(req);
1390
+ if (!token) {
1391
+ sendOpenAIError(res, 401, "Missing Authorization bearer", "auth_error");
1392
+ return;
1393
+ }
1394
+ const broker = getActiveBrokerByToken(token);
1395
+ if (!broker) {
1396
+ sendOpenAIError(
1397
+ res,
1398
+ 401,
1399
+ "No active coding-agent stream is currently authorised on this helper.",
1400
+ "auth_error"
1401
+ );
1402
+ return;
1403
+ }
1404
+ const body = req.body;
1405
+ if (!body || typeof body !== "object" || !Array.isArray(body.messages) || body.messages.length === 0) {
1406
+ sendOpenAIError(res, 400, "messages[] is required and must be non-empty");
1407
+ return;
1408
+ }
1409
+ const brokerRequest = translateRequest(body);
1410
+ const completionId = `chatcmpl-${Math.random().toString(36).slice(2, 10)}`;
1411
+ const stream = body.stream === true;
1412
+ if (stream) {
1413
+ res.setHeader("Content-Type", "text/event-stream");
1414
+ res.setHeader("Cache-Control", "no-cache, no-transform");
1415
+ res.setHeader("Connection", "keep-alive");
1416
+ res.flushHeaders?.();
1417
+ const writeOpenAi = (delta) => {
1418
+ res.write(`data: ${JSON.stringify(delta)}
1419
+
1420
+ `);
1421
+ };
1422
+ writeOpenAi({
1423
+ id: completionId,
1424
+ object: "chat.completion.chunk",
1425
+ created: Math.floor(Date.now() / 1e3),
1426
+ model: body.model,
1427
+ choices: [
1428
+ { index: 0, delta: { role: "assistant" }, finish_reason: null }
1429
+ ]
1430
+ });
1431
+ const finalPayloadRef = { current: null };
1432
+ const observer = (chunk) => {
1433
+ switch (chunk.type) {
1434
+ case "text_delta":
1435
+ writeOpenAi({
1436
+ id: completionId,
1437
+ object: "chat.completion.chunk",
1438
+ created: Math.floor(Date.now() / 1e3),
1439
+ model: body.model,
1440
+ choices: [
1441
+ {
1442
+ index: 0,
1443
+ delta: { content: chunk.text },
1444
+ finish_reason: null
1445
+ }
1446
+ ]
1447
+ });
1448
+ break;
1449
+ case "tool_call_delta":
1450
+ writeOpenAi({
1451
+ id: completionId,
1452
+ object: "chat.completion.chunk",
1453
+ created: Math.floor(Date.now() / 1e3),
1454
+ model: body.model,
1455
+ choices: [
1456
+ {
1457
+ index: 0,
1458
+ delta: {
1459
+ tool_calls: [
1460
+ {
1461
+ index: chunk.index,
1462
+ ...chunk.id ? { id: chunk.id } : {},
1463
+ ...chunk.id || chunk.name ? { type: "function" } : {},
1464
+ ...chunk.name || chunk.argumentsDelta !== void 0 ? {
1465
+ function: {
1466
+ ...chunk.name ? { name: chunk.name } : {},
1467
+ ...chunk.argumentsDelta !== void 0 ? { arguments: chunk.argumentsDelta } : {}
1468
+ }
1469
+ } : {}
1470
+ }
1471
+ ]
1472
+ },
1473
+ finish_reason: null
1474
+ }
1475
+ ]
1476
+ });
1477
+ break;
1478
+ case "complete":
1479
+ finalPayloadRef.current = translateResponse(
1480
+ chunk.payload,
1481
+ completionId,
1482
+ body.model
1483
+ );
1484
+ break;
1485
+ case "error":
1486
+ break;
1487
+ }
1488
+ };
1489
+ try {
1490
+ await broker.request(brokerRequest, observer);
1491
+ } catch (err) {
1492
+ writeOpenAi({
1493
+ id: completionId,
1494
+ object: "chat.completion.chunk",
1495
+ created: Math.floor(Date.now() / 1e3),
1496
+ model: body.model,
1497
+ choices: [
1498
+ {
1499
+ index: 0,
1500
+ delta: {},
1501
+ finish_reason: "stop"
1502
+ }
1503
+ ]
1504
+ });
1505
+ res.write("data: [DONE]\n\n");
1506
+ res.end();
1507
+ void err;
1508
+ return;
1509
+ }
1510
+ const finishReason = finalPayloadRef.current?.choices[0]?.finish_reason ?? "stop";
1511
+ writeOpenAi({
1512
+ id: completionId,
1513
+ object: "chat.completion.chunk",
1514
+ created: Math.floor(Date.now() / 1e3),
1515
+ model: body.model,
1516
+ choices: [
1517
+ {
1518
+ index: 0,
1519
+ delta: {},
1520
+ finish_reason: finishReason
1521
+ }
1522
+ ]
1523
+ });
1524
+ res.write("data: [DONE]\n\n");
1525
+ res.end();
1526
+ return;
1527
+ }
1528
+ let brokerResponse;
1529
+ try {
1530
+ brokerResponse = await broker.request(brokerRequest);
1531
+ } catch (err) {
1532
+ sendOpenAIError(
1533
+ res,
1534
+ 502,
1535
+ err instanceof Error ? err.message : "Broker call failed",
1536
+ "api_error"
1537
+ );
1538
+ return;
1539
+ }
1540
+ if (brokerResponse.error) {
1541
+ sendOpenAIError(res, 502, brokerResponse.error, "api_error");
1542
+ return;
1543
+ }
1544
+ const out = translateResponse(brokerResponse, completionId, body.model);
1545
+ res.json(out);
1546
+ }
1547
+ );
1548
+ return router;
1549
+ };
1550
+
1551
+ // src/adapters/mock-adapter.ts
1552
+ import { setTimeout as delay } from "node:timers/promises";
1553
+ var RESPONSE_FRAGMENTS = [
1554
+ "Got it \u2014 looking at",
1555
+ " the repo at",
1556
+ " {repoPath}.",
1557
+ "\n\nThis is a mock response from MockOpenCodeAdapter,",
1558
+ " so the actual analysis hasn't run.",
1559
+ " The real opencode integration lands in Step 9; until then",
1560
+ " every prompt produces this same canned reply plus a small synthetic",
1561
+ " diff so you can exercise the apply flow safely."
1562
+ ];
1563
+ var SYNTHETIC_DIFF = {
1564
+ type: "diff_proposed",
1565
+ unified: [
1566
+ "diff --git a/MOCK_NOTES.md b/MOCK_NOTES.md",
1567
+ "new file mode 100644",
1568
+ "index 0000000..1111111",
1569
+ "--- /dev/null",
1570
+ "+++ b/MOCK_NOTES.md",
1571
+ "@@ -0,0 +1,3 @@",
1572
+ "+# Mock notes",
1573
+ "+",
1574
+ "+Created by MockOpenCodeAdapter to demonstrate the diff flow.",
1575
+ ""
1576
+ ].join("\n"),
1577
+ files: [
1578
+ {
1579
+ path: "MOCK_NOTES.md",
1580
+ additions: 3,
1581
+ deletions: 0,
1582
+ status: "added"
1583
+ }
1584
+ ]
1585
+ };
1586
+ var MockOpenCodeAdapter = class {
1587
+ constructor(options = {}) {
1588
+ this.options = options;
1589
+ }
1590
+ name = "mock";
1591
+ async *streamMessage(request) {
1592
+ const tokenDelay = this.options.tokenDelayMs ?? 0;
1593
+ let brokerPrefix = null;
1594
+ if (this.options.useBroker && request.broker) {
1595
+ this.throwIfAborted(request.signal);
1596
+ try {
1597
+ const response = await request.broker({
1598
+ provider: this.options.brokerProvider,
1599
+ model: this.options.brokerModel,
1600
+ messages: [
1601
+ { role: "user", content: request.prompt }
1602
+ ]
1603
+ });
1604
+ brokerPrefix = `[brokered via ${response.provider}:${response.model}] ` + response.text.slice(0, 200);
1605
+ } catch (err) {
1606
+ yield {
1607
+ type: "error",
1608
+ message: `Brokered LLM call failed: ${err.message}`,
1609
+ recoverable: true
1610
+ };
1611
+ }
1612
+ }
1613
+ if (brokerPrefix) {
1614
+ yield { type: "text_delta", text: brokerPrefix + "\n\n" };
1615
+ }
1616
+ for (const fragment of RESPONSE_FRAGMENTS) {
1617
+ this.throwIfAborted(request.signal);
1618
+ const text = fragment.replace("{repoPath}", request.repoPath);
1619
+ yield { type: "text_delta", text };
1620
+ if (tokenDelay > 0) {
1621
+ await delay(tokenDelay);
1622
+ }
1623
+ }
1624
+ if (!this.options.skipToolCall) {
1625
+ this.throwIfAborted(request.signal);
1626
+ const toolCallId = `mock-tool-${Date.now()}`;
1627
+ yield {
1628
+ type: "tool_call_start",
1629
+ toolCallId,
1630
+ tool: "fs.read",
1631
+ args: { path: "README.md" }
1632
+ };
1633
+ if (tokenDelay > 0) {
1634
+ await delay(tokenDelay);
1635
+ }
1636
+ yield {
1637
+ type: "tool_call_end",
1638
+ toolCallId,
1639
+ ok: true,
1640
+ summary: "Read README.md (mock \u2014 no actual file IO performed)"
1641
+ };
1642
+ }
1643
+ if (!this.options.skipDiff) {
1644
+ this.throwIfAborted(request.signal);
1645
+ yield SYNTHETIC_DIFF;
1646
+ }
1647
+ this.throwIfAborted(request.signal);
1648
+ yield {
1649
+ type: "done",
1650
+ messageId: this.options.fixedMessageId ?? `mock-msg-${request.sessionId}-${Date.now()}`
1651
+ };
1652
+ }
1653
+ throwIfAborted(signal) {
1654
+ if (signal?.aborted) {
1655
+ const err = new Error("aborted");
1656
+ err.name = "AbortError";
1657
+ throw err;
1658
+ }
1659
+ }
1660
+ };
1661
+
1662
+ // src/adapters/opencode-event-mapper.ts
1663
+ var createMapState = (ourSessionId, openCodeSessionId) => ({
1664
+ ourSessionId,
1665
+ openCodeSessionId,
1666
+ startedTools: /* @__PURE__ */ new Set(),
1667
+ endedTools: /* @__PURE__ */ new Set(),
1668
+ announcedPatches: /* @__PURE__ */ new Set()
1669
+ });
1670
+ var EMPTY = { events: [], done: false };
1671
+ var mapEvent = (item, state) => {
1672
+ const sid = item.properties?.sessionID;
1673
+ if (sid && sid !== state.openCodeSessionId) {
1674
+ return EMPTY;
1675
+ }
1676
+ if (item.type === "session.idle" && sid === state.openCodeSessionId) {
1677
+ return { events: [{ type: "done" }], done: true };
1678
+ }
1679
+ if (item.type !== "message.part.updated") {
1680
+ return EMPTY;
1681
+ }
1682
+ const part = item.properties?.part;
1683
+ if (!part) return EMPTY;
1684
+ switch (part.type) {
1685
+ case "text": {
1686
+ const delta = item.properties?.delta ?? part.text ?? "";
1687
+ if (!delta) return EMPTY;
1688
+ return { events: [{ type: "text_delta", text: delta }], done: false };
1689
+ }
1690
+ case "tool": {
1691
+ const callId = part.callID ?? part.id ?? "";
1692
+ const tool = part.tool ?? "tool";
1693
+ const status = part.state?.status;
1694
+ if (!callId || !status) return EMPTY;
1695
+ if (status === "pending" || status === "running") {
1696
+ if (state.startedTools.has(callId)) {
1697
+ return EMPTY;
1698
+ }
1699
+ state.startedTools.add(callId);
1700
+ const event = {
1701
+ type: "tool_call_start",
1702
+ toolCallId: callId,
1703
+ tool,
1704
+ args: part.state?.input
1705
+ };
1706
+ return { events: [event], done: false };
1707
+ }
1708
+ if (status === "completed" || status === "error") {
1709
+ if (state.endedTools.has(callId)) {
1710
+ return EMPTY;
1711
+ }
1712
+ state.endedTools.add(callId);
1713
+ const event = {
1714
+ type: "tool_call_end",
1715
+ toolCallId: callId,
1716
+ ok: status === "completed",
1717
+ summary: status === "error" ? part.state?.error ?? "Tool failed" : part.state?.title
1718
+ };
1719
+ return { events: [event], done: false };
1720
+ }
1721
+ return EMPTY;
1722
+ }
1723
+ case "patch": {
1724
+ const hash = part.hash ?? "";
1725
+ if (!hash || state.announcedPatches.has(hash)) {
1726
+ return EMPTY;
1727
+ }
1728
+ state.announcedPatches.add(hash);
1729
+ const files = part.files ?? [];
1730
+ return {
1731
+ events: [
1732
+ {
1733
+ type: "diff_proposed",
1734
+ unified: "",
1735
+ files: files.map((path6) => ({
1736
+ path: path6,
1737
+ additions: 0,
1738
+ deletions: 0,
1739
+ status: "modified"
1740
+ }))
1741
+ }
1742
+ ],
1743
+ done: false,
1744
+ patchToFetch: { hash, files }
1745
+ };
1746
+ }
1747
+ default:
1748
+ return EMPTY;
1749
+ }
1750
+ };
1751
+
1752
+ // src/adapters/engine-locator.ts
1753
+ var ENGINE_LOCATOR_ENV = "LOCAL_AGENT_ENGINE";
1754
+ var DEFAULT_LOCATOR_NAME = "local";
1755
+ var isLocatorName = (value) => value === "npm" || value === "local";
1756
+ var createEngineLocator = async (name) => {
1757
+ const resolved = name ?? process.env[ENGINE_LOCATOR_ENV] ?? DEFAULT_LOCATOR_NAME;
1758
+ if (!isLocatorName(resolved)) {
1759
+ throw new Error(
1760
+ `[engine-locator] Unknown engine "${resolved}". Set ${ENGINE_LOCATOR_ENV} to "npm" or "local".`
1761
+ );
1762
+ }
1763
+ if (resolved === "npm") {
1764
+ const { NpmSdkLocator: NpmSdkLocator2 } = await Promise.resolve().then(() => (init_engine_locator_npm(), engine_locator_npm_exports));
1765
+ return new NpmSdkLocator2();
1766
+ }
1767
+ const { LocalEngineLocator: LocalEngineLocator2 } = await Promise.resolve().then(() => (init_engine_locator_local(), engine_locator_local_exports));
1768
+ return new LocalEngineLocator2();
1769
+ };
1770
+
1771
+ // src/adapters/opencode-adapter.ts
1772
+ init_engine_locator_npm();
1773
+ var DIOLOGUE_PROVIDER_ID = "diologue";
1774
+ var DIOLOGUE_MODEL_ID = "diologue-routed";
1775
+ var OpenCodeProcessAdapter = class {
1776
+ constructor(options = {}) {
1777
+ this.options = options;
1778
+ }
1779
+ name = "opencode";
1780
+ serverPromise = null;
1781
+ /** Maps our sessionId → opencode session id so successive turns within
1782
+ * one coding-agent session reuse the same opencode conversation. */
1783
+ sessionMap = /* @__PURE__ */ new Map();
1784
+ async resolveLocator() {
1785
+ if (this.options.engineLocator) {
1786
+ return this.options.engineLocator;
1787
+ }
1788
+ if (this.options.sdkLoader) {
1789
+ return new NpmSdkLocator({
1790
+ sdkLoader: this.options.sdkLoader
1791
+ });
1792
+ }
1793
+ return createEngineLocator();
1794
+ }
1795
+ resolveHelperBaseUrl() {
1796
+ if (this.options.helperBaseUrl) return this.options.helperBaseUrl;
1797
+ const port = process.env.LOCAL_AGENT_PORT ?? "4099";
1798
+ return `http://127.0.0.1:${port}`;
1799
+ }
1800
+ buildShimConfig() {
1801
+ const baseURL = `${this.resolveHelperBaseUrl()}/llm-shim/v1`;
1802
+ return {
1803
+ provider: {
1804
+ [DIOLOGUE_PROVIDER_ID]: {
1805
+ name: "Diologue (brokered)",
1806
+ // openai-compatible AI SDK provider — present in opencode's
1807
+ // bundled providers. Any other custom-base-URL provider would
1808
+ // work too; openai is the most permissive.
1809
+ npm: "@ai-sdk/openai",
1810
+ models: {
1811
+ [DIOLOGUE_MODEL_ID]: {
1812
+ id: DIOLOGUE_MODEL_ID,
1813
+ name: "Diologue brokered model"
1814
+ }
1815
+ },
1816
+ options: {
1817
+ baseURL,
1818
+ // The real apiKey gets injected per-stream via the
1819
+ // active-broker handle. This placeholder is what opencode
1820
+ // sees at config time; we'll override via env at runtime.
1821
+ apiKey: "placeholder-will-be-replaced"
1822
+ }
1823
+ }
1824
+ },
1825
+ model: `${DIOLOGUE_PROVIDER_ID}/${DIOLOGUE_MODEL_ID}`
1826
+ };
1827
+ }
1828
+ async ensureServer() {
1829
+ if (this.serverPromise) {
1830
+ return this.serverPromise;
1831
+ }
1832
+ this.serverPromise = (async () => {
1833
+ const locator = await this.resolveLocator();
1834
+ return locator.start({ config: this.buildShimConfig() });
1835
+ })();
1836
+ try {
1837
+ return await this.serverPromise;
1838
+ } catch (err) {
1839
+ this.serverPromise = null;
1840
+ throw err;
1841
+ }
1842
+ }
1843
+ async ensureOpencodeSession(handle, ourSessionId, repoPath, title) {
1844
+ const existing = this.sessionMap.get(ourSessionId);
1845
+ if (existing) return existing;
1846
+ const created = await handle.client.session.create({
1847
+ body: { title },
1848
+ query: { directory: repoPath }
1849
+ });
1850
+ const id = created.data?.id;
1851
+ if (!id) {
1852
+ throw new Error("opencode session.create returned no id");
1853
+ }
1854
+ this.sessionMap.set(ourSessionId, id);
1855
+ return id;
1856
+ }
1857
+ async *streamMessage(request) {
1858
+ let handle;
1859
+ try {
1860
+ handle = await this.ensureServer();
1861
+ } catch (err) {
1862
+ yield {
1863
+ type: "error",
1864
+ message: err instanceof Error ? err.message : "Failed to start opencode server",
1865
+ recoverable: false
1866
+ };
1867
+ return;
1868
+ }
1869
+ let openCodeSessionId;
1870
+ try {
1871
+ openCodeSessionId = await this.ensureOpencodeSession(
1872
+ handle,
1873
+ request.sessionId,
1874
+ request.repoPath,
1875
+ request.prompt.slice(0, 80)
1876
+ );
1877
+ } catch (err) {
1878
+ yield {
1879
+ type: "error",
1880
+ message: err instanceof Error ? err.message : "Failed to start opencode session",
1881
+ recoverable: true
1882
+ };
1883
+ return;
1884
+ }
1885
+ const mapState = createMapState(
1886
+ request.sessionId,
1887
+ openCodeSessionId
1888
+ );
1889
+ let sawAnyPatch = false;
1890
+ let lastPatchHash = "";
1891
+ let activeBrokerHandle = null;
1892
+ let streamScopedBroker = null;
1893
+ if (request.broker) {
1894
+ const callback = request.broker;
1895
+ streamScopedBroker = new LlmBroker({
1896
+ emitToStream: () => {
1897
+ }
1898
+ });
1899
+ const realCallback = callback;
1900
+ streamScopedBroker.request = async (payload) => {
1901
+ return realCallback(payload);
1902
+ };
1903
+ activeBrokerHandle = bindActiveBroker(streamScopedBroker);
1904
+ }
1905
+ const eventController = new AbortController();
1906
+ const stopOnAbort = () => eventController.abort();
1907
+ if (request.signal) {
1908
+ if (request.signal.aborted) {
1909
+ return;
1910
+ }
1911
+ request.signal.addEventListener("abort", stopOnAbort);
1912
+ }
1913
+ let eventStream;
1914
+ try {
1915
+ const res = await handle.client.global.event({
1916
+ signal: eventController.signal
1917
+ });
1918
+ eventStream = res.stream;
1919
+ } catch (err) {
1920
+ yield {
1921
+ type: "error",
1922
+ message: err instanceof Error ? err.message : "Failed to subscribe to opencode events",
1923
+ recoverable: true
1924
+ };
1925
+ request.signal?.removeEventListener("abort", stopOnAbort);
1926
+ return;
1927
+ }
1928
+ const promptBody = {
1929
+ parts: [{ type: "text", text: request.prompt }]
1930
+ };
1931
+ if (streamScopedBroker && activeBrokerHandle) {
1932
+ promptBody.model = {
1933
+ providerID: DIOLOGUE_PROVIDER_ID,
1934
+ modelID: DIOLOGUE_MODEL_ID
1935
+ };
1936
+ }
1937
+ const promptPromise = handle.client.session.prompt({
1938
+ path: { id: openCodeSessionId },
1939
+ query: { directory: request.repoPath },
1940
+ body: promptBody
1941
+ }).catch((err) => {
1942
+ return err instanceof Error ? err : new Error(String(err));
1943
+ });
1944
+ try {
1945
+ for await (const item of eventStream) {
1946
+ if (request.signal?.aborted) break;
1947
+ const mapped = mapEvent(item, mapState);
1948
+ for (const event of mapped.events) {
1949
+ if (event.type === "diff_proposed" && mapped.patchToFetch) {
1950
+ sawAnyPatch = true;
1951
+ lastPatchHash = mapped.patchToFetch.hash;
1952
+ }
1953
+ yield event;
1954
+ }
1955
+ if (mapped.done) {
1956
+ break;
1957
+ }
1958
+ }
1959
+ } finally {
1960
+ eventController.abort();
1961
+ request.signal?.removeEventListener("abort", stopOnAbort);
1962
+ activeBrokerHandle?.release();
1963
+ streamScopedBroker?.close("turn_ended");
1964
+ }
1965
+ if (sawAnyPatch) {
1966
+ try {
1967
+ const diffRes = await handle.client.session.diff({
1968
+ path: { id: openCodeSessionId },
1969
+ query: { directory: request.repoPath }
1970
+ });
1971
+ const unified = diffRes.data?.unified ?? "";
1972
+ if (unified) {
1973
+ yield {
1974
+ type: "diff_proposed",
1975
+ unified,
1976
+ files: parseUnifiedDiffFiles(unified)
1977
+ };
1978
+ }
1979
+ } catch {
1980
+ }
1981
+ }
1982
+ const promptResult = await promptPromise;
1983
+ if (promptResult instanceof Error) {
1984
+ yield { type: "error", message: promptResult.message, recoverable: true };
1985
+ }
1986
+ void lastPatchHash;
1987
+ }
1988
+ async close() {
1989
+ if (!this.serverPromise) return;
1990
+ try {
1991
+ const handle = await this.serverPromise;
1992
+ await handle.close();
1993
+ } catch {
1994
+ } finally {
1995
+ this.serverPromise = null;
1996
+ this.sessionMap.clear();
1997
+ }
1998
+ }
1999
+ };
2000
+ var parseUnifiedDiffFiles = (unified) => {
2001
+ const files = [];
2002
+ let current = null;
2003
+ let oldPath = "";
2004
+ let newPath = "";
2005
+ for (const line of unified.split("\n")) {
2006
+ if (line.startsWith("diff --git")) {
2007
+ if (current) files.push(current);
2008
+ const match = /^diff --git a\/(.+) b\/(.+)$/.exec(line);
2009
+ if (match) {
2010
+ oldPath = match[1];
2011
+ newPath = match[2];
2012
+ } else {
2013
+ oldPath = "";
2014
+ newPath = "";
2015
+ }
2016
+ current = {
2017
+ path: newPath || oldPath,
2018
+ additions: 0,
2019
+ deletions: 0,
2020
+ status: oldPath === newPath ? "modified" : "renamed"
2021
+ };
2022
+ continue;
2023
+ }
2024
+ if (!current) continue;
2025
+ if (line.startsWith("--- /dev/null")) {
2026
+ current.status = "added";
2027
+ } else if (line.startsWith("+++ /dev/null")) {
2028
+ current.status = "deleted";
2029
+ } else if (line.startsWith("+") && !line.startsWith("+++")) {
2030
+ current.additions++;
2031
+ } else if (line.startsWith("-") && !line.startsWith("---")) {
2032
+ current.deletions++;
2033
+ }
2034
+ }
2035
+ if (current) files.push(current);
2036
+ return files;
2037
+ };
2038
+
2039
+ // src/adapters/index.ts
2040
+ var buildDefaultAdapter = () => {
2041
+ const forced = (process.env.LOCAL_AGENT_ADAPTER ?? "").trim().toLowerCase();
2042
+ if (forced === "mock") {
2043
+ return new MockOpenCodeAdapter({ tokenDelayMs: 40 });
2044
+ }
2045
+ return new OpenCodeProcessAdapter();
2046
+ };
2047
+
2048
+ // src/server.ts
2049
+ var createApp = (options) => {
2050
+ const state = options.state ?? createState();
2051
+ const adapter = options.adapter ?? buildDefaultAdapter();
2052
+ const brokerRegistry = createBrokerRegistry();
2053
+ const app = express();
2054
+ app.use(express.json({ limit: "1mb" }));
2055
+ app.use(createCorsMiddleware({ allowedOrigin: options.config.allowedOrigin }));
2056
+ app.use(createAuthMiddleware({ token: options.config.token }));
2057
+ app.get("/health", createHealthHandler(options.config));
2058
+ app.use("/repo", createRepoRouter(state));
2059
+ app.use("/agent", createAgentRouter({ state, adapter, brokerRegistry }));
2060
+ app.use("/agent/llm-response", createLlmResponseRouter({ brokerRegistry }));
2061
+ app.use("/agent/llm-chunk", createLlmChunkRouter({ brokerRegistry }));
2062
+ app.use("/llm-shim", createLlmShimRouter());
2063
+ app.use((req, res) => {
2064
+ res.status(404).json({ error: "not_found", method: req.method, path: req.path });
2065
+ });
2066
+ return { app, state, adapter, brokerRegistry };
2067
+ };
2068
+
2069
+ // src/modes/quickstart.ts
2070
+ var checkInQuickstart = async (cloudUrl, setupToken, helperVersion, port) => {
2071
+ const res = await fetch(`${cloudUrl}/api/coding/quickstart/checked-in`, {
2072
+ method: "POST",
2073
+ headers: { "content-type": "application/json" },
2074
+ body: JSON.stringify({
2075
+ setupToken,
2076
+ helperVersion,
2077
+ port
2078
+ })
2079
+ });
2080
+ if (!res.ok) {
2081
+ const text = await res.text().catch(() => "");
2082
+ throw new Error(
2083
+ `Cloud quickstart/checked-in returned ${res.status}: ${text || "no body"}`
2084
+ );
2085
+ }
2086
+ };
2087
+ var tryOpenBrowser = async (url) => {
2088
+ try {
2089
+ const open = (await import("open")).default;
2090
+ await open(url);
2091
+ return true;
2092
+ } catch {
2093
+ return false;
2094
+ }
2095
+ };
2096
+ var runQuickstart = async (options) => {
2097
+ const cloudUrl = options.cloudUrl.replace(/\/$/, "");
2098
+ if (!options.setupToken) {
2099
+ const browserUrl = `${cloudUrl}/coding-agent/quickstart`;
2100
+ console.log("[quickstart] No --token supplied.");
2101
+ console.log(`[quickstart] Open the quickstart page first:`);
2102
+ console.log(`[quickstart] ${browserUrl}`);
2103
+ console.log("");
2104
+ console.log(
2105
+ "[quickstart] It'll give you the exact `npx` command to paste here."
2106
+ );
2107
+ if (options.autoOpenBrowser) {
2108
+ await tryOpenBrowser(browserUrl);
2109
+ }
2110
+ return;
2111
+ }
2112
+ console.log(`[quickstart] Cloud: ${cloudUrl}`);
2113
+ console.log(`[quickstart] Setup token: ${options.setupToken.slice(0, 12)}\u2026`);
2114
+ process.env.LOCAL_AGENT_TOKEN = options.setupToken;
2115
+ process.env.LOCAL_AGENT_ALLOWED_ORIGIN = cloudUrl;
2116
+ process.env.LOCAL_AGENT_PORT = String(options.port);
2117
+ const config = loadConfig();
2118
+ const { app, adapter } = createApp({ config });
2119
+ const server = app.listen(config.port, config.host, async () => {
2120
+ const url = `http://${config.host}:${config.port}`;
2121
+ console.log(`[quickstart] Helper listening on ${url}`);
2122
+ console.log(`[quickstart] Adapter: ${adapter.name}`);
2123
+ console.log("[quickstart] Telling the cloud the helper is ready\u2026");
2124
+ try {
2125
+ await checkInQuickstart(
2126
+ cloudUrl,
2127
+ options.setupToken,
2128
+ options.helperVersion,
2129
+ config.port
2130
+ );
2131
+ console.log(
2132
+ "[quickstart] \u2714 Ready. The browser tab should redirect you to /coding-agent in a moment."
2133
+ );
2134
+ } catch (err) {
2135
+ console.error(
2136
+ `[quickstart] Couldn't notify the cloud \u2014 the browser will keep polling but won't auto-connect.`
2137
+ );
2138
+ console.error(`[quickstart] Underlying error: ${err.message}`);
2139
+ console.error("");
2140
+ console.error(
2141
+ `[quickstart] Workaround: in the browser, paste these into the connection panel:`
2142
+ );
2143
+ console.error(`[quickstart] Helper URL: ${url}`);
2144
+ console.error(`[quickstart] Pairing token: ${options.setupToken}`);
2145
+ }
2146
+ console.log("");
2147
+ console.log("[quickstart] Press Ctrl+C to stop the helper.");
2148
+ });
2149
+ const shutdown = (signal) => {
2150
+ console.log(`
2151
+ [quickstart] Received ${signal}, shutting down\u2026`);
2152
+ server.close(() => process.exit(0));
2153
+ setTimeout(() => process.exit(0), 3e3).unref();
2154
+ };
2155
+ process.on("SIGINT", shutdown);
2156
+ process.on("SIGTERM", shutdown);
2157
+ };
2158
+
2159
+ // src/lib/keychain.ts
2160
+ import { promises as fs } from "node:fs";
2161
+ import path5 from "node:path";
2162
+ import os from "node:os";
2163
+ var KEYTAR_SERVICE = "diologue.local-agent";
2164
+ var KEYTAR_ACCOUNT = "device-credential";
2165
+ var keytarPromise = null;
2166
+ var loadKeytar = async () => {
2167
+ if (keytarPromise) return keytarPromise;
2168
+ keytarPromise = (async () => {
2169
+ try {
2170
+ const moduleName = "keytar";
2171
+ const mod = await import(
2172
+ /* @vite-ignore */
2173
+ moduleName
2174
+ );
2175
+ return mod.default ?? mod;
2176
+ } catch {
2177
+ return null;
2178
+ }
2179
+ })();
2180
+ return keytarPromise;
2181
+ };
2182
+ var fileFallbackPath = () => {
2183
+ const home = os.homedir();
2184
+ return path5.join(home, ".diologue", "credentials");
2185
+ };
2186
+ var createCredentialStore = (options = {}) => {
2187
+ const filePath = options.filePathOverride ?? fileFallbackPath();
2188
+ const forceBackend = options.forceBackend;
2189
+ const resolveBackend = async () => {
2190
+ if (forceBackend) return forceBackend;
2191
+ const keytar = await loadKeytar();
2192
+ return keytar ? "keychain" : "file";
2193
+ };
2194
+ return {
2195
+ async load() {
2196
+ const backend = await resolveBackend();
2197
+ if (backend === "keychain") {
2198
+ const keytar = await loadKeytar();
2199
+ if (!keytar) return null;
2200
+ const raw = await keytar.getPassword(KEYTAR_SERVICE, KEYTAR_ACCOUNT);
2201
+ if (!raw) return null;
2202
+ try {
2203
+ const credential = JSON.parse(raw);
2204
+ return { credential, backend: "keychain" };
2205
+ } catch {
2206
+ return null;
2207
+ }
2208
+ }
2209
+ try {
2210
+ const raw = await fs.readFile(filePath, "utf8");
2211
+ const credential = JSON.parse(raw);
2212
+ return { credential, backend: "file" };
2213
+ } catch {
2214
+ return null;
2215
+ }
2216
+ },
2217
+ async save(credential) {
2218
+ const backend = await resolveBackend();
2219
+ if (backend === "keychain") {
2220
+ const keytar = await loadKeytar();
2221
+ if (!keytar) {
2222
+ } else {
2223
+ await keytar.setPassword(
2224
+ KEYTAR_SERVICE,
2225
+ KEYTAR_ACCOUNT,
2226
+ JSON.stringify(credential)
2227
+ );
2228
+ return { backend: "keychain" };
2229
+ }
2230
+ }
2231
+ await fs.mkdir(path5.dirname(filePath), { recursive: true });
2232
+ await fs.writeFile(filePath, JSON.stringify(credential, null, 2), {
2233
+ mode: 384
2234
+ });
2235
+ return { backend: "file" };
2236
+ },
2237
+ async clear() {
2238
+ const backend = await resolveBackend();
2239
+ if (backend === "keychain") {
2240
+ const keytar = await loadKeytar();
2241
+ if (keytar) {
2242
+ await keytar.deletePassword(KEYTAR_SERVICE, KEYTAR_ACCOUNT).catch(() => void 0);
2243
+ }
2244
+ }
2245
+ await fs.rm(filePath, { force: true }).catch(() => void 0);
2246
+ }
2247
+ };
2248
+ };
2249
+
2250
+ // src/modes/start.ts
2251
+ var printTunnelStatus = async () => {
2252
+ const store = createCredentialStore();
2253
+ const loaded = await store.load().catch(() => null);
2254
+ if (!loaded) {
2255
+ console.log("[local-agent] Tunnel: not paired.");
2256
+ console.log(
2257
+ "[local-agent] Run `diologue-local-agent pair <code>` to enable remote-machine use."
2258
+ );
2259
+ return;
2260
+ }
2261
+ const expires = new Date(loaded.credential.expiresAt);
2262
+ const daysLeft = Math.max(
2263
+ 0,
2264
+ Math.round((expires.getTime() - Date.now()) / 864e5)
2265
+ );
2266
+ console.log(
2267
+ `[local-agent] Tunnel: paired as "${loaded.credential.label}" (${loaded.credential.deviceId})`
2268
+ );
2269
+ console.log(
2270
+ `[local-agent] Cloud: ${loaded.credential.cloudUrl} Expires in ${daysLeft}d Storage: ${loaded.backend}`
2271
+ );
2272
+ };
2273
+ var runStart = async (options) => {
2274
+ if (options.port) process.env.LOCAL_AGENT_PORT = String(options.port);
2275
+ if (options.token) process.env.LOCAL_AGENT_TOKEN = options.token;
2276
+ if (options.allowedOrigin)
2277
+ process.env.LOCAL_AGENT_ALLOWED_ORIGIN = options.allowedOrigin;
2278
+ if (options.adapter) process.env.LOCAL_AGENT_ADAPTER = options.adapter;
2279
+ const config = loadConfig();
2280
+ const { app, adapter } = createApp({ config });
2281
+ const server = app.listen(config.port, config.host, async () => {
2282
+ const url = `http://${config.host}:${config.port}`;
2283
+ console.log(`[local-agent] Listening on ${url}`);
2284
+ console.log(`[local-agent] Adapter: ${adapter.name}`);
2285
+ console.log("[local-agent] Pairing token (paste into the web UI):");
2286
+ console.log(`[local-agent] ${config.token}`);
2287
+ console.log(`[local-agent] Allowed browser origin: ${config.allowedOrigin}`);
2288
+ await printTunnelStatus();
2289
+ console.log(
2290
+ "[local-agent] Press Ctrl+C to stop. The token is regenerated on each restart."
2291
+ );
2292
+ });
2293
+ const shutdown = (signal) => {
2294
+ console.log(`
2295
+ [local-agent] Received ${signal}, shutting down...`);
2296
+ server.close(() => process.exit(0));
2297
+ setTimeout(() => process.exit(0), 3e3).unref();
2298
+ };
2299
+ process.on("SIGINT", shutdown);
2300
+ process.on("SIGTERM", shutdown);
2301
+ };
2302
+
2303
+ // src/modes/pair.ts
2304
+ import os2 from "node:os";
2305
+ var detectOs = () => {
2306
+ const p = process.platform;
2307
+ if (p === "darwin" || p === "linux" || p === "win32") return p;
2308
+ return void 0;
2309
+ };
2310
+ var detectLabel = () => {
2311
+ const hostname = os2.hostname() || "unknown-host";
2312
+ return hostname.replace(/\.local$/i, "");
2313
+ };
2314
+ var runPair = async (options) => {
2315
+ const cloudUrl = options.cloudUrl.replace(/\/$/, "");
2316
+ const label = process.env.DIOLOGUE_DEVICE_LABEL?.trim() || detectLabel();
2317
+ const osId = detectOs();
2318
+ console.log(`[pair] Cloud: ${cloudUrl}`);
2319
+ console.log(`[pair] Label: ${label}`);
2320
+ console.log(`[pair] OS: ${osId ?? "(unknown)"}`);
2321
+ console.log(`[pair] Code: ${options.code}`);
2322
+ console.log("[pair] Exchanging code for a device credential\u2026");
2323
+ let response;
2324
+ try {
2325
+ response = await fetch(`${cloudUrl}/api/devices/pair-complete`, {
2326
+ method: "POST",
2327
+ headers: { "content-type": "application/json" },
2328
+ body: JSON.stringify({
2329
+ code: options.code,
2330
+ label,
2331
+ ...osId ? { os: osId } : {},
2332
+ helperVersion: options.helperVersion
2333
+ })
2334
+ });
2335
+ } catch (err) {
2336
+ console.error(
2337
+ "[pair] Could not reach the cloud. Check --cloud / DIOLOGUE_CLOUD_URL and your network."
2338
+ );
2339
+ console.error(`[pair] Underlying error: ${err.message}`);
2340
+ process.exit(1);
2341
+ }
2342
+ if (response.status === 404) {
2343
+ console.error(
2344
+ "[pair] Code not found. Generate a fresh one from Settings \u2192 Devices."
2345
+ );
2346
+ process.exit(1);
2347
+ }
2348
+ if (response.status === 410) {
2349
+ console.error("[pair] Code already used or expired. Generate a fresh one.");
2350
+ process.exit(1);
2351
+ }
2352
+ if (!response.ok) {
2353
+ const text = await response.text().catch(() => "");
2354
+ console.error(`[pair] Cloud returned ${response.status}: ${text}`);
2355
+ process.exit(1);
2356
+ }
2357
+ const data = await response.json();
2358
+ if (!data.jwt || !data.device?.id) {
2359
+ console.error("[pair] Cloud response missing fields:", data);
2360
+ process.exit(1);
2361
+ }
2362
+ const store = createCredentialStore();
2363
+ const { backend } = await store.save({
2364
+ jwt: data.jwt,
2365
+ deviceId: data.device.id,
2366
+ expiresAt: data.expiresAt,
2367
+ pairedAt: (/* @__PURE__ */ new Date()).toISOString(),
2368
+ label: data.device.label,
2369
+ cloudUrl
2370
+ });
2371
+ console.log("");
2372
+ console.log(`[pair] \u2714 Paired as "${data.device.label}" (${data.device.id})`);
2373
+ console.log(`[pair] Credential expires: ${data.expiresAt}`);
2374
+ console.log(
2375
+ `[pair] Stored in: ${backend === "keychain" ? "OS keychain" : "~/.diologue/credentials (file, 0600)"}`
2376
+ );
2377
+ if (backend === "file") {
2378
+ console.log(
2379
+ "[pair] Note: keytar (OS keychain) couldn't load; using file fallback."
2380
+ );
2381
+ }
2382
+ console.log("");
2383
+ console.log("[pair] The helper will pick this credential up on next start.");
2384
+ console.log("[pair] Start it with: diologue-local-agent");
2385
+ };
2386
+
2387
+ // src/cli.ts
2388
+ var HELPER_VERSION = "0.1.0";
2389
+ var DEFAULT_CLOUD_URL = "http://localhost:5000";
2390
+ var parseArgs = (argv) => {
2391
+ const flags = /* @__PURE__ */ new Map();
2392
+ const positional = [];
2393
+ let subcommand = null;
2394
+ for (let i = 0; i < argv.length; i++) {
2395
+ const arg = argv[i];
2396
+ if (arg === "--help" || arg === "-h") {
2397
+ return { subcommand: "help", flags, positional };
2398
+ }
2399
+ if (arg === "--version" || arg === "-v") {
2400
+ return { subcommand: "version", flags, positional };
2401
+ }
2402
+ if (arg.startsWith("--")) {
2403
+ const eq = arg.indexOf("=");
2404
+ if (eq !== -1) {
2405
+ flags.set(arg.slice(2, eq), arg.slice(eq + 1));
2406
+ } else {
2407
+ const next = argv[i + 1];
2408
+ if (next && !next.startsWith("--")) {
2409
+ flags.set(arg.slice(2), next);
2410
+ i++;
2411
+ } else {
2412
+ flags.set(arg.slice(2), true);
2413
+ }
2414
+ }
2415
+ continue;
2416
+ }
2417
+ if (subcommand === null && ["quickstart", "start", "pair"].includes(arg)) {
2418
+ subcommand = arg;
2419
+ continue;
2420
+ }
2421
+ positional.push(arg);
2422
+ }
2423
+ return {
2424
+ subcommand: subcommand ?? "quickstart",
2425
+ flags,
2426
+ positional
2427
+ };
2428
+ };
2429
+ var printHelp = () => {
2430
+ console.log(`
2431
+ Diologue Local Agent v${HELPER_VERSION}
2432
+
2433
+ USAGE
2434
+ diologue-local-agent [<command>] [options]
2435
+
2436
+ COMMANDS
2437
+ quickstart (default) Start the helper + open the browser to pair
2438
+ start Start the helper in raw token mode (no browser)
2439
+ pair <code> Pair this machine with a previously-issued code
2440
+ --help Show this message
2441
+ --version Show version
2442
+
2443
+ QUICKSTART OPTIONS
2444
+ --cloud=<url> Cloud URL to pair with
2445
+ (default: \${DIOLOGUE_CLOUD_URL:-${DEFAULT_CLOUD_URL}})
2446
+ --no-open Don't auto-open the browser; print the URL instead
2447
+ --port=<n> TCP port for the helper (default: 4099)
2448
+
2449
+ START OPTIONS
2450
+ --token=<token> Use this pairing token instead of a random one
2451
+ --origin=<url> Allowed browser origin for CORS
2452
+ --port=<n> TCP port (default: 4099)
2453
+ --adapter=<mock|opencode> Engine adapter (default: opencode)
2454
+
2455
+ EXAMPLES
2456
+ # New user \u2014 go from zero to chatting in 30s
2457
+ npx @diologue/local-agent --cloud=https://app.your-domain.com
2458
+
2459
+ # Power user \u2014 manual flags
2460
+ diologue-local-agent start --port=5099 --origin=https://your.app
2461
+
2462
+ # Pair a device (for the future tunnel transport)
2463
+ diologue-local-agent pair K9F-27R --cloud=https://app.your-domain.com
2464
+
2465
+ ENVIRONMENT
2466
+ DIOLOGUE_CLOUD_URL Default cloud URL (overridden by --cloud)
2467
+ LOCAL_AGENT_ALLOWED_ORIGIN Default CORS origin
2468
+ LOCAL_AGENT_ADAPTER "mock" forces the canned-response adapter
2469
+ LOCAL_AGENT_TOKEN Pin a specific pairing token (testing)
2470
+ LOCAL_AGENT_PORT Default TCP port
2471
+
2472
+ For docs + the source, visit:
2473
+ https://github.com/Deplova-Ltd/AI-Personas-and-Multi-LLM
2474
+ `.trim());
2475
+ };
2476
+ var main = async () => {
2477
+ const argv = process2.argv.slice(2);
2478
+ const parsed = parseArgs(argv);
2479
+ switch (parsed.subcommand) {
2480
+ case "help":
2481
+ printHelp();
2482
+ return;
2483
+ case "version":
2484
+ console.log(`diologue-local-agent v${HELPER_VERSION}`);
2485
+ return;
2486
+ case "quickstart":
2487
+ await runQuickstart({
2488
+ cloudUrl: parsed.flags.get("cloud") ?? process2.env.DIOLOGUE_CLOUD_URL ?? DEFAULT_CLOUD_URL,
2489
+ port: parseFlagInt(parsed.flags.get("port"), 4099),
2490
+ autoOpenBrowser: parsed.flags.get("no-open") !== true,
2491
+ helperVersion: HELPER_VERSION,
2492
+ setupToken: parsed.flags.get("token")
2493
+ });
2494
+ return;
2495
+ case "start":
2496
+ await runStart({
2497
+ port: parseFlagInt(parsed.flags.get("port"), 4099),
2498
+ token: parsed.flags.get("token"),
2499
+ allowedOrigin: parsed.flags.get("origin"),
2500
+ adapter: parsed.flags.get("adapter")
2501
+ });
2502
+ return;
2503
+ case "pair": {
2504
+ const code = parsed.positional[0];
2505
+ if (!code) {
2506
+ console.error("Usage: diologue-local-agent pair <code>");
2507
+ process2.exit(2);
2508
+ }
2509
+ await runPair({
2510
+ code,
2511
+ cloudUrl: parsed.flags.get("cloud") ?? process2.env.DIOLOGUE_CLOUD_URL ?? DEFAULT_CLOUD_URL,
2512
+ helperVersion: HELPER_VERSION
2513
+ });
2514
+ return;
2515
+ }
2516
+ }
2517
+ };
2518
+ var parseFlagInt = (raw, fallback) => {
2519
+ if (typeof raw !== "string") return fallback;
2520
+ const n = Number.parseInt(raw, 10);
2521
+ return Number.isFinite(n) && n > 0 ? n : fallback;
2522
+ };
2523
+ main().catch((err) => {
2524
+ console.error("[cli] Fatal error:", err);
2525
+ process2.exit(1);
2526
+ });
2527
+ //# sourceMappingURL=cli.mjs.map