@sjawhar/opencode-legion-envoy 7.8.0 → 8.0.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.
@@ -15,7 +15,7 @@ var __export = (target, all) => {
15
15
  };
16
16
 
17
17
  // src/server.ts
18
- import { existsSync, readFileSync as readFileSync3 } from "fs";
18
+ import { existsSync as existsSync2, readFileSync as readFileSync3 } from "fs";
19
19
  import path3 from "path";
20
20
  import { fileURLToPath } from "url";
21
21
 
@@ -14372,36 +14372,135 @@ function zodSchemaApi(zod) {
14372
14372
  }
14373
14373
  };
14374
14374
  }
14375
- // ../envoy-client/src/defaults.ts
14376
- var DEFAULT_ENVOY_URL = "http://127.0.0.1:9020";
14377
- var DEFAULT_HEARTBEAT_MS = 120000;
14378
- var MIN_HEARTBEAT_MS = 25;
14379
- function envoyDefaultsFromEnvironment(environment) {
14380
- const rawHeartbeatMs = Number(environment["ENVOY_HEARTBEAT_MS"]);
14381
- const heartbeatMs = Number.isFinite(rawHeartbeatMs) && rawHeartbeatMs > 0 ? Math.max(rawHeartbeatMs, MIN_HEARTBEAT_MS) : DEFAULT_HEARTBEAT_MS;
14382
- const natsUrls = environment["ENVOY_NATS_URL"]?.split(",").map((url2) => url2.trim()).filter((url2) => url2.length > 0) ?? [];
14383
- return {
14384
- envoyUrl: normalizeEnvoyUrl(environment["ENVOY_URL"] ?? DEFAULT_ENVOY_URL),
14385
- natsUrls,
14386
- heartbeatMs
14387
- };
14375
+ // ../envoy-client/src/dispatch-first.ts
14376
+ import { join } from "path";
14377
+
14378
+ // ../envoy-client/src/errors.ts
14379
+ function messageFor(error48) {
14380
+ return error48 instanceof Error ? error48.message : String(error48);
14388
14381
  }
14389
- function normalizeEnvoyUrl(value) {
14390
- return value.replace(/\/+$/, "");
14382
+ function hasErrnoCode(error48, code) {
14383
+ return error48 instanceof Error && "code" in error48 && error48.code === code;
14391
14384
  }
14392
14385
 
14393
- // ../envoy-client/src/dispatch-config.ts
14394
- import { readFileSync as readFileSync2 } from "fs";
14395
- import { homedir } from "os";
14386
+ // ../envoy-client/src/dispatch-first.ts
14387
+ function dispatchFirstSkillFile(skillsDirectory) {
14388
+ return join(skillsDirectory, "dispatch-first", "SKILL.md");
14389
+ }
14390
+
14391
+ // ../envoy-client/src/machine.ts
14392
+ import { hostname as hostname3 } from "os";
14393
+ function machineID() {
14394
+ return process.env["ENVOY_MACHINE_ID"] || hostname3();
14395
+ }
14396
+
14397
+ // ../envoy-client/src/envoy-config.ts
14398
+ import {
14399
+ chmodSync,
14400
+ closeSync,
14401
+ existsSync,
14402
+ lstatSync,
14403
+ mkdtempSync,
14404
+ openSync,
14405
+ readFileSync as readFileSync2,
14406
+ readlinkSync,
14407
+ rmSync,
14408
+ statSync
14409
+ } from "fs";
14410
+ import { homedir, tmpdir } from "os";
14396
14411
  import * as path from "path";
14397
14412
 
14398
- // ../envoy-client/src/errors.ts
14399
- function messageFor(error48) {
14400
- return error48 instanceof Error ? error48.message : String(error48);
14413
+ // ../envoy-client/src/json-position.ts
14414
+ function jsonErrorPosition(text) {
14415
+ let at = 0;
14416
+ const fail = () => {
14417
+ throw new RangeError("not JSON");
14418
+ };
14419
+ const space = () => {
14420
+ while (at < text.length && `
14421
+ \r`.includes(text.charAt(at)))
14422
+ at += 1;
14423
+ };
14424
+ const expect = (char) => {
14425
+ if (text.charAt(at) !== char)
14426
+ fail();
14427
+ at += 1;
14428
+ };
14429
+ const string4 = () => {
14430
+ expect('"');
14431
+ while (at < text.length) {
14432
+ const char = text.charAt(at);
14433
+ if (char === '"') {
14434
+ at += 1;
14435
+ return;
14436
+ }
14437
+ if (char < " ")
14438
+ fail();
14439
+ at += char === "\\" ? 2 : 1;
14440
+ }
14441
+ fail();
14442
+ };
14443
+ const value = () => {
14444
+ space();
14445
+ const char = text.charAt(at);
14446
+ if (char === "{" || char === "[") {
14447
+ const close = char === "{" ? "}" : "]";
14448
+ at += 1;
14449
+ space();
14450
+ if (text.charAt(at) === close) {
14451
+ at += 1;
14452
+ return;
14453
+ }
14454
+ for (;; ) {
14455
+ if (close === "}") {
14456
+ space();
14457
+ string4();
14458
+ space();
14459
+ expect(":");
14460
+ }
14461
+ value();
14462
+ space();
14463
+ if (text.charAt(at) === ",") {
14464
+ at += 1;
14465
+ continue;
14466
+ }
14467
+ expect(close);
14468
+ return;
14469
+ }
14470
+ }
14471
+ if (char === '"') {
14472
+ string4();
14473
+ return;
14474
+ }
14475
+ for (const word of ["true", "false", "null"]) {
14476
+ if (text.startsWith(word, at)) {
14477
+ at += word.length;
14478
+ return;
14479
+ }
14480
+ }
14481
+ const number4 = /^-?(0|[1-9][0-9]*)(\.[0-9]+)?([eE][+-]?[0-9]+)?/.exec(text.slice(at));
14482
+ if (number4 === null)
14483
+ fail();
14484
+ at += number4?.[0].length ?? 0;
14485
+ };
14486
+ try {
14487
+ value();
14488
+ space();
14489
+ if (at < text.length)
14490
+ fail();
14491
+ return;
14492
+ } catch {
14493
+ const before = text.slice(0, at).split(`
14494
+ `);
14495
+ return `line ${before.length}, column ${(before.at(-1)?.length ?? 0) + 1}`;
14496
+ }
14401
14497
  }
14402
14498
 
14403
14499
  // ../envoy-client/src/secret-file.ts
14404
14500
  import { readFileSync } from "fs";
14501
+ function sendable(value) {
14502
+ return !/[\s\p{Cc}]/u.test(value);
14503
+ }
14405
14504
  function readSecretFile(variable, filePath) {
14406
14505
  let contents;
14407
14506
  try {
@@ -14412,134 +14511,578 @@ function readSecretFile(variable, filePath) {
14412
14511
  const value = contents.trim();
14413
14512
  if (value.length === 0)
14414
14513
  throw new Error(`${variable} names ${filePath}, which is empty`);
14514
+ if (!sendable(value)) {
14515
+ throw new Error(`${variable} names ${filePath}, which holds whitespace or a control character`);
14516
+ }
14415
14517
  return value;
14416
14518
  }
14417
14519
 
14418
- // ../envoy-client/src/dispatch-config.ts
14419
- var DEFAULT_SERVER_URL = "http://localhost:8766";
14420
- function normalizeDispatchUrl(url2) {
14421
- return url2.replace(/\/+$/, "");
14520
+ // ../envoy-client/src/envoy-config.ts
14521
+ var COMMAND_TIMEOUT_MS = 1e4;
14522
+ function nonEmpty(value) {
14523
+ return value === undefined || value === "" ? undefined : value;
14422
14524
  }
14423
- function parsedDispatchUrl(value, source) {
14424
- const url2 = normalizeDispatchUrl(value);
14425
- try {
14426
- new URL(url2);
14427
- return { url: url2, error: null };
14428
- } catch {
14429
- return { url: null, error: `${source} must be a valid URL` };
14525
+ function absolute(name, value) {
14526
+ if (value !== undefined && !path.isAbsolute(value)) {
14527
+ throw new Error(`${name} is ${value}, which is not an absolute path`);
14430
14528
  }
14529
+ return value;
14431
14530
  }
14531
+ function neutralConfig(env) {
14532
+ const xdgConfigHome = nonEmpty(env.XDG_CONFIG_HOME);
14533
+ if (xdgConfigHome !== undefined && !path.isAbsolute(xdgConfigHome)) {
14534
+ return { error: `XDG_CONFIG_HOME is ${xdgConfigHome}, which is not an absolute path` };
14535
+ }
14536
+ const configHome = xdgConfigHome ?? path.join(nonEmpty(env.HOME) ?? homedir(), ".config");
14537
+ return { path: path.resolve(configHome, "legion", "envoy.json") };
14538
+ }
14539
+ var HARNESS_DIRECTORY_VARIABLE = {
14540
+ omp: "PI_CODING_AGENT_DIR",
14541
+ claude: "CLAUDE_CONFIG_DIR",
14542
+ opencode: "OPENCODE_CONFIG_DIR"
14543
+ };
14544
+ var PLUGIN_HARNESSES = Object.keys(HARNESS_DIRECTORY_VARIABLE);
14545
+ function namedDirectory(harness, env) {
14546
+ if (harness === "omp") {
14547
+ const profile = nonEmpty(env.OMP_PROFILE);
14548
+ if (profile === undefined || profile === "default")
14549
+ return;
14550
+ }
14551
+ return nonEmpty(env[HARNESS_DIRECTORY_VARIABLE[harness]]);
14552
+ }
14553
+ function harnessDirectory(harness, env) {
14554
+ for (const plugin of harness === "command" ? PLUGIN_HARNESSES : [harness]) {
14555
+ if (harness === "command" && plugin === "claude" && env.CLAUDE_CODE_CHILD_SESSION !== "1") {
14556
+ continue;
14557
+ }
14558
+ const directory = namedDirectory(plugin, env);
14559
+ if (directory !== undefined)
14560
+ return absolute(HARNESS_DIRECTORY_VARIABLE[plugin], directory);
14561
+ }
14562
+ return;
14563
+ }
14564
+ function handedConfigPath(env) {
14565
+ const handed = nonEmpty(env.ENVOY_CONFIG_FILE);
14566
+ return handed === undefined ? undefined : path.resolve(absolute("ENVOY_CONFIG_FILE", handed));
14567
+ }
14568
+ function envoyConfigPath(harness, env) {
14569
+ const handedFirst = harness === "command" ? handedConfigPath(env) : undefined;
14570
+ if (handedFirst !== undefined)
14571
+ return handedFirst;
14572
+ const directory = harnessDirectory(harness, env);
14573
+ if (directory !== undefined)
14574
+ return path.resolve(directory, "envoy.json");
14575
+ const handed = handedConfigPath(env);
14576
+ if (handed !== undefined)
14577
+ return handed;
14578
+ const neutral = neutralConfig(env);
14579
+ if ("error" in neutral)
14580
+ throw new Error(neutral.error);
14581
+ return neutral.path;
14582
+ }
14583
+ var httpUrlSchema = exports_external.url({ protocol: /^https?$/, error: "must be an http or https URL" });
14584
+ var DispatchSectionSchema = exports_external.strictObject({
14585
+ enabled: exports_external.boolean().optional(),
14586
+ serverUrl: httpUrlSchema.optional(),
14587
+ token: exports_external.string().optional()
14588
+ });
14589
+ var EnvoySectionSchema = exports_external.strictObject({
14590
+ enabled: exports_external.boolean().optional(),
14591
+ url: httpUrlSchema.optional(),
14592
+ natsUrls: exports_external.array(exports_external.string().trim().min(1, "must not be empty")).optional(),
14593
+ token: exports_external.string().optional(),
14594
+ natsSeed: exports_external.string().optional()
14595
+ });
14432
14596
  var EnvoyFileSchema = exports_external.looseObject({
14433
14597
  $schema: exports_external.string().optional(),
14434
- natsUrls: exports_external.array(exports_external.string()).optional(),
14435
- dispatch: exports_external.strictObject({
14436
- enabled: exports_external.boolean().optional(),
14437
- serverUrl: exports_external.url().optional(),
14438
- token: exports_external.string().optional()
14439
- }).optional()
14598
+ dispatch: DispatchSectionSchema.optional(),
14599
+ envoy: EnvoySectionSchema.optional()
14600
+ });
14601
+ var CheckoutFileSchema = exports_external.looseObject({
14602
+ $schema: exports_external.string().optional(),
14603
+ dispatch: DispatchSectionSchema.optional()
14440
14604
  });
14605
+ function offWithNotice(notice) {
14606
+ return { enabled: false, notice, error: null };
14607
+ }
14608
+ function offWithError(error48) {
14609
+ return { enabled: false, notice: null, error: error48 };
14610
+ }
14611
+ function httpUrl2(value) {
14612
+ let url2;
14613
+ try {
14614
+ url2 = new URL(value);
14615
+ } catch {
14616
+ return;
14617
+ }
14618
+ return url2.protocol === "http:" || url2.protocol === "https:" ? url2 : undefined;
14619
+ }
14441
14620
  function describeSchemaIssue(filePath, error48) {
14442
14621
  const issue2 = error48.issues[0];
14443
14622
  if (!issue2)
14444
- return `${filePath}: invalid dispatch config`;
14623
+ return `${filePath}: invalid envoy.json`;
14445
14624
  if (issue2.code === "unrecognized_keys") {
14446
- const keys = issue2.keys.map((key) => `dispatch.${key}`).join(", ");
14447
- return `${filePath}: unrecognized dispatch key(s): ${keys}`;
14625
+ const section = issue2.path.join(".");
14626
+ const keys = issue2.keys.map((key) => `${section}.${key}`).join(", ");
14627
+ return `${filePath}: unrecognized ${section} key(s): ${keys}`;
14448
14628
  }
14449
14629
  return `${filePath}: ${issue2.path.join(".")}: ${issue2.message}`;
14450
14630
  }
14451
- function readEnvoyFile(filePath) {
14631
+ function danglingLink(filePath) {
14632
+ try {
14633
+ return lstatSync(filePath).isSymbolicLink() ? readlinkSync(filePath) : null;
14634
+ } catch {
14635
+ return null;
14636
+ }
14637
+ }
14638
+ function parseFile(filePath, schema, checkout) {
14452
14639
  let raw;
14453
14640
  try {
14454
14641
  raw = readFileSync2(filePath, "utf-8");
14455
- } catch {
14456
- return { kind: "absent" };
14642
+ } catch (error48) {
14643
+ if (hasErrnoCode(error48, "ENOENT") || hasErrnoCode(error48, "ENOTDIR")) {
14644
+ return { kind: "absent", link: danglingLink(filePath) };
14645
+ }
14646
+ return { kind: "invalid", reason: `${filePath}: could not be read (${messageFor(error48)})` };
14457
14647
  }
14458
14648
  let parsedJson;
14459
14649
  try {
14460
14650
  parsedJson = JSON.parse(raw);
14461
- } catch (err) {
14462
- return { kind: "invalid", reason: `${filePath}: invalid JSON (${messageFor(err)})` };
14651
+ } catch {
14652
+ const position = jsonErrorPosition(raw);
14653
+ return {
14654
+ kind: "invalid",
14655
+ reason: `${filePath}: invalid JSON${position === undefined ? "" : ` at ${position}`}`
14656
+ };
14463
14657
  }
14464
- const parsed = EnvoyFileSchema.safeParse(parsedJson);
14658
+ if (typeof parsedJson === "object" && parsedJson !== null && "natsUrls" in parsedJson) {
14659
+ return {
14660
+ kind: "invalid",
14661
+ reason: checkout ? `${filePath}: the top-level natsUrls is not read, and a checkout's envoy.json holds only a dispatch section` : `${filePath}: the top-level natsUrls is not read; move it to envoy.natsUrls`
14662
+ };
14663
+ }
14664
+ const parsed = schema.safeParse(parsedJson);
14465
14665
  if (!parsed.success) {
14466
14666
  return { kind: "invalid", reason: describeSchemaIssue(filePath, parsed.error) };
14467
14667
  }
14468
- return { kind: "valid", settings: parsed.data.dispatch ?? null };
14469
- }
14470
- function resolveDispatchConfig(env, options = {}) {
14471
- const explicitUrl = env.DISPATCH_URL;
14472
- const home = options.home ?? env.HOME ?? homedir();
14473
- const cwd = options.cwd ?? process.cwd();
14474
- const userFile = readEnvoyFile(path.join(home, ".config", "opencode", "envoy.json"));
14475
- const repoFile = readEnvoyFile(path.join(cwd, ".opencode", "envoy.json"));
14476
- for (const file2 of [userFile, repoFile]) {
14477
- if (file2.kind === "invalid") {
14478
- return { enabled: false, url: null, token: null, error: file2.reason };
14479
- }
14480
- }
14481
- const userSettings = userFile.kind === "valid" ? userFile.settings : null;
14482
- const repoSettings = repoFile.kind === "valid" ? repoFile.settings : null;
14483
- const merged = { ...userSettings, ...repoSettings };
14484
- const repoURL = repoSettings?.serverUrl;
14485
- if (explicitUrl === undefined && merged.enabled === true && repoURL !== undefined) {
14486
- const token2 = repoSettings?.token ?? null;
14487
- if (!token2) {
14668
+ return { kind: "valid", settings: parsed.data };
14669
+ }
14670
+ var FAILURE_KEPT_MS = 60000;
14671
+ var COMMAND_OUTCOMES = Symbol.for("legion.envoy-config.commands.v1");
14672
+ var processGlobal = globalThis;
14673
+ var commandOutcomes = processGlobal[COMMAND_OUTCOMES] ?? new Map;
14674
+ processGlobal[COMMAND_OUTCOMES] = commandOutcomes;
14675
+ function runCommand(command, env, cwd) {
14676
+ const childEnv = {};
14677
+ for (const [name, variable] of Object.entries(env)) {
14678
+ if (variable !== undefined)
14679
+ childEnv[name] = variable;
14680
+ }
14681
+ const directory = mkdtempSync(path.join(tmpdir(), "envoy-command-"));
14682
+ try {
14683
+ chmodSync(directory, 448);
14684
+ const output = path.join(directory, "stdout");
14685
+ const fd = openSync(output, "wx", 384);
14686
+ let result;
14687
+ try {
14688
+ result = Bun.spawnSync(["/bin/sh", "-c", command], {
14689
+ cwd,
14690
+ env: childEnv,
14691
+ stdin: "ignore",
14692
+ stdout: fd,
14693
+ stderr: "ignore",
14694
+ timeout: COMMAND_TIMEOUT_MS,
14695
+ detached: true
14696
+ });
14697
+ } catch (error48) {
14698
+ return { failure: `could not start: ${messageFor(error48)}`, timedOut: false };
14699
+ } finally {
14700
+ closeSync(fd);
14701
+ }
14702
+ try {
14703
+ process.kill(-result.pid, "SIGKILL");
14704
+ } catch {}
14705
+ if (result.exitedDueToTimeout) {
14706
+ return {
14707
+ failure: `did not finish within ${COMMAND_TIMEOUT_MS / 1000} s, and does not run again in this process`,
14708
+ timedOut: true
14709
+ };
14710
+ }
14711
+ if (!result.success) {
14488
14712
  return {
14489
- enabled: false,
14490
- url: null,
14491
- token: null,
14492
- error: "repository dispatch.serverUrl requires dispatch.token from the same .opencode/envoy.json or DISPATCH_URL with DISPATCH_TOKEN"
14713
+ failure: `exited with ${result.signalCode ?? `status ${result.exitCode}`}`,
14714
+ timedOut: false
14493
14715
  };
14494
14716
  }
14495
- const url3 = parsedDispatchUrl(repoURL, "repository dispatch.serverUrl");
14496
- if (url3.error !== null)
14497
- return { enabled: false, url: null, token: null, error: url3.error };
14498
- return { enabled: true, url: url3.url, token: token2, error: null };
14717
+ const value = readFileSync2(output, "utf-8").trim();
14718
+ if (value.length === 0)
14719
+ return { failure: "printed nothing", timedOut: false };
14720
+ if (!sendable(value)) {
14721
+ return {
14722
+ failure: "printed more than one line, or whitespace inside its value",
14723
+ timedOut: false
14724
+ };
14725
+ }
14726
+ return { value };
14727
+ } finally {
14728
+ rmSync(directory, { recursive: true, force: true });
14729
+ }
14730
+ }
14731
+ function fileValue(value, field, filePath, env) {
14732
+ if (!value.startsWith("!")) {
14733
+ if (!sendable(value)) {
14734
+ throw new Error(`${filePath}: ${field} holds whitespace or a control character`);
14735
+ }
14736
+ return value;
14737
+ }
14738
+ const command = value.slice(1);
14739
+ const cwd = path.dirname(path.resolve(filePath));
14740
+ const key = JSON.stringify([command, filePath, cwd]);
14741
+ let outcome = commandOutcomes.get(key);
14742
+ if (outcome === undefined || "failure" in outcome && !outcome.timedOut && Date.now() - outcome.at >= FAILURE_KEPT_MS) {
14743
+ outcome = { ...runCommand(command, env, cwd), at: Date.now() };
14744
+ commandOutcomes.set(key, outcome);
14745
+ }
14746
+ if ("failure" in outcome)
14747
+ throw new Error(`${filePath}: ${field}'s command ${outcome.failure}`);
14748
+ return outcome.value;
14749
+ }
14750
+ function statDevice(directory) {
14751
+ try {
14752
+ return statSync(directory).dev;
14753
+ } catch {
14754
+ return;
14755
+ }
14756
+ }
14757
+ function moveNotice(oldFile, newFile, move, deviceOf) {
14758
+ const oldDevice = deviceOf(path.dirname(oldFile));
14759
+ let ancestor = path.dirname(newFile);
14760
+ let newDevice = deviceOf(ancestor);
14761
+ while (newDevice === undefined && path.dirname(ancestor) !== ancestor) {
14762
+ ancestor = path.dirname(ancestor);
14763
+ newDevice = deviceOf(ancestor);
14764
+ }
14765
+ if (oldDevice !== undefined && newDevice !== undefined && oldDevice !== newDevice) {
14766
+ return `${oldFile} is not read, and is on another filesystem than ${newFile} (a mount): create ${newFile}, and move ${oldFile} on the machine that owns it`;
14767
+ }
14768
+ return `${oldFile} is not read: ${move}`;
14769
+ }
14770
+ function oldUserFileNotice(filePath, env, deviceOf) {
14771
+ const home = nonEmpty(env.HOME) ?? homedir();
14772
+ const old = path.join(home, ".config", "opencode", "envoy.json");
14773
+ if (!existsSync(old))
14774
+ return;
14775
+ const selected = path.resolve(filePath);
14776
+ const userFile = path.resolve(home, ".config", "legion", "envoy.json");
14777
+ if (selected === userFile) {
14778
+ return moveNotice(old, userFile, "mkdir -p ~/.config/legion && mv ~/.config/opencode/envoy.json ~/.config/legion/", deviceOf);
14779
+ }
14780
+ const profileFile = PLUGIN_HARNESSES.map((harness) => namedDirectory(harness, env)).some((directory) => directory !== undefined && path.isAbsolute(directory) && selected === path.resolve(directory, "envoy.json"));
14781
+ if (profileFile) {
14782
+ return `${old} is not read for this profile; copy or link it to ${filePath}, or write that file`;
14783
+ }
14784
+ const configHome = nonEmpty(env.XDG_CONFIG_HOME);
14785
+ const neutral = neutralConfig(env);
14786
+ if (configHome !== undefined && path.isAbsolute(configHome) && "path" in neutral && selected === neutral.path) {
14787
+ return `${old} is not read under XDG_CONFIG_HOME=${configHome}`;
14788
+ }
14789
+ return;
14790
+ }
14791
+ function missingConfigNotice(filePath, missing, off) {
14792
+ const link = missing.link === null ? "" : ` (a link to ${missing.link}, which does not exist)`;
14793
+ const old = missing.oldUserFileNotice === undefined ? "" : `; ${missing.oldUserFileNotice}`;
14794
+ return `no envoy.json at ${filePath}${link}: ${off}${old}`;
14795
+ }
14796
+ function readEnvoyConfig(filePath, env, options) {
14797
+ const deviceOf = options.deviceOf ?? statDevice;
14798
+ const chosen = parseFile(filePath, EnvoyFileSchema, false);
14799
+ if (chosen.kind === "invalid")
14800
+ return { kind: "failed", path: filePath, error: chosen.reason };
14801
+ const resolvedChosen = path.resolve(filePath);
14802
+ const checkoutPath = path.resolve(options.cwd, ".envoy", "envoy.json");
14803
+ const checkoutFile = checkoutPath === resolvedChosen ? { kind: "absent", link: null } : parseFile(checkoutPath, CheckoutFileSchema, true);
14804
+ const own = chosen.kind === "valid" ? chosen.settings : {};
14805
+ let checkout = null;
14806
+ if (checkoutFile.kind === "invalid") {
14807
+ checkout = { kind: "invalid", path: checkoutPath, error: checkoutFile.reason };
14808
+ } else if (checkoutFile.kind === "valid") {
14809
+ checkout = {
14810
+ kind: "valid",
14811
+ path: checkoutPath,
14812
+ dispatch: checkoutFile.settings.dispatch ?? {},
14813
+ envoyError: Object.hasOwn(checkoutFile.settings, "envoy") ? `${checkoutPath}: envoy is not read from a checkout's envoy.json; put it in ${filePath}` : null
14814
+ };
14815
+ }
14816
+ const oldCheckout = path.resolve(options.cwd, ".opencode", "envoy.json");
14817
+ const warnings = oldCheckout !== resolvedChosen && !existsSync(checkoutPath) && existsSync(oldCheckout) ? [
14818
+ moveNotice(oldCheckout, checkoutPath, `mkdir -p ${path.dirname(checkoutPath)} && mv ${oldCheckout} ${checkoutPath}`, deviceOf)
14819
+ ] : [];
14820
+ const unreadOldUserFile = chosen.kind === "absent" ? oldUserFileNotice(filePath, env, deviceOf) : undefined;
14821
+ return {
14822
+ kind: "read",
14823
+ path: filePath,
14824
+ dispatch: own.dispatch ?? {},
14825
+ envoy: own.envoy ?? {},
14826
+ checkout,
14827
+ missing: chosen.kind === "absent" ? {
14828
+ link: chosen.link,
14829
+ ...unreadOldUserFile === undefined ? {} : { oldUserFileNotice: unreadOldUserFile }
14830
+ } : null,
14831
+ warnings
14832
+ };
14833
+ }
14834
+
14835
+ // ../envoy-client/src/bearer.ts
14836
+ function bearerSource(env, section, written) {
14837
+ const pointer = nonEmpty(env[`${section}_TOKEN_FILE`]);
14838
+ if (pointer !== undefined) {
14839
+ return { kind: "variable", token: readSecretFile(`${section}_TOKEN_FILE`, pointer) };
14840
+ }
14841
+ const plain = nonEmpty(env[`${section}_TOKEN`]);
14842
+ if (plain !== undefined) {
14843
+ if (!sendable(plain)) {
14844
+ throw new Error(`${section}_TOKEN holds whitespace or a control character`);
14845
+ }
14846
+ return { kind: "variable", token: plain };
14847
+ }
14848
+ const bearer = written();
14849
+ return bearer === undefined ? { kind: "none" } : { kind: "file", ...bearer };
14850
+ }
14851
+ function fileBearer(bearer, url2, section, env) {
14852
+ const named = httpUrl2(url2)?.origin;
14853
+ const paired = httpUrl2(bearer.server)?.origin;
14854
+ if (named !== paired) {
14855
+ throw new Error(`${section}_URL names ${named}, but ${bearer.path}'s ${bearer.field} is for ${paired}; set ${section}_TOKEN or ${section}_TOKEN_FILE for it`);
14856
+ }
14857
+ if (bearer.checkout && bearer.token.startsWith("!")) {
14858
+ throw new Error(`${bearer.path}: ${bearer.field} is a command, which a checkout's envoy.json cannot run`);
14859
+ }
14860
+ if (bearer.token === "") {
14861
+ throw new Error(`${bearer.field} in ${bearer.path} must be a non-empty bearer token`);
14862
+ }
14863
+ return fileValue(bearer.token, bearer.field, bearer.path, env);
14864
+ }
14865
+
14866
+ // ../envoy-client/src/dispatch-config.ts
14867
+ var DEFAULT_SERVER_URL = "http://localhost:8766";
14868
+ function checkoutPair(checkout, serverUrl) {
14869
+ const { token } = checkout.dispatch;
14870
+ if (token === undefined) {
14871
+ throw new Error(`${checkout.path}: dispatch.serverUrl requires dispatch.token in the same file`);
14872
+ }
14873
+ return {
14874
+ kind: "file",
14875
+ token,
14876
+ field: "dispatch.token",
14877
+ path: checkout.path,
14878
+ server: serverUrl,
14879
+ checkout: true
14880
+ };
14881
+ }
14882
+ function writtenToken(config2, checkout) {
14883
+ if (checkout?.dispatch.token !== undefined) {
14884
+ throw new Error(`${checkout.path}: dispatch.token requires dispatch.serverUrl in the same file`);
14885
+ }
14886
+ const own = config2.dispatch;
14887
+ if (own.token === undefined)
14888
+ return;
14889
+ const server = own.serverUrl ?? DEFAULT_SERVER_URL;
14890
+ return {
14891
+ token: own.token,
14892
+ field: "dispatch.token",
14893
+ path: config2.path,
14894
+ server,
14895
+ checkout: false
14896
+ };
14897
+ }
14898
+ function dispatchSettings(env, config2) {
14899
+ if (config2.kind === "failed")
14900
+ return offWithError(config2.error);
14901
+ const explicitUrl = nonEmpty(env.DISPATCH_URL);
14902
+ const checkoutFile = explicitUrl === undefined ? config2.checkout : null;
14903
+ if (checkoutFile?.kind === "invalid")
14904
+ return offWithError(checkoutFile.error);
14905
+ const checkout = checkoutFile?.kind === "valid" ? checkoutFile : null;
14906
+ const own = config2.dispatch;
14907
+ if (own.enabled === false) {
14908
+ return offWithNotice(`${config2.path} sets dispatch.enabled to false: Dispatch is off`);
14909
+ }
14910
+ if (checkout?.dispatch.enabled === false) {
14911
+ return offWithNotice(`${checkout.path} sets dispatch.enabled to false: Dispatch is off`);
14912
+ }
14913
+ const checkoutServer = checkout?.dispatch.serverUrl;
14914
+ const addressed = explicitUrl !== undefined || checkoutServer !== undefined || own.serverUrl !== undefined || own.enabled === true;
14915
+ if (!addressed) {
14916
+ return offWithNotice(config2.missing === null ? `${config2.path} names no Dispatch server, and DISPATCH_URL is not set: Dispatch is off` : missingConfigNotice(config2.path, config2.missing, "Dispatch is off"));
14917
+ }
14918
+ if (explicitUrl !== undefined && httpUrl2(explicitUrl) === undefined) {
14919
+ return offWithError("DISPATCH_URL must be an http or https URL");
14920
+ }
14921
+ try {
14922
+ const source = checkout !== null && checkoutServer !== undefined ? checkoutPair(checkout, checkoutServer) : bearerSource(env, "DISPATCH", () => writtenToken(config2, checkout));
14923
+ if (source.kind === "none") {
14924
+ return offWithError(explicitUrl === undefined ? `${config2.path}: dispatch is enabled and no token is: set dispatch.token, or DISPATCH_TOKEN or DISPATCH_TOKEN_FILE` : `DISPATCH_URL is set and no token is: set DISPATCH_TOKEN or DISPATCH_TOKEN_FILE, or dispatch.token in ${config2.path}`);
14925
+ }
14926
+ const url2 = (explicitUrl ?? (source.kind === "file" ? source.server : own.serverUrl ?? DEFAULT_SERVER_URL)).replace(/\/+$/, "");
14927
+ const token = source.kind === "variable" ? source.token : fileBearer(source, url2, "DISPATCH", env);
14928
+ return { enabled: true, url: url2, token };
14929
+ } catch (error48) {
14930
+ return offWithError(messageFor(error48));
14931
+ }
14932
+ }
14933
+
14934
+ // ../envoy-client/src/envoy-settings.ts
14935
+ var DEFAULT_ENVOY_URL = "http://127.0.0.1:9020";
14936
+ var DEFAULT_HEARTBEAT_MS = 120000;
14937
+ var MIN_HEARTBEAT_MS = 25;
14938
+ function natsSeedFromEnvironment(env) {
14939
+ const { NATS_NKEY_SEED_FILE: file2, NATS_NKEY_SEED: plain } = env;
14940
+ if (file2 !== undefined) {
14941
+ if (file2 === "")
14942
+ throw new Error("NATS_NKEY_SEED_FILE is set but empty");
14943
+ return {
14944
+ value: readSecretFile("NATS_NKEY_SEED_FILE", file2),
14945
+ source: `NATS_NKEY_SEED_FILE (${file2})`
14946
+ };
14947
+ }
14948
+ if (plain === undefined)
14949
+ return;
14950
+ const value = plain.trim();
14951
+ if (value.length === 0)
14952
+ throw new Error("NATS_NKEY_SEED is set but empty");
14953
+ return { value, source: "NATS_NKEY_SEED" };
14954
+ }
14955
+ var NO_NATS_SERVER = "Envoy has no NATS server (envoy.natsUrls, ENVOY_NATS_URL)";
14956
+ var NO_NATS_CONNECTION = "this session opens no NATS connection";
14957
+ function natsServerKey(url2) {
14958
+ const match = /^([a-z][a-z0-9+.-]*):\/\/([^@/]*@)?(\[[^\]]*\]|[^:/]*)(?::(\d+))?(\/.*)?$/i.exec(url2);
14959
+ if (match === null)
14960
+ return url2;
14961
+ const [, scheme = "", userinfo = "", host = "", port = "4222", rest = ""] = match;
14962
+ return `${scheme.toLowerCase()}://${userinfo}${host.toLowerCase()}:${port}${rest.replace(/\/+$/, "")}`;
14963
+ }
14964
+ function natsCredential(env, section, namedNatsUrls, filePath) {
14965
+ const fromEnvironment = natsSeedFromEnvironment(env);
14966
+ if (fromEnvironment !== undefined || section.natsSeed === undefined)
14967
+ return fromEnvironment;
14968
+ if (namedNatsUrls.length > 0) {
14969
+ const servers = new Set((section.natsUrls ?? []).map(natsServerKey));
14970
+ const named = new Set(namedNatsUrls.map(natsServerKey));
14971
+ if (named.size !== servers.size || ![...named].every((server) => servers.has(server))) {
14972
+ throw new Error(`ENVOY_NATS_URL names other servers than ${filePath}'s envoy.natsUrls; set NATS_NKEY_SEED_FILE for them`);
14973
+ }
14974
+ }
14975
+ return {
14976
+ value: fileValue(section.natsSeed, "envoy.natsSeed", filePath, env),
14977
+ source: `envoy.natsSeed in ${filePath}`
14978
+ };
14979
+ }
14980
+ function envoySettings(env, config2, options = {}) {
14981
+ if (config2.kind === "failed")
14982
+ return offWithError(config2.error);
14983
+ const section = config2.envoy;
14984
+ if (section.enabled === false) {
14985
+ return offWithNotice(`${config2.path} sets envoy.enabled to false: Envoy is off`);
14499
14986
  }
14500
- const rawUrl = explicitUrl !== undefined ? { value: explicitUrl, source: "DISPATCH_URL" } : merged.enabled === true ? { value: merged.serverUrl ?? DEFAULT_SERVER_URL, source: "dispatch.serverUrl" } : null;
14501
- const url2 = rawUrl ? parsedDispatchUrl(rawUrl.value, rawUrl.source) : { url: null, error: null };
14987
+ const namedUrl = nonEmpty(env.ENVOY_URL);
14988
+ const namedNatsUrls = (env.ENVOY_NATS_URL ?? "").split(",").map((url3) => url3.trim()).filter((url3) => url3.length > 0);
14989
+ const addressed = namedUrl !== undefined || namedNatsUrls.length > 0 || section.url !== undefined || (section.natsUrls ?? []).length > 0 || section.enabled === true;
14990
+ if (!addressed) {
14991
+ return offWithNotice(config2.missing === null ? `${config2.path} names no Envoy listener or NATS server, and neither ENVOY_URL nor ENVOY_NATS_URL is set: Envoy is off` : missingConfigNotice(config2.path, config2.missing, "Envoy is off"));
14992
+ }
14993
+ if (namedUrl !== undefined && httpUrl2(namedUrl) === undefined) {
14994
+ return offWithError("ENVOY_URL must be an http or https URL");
14995
+ }
14996
+ const listener = section.url ?? DEFAULT_ENVOY_URL;
14997
+ const url2 = namedUrl ?? listener;
14502
14998
  let token;
14503
- let tokenSource;
14504
- if (env.DISPATCH_TOKEN_FILE !== undefined) {
14505
- tokenSource = "DISPATCH_TOKEN_FILE";
14999
+ try {
15000
+ const source = bearerSource(env, "ENVOY", () => section.token === undefined ? undefined : {
15001
+ token: section.token,
15002
+ field: "envoy.token",
15003
+ path: config2.path,
15004
+ server: listener,
15005
+ checkout: false
15006
+ });
15007
+ token = source.kind === "none" ? undefined : source.kind === "variable" ? source.token : fileBearer(source, url2, "ENVOY", env);
15008
+ } catch (error48) {
15009
+ return offWithError(messageFor(error48));
15010
+ }
15011
+ const natsUrls = namedNatsUrls.length > 0 ? namedNatsUrls : section.natsUrls ?? [];
15012
+ let inbound;
15013
+ if (options.nats === false)
15014
+ inbound = { off: NO_NATS_CONNECTION };
15015
+ else if (natsUrls.length === 0)
15016
+ inbound = { off: NO_NATS_SERVER };
15017
+ else {
14506
15018
  try {
14507
- token = readSecretFile(tokenSource, env.DISPATCH_TOKEN_FILE);
15019
+ inbound = { natsUrls, natsSeed: natsCredential(env, section, namedNatsUrls, config2.path) };
14508
15020
  } catch (error48) {
14509
- return { enabled: false, url: url2.url, token: null, error: messageFor(error48) };
15021
+ inbound = { off: messageFor(error48) };
14510
15022
  }
14511
- } else if (env.DISPATCH_TOKEN !== undefined) {
14512
- tokenSource = "DISPATCH_TOKEN";
14513
- token = env.DISPATCH_TOKEN;
14514
- } else {
14515
- tokenSource = "dispatch.token";
14516
- token = merged.token ?? null;
14517
- }
14518
- if (url2.error !== null)
14519
- return { enabled: false, url: null, token, error: url2.error };
14520
- if (url2.url === null)
14521
- return { enabled: false, url: null, token, error: null };
14522
- if (!token) {
14523
- return {
14524
- enabled: false,
14525
- url: url2.url,
14526
- token,
14527
- error: `${tokenSource} must be a non-empty bearer token`
14528
- };
14529
15023
  }
14530
- return { enabled: true, url: url2.url, token, error: null };
15024
+ const rawHeartbeatMs = Number(env.ENVOY_HEARTBEAT_MS);
15025
+ return {
15026
+ enabled: true,
15027
+ url: normalizeEnvoyUrl(url2),
15028
+ token,
15029
+ inbound,
15030
+ heartbeatMs: Number.isFinite(rawHeartbeatMs) && rawHeartbeatMs > 0 ? Math.max(rawHeartbeatMs, MIN_HEARTBEAT_MS) : DEFAULT_HEARTBEAT_MS
15031
+ };
14531
15032
  }
14532
-
14533
- // ../envoy-client/src/dispatch-first.ts
14534
- import { join as join2 } from "path";
14535
- function dispatchFirstSkillFile(skillsDirectory) {
14536
- return join2(skillsDirectory, "dispatch-first", "SKILL.md");
15033
+ function normalizeEnvoyUrl(value) {
15034
+ return value.replace(/\/+$/, "");
14537
15035
  }
14538
15036
 
14539
- // ../envoy-client/src/machine.ts
14540
- import { hostname as hostname3 } from "os";
14541
- function machineID() {
14542
- return process.env["ENVOY_MACHINE_ID"] || hostname3();
15037
+ // ../envoy-client/src/session-settings.ts
15038
+ function sessionConfigFile(harness, env) {
15039
+ try {
15040
+ return { path: envoyConfigPath(harness, env), error: null };
15041
+ } catch (error48) {
15042
+ return { path: null, error: messageFor(error48) };
15043
+ }
15044
+ }
15045
+ function startNotice(config2, dispatch, envoy, nats) {
15046
+ if (config2.kind === "failed")
15047
+ return `${config2.error}: Dispatch and Envoy are off`;
15048
+ const { missing, checkout } = config2;
15049
+ const dispatchQuiet = !dispatch.enabled && dispatch.error === null;
15050
+ const envoyQuiet = !envoy.enabled && envoy.error === null;
15051
+ const parts = [];
15052
+ if (missing !== null && dispatchQuiet && envoyQuiet) {
15053
+ parts.push(missingConfigNotice(config2.path, missing, "Dispatch and Envoy are off"));
15054
+ } else {
15055
+ if (!dispatch.enabled && dispatch.error !== null)
15056
+ parts.push(`Dispatch is off: ${dispatch.error}`);
15057
+ else if (dispatchQuiet && missing !== null) {
15058
+ parts.push(missingConfigNotice(config2.path, missing, "Dispatch is off"));
15059
+ }
15060
+ if (!envoy.enabled && envoy.error !== null)
15061
+ parts.push(`Envoy is off: ${envoy.error}`);
15062
+ else if (envoyQuiet && missing !== null) {
15063
+ parts.push(missingConfigNotice(config2.path, missing, "Envoy is off"));
15064
+ }
15065
+ }
15066
+ if (checkout?.kind === "valid" && checkout.envoyError !== null)
15067
+ parts.push(checkout.envoyError);
15068
+ if (checkout?.kind === "invalid" && (dispatch.enabled || dispatch.error !== checkout.error)) {
15069
+ parts.push(`${checkout.error}; not read while DISPATCH_URL is set`);
15070
+ }
15071
+ parts.push(...config2.warnings);
15072
+ if (envoy.enabled && nats && "off" in envoy.inbound) {
15073
+ parts.push(`Envoy's inbound messages are off: ${envoy.inbound.off}; the Envoy tools still work`);
15074
+ }
15075
+ return parts.length === 0 ? null : parts.join("; ");
15076
+ }
15077
+ function sessionSettings(file2, env, options) {
15078
+ if (file2.path === null) {
15079
+ const off = offWithError(file2.error);
15080
+ return { dispatch: off, envoy: off, notice: `${file2.error}: Dispatch and Envoy are off` };
15081
+ }
15082
+ const config2 = readEnvoyConfig(file2.path, env, options);
15083
+ const dispatch = dispatchSettings(env, config2);
15084
+ const envoy = envoySettings(env, config2, { nats: options.nats });
15085
+ return { dispatch, envoy, notice: startNotice(config2, dispatch, envoy, options.nats !== false) };
14543
15086
  }
14544
15087
 
14545
15088
  // ../envoy-client/src/dispatch-command.ts
@@ -14736,16 +15279,10 @@ class EnvoyApiError extends Error {
14736
15279
  this.expected = parsed?.expected;
14737
15280
  }
14738
15281
  }
14739
- function resolveEnvoyToken(env) {
14740
- const { ENVOY_TOKEN_FILE: file2, ENVOY_TOKEN: plain } = env;
14741
- if (file2 !== undefined && file2 !== "")
14742
- return readSecretFile("ENVOY_TOKEN_FILE", file2);
14743
- return plain || undefined;
14744
- }
14745
15282
  function createEnvoyClient(config2) {
14746
15283
  const baseUrl = normalizeEnvoyUrl(config2.baseUrl);
14747
15284
  const timeoutMs = config2.timeoutMs ?? DEFAULT_TIMEOUT_MS;
14748
- const apiToken = resolveEnvoyToken(process.env);
15285
+ const apiToken = config2.token;
14749
15286
  const request = async (path2, init) => {
14750
15287
  const url2 = `${baseUrl}${path2}`;
14751
15288
  for (let attempt = 0;attempt < 2; attempt += 1) {
@@ -14876,8 +15413,8 @@ function createEnvoyClient(config2) {
14876
15413
  };
14877
15414
  }
14878
15415
  function waitForRetry() {
14879
- const { promise: promise2, resolve } = Promise.withResolvers();
14880
- setTimeout(resolve, RETRY_DELAY_MS);
15416
+ const { promise: promise2, resolve: resolve2 } = Promise.withResolvers();
15417
+ setTimeout(resolve2, RETRY_DELAY_MS);
14881
15418
  return promise2;
14882
15419
  }
14883
15420
  function messageMetadata(input) {
@@ -14957,12 +15494,12 @@ function portFromSsOutput(output, pid) {
14957
15494
  }
14958
15495
 
14959
15496
  // src/port.ts
14960
- var defaultExec = (command, args, options) => new Promise((resolve, reject) => {
15497
+ var defaultExec = (command, args, options) => new Promise((resolve2, reject) => {
14961
15498
  execFile(command, args, { encoding: options.encoding }, (error48, stdout) => {
14962
15499
  if (error48)
14963
15500
  reject(error48);
14964
15501
  else
14965
- resolve(stdout);
15502
+ resolve2(stdout);
14966
15503
  });
14967
15504
  });
14968
15505
  async function resolvePort(serverUrl, exec = defaultExec) {
@@ -14983,7 +15520,7 @@ var moduleDirectory = path3.dirname(fileURLToPath(import.meta.url));
14983
15520
  var skillsDirectory = [
14984
15521
  path3.resolve(moduleDirectory, "../../skills"),
14985
15522
  path3.resolve(moduleDirectory, "../../../skills")
14986
- ].find((dir) => existsSync(dir));
15523
+ ].find((dir) => existsSync2(dir));
14987
15524
  var PACKAGE_NAME = "@sjawhar/opencode-legion-envoy";
14988
15525
  function packageRoot(from) {
14989
15526
  for (let directory = from;; directory = path3.dirname(directory)) {
@@ -15017,24 +15554,138 @@ var publishSpec = toolSpec("envoy_publish");
15017
15554
  var roleSetSpec = toolSpec("envoy_role_set");
15018
15555
  var whoamiSpec = toolSpec("envoy_whoami");
15019
15556
  var sessionsSpec = toolSpec("envoy_sessions");
15557
+ function envoyTools(client, port, title) {
15558
+ return {
15559
+ envoy_subscribe: tool({
15560
+ description: subscribeSpec.description,
15561
+ args: subscribeSpec.arguments(zodSchemaApi(tool.schema)),
15562
+ async execute(args, ctx) {
15563
+ ctx.metadata({ title: "Envoy subscribe" });
15564
+ return JSON.stringify(await client.subscribe({
15565
+ sessionID: ctx.sessionID,
15566
+ directory: ctx.directory,
15567
+ topics: args.topics,
15568
+ port: port() ?? 0,
15569
+ title: title() ?? "",
15570
+ driving: true
15571
+ }));
15572
+ }
15573
+ }),
15574
+ envoy_unsubscribe: tool({
15575
+ description: unsubscribeSpec.description,
15576
+ args: unsubscribeSpec.arguments(zodSchemaApi(tool.schema)),
15577
+ async execute(args, ctx) {
15578
+ ctx.metadata({ title: "Envoy unsubscribe" });
15579
+ await client.unsubscribe({
15580
+ sessionID: ctx.sessionID,
15581
+ topics: args.topics ?? []
15582
+ });
15583
+ return "ok";
15584
+ }
15585
+ }),
15586
+ envoy_list: tool({
15587
+ description: listSpec.description,
15588
+ args: listSpec.arguments(zodSchemaApi(tool.schema)),
15589
+ async execute(_args, ctx) {
15590
+ ctx.metadata({ title: "Envoy list" });
15591
+ return JSON.stringify(await client.getInterest(ctx.sessionID));
15592
+ }
15593
+ }),
15594
+ envoy_send: tool({
15595
+ description: sendSpec.description,
15596
+ args: sendSpec.arguments(zodSchemaApi(tool.schema)),
15597
+ async execute(args, ctx) {
15598
+ ctx.metadata({ title: "Envoy send" });
15599
+ return JSON.stringify(await client.send({
15600
+ sourceSessionID: ctx.sessionID,
15601
+ targetSessionID: args.session_id,
15602
+ message: args.message,
15603
+ ...toMessageMetadata(args)
15604
+ }));
15605
+ }
15606
+ }),
15607
+ envoy_publish: tool({
15608
+ description: publishSpec.description,
15609
+ args: publishSpec.arguments(zodSchemaApi(tool.schema)),
15610
+ async execute(args, ctx) {
15611
+ ctx.metadata({ title: "Envoy publish" });
15612
+ return JSON.stringify(await client.publish({
15613
+ sourceSessionID: ctx.sessionID,
15614
+ topic: args.topic,
15615
+ message: args.message,
15616
+ ...toMessageMetadata(args)
15617
+ }));
15618
+ }
15619
+ }),
15620
+ envoy_role_set: tool({
15621
+ description: roleSetSpec.description,
15622
+ args: roleSetSpec.arguments(zodSchemaApi(tool.schema)),
15623
+ async execute(args, ctx) {
15624
+ ctx.metadata({ title: "Set Envoy role" });
15625
+ const result = await client.setRole({
15626
+ sessionID: ctx.sessionID,
15627
+ role: args.role
15628
+ });
15629
+ if (!result.claimed)
15630
+ throw new Error(`role ${args.role} is held by ${result.holder}`);
15631
+ return JSON.stringify(result.interest);
15632
+ }
15633
+ }),
15634
+ envoy_whoami: tool({
15635
+ description: whoamiSpec.description,
15636
+ args: whoamiSpec.arguments(zodSchemaApi(tool.schema)),
15637
+ async execute(_args, ctx) {
15638
+ ctx.metadata({ title: "Envoy whoami" });
15639
+ return JSON.stringify({
15640
+ session_id: ctx.sessionID,
15641
+ machine_id: machineID(),
15642
+ port: port(),
15643
+ dir: ctx.directory
15644
+ }, null, 2);
15645
+ }
15646
+ }),
15647
+ envoy_sessions: tool({
15648
+ description: sessionsSpec.description,
15649
+ args: sessionsSpec.arguments(zodSchemaApi(tool.schema)),
15650
+ async execute(args, ctx) {
15651
+ ctx.metadata({ title: "Envoy sessions" });
15652
+ const sessions = await client.listSessions({
15653
+ ...args.dir === undefined ? {} : { directory: args.dir },
15654
+ ...args.title === undefined ? {} : { title: args.title }
15655
+ });
15656
+ const machine = args.machine;
15657
+ return JSON.stringify(machine ? sessions.filter((session) => session.machine_id === machine) : sessions, null, 2);
15658
+ }
15659
+ })
15660
+ };
15661
+ }
15020
15662
  var server_default = async (input) => {
15021
15663
  const cwd = process.cwd();
15022
- const dispatchConfig = resolveDispatchConfig(process.env, { cwd });
15023
- if (!dispatchConfig.enabled) {
15024
- logger.warn(`envoy: Dispatch disabled \u2014 ${dispatchConfig.error ?? "no Dispatch URL configured"}`);
15025
- }
15664
+ const configFile = sessionConfigFile("opencode", process.env);
15665
+ const { dispatch, envoy, notice } = sessionSettings(configFile, process.env, {
15666
+ cwd,
15667
+ nats: false
15668
+ });
15669
+ if (notice !== null)
15670
+ logger.warn(`envoy: ${notice}`);
15026
15671
  let dispatchFirst;
15027
- if (dispatchConfig.enabled) {
15672
+ if (dispatch.enabled) {
15028
15673
  if (skillsDirectory === undefined) {
15029
15674
  throw new Error(`envoy: no skills directory beside ${moduleDirectory}`);
15030
15675
  }
15031
15676
  dispatchFirst = dispatchFirstSkillFile(skillsDirectory);
15032
- if (!existsSync(dispatchFirst)) {
15677
+ if (!existsSync2(dispatchFirst)) {
15033
15678
  throw new Error(`envoy: the dispatch-first skill ${dispatchFirst} is missing`);
15034
15679
  }
15035
15680
  }
15036
- const envoyDefaults = envoyDefaultsFromEnvironment(process.env);
15037
- const envoy = createEnvoyClient({ baseUrl: envoyDefaults.envoyUrl, fetch: globalThis.fetch });
15681
+ const envoyOn = envoy.enabled ? {
15682
+ heartbeatMs: envoy.heartbeatMs,
15683
+ client: createEnvoyClient({
15684
+ baseUrl: envoy.url,
15685
+ fetch: globalThis.fetch,
15686
+ token: envoy.token
15687
+ })
15688
+ } : undefined;
15038
15689
  let activeSessionID = null;
15039
15690
  let activeSessionTitle = null;
15040
15691
  const trackedSessions = new Map;
@@ -15073,15 +15724,7 @@ var server_default = async (input) => {
15073
15724
  return null;
15074
15725
  }
15075
15726
  };
15076
- const timer = setInterval(() => {
15077
- syncPort().then((resolved) => {
15078
- if (resolved)
15079
- clearInterval(timer);
15080
- });
15081
- }, 1000);
15082
- timer.unref();
15083
- syncPort().catch(() => {});
15084
- const subscribeSession = (sessionID, title, port, driving) => envoy.subscribe({
15727
+ const subscribeSession = (client, sessionID, title, port, driving) => client.subscribe({
15085
15728
  sessionID,
15086
15729
  directory: cwd,
15087
15730
  topics: [agentSubject(sessionID)],
@@ -15089,20 +15732,31 @@ var server_default = async (input) => {
15089
15732
  title: title ?? "",
15090
15733
  driving
15091
15734
  }).catch(() => {});
15092
- const heartbeatMs = envoyDefaults.heartbeatMs;
15093
- const heartbeatInterval = setInterval(() => {
15094
- const port = currentPort();
15095
- if (!port)
15096
- return;
15097
- for (const [sessionID, info] of trackedSessions) {
15098
- subscribeSession(sessionID, info.title, port, info.driving);
15099
- }
15100
- }, heartbeatMs);
15101
- heartbeatInterval.unref?.();
15102
- process.on("exit", () => {
15103
- clearInterval(timer);
15104
- clearInterval(heartbeatInterval);
15105
- });
15735
+ let stopEnvoy = () => {};
15736
+ if (envoyOn !== undefined) {
15737
+ const portTimer = setInterval(() => {
15738
+ syncPort().then((resolved) => {
15739
+ if (resolved)
15740
+ clearInterval(portTimer);
15741
+ });
15742
+ }, 1000);
15743
+ portTimer.unref();
15744
+ syncPort().catch(() => {});
15745
+ const heartbeatTimer = setInterval(() => {
15746
+ const port = currentPort();
15747
+ if (!port)
15748
+ return;
15749
+ for (const [sessionID, info] of trackedSessions) {
15750
+ subscribeSession(envoyOn.client, sessionID, info.title, port, info.driving);
15751
+ }
15752
+ }, envoyOn.heartbeatMs);
15753
+ heartbeatTimer.unref?.();
15754
+ stopEnvoy = () => {
15755
+ clearInterval(portTimer);
15756
+ clearInterval(heartbeatTimer);
15757
+ };
15758
+ }
15759
+ process.on("exit", stopEnvoy);
15106
15760
  return {
15107
15761
  config: (cfg) => {
15108
15762
  if (skillsDirectory) {
@@ -15121,6 +15775,8 @@ var server_default = async (input) => {
15121
15775
  "shell.env": async (input2, output) => {
15122
15776
  output.env.PATH = `${PLUGIN_BIN}${path3.delimiter}${output.env.PATH ?? process.env.PATH ?? ""}`;
15123
15777
  output.env.DISPATCH_HOST = "opencode";
15778
+ if (configFile.path !== null)
15779
+ output.env.ENVOY_CONFIG_FILE = configFile.path;
15124
15780
  output.env.DISPATCH_SESSION_ID = "";
15125
15781
  output.env.DISPATCH_SESSION_TITLE = "";
15126
15782
  if (input2.sessionID) {
@@ -15133,7 +15789,7 @@ var server_default = async (input) => {
15133
15789
  event: async ({
15134
15790
  event
15135
15791
  }) => {
15136
- if (activeSessionID)
15792
+ if (envoy.enabled && activeSessionID)
15137
15793
  syncPort().catch(() => {});
15138
15794
  if (event.type === "session.status" && event.properties?.status?.type === "busy") {
15139
15795
  const sessionID = event.properties?.sessionID;
@@ -15143,26 +15799,29 @@ var server_default = async (input) => {
15143
15799
  if (!trackedSessions.has(sessionID)) {
15144
15800
  trackedSessions.set(sessionID, { title: null, driving: true });
15145
15801
  }
15146
- await syncPort();
15147
- const port = currentPort();
15148
- const titlePromise = fetchTitle(sessionID).then((t) => {
15149
- if (t && activeSessionID === sessionID)
15150
- activeSessionTitle = t;
15151
- return t;
15802
+ const titlePromise = fetchTitle(sessionID).then((title) => {
15803
+ if (!title)
15804
+ return null;
15805
+ const tracked = trackedSessions.get(sessionID);
15806
+ if (tracked !== undefined) {
15807
+ trackedSessions.set(sessionID, { title, driving: tracked.driving });
15808
+ }
15809
+ if (activeSessionID === sessionID)
15810
+ activeSessionTitle = title;
15811
+ return title;
15152
15812
  });
15153
- if (port) {
15154
- await subscribeSession(sessionID, activeSessionTitle, port, true);
15155
- titlePromise.then((title) => {
15156
- if (!title)
15157
- return;
15158
- if (trackedSessions.has(sessionID)) {
15159
- const driving = trackedSessions.get(sessionID)?.driving ?? true;
15160
- trackedSessions.set(sessionID, { title, driving });
15161
- subscribeSession(sessionID, title, currentPort() ?? 0, driving);
15162
- }
15163
- if (activeSessionID === sessionID)
15164
- activeSessionTitle = title;
15165
- });
15813
+ if (envoyOn !== undefined) {
15814
+ await syncPort();
15815
+ const port = currentPort();
15816
+ if (port) {
15817
+ await subscribeSession(envoyOn.client, sessionID, activeSessionTitle, port, true);
15818
+ titlePromise.then((title) => {
15819
+ const tracked = trackedSessions.get(sessionID);
15820
+ if (title && tracked !== undefined) {
15821
+ subscribeSession(envoyOn.client, sessionID, title, currentPort() ?? 0, tracked.driving);
15822
+ }
15823
+ });
15824
+ }
15166
15825
  }
15167
15826
  }
15168
15827
  }
@@ -15175,119 +15834,12 @@ var server_default = async (input) => {
15175
15834
  activeSessionID = null;
15176
15835
  activeSessionTitle = null;
15177
15836
  }
15178
- envoy.unsubscribe({ sessionID: deletedID, topics: [] }).catch(() => {});
15837
+ envoyOn?.client.unsubscribe({ sessionID: deletedID, topics: [] }).catch(() => {});
15179
15838
  }
15180
15839
  }
15181
15840
  },
15182
- dispose: () => {
15183
- clearInterval(timer);
15184
- clearInterval(heartbeatInterval);
15185
- },
15186
- tool: {
15187
- envoy_subscribe: tool({
15188
- description: subscribeSpec.description,
15189
- args: subscribeSpec.arguments(zodSchemaApi(tool.schema)),
15190
- async execute(args, ctx) {
15191
- ctx.metadata({ title: "Envoy subscribe" });
15192
- return JSON.stringify(await envoy.subscribe({
15193
- sessionID: ctx.sessionID,
15194
- directory: ctx.directory,
15195
- topics: args.topics,
15196
- port: currentPort() ?? 0,
15197
- title: activeSessionTitle ?? "",
15198
- driving: true
15199
- }));
15200
- }
15201
- }),
15202
- envoy_unsubscribe: tool({
15203
- description: unsubscribeSpec.description,
15204
- args: unsubscribeSpec.arguments(zodSchemaApi(tool.schema)),
15205
- async execute(args, ctx) {
15206
- ctx.metadata({ title: "Envoy unsubscribe" });
15207
- await envoy.unsubscribe({
15208
- sessionID: ctx.sessionID,
15209
- topics: args.topics ?? []
15210
- });
15211
- return "ok";
15212
- }
15213
- }),
15214
- envoy_list: tool({
15215
- description: listSpec.description,
15216
- args: listSpec.arguments(zodSchemaApi(tool.schema)),
15217
- async execute(_args, ctx) {
15218
- ctx.metadata({ title: "Envoy list" });
15219
- return JSON.stringify(await envoy.getInterest(ctx.sessionID));
15220
- }
15221
- }),
15222
- envoy_send: tool({
15223
- description: sendSpec.description,
15224
- args: sendSpec.arguments(zodSchemaApi(tool.schema)),
15225
- async execute(args, ctx) {
15226
- ctx.metadata({ title: "Envoy send" });
15227
- return JSON.stringify(await envoy.send({
15228
- sourceSessionID: ctx.sessionID,
15229
- targetSessionID: args.session_id,
15230
- message: args.message,
15231
- ...toMessageMetadata(args)
15232
- }));
15233
- }
15234
- }),
15235
- envoy_publish: tool({
15236
- description: publishSpec.description,
15237
- args: publishSpec.arguments(zodSchemaApi(tool.schema)),
15238
- async execute(args, ctx) {
15239
- ctx.metadata({ title: "Envoy publish" });
15240
- return JSON.stringify(await envoy.publish({
15241
- sourceSessionID: ctx.sessionID,
15242
- topic: args.topic,
15243
- message: args.message,
15244
- ...toMessageMetadata(args)
15245
- }));
15246
- }
15247
- }),
15248
- envoy_role_set: tool({
15249
- description: roleSetSpec.description,
15250
- args: roleSetSpec.arguments(zodSchemaApi(tool.schema)),
15251
- async execute(args, ctx) {
15252
- ctx.metadata({ title: "Set Envoy role" });
15253
- const result = await envoy.setRole({
15254
- sessionID: ctx.sessionID,
15255
- role: args.role
15256
- });
15257
- if (!result.claimed)
15258
- throw new Error(`role ${args.role} is held by ${result.holder}`);
15259
- return JSON.stringify(result.interest);
15260
- }
15261
- }),
15262
- envoy_whoami: tool({
15263
- description: whoamiSpec.description,
15264
- args: whoamiSpec.arguments(zodSchemaApi(tool.schema)),
15265
- async execute(_args, ctx) {
15266
- ctx.metadata({ title: "Envoy whoami" });
15267
- const sessionID = ctx.sessionID;
15268
- const port = currentPort();
15269
- return JSON.stringify({
15270
- session_id: sessionID,
15271
- machine_id: machineID(),
15272
- port,
15273
- dir: ctx.directory
15274
- }, null, 2);
15275
- }
15276
- }),
15277
- envoy_sessions: tool({
15278
- description: sessionsSpec.description,
15279
- args: sessionsSpec.arguments(zodSchemaApi(tool.schema)),
15280
- async execute(args, ctx) {
15281
- ctx.metadata({ title: "Envoy sessions" });
15282
- const sessions = await envoy.listSessions({
15283
- ...args.dir === undefined ? {} : { directory: args.dir },
15284
- ...args.title === undefined ? {} : { title: args.title }
15285
- });
15286
- const machine = args.machine;
15287
- return JSON.stringify(machine ? sessions.filter((session) => session.machine_id === machine) : sessions, null, 2);
15288
- }
15289
- })
15290
- }
15841
+ dispose: stopEnvoy,
15842
+ tool: envoyOn === undefined ? {} : envoyTools(envoyOn.client, currentPort, () => activeSessionTitle)
15291
15843
  };
15292
15844
  };
15293
15845
  export {