@sjawhar/opencode-legion-envoy 0.16.0 → 0.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -9
- package/dist/src/server.js +869 -331
- package/package.json +1 -1
- package/skills/dispatch/SKILL.md +189 -220
- package/skills/legion-architect/SKILL.md +61 -38
- package/skills/legion-retro/SKILL.md +1 -1
- package/skills/legion-worker/SKILL.md +9 -8
- package/src/server.ts +74 -93
package/dist/src/server.js
CHANGED
|
@@ -19,6 +19,122 @@ import { existsSync } from "fs";
|
|
|
19
19
|
import path3 from "path";
|
|
20
20
|
import { fileURLToPath } from "url";
|
|
21
21
|
|
|
22
|
+
// ../contracts/src/dispatch-tools.ts
|
|
23
|
+
var ISSUE_REFERENCE = "An issue is a native KEY or external owner/repo#n reference; an external reference creates its native issue on first use when its repository is mapped in DISPATCH_REPO_PROJECTS.";
|
|
24
|
+
var ASK_URGENCIES = ["low", "med", "high", "blocking"];
|
|
25
|
+
var DOC_EDIT_OPS = ["replace", "delete", "insert"];
|
|
26
|
+
var dispatchToolSpecs = [
|
|
27
|
+
{
|
|
28
|
+
name: "dispatch_issue",
|
|
29
|
+
description: "Create a native Dispatch issue for newly tracked work. Do not use it when an existing issue " + `already covers the work; read or update that issue instead. ${ISSUE_REFERENCE}`,
|
|
30
|
+
arguments: (z) => ({
|
|
31
|
+
project: z.string().describe("Project key for the new issue."),
|
|
32
|
+
title: z.string().describe("Concise issue title."),
|
|
33
|
+
parent: z.string().describe("Optional parent issue.").optional(),
|
|
34
|
+
external: z.string().describe("Optional external issue reference.").optional(),
|
|
35
|
+
spec: z.string().describe("Optional initial primary-document markdown.").optional()
|
|
36
|
+
})
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
name: "dispatch_ask",
|
|
40
|
+
description: "Open a durable, answerable decision on an issue. Do not use it for a status update or discussion; " + `use dispatch_message instead. Question is at most 800 characters and has at most 8 options. ${ISSUE_REFERENCE}`,
|
|
41
|
+
arguments: (z) => ({
|
|
42
|
+
issue: z.string().describe(ISSUE_REFERENCE),
|
|
43
|
+
question: z.string({ max: 800 }).describe("Decision question, at most 800 characters."),
|
|
44
|
+
options: z.array(z.object({
|
|
45
|
+
label: z.string().describe("Selectable option label."),
|
|
46
|
+
description: z.string().describe("Optional option context.").optional()
|
|
47
|
+
}), { max: 8 }).describe("Optional choices, at most 8.").optional(),
|
|
48
|
+
multiple: z.boolean().describe("Whether multiple choices may be selected.").optional(),
|
|
49
|
+
custom: z.boolean().describe("Whether a free-text answer is allowed.").optional(),
|
|
50
|
+
urgency: z.enum(ASK_URGENCIES).describe("Optional decision urgency.").optional(),
|
|
51
|
+
anchor: z.object({
|
|
52
|
+
artifact: z.string().describe("Artifact slug or id containing the quoted text."),
|
|
53
|
+
quote: z.string().describe("Exact text the decision concerns."),
|
|
54
|
+
occurrence: z.number({ int: true, min: 0 }).describe("Zero-based occurrence of the quote.").optional()
|
|
55
|
+
}).describe("Optional document location for the question.").optional()
|
|
56
|
+
})
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
name: "dispatch_comment",
|
|
60
|
+
description: "Add review feedback to an issue or document quote. Do not use it for an exact replacement; use " + `dispatch_suggest instead. Body is at most 2,000 characters. ${ISSUE_REFERENCE}`,
|
|
61
|
+
arguments: (z) => ({
|
|
62
|
+
issue: z.string().describe(ISSUE_REFERENCE),
|
|
63
|
+
artifact: z.string().describe("Artifact slug or id required when quote is given.").optional(),
|
|
64
|
+
quote: z.string().describe("Optional exact quoted document text.").optional(),
|
|
65
|
+
occurrence: z.number({ int: true, min: 0 }).describe("Optional zero-based occurrence of quote.").optional(),
|
|
66
|
+
body: z.string({ max: 2000 }).describe("Review comment, at most 2,000 characters."),
|
|
67
|
+
reply_to: z.string().describe("Optional comment id to reply to.").optional()
|
|
68
|
+
})
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
name: "dispatch_suggest",
|
|
72
|
+
description: "Propose an exact replacement for quoted document text. Do not use it for general feedback; use " + `dispatch_comment instead. Optional explanation is at most 2,000 characters. ${ISSUE_REFERENCE}`,
|
|
73
|
+
arguments: (z) => ({
|
|
74
|
+
issue: z.string().describe(ISSUE_REFERENCE),
|
|
75
|
+
artifact: z.string().describe("Artifact slug or id containing the quoted text."),
|
|
76
|
+
quote: z.string().describe("Exact document text to replace."),
|
|
77
|
+
replace_with: z.string().describe("Replacement text."),
|
|
78
|
+
body: z.string({ max: 2000 }).describe("Optional rationale, at most 2,000 characters.").optional(),
|
|
79
|
+
occurrence: z.number({ int: true, min: 0 }).describe("Optional zero-based occurrence of quote.").optional()
|
|
80
|
+
})
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
name: "dispatch_message",
|
|
84
|
+
description: "Post a plain issue update. Do not use it for a decision or line-specific review; use dispatch_ask " + `or dispatch_comment instead. Body is at most 2,000 characters. ${ISSUE_REFERENCE}`,
|
|
85
|
+
arguments: (z) => ({
|
|
86
|
+
issue: z.string().describe(ISSUE_REFERENCE),
|
|
87
|
+
body: z.string({ max: 2000 }).describe("Update text, at most 2,000 characters.")
|
|
88
|
+
})
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
name: "dispatch_doc_edit",
|
|
92
|
+
description: "Apply deterministic text edits to a document. Do not use it for review feedback or for reading; use " + `dispatch_comment, dispatch_suggest, or dispatch_doc_read instead. ${ISSUE_REFERENCE}`,
|
|
93
|
+
arguments: (z) => ({
|
|
94
|
+
issue: z.string().describe(ISSUE_REFERENCE),
|
|
95
|
+
artifact: z.string().describe("Artifact slug or id for the document."),
|
|
96
|
+
ops: z.array(z.object({
|
|
97
|
+
op: z.enum(DOC_EDIT_OPS).describe("Edit operation."),
|
|
98
|
+
find: z.string().describe("Text to find for replace or delete.").optional(),
|
|
99
|
+
with: z.string().describe("Replacement text for replace.").optional(),
|
|
100
|
+
occurrence: z.number({ int: true, min: 0 }).describe("Optional zero-based match occurrence.").optional(),
|
|
101
|
+
markdown: z.string().describe("Markdown to insert.").optional(),
|
|
102
|
+
after: z.string().describe("Anchor after which to insert.").optional(),
|
|
103
|
+
before: z.string().describe("Anchor before which to insert.").optional()
|
|
104
|
+
})).describe("Flat tagged edits; the server validates fields required for each operation."),
|
|
105
|
+
summary: z.string().describe("Optional named-version summary.").optional()
|
|
106
|
+
})
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
name: "dispatch_doc_read",
|
|
110
|
+
description: "Read a live document or a named document version. Do not use it for issue status, asks, or events; " + "use dispatch_read instead. Supply ref or issue; issue plus an omitted artifact reads the primary document. " + `${ISSUE_REFERENCE}`,
|
|
111
|
+
arguments: (z) => ({
|
|
112
|
+
issue: z.string().describe(ISSUE_REFERENCE).optional(),
|
|
113
|
+
artifact: z.string().describe("Optional artifact slug or id; primary document by default.").optional(),
|
|
114
|
+
version: z.number({ int: true, min: 1 }).describe("Optional version number.").optional(),
|
|
115
|
+
ref: z.string().describe("Optional dispatch:// document reference.").optional()
|
|
116
|
+
})
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
name: "dispatch_artifact",
|
|
120
|
+
description: "Upload a local file as an issue artifact. Do not use it to edit a live document; use dispatch_doc_edit " + `instead. Files are limited to 25 MiB. ${ISSUE_REFERENCE}`,
|
|
121
|
+
arguments: (z) => ({
|
|
122
|
+
issue: z.string().describe(ISSUE_REFERENCE),
|
|
123
|
+
name: z.string().describe("Artifact filename shown in Dispatch."),
|
|
124
|
+
path: z.string().describe("Local path to the file to upload."),
|
|
125
|
+
primary: z.boolean().describe("Make this document the issue primary artifact.").optional(),
|
|
126
|
+
summary: z.string().describe("Optional version summary.").optional()
|
|
127
|
+
})
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
name: "dispatch_read",
|
|
131
|
+
description: "Read an issue summary, targeted ask, or targeted comment reply chain. Do not use it for document " + "contents; use dispatch_doc_read instead. Supply issue or ref. " + ISSUE_REFERENCE,
|
|
132
|
+
arguments: (z) => ({
|
|
133
|
+
issue: z.string().describe(ISSUE_REFERENCE).optional(),
|
|
134
|
+
ref: z.string().describe("Optional dispatch:// issue reference.").optional()
|
|
135
|
+
})
|
|
136
|
+
}
|
|
137
|
+
];
|
|
22
138
|
// ../../node_modules/.bun/zod@4.3.6/node_modules/zod/v4/classic/external.js
|
|
23
139
|
var exports_external = {};
|
|
24
140
|
__export(exports_external, {
|
|
@@ -13551,25 +13667,20 @@ function date4(params) {
|
|
|
13551
13667
|
|
|
13552
13668
|
// ../../node_modules/.bun/zod@4.3.6/node_modules/zod/v4/classic/external.js
|
|
13553
13669
|
config(en_default());
|
|
13554
|
-
// ../contracts/src/dispatch-question.ts
|
|
13555
|
-
var DispatchQuestionOptionSchema = strictObject({
|
|
13556
|
-
label: string2().min(1),
|
|
13557
|
-
description: string2().optional()
|
|
13558
|
-
});
|
|
13559
|
-
var DispatchQuestionSchema = strictObject({
|
|
13560
|
-
askId: string2().optional(),
|
|
13561
|
-
question: string2().min(1),
|
|
13562
|
-
header: string2().optional(),
|
|
13563
|
-
options: array(DispatchQuestionOptionSchema).optional(),
|
|
13564
|
-
multiple: boolean2().optional(),
|
|
13565
|
-
custom: boolean2().optional()
|
|
13566
|
-
});
|
|
13567
|
-
var DispatchQuestionInputSchema = DispatchQuestionSchema.omit({ askId: true });
|
|
13568
13670
|
// ../contracts/src/envelope.ts
|
|
13569
13671
|
var isSubject = (value) => typeof value === "string" && value.length > 0;
|
|
13570
13672
|
var EnvelopeSchema = object({
|
|
13571
13673
|
event_id: string2().min(1),
|
|
13572
|
-
source: _enum2([
|
|
13674
|
+
source: _enum2([
|
|
13675
|
+
"agent",
|
|
13676
|
+
"human",
|
|
13677
|
+
"envoy",
|
|
13678
|
+
"github",
|
|
13679
|
+
"slack",
|
|
13680
|
+
"whatsapp",
|
|
13681
|
+
"ghostwispr",
|
|
13682
|
+
"dispatch"
|
|
13683
|
+
]),
|
|
13573
13684
|
source_event_id: string2().min(1),
|
|
13574
13685
|
source_session: string2().optional(),
|
|
13575
13686
|
topic: custom(isSubject, { message: "topic must be a non-empty subject" }),
|
|
@@ -13901,6 +14012,41 @@ var LegionDaemonApi = {
|
|
|
13901
14012
|
response: object({ token: nonEmptyString, appLogin: string2().endsWith("[bot]") })
|
|
13902
14013
|
}
|
|
13903
14014
|
};
|
|
14015
|
+
// ../contracts/src/tool-schema.ts
|
|
14016
|
+
function zodSchemaApi(zod) {
|
|
14017
|
+
const api = zod;
|
|
14018
|
+
return {
|
|
14019
|
+
string: (opts = {}) => {
|
|
14020
|
+
let schema = api.string();
|
|
14021
|
+
if (opts.min !== undefined)
|
|
14022
|
+
schema = schema.min(opts.min);
|
|
14023
|
+
if (opts.max !== undefined)
|
|
14024
|
+
schema = schema.max(opts.max);
|
|
14025
|
+
return schema;
|
|
14026
|
+
},
|
|
14027
|
+
number: (opts = {}) => {
|
|
14028
|
+
let schema = api.number();
|
|
14029
|
+
if (opts.int)
|
|
14030
|
+
schema = schema.int();
|
|
14031
|
+
if (opts.min !== undefined)
|
|
14032
|
+
schema = schema.min(opts.min);
|
|
14033
|
+
if (opts.max !== undefined)
|
|
14034
|
+
schema = schema.max(opts.max);
|
|
14035
|
+
return schema;
|
|
14036
|
+
},
|
|
14037
|
+
boolean: () => api.boolean(),
|
|
14038
|
+
enum: (values) => api.enum(values),
|
|
14039
|
+
array: (item, opts = {}) => {
|
|
14040
|
+
let schema = api.array(item);
|
|
14041
|
+
if (opts.min !== undefined)
|
|
14042
|
+
schema = schema.min(opts.min);
|
|
14043
|
+
if (opts.max !== undefined)
|
|
14044
|
+
schema = schema.max(opts.max);
|
|
14045
|
+
return schema;
|
|
14046
|
+
},
|
|
14047
|
+
object: (shape) => api.object(shape)
|
|
14048
|
+
};
|
|
14049
|
+
}
|
|
13904
14050
|
// ../envoy-client/src/defaults.ts
|
|
13905
14051
|
var DEFAULT_ENVOY_URL = "http://127.0.0.1:9020";
|
|
13906
14052
|
var DEFAULT_HEARTBEAT_MS = 120000;
|
|
@@ -13919,81 +14065,109 @@ function normalizeEnvoyUrl(value) {
|
|
|
13919
14065
|
return value.replace(/\/+$/, "");
|
|
13920
14066
|
}
|
|
13921
14067
|
|
|
13922
|
-
// ../envoy-client/src/dispatch-
|
|
13923
|
-
|
|
13924
|
-
|
|
13925
|
-
|
|
13926
|
-
|
|
13927
|
-
|
|
13928
|
-
|
|
13929
|
-
|
|
13930
|
-
thread: 'Continue an existing thread: "<n>" (an open issue in the repo; a plain work issue is adopted as a thread) or "owner/name#<n>". When set, omit subject, urgency, repo, and parent.',
|
|
13931
|
-
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.`,
|
|
13932
|
-
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\`.`,
|
|
13933
|
-
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.",
|
|
13934
|
-
urgency: "low | med | high | blocking (default med). Opening a thread only.",
|
|
13935
|
-
repo: "owner/name. Opening a thread only; defaults to the working directory's GitHub repo.",
|
|
13936
|
-
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."
|
|
13937
|
-
};
|
|
13938
|
-
class DispatchArgumentError extends Error {
|
|
13939
|
-
name = "DispatchArgumentError";
|
|
13940
|
-
}
|
|
13941
|
-
var prose = {
|
|
13942
|
-
context: string2(),
|
|
13943
|
-
question: string2(),
|
|
13944
|
-
ask: array(DispatchQuestionInputSchema).optional()
|
|
13945
|
-
};
|
|
13946
|
-
var OpenThreadCallSchema = strictObject({
|
|
13947
|
-
subject: string2(),
|
|
13948
|
-
...prose,
|
|
13949
|
-
urgency: _enum2(DISPATCH_URGENCIES).optional(),
|
|
13950
|
-
repo: string2().optional(),
|
|
13951
|
-
parent: string2().optional()
|
|
13952
|
-
});
|
|
13953
|
-
var ContinueThreadCallSchema = strictObject({ thread: string2(), ...prose });
|
|
13954
|
-
var dispatchToolShape = {
|
|
13955
|
-
subject: string2().describe(DISPATCH_ARGUMENTS.subject).optional(),
|
|
13956
|
-
thread: string2().describe(DISPATCH_ARGUMENTS.thread).optional(),
|
|
13957
|
-
context: string2().describe(DISPATCH_ARGUMENTS.context),
|
|
13958
|
-
question: string2().describe(DISPATCH_ARGUMENTS.question),
|
|
13959
|
-
ask: array(DispatchQuestionInputSchema).describe(DISPATCH_ARGUMENTS.ask).optional(),
|
|
13960
|
-
urgency: _enum2(DISPATCH_URGENCIES).describe(DISPATCH_ARGUMENTS.urgency).optional(),
|
|
13961
|
-
repo: string2().describe(DISPATCH_ARGUMENTS.repo).optional(),
|
|
13962
|
-
parent: string2().describe(DISPATCH_ARGUMENTS.parent).optional()
|
|
13963
|
-
};
|
|
13964
|
-
var DISPATCH_TOOL_JSON_SCHEMA = toJSONSchema(object(dispatchToolShape));
|
|
13965
|
-
function isContinueCall(call) {
|
|
13966
|
-
return "thread" in call;
|
|
14068
|
+
// ../envoy-client/src/dispatch-config.ts
|
|
14069
|
+
import { readFileSync } from "fs";
|
|
14070
|
+
import { homedir } from "os";
|
|
14071
|
+
import * as path from "path";
|
|
14072
|
+
|
|
14073
|
+
// ../envoy-client/src/errors.ts
|
|
14074
|
+
function messageFor(error) {
|
|
14075
|
+
return error instanceof Error ? error.message : String(error);
|
|
13967
14076
|
}
|
|
13968
|
-
|
|
13969
|
-
|
|
14077
|
+
|
|
14078
|
+
// ../envoy-client/src/dispatch-config.ts
|
|
14079
|
+
var DEFAULT_SERVER_URL = "http://localhost:8766";
|
|
14080
|
+
function normalizeDispatchUrl(url2) {
|
|
14081
|
+
return url2.replace(/\/+$/, "");
|
|
13970
14082
|
}
|
|
13971
|
-
function
|
|
13972
|
-
return
|
|
14083
|
+
function deprecatedMcpUrl(url2) {
|
|
14084
|
+
return normalizeDispatchUrl(url2).replace(/\/mcp$/, "");
|
|
13973
14085
|
}
|
|
13974
|
-
function
|
|
13975
|
-
|
|
13976
|
-
|
|
14086
|
+
function parsedDispatchUrl(value, source) {
|
|
14087
|
+
const url2 = normalizeDispatchUrl(value);
|
|
14088
|
+
try {
|
|
14089
|
+
new URL(url2);
|
|
14090
|
+
return { url: url2, error: null };
|
|
14091
|
+
} catch {
|
|
14092
|
+
return { url: null, error: `${source} must be a valid URL` };
|
|
13977
14093
|
}
|
|
13978
|
-
|
|
13979
|
-
|
|
13980
|
-
|
|
13981
|
-
|
|
13982
|
-
|
|
14094
|
+
}
|
|
14095
|
+
var EnvoyFileSchema = looseObject({
|
|
14096
|
+
$schema: string2().optional(),
|
|
14097
|
+
natsUrls: array(string2()).optional(),
|
|
14098
|
+
dispatch: strictObject({
|
|
14099
|
+
enabled: boolean2().optional(),
|
|
14100
|
+
serverUrl: url().optional(),
|
|
14101
|
+
token: string2().optional()
|
|
14102
|
+
}).optional()
|
|
14103
|
+
});
|
|
14104
|
+
function describeSchemaIssue(filePath, error) {
|
|
14105
|
+
const issue = error.issues[0];
|
|
14106
|
+
if (!issue)
|
|
14107
|
+
return `${filePath}: invalid dispatch config`;
|
|
14108
|
+
if (issue.code === "unrecognized_keys") {
|
|
14109
|
+
const keys = issue.keys.map((key) => `dispatch.${key}`).join(", ");
|
|
14110
|
+
return `${filePath}: unrecognized dispatch key(s): ${keys}`;
|
|
13983
14111
|
}
|
|
13984
|
-
|
|
13985
|
-
|
|
14112
|
+
return `${filePath}: ${issue.path.join(".")}: ${issue.message}`;
|
|
14113
|
+
}
|
|
14114
|
+
function readEnvoyFile(filePath) {
|
|
14115
|
+
let raw;
|
|
14116
|
+
try {
|
|
14117
|
+
raw = readFileSync(filePath, "utf-8");
|
|
14118
|
+
} catch {
|
|
14119
|
+
return { kind: "absent" };
|
|
13986
14120
|
}
|
|
13987
|
-
|
|
13988
|
-
|
|
14121
|
+
let parsedJson;
|
|
14122
|
+
try {
|
|
14123
|
+
parsedJson = JSON.parse(raw);
|
|
14124
|
+
} catch (err) {
|
|
14125
|
+
return { kind: "invalid", reason: `${filePath}: invalid JSON (${messageFor(err)})` };
|
|
13989
14126
|
}
|
|
13990
|
-
const parsed =
|
|
14127
|
+
const parsed = EnvoyFileSchema.safeParse(parsedJson);
|
|
13991
14128
|
if (!parsed.success) {
|
|
13992
|
-
|
|
14129
|
+
return { kind: "invalid", reason: describeSchemaIssue(filePath, parsed.error) };
|
|
14130
|
+
}
|
|
14131
|
+
return { kind: "valid", settings: parsed.data.dispatch ?? null };
|
|
14132
|
+
}
|
|
14133
|
+
function resolveDispatchConfig(env, options = {}) {
|
|
14134
|
+
const explicitUrl = env.DISPATCH_URL;
|
|
14135
|
+
const deprecatedUrl = explicitUrl ? undefined : env.DISPATCH_MCP_URL;
|
|
14136
|
+
const home = options.home ?? env.HOME ?? homedir();
|
|
14137
|
+
const cwd = options.cwd ?? process.cwd();
|
|
14138
|
+
const userFile = readEnvoyFile(path.join(home, ".config", "opencode", "envoy.json"));
|
|
14139
|
+
const repoFile = readEnvoyFile(path.join(cwd, ".opencode", "envoy.json"));
|
|
14140
|
+
for (const file of [userFile, repoFile]) {
|
|
14141
|
+
if (file.kind === "invalid") {
|
|
14142
|
+
return { enabled: false, url: null, token: null, error: file.reason };
|
|
14143
|
+
}
|
|
14144
|
+
}
|
|
14145
|
+
const merged = {
|
|
14146
|
+
...userFile.kind === "valid" ? userFile.settings : null,
|
|
14147
|
+
...repoFile.kind === "valid" ? repoFile.settings : null
|
|
14148
|
+
};
|
|
14149
|
+
const rawUrl = explicitUrl !== undefined ? { value: explicitUrl, source: "DISPATCH_URL" } : deprecatedUrl !== undefined ? { value: deprecatedMcpUrl(deprecatedUrl), source: "DISPATCH_MCP_URL" } : merged.enabled === true ? { value: merged.serverUrl ?? DEFAULT_SERVER_URL, source: "dispatch.serverUrl" } : null;
|
|
14150
|
+
const url2 = rawUrl ? parsedDispatchUrl(rawUrl.value, rawUrl.source) : { url: null, error: null };
|
|
14151
|
+
const token = env.DISPATCH_TOKEN ?? merged.token ?? null;
|
|
14152
|
+
const tokenSource = env.DISPATCH_TOKEN === undefined ? "dispatch.token" : "DISPATCH_TOKEN";
|
|
14153
|
+
if (url2.error !== null)
|
|
14154
|
+
return { enabled: false, url: null, token, error: url2.error };
|
|
14155
|
+
if (url2.url === null)
|
|
14156
|
+
return { enabled: false, url: null, token, error: null };
|
|
14157
|
+
if (!token) {
|
|
14158
|
+
return {
|
|
14159
|
+
enabled: false,
|
|
14160
|
+
url: url2.url,
|
|
14161
|
+
token,
|
|
14162
|
+
error: `${tokenSource} must be a non-empty bearer token`
|
|
14163
|
+
};
|
|
13993
14164
|
}
|
|
13994
|
-
return
|
|
14165
|
+
return { enabled: true, url: url2.url, token, error: null };
|
|
13995
14166
|
}
|
|
13996
14167
|
|
|
14168
|
+
// ../envoy-client/src/dispatch-execute.ts
|
|
14169
|
+
import { resolve as resolvePath } from "path";
|
|
14170
|
+
|
|
13997
14171
|
// ../envoy-client/src/dispatch-cwd.ts
|
|
13998
14172
|
import { execFile } from "child_process";
|
|
13999
14173
|
import { promisify } from "util";
|
|
@@ -14076,229 +14250,592 @@ async function resolveOrigin(env, exec, cwd) {
|
|
|
14076
14250
|
return origin;
|
|
14077
14251
|
}
|
|
14078
14252
|
|
|
14079
|
-
// ../envoy-client/src/
|
|
14080
|
-
function messageFor(error) {
|
|
14081
|
-
return error instanceof Error ? error.message : String(error);
|
|
14082
|
-
}
|
|
14083
|
-
|
|
14084
|
-
// ../envoy-client/src/dispatch-client.ts
|
|
14085
|
-
function ghTokenGetter(cwd, exec = defaultExec) {
|
|
14086
|
-
return async () => {
|
|
14087
|
-
try {
|
|
14088
|
-
const { stdout } = await exec("gh", ["auth", "token"], { cwd });
|
|
14089
|
-
const value = stdout.trim();
|
|
14090
|
-
return value.length > 0 ? value : null;
|
|
14091
|
-
} catch {
|
|
14092
|
-
return null;
|
|
14093
|
-
}
|
|
14094
|
-
};
|
|
14095
|
-
}
|
|
14096
|
-
|
|
14253
|
+
// ../envoy-client/src/dispatch-http.ts
|
|
14097
14254
|
class DispatchServiceError extends Error {
|
|
14098
|
-
|
|
14255
|
+
code;
|
|
14256
|
+
status;
|
|
14257
|
+
candidates;
|
|
14099
14258
|
name = "DispatchServiceError";
|
|
14100
|
-
constructor(
|
|
14259
|
+
constructor(code, status, message, candidates) {
|
|
14101
14260
|
super(message);
|
|
14102
|
-
this.
|
|
14261
|
+
this.code = code;
|
|
14262
|
+
this.status = status;
|
|
14263
|
+
this.candidates = candidates;
|
|
14103
14264
|
}
|
|
14104
14265
|
}
|
|
14105
|
-
function
|
|
14106
|
-
|
|
14107
|
-
return JSON.parse(body);
|
|
14108
|
-
for (const line of body.split(`
|
|
14109
|
-
`)) {
|
|
14110
|
-
const payload = line.match(/^data:\s*(.+)$/)?.[1];
|
|
14111
|
-
if (payload !== undefined)
|
|
14112
|
-
return JSON.parse(payload);
|
|
14113
|
-
}
|
|
14114
|
-
return null;
|
|
14266
|
+
function asErrorShape(value) {
|
|
14267
|
+
return typeof value === "object" && value !== null ? value : {};
|
|
14115
14268
|
}
|
|
14116
|
-
function
|
|
14117
|
-
return
|
|
14269
|
+
function isJson(response) {
|
|
14270
|
+
return response.headers.get("content-type")?.includes("application/json") ?? false;
|
|
14118
14271
|
}
|
|
14119
|
-
|
|
14120
|
-
|
|
14121
|
-
|
|
14122
|
-
|
|
14123
|
-
|
|
14124
|
-
|
|
14125
|
-
|
|
14126
|
-
|
|
14127
|
-
|
|
14128
|
-
|
|
14129
|
-
|
|
14130
|
-
|
|
14131
|
-
|
|
14132
|
-
|
|
14133
|
-
|
|
14134
|
-
|
|
14135
|
-
|
|
14136
|
-
|
|
14137
|
-
|
|
14138
|
-
|
|
14139
|
-
|
|
14272
|
+
|
|
14273
|
+
class DispatchClient {
|
|
14274
|
+
token;
|
|
14275
|
+
fetchImpl;
|
|
14276
|
+
#baseUrl;
|
|
14277
|
+
#resolvedIssues = new Map;
|
|
14278
|
+
#creatingIssues = new Map;
|
|
14279
|
+
constructor(baseUrl, token, fetchImpl = fetch) {
|
|
14280
|
+
this.token = token;
|
|
14281
|
+
this.fetchImpl = fetchImpl;
|
|
14282
|
+
this.#baseUrl = baseUrl.replace(/\/+$/, "");
|
|
14283
|
+
}
|
|
14284
|
+
async issue(input) {
|
|
14285
|
+
return this.#json("POST", ["api", "v1", "issues"], input);
|
|
14286
|
+
}
|
|
14287
|
+
async getIssue(issue) {
|
|
14288
|
+
return this.#json("GET", ["api", "v1", "issues", await this.#resolveIssue(issue)]);
|
|
14289
|
+
}
|
|
14290
|
+
async getIssueEvents(issue, after = 0, limit = 200) {
|
|
14291
|
+
return this.#json("GET", ["api", "v1", "issues", await this.#resolveIssue(issue), "events"], undefined, { after, limit });
|
|
14292
|
+
}
|
|
14293
|
+
async read(issueReference) {
|
|
14294
|
+
const issueKey = await this.#resolveIssue(issueReference);
|
|
14295
|
+
const issue = await this.getIssue(issueKey);
|
|
14296
|
+
const events = await this.getIssueEvents(issueKey, Math.max(0, issue.last_seq - 10), 10);
|
|
14297
|
+
return { issue, events };
|
|
14298
|
+
}
|
|
14299
|
+
async ask(issue, input) {
|
|
14300
|
+
return this.#json("POST", ["api", "v1", "issues", await this.#resolveIssue(issue), "asks"], input);
|
|
14301
|
+
}
|
|
14302
|
+
async comment(issue, input) {
|
|
14303
|
+
return this.#json("POST", ["api", "v1", "issues", await this.#resolveIssue(issue), "comments"], input);
|
|
14304
|
+
}
|
|
14305
|
+
async suggest(issue, input) {
|
|
14306
|
+
const { replace_with, ...comment } = input;
|
|
14307
|
+
return this.#json("POST", ["api", "v1", "issues", await this.#resolveIssue(issue), "comments"], {
|
|
14308
|
+
...comment,
|
|
14309
|
+
suggestion: { replace_with }
|
|
14140
14310
|
});
|
|
14141
|
-
} catch (error) {
|
|
14142
|
-
throw new DispatchServiceError("transport", `dispatch service unreachable at ${options.serviceUrl}: ${messageFor(error)}`);
|
|
14143
14311
|
}
|
|
14144
|
-
|
|
14145
|
-
|
|
14146
|
-
throw new DispatchServiceError("auth", `dispatch service rejected the GitHub token (401): ${body.slice(0, 200)}`);
|
|
14312
|
+
async message(issue, input) {
|
|
14313
|
+
return this.#json("POST", ["api", "v1", "issues", await this.#resolveIssue(issue), "messages"], input);
|
|
14147
14314
|
}
|
|
14148
|
-
|
|
14149
|
-
|
|
14315
|
+
async artifact(issue, input) {
|
|
14316
|
+
const form = new FormData;
|
|
14317
|
+
form.set("name", input.name);
|
|
14318
|
+
if (input.primary !== undefined)
|
|
14319
|
+
form.set("primary", String(input.primary));
|
|
14320
|
+
if (input.summary !== undefined)
|
|
14321
|
+
form.set("summary", input.summary);
|
|
14322
|
+
form.set("actor", JSON.stringify(input.actor));
|
|
14323
|
+
form.set("file", input.file, input.name);
|
|
14324
|
+
return this.#form("POST", ["api", "v1", "issues", await this.#resolveIssue(issue), "artifacts"], form);
|
|
14150
14325
|
}
|
|
14151
|
-
|
|
14152
|
-
|
|
14153
|
-
parsed = parseResponseBody(body, response.headers.get("content-type") ?? "");
|
|
14154
|
-
} catch (error) {
|
|
14155
|
-
throw new DispatchServiceError("transport", `dispatch service sent an unreadable response: ${messageFor(error)}`);
|
|
14326
|
+
async getArtifact(id) {
|
|
14327
|
+
return this.#json("GET", ["api", "v1", "artifacts", id]);
|
|
14156
14328
|
}
|
|
14157
|
-
|
|
14158
|
-
|
|
14159
|
-
throw new DispatchServiceError("tool", rpc.error.message ?? "dispatch service returned an error");
|
|
14329
|
+
async docRead(id, version) {
|
|
14330
|
+
return version === undefined ? this.#json("GET", ["api", "v1", "artifacts", id, "text"]) : this.#json("GET", ["api", "v1", "artifacts", id, "versions", String(version)]);
|
|
14160
14331
|
}
|
|
14161
|
-
|
|
14162
|
-
|
|
14163
|
-
if (rpc.result?.isError)
|
|
14164
|
-
throw new DispatchServiceError("tool", text || "dispatch failed");
|
|
14165
|
-
let result;
|
|
14166
|
-
try {
|
|
14167
|
-
result = JSON.parse(text);
|
|
14168
|
-
} catch {
|
|
14169
|
-
throw new DispatchServiceError("transport", `dispatch service returned a non-JSON result: ${text.slice(0, 200)}`);
|
|
14332
|
+
async docEdit(id, input) {
|
|
14333
|
+
return this.#json("POST", ["api", "v1", "artifacts", id, "edits"], input);
|
|
14170
14334
|
}
|
|
14171
|
-
|
|
14172
|
-
|
|
14335
|
+
async nameArtifactVersion(id, input) {
|
|
14336
|
+
return this.#json("POST", ["api", "v1", "artifacts", id, "versions"], input);
|
|
14337
|
+
}
|
|
14338
|
+
async getAsk(id) {
|
|
14339
|
+
return this.#json("GET", ["api", "v1", "asks", id]);
|
|
14340
|
+
}
|
|
14341
|
+
async getComment(id) {
|
|
14342
|
+
return this.#json("GET", ["api", "v1", "comments", id]);
|
|
14343
|
+
}
|
|
14344
|
+
async getComments(issue, artifact) {
|
|
14345
|
+
return this.#json("GET", ["api", "v1", "issues", await this.#resolveIssue(issue), "comments"], undefined, artifact ? { artifact } : undefined);
|
|
14346
|
+
}
|
|
14347
|
+
async ensureIssue(issueReference, actor) {
|
|
14348
|
+
if (!issueReference.includes("#"))
|
|
14349
|
+
return issueReference;
|
|
14350
|
+
try {
|
|
14351
|
+
return await this.#resolveIssue(issueReference);
|
|
14352
|
+
} catch (error) {
|
|
14353
|
+
if (!(error instanceof DispatchServiceError) || error.status !== 404)
|
|
14354
|
+
throw error;
|
|
14355
|
+
}
|
|
14356
|
+
let creating = this.#creatingIssues.get(issueReference);
|
|
14357
|
+
if (!creating) {
|
|
14358
|
+
creating = this.#createExternalIssue(issueReference, actor);
|
|
14359
|
+
this.#creatingIssues.set(issueReference, creating);
|
|
14360
|
+
}
|
|
14361
|
+
try {
|
|
14362
|
+
return await creating;
|
|
14363
|
+
} finally {
|
|
14364
|
+
if (this.#creatingIssues.get(issueReference) === creating) {
|
|
14365
|
+
this.#creatingIssues.delete(issueReference);
|
|
14366
|
+
}
|
|
14367
|
+
}
|
|
14368
|
+
}
|
|
14369
|
+
async#createExternalIssue(issueReference, actor) {
|
|
14370
|
+
try {
|
|
14371
|
+
const created = await this.#json("POST", ["api", "v1", "issues"], {
|
|
14372
|
+
external: issueReference,
|
|
14373
|
+
actor
|
|
14374
|
+
});
|
|
14375
|
+
this.#resolvedIssues.set(issueReference, Promise.resolve(created.key));
|
|
14376
|
+
return created.key;
|
|
14377
|
+
} catch (error) {
|
|
14378
|
+
if (error instanceof DispatchServiceError && (error.status === 409 || error.status === 500)) {
|
|
14379
|
+
return this.#resolveIssue(issueReference);
|
|
14380
|
+
}
|
|
14381
|
+
throw error;
|
|
14382
|
+
}
|
|
14383
|
+
}
|
|
14384
|
+
async#resolveIssue(issueReference) {
|
|
14385
|
+
if (!issueReference.includes("#"))
|
|
14386
|
+
return issueReference;
|
|
14387
|
+
let resolved = this.#resolvedIssues.get(issueReference);
|
|
14388
|
+
if (!resolved) {
|
|
14389
|
+
resolved = this.#json("GET", ["api", "v1", "issues", "resolve"], undefined, {
|
|
14390
|
+
ref: issueReference
|
|
14391
|
+
}).then((resolution) => resolution.key);
|
|
14392
|
+
this.#resolvedIssues.set(issueReference, resolved);
|
|
14393
|
+
}
|
|
14394
|
+
try {
|
|
14395
|
+
return await resolved;
|
|
14396
|
+
} catch (error) {
|
|
14397
|
+
this.#resolvedIssues.delete(issueReference);
|
|
14398
|
+
throw error;
|
|
14399
|
+
}
|
|
14400
|
+
}
|
|
14401
|
+
async#json(method, path, body, query) {
|
|
14402
|
+
const headers = {
|
|
14403
|
+
Accept: "application/json",
|
|
14404
|
+
Authorization: `Bearer ${this.token}`
|
|
14405
|
+
};
|
|
14406
|
+
if (body !== undefined)
|
|
14407
|
+
headers["Content-Type"] = "application/json";
|
|
14408
|
+
const response = await this.fetchImpl(this.#url(path, query), {
|
|
14409
|
+
method,
|
|
14410
|
+
headers,
|
|
14411
|
+
...body === undefined ? {} : { body: JSON.stringify(body) }
|
|
14412
|
+
});
|
|
14413
|
+
return this.#response(response);
|
|
14414
|
+
}
|
|
14415
|
+
async#form(method, path, body) {
|
|
14416
|
+
const response = await this.fetchImpl(this.#url(path), {
|
|
14417
|
+
method,
|
|
14418
|
+
headers: { Accept: "application/json", Authorization: `Bearer ${this.token}` },
|
|
14419
|
+
body
|
|
14420
|
+
});
|
|
14421
|
+
return this.#response(response);
|
|
14422
|
+
}
|
|
14423
|
+
#url(path, query) {
|
|
14424
|
+
const url = new URL(`${path.map((segment) => encodeURIComponent(segment)).join("/")}`, `${this.#baseUrl}/`);
|
|
14425
|
+
if (query) {
|
|
14426
|
+
for (const [name, value] of Object.entries(query))
|
|
14427
|
+
url.searchParams.set(name, String(value));
|
|
14428
|
+
}
|
|
14429
|
+
return url.toString();
|
|
14430
|
+
}
|
|
14431
|
+
async#response(response) {
|
|
14432
|
+
const text = await response.text();
|
|
14433
|
+
let payload = text;
|
|
14434
|
+
if (text && isJson(response)) {
|
|
14435
|
+
try {
|
|
14436
|
+
payload = JSON.parse(text);
|
|
14437
|
+
} catch {
|
|
14438
|
+
payload = text;
|
|
14439
|
+
}
|
|
14440
|
+
}
|
|
14441
|
+
if (!response.ok) {
|
|
14442
|
+
const error = asErrorShape(payload);
|
|
14443
|
+
throw new DispatchServiceError(error.code ?? `HTTP_${response.status}`, response.status, error.error ?? (typeof payload === "string" && payload ? payload : response.statusText), error.candidates);
|
|
14444
|
+
}
|
|
14445
|
+
return payload;
|
|
14173
14446
|
}
|
|
14174
|
-
return result;
|
|
14175
14447
|
}
|
|
14176
14448
|
|
|
14177
|
-
// ../envoy-client/src/dispatch-
|
|
14178
|
-
var
|
|
14179
|
-
|
|
14180
|
-
|
|
14181
|
-
|
|
14182
|
-
|
|
14183
|
-
|
|
14184
|
-
|
|
14185
|
-
|
|
14186
|
-
|
|
14187
|
-
|
|
14188
|
-
|
|
14189
|
-
repo = resolved;
|
|
14190
|
-
}
|
|
14191
|
-
const resolvedOrigin = await resolveOrigin(input.env, input.exec, cwd);
|
|
14192
|
-
const origin = {
|
|
14193
|
-
...resolvedOrigin,
|
|
14194
|
-
host: input.host,
|
|
14195
|
-
...input.sessionId ? { sessionId: input.sessionId } : {},
|
|
14196
|
-
...input.sessionTitle ? { sessionTitle: input.sessionTitle } : {}
|
|
14197
|
-
};
|
|
14198
|
-
return { ...call, ...repo === undefined ? {} : { repo }, origin };
|
|
14449
|
+
// ../envoy-client/src/dispatch-execute.ts
|
|
14450
|
+
var nativeIssueKeyPattern = /^[A-Z][A-Z0-9]{1,9}-[0-9]+$/;
|
|
14451
|
+
var externalIssueRefPattern = /^([^/\s]+)\/([^/\s#]+)#([1-9][0-9]*)$/;
|
|
14452
|
+
var bareIssueNumberPattern = /^[1-9][0-9]*$/;
|
|
14453
|
+
function dispatchTopic(issue) {
|
|
14454
|
+
return `notifications.dispatch.issue.${issue}.>`;
|
|
14455
|
+
}
|
|
14456
|
+
function stringArg(args, name) {
|
|
14457
|
+
const value = args[name];
|
|
14458
|
+
if (typeof value !== "string")
|
|
14459
|
+
throw new Error(`${name} is required`);
|
|
14460
|
+
return value;
|
|
14199
14461
|
}
|
|
14200
|
-
|
|
14201
|
-
const
|
|
14202
|
-
|
|
14203
|
-
call: input.call,
|
|
14204
|
-
cwd: input.cwd,
|
|
14205
|
-
host: input.host,
|
|
14206
|
-
...input.sessionId === undefined ? {} : { sessionId: input.sessionId },
|
|
14207
|
-
...input.sessionTitle === undefined ? {} : { sessionTitle: input.sessionTitle },
|
|
14208
|
-
env: input.env ?? process.env,
|
|
14209
|
-
exec
|
|
14210
|
-
});
|
|
14211
|
-
return callDispatch({
|
|
14212
|
-
serviceUrl: input.serviceUrl,
|
|
14213
|
-
getToken: input.getToken ?? ghTokenGetter(input.cwd, exec),
|
|
14214
|
-
...input.fetchImpl ? { fetchImpl: input.fetchImpl } : {}
|
|
14215
|
-
}, prepared);
|
|
14462
|
+
function optionalString(args, name) {
|
|
14463
|
+
const value = args[name];
|
|
14464
|
+
return typeof value === "string" ? value : undefined;
|
|
14216
14465
|
}
|
|
14217
|
-
|
|
14218
|
-
|
|
14219
|
-
|
|
14220
|
-
|
|
14221
|
-
|
|
14222
|
-
|
|
14223
|
-
|
|
14224
|
-
|
|
14225
|
-
|
|
14226
|
-
|
|
14227
|
-
|
|
14228
|
-
|
|
14229
|
-
|
|
14230
|
-
});
|
|
14231
|
-
|
|
14232
|
-
|
|
14233
|
-
|
|
14234
|
-
|
|
14235
|
-
|
|
14236
|
-
|
|
14237
|
-
return
|
|
14466
|
+
function optionalBoolean(args, name) {
|
|
14467
|
+
const value = args[name];
|
|
14468
|
+
return typeof value === "boolean" ? value : undefined;
|
|
14469
|
+
}
|
|
14470
|
+
function optionalNumber(args, name) {
|
|
14471
|
+
const value = args[name];
|
|
14472
|
+
return typeof value === "number" ? value : undefined;
|
|
14473
|
+
}
|
|
14474
|
+
function askUrgency(args) {
|
|
14475
|
+
const value = args.urgency;
|
|
14476
|
+
return ASK_URGENCIES.find((urgency) => urgency === value);
|
|
14477
|
+
}
|
|
14478
|
+
function parseDispatchRef(ref) {
|
|
14479
|
+
const match = ref.match(/^dispatch:\/\/([A-Z][A-Z0-9]{1,9}-[1-9][0-9]*)(?:\/(spec)|\/artifact\/([^/@]+)(?:@v(\d+))?|\/ask\/([^/]+)|\/comment\/([^/]+))?$/);
|
|
14480
|
+
if (!match)
|
|
14481
|
+
return null;
|
|
14482
|
+
const [, issue, spec, artifact, version, ask, comment] = match;
|
|
14483
|
+
if (!issue || version !== undefined && Number(version) < 1)
|
|
14484
|
+
return null;
|
|
14485
|
+
if (spec)
|
|
14486
|
+
return { issue, kind: "spec", id: spec };
|
|
14487
|
+
if (artifact) {
|
|
14488
|
+
return {
|
|
14489
|
+
issue,
|
|
14490
|
+
kind: "artifact",
|
|
14491
|
+
id: artifact,
|
|
14492
|
+
...version === undefined ? {} : { version: Number(version) }
|
|
14493
|
+
};
|
|
14238
14494
|
}
|
|
14239
|
-
|
|
14495
|
+
if (ask)
|
|
14496
|
+
return { issue, kind: "ask", id: ask };
|
|
14497
|
+
if (comment)
|
|
14498
|
+
return { issue, kind: "comment", id: comment };
|
|
14499
|
+
return { issue, kind: "issue", id: issue };
|
|
14500
|
+
}
|
|
14501
|
+
function toolSchema(tool) {
|
|
14502
|
+
const spec = dispatchToolSpecs.find((candidate) => candidate.name === tool);
|
|
14503
|
+
if (!spec)
|
|
14504
|
+
throw new Error(`Unknown Dispatch tool: ${tool}`);
|
|
14505
|
+
return object(spec.arguments(zodSchemaApi(exports_external))).strict();
|
|
14506
|
+
}
|
|
14507
|
+
async function resolveIssueArguments(tool, args, cwd, env, exec) {
|
|
14508
|
+
if (tool === "dispatch_issue")
|
|
14509
|
+
return { args, ref: null };
|
|
14510
|
+
const refArgument = args.ref;
|
|
14511
|
+
const ref = typeof refArgument === "string" ? parseDispatchRef(refArgument) ?? (() => {
|
|
14512
|
+
throw new Error("ref must be a valid dispatch:// reference");
|
|
14513
|
+
})() : null;
|
|
14514
|
+
const issueArgument = args.issue;
|
|
14515
|
+
const artifactArgument = args.artifact;
|
|
14516
|
+
const versionArgument = args.version;
|
|
14517
|
+
if (issueArgument !== undefined || ref !== null) {
|
|
14518
|
+
return {
|
|
14519
|
+
args: {
|
|
14520
|
+
...args,
|
|
14521
|
+
...issueArgument === undefined && ref?.issue !== undefined ? { issue: ref.issue } : {},
|
|
14522
|
+
...artifactArgument === undefined && (ref?.kind === "spec" || ref?.kind === "artifact") ? { artifact: ref.id } : {},
|
|
14523
|
+
...versionArgument === undefined && ref?.version !== undefined ? { version: ref.version } : {}
|
|
14524
|
+
},
|
|
14525
|
+
ref
|
|
14526
|
+
};
|
|
14527
|
+
}
|
|
14528
|
+
const legionIssue = env.LEGION_ISSUE;
|
|
14529
|
+
if (!legionIssue)
|
|
14530
|
+
throw new Error("issue is required; supply issue or set LEGION_ISSUE");
|
|
14531
|
+
if (nativeIssueKeyPattern.test(legionIssue) || externalIssueRefPattern.test(legionIssue)) {
|
|
14532
|
+
return { args: { ...args, issue: legionIssue }, ref: null };
|
|
14533
|
+
}
|
|
14534
|
+
if (!bareIssueNumberPattern.test(legionIssue)) {
|
|
14535
|
+
throw new Error("LEGION_ISSUE must be a native issue key (e.g. LEGION-3), an external owner/repo#n reference, or a bare positive issue number");
|
|
14536
|
+
}
|
|
14537
|
+
const repo = await resolveCwdRepo(cwd, exec);
|
|
14538
|
+
if (!repo)
|
|
14539
|
+
throw new Error("issue is required; LEGION_ISSUE needs a GitHub repository in cwd");
|
|
14540
|
+
return { args: { ...args, issue: `${repo}#${legionIssue}` }, ref: null };
|
|
14240
14541
|
}
|
|
14241
|
-
function
|
|
14242
|
-
|
|
14243
|
-
|
|
14244
|
-
|
|
14245
|
-
|
|
14246
|
-
return { kind: "absent" };
|
|
14542
|
+
async function resolveArtifact(client, issueReference, artifactReference) {
|
|
14543
|
+
const issue = await client.getIssue(issueReference);
|
|
14544
|
+
const artifact = artifactReference === undefined || artifactReference === "spec" ? issue.artifacts.find((candidate) => candidate.primary || candidate.id === issue.primary_artifact_id) : issue.artifacts.find((candidate) => candidate.id === artifactReference || candidate.slug === artifactReference);
|
|
14545
|
+
if (!artifact) {
|
|
14546
|
+
throw new Error(`artifact ${artifactReference ?? "spec"} was not found on issue ${issue.key}`);
|
|
14247
14547
|
}
|
|
14248
|
-
|
|
14548
|
+
return { issue, artifact };
|
|
14549
|
+
}
|
|
14550
|
+
function anchor(artifact, args) {
|
|
14551
|
+
const quote = optionalString(args, "quote");
|
|
14552
|
+
if (quote === undefined)
|
|
14553
|
+
return;
|
|
14554
|
+
const occurrence = optionalNumber(args, "occurrence");
|
|
14555
|
+
return { artifact: artifact.id, quote, ...occurrence === undefined ? {} : { occurrence } };
|
|
14556
|
+
}
|
|
14557
|
+
function toolActor(origin, input) {
|
|
14558
|
+
return {
|
|
14559
|
+
kind: "session",
|
|
14560
|
+
id: input.sessionId ?? "unknown",
|
|
14561
|
+
origin: {
|
|
14562
|
+
...origin,
|
|
14563
|
+
host: input.host,
|
|
14564
|
+
...input.sessionTitle === undefined ? {} : { session_title: input.sessionTitle }
|
|
14565
|
+
}
|
|
14566
|
+
};
|
|
14567
|
+
}
|
|
14568
|
+
function issueSummary(issue, events) {
|
|
14569
|
+
const asks = issue.open_asks;
|
|
14570
|
+
return [
|
|
14571
|
+
`Title: ${issue.title}`,
|
|
14572
|
+
`Key: ${issue.key}`,
|
|
14573
|
+
`Status: ${issue.status}`,
|
|
14574
|
+
`Route: ${issue.route ?? "none"}`,
|
|
14575
|
+
"Open asks:",
|
|
14576
|
+
...asks.length === 0 ? ["- none"] : asks.map((ask) => `- ${ask.id}: ${ask.question}`),
|
|
14577
|
+
"Events:",
|
|
14578
|
+
...events.length === 0 ? ["- none"] : events.map((event) => `- #${event.seq} ${event.type} \xB7 ${event.actor.kind} ${event.actor.id} \xB7 ${event.created_at}`)
|
|
14579
|
+
].join(`
|
|
14580
|
+
`);
|
|
14581
|
+
}
|
|
14582
|
+
function askSummary(ask) {
|
|
14583
|
+
const answer = ask.answer;
|
|
14584
|
+
return [
|
|
14585
|
+
`Question: ${ask.question}`,
|
|
14586
|
+
"Options:",
|
|
14587
|
+
...ask.options.length === 0 ? ["- none"] : ask.options.map((option) => `- ${option.label}${option.description ? ` \u2014 ${option.description}` : ""}`),
|
|
14588
|
+
`State: ${ask.state}`,
|
|
14589
|
+
"Answer:",
|
|
14590
|
+
...answer === null ? ["- none"] : [
|
|
14591
|
+
`- By: ${answer.user}`,
|
|
14592
|
+
`- Selected: ${answer.selected.length === 0 ? "none" : answer.selected.join(", ")}`,
|
|
14593
|
+
...answer.text === null ? [] : [`- Text: ${answer.text}`]
|
|
14594
|
+
]
|
|
14595
|
+
].join(`
|
|
14596
|
+
`);
|
|
14597
|
+
}
|
|
14598
|
+
function commentSummary({ comment, replies }) {
|
|
14599
|
+
const root = [
|
|
14600
|
+
`${comment.id} \xB7 ${comment.author.kind} ${comment.author.id}`,
|
|
14601
|
+
...comment.anchor?.quote === undefined ? [] : [`> ${comment.anchor.quote}`],
|
|
14602
|
+
`Body: ${comment.body}`
|
|
14603
|
+
];
|
|
14604
|
+
const chain = replies.flatMap((reply) => [
|
|
14605
|
+
`${reply.id} \xB7 ${reply.author.kind} ${reply.author.id}`,
|
|
14606
|
+
...reply.anchor?.quote === undefined ? [] : [`> ${reply.anchor.quote}`],
|
|
14607
|
+
`Body: ${reply.body}`
|
|
14608
|
+
]);
|
|
14609
|
+
return ["Comment:", ...root, "Reply chain:", ...chain.length === 0 ? ["- none"] : chain].join(`
|
|
14610
|
+
`);
|
|
14611
|
+
}
|
|
14612
|
+
async function openArtifactMarks(client, resolved) {
|
|
14613
|
+
const marks = resolved.issue.open_asks.filter((ask) => ask.state === "open" && ask.anchor?.artifact_id === resolved.artifact.id).map((ask) => `ask ${ask.id}`);
|
|
14614
|
+
let comments;
|
|
14249
14615
|
try {
|
|
14250
|
-
|
|
14251
|
-
} catch (
|
|
14252
|
-
|
|
14616
|
+
comments = await client.getComments(resolved.issue.key, resolved.artifact.id);
|
|
14617
|
+
} catch (error) {
|
|
14618
|
+
if (error instanceof DispatchServiceError && error.status === 404)
|
|
14619
|
+
return marks;
|
|
14620
|
+
throw error;
|
|
14253
14621
|
}
|
|
14254
|
-
|
|
14255
|
-
|
|
14256
|
-
|
|
14622
|
+
return [
|
|
14623
|
+
...marks,
|
|
14624
|
+
...comments.filter((comment) => !comment.resolved && comment.anchor?.artifact_id === resolved.artifact.id).map((comment) => `comment ${comment.id}`)
|
|
14625
|
+
];
|
|
14626
|
+
}
|
|
14627
|
+
async function executeDispatchTool(input) {
|
|
14628
|
+
if (!input.config.enabled || !input.config.url || !input.config.token) {
|
|
14629
|
+
throw new Error("Dispatch is disabled; resolve both DISPATCH_URL and DISPATCH_TOKEN");
|
|
14630
|
+
}
|
|
14631
|
+
const env = input.env ?? process.env;
|
|
14632
|
+
const exec = input.exec ?? defaultExec;
|
|
14633
|
+
const issueArguments = await resolveIssueArguments(input.tool, input.args, input.cwd, env, exec);
|
|
14634
|
+
const args = toolSchema(input.tool).parse(issueArguments.args);
|
|
14635
|
+
const actor = toolActor(await resolveOrigin(env, exec, input.cwd), input);
|
|
14636
|
+
const client = new DispatchClient(input.config.url, input.config.token, input.fetchImpl);
|
|
14637
|
+
const issueKey = input.tool === "dispatch_issue" ? null : await ensureIssue(client, stringArg(args, "issue"), actor);
|
|
14638
|
+
const issue = () => {
|
|
14639
|
+
if (issueKey === null)
|
|
14640
|
+
throw new Error("issue is required");
|
|
14641
|
+
return issueKey;
|
|
14642
|
+
};
|
|
14643
|
+
switch (input.tool) {
|
|
14644
|
+
case "dispatch_issue": {
|
|
14645
|
+
const parent = optionalString(args, "parent");
|
|
14646
|
+
const external = optionalString(args, "external");
|
|
14647
|
+
const spec = optionalString(args, "spec");
|
|
14648
|
+
const created = await client.issue({
|
|
14649
|
+
project: stringArg(args, "project"),
|
|
14650
|
+
title: stringArg(args, "title"),
|
|
14651
|
+
...parent === undefined ? {} : { parent },
|
|
14652
|
+
...external === undefined ? {} : { external },
|
|
14653
|
+
...spec === undefined ? {} : { spec },
|
|
14654
|
+
actor
|
|
14655
|
+
});
|
|
14656
|
+
return {
|
|
14657
|
+
text: `Created ${created.key}: ${created.title}`,
|
|
14658
|
+
details: { issue: created.key, topic: dispatchTopic(created.key) }
|
|
14659
|
+
};
|
|
14660
|
+
}
|
|
14661
|
+
case "dispatch_ask": {
|
|
14662
|
+
const anchorArgs = asObject(args.anchor);
|
|
14663
|
+
const resolved = anchorArgs ? await resolveArtifact(client, issue(), stringArg(anchorArgs, "artifact")) : undefined;
|
|
14664
|
+
const options = args.options;
|
|
14665
|
+
const multiple = optionalBoolean(args, "multiple");
|
|
14666
|
+
const custom = optionalBoolean(args, "custom");
|
|
14667
|
+
const urgency = askUrgency(args);
|
|
14668
|
+
const anchored = anchorArgs && resolved ? anchor(resolved.artifact, anchorArgs) : undefined;
|
|
14669
|
+
const ask = await client.ask(issue(), {
|
|
14670
|
+
question: stringArg(args, "question"),
|
|
14671
|
+
...Array.isArray(options) ? { options } : {},
|
|
14672
|
+
...multiple === undefined ? {} : { multiple },
|
|
14673
|
+
...custom === undefined ? {} : { custom },
|
|
14674
|
+
...urgency === undefined ? {} : { urgency },
|
|
14675
|
+
...anchored === undefined ? {} : { anchor: anchored },
|
|
14676
|
+
actor
|
|
14677
|
+
});
|
|
14678
|
+
return {
|
|
14679
|
+
text: `Opened ask ${ask.id}: ${ask.question}`,
|
|
14680
|
+
details: { issue: ask.issue_key, topic: dispatchTopic(ask.issue_key), ask: ask.id }
|
|
14681
|
+
};
|
|
14682
|
+
}
|
|
14683
|
+
case "dispatch_comment": {
|
|
14684
|
+
const artifactReference = optionalString(args, "artifact");
|
|
14685
|
+
if (optionalString(args, "quote") !== undefined && artifactReference === undefined) {
|
|
14686
|
+
throw new Error("artifact is required when quote is supplied");
|
|
14687
|
+
}
|
|
14688
|
+
const resolved = artifactReference ? await resolveArtifact(client, issue(), artifactReference) : undefined;
|
|
14689
|
+
const anchored = resolved ? anchor(resolved.artifact, args) : undefined;
|
|
14690
|
+
const replyTo = optionalString(args, "reply_to");
|
|
14691
|
+
const comment = await client.comment(issue(), {
|
|
14692
|
+
body: stringArg(args, "body"),
|
|
14693
|
+
...anchored === undefined ? {} : { anchor: anchored },
|
|
14694
|
+
...replyTo === undefined ? {} : { reply_to: replyTo },
|
|
14695
|
+
actor
|
|
14696
|
+
});
|
|
14697
|
+
return {
|
|
14698
|
+
text: `Posted comment ${comment.id}`,
|
|
14699
|
+
details: {
|
|
14700
|
+
issue: comment.issue_key,
|
|
14701
|
+
topic: dispatchTopic(comment.issue_key),
|
|
14702
|
+
comment: comment.id
|
|
14703
|
+
}
|
|
14704
|
+
};
|
|
14705
|
+
}
|
|
14706
|
+
case "dispatch_suggest": {
|
|
14707
|
+
const resolved = await resolveArtifact(client, issue(), stringArg(args, "artifact"));
|
|
14708
|
+
const anchored = anchor(resolved.artifact, args);
|
|
14709
|
+
if (anchored === undefined)
|
|
14710
|
+
throw new Error("quote is required");
|
|
14711
|
+
const body = optionalString(args, "body");
|
|
14712
|
+
const comment = await client.suggest(issue(), {
|
|
14713
|
+
...body === undefined ? {} : { body },
|
|
14714
|
+
anchor: anchored,
|
|
14715
|
+
replace_with: stringArg(args, "replace_with"),
|
|
14716
|
+
actor
|
|
14717
|
+
});
|
|
14718
|
+
return {
|
|
14719
|
+
text: `Posted suggestion ${comment.id}`,
|
|
14720
|
+
details: {
|
|
14721
|
+
issue: comment.issue_key,
|
|
14722
|
+
topic: dispatchTopic(comment.issue_key),
|
|
14723
|
+
comment: comment.id
|
|
14724
|
+
}
|
|
14725
|
+
};
|
|
14726
|
+
}
|
|
14727
|
+
case "dispatch_message": {
|
|
14728
|
+
const message = await client.message(issue(), { body: stringArg(args, "body"), actor });
|
|
14729
|
+
return {
|
|
14730
|
+
text: `Posted message ${message.id}`,
|
|
14731
|
+
details: {
|
|
14732
|
+
issue: message.issue_key,
|
|
14733
|
+
topic: dispatchTopic(message.issue_key),
|
|
14734
|
+
message: message.id
|
|
14735
|
+
}
|
|
14736
|
+
};
|
|
14737
|
+
}
|
|
14738
|
+
case "dispatch_doc_edit": {
|
|
14739
|
+
const resolved = await resolveArtifact(client, issue(), stringArg(args, "artifact"));
|
|
14740
|
+
const ops = args.ops;
|
|
14741
|
+
const summary = optionalString(args, "summary");
|
|
14742
|
+
const edited = await client.docEdit(resolved.artifact.id, {
|
|
14743
|
+
ops,
|
|
14744
|
+
...summary === undefined ? {} : { summary },
|
|
14745
|
+
actor
|
|
14746
|
+
});
|
|
14747
|
+
return {
|
|
14748
|
+
text: edited.version === null ? `Applied ${edited.applied} ops (no new version)` : `Applied ${edited.applied} ops (version ${edited.version.number})`,
|
|
14749
|
+
details: {
|
|
14750
|
+
issue: resolved.issue.key,
|
|
14751
|
+
topic: dispatchTopic(resolved.issue.key),
|
|
14752
|
+
applied: edited.applied,
|
|
14753
|
+
...edited.version === null ? {} : { version: edited.version.number }
|
|
14754
|
+
}
|
|
14755
|
+
};
|
|
14756
|
+
}
|
|
14757
|
+
case "dispatch_doc_read": {
|
|
14758
|
+
const artifactReference = optionalString(args, "artifact") ?? (issueArguments.ref?.kind === "spec" || issueArguments.ref?.kind === "artifact" ? issueArguments.ref.id : undefined);
|
|
14759
|
+
const resolved = await resolveArtifact(client, issue(), artifactReference);
|
|
14760
|
+
const version = optionalNumber(args, "version") ?? issueArguments.ref?.version;
|
|
14761
|
+
const document = await client.docRead(resolved.artifact.id, version);
|
|
14762
|
+
const marks = await openArtifactMarks(client, resolved);
|
|
14763
|
+
return {
|
|
14764
|
+
text: marks.length === 0 ? document.markdown : `${document.markdown}
|
|
14765
|
+
|
|
14766
|
+
Open anchored asks/comments: ${marks.join(", ")}`,
|
|
14767
|
+
details: { issue: resolved.issue.key, topic: dispatchTopic(resolved.issue.key) }
|
|
14768
|
+
};
|
|
14769
|
+
}
|
|
14770
|
+
case "dispatch_artifact": {
|
|
14771
|
+
const primary = optionalBoolean(args, "primary");
|
|
14772
|
+
const summary = optionalString(args, "summary");
|
|
14773
|
+
const result = await client.artifact(issue(), {
|
|
14774
|
+
name: stringArg(args, "name"),
|
|
14775
|
+
file: Bun.file(resolvePath(input.cwd, stringArg(args, "path"))),
|
|
14776
|
+
...primary === undefined ? {} : { primary },
|
|
14777
|
+
...summary === undefined ? {} : { summary },
|
|
14778
|
+
actor
|
|
14779
|
+
});
|
|
14780
|
+
return {
|
|
14781
|
+
text: `Uploaded ${result.artifact.name} as version ${result.version.number}`,
|
|
14782
|
+
details: {
|
|
14783
|
+
issue: result.artifact.issue_key,
|
|
14784
|
+
topic: dispatchTopic(result.artifact.issue_key),
|
|
14785
|
+
artifact: result.artifact.id,
|
|
14786
|
+
version: result.version.number
|
|
14787
|
+
}
|
|
14788
|
+
};
|
|
14789
|
+
}
|
|
14790
|
+
case "dispatch_read": {
|
|
14791
|
+
if (issueArguments.ref?.kind === "ask") {
|
|
14792
|
+
const ask = await client.getAsk(issueArguments.ref.id);
|
|
14793
|
+
return {
|
|
14794
|
+
text: askSummary(ask),
|
|
14795
|
+
details: { issue: ask.issue_key, topic: dispatchTopic(ask.issue_key) }
|
|
14796
|
+
};
|
|
14797
|
+
}
|
|
14798
|
+
if (issueArguments.ref?.kind === "comment") {
|
|
14799
|
+
const comment = await client.getComment(issueArguments.ref.id);
|
|
14800
|
+
return {
|
|
14801
|
+
text: commentSummary(comment),
|
|
14802
|
+
details: {
|
|
14803
|
+
issue: comment.comment.issue_key,
|
|
14804
|
+
topic: dispatchTopic(comment.comment.issue_key)
|
|
14805
|
+
}
|
|
14806
|
+
};
|
|
14807
|
+
}
|
|
14808
|
+
const read = await client.read(issue());
|
|
14809
|
+
return {
|
|
14810
|
+
text: issueSummary(read.issue, read.events),
|
|
14811
|
+
details: { issue: read.issue.key, topic: dispatchTopic(read.issue.key) }
|
|
14812
|
+
};
|
|
14813
|
+
}
|
|
14814
|
+
default:
|
|
14815
|
+
throw new Error(`Unknown Dispatch tool: ${input.tool}`);
|
|
14257
14816
|
}
|
|
14258
|
-
return { kind: "valid", settings: parsed.data.dispatch ?? null };
|
|
14259
14817
|
}
|
|
14260
|
-
function
|
|
14261
|
-
|
|
14262
|
-
|
|
14263
|
-
|
|
14264
|
-
|
|
14265
|
-
|
|
14266
|
-
|
|
14267
|
-
|
|
14268
|
-
|
|
14269
|
-
if (file.kind === "invalid")
|
|
14270
|
-
return { url: null, error: file.reason };
|
|
14818
|
+
async function ensureIssue(client, issueReference, actor) {
|
|
14819
|
+
try {
|
|
14820
|
+
return await client.ensureIssue(issueReference, actor);
|
|
14821
|
+
} catch (error) {
|
|
14822
|
+
if (error instanceof DispatchServiceError && error.code === "PROJECT_UNMAPPED") {
|
|
14823
|
+
const repository = issueReference.slice(0, issueReference.lastIndexOf("#"));
|
|
14824
|
+
throw new Error(`repository ${repository} is not mapped in DISPATCH_REPO_PROJECTS`);
|
|
14825
|
+
}
|
|
14826
|
+
throw error;
|
|
14271
14827
|
}
|
|
14272
|
-
|
|
14273
|
-
|
|
14274
|
-
|
|
14275
|
-
};
|
|
14276
|
-
if (merged.enabled !== true)
|
|
14277
|
-
return { url: null, error: null };
|
|
14278
|
-
const baseUrl = (merged.serverUrl ?? DEFAULT_SERVER_URL).replace(/\/+$/, "");
|
|
14279
|
-
return { url: `${baseUrl}/mcp`, error: null };
|
|
14828
|
+
}
|
|
14829
|
+
function asObject(value) {
|
|
14830
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
|
|
14280
14831
|
}
|
|
14281
14832
|
|
|
14282
14833
|
// ../envoy-client/src/dispatch-subscribe.ts
|
|
14283
|
-
|
|
14284
|
-
|
|
14285
|
-
return tool === DISPATCH_TOOL_NAME;
|
|
14286
|
-
}
|
|
14287
|
-
function dispatchThreadTopic(owner, repo, thread) {
|
|
14288
|
-
return `notifications.github.${owner}.${repo}.issue.${thread}.>`;
|
|
14289
|
-
}
|
|
14290
|
-
function dispatchSubscriptionTopic(tool, output) {
|
|
14291
|
-
if (!isDispatchTool(tool))
|
|
14292
|
-
return null;
|
|
14293
|
-
const match = ISSUE_URL_RE.exec(output);
|
|
14294
|
-
if (!match)
|
|
14834
|
+
function dispatchSubscriptionTopic(details) {
|
|
14835
|
+
if (typeof details !== "object" || details === null || !("topic" in details))
|
|
14295
14836
|
return null;
|
|
14296
|
-
const
|
|
14297
|
-
|
|
14298
|
-
const thread = Number(match[3]);
|
|
14299
|
-
if (!Number.isInteger(thread) || thread <= 0)
|
|
14300
|
-
return null;
|
|
14301
|
-
return dispatchThreadTopic(owner, repo, thread);
|
|
14837
|
+
const { topic } = details;
|
|
14838
|
+
return typeof topic === "string" && topic.startsWith("notifications.dispatch.") ? topic : null;
|
|
14302
14839
|
}
|
|
14303
14840
|
|
|
14304
14841
|
// ../envoy-client/src/tool-contract.ts
|
|
@@ -14312,10 +14849,19 @@ function messageMetadataShape(schema) {
|
|
|
14312
14849
|
supersedes: schema.string().optional(),
|
|
14313
14850
|
urgency: schema.enum(URGENCY_VALUES).optional(),
|
|
14314
14851
|
expects_reply: schema.enum(EXPECTS_REPLY_VALUES).optional(),
|
|
14315
|
-
expires_at: schema.number(
|
|
14852
|
+
expires_at: schema.number({ int: true }).optional()
|
|
14853
|
+
};
|
|
14854
|
+
}
|
|
14855
|
+
var MessageMetadataSchema = object(messageMetadataShape(zodSchemaApi(exports_external)));
|
|
14856
|
+
function toMessageMetadata(args) {
|
|
14857
|
+
return {
|
|
14858
|
+
...args.in_reply_to === undefined ? {} : { inReplyTo: args.in_reply_to },
|
|
14859
|
+
...args.supersedes === undefined ? {} : { supersedes: args.supersedes },
|
|
14860
|
+
...args.urgency === undefined ? {} : { urgency: args.urgency },
|
|
14861
|
+
...args.expects_reply === undefined ? {} : { expectsReply: args.expects_reply },
|
|
14862
|
+
...args.expires_at === undefined ? {} : { expiresAt: args.expires_at }
|
|
14316
14863
|
};
|
|
14317
14864
|
}
|
|
14318
|
-
var MessageMetadataSchema = object(messageMetadataShape(exports_external));
|
|
14319
14865
|
function messageArguments(schema) {
|
|
14320
14866
|
return { message: schema.string(), ...messageMetadataShape(schema) };
|
|
14321
14867
|
}
|
|
@@ -14705,33 +15251,12 @@ var publishSpec = toolSpec("envoy_publish");
|
|
|
14705
15251
|
var roleSetSpec = toolSpec("envoy_role_set");
|
|
14706
15252
|
var whoamiSpec = toolSpec("envoy_whoami");
|
|
14707
15253
|
var sessionsSpec = toolSpec("envoy_sessions");
|
|
14708
|
-
var dispatchQuestionOption = tool.schema.strictObject({
|
|
14709
|
-
label: tool.schema.string().min(1),
|
|
14710
|
-
description: tool.schema.string().optional()
|
|
14711
|
-
});
|
|
14712
|
-
var dispatchQuestion = tool.schema.strictObject({
|
|
14713
|
-
question: tool.schema.string().min(1),
|
|
14714
|
-
header: tool.schema.string().optional(),
|
|
14715
|
-
options: tool.schema.array(dispatchQuestionOption).optional(),
|
|
14716
|
-
multiple: tool.schema.boolean().optional(),
|
|
14717
|
-
custom: tool.schema.boolean().optional()
|
|
14718
|
-
});
|
|
14719
|
-
var dispatchArgs = {
|
|
14720
|
-
subject: tool.schema.string().describe(DISPATCH_ARGUMENTS.subject).optional(),
|
|
14721
|
-
thread: tool.schema.string().describe(DISPATCH_ARGUMENTS.thread).optional(),
|
|
14722
|
-
context: tool.schema.string().describe(DISPATCH_ARGUMENTS.context),
|
|
14723
|
-
question: tool.schema.string().describe(DISPATCH_ARGUMENTS.question),
|
|
14724
|
-
ask: tool.schema.array(dispatchQuestion).describe(DISPATCH_ARGUMENTS.ask).optional(),
|
|
14725
|
-
urgency: tool.schema.enum(DISPATCH_URGENCIES).describe(DISPATCH_ARGUMENTS.urgency).optional(),
|
|
14726
|
-
repo: tool.schema.string().describe(DISPATCH_ARGUMENTS.repo).optional(),
|
|
14727
|
-
parent: tool.schema.string().describe(DISPATCH_ARGUMENTS.parent).optional()
|
|
14728
|
-
};
|
|
14729
15254
|
var server_default = async (input) => {
|
|
14730
15255
|
const cwd = process.cwd();
|
|
14731
15256
|
const dispatchConfig = resolveDispatchConfig(process.env, { cwd });
|
|
14732
|
-
if (dispatchConfig.
|
|
14733
|
-
|
|
14734
|
-
|
|
15257
|
+
if (!dispatchConfig.enabled) {
|
|
15258
|
+
logger.warn(`envoy: dispatch tools disabled \u2014 ${dispatchConfig.error ?? "no Dispatch URL configured"}`);
|
|
15259
|
+
}
|
|
14735
15260
|
const envoyDefaults = envoyDefaultsFromEnvironment(process.env);
|
|
14736
15261
|
const envoy = createEnvoyClient({ baseUrl: envoyDefaults.envoyUrl, fetch: globalThis.fetch });
|
|
14737
15262
|
let activeSessionID = null;
|
|
@@ -14802,25 +15327,29 @@ var server_default = async (input) => {
|
|
|
14802
15327
|
clearInterval(timer);
|
|
14803
15328
|
clearInterval(heartbeatInterval);
|
|
14804
15329
|
});
|
|
14805
|
-
const
|
|
14806
|
-
|
|
14807
|
-
|
|
14808
|
-
|
|
14809
|
-
|
|
14810
|
-
|
|
14811
|
-
|
|
14812
|
-
|
|
14813
|
-
|
|
14814
|
-
|
|
14815
|
-
|
|
14816
|
-
|
|
14817
|
-
|
|
14818
|
-
|
|
14819
|
-
|
|
14820
|
-
|
|
14821
|
-
|
|
14822
|
-
|
|
14823
|
-
|
|
15330
|
+
const dispatchTools = {};
|
|
15331
|
+
if (dispatchConfig.enabled) {
|
|
15332
|
+
for (const spec of dispatchToolSpecs) {
|
|
15333
|
+
dispatchTools[spec.name] = tool({
|
|
15334
|
+
description: spec.description,
|
|
15335
|
+
args: spec.arguments(zodSchemaApi(tool.schema)),
|
|
15336
|
+
async execute(args, ctx) {
|
|
15337
|
+
ctx.metadata({ title: "Dispatch" });
|
|
15338
|
+
const result = await executeDispatchTool({
|
|
15339
|
+
tool: spec.name,
|
|
15340
|
+
args,
|
|
15341
|
+
cwd: ctx.directory,
|
|
15342
|
+
host: "opencode",
|
|
15343
|
+
sessionId: ctx.sessionID,
|
|
15344
|
+
sessionTitle: await fetchTitle(ctx.sessionID) ?? undefined,
|
|
15345
|
+
config: dispatchConfig,
|
|
15346
|
+
env: process.env
|
|
15347
|
+
});
|
|
15348
|
+
return { title: "Dispatch", output: result.text, metadata: result.details };
|
|
15349
|
+
}
|
|
15350
|
+
});
|
|
15351
|
+
}
|
|
15352
|
+
}
|
|
14824
15353
|
return {
|
|
14825
15354
|
config: (cfg) => {
|
|
14826
15355
|
if (skillsDirectory) {
|
|
@@ -14881,7 +15410,7 @@ var server_default = async (input) => {
|
|
|
14881
15410
|
}
|
|
14882
15411
|
},
|
|
14883
15412
|
"tool.execute.after": async (input, output) => {
|
|
14884
|
-
const topic = dispatchSubscriptionTopic(
|
|
15413
|
+
const topic = dispatchSubscriptionTopic(output.metadata);
|
|
14885
15414
|
if (!topic)
|
|
14886
15415
|
return;
|
|
14887
15416
|
try {
|
|
@@ -14903,10 +15432,10 @@ var server_default = async (input) => {
|
|
|
14903
15432
|
clearInterval(heartbeatInterval);
|
|
14904
15433
|
},
|
|
14905
15434
|
tool: {
|
|
14906
|
-
...
|
|
15435
|
+
...dispatchTools,
|
|
14907
15436
|
envoy_subscribe: tool({
|
|
14908
15437
|
description: subscribeSpec.description,
|
|
14909
|
-
args:
|
|
15438
|
+
args: subscribeSpec.arguments(zodSchemaApi(tool.schema)),
|
|
14910
15439
|
async execute(args, ctx) {
|
|
14911
15440
|
ctx.metadata({ title: "Envoy subscribe" });
|
|
14912
15441
|
return JSON.stringify(await envoy.subscribe({
|
|
@@ -14921,16 +15450,19 @@ var server_default = async (input) => {
|
|
|
14921
15450
|
}),
|
|
14922
15451
|
envoy_unsubscribe: tool({
|
|
14923
15452
|
description: unsubscribeSpec.description,
|
|
14924
|
-
args:
|
|
15453
|
+
args: unsubscribeSpec.arguments(zodSchemaApi(tool.schema)),
|
|
14925
15454
|
async execute(args, ctx) {
|
|
14926
15455
|
ctx.metadata({ title: "Envoy unsubscribe" });
|
|
14927
|
-
await envoy.unsubscribe({
|
|
15456
|
+
await envoy.unsubscribe({
|
|
15457
|
+
sessionID: ctx.sessionID,
|
|
15458
|
+
topics: args.topics ?? []
|
|
15459
|
+
});
|
|
14928
15460
|
return "ok";
|
|
14929
15461
|
}
|
|
14930
15462
|
}),
|
|
14931
15463
|
envoy_list: tool({
|
|
14932
15464
|
description: listSpec.description,
|
|
14933
|
-
args:
|
|
15465
|
+
args: listSpec.arguments(zodSchemaApi(tool.schema)),
|
|
14934
15466
|
async execute(_args, ctx) {
|
|
14935
15467
|
ctx.metadata({ title: "Envoy list" });
|
|
14936
15468
|
return JSON.stringify(await envoy.getInterest(ctx.sessionID));
|
|
@@ -14938,31 +15470,33 @@ var server_default = async (input) => {
|
|
|
14938
15470
|
}),
|
|
14939
15471
|
envoy_send: tool({
|
|
14940
15472
|
description: sendSpec.description,
|
|
14941
|
-
args:
|
|
15473
|
+
args: sendSpec.arguments(zodSchemaApi(tool.schema)),
|
|
14942
15474
|
async execute(args, ctx) {
|
|
14943
15475
|
ctx.metadata({ title: "Envoy send" });
|
|
14944
15476
|
return JSON.stringify(await envoy.send({
|
|
14945
15477
|
sourceSessionID: ctx.sessionID,
|
|
14946
15478
|
targetSessionID: args.session_id,
|
|
14947
|
-
message: args.message
|
|
15479
|
+
message: args.message,
|
|
15480
|
+
...toMessageMetadata(args)
|
|
14948
15481
|
}));
|
|
14949
15482
|
}
|
|
14950
15483
|
}),
|
|
14951
15484
|
envoy_publish: tool({
|
|
14952
15485
|
description: publishSpec.description,
|
|
14953
|
-
args:
|
|
15486
|
+
args: publishSpec.arguments(zodSchemaApi(tool.schema)),
|
|
14954
15487
|
async execute(args, ctx) {
|
|
14955
15488
|
ctx.metadata({ title: "Envoy publish" });
|
|
14956
15489
|
return JSON.stringify(await envoy.publish({
|
|
14957
15490
|
sourceSessionID: ctx.sessionID,
|
|
14958
15491
|
topic: args.topic,
|
|
14959
|
-
message: args.message
|
|
15492
|
+
message: args.message,
|
|
15493
|
+
...toMessageMetadata(args)
|
|
14960
15494
|
}));
|
|
14961
15495
|
}
|
|
14962
15496
|
}),
|
|
14963
15497
|
envoy_role_set: tool({
|
|
14964
15498
|
description: roleSetSpec.description,
|
|
14965
|
-
args:
|
|
15499
|
+
args: roleSetSpec.arguments(zodSchemaApi(tool.schema)),
|
|
14966
15500
|
async execute(args, ctx) {
|
|
14967
15501
|
ctx.metadata({ title: "Set Envoy role" });
|
|
14968
15502
|
return JSON.stringify(await envoy.setRole({ sessionID: ctx.sessionID, role: args.role }));
|
|
@@ -14970,7 +15504,7 @@ var server_default = async (input) => {
|
|
|
14970
15504
|
}),
|
|
14971
15505
|
envoy_whoami: tool({
|
|
14972
15506
|
description: whoamiSpec.description,
|
|
14973
|
-
args:
|
|
15507
|
+
args: whoamiSpec.arguments(zodSchemaApi(tool.schema)),
|
|
14974
15508
|
async execute(_args, ctx) {
|
|
14975
15509
|
ctx.metadata({ title: "Envoy whoami" });
|
|
14976
15510
|
const sessionID = ctx.sessionID;
|
|
@@ -14985,11 +15519,15 @@ var server_default = async (input) => {
|
|
|
14985
15519
|
}),
|
|
14986
15520
|
envoy_sessions: tool({
|
|
14987
15521
|
description: sessionsSpec.description,
|
|
14988
|
-
args:
|
|
15522
|
+
args: sessionsSpec.arguments(zodSchemaApi(tool.schema)),
|
|
14989
15523
|
async execute(args, ctx) {
|
|
14990
15524
|
ctx.metadata({ title: "Envoy sessions" });
|
|
14991
|
-
const sessions = await envoy.listSessions(
|
|
14992
|
-
|
|
15525
|
+
const sessions = await envoy.listSessions({
|
|
15526
|
+
...args.dir === undefined ? {} : { directory: args.dir },
|
|
15527
|
+
...args.title === undefined ? {} : { title: args.title }
|
|
15528
|
+
});
|
|
15529
|
+
const machine = args.machine;
|
|
15530
|
+
return JSON.stringify(machine ? sessions.filter((session) => session.machine_id === machine) : sessions, null, 2);
|
|
14993
15531
|
}
|
|
14994
15532
|
})
|
|
14995
15533
|
}
|