@sjawhar/opencode-legion-envoy 0.9.0 → 0.11.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.
@@ -1,7 +1,7 @@
1
1
  // @bun
2
2
  // src/server.ts
3
- import { existsSync as existsSync3 } from "fs";
4
- import path4 from "path";
3
+ import { existsSync } from "fs";
4
+ import path3 from "path";
5
5
  import { fileURLToPath } from "url";
6
6
 
7
7
  // ../../node_modules/.bun/zod@4.3.6/node_modules/zod/v4/core/core.js
@@ -86,6 +86,9 @@ function getEnumValues(entries) {
86
86
  const values = Object.entries(entries).filter(([k, _]) => numericValues.indexOf(+k) === -1).map(([_, v]) => v);
87
87
  return values;
88
88
  }
89
+ function joinValues(array, separator = "|") {
90
+ return array.map((val) => stringifyPrimitive(val)).join(separator);
91
+ }
89
92
  function jsonStringifyReplacer(_, value) {
90
93
  if (typeof value === "bigint")
91
94
  return value.toString();
@@ -237,6 +240,13 @@ function normalizeParams(_params) {
237
240
  return { ...params, error: () => params.error };
238
241
  return params;
239
242
  }
243
+ function stringifyPrimitive(value) {
244
+ if (typeof value === "bigint")
245
+ return value.toString() + "n";
246
+ if (typeof value === "string")
247
+ return `"${value}"`;
248
+ return `${value}`;
249
+ }
240
250
  function optionalKeys(shape) {
241
251
  return Object.keys(shape).filter((k) => {
242
252
  return shape[k]._zod.optin === "optional" && shape[k]._zod.optout === "optional";
@@ -459,6 +469,27 @@ function getLengthableOrigin(input) {
459
469
  return "string";
460
470
  return "unknown";
461
471
  }
472
+ function parsedType(data) {
473
+ const t = typeof data;
474
+ switch (t) {
475
+ case "number": {
476
+ return Number.isNaN(data) ? "nan" : "number";
477
+ }
478
+ case "object": {
479
+ if (data === null) {
480
+ return "null";
481
+ }
482
+ if (Array.isArray(data)) {
483
+ return "array";
484
+ }
485
+ const obj = data;
486
+ if (obj && Object.getPrototypeOf(obj) !== Object.prototype && "constructor" in obj && obj.constructor) {
487
+ return obj.constructor.name;
488
+ }
489
+ }
490
+ }
491
+ return t;
492
+ }
462
493
  function issue(...args) {
463
494
  const [iss, input, inst] = args;
464
495
  if (typeof iss === "string") {
@@ -2473,6 +2504,112 @@ function handleRefineResult(result, payload, input, inst) {
2473
2504
  payload.issues.push(issue(_iss));
2474
2505
  }
2475
2506
  }
2507
+ // ../../node_modules/.bun/zod@4.3.6/node_modules/zod/v4/locales/en.js
2508
+ var error = () => {
2509
+ const Sizable = {
2510
+ string: { unit: "characters", verb: "to have" },
2511
+ file: { unit: "bytes", verb: "to have" },
2512
+ array: { unit: "items", verb: "to have" },
2513
+ set: { unit: "items", verb: "to have" },
2514
+ map: { unit: "entries", verb: "to have" }
2515
+ };
2516
+ function getSizing(origin) {
2517
+ return Sizable[origin] ?? null;
2518
+ }
2519
+ const FormatDictionary = {
2520
+ regex: "input",
2521
+ email: "email address",
2522
+ url: "URL",
2523
+ emoji: "emoji",
2524
+ uuid: "UUID",
2525
+ uuidv4: "UUIDv4",
2526
+ uuidv6: "UUIDv6",
2527
+ nanoid: "nanoid",
2528
+ guid: "GUID",
2529
+ cuid: "cuid",
2530
+ cuid2: "cuid2",
2531
+ ulid: "ULID",
2532
+ xid: "XID",
2533
+ ksuid: "KSUID",
2534
+ datetime: "ISO datetime",
2535
+ date: "ISO date",
2536
+ time: "ISO time",
2537
+ duration: "ISO duration",
2538
+ ipv4: "IPv4 address",
2539
+ ipv6: "IPv6 address",
2540
+ mac: "MAC address",
2541
+ cidrv4: "IPv4 range",
2542
+ cidrv6: "IPv6 range",
2543
+ base64: "base64-encoded string",
2544
+ base64url: "base64url-encoded string",
2545
+ json_string: "JSON string",
2546
+ e164: "E.164 number",
2547
+ jwt: "JWT",
2548
+ template_literal: "input"
2549
+ };
2550
+ const TypeDictionary = {
2551
+ nan: "NaN"
2552
+ };
2553
+ return (issue) => {
2554
+ switch (issue.code) {
2555
+ case "invalid_type": {
2556
+ const expected = TypeDictionary[issue.expected] ?? issue.expected;
2557
+ const receivedType = parsedType(issue.input);
2558
+ const received = TypeDictionary[receivedType] ?? receivedType;
2559
+ return `Invalid input: expected ${expected}, received ${received}`;
2560
+ }
2561
+ case "invalid_value":
2562
+ if (issue.values.length === 1)
2563
+ return `Invalid input: expected ${stringifyPrimitive(issue.values[0])}`;
2564
+ return `Invalid option: expected one of ${joinValues(issue.values, "|")}`;
2565
+ case "too_big": {
2566
+ const adj = issue.inclusive ? "<=" : "<";
2567
+ const sizing = getSizing(issue.origin);
2568
+ if (sizing)
2569
+ return `Too big: expected ${issue.origin ?? "value"} to have ${adj}${issue.maximum.toString()} ${sizing.unit ?? "elements"}`;
2570
+ return `Too big: expected ${issue.origin ?? "value"} to be ${adj}${issue.maximum.toString()}`;
2571
+ }
2572
+ case "too_small": {
2573
+ const adj = issue.inclusive ? ">=" : ">";
2574
+ const sizing = getSizing(issue.origin);
2575
+ if (sizing) {
2576
+ return `Too small: expected ${issue.origin} to have ${adj}${issue.minimum.toString()} ${sizing.unit}`;
2577
+ }
2578
+ return `Too small: expected ${issue.origin} to be ${adj}${issue.minimum.toString()}`;
2579
+ }
2580
+ case "invalid_format": {
2581
+ const _issue = issue;
2582
+ if (_issue.format === "starts_with") {
2583
+ return `Invalid string: must start with "${_issue.prefix}"`;
2584
+ }
2585
+ if (_issue.format === "ends_with")
2586
+ return `Invalid string: must end with "${_issue.suffix}"`;
2587
+ if (_issue.format === "includes")
2588
+ return `Invalid string: must include "${_issue.includes}"`;
2589
+ if (_issue.format === "regex")
2590
+ return `Invalid string: must match pattern ${_issue.pattern}`;
2591
+ return `Invalid ${FormatDictionary[_issue.format] ?? issue.format}`;
2592
+ }
2593
+ case "not_multiple_of":
2594
+ return `Invalid number: must be a multiple of ${issue.divisor}`;
2595
+ case "unrecognized_keys":
2596
+ return `Unrecognized key${issue.keys.length > 1 ? "s" : ""}: ${joinValues(issue.keys, ", ")}`;
2597
+ case "invalid_key":
2598
+ return `Invalid key in ${issue.origin}`;
2599
+ case "invalid_union":
2600
+ return "Invalid input";
2601
+ case "invalid_element":
2602
+ return `Invalid value in ${issue.origin}`;
2603
+ default:
2604
+ return `Invalid input`;
2605
+ }
2606
+ };
2607
+ };
2608
+ function en_default() {
2609
+ return {
2610
+ localeError: error()
2611
+ };
2612
+ }
2476
2613
  // ../../node_modules/.bun/zod@4.3.6/node_modules/zod/v4/core/registries.js
2477
2614
  var _a;
2478
2615
  var $output = Symbol("ZodOutput");
@@ -3415,10 +3552,45 @@ var numberProcessor = (schema, ctx, _json, _params) => {
3415
3552
  var booleanProcessor = (_schema, _ctx, json, _params) => {
3416
3553
  json.type = "boolean";
3417
3554
  };
3555
+ var bigintProcessor = (_schema, ctx, _json, _params) => {
3556
+ if (ctx.unrepresentable === "throw") {
3557
+ throw new Error("BigInt cannot be represented in JSON Schema");
3558
+ }
3559
+ };
3560
+ var symbolProcessor = (_schema, ctx, _json, _params) => {
3561
+ if (ctx.unrepresentable === "throw") {
3562
+ throw new Error("Symbols cannot be represented in JSON Schema");
3563
+ }
3564
+ };
3565
+ var nullProcessor = (_schema, ctx, json, _params) => {
3566
+ if (ctx.target === "openapi-3.0") {
3567
+ json.type = "string";
3568
+ json.nullable = true;
3569
+ json.enum = [null];
3570
+ } else {
3571
+ json.type = "null";
3572
+ }
3573
+ };
3574
+ var undefinedProcessor = (_schema, ctx, _json, _params) => {
3575
+ if (ctx.unrepresentable === "throw") {
3576
+ throw new Error("Undefined cannot be represented in JSON Schema");
3577
+ }
3578
+ };
3579
+ var voidProcessor = (_schema, ctx, _json, _params) => {
3580
+ if (ctx.unrepresentable === "throw") {
3581
+ throw new Error("Void cannot be represented in JSON Schema");
3582
+ }
3583
+ };
3418
3584
  var neverProcessor = (_schema, _ctx, json, _params) => {
3419
3585
  json.not = {};
3420
3586
  };
3587
+ var anyProcessor = (_schema, _ctx, _json, _params) => {};
3421
3588
  var unknownProcessor = (_schema, _ctx, _json, _params) => {};
3589
+ var dateProcessor = (_schema, ctx, _json, _params) => {
3590
+ if (ctx.unrepresentable === "throw") {
3591
+ throw new Error("Date cannot be represented in JSON Schema");
3592
+ }
3593
+ };
3422
3594
  var enumProcessor = (schema, _ctx, json, _params) => {
3423
3595
  const def = schema._zod.def;
3424
3596
  const values = getEnumValues(def.entries);
@@ -3466,16 +3638,71 @@ var literalProcessor = (schema, ctx, json, _params) => {
3466
3638
  json.enum = vals;
3467
3639
  }
3468
3640
  };
3641
+ var nanProcessor = (_schema, ctx, _json, _params) => {
3642
+ if (ctx.unrepresentable === "throw") {
3643
+ throw new Error("NaN cannot be represented in JSON Schema");
3644
+ }
3645
+ };
3646
+ var templateLiteralProcessor = (schema, _ctx, json, _params) => {
3647
+ const _json = json;
3648
+ const pattern = schema._zod.pattern;
3649
+ if (!pattern)
3650
+ throw new Error("Pattern not found in template literal");
3651
+ _json.type = "string";
3652
+ _json.pattern = pattern.source;
3653
+ };
3654
+ var fileProcessor = (schema, _ctx, json, _params) => {
3655
+ const _json = json;
3656
+ const file = {
3657
+ type: "string",
3658
+ format: "binary",
3659
+ contentEncoding: "binary"
3660
+ };
3661
+ const { minimum, maximum, mime } = schema._zod.bag;
3662
+ if (minimum !== undefined)
3663
+ file.minLength = minimum;
3664
+ if (maximum !== undefined)
3665
+ file.maxLength = maximum;
3666
+ if (mime) {
3667
+ if (mime.length === 1) {
3668
+ file.contentMediaType = mime[0];
3669
+ Object.assign(_json, file);
3670
+ } else {
3671
+ Object.assign(_json, file);
3672
+ _json.anyOf = mime.map((m) => ({ contentMediaType: m }));
3673
+ }
3674
+ } else {
3675
+ Object.assign(_json, file);
3676
+ }
3677
+ };
3678
+ var successProcessor = (_schema, _ctx, json, _params) => {
3679
+ json.type = "boolean";
3680
+ };
3469
3681
  var customProcessor = (_schema, ctx, _json, _params) => {
3470
3682
  if (ctx.unrepresentable === "throw") {
3471
3683
  throw new Error("Custom types cannot be represented in JSON Schema");
3472
3684
  }
3473
3685
  };
3686
+ var functionProcessor = (_schema, ctx, _json, _params) => {
3687
+ if (ctx.unrepresentable === "throw") {
3688
+ throw new Error("Function types cannot be represented in JSON Schema");
3689
+ }
3690
+ };
3474
3691
  var transformProcessor = (_schema, ctx, _json, _params) => {
3475
3692
  if (ctx.unrepresentable === "throw") {
3476
3693
  throw new Error("Transforms cannot be represented in JSON Schema");
3477
3694
  }
3478
3695
  };
3696
+ var mapProcessor = (_schema, ctx, _json, _params) => {
3697
+ if (ctx.unrepresentable === "throw") {
3698
+ throw new Error("Map cannot be represented in JSON Schema");
3699
+ }
3700
+ };
3701
+ var setProcessor = (_schema, ctx, _json, _params) => {
3702
+ if (ctx.unrepresentable === "throw") {
3703
+ throw new Error("Set cannot be represented in JSON Schema");
3704
+ }
3705
+ };
3479
3706
  var arrayProcessor = (schema, ctx, _json, params) => {
3480
3707
  const json = _json;
3481
3708
  const def = schema._zod.def;
@@ -3553,6 +3780,48 @@ var intersectionProcessor = (schema, ctx, json, params) => {
3553
3780
  ];
3554
3781
  json.allOf = allOf;
3555
3782
  };
3783
+ var tupleProcessor = (schema, ctx, _json, params) => {
3784
+ const json = _json;
3785
+ const def = schema._zod.def;
3786
+ json.type = "array";
3787
+ const prefixPath = ctx.target === "draft-2020-12" ? "prefixItems" : "items";
3788
+ const restPath = ctx.target === "draft-2020-12" ? "items" : ctx.target === "openapi-3.0" ? "items" : "additionalItems";
3789
+ const prefixItems = def.items.map((x, i) => process2(x, ctx, {
3790
+ ...params,
3791
+ path: [...params.path, prefixPath, i]
3792
+ }));
3793
+ const rest = def.rest ? process2(def.rest, ctx, {
3794
+ ...params,
3795
+ path: [...params.path, restPath, ...ctx.target === "openapi-3.0" ? [def.items.length] : []]
3796
+ }) : null;
3797
+ if (ctx.target === "draft-2020-12") {
3798
+ json.prefixItems = prefixItems;
3799
+ if (rest) {
3800
+ json.items = rest;
3801
+ }
3802
+ } else if (ctx.target === "openapi-3.0") {
3803
+ json.items = {
3804
+ anyOf: prefixItems
3805
+ };
3806
+ if (rest) {
3807
+ json.items.anyOf.push(rest);
3808
+ }
3809
+ json.minItems = prefixItems.length;
3810
+ if (!rest) {
3811
+ json.maxItems = prefixItems.length;
3812
+ }
3813
+ } else {
3814
+ json.items = prefixItems;
3815
+ if (rest) {
3816
+ json.additionalItems = rest;
3817
+ }
3818
+ }
3819
+ const { minimum, maximum } = schema._zod.bag;
3820
+ if (typeof minimum === "number")
3821
+ json.minItems = minimum;
3822
+ if (typeof maximum === "number")
3823
+ json.maxItems = maximum;
3824
+ };
3556
3825
  var recordProcessor = (schema, ctx, _json, params) => {
3557
3826
  const json = _json;
3558
3827
  const def = schema._zod.def;
@@ -3648,12 +3917,99 @@ var readonlyProcessor = (schema, ctx, json, params) => {
3648
3917
  seen.ref = def.innerType;
3649
3918
  json.readOnly = true;
3650
3919
  };
3920
+ var promiseProcessor = (schema, ctx, _json, params) => {
3921
+ const def = schema._zod.def;
3922
+ process2(def.innerType, ctx, params);
3923
+ const seen = ctx.seen.get(schema);
3924
+ seen.ref = def.innerType;
3925
+ };
3651
3926
  var optionalProcessor = (schema, ctx, _json, params) => {
3652
3927
  const def = schema._zod.def;
3653
3928
  process2(def.innerType, ctx, params);
3654
3929
  const seen = ctx.seen.get(schema);
3655
3930
  seen.ref = def.innerType;
3656
3931
  };
3932
+ var lazyProcessor = (schema, ctx, _json, params) => {
3933
+ const innerType = schema._zod.innerType;
3934
+ process2(innerType, ctx, params);
3935
+ const seen = ctx.seen.get(schema);
3936
+ seen.ref = innerType;
3937
+ };
3938
+ var allProcessors = {
3939
+ string: stringProcessor,
3940
+ number: numberProcessor,
3941
+ boolean: booleanProcessor,
3942
+ bigint: bigintProcessor,
3943
+ symbol: symbolProcessor,
3944
+ null: nullProcessor,
3945
+ undefined: undefinedProcessor,
3946
+ void: voidProcessor,
3947
+ never: neverProcessor,
3948
+ any: anyProcessor,
3949
+ unknown: unknownProcessor,
3950
+ date: dateProcessor,
3951
+ enum: enumProcessor,
3952
+ literal: literalProcessor,
3953
+ nan: nanProcessor,
3954
+ template_literal: templateLiteralProcessor,
3955
+ file: fileProcessor,
3956
+ success: successProcessor,
3957
+ custom: customProcessor,
3958
+ function: functionProcessor,
3959
+ transform: transformProcessor,
3960
+ map: mapProcessor,
3961
+ set: setProcessor,
3962
+ array: arrayProcessor,
3963
+ object: objectProcessor,
3964
+ union: unionProcessor,
3965
+ intersection: intersectionProcessor,
3966
+ tuple: tupleProcessor,
3967
+ record: recordProcessor,
3968
+ nullable: nullableProcessor,
3969
+ nonoptional: nonoptionalProcessor,
3970
+ default: defaultProcessor,
3971
+ prefault: prefaultProcessor,
3972
+ catch: catchProcessor,
3973
+ pipe: pipeProcessor,
3974
+ readonly: readonlyProcessor,
3975
+ promise: promiseProcessor,
3976
+ optional: optionalProcessor,
3977
+ lazy: lazyProcessor
3978
+ };
3979
+ function toJSONSchema(input, params) {
3980
+ if ("_idmap" in input) {
3981
+ const registry = input;
3982
+ const ctx = initializeContext({ ...params, processors: allProcessors });
3983
+ const defs = {};
3984
+ for (const entry of registry._idmap.entries()) {
3985
+ const [_, schema] = entry;
3986
+ process2(schema, ctx);
3987
+ }
3988
+ const schemas = {};
3989
+ const external = {
3990
+ registry,
3991
+ uri: params?.uri,
3992
+ defs
3993
+ };
3994
+ ctx.external = external;
3995
+ for (const entry of registry._idmap.entries()) {
3996
+ const [key, schema] = entry;
3997
+ extractDefs(ctx, schema);
3998
+ schemas[key] = finalize(ctx, schema);
3999
+ }
4000
+ if (Object.keys(defs).length > 0) {
4001
+ const defsSegment = ctx.target === "draft-2020-12" ? "$defs" : "definitions";
4002
+ schemas.__shared = {
4003
+ [defsSegment]: defs
4004
+ };
4005
+ }
4006
+ return { schemas };
4007
+ }
4008
+ const ctx = initializeContext({ ...params, processors: allProcessors });
4009
+ process2(input, ctx);
4010
+ extractDefs(ctx, input);
4011
+ return finalize(ctx, input);
4012
+ }
3657
4013
  // ../../node_modules/.bun/zod@4.3.6/node_modules/zod/v4/classic/iso.js
3658
4014
  var ZodISODateTime = /* @__PURE__ */ $constructor("ZodISODateTime", (inst, def) => {
3659
4015
  $ZodISODateTime.init(inst, def);
@@ -4372,11 +4728,27 @@ function refine(fn, _params = {}) {
4372
4728
  function superRefine(fn) {
4373
4729
  return _superRefine(fn);
4374
4730
  }
4731
+ // ../../node_modules/.bun/zod@4.3.6/node_modules/zod/v4/classic/external.js
4732
+ config(en_default());
4733
+ // ../contracts/src/dispatch-question.ts
4734
+ var DispatchQuestionOptionSchema = strictObject({
4735
+ label: string2().min(1),
4736
+ description: string2().optional()
4737
+ });
4738
+ var DispatchQuestionSchema = strictObject({
4739
+ askId: string2().optional(),
4740
+ question: string2().min(1),
4741
+ header: string2().optional(),
4742
+ options: array(DispatchQuestionOptionSchema).optional(),
4743
+ multiple: boolean2().optional(),
4744
+ custom: boolean2().optional()
4745
+ });
4746
+ var DispatchQuestionInputSchema = DispatchQuestionSchema.omit({ askId: true });
4375
4747
  // ../contracts/src/envelope.ts
4376
4748
  var isSubject = (value) => typeof value === "string" && value.length > 0;
4377
4749
  var EnvelopeSchema = object({
4378
4750
  event_id: string2().min(1),
4379
- source: _enum(["agent", "envoy", "github", "slack", "whatsapp", "ghostwispr"]),
4751
+ source: _enum(["agent", "human", "envoy", "github", "slack", "whatsapp", "ghostwispr"]),
4380
4752
  source_event_id: string2().min(1),
4381
4753
  source_session: string2().optional(),
4382
4754
  topic: custom(isSubject, { message: "topic must be a non-empty subject" }),
@@ -4669,11 +5041,370 @@ function normalizeEnvoyUrl(value) {
4669
5041
  return value.replace(/\/+$/, "");
4670
5042
  }
4671
5043
 
5044
+ // ../envoy-client/src/dispatch-contract.ts
5045
+ var DISPATCH_TOOL_NAME = "dispatch";
5046
+ var DISPATCH_CONTEXT_MAX = 1200;
5047
+ var DISPATCH_QUESTION_MAX = 800;
5048
+ var DISPATCH_URGENCIES = ["low", "med", "high", "blocking"];
5049
+ var DISPATCH_TOOL_DESCRIPTION = "Raise a durable question to the human as a Dispatch thread (a GitHub issue shown on the dashboard), or continue an existing thread with a follow-up question. The reader has NOT seen your transcript. Open a thread with `subject`; continue one with `thread`. The reply arrives in this session as a steer.";
5050
+ var DISPATCH_ARGUMENTS = {
5051
+ subject: "Open a new thread: one line naming the decision needed (the issue title). Omit when continuing a thread with `thread`.",
5052
+ thread: `Continue an existing thread: "<n>" (an issue in the working directory's repo) or "owner/name#<n>". When set, omit subject, urgency, repo, and parent.`,
5053
+ context: `What you are doing, what you found, why you are stuck \u2014 at most ${DISPATCH_CONTEXT_MAX} characters, at most three short paragraphs or a bullet list. The reader has NOT seen your transcript: no nouns you coined this session, no internal identifiers unless the question is about them. GitHub references (#N, owner/repo#N, URLs) may be bare; the dashboard unfurls them.`,
5054
+ question: `The ask, at most ${DISPATCH_QUESTION_MAX} characters, as a list: current state \u2192 desired state \u2192 your recommendation and why; options go in \`ask\`.`,
5055
+ ask: "Structured questions rendered as buttons on the dashboard. Each: { question, header?, options: [{ label, description? }], multiple?, custom? }. Use this whenever the answer is one of N choices.",
5056
+ urgency: "low | med | high | blocking (default med). Opening a thread only.",
5057
+ repo: "owner/name. Opening a thread only; defaults to the working directory's GitHub repo.",
5058
+ parent: "<n> | owner/name#<n>[#<commentId>]. Opening a thread only: link the thread as a sub-issue of an existing issue and append a breadcrumb to the comment."
5059
+ };
5060
+ class DispatchArgumentError extends Error {
5061
+ name = "DispatchArgumentError";
5062
+ }
5063
+ var prose = {
5064
+ context: string2(),
5065
+ question: string2(),
5066
+ ask: array(DispatchQuestionInputSchema).optional()
5067
+ };
5068
+ var OpenThreadCallSchema = strictObject({
5069
+ subject: string2(),
5070
+ ...prose,
5071
+ urgency: _enum(DISPATCH_URGENCIES).optional(),
5072
+ repo: string2().optional(),
5073
+ parent: string2().optional()
5074
+ });
5075
+ var ContinueThreadCallSchema = strictObject({ thread: string2(), ...prose });
5076
+ var dispatchToolShape = {
5077
+ subject: string2().describe(DISPATCH_ARGUMENTS.subject).optional(),
5078
+ thread: string2().describe(DISPATCH_ARGUMENTS.thread).optional(),
5079
+ context: string2().describe(DISPATCH_ARGUMENTS.context),
5080
+ question: string2().describe(DISPATCH_ARGUMENTS.question),
5081
+ ask: array(DispatchQuestionInputSchema).describe(DISPATCH_ARGUMENTS.ask).optional(),
5082
+ urgency: _enum(DISPATCH_URGENCIES).describe(DISPATCH_ARGUMENTS.urgency).optional(),
5083
+ repo: string2().describe(DISPATCH_ARGUMENTS.repo).optional(),
5084
+ parent: string2().describe(DISPATCH_ARGUMENTS.parent).optional()
5085
+ };
5086
+ var DISPATCH_TOOL_JSON_SCHEMA = toJSONSchema(object(dispatchToolShape));
5087
+ function isContinueCall(call) {
5088
+ return "thread" in call;
5089
+ }
5090
+ function present(raw) {
5091
+ return Object.fromEntries(Object.entries(raw).filter(([, value]) => value !== undefined));
5092
+ }
5093
+ function describeIssues(error) {
5094
+ return error.issues.map((issue) => `${issue.path.join(".") || "(root)"}: ${issue.message}`).join("; ");
5095
+ }
5096
+ function parseDispatchCall(raw) {
5097
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
5098
+ throw new DispatchArgumentError("dispatch: invalid arguments \u2014 expected an object");
5099
+ }
5100
+ const args = present(raw);
5101
+ const hasSubject = "subject" in args;
5102
+ const hasThread = "thread" in args;
5103
+ if (hasSubject && hasThread) {
5104
+ throw new DispatchArgumentError("dispatch: pass either subject (open a thread) or thread (continue one), not both");
5105
+ }
5106
+ if (!hasSubject && !hasThread) {
5107
+ throw new DispatchArgumentError("dispatch: subject or thread is required");
5108
+ }
5109
+ if (hasThread && (("urgency" in args) || ("repo" in args) || ("parent" in args))) {
5110
+ throw new DispatchArgumentError("dispatch: thread cannot be combined with urgency, repo, or parent");
5111
+ }
5112
+ const parsed = hasThread ? ContinueThreadCallSchema.safeParse(args) : OpenThreadCallSchema.safeParse(args);
5113
+ if (!parsed.success) {
5114
+ throw new DispatchArgumentError(`dispatch: invalid arguments \u2014 ${describeIssues(parsed.error)}`);
5115
+ }
5116
+ return parsed.data;
5117
+ }
5118
+
5119
+ // ../envoy-client/src/dispatch-cwd.ts
5120
+ import { execFile } from "child_process";
5121
+ import { promisify } from "util";
5122
+
5123
+ // ../envoy-client/src/machine.ts
5124
+ import { hostname } from "os";
5125
+ function machineID() {
5126
+ return process.env["ENVOY_MACHINE_ID"] || hostname();
5127
+ }
5128
+
5129
+ // ../envoy-client/src/dispatch-cwd.ts
5130
+ var execFileAsync = promisify(execFile);
5131
+ var defaultExec = (file, args, options) => execFileAsync(file, args, { cwd: options.cwd, timeout: 5000 });
5132
+ async function tryExec(exec, file, args, cwd) {
5133
+ try {
5134
+ const { stdout } = await exec(file, args, { cwd });
5135
+ return stdout;
5136
+ } catch {
5137
+ return null;
5138
+ }
5139
+ }
5140
+ function parseRemoteList(stdout) {
5141
+ const remotes = new Map;
5142
+ for (const line of stdout.split(`
5143
+ `)) {
5144
+ const trimmed = line.trim();
5145
+ if (!trimmed)
5146
+ continue;
5147
+ const match = trimmed.match(/^(\S+)\s+(\S+)/);
5148
+ const [, name, url] = match ?? [];
5149
+ if (name && url)
5150
+ remotes.set(name, url);
5151
+ }
5152
+ return remotes;
5153
+ }
5154
+ function selectRemoteUrl(remotes) {
5155
+ const origin = remotes.get("origin");
5156
+ if (origin)
5157
+ return origin;
5158
+ const candidates = [...remotes.entries()].filter(([name]) => name !== "upstream");
5159
+ return candidates.length === 1 ? candidates[0]?.[1] ?? null : null;
5160
+ }
5161
+ var GITHUB_REMOTE_PATTERNS = [
5162
+ /^https:\/\/(?:[^@/\s]+@)?github\.com\/([^/]+)\/([^/]+?)(?:\.git)?\/?$/i,
5163
+ /^git@github\.com:([^/]+)\/([^/]+?)(?:\.git)?\/?$/i,
5164
+ /^ssh:\/\/git@github\.com\/([^/]+)\/([^/]+?)(?:\.git)?\/?$/i
5165
+ ];
5166
+ function parseGitHubRemoteUrl(url) {
5167
+ const trimmed = url.trim();
5168
+ for (const pattern of GITHUB_REMOTE_PATTERNS) {
5169
+ const match = trimmed.match(pattern);
5170
+ if (match)
5171
+ return `${match[1]}/${match[2]}`;
5172
+ }
5173
+ return null;
5174
+ }
5175
+ async function resolveCwdRepo(cwd, exec) {
5176
+ const jjOutput = await tryExec(exec, "jj", ["git", "remote", "list"], cwd);
5177
+ if (jjOutput !== null) {
5178
+ const url = selectRemoteUrl(parseRemoteList(jjOutput));
5179
+ return url ? parseGitHubRemoteUrl(url) : null;
5180
+ }
5181
+ const originUrl = await tryExec(exec, "git", ["remote", "get-url", "origin"], cwd);
5182
+ return originUrl ? parseGitHubRemoteUrl(originUrl) : null;
5183
+ }
5184
+ async function resolveOrigin(env, exec, cwd) {
5185
+ const origin = {
5186
+ cwd,
5187
+ machine: machineID()
5188
+ };
5189
+ const pane = env.TMUX_PANE;
5190
+ if (pane) {
5191
+ const output = await tryExec(exec, "tmux", ["display-message", "-p", "-t", pane, "#S:#I.#P #{pane_id}"], cwd);
5192
+ const [target, paneId] = output?.trim().split(" ") ?? [];
5193
+ if (target)
5194
+ origin.tmux = target;
5195
+ if (paneId)
5196
+ origin.pane = paneId;
5197
+ }
5198
+ return origin;
5199
+ }
5200
+
5201
+ // ../envoy-client/src/errors.ts
5202
+ function messageFor(error) {
5203
+ return error instanceof Error ? error.message : String(error);
5204
+ }
5205
+
5206
+ // ../envoy-client/src/dispatch-client.ts
5207
+ function ghTokenGetter(cwd, exec = defaultExec) {
5208
+ return async () => {
5209
+ try {
5210
+ const { stdout } = await exec("gh", ["auth", "token"], { cwd });
5211
+ const value = stdout.trim();
5212
+ return value.length > 0 ? value : null;
5213
+ } catch {
5214
+ return null;
5215
+ }
5216
+ };
5217
+ }
5218
+
5219
+ class DispatchServiceError extends Error {
5220
+ kind;
5221
+ name = "DispatchServiceError";
5222
+ constructor(kind, message) {
5223
+ super(message);
5224
+ this.kind = kind;
5225
+ }
5226
+ }
5227
+ function parseResponseBody(body, contentType) {
5228
+ if (!contentType.includes("text/event-stream"))
5229
+ return JSON.parse(body);
5230
+ for (const line of body.split(`
5231
+ `)) {
5232
+ const payload = line.match(/^data:\s*(.+)$/)?.[1];
5233
+ if (payload !== undefined)
5234
+ return JSON.parse(payload);
5235
+ }
5236
+ return null;
5237
+ }
5238
+ function isDispatchServiceResult(value) {
5239
+ return typeof value === "object" && value !== null && "thread" in value && typeof value.thread === "number" && "url" in value && typeof value.url === "string";
5240
+ }
5241
+ async function callDispatch(options, args) {
5242
+ const token = await options.getToken();
5243
+ if (!token) {
5244
+ throw new DispatchServiceError("auth", `dispatch: gh auth token returned empty in ${args.origin.cwd} \u2014 check your gh-app setup`);
5245
+ }
5246
+ const fetchImpl = options.fetchImpl ?? fetch;
5247
+ let response;
5248
+ try {
5249
+ response = await fetchImpl(options.serviceUrl, {
5250
+ method: "POST",
5251
+ headers: {
5252
+ Authorization: `Bearer ${token}`,
5253
+ "Content-Type": "application/json",
5254
+ Accept: "application/json, text/event-stream"
5255
+ },
5256
+ body: JSON.stringify({
5257
+ jsonrpc: "2.0",
5258
+ id: 1,
5259
+ method: "tools/call",
5260
+ params: { name: DISPATCH_TOOL_NAME, arguments: args }
5261
+ })
5262
+ });
5263
+ } catch (error) {
5264
+ throw new DispatchServiceError("transport", `dispatch service unreachable at ${options.serviceUrl}: ${messageFor(error)}`);
5265
+ }
5266
+ const body = await response.text();
5267
+ if (response.status === 401) {
5268
+ throw new DispatchServiceError("auth", `dispatch service rejected the GitHub token (401): ${body.slice(0, 200)}`);
5269
+ }
5270
+ if (!response.ok) {
5271
+ throw new DispatchServiceError("transport", `dispatch service returned ${response.status} ${response.statusText}: ${body.slice(0, 200)}`);
5272
+ }
5273
+ let parsed;
5274
+ try {
5275
+ parsed = parseResponseBody(body, response.headers.get("content-type") ?? "");
5276
+ } catch (error) {
5277
+ throw new DispatchServiceError("transport", `dispatch service sent an unreadable response: ${messageFor(error)}`);
5278
+ }
5279
+ const rpc = parsed ?? {};
5280
+ if (rpc.error) {
5281
+ throw new DispatchServiceError("tool", rpc.error.message ?? "dispatch service returned an error");
5282
+ }
5283
+ const text = (rpc.result?.content ?? []).map((item) => item.text ?? "").filter(Boolean).join(`
5284
+ `);
5285
+ if (rpc.result?.isError)
5286
+ throw new DispatchServiceError("tool", text || "dispatch failed");
5287
+ let result;
5288
+ try {
5289
+ result = JSON.parse(text);
5290
+ } catch {
5291
+ throw new DispatchServiceError("transport", `dispatch service returned a non-JSON result: ${text.slice(0, 200)}`);
5292
+ }
5293
+ if (!isDispatchServiceResult(result)) {
5294
+ throw new DispatchServiceError("transport", `dispatch service result lacks thread/url: ${text.slice(0, 200)}`);
5295
+ }
5296
+ return result;
5297
+ }
5298
+
5299
+ // ../envoy-client/src/dispatch-call.ts
5300
+ var QUALIFIED_REF = /^[^/\s#]+\/[^/\s#]+#\d+/;
5301
+ async function prepareDispatchCall(input) {
5302
+ const { call, cwd } = input;
5303
+ const continuing = isContinueCall(call);
5304
+ const needsRepo = continuing ? !QUALIFIED_REF.test(call.thread) : call.repo === undefined && !QUALIFIED_REF.test(call.parent ?? "");
5305
+ let repo;
5306
+ if (needsRepo) {
5307
+ const resolved = await resolveCwdRepo(cwd, input.exec);
5308
+ if (resolved === null) {
5309
+ throw new DispatchArgumentError(continuing ? `dispatch: ${cwd} has no GitHub remote; pass thread=owner/name#<n>` : `dispatch: ${cwd} has no GitHub remote; pass repo=owner/name`);
5310
+ }
5311
+ repo = resolved;
5312
+ }
5313
+ const resolvedOrigin = await resolveOrigin(input.env, input.exec, cwd);
5314
+ const origin = {
5315
+ ...resolvedOrigin,
5316
+ host: input.host,
5317
+ ...input.sessionId ? { sessionId: input.sessionId } : {},
5318
+ ...input.sessionTitle ? { sessionTitle: input.sessionTitle } : {}
5319
+ };
5320
+ return { ...call, ...repo === undefined ? {} : { repo }, origin };
5321
+ }
5322
+ async function executeDispatch(input) {
5323
+ const exec = input.exec ?? defaultExec;
5324
+ const prepared = await prepareDispatchCall({
5325
+ call: input.call,
5326
+ cwd: input.cwd,
5327
+ host: input.host,
5328
+ ...input.sessionId === undefined ? {} : { sessionId: input.sessionId },
5329
+ ...input.sessionTitle === undefined ? {} : { sessionTitle: input.sessionTitle },
5330
+ env: input.env ?? process.env,
5331
+ exec
5332
+ });
5333
+ return callDispatch({
5334
+ serviceUrl: input.serviceUrl,
5335
+ getToken: input.getToken ?? ghTokenGetter(input.cwd, exec),
5336
+ ...input.fetchImpl ? { fetchImpl: input.fetchImpl } : {}
5337
+ }, prepared);
5338
+ }
5339
+
5340
+ // ../envoy-client/src/dispatch-config.ts
5341
+ import { readFileSync } from "fs";
5342
+ import { homedir } from "os";
5343
+ import * as path from "path";
5344
+ var DEFAULT_SERVER_URL = "http://localhost:8766";
5345
+ var EnvoyFileSchema = looseObject({
5346
+ $schema: string2().optional(),
5347
+ natsUrls: array(string2()).optional(),
5348
+ dispatch: strictObject({
5349
+ enabled: boolean2().optional(),
5350
+ serverUrl: url().optional()
5351
+ }).optional()
5352
+ });
5353
+ function describeSchemaIssue(filePath, error) {
5354
+ const issue = error.issues[0];
5355
+ if (!issue)
5356
+ return `${filePath}: invalid dispatch config`;
5357
+ if (issue.code === "unrecognized_keys") {
5358
+ const keys = issue.keys.map((key) => `dispatch.${key}`).join(", ");
5359
+ return `${filePath}: unrecognized dispatch key(s): ${keys}`;
5360
+ }
5361
+ return `${filePath}: ${issue.path.join(".")}: ${issue.message}`;
5362
+ }
5363
+ function readEnvoyFile(filePath) {
5364
+ let raw;
5365
+ try {
5366
+ raw = readFileSync(filePath, "utf-8");
5367
+ } catch {
5368
+ return { kind: "absent" };
5369
+ }
5370
+ let parsedJson;
5371
+ try {
5372
+ parsedJson = JSON.parse(raw);
5373
+ } catch (err) {
5374
+ return { kind: "invalid", reason: `${filePath}: invalid JSON (${messageFor(err)})` };
5375
+ }
5376
+ const parsed = EnvoyFileSchema.safeParse(parsedJson);
5377
+ if (!parsed.success) {
5378
+ return { kind: "invalid", reason: describeSchemaIssue(filePath, parsed.error) };
5379
+ }
5380
+ return { kind: "valid", settings: parsed.data.dispatch ?? null };
5381
+ }
5382
+ function resolveDispatchConfig(env, options = {}) {
5383
+ const explicit = env.DISPATCH_MCP_URL;
5384
+ if (explicit)
5385
+ return { url: explicit, error: null };
5386
+ const home = options.home ?? env.HOME ?? homedir();
5387
+ const cwd = options.cwd ?? process.cwd();
5388
+ const userFile = readEnvoyFile(path.join(home, ".config", "opencode", "envoy.json"));
5389
+ const repoFile = readEnvoyFile(path.join(cwd, ".opencode", "envoy.json"));
5390
+ for (const file of [userFile, repoFile]) {
5391
+ if (file.kind === "invalid")
5392
+ return { url: null, error: file.reason };
5393
+ }
5394
+ const merged = {
5395
+ ...userFile.kind === "valid" ? userFile.settings : null,
5396
+ ...repoFile.kind === "valid" ? repoFile.settings : null
5397
+ };
5398
+ if (merged.enabled !== true)
5399
+ return { url: null, error: null };
5400
+ const baseUrl = (merged.serverUrl ?? DEFAULT_SERVER_URL).replace(/\/+$/, "");
5401
+ return { url: `${baseUrl}/mcp`, error: null };
5402
+ }
5403
+
4672
5404
  // ../envoy-client/src/dispatch-subscribe.ts
4673
5405
  var ISSUE_URL_RE = /https?:\/\/github\.com\/([^/\s"]+)\/([^/\s"]+)\/issues\/(\d+)/i;
4674
- var DISPATCH_TOOL_RE = /(^|[-._])dispatch$/i;
4675
5406
  function isDispatchTool(tool) {
4676
- return DISPATCH_TOOL_RE.test(tool);
5407
+ return tool === DISPATCH_TOOL_NAME;
4677
5408
  }
4678
5409
  function dispatchThreadTopic(owner, repo, thread) {
4679
5410
  return `notifications.github.${owner}.${repo}.issue.${thread}.>`;
@@ -4692,12 +5423,6 @@ function dispatchSubscriptionTopic(tool, output) {
4692
5423
  return dispatchThreadTopic(owner, repo, thread);
4693
5424
  }
4694
5425
 
4695
- // ../envoy-client/src/machine.ts
4696
- import { hostname } from "os";
4697
- function machineID() {
4698
- return process.env["ENVOY_MACHINE_ID"] || hostname();
4699
- }
4700
-
4701
5426
  // ../envoy-client/src/tool-contract.ts
4702
5427
  var EnvoyToolOperation = {
4703
5428
  subscribe: "subscribe",
@@ -4840,18 +5565,23 @@ function createEnvoyClient(config) {
4840
5565
  },
4841
5566
  getInterest: async (sessionID) => InterestWireSchema.parse(JSON.parse(await request(`/v1/interests/${sessionID}`, {}))),
4842
5567
  send: async (input) => toEnvelope(await post("/v1/messages/send", {
4843
- source_session: input.sourceSessionID,
5568
+ source: input.source ?? "agent",
5569
+ ...input.sourceSessionID === undefined ? {} : { source_session: input.sourceSessionID },
4844
5570
  target_session: input.targetSessionID,
4845
5571
  message: input.message,
4846
5572
  ...input.idempotencyKey === undefined ? {} : { idempotency_key: input.idempotencyKey }
4847
5573
  })),
4848
5574
  publish: async (input) => toEnvelope(await post("/v1/messages/publish", {
4849
- source_session: input.sourceSessionID,
5575
+ source: input.source ?? "agent",
5576
+ ...input.sourceSessionID === undefined ? {} : { source_session: input.sourceSessionID },
4850
5577
  topic: input.topic,
4851
5578
  message: input.message,
4852
5579
  ...input.payload === undefined ? {} : { payload: input.payload },
4853
5580
  ...input.idempotencyKey === undefined ? {} : { idempotency_key: input.idempotencyKey }
4854
5581
  })),
5582
+ unregisterSession: async (sessionID) => {
5583
+ await request(`/v1/sessions/${encodeURIComponent(sessionID)}`, { method: "DELETE" });
5584
+ },
4855
5585
  setRole: async (input) => InterestWireSchema.parse(JSON.parse(await post("/v1/roles/set", { session_id: input.sessionID, role: input.role }))),
4856
5586
  listSessions: async () => SessionWireSchema.array().parse(JSON.parse(await request("/v1/sessions", {})))
4857
5587
  };
@@ -4861,135 +5591,17 @@ function toEnvelope(value) {
4861
5591
  }
4862
5592
 
4863
5593
  // src/server.ts
4864
- import { tool as tool2 } from "@opencode-ai/plugin/tool";
4865
-
4866
- // src/config/index.ts
4867
- import { existsSync, readFileSync } from "fs";
4868
- import os from "os";
4869
- import path from "path";
4870
-
4871
- // ../envoy-client/src/errors.ts
4872
- function messageFor(error) {
4873
- return error instanceof Error ? error.message : String(error);
4874
- }
4875
-
4876
- // src/config/schema.ts
4877
- import { tool } from "@opencode-ai/plugin";
4878
- var z = tool.schema;
4879
- var DispatchConfigSchema = z.object({
4880
- enabled: z.boolean().optional(),
4881
- serverUrl: z.string().url().optional()
4882
- }).strict();
4883
- var EnvoyConfigSchema = z.object({
4884
- $schema: z.string().optional(),
4885
- natsUrls: z.array(z.string()).optional(),
4886
- dispatch: DispatchConfigSchema.optional()
4887
- }).passthrough();
4888
-
4889
- // src/config/index.ts
4890
- class EnvoyConfigError extends Error {
4891
- filePath;
4892
- constructor(filePath, detail) {
4893
- super(`[envoy-plugin] invalid config at ${filePath}: ${detail}`);
4894
- this.name = "EnvoyConfigError";
4895
- this.filePath = filePath;
4896
- }
4897
- }
4898
- function readConfigFile(filePath) {
4899
- if (!existsSync(filePath))
4900
- return null;
4901
- let raw;
4902
- try {
4903
- raw = JSON.parse(readFileSync(filePath, "utf-8"));
4904
- } catch (error) {
4905
- throw new EnvoyConfigError(filePath, messageFor(error));
4906
- }
4907
- const parsed = EnvoyConfigSchema.safeParse(raw);
4908
- if (!parsed.success) {
4909
- const issues = parsed.error.issues.map((issue) => `${issue.path.join(".")}: ${issue.message}`).join(", ");
4910
- throw new EnvoyConfigError(filePath, issues);
4911
- }
4912
- return parsed.data;
4913
- }
4914
- function mergeConfig(base, override) {
4915
- return {
4916
- ...base,
4917
- ...override,
4918
- dispatch: base.dispatch || override.dispatch ? {
4919
- ...base.dispatch,
4920
- ...override.dispatch
4921
- } : undefined
4922
- };
4923
- }
4924
- async function loadEnvoyConfig(directory, options = {}) {
4925
- const homeDir = options.homeDir ?? os.homedir();
4926
- const userConfigPath = path.join(homeDir, ".config", "opencode", "envoy.json");
4927
- const repoConfigPath = path.join(directory, ".opencode", "envoy.json");
4928
- let merged = {};
4929
- const userConfig = readConfigFile(userConfigPath);
4930
- if (userConfig)
4931
- merged = mergeConfig(merged, userConfig);
4932
- const repoConfig = readConfigFile(repoConfigPath);
4933
- if (repoConfig)
4934
- merged = mergeConfig(merged, repoConfig);
4935
- return merged;
4936
- }
4937
-
4938
- // src/dispatch-mcp.ts
4939
- import { existsSync as existsSync2 } from "fs";
4940
- import path2 from "path";
4941
- var DEFAULT_SERVER_URL = "http://localhost:8766";
4942
- function defaultShimPath() {
4943
- const packageRoot = path2.join(import.meta.dir, "..");
4944
- const candidates = [
4945
- path2.join(packageRoot, "bin", "dispatch-mcp-shim.js"),
4946
- path2.join(packageRoot, "bin", "dispatch-mcp-shim.ts")
4947
- ];
4948
- const found = candidates.find((candidate) => existsSync2(candidate));
4949
- if (!found) {
4950
- throw new Error(`dispatch MCP shim not found; tried: ${candidates.join(", ")}`);
4951
- }
4952
- return found;
4953
- }
4954
- function buildDispatchMcpEntry(opts) {
4955
- if (!opts.dispatch?.enabled)
4956
- return null;
4957
- const baseUrl = (opts.dispatch.serverUrl ?? DEFAULT_SERVER_URL).replace(/\/+$/, "");
4958
- const shimPath = opts.shimPath ?? defaultShimPath();
4959
- const runtime = opts.runtime ?? "bun";
4960
- return {
4961
- type: "local",
4962
- command: [runtime, shimPath],
4963
- environment: {
4964
- DISPATCH_MCP_URL: `${baseUrl}/mcp`
4965
- },
4966
- enabled: true
4967
- };
4968
- }
4969
- function injectEnvoyMcp(cfg, entry) {
4970
- cfg.mcp = cfg.mcp ?? {};
4971
- const existing = cfg.mcp.envoy;
4972
- if (existing !== undefined) {
4973
- if (JSON.stringify(existing) === JSON.stringify(entry)) {
4974
- return {};
4975
- }
4976
- return {
4977
- warning: "[envoy-plugin] envoy MCP entry already present in config; not overriding"
4978
- };
4979
- }
4980
- cfg.mcp.envoy = entry;
4981
- return {};
4982
- }
5594
+ import { tool } from "@opencode-ai/plugin/tool";
4983
5595
 
4984
5596
  // src/log.ts
4985
5597
  import { appendFile, mkdir } from "fs/promises";
4986
- import os2 from "os";
4987
- import path3 from "path";
5598
+ import os from "os";
5599
+ import path2 from "path";
4988
5600
  function defaultLogDir() {
4989
- const home = os2.homedir();
5601
+ const home = os.homedir();
4990
5602
  if (!home)
4991
- return path3.join(os2.tmpdir(), "opencode-log");
4992
- return path3.join(home, ".local", "share", "opencode", "log");
5603
+ return path2.join(os.tmpdir(), "opencode-log");
5604
+ return path2.join(home, ".local", "share", "opencode", "log");
4993
5605
  }
4994
5606
  function format(level, message) {
4995
5607
  return `${new Date().toISOString()} ${level} ${message}
@@ -4997,7 +5609,7 @@ function format(level, message) {
4997
5609
  }
4998
5610
  function createLogger(options = {}) {
4999
5611
  const dir = options.logDir ?? defaultLogDir();
5000
- const file = path3.join(dir, options.fileName ?? "envoy-plugin.log");
5612
+ const file = path2.join(dir, options.fileName ?? "envoy-plugin.log");
5001
5613
  let chain = Promise.resolve();
5002
5614
  const append = (line) => {
5003
5615
  chain = chain.then(() => mkdir(dir, { recursive: true })).then(() => appendFile(file, line)).catch(() => {});
@@ -5020,7 +5632,7 @@ function createLogger(options = {}) {
5020
5632
  var logger = createLogger();
5021
5633
 
5022
5634
  // src/port.ts
5023
- import { execFile } from "child_process";
5635
+ import { execFile as execFile2 } from "child_process";
5024
5636
 
5025
5637
  // src/ss.ts
5026
5638
  function portFromSsOutput(output, pid) {
@@ -5042,15 +5654,15 @@ function portFromSsOutput(output, pid) {
5042
5654
  }
5043
5655
 
5044
5656
  // src/port.ts
5045
- var defaultExec = (command, args, options) => new Promise((resolve, reject) => {
5046
- execFile(command, args, { encoding: options.encoding }, (error, stdout) => {
5657
+ var defaultExec2 = (command, args, options) => new Promise((resolve, reject) => {
5658
+ execFile2(command, args, { encoding: options.encoding }, (error, stdout) => {
5047
5659
  if (error)
5048
5660
  reject(error);
5049
5661
  else
5050
5662
  resolve(stdout);
5051
5663
  });
5052
5664
  });
5053
- async function resolvePort(serverUrl, exec = defaultExec) {
5665
+ async function resolvePort(serverUrl, exec = defaultExec2) {
5054
5666
  const urlPort = Number.parseInt(serverUrl.port, 10);
5055
5667
  if (Number.isFinite(urlPort) && urlPort > 0)
5056
5668
  return urlPort;
@@ -5064,11 +5676,11 @@ async function resolvePort(serverUrl, exec = defaultExec) {
5064
5676
  }
5065
5677
 
5066
5678
  // src/server.ts
5067
- var moduleDirectory = path4.dirname(fileURLToPath(import.meta.url));
5679
+ var moduleDirectory = path3.dirname(fileURLToPath(import.meta.url));
5068
5680
  var skillsDirectory = [
5069
- path4.resolve(moduleDirectory, "../../skills"),
5070
- path4.resolve(moduleDirectory, "../../../skills")
5071
- ].find((dir) => existsSync3(dir));
5681
+ path3.resolve(moduleDirectory, "../../skills"),
5682
+ path3.resolve(moduleDirectory, "../../../skills")
5683
+ ].find((dir) => existsSync(dir));
5072
5684
  var [
5073
5685
  subscribeSpec,
5074
5686
  unsubscribeSpec,
@@ -5079,9 +5691,33 @@ var [
5079
5691
  whoamiSpec,
5080
5692
  sessionsSpec
5081
5693
  ] = envoyToolSpecs;
5694
+ var dispatchQuestionOption = tool.schema.strictObject({
5695
+ label: tool.schema.string().min(1),
5696
+ description: tool.schema.string().optional()
5697
+ });
5698
+ var dispatchQuestion = tool.schema.strictObject({
5699
+ question: tool.schema.string().min(1),
5700
+ header: tool.schema.string().optional(),
5701
+ options: tool.schema.array(dispatchQuestionOption).optional(),
5702
+ multiple: tool.schema.boolean().optional(),
5703
+ custom: tool.schema.boolean().optional()
5704
+ });
5705
+ var dispatchArgs = {
5706
+ subject: tool.schema.string().describe(DISPATCH_ARGUMENTS.subject).optional(),
5707
+ thread: tool.schema.string().describe(DISPATCH_ARGUMENTS.thread).optional(),
5708
+ context: tool.schema.string().describe(DISPATCH_ARGUMENTS.context),
5709
+ question: tool.schema.string().describe(DISPATCH_ARGUMENTS.question),
5710
+ ask: tool.schema.array(dispatchQuestion).describe(DISPATCH_ARGUMENTS.ask).optional(),
5711
+ urgency: tool.schema.enum(DISPATCH_URGENCIES).describe(DISPATCH_ARGUMENTS.urgency).optional(),
5712
+ repo: tool.schema.string().describe(DISPATCH_ARGUMENTS.repo).optional(),
5713
+ parent: tool.schema.string().describe(DISPATCH_ARGUMENTS.parent).optional()
5714
+ };
5082
5715
  var server_default = async (input) => {
5083
5716
  const cwd = process.cwd();
5084
- const config = await loadEnvoyConfig(cwd);
5717
+ const dispatchConfig = resolveDispatchConfig(process.env, { cwd });
5718
+ if (dispatchConfig.error !== null)
5719
+ throw new Error(`[envoy-plugin] ${dispatchConfig.error}`);
5720
+ const dispatchServiceUrl = dispatchConfig.url;
5085
5721
  const envoyDefaults = envoyDefaultsFromEnvironment(process.env);
5086
5722
  const envoy = createEnvoyClient({ baseUrl: envoyDefaults.envoyUrl, fetch: globalThis.fetch });
5087
5723
  let activeSessionID = null;
@@ -5152,6 +5788,25 @@ var server_default = async (input) => {
5152
5788
  clearInterval(timer);
5153
5789
  clearInterval(heartbeatInterval);
5154
5790
  });
5791
+ const dispatchTool = dispatchServiceUrl === null ? {} : {
5792
+ [DISPATCH_TOOL_NAME]: tool({
5793
+ description: DISPATCH_TOOL_DESCRIPTION,
5794
+ args: dispatchArgs,
5795
+ async execute(args, ctx) {
5796
+ ctx.metadata({ title: "Dispatch" });
5797
+ const call = parseDispatchCall(args);
5798
+ const result = await executeDispatch({
5799
+ call,
5800
+ cwd: ctx.directory,
5801
+ host: "opencode",
5802
+ sessionId: ctx.sessionID,
5803
+ sessionTitle: await fetchTitle(ctx.sessionID) ?? undefined,
5804
+ serviceUrl: dispatchServiceUrl
5805
+ });
5806
+ return JSON.stringify(result);
5807
+ }
5808
+ })
5809
+ };
5155
5810
  return {
5156
5811
  config: (cfg) => {
5157
5812
  if (skillsDirectory) {
@@ -5161,14 +5816,6 @@ var server_default = async (input) => {
5161
5816
  cfg.skills.paths.push(skillsDirectory);
5162
5817
  }
5163
5818
  }
5164
- const entry = buildDispatchMcpEntry({
5165
- dispatch: config.dispatch
5166
- });
5167
- if (!entry)
5168
- return;
5169
- const { warning } = injectEnvoyMcp(cfg, entry);
5170
- if (warning)
5171
- logger.warn(warning);
5172
5819
  },
5173
5820
  event: async ({
5174
5821
  event
@@ -5242,9 +5889,10 @@ var server_default = async (input) => {
5242
5889
  clearInterval(heartbeatInterval);
5243
5890
  },
5244
5891
  tool: {
5245
- envoy_subscribe: tool2({
5892
+ ...dispatchTool,
5893
+ envoy_subscribe: tool({
5246
5894
  description: subscribeSpec.description,
5247
- args: { topics: tool2.schema.array(tool2.schema.string()) },
5895
+ args: { topics: tool.schema.array(tool.schema.string()) },
5248
5896
  async execute(args, ctx) {
5249
5897
  ctx.metadata({ title: "Envoy subscribe" });
5250
5898
  return JSON.stringify(await envoy.subscribe({
@@ -5257,16 +5905,16 @@ var server_default = async (input) => {
5257
5905
  }));
5258
5906
  }
5259
5907
  }),
5260
- envoy_unsubscribe: tool2({
5908
+ envoy_unsubscribe: tool({
5261
5909
  description: unsubscribeSpec.description,
5262
- args: { topics: tool2.schema.array(tool2.schema.string()).optional() },
5910
+ args: { topics: tool.schema.array(tool.schema.string()).optional() },
5263
5911
  async execute(args, ctx) {
5264
5912
  ctx.metadata({ title: "Envoy unsubscribe" });
5265
5913
  await envoy.unsubscribe({ sessionID: ctx.sessionID, topics: args.topics ?? [] });
5266
5914
  return "ok";
5267
5915
  }
5268
5916
  }),
5269
- envoy_list: tool2({
5917
+ envoy_list: tool({
5270
5918
  description: listSpec.description,
5271
5919
  args: {},
5272
5920
  async execute(_args, ctx) {
@@ -5274,9 +5922,9 @@ var server_default = async (input) => {
5274
5922
  return JSON.stringify(await envoy.getInterest(ctx.sessionID));
5275
5923
  }
5276
5924
  }),
5277
- envoy_send: tool2({
5925
+ envoy_send: tool({
5278
5926
  description: sendSpec.description,
5279
- args: { session_id: tool2.schema.string(), message: tool2.schema.string() },
5927
+ args: { session_id: tool.schema.string(), message: tool.schema.string() },
5280
5928
  async execute(args, ctx) {
5281
5929
  ctx.metadata({ title: "Envoy send" });
5282
5930
  return JSON.stringify(await envoy.send({
@@ -5286,9 +5934,9 @@ var server_default = async (input) => {
5286
5934
  }));
5287
5935
  }
5288
5936
  }),
5289
- envoy_publish: tool2({
5937
+ envoy_publish: tool({
5290
5938
  description: publishSpec.description,
5291
- args: { topic: tool2.schema.string(), message: tool2.schema.string() },
5939
+ args: { topic: tool.schema.string(), message: tool.schema.string() },
5292
5940
  async execute(args, ctx) {
5293
5941
  ctx.metadata({ title: "Envoy publish" });
5294
5942
  return JSON.stringify(await envoy.publish({
@@ -5298,15 +5946,15 @@ var server_default = async (input) => {
5298
5946
  }));
5299
5947
  }
5300
5948
  }),
5301
- envoy_role_set: tool2({
5949
+ envoy_role_set: tool({
5302
5950
  description: roleSetSpec.description,
5303
- args: { role: tool2.schema.string() },
5951
+ args: { role: tool.schema.string() },
5304
5952
  async execute(args, ctx) {
5305
5953
  ctx.metadata({ title: "Set Envoy role" });
5306
5954
  return JSON.stringify(await envoy.setRole({ sessionID: ctx.sessionID, role: args.role }));
5307
5955
  }
5308
5956
  }),
5309
- envoy_whoami: tool2({
5957
+ envoy_whoami: tool({
5310
5958
  description: whoamiSpec.description,
5311
5959
  args: {},
5312
5960
  async execute(_args, ctx) {
@@ -5321,9 +5969,9 @@ var server_default = async (input) => {
5321
5969
  }, null, 2);
5322
5970
  }
5323
5971
  }),
5324
- envoy_sessions: tool2({
5972
+ envoy_sessions: tool({
5325
5973
  description: sessionsSpec.description,
5326
- args: { machine: tool2.schema.string().optional() },
5974
+ args: { machine: tool.schema.string().optional() },
5327
5975
  async execute(args, ctx) {
5328
5976
  ctx.metadata({ title: "Envoy sessions" });
5329
5977
  const sessions = await envoy.listSessions();