@sjawhar/pi-envoy 9.7.1 → 10.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +51 -17
- package/dist/dispatch.js +650 -232
- package/dist/envoy.js +872 -319
- package/dist/skills/dispatch/SKILL.md +24 -24
- package/dist/skills/dispatch/references/api.md +16 -3
- package/dist/skills/dispatch/references/documents.md +21 -1
- package/dist/skills/dispatch/references/issues.md +5 -1
- package/package.json +1 -1
package/dist/dispatch.js
CHANGED
|
@@ -14290,12 +14290,12 @@ var dispatchToolSpecs = [
|
|
|
14290
14290
|
{
|
|
14291
14291
|
name: "dispatch_request_approval",
|
|
14292
14292
|
example: { issue: "DSP-1", summary: "A live sync replaces the nightly export." },
|
|
14293
|
-
description: "Ask a human to approve a document at its current version. Before calling, have a fresh read-only reader check the spec against the code and the human's words, then resolve every finding; a Legion architect uses `plan-gap-analyst`. Opens an approval ask (Approve / " + "Request changes) in the human's Inbox whose question names the document and version, " + "followed by the summary; the answer pins a review to that version and arrives as " + "artifact.approved or artifact.changes_requested. An open request follows the document: a " + "later version moves it to that version and leaves it waiting on you, as a human's reply in " + "its thread does. Only the first move since the request was opened or handed back sends an " + "event, and never to the session whose version made it; dispatch doc-read shows whom it " + "waits on. Once the revision is complete and " + HUMAN_AGREED_TO_DOCUMENT + ", call this again to hand that same Inbox row back. The request carries nothing new. A " + "call while it already waits on the human hands nothing back: the same summary changes " + "nothing, and a different one is refused, since it would rewrite the card the human is " + "reading. Approve and Request changes each close the request, so the next call opens a new " + "one. Call it once per revision, when the revision is complete, never after each edit. An " + "approval goes stale when the document changes after it: request approval again once that " + "
|
|
14293
|
+
description: "Ask a human to approve a document at its current version. Before calling, have a fresh read-only reader check the spec against the code and the human's words, then resolve every finding; a Legion architect uses `plan-gap-analyst`. Opens an approval ask (Approve / " + "Request changes) in the human's Inbox whose question names the document and version, " + "followed by the summary; the answer pins a review to that version and arrives as " + "artifact.approved or artifact.changes_requested. An open request follows the document: a " + "later version moves it to that version and leaves it waiting on you, as a human's reply in " + "its thread does. Only the first move since the request was opened or handed back sends an " + "event, and never to the session whose version made it; dispatch doc-read shows whom it " + "waits on. Once the revision is complete and " + HUMAN_AGREED_TO_DOCUMENT + ", call this again to hand that same Inbox row back. The request carries nothing new. A " + "call while it already waits on the human hands nothing back: the same summary changes " + "nothing, and a different one is refused, since it would rewrite the card the human is " + "reading. Approve and Request changes each close the request, so the next call opens a new " + "one. Call it once per revision, when the revision is complete, never after each edit. An " + "approval goes stale when the document changes outside its decision blocks after it: " + "request approval again once that revision is complete and " + HUMAN_AGREED_TO_DOCUMENT + ", with a one-sentence summary naming what changed since the earlier approval: the card " + "names that approval (version, date, reviewer) and its Show diff control carries the detail. " + "A human's own answer to a decision block carries the approval to the version it writes when " + "that answer is the whole change since the approved version, so nothing is requested for " + "it. A new version of a Legion root spec closes its armed design gate until a human " + "approves it; the carried approval reopens it too. " + "Refused, with nothing sent, while the document holds an open decision block, even when a " + "human asked for approval: the refusal names each block; ask the human to answer or waive " + "it first. " + OWNER_REFERENCE,
|
|
14294
14294
|
arguments: (z2) => ({
|
|
14295
14295
|
issue: z2.string().describe(ISSUE_REFERENCE).optional(),
|
|
14296
14296
|
project: z2.string().describe("Project key owning the document.").optional(),
|
|
14297
14297
|
artifact: z2.string().describe("Project document artifact id, slug, or filename; primary document by default for an issue.").optional(),
|
|
14298
|
-
summary: z2.string({ min: 1 }).describe(`What the human is approving, in one to three sentences, and nothing else: no commentary on itself or the conversation, and no question. Request approval only once ${HUMAN_AGREED_TO_DOCUMENT}.`)
|
|
14298
|
+
summary: z2.string({ min: 1 }).describe(`What the human is approving, in one to three sentences, and nothing else: no commentary on itself or the conversation, and no question; after an earlier approval, one sentence naming what changed since it, since the card shows that approval and a diff. Request approval only once ${HUMAN_AGREED_TO_DOCUMENT}.`)
|
|
14299
14299
|
}),
|
|
14300
14300
|
validation: documentOwnerValidation(true)
|
|
14301
14301
|
},
|
|
@@ -14958,150 +14958,6 @@ function commandsHelp() {
|
|
|
14958
14958
|
`);
|
|
14959
14959
|
}
|
|
14960
14960
|
|
|
14961
|
-
// ../envoy-client/src/dispatch-config.ts
|
|
14962
|
-
import { readFileSync as readFileSync2 } from "fs";
|
|
14963
|
-
import { homedir } from "os";
|
|
14964
|
-
import * as path from "path";
|
|
14965
|
-
|
|
14966
|
-
// ../envoy-client/src/secret-file.ts
|
|
14967
|
-
import { readFileSync } from "fs";
|
|
14968
|
-
function readSecretFile(variable, filePath) {
|
|
14969
|
-
let contents;
|
|
14970
|
-
try {
|
|
14971
|
-
contents = readFileSync(filePath, "utf8");
|
|
14972
|
-
} catch (error48) {
|
|
14973
|
-
throw new Error(`${variable} names ${filePath}, which could not be read: ${messageFor(error48)}`);
|
|
14974
|
-
}
|
|
14975
|
-
const value = contents.trim();
|
|
14976
|
-
if (value.length === 0)
|
|
14977
|
-
throw new Error(`${variable} names ${filePath}, which is empty`);
|
|
14978
|
-
return value;
|
|
14979
|
-
}
|
|
14980
|
-
|
|
14981
|
-
// ../envoy-client/src/dispatch-config.ts
|
|
14982
|
-
var DEFAULT_SERVER_URL = "http://localhost:8766";
|
|
14983
|
-
function normalizeDispatchUrl(url2) {
|
|
14984
|
-
return url2.replace(/\/+$/, "");
|
|
14985
|
-
}
|
|
14986
|
-
function parsedDispatchUrl(value, source) {
|
|
14987
|
-
const url2 = normalizeDispatchUrl(value);
|
|
14988
|
-
try {
|
|
14989
|
-
new URL(url2);
|
|
14990
|
-
return { url: url2, error: null };
|
|
14991
|
-
} catch {
|
|
14992
|
-
return { url: null, error: `${source} must be a valid URL` };
|
|
14993
|
-
}
|
|
14994
|
-
}
|
|
14995
|
-
var EnvoyFileSchema = exports_external.looseObject({
|
|
14996
|
-
$schema: exports_external.string().optional(),
|
|
14997
|
-
natsUrls: exports_external.array(exports_external.string()).optional(),
|
|
14998
|
-
dispatch: exports_external.strictObject({
|
|
14999
|
-
enabled: exports_external.boolean().optional(),
|
|
15000
|
-
serverUrl: exports_external.url().optional(),
|
|
15001
|
-
token: exports_external.string().optional()
|
|
15002
|
-
}).optional()
|
|
15003
|
-
});
|
|
15004
|
-
function describeSchemaIssue(filePath, error48) {
|
|
15005
|
-
const issue2 = error48.issues[0];
|
|
15006
|
-
if (!issue2)
|
|
15007
|
-
return `${filePath}: invalid dispatch config`;
|
|
15008
|
-
if (issue2.code === "unrecognized_keys") {
|
|
15009
|
-
const keys = issue2.keys.map((key) => `dispatch.${key}`).join(", ");
|
|
15010
|
-
return `${filePath}: unrecognized dispatch key(s): ${keys}`;
|
|
15011
|
-
}
|
|
15012
|
-
return `${filePath}: ${issue2.path.join(".")}: ${issue2.message}`;
|
|
15013
|
-
}
|
|
15014
|
-
function readEnvoyFile(filePath) {
|
|
15015
|
-
let raw;
|
|
15016
|
-
try {
|
|
15017
|
-
raw = readFileSync2(filePath, "utf-8");
|
|
15018
|
-
} catch {
|
|
15019
|
-
return { kind: "absent" };
|
|
15020
|
-
}
|
|
15021
|
-
let parsedJson;
|
|
15022
|
-
try {
|
|
15023
|
-
parsedJson = JSON.parse(raw);
|
|
15024
|
-
} catch (err) {
|
|
15025
|
-
return { kind: "invalid", reason: `${filePath}: invalid JSON (${messageFor(err)})` };
|
|
15026
|
-
}
|
|
15027
|
-
const parsed = EnvoyFileSchema.safeParse(parsedJson);
|
|
15028
|
-
if (!parsed.success) {
|
|
15029
|
-
return { kind: "invalid", reason: describeSchemaIssue(filePath, parsed.error) };
|
|
15030
|
-
}
|
|
15031
|
-
return { kind: "valid", settings: parsed.data.dispatch ?? null };
|
|
15032
|
-
}
|
|
15033
|
-
function resolveDispatchConfig(env, options = {}) {
|
|
15034
|
-
const explicitUrl = env.DISPATCH_URL;
|
|
15035
|
-
const home = options.home ?? env.HOME ?? homedir();
|
|
15036
|
-
const cwd = options.cwd ?? process.cwd();
|
|
15037
|
-
const userFile = readEnvoyFile(path.join(home, ".config", "opencode", "envoy.json"));
|
|
15038
|
-
const repoFile = readEnvoyFile(path.join(cwd, ".opencode", "envoy.json"));
|
|
15039
|
-
for (const file2 of [userFile, repoFile]) {
|
|
15040
|
-
if (file2.kind === "invalid") {
|
|
15041
|
-
return { enabled: false, url: null, token: null, error: file2.reason };
|
|
15042
|
-
}
|
|
15043
|
-
}
|
|
15044
|
-
const userSettings = userFile.kind === "valid" ? userFile.settings : null;
|
|
15045
|
-
const repoSettings = repoFile.kind === "valid" ? repoFile.settings : null;
|
|
15046
|
-
const merged = { ...userSettings, ...repoSettings };
|
|
15047
|
-
const repoURL = repoSettings?.serverUrl;
|
|
15048
|
-
if (explicitUrl === undefined && merged.enabled === true && repoURL !== undefined) {
|
|
15049
|
-
const token2 = repoSettings?.token ?? null;
|
|
15050
|
-
if (!token2) {
|
|
15051
|
-
return {
|
|
15052
|
-
enabled: false,
|
|
15053
|
-
url: null,
|
|
15054
|
-
token: null,
|
|
15055
|
-
error: "repository dispatch.serverUrl requires dispatch.token from the same .opencode/envoy.json or DISPATCH_URL with DISPATCH_TOKEN"
|
|
15056
|
-
};
|
|
15057
|
-
}
|
|
15058
|
-
const url3 = parsedDispatchUrl(repoURL, "repository dispatch.serverUrl");
|
|
15059
|
-
if (url3.error !== null)
|
|
15060
|
-
return { enabled: false, url: null, token: null, error: url3.error };
|
|
15061
|
-
return { enabled: true, url: url3.url, token: token2, error: null };
|
|
15062
|
-
}
|
|
15063
|
-
const rawUrl = explicitUrl !== undefined ? { value: explicitUrl, source: "DISPATCH_URL" } : merged.enabled === true ? { value: merged.serverUrl ?? DEFAULT_SERVER_URL, source: "dispatch.serverUrl" } : null;
|
|
15064
|
-
const url2 = rawUrl ? parsedDispatchUrl(rawUrl.value, rawUrl.source) : { url: null, error: null };
|
|
15065
|
-
let token;
|
|
15066
|
-
let tokenSource;
|
|
15067
|
-
if (env.DISPATCH_TOKEN_FILE !== undefined) {
|
|
15068
|
-
tokenSource = "DISPATCH_TOKEN_FILE";
|
|
15069
|
-
try {
|
|
15070
|
-
token = readSecretFile(tokenSource, env.DISPATCH_TOKEN_FILE);
|
|
15071
|
-
} catch (error48) {
|
|
15072
|
-
return { enabled: false, url: url2.url, token: null, error: messageFor(error48) };
|
|
15073
|
-
}
|
|
15074
|
-
} else if (env.DISPATCH_TOKEN !== undefined) {
|
|
15075
|
-
tokenSource = "DISPATCH_TOKEN";
|
|
15076
|
-
token = env.DISPATCH_TOKEN;
|
|
15077
|
-
} else {
|
|
15078
|
-
tokenSource = "dispatch.token";
|
|
15079
|
-
token = merged.token ?? null;
|
|
15080
|
-
}
|
|
15081
|
-
if (url2.error !== null)
|
|
15082
|
-
return { enabled: false, url: null, token, error: url2.error };
|
|
15083
|
-
if (url2.url === null)
|
|
15084
|
-
return { enabled: false, url: null, token, error: null };
|
|
15085
|
-
if (!token) {
|
|
15086
|
-
return {
|
|
15087
|
-
enabled: false,
|
|
15088
|
-
url: url2.url,
|
|
15089
|
-
token,
|
|
15090
|
-
error: `${tokenSource} must be a non-empty bearer token`
|
|
15091
|
-
};
|
|
15092
|
-
}
|
|
15093
|
-
return { enabled: true, url: url2.url, token, error: null };
|
|
15094
|
-
}
|
|
15095
|
-
function activeDispatchConfig(env, options = {}) {
|
|
15096
|
-
const config2 = resolveDispatchConfig(env, options);
|
|
15097
|
-
if (config2.error !== null)
|
|
15098
|
-
throw new Error(`dispatch config: ${config2.error}`);
|
|
15099
|
-
const { url: url2, token } = config2;
|
|
15100
|
-
if (!config2.enabled || url2 === null || token === null)
|
|
15101
|
-
return null;
|
|
15102
|
-
return { ...config2, url: url2, token };
|
|
15103
|
-
}
|
|
15104
|
-
|
|
15105
14961
|
// ../envoy-client/src/dispatch-execute.ts
|
|
15106
14962
|
import { resolve as resolvePath2 } from "path";
|
|
15107
14963
|
|
|
@@ -15337,13 +15193,13 @@ class DispatchClient {
|
|
|
15337
15193
|
return this.#resolveIssue(issueReference);
|
|
15338
15194
|
}
|
|
15339
15195
|
async listIssuePage(options, page) {
|
|
15340
|
-
const
|
|
15196
|
+
const path = ["api", "v1", "issues"];
|
|
15341
15197
|
const query = { ...options, limit: page.limit, offset: page.offset };
|
|
15342
|
-
const answer = await this.#json("GET",
|
|
15198
|
+
const answer = await this.#json("GET", path, undefined, query);
|
|
15343
15199
|
const refusal = whyNotAPage(answer);
|
|
15344
15200
|
if (refusal === undefined)
|
|
15345
15201
|
return answer;
|
|
15346
|
-
throw new Error(`GET ${this.#url(
|
|
15202
|
+
throw new Error(`GET ${this.#url(path, query)} asked for a page ({issues, total, limit, offset}) and got ` + refusal);
|
|
15347
15203
|
}
|
|
15348
15204
|
async listProjectArtifacts(project, unlinked = false) {
|
|
15349
15205
|
return this.#json("GET", ["api", "v1", "projects", project, "artifacts"], undefined, unlinked ? { unlinked: "true" } : undefined);
|
|
@@ -15584,17 +15440,17 @@ class DispatchClient {
|
|
|
15584
15440
|
throw error48;
|
|
15585
15441
|
}
|
|
15586
15442
|
}
|
|
15587
|
-
async#json(method,
|
|
15588
|
-
return (await this.#jsonAnswer(method,
|
|
15443
|
+
async#json(method, path, body, query) {
|
|
15444
|
+
return (await this.#jsonAnswer(method, path, body, query)).payload;
|
|
15589
15445
|
}
|
|
15590
|
-
async#jsonAnswer(method,
|
|
15446
|
+
async#jsonAnswer(method, path, body, query) {
|
|
15591
15447
|
const headers = {
|
|
15592
15448
|
Accept: "application/json",
|
|
15593
15449
|
Authorization: `Bearer ${this.token}`
|
|
15594
15450
|
};
|
|
15595
15451
|
if (body !== undefined)
|
|
15596
15452
|
headers["Content-Type"] = "application/json";
|
|
15597
|
-
const url2 = this.#url(
|
|
15453
|
+
const url2 = this.#url(path, query);
|
|
15598
15454
|
const response = await this.fetchImpl(url2, {
|
|
15599
15455
|
method,
|
|
15600
15456
|
headers,
|
|
@@ -15603,9 +15459,9 @@ class DispatchClient {
|
|
|
15603
15459
|
});
|
|
15604
15460
|
return { status: response.status, payload: await this.#response(method, url2, response) };
|
|
15605
15461
|
}
|
|
15606
|
-
async#upload(
|
|
15462
|
+
async#upload(path, input) {
|
|
15607
15463
|
if ("content" in input)
|
|
15608
|
-
return this.#json("POST",
|
|
15464
|
+
return this.#json("POST", path, input);
|
|
15609
15465
|
const form = new FormData;
|
|
15610
15466
|
form.set("name", input.name);
|
|
15611
15467
|
if (input.summary !== undefined)
|
|
@@ -15613,10 +15469,10 @@ class DispatchClient {
|
|
|
15613
15469
|
if (input.actor !== undefined)
|
|
15614
15470
|
form.set("actor", JSON.stringify(input.actor));
|
|
15615
15471
|
form.set("file", input.file, input.name);
|
|
15616
|
-
return this.#form("POST",
|
|
15472
|
+
return this.#form("POST", path, form);
|
|
15617
15473
|
}
|
|
15618
|
-
async#form(method,
|
|
15619
|
-
const url2 = this.#url(
|
|
15474
|
+
async#form(method, path, body) {
|
|
15475
|
+
const url2 = this.#url(path);
|
|
15620
15476
|
const response = await this.fetchImpl(url2, {
|
|
15621
15477
|
method,
|
|
15622
15478
|
headers: { Accept: "application/json", Authorization: `Bearer ${this.token}` },
|
|
@@ -15625,8 +15481,8 @@ class DispatchClient {
|
|
|
15625
15481
|
});
|
|
15626
15482
|
return this.#response(method, url2, response);
|
|
15627
15483
|
}
|
|
15628
|
-
#url(
|
|
15629
|
-
const url2 = new URL(`${
|
|
15484
|
+
#url(path, query) {
|
|
15485
|
+
const url2 = new URL(`${path.map((segment) => encodeURIComponent(segment)).join("/")}`, `${this.#baseUrl}/`);
|
|
15630
15486
|
if (query) {
|
|
15631
15487
|
for (const [name, value] of Object.entries(query)) {
|
|
15632
15488
|
if (typeof value === "string" || typeof value === "number") {
|
|
@@ -15852,9 +15708,9 @@ function shapeOf(schema) {
|
|
|
15852
15708
|
const def = unwrapped.def;
|
|
15853
15709
|
return def.type === "object" && "shape" in def ? def.shape : undefined;
|
|
15854
15710
|
}
|
|
15855
|
-
function schemaAt(schema,
|
|
15711
|
+
function schemaAt(schema, path) {
|
|
15856
15712
|
let current = schema;
|
|
15857
|
-
for (const key of
|
|
15713
|
+
for (const key of path) {
|
|
15858
15714
|
const unwrapped = unwrap(current);
|
|
15859
15715
|
if (unwrapped === undefined)
|
|
15860
15716
|
return;
|
|
@@ -15901,10 +15757,10 @@ function describeExpected(expected, schema) {
|
|
|
15901
15757
|
return `a ${expected}`;
|
|
15902
15758
|
}
|
|
15903
15759
|
}
|
|
15904
|
-
function pathText(
|
|
15905
|
-
const [head, ...rest] =
|
|
15760
|
+
function pathText(path, cliTool) {
|
|
15761
|
+
const [head, ...rest] = path.map(String);
|
|
15906
15762
|
if (cliTool === undefined || head === undefined)
|
|
15907
|
-
return
|
|
15763
|
+
return path.map(String).join(".");
|
|
15908
15764
|
let flag = fieldFlag(cliTool, head);
|
|
15909
15765
|
let index = 0;
|
|
15910
15766
|
for (;index < rest.length && /^\d+$/.test(rest[index]); index++) {
|
|
@@ -15917,33 +15773,33 @@ function formatZodIssues(issues, schema, tool) {
|
|
|
15917
15773
|
const cliTool = isDispatchTool(tool) ? tool : undefined;
|
|
15918
15774
|
const allowed = Object.keys(shapeOf(schema) ?? {}).join(", ");
|
|
15919
15775
|
return issues.flatMap((issue2) => {
|
|
15920
|
-
const
|
|
15776
|
+
const path = pathText(issue2.path, cliTool);
|
|
15921
15777
|
switch (issue2.code) {
|
|
15922
15778
|
case "invalid_type":
|
|
15923
|
-
return issue2.input === undefined ? [`${
|
|
15924
|
-
`${
|
|
15779
|
+
return issue2.input === undefined ? [`${path} is required (${issue2.expected})`] : [
|
|
15780
|
+
`${path} must be ${describeExpected(issue2.expected, schemaAt(schema, issue2.path))}, not ${describeInput(issue2.input)}`
|
|
15925
15781
|
];
|
|
15926
15782
|
case "unrecognized_keys": {
|
|
15927
15783
|
if (cliTool !== undefined && issue2.path.length === 0) {
|
|
15928
15784
|
return issue2.keys.map((key) => `unknown flag ${fieldFlag(cliTool, key)}`);
|
|
15929
15785
|
}
|
|
15930
15786
|
const here = Object.keys(shapeOf(schemaAt(schema, issue2.path)) ?? {}).join(", ") || allowed;
|
|
15931
|
-
const where =
|
|
15787
|
+
const where = path === "" ? "" : ` in ${path}`;
|
|
15932
15788
|
return issue2.keys.map((key) => `unknown field "${key}"${where}; allowed: ${here}`);
|
|
15933
15789
|
}
|
|
15934
15790
|
case "invalid_value":
|
|
15935
15791
|
return [
|
|
15936
|
-
`${
|
|
15792
|
+
`${path} must be one of ${issue2.values.map(String).join("|")}; got ${JSON.stringify(issue2.input)}`
|
|
15937
15793
|
];
|
|
15938
15794
|
case "too_big":
|
|
15939
15795
|
if (issue2.origin === "array" && Array.isArray(issue2.input)) {
|
|
15940
|
-
return [`${
|
|
15796
|
+
return [`${path} has ${issue2.input.length} items; the limit is ${issue2.maximum}`];
|
|
15941
15797
|
}
|
|
15942
|
-
return [`${
|
|
15798
|
+
return [`${path} ${issue2.message}`];
|
|
15943
15799
|
case "custom":
|
|
15944
15800
|
return [issue2.message];
|
|
15945
15801
|
default:
|
|
15946
|
-
return [
|
|
15802
|
+
return [path === "" ? issue2.message : `${path}: ${issue2.message}`];
|
|
15947
15803
|
}
|
|
15948
15804
|
});
|
|
15949
15805
|
}
|
|
@@ -15953,7 +15809,7 @@ var PICTURE_TYPE_NAMES = "a PNG, JPEG, GIF or WebP";
|
|
|
15953
15809
|
async function localPictures(images, cwd, problems) {
|
|
15954
15810
|
if (!Array.isArray(images))
|
|
15955
15811
|
return [];
|
|
15956
|
-
const checked = await Promise.all(images.map((
|
|
15812
|
+
const checked = await Promise.all(images.map((path) => localPicture(path, cwd)));
|
|
15957
15813
|
const pictures = [];
|
|
15958
15814
|
for (const outcome of checked) {
|
|
15959
15815
|
if (typeof outcome === "string")
|
|
@@ -15963,30 +15819,30 @@ async function localPictures(images, cwd, problems) {
|
|
|
15963
15819
|
}
|
|
15964
15820
|
return pictures;
|
|
15965
15821
|
}
|
|
15966
|
-
async function localPicture(
|
|
15967
|
-
if (typeof
|
|
15822
|
+
async function localPicture(path, cwd) {
|
|
15823
|
+
if (typeof path !== "string" || path === "")
|
|
15968
15824
|
return;
|
|
15969
|
-
const absolute = resolvePath(cwd,
|
|
15825
|
+
const absolute = resolvePath(cwd, path);
|
|
15970
15826
|
const file2 = Bun.file(absolute);
|
|
15971
15827
|
let size;
|
|
15972
15828
|
let head;
|
|
15973
15829
|
try {
|
|
15974
15830
|
const stats = await file2.stat();
|
|
15975
15831
|
if (!stats.isFile())
|
|
15976
|
-
return `images: ${
|
|
15832
|
+
return `images: ${path} is not a file`;
|
|
15977
15833
|
size = stats.size;
|
|
15978
15834
|
head = new Uint8Array(await file2.slice(0, PICTURE_SNIFF_BYTES).arrayBuffer());
|
|
15979
15835
|
} catch (error48) {
|
|
15980
|
-
return `images: ${
|
|
15836
|
+
return `images: ${path} cannot be read: ${messageFor(error48)}`;
|
|
15981
15837
|
}
|
|
15982
15838
|
if (size > PICTURE_UPLOAD_MAX_BYTES) {
|
|
15983
|
-
return `images: ${
|
|
15839
|
+
return `images: ${path} is ${size.toLocaleString("en-US")} bytes, over the ${PICTURE_UPLOAD_MAX_BYTES / 1024 / 1024} MiB one Dispatch upload takes`;
|
|
15984
15840
|
}
|
|
15985
15841
|
const type = sniffPictureType(head);
|
|
15986
15842
|
if (type === undefined) {
|
|
15987
|
-
return `images: ${
|
|
15843
|
+
return `images: ${path} is not ${PICTURE_TYPE_NAMES} picture (judged by its bytes, not its name)`;
|
|
15988
15844
|
}
|
|
15989
|
-
return { path
|
|
15845
|
+
return { path, name: basename(absolute), file: Bun.file(absolute, { type }) };
|
|
15990
15846
|
}
|
|
15991
15847
|
function uploadSlug(name) {
|
|
15992
15848
|
const slug = name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
@@ -16929,7 +16785,7 @@ function approvalLine(artifact) {
|
|
|
16929
16785
|
case "approved":
|
|
16930
16786
|
return `Approval: approved v${approval.version} by ${approval.by?.id ?? "unknown"}`;
|
|
16931
16787
|
case "stale":
|
|
16932
|
-
return `Approval: approved v${approval.version} by ${approval.by?.id ?? "unknown"},
|
|
16788
|
+
return `Approval: approved v${approval.version} by ${approval.by?.id ?? "unknown"}, changed since (now v${approval.latest_version}) - the human's own answer to a decision block carries the approval when that answer is the whole change since v${approval.version}; for any other change, request approval again once the human has agreed to every point in this version, with a --summary naming what changed since v${approval.version}`;
|
|
16933
16789
|
case "changes_requested":
|
|
16934
16790
|
return `Approval: changes requested on v${approval.version} by ${approval.by?.id ?? "unknown"}: ${approval.reason ?? ""}`;
|
|
16935
16791
|
}
|
|
@@ -17226,11 +17082,11 @@ function anchorLines(record2) {
|
|
|
17226
17082
|
];
|
|
17227
17083
|
}
|
|
17228
17084
|
function positionText(block) {
|
|
17229
|
-
const { table, path
|
|
17230
|
-
const segments =
|
|
17085
|
+
const { table, path } = block;
|
|
17086
|
+
const segments = path.map((entry) => `${entry.type}[${entry.index}]`);
|
|
17231
17087
|
if (table === undefined || table.row === null)
|
|
17232
17088
|
return segments.join(" \u203A ");
|
|
17233
|
-
const tableAt =
|
|
17089
|
+
const tableAt = path.findIndex((entry) => entry.type === "table");
|
|
17234
17090
|
const cells = table.cells ?? [];
|
|
17235
17091
|
const label = (table.column === null ? cells : cells.slice(0, table.column)).map((cell) => cell.trim()).filter((cell) => cell !== "" && !/^\d+$/.test(cell)).join(" \xB7 ");
|
|
17236
17092
|
const row = label === "" ? `row ${table.row}` : `row ${table.row} (${label})`;
|
|
@@ -17378,7 +17234,7 @@ async function refuseOpenDecisionBlocks(client, tool, resolved) {
|
|
|
17378
17234
|
const asks = new Map(documentAsks.map((ask) => [ask.block_id, ask]));
|
|
17379
17235
|
const lines = version2.markdown.split(`
|
|
17380
17236
|
`);
|
|
17381
|
-
const stillOpen = (named, state) => state === "answered" ? `${named}, answered but still open in version ${latest}: it settles into a version on its own in about two seconds; wait
|
|
17237
|
+
const stillOpen = (named, state) => state === "answered" ? `${named}, answered but still open in version ${latest}: it settles into a version on its own in about two seconds; wait for that version` : `${named}, ${state} but still open in version ${latest}: write the decision into the text with dispatch doc-edit, which writes a version that carries it`;
|
|
17382
17238
|
const open = blocks.flatMap((block) => {
|
|
17383
17239
|
const ask = asks.get(block.id);
|
|
17384
17240
|
const openers = lines.filter((line) => line.includes(`ask{#${block.id} `) || line.includes(`ask{#${block.id}}`));
|
|
@@ -17407,7 +17263,7 @@ async function refuseOpenDecisionBlocks(client, tool, resolved) {
|
|
|
17407
17263
|
throw new Error([
|
|
17408
17264
|
`dispatch ${commandName(tool)} was not called: ${artifact.name} (version ${latest}) has ${count}. Answering one writes a new version, which would move this request to that version and leave it waiting on you.`,
|
|
17409
17265
|
...open.map((line) => `- ${line}`),
|
|
17410
|
-
"Do not request approval over an open block, even when a human asked for it. Tell the human which block is open and ask them to answer it or to waive it. Once it is answered, it settles into a version on its own in about two seconds
|
|
17266
|
+
"Do not request approval over an open block, even when a human asked for it. Tell the human which block is open and ask them to answer it or to waive it. Once it is answered, it settles into a version on its own in about two seconds. When the human approved an earlier version and their answer is the whole change since it, Dispatch carries the approval to the new version in their name: request nothing. When the answer changes design text outside the block, rewrite that text (never to restate the answer), then request approval again once the human has agreed to every point in the new version, with a one-sentence --summary naming what changed since the earlier approval. On a document nobody has approved, wait for that version, then request. If they waive it, close the block with dispatch resolve-ask (--kind resolved, their words as the --reason), write their decision into the text with dispatch doc-edit, then request approval again once the human has agreed to every point in the new version. The call opens the request, or hands an open one back to the human."
|
|
17411
17267
|
].join(`
|
|
17412
17268
|
`));
|
|
17413
17269
|
}
|
|
@@ -17471,11 +17327,7 @@ function withAccount(error48, account) {
|
|
|
17471
17327
|
return new Error(told, { cause: error48 });
|
|
17472
17328
|
}
|
|
17473
17329
|
async function executeDispatchTool(input) {
|
|
17474
|
-
const configUrl = input.config
|
|
17475
|
-
const configToken = input.config.token;
|
|
17476
|
-
if (!input.config.enabled || !configUrl || !configToken) {
|
|
17477
|
-
throw new Error("Dispatch is disabled; resolve both DISPATCH_URL and DISPATCH_TOKEN");
|
|
17478
|
-
}
|
|
17330
|
+
const { url: configUrl, token: configToken } = input.config;
|
|
17479
17331
|
const baseFetch = input.fetchImpl ?? fetch;
|
|
17480
17332
|
const fetchImpl = Object.assign(async (...args2) => {
|
|
17481
17333
|
try {
|
|
@@ -18202,7 +18054,7 @@ ${trailer.join(`
|
|
|
18202
18054
|
});
|
|
18203
18055
|
if (result.ask === null) {
|
|
18204
18056
|
return {
|
|
18205
|
-
text: `${resolved.artifact.name} (document id ${resolved.artifact.id}) is already approved at version ${result.version} by ${result.approval.by?.id ?? "unknown"}; no new request was opened.
|
|
18057
|
+
text: `${resolved.artifact.name} (document id ${resolved.artifact.id}) is already approved at version ${result.version} by ${result.approval.by?.id ?? "unknown"}; no new request was opened. A change outside the decision blocks after approval makes it stale, so request again only for such a version, once the human has agreed to every point in it, with a --summary naming what changed since version ${result.version}; the human's own answer to a decision block carries the approval when that answer is the whole change.`,
|
|
18206
18058
|
details: {
|
|
18207
18059
|
...resolved.owner.kind === "project" ? documentResultDetails(resolved.artifact) : { issue: resolved.issue?.key },
|
|
18208
18060
|
artifact: resolved.artifact.id,
|
|
@@ -18213,7 +18065,7 @@ ${trailer.join(`
|
|
|
18213
18065
|
const details = await followedAskDetails(client, result.ask, resolved.artifact);
|
|
18214
18066
|
const outcome = result.recorded ? `Approval requested for ${resolved.artifact.name} (document id ${resolved.artifact.id}) at version ${result.version} (ask ${result.ask.id}).` : `The approval request for ${resolved.artifact.name} (document id ${resolved.artifact.id}) at version ${result.version} (ask ${result.ask.id}) already waits on the human, so this call changed nothing: nothing since it last reached the human (a newer version, a human's reply in its thread, or your progress note) left it waiting on you.`;
|
|
18215
18067
|
return {
|
|
18216
|
-
text: `${outcome} The human's Inbox asks: ${JSON.stringify(result.ask.question)}. The answer arrives as artifact.approved or artifact.changes_requested. An edit before the answer moves this request to the new version and leaves it waiting on you, and
|
|
18068
|
+
text: `${outcome} The human's Inbox asks: ${JSON.stringify(result.ask.question)}. The answer arrives as artifact.approved or artifact.changes_requested. An edit before the answer moves this request to the new version and leaves it waiting on you, and a change outside the decision blocks after approval makes the approval stale: either way, request again for the new version once the human has agreed to every point in it, which hands this request back or opens a new one, with a --summary naming what changed since the earlier approval. The human's own answer to a decision block carries the approval when that answer is the whole change since the approved version, and answers an open request Approve in their name: request nothing then.`,
|
|
18217
18069
|
details: { ...details, artifact: resolved.artifact.id, version: result.version }
|
|
18218
18070
|
};
|
|
18219
18071
|
}
|
|
@@ -18428,15 +18280,15 @@ import {
|
|
|
18428
18280
|
mkdirSync,
|
|
18429
18281
|
openSync,
|
|
18430
18282
|
readdirSync,
|
|
18431
|
-
readFileSync
|
|
18283
|
+
readFileSync,
|
|
18432
18284
|
readSync,
|
|
18433
18285
|
renameSync,
|
|
18434
18286
|
rmSync,
|
|
18435
18287
|
statSync,
|
|
18436
18288
|
writeFileSync
|
|
18437
18289
|
} from "fs";
|
|
18438
|
-
import { homedir
|
|
18439
|
-
import { join
|
|
18290
|
+
import { homedir } from "os";
|
|
18291
|
+
import { join } from "path";
|
|
18440
18292
|
var SESSION_ID = /^[A-Za-z0-9._-]{1,128}$/;
|
|
18441
18293
|
var PRIVATE_DIR = 448;
|
|
18442
18294
|
var PRIVATE_FILE = 384;
|
|
@@ -18452,44 +18304,44 @@ var PICTURE_EXTENSIONS = {
|
|
|
18452
18304
|
function sessionStateRoot(env) {
|
|
18453
18305
|
if (env.DISPATCH_STATE_DIR)
|
|
18454
18306
|
return env.DISPATCH_STATE_DIR;
|
|
18455
|
-
return
|
|
18307
|
+
return join(env.XDG_STATE_HOME || join(homedir(), ".local", "state"), "dispatch");
|
|
18456
18308
|
}
|
|
18457
18309
|
function sessionDirectory(env, sessionId) {
|
|
18458
18310
|
if (!SESSION_ID.test(sessionId) || sessionId === "." || sessionId === "..") {
|
|
18459
18311
|
throw new Error(`dispatch: the session id ${JSON.stringify(sessionId)} is not 1-128 letters, digits, ".", "_" or "-"`);
|
|
18460
18312
|
}
|
|
18461
|
-
return
|
|
18313
|
+
return join(sessionStateRoot(env), "sessions", sessionId);
|
|
18462
18314
|
}
|
|
18463
18315
|
function ensureDirectory(dir) {
|
|
18464
18316
|
mkdirSync(dir, { recursive: true, mode: PRIVATE_DIR });
|
|
18465
18317
|
}
|
|
18466
|
-
function readIfPresent(
|
|
18318
|
+
function readIfPresent(path) {
|
|
18467
18319
|
try {
|
|
18468
|
-
return
|
|
18320
|
+
return readFileSync(path, "utf-8");
|
|
18469
18321
|
} catch (error48) {
|
|
18470
18322
|
if (hasErrnoCode(error48, "ENOENT"))
|
|
18471
18323
|
return;
|
|
18472
18324
|
throw error48;
|
|
18473
18325
|
}
|
|
18474
18326
|
}
|
|
18475
|
-
function writeAtomically(
|
|
18476
|
-
const temporary = `${
|
|
18327
|
+
function writeAtomically(path, text) {
|
|
18328
|
+
const temporary = `${path}.${process.pid}.${Date.now()}.tmp`;
|
|
18477
18329
|
writeFileSync(temporary, text, { mode: PRIVATE_FILE });
|
|
18478
|
-
renameSync(temporary,
|
|
18330
|
+
renameSync(temporary, path);
|
|
18479
18331
|
}
|
|
18480
18332
|
function stringList(value) {
|
|
18481
18333
|
return Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
|
|
18482
18334
|
}
|
|
18483
18335
|
function loadSessionMemory(dir, sessionId) {
|
|
18484
|
-
const
|
|
18485
|
-
const text = readIfPresent(
|
|
18336
|
+
const path = join(dir, "state.json");
|
|
18337
|
+
const text = readIfPresent(path);
|
|
18486
18338
|
if (text === undefined)
|
|
18487
18339
|
return new Set;
|
|
18488
18340
|
let state;
|
|
18489
18341
|
try {
|
|
18490
18342
|
state = JSON.parse(text);
|
|
18491
18343
|
} catch (error48) {
|
|
18492
|
-
throw new Error(`the session state ${
|
|
18344
|
+
throw new Error(`the session state ${path} is not JSON (${error48.message}); remove it to start this session's memory over`);
|
|
18493
18345
|
}
|
|
18494
18346
|
const advice = adviceMemory();
|
|
18495
18347
|
for (const key of stringList(state.advice))
|
|
@@ -18505,12 +18357,12 @@ function writeMemory(dir, sessionId, follows) {
|
|
|
18505
18357
|
pictures: [...shownPictures(sessionId)],
|
|
18506
18358
|
follows: [...follows]
|
|
18507
18359
|
};
|
|
18508
|
-
writeAtomically(
|
|
18360
|
+
writeAtomically(join(dir, "state.json"), `${JSON.stringify(state)}
|
|
18509
18361
|
`);
|
|
18510
18362
|
}
|
|
18511
18363
|
function writeResult(dir, entry) {
|
|
18512
18364
|
const line = { at: new Date().toISOString(), ...entry };
|
|
18513
|
-
appendFileSync(
|
|
18365
|
+
appendFileSync(join(dir, "results.jsonl"), `${JSON.stringify(line)}
|
|
18514
18366
|
`, { mode: PRIVATE_FILE });
|
|
18515
18367
|
}
|
|
18516
18368
|
function recordOutcome(dir, sessionId, follows, entry) {
|
|
@@ -18521,29 +18373,29 @@ function recordOutcome(dir, sessionId, follows, entry) {
|
|
|
18521
18373
|
function writePicture(dir, image) {
|
|
18522
18374
|
const bytes = Buffer.from(image.data, "base64");
|
|
18523
18375
|
const name = createHash("sha256").update(bytes).digest("hex").slice(0, 16);
|
|
18524
|
-
const pictures =
|
|
18525
|
-
const
|
|
18376
|
+
const pictures = join(dir, "pictures");
|
|
18377
|
+
const path = join(pictures, `${name}.${PICTURE_EXTENSIONS[image.mimeType]}`);
|
|
18526
18378
|
ensureDirectory(pictures);
|
|
18527
18379
|
try {
|
|
18528
|
-
writeFileSync(
|
|
18380
|
+
writeFileSync(path, bytes, { mode: PRIVATE_FILE, flag: "wx" });
|
|
18529
18381
|
} catch (error48) {
|
|
18530
18382
|
if (!hasErrnoCode(error48, "EEXIST"))
|
|
18531
18383
|
throw error48;
|
|
18532
18384
|
}
|
|
18533
|
-
return { path
|
|
18385
|
+
return { path, bytes: bytes.length };
|
|
18534
18386
|
}
|
|
18535
18387
|
function writeLongOutput(dir, text) {
|
|
18536
|
-
const out =
|
|
18388
|
+
const out = join(dir, "out");
|
|
18537
18389
|
ensureDirectory(out);
|
|
18538
|
-
const
|
|
18539
|
-
writeFileSync(
|
|
18540
|
-
return
|
|
18390
|
+
const path = join(out, `${new Date().toISOString().replaceAll(":", "-")}-${process.pid}.md`);
|
|
18391
|
+
writeFileSync(path, text, { mode: PRIVATE_FILE });
|
|
18392
|
+
return path;
|
|
18541
18393
|
}
|
|
18542
18394
|
function readSessionTitle(dir) {
|
|
18543
|
-
return readIfPresent(
|
|
18395
|
+
return readIfPresent(join(dir, "title")) || undefined;
|
|
18544
18396
|
}
|
|
18545
18397
|
function pruneSessions(root, now) {
|
|
18546
|
-
const stamp =
|
|
18398
|
+
const stamp = join(root, PRUNED_STAMP);
|
|
18547
18399
|
try {
|
|
18548
18400
|
if (now - statSync(stamp).mtimeMs < PRUNE_INTERVAL_MS)
|
|
18549
18401
|
return;
|
|
@@ -18551,7 +18403,7 @@ function pruneSessions(root, now) {
|
|
|
18551
18403
|
if (!hasErrnoCode(error48, "ENOENT"))
|
|
18552
18404
|
throw error48;
|
|
18553
18405
|
}
|
|
18554
|
-
const sessions =
|
|
18406
|
+
const sessions = join(root, "sessions");
|
|
18555
18407
|
let entries = [];
|
|
18556
18408
|
try {
|
|
18557
18409
|
entries = readdirSync(sessions, { withFileTypes: true });
|
|
@@ -18562,7 +18414,7 @@ function pruneSessions(root, now) {
|
|
|
18562
18414
|
for (const entry of entries) {
|
|
18563
18415
|
if (!entry.isDirectory())
|
|
18564
18416
|
continue;
|
|
18565
|
-
const dir =
|
|
18417
|
+
const dir = join(sessions, entry.name);
|
|
18566
18418
|
try {
|
|
18567
18419
|
if (now - statSync(dir).mtimeMs > IDLE_SESSION_MS) {
|
|
18568
18420
|
rmSync(dir, { recursive: true, force: true });
|
|
@@ -18603,11 +18455,575 @@ function dispatchFollowNotice(details) {
|
|
|
18603
18455
|
};
|
|
18604
18456
|
}
|
|
18605
18457
|
|
|
18458
|
+
// ../envoy-client/src/envoy-config.ts
|
|
18459
|
+
import {
|
|
18460
|
+
chmodSync,
|
|
18461
|
+
closeSync as closeSync2,
|
|
18462
|
+
existsSync,
|
|
18463
|
+
lstatSync,
|
|
18464
|
+
mkdtempSync,
|
|
18465
|
+
openSync as openSync2,
|
|
18466
|
+
readFileSync as readFileSync3,
|
|
18467
|
+
readlinkSync,
|
|
18468
|
+
rmSync as rmSync2,
|
|
18469
|
+
statSync as statSync2
|
|
18470
|
+
} from "fs";
|
|
18471
|
+
import { homedir as homedir2, tmpdir } from "os";
|
|
18472
|
+
import * as path from "path";
|
|
18473
|
+
|
|
18474
|
+
// ../envoy-client/src/json-position.ts
|
|
18475
|
+
function jsonErrorPosition(text) {
|
|
18476
|
+
let at = 0;
|
|
18477
|
+
const fail = () => {
|
|
18478
|
+
throw new RangeError("not JSON");
|
|
18479
|
+
};
|
|
18480
|
+
const space = () => {
|
|
18481
|
+
while (at < text.length && `
|
|
18482
|
+
\r`.includes(text.charAt(at)))
|
|
18483
|
+
at += 1;
|
|
18484
|
+
};
|
|
18485
|
+
const expect = (char) => {
|
|
18486
|
+
if (text.charAt(at) !== char)
|
|
18487
|
+
fail();
|
|
18488
|
+
at += 1;
|
|
18489
|
+
};
|
|
18490
|
+
const string4 = () => {
|
|
18491
|
+
expect('"');
|
|
18492
|
+
while (at < text.length) {
|
|
18493
|
+
const char = text.charAt(at);
|
|
18494
|
+
if (char === '"') {
|
|
18495
|
+
at += 1;
|
|
18496
|
+
return;
|
|
18497
|
+
}
|
|
18498
|
+
if (char < " ")
|
|
18499
|
+
fail();
|
|
18500
|
+
at += char === "\\" ? 2 : 1;
|
|
18501
|
+
}
|
|
18502
|
+
fail();
|
|
18503
|
+
};
|
|
18504
|
+
const value = () => {
|
|
18505
|
+
space();
|
|
18506
|
+
const char = text.charAt(at);
|
|
18507
|
+
if (char === "{" || char === "[") {
|
|
18508
|
+
const close = char === "{" ? "}" : "]";
|
|
18509
|
+
at += 1;
|
|
18510
|
+
space();
|
|
18511
|
+
if (text.charAt(at) === close) {
|
|
18512
|
+
at += 1;
|
|
18513
|
+
return;
|
|
18514
|
+
}
|
|
18515
|
+
for (;; ) {
|
|
18516
|
+
if (close === "}") {
|
|
18517
|
+
space();
|
|
18518
|
+
string4();
|
|
18519
|
+
space();
|
|
18520
|
+
expect(":");
|
|
18521
|
+
}
|
|
18522
|
+
value();
|
|
18523
|
+
space();
|
|
18524
|
+
if (text.charAt(at) === ",") {
|
|
18525
|
+
at += 1;
|
|
18526
|
+
continue;
|
|
18527
|
+
}
|
|
18528
|
+
expect(close);
|
|
18529
|
+
return;
|
|
18530
|
+
}
|
|
18531
|
+
}
|
|
18532
|
+
if (char === '"') {
|
|
18533
|
+
string4();
|
|
18534
|
+
return;
|
|
18535
|
+
}
|
|
18536
|
+
for (const word of ["true", "false", "null"]) {
|
|
18537
|
+
if (text.startsWith(word, at)) {
|
|
18538
|
+
at += word.length;
|
|
18539
|
+
return;
|
|
18540
|
+
}
|
|
18541
|
+
}
|
|
18542
|
+
const number4 = /^-?(0|[1-9][0-9]*)(\.[0-9]+)?([eE][+-]?[0-9]+)?/.exec(text.slice(at));
|
|
18543
|
+
if (number4 === null)
|
|
18544
|
+
fail();
|
|
18545
|
+
at += number4?.[0].length ?? 0;
|
|
18546
|
+
};
|
|
18547
|
+
try {
|
|
18548
|
+
value();
|
|
18549
|
+
space();
|
|
18550
|
+
if (at < text.length)
|
|
18551
|
+
fail();
|
|
18552
|
+
return;
|
|
18553
|
+
} catch {
|
|
18554
|
+
const before = text.slice(0, at).split(`
|
|
18555
|
+
`);
|
|
18556
|
+
return `line ${before.length}, column ${(before.at(-1)?.length ?? 0) + 1}`;
|
|
18557
|
+
}
|
|
18558
|
+
}
|
|
18559
|
+
|
|
18560
|
+
// ../envoy-client/src/secret-file.ts
|
|
18561
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
18562
|
+
function sendable(value) {
|
|
18563
|
+
return !/[\s\p{Cc}]/u.test(value);
|
|
18564
|
+
}
|
|
18565
|
+
function readSecretFile(variable, filePath) {
|
|
18566
|
+
let contents;
|
|
18567
|
+
try {
|
|
18568
|
+
contents = readFileSync2(filePath, "utf8");
|
|
18569
|
+
} catch (error48) {
|
|
18570
|
+
throw new Error(`${variable} names ${filePath}, which could not be read: ${messageFor(error48)}`);
|
|
18571
|
+
}
|
|
18572
|
+
const value = contents.trim();
|
|
18573
|
+
if (value.length === 0)
|
|
18574
|
+
throw new Error(`${variable} names ${filePath}, which is empty`);
|
|
18575
|
+
if (!sendable(value)) {
|
|
18576
|
+
throw new Error(`${variable} names ${filePath}, which holds whitespace or a control character`);
|
|
18577
|
+
}
|
|
18578
|
+
return value;
|
|
18579
|
+
}
|
|
18580
|
+
|
|
18581
|
+
// ../envoy-client/src/envoy-config.ts
|
|
18582
|
+
var COMMAND_TIMEOUT_MS = 1e4;
|
|
18583
|
+
function nonEmpty(value) {
|
|
18584
|
+
return value === undefined || value === "" ? undefined : value;
|
|
18585
|
+
}
|
|
18586
|
+
function absolute(name, value) {
|
|
18587
|
+
if (value !== undefined && !path.isAbsolute(value)) {
|
|
18588
|
+
throw new Error(`${name} is ${value}, which is not an absolute path`);
|
|
18589
|
+
}
|
|
18590
|
+
return value;
|
|
18591
|
+
}
|
|
18592
|
+
function neutralConfig(env) {
|
|
18593
|
+
const xdgConfigHome = nonEmpty(env.XDG_CONFIG_HOME);
|
|
18594
|
+
if (xdgConfigHome !== undefined && !path.isAbsolute(xdgConfigHome)) {
|
|
18595
|
+
return { error: `XDG_CONFIG_HOME is ${xdgConfigHome}, which is not an absolute path` };
|
|
18596
|
+
}
|
|
18597
|
+
const configHome = xdgConfigHome ?? path.join(nonEmpty(env.HOME) ?? homedir2(), ".config");
|
|
18598
|
+
return { path: path.resolve(configHome, "legion", "envoy.json") };
|
|
18599
|
+
}
|
|
18600
|
+
var HARNESS_DIRECTORY_VARIABLE = {
|
|
18601
|
+
omp: "PI_CODING_AGENT_DIR",
|
|
18602
|
+
claude: "CLAUDE_CONFIG_DIR",
|
|
18603
|
+
opencode: "OPENCODE_CONFIG_DIR"
|
|
18604
|
+
};
|
|
18605
|
+
var PLUGIN_HARNESSES = Object.keys(HARNESS_DIRECTORY_VARIABLE);
|
|
18606
|
+
function namedDirectory(harness, env) {
|
|
18607
|
+
if (harness === "omp") {
|
|
18608
|
+
const profile = nonEmpty(env.OMP_PROFILE);
|
|
18609
|
+
if (profile === undefined || profile === "default")
|
|
18610
|
+
return;
|
|
18611
|
+
}
|
|
18612
|
+
return nonEmpty(env[HARNESS_DIRECTORY_VARIABLE[harness]]);
|
|
18613
|
+
}
|
|
18614
|
+
function harnessDirectory(harness, env) {
|
|
18615
|
+
for (const plugin of harness === "command" ? PLUGIN_HARNESSES : [harness]) {
|
|
18616
|
+
if (harness === "command" && plugin === "claude" && env.CLAUDE_CODE_CHILD_SESSION !== "1") {
|
|
18617
|
+
continue;
|
|
18618
|
+
}
|
|
18619
|
+
const directory = namedDirectory(plugin, env);
|
|
18620
|
+
if (directory !== undefined)
|
|
18621
|
+
return absolute(HARNESS_DIRECTORY_VARIABLE[plugin], directory);
|
|
18622
|
+
}
|
|
18623
|
+
return;
|
|
18624
|
+
}
|
|
18625
|
+
function handedConfigPath(env) {
|
|
18626
|
+
const handed = nonEmpty(env.ENVOY_CONFIG_FILE);
|
|
18627
|
+
return handed === undefined ? undefined : path.resolve(absolute("ENVOY_CONFIG_FILE", handed));
|
|
18628
|
+
}
|
|
18629
|
+
function envoyConfigPath(harness, env) {
|
|
18630
|
+
const handedFirst = harness === "command" ? handedConfigPath(env) : undefined;
|
|
18631
|
+
if (handedFirst !== undefined)
|
|
18632
|
+
return handedFirst;
|
|
18633
|
+
const directory = harnessDirectory(harness, env);
|
|
18634
|
+
if (directory !== undefined)
|
|
18635
|
+
return path.resolve(directory, "envoy.json");
|
|
18636
|
+
const handed = handedConfigPath(env);
|
|
18637
|
+
if (handed !== undefined)
|
|
18638
|
+
return handed;
|
|
18639
|
+
const neutral = neutralConfig(env);
|
|
18640
|
+
if ("error" in neutral)
|
|
18641
|
+
throw new Error(neutral.error);
|
|
18642
|
+
return neutral.path;
|
|
18643
|
+
}
|
|
18644
|
+
var httpUrlSchema = exports_external.url({ protocol: /^https?$/, error: "must be an http or https URL" });
|
|
18645
|
+
var DispatchSectionSchema = exports_external.strictObject({
|
|
18646
|
+
enabled: exports_external.boolean().optional(),
|
|
18647
|
+
serverUrl: httpUrlSchema.optional(),
|
|
18648
|
+
token: exports_external.string().optional()
|
|
18649
|
+
});
|
|
18650
|
+
var EnvoySectionSchema = exports_external.strictObject({
|
|
18651
|
+
enabled: exports_external.boolean().optional(),
|
|
18652
|
+
url: httpUrlSchema.optional(),
|
|
18653
|
+
natsUrls: exports_external.array(exports_external.string().trim().min(1, "must not be empty")).optional(),
|
|
18654
|
+
token: exports_external.string().optional(),
|
|
18655
|
+
natsSeed: exports_external.string().optional()
|
|
18656
|
+
});
|
|
18657
|
+
var EnvoyFileSchema = exports_external.looseObject({
|
|
18658
|
+
$schema: exports_external.string().optional(),
|
|
18659
|
+
dispatch: DispatchSectionSchema.optional(),
|
|
18660
|
+
envoy: EnvoySectionSchema.optional()
|
|
18661
|
+
});
|
|
18662
|
+
var CheckoutFileSchema = exports_external.looseObject({
|
|
18663
|
+
$schema: exports_external.string().optional(),
|
|
18664
|
+
dispatch: DispatchSectionSchema.optional()
|
|
18665
|
+
});
|
|
18666
|
+
function offWithNotice(notice) {
|
|
18667
|
+
return { enabled: false, notice, error: null };
|
|
18668
|
+
}
|
|
18669
|
+
function offWithError(error48) {
|
|
18670
|
+
return { enabled: false, notice: null, error: error48 };
|
|
18671
|
+
}
|
|
18672
|
+
function httpUrl2(value) {
|
|
18673
|
+
let url2;
|
|
18674
|
+
try {
|
|
18675
|
+
url2 = new URL(value);
|
|
18676
|
+
} catch {
|
|
18677
|
+
return;
|
|
18678
|
+
}
|
|
18679
|
+
return url2.protocol === "http:" || url2.protocol === "https:" ? url2 : undefined;
|
|
18680
|
+
}
|
|
18681
|
+
function describeSchemaIssue(filePath, error48) {
|
|
18682
|
+
const issue2 = error48.issues[0];
|
|
18683
|
+
if (!issue2)
|
|
18684
|
+
return `${filePath}: invalid envoy.json`;
|
|
18685
|
+
if (issue2.code === "unrecognized_keys") {
|
|
18686
|
+
const section = issue2.path.join(".");
|
|
18687
|
+
const keys = issue2.keys.map((key) => `${section}.${key}`).join(", ");
|
|
18688
|
+
return `${filePath}: unrecognized ${section} key(s): ${keys}`;
|
|
18689
|
+
}
|
|
18690
|
+
return `${filePath}: ${issue2.path.join(".")}: ${issue2.message}`;
|
|
18691
|
+
}
|
|
18692
|
+
function danglingLink(filePath) {
|
|
18693
|
+
try {
|
|
18694
|
+
return lstatSync(filePath).isSymbolicLink() ? readlinkSync(filePath) : null;
|
|
18695
|
+
} catch {
|
|
18696
|
+
return null;
|
|
18697
|
+
}
|
|
18698
|
+
}
|
|
18699
|
+
function parseFile(filePath, schema, checkout) {
|
|
18700
|
+
let raw;
|
|
18701
|
+
try {
|
|
18702
|
+
raw = readFileSync3(filePath, "utf-8");
|
|
18703
|
+
} catch (error48) {
|
|
18704
|
+
if (hasErrnoCode(error48, "ENOENT") || hasErrnoCode(error48, "ENOTDIR")) {
|
|
18705
|
+
return { kind: "absent", link: danglingLink(filePath) };
|
|
18706
|
+
}
|
|
18707
|
+
return { kind: "invalid", reason: `${filePath}: could not be read (${messageFor(error48)})` };
|
|
18708
|
+
}
|
|
18709
|
+
let parsedJson;
|
|
18710
|
+
try {
|
|
18711
|
+
parsedJson = JSON.parse(raw);
|
|
18712
|
+
} catch {
|
|
18713
|
+
const position = jsonErrorPosition(raw);
|
|
18714
|
+
return {
|
|
18715
|
+
kind: "invalid",
|
|
18716
|
+
reason: `${filePath}: invalid JSON${position === undefined ? "" : ` at ${position}`}`
|
|
18717
|
+
};
|
|
18718
|
+
}
|
|
18719
|
+
if (typeof parsedJson === "object" && parsedJson !== null && "natsUrls" in parsedJson) {
|
|
18720
|
+
return {
|
|
18721
|
+
kind: "invalid",
|
|
18722
|
+
reason: checkout ? `${filePath}: the top-level natsUrls is not read, and a checkout's envoy.json holds only a dispatch section` : `${filePath}: the top-level natsUrls is not read; move it to envoy.natsUrls`
|
|
18723
|
+
};
|
|
18724
|
+
}
|
|
18725
|
+
const parsed = schema.safeParse(parsedJson);
|
|
18726
|
+
if (!parsed.success) {
|
|
18727
|
+
return { kind: "invalid", reason: describeSchemaIssue(filePath, parsed.error) };
|
|
18728
|
+
}
|
|
18729
|
+
return { kind: "valid", settings: parsed.data };
|
|
18730
|
+
}
|
|
18731
|
+
var FAILURE_KEPT_MS = 60000;
|
|
18732
|
+
var COMMAND_OUTCOMES = Symbol.for("legion.envoy-config.commands.v1");
|
|
18733
|
+
var processGlobal = globalThis;
|
|
18734
|
+
var commandOutcomes = processGlobal[COMMAND_OUTCOMES] ?? new Map;
|
|
18735
|
+
processGlobal[COMMAND_OUTCOMES] = commandOutcomes;
|
|
18736
|
+
function runCommand(command, env, cwd) {
|
|
18737
|
+
const childEnv = {};
|
|
18738
|
+
for (const [name, variable] of Object.entries(env)) {
|
|
18739
|
+
if (variable !== undefined)
|
|
18740
|
+
childEnv[name] = variable;
|
|
18741
|
+
}
|
|
18742
|
+
const directory = mkdtempSync(path.join(tmpdir(), "envoy-command-"));
|
|
18743
|
+
try {
|
|
18744
|
+
chmodSync(directory, 448);
|
|
18745
|
+
const output = path.join(directory, "stdout");
|
|
18746
|
+
const fd = openSync2(output, "wx", 384);
|
|
18747
|
+
let result;
|
|
18748
|
+
try {
|
|
18749
|
+
result = Bun.spawnSync(["/bin/sh", "-c", command], {
|
|
18750
|
+
cwd,
|
|
18751
|
+
env: childEnv,
|
|
18752
|
+
stdin: "ignore",
|
|
18753
|
+
stdout: fd,
|
|
18754
|
+
stderr: "ignore",
|
|
18755
|
+
timeout: COMMAND_TIMEOUT_MS,
|
|
18756
|
+
detached: true
|
|
18757
|
+
});
|
|
18758
|
+
} catch (error48) {
|
|
18759
|
+
return { failure: `could not start: ${messageFor(error48)}`, timedOut: false };
|
|
18760
|
+
} finally {
|
|
18761
|
+
closeSync2(fd);
|
|
18762
|
+
}
|
|
18763
|
+
try {
|
|
18764
|
+
process.kill(-result.pid, "SIGKILL");
|
|
18765
|
+
} catch {}
|
|
18766
|
+
if (result.exitedDueToTimeout) {
|
|
18767
|
+
return {
|
|
18768
|
+
failure: `did not finish within ${COMMAND_TIMEOUT_MS / 1000} s, and does not run again in this process`,
|
|
18769
|
+
timedOut: true
|
|
18770
|
+
};
|
|
18771
|
+
}
|
|
18772
|
+
if (!result.success) {
|
|
18773
|
+
return {
|
|
18774
|
+
failure: `exited with ${result.signalCode ?? `status ${result.exitCode}`}`,
|
|
18775
|
+
timedOut: false
|
|
18776
|
+
};
|
|
18777
|
+
}
|
|
18778
|
+
const value = readFileSync3(output, "utf-8").trim();
|
|
18779
|
+
if (value.length === 0)
|
|
18780
|
+
return { failure: "printed nothing", timedOut: false };
|
|
18781
|
+
if (!sendable(value)) {
|
|
18782
|
+
return {
|
|
18783
|
+
failure: "printed more than one line, or whitespace inside its value",
|
|
18784
|
+
timedOut: false
|
|
18785
|
+
};
|
|
18786
|
+
}
|
|
18787
|
+
return { value };
|
|
18788
|
+
} finally {
|
|
18789
|
+
rmSync2(directory, { recursive: true, force: true });
|
|
18790
|
+
}
|
|
18791
|
+
}
|
|
18792
|
+
function fileValue(value, field, filePath, env) {
|
|
18793
|
+
if (!value.startsWith("!")) {
|
|
18794
|
+
if (!sendable(value)) {
|
|
18795
|
+
throw new Error(`${filePath}: ${field} holds whitespace or a control character`);
|
|
18796
|
+
}
|
|
18797
|
+
return value;
|
|
18798
|
+
}
|
|
18799
|
+
const command = value.slice(1);
|
|
18800
|
+
const cwd = path.dirname(path.resolve(filePath));
|
|
18801
|
+
const key = JSON.stringify([command, filePath, cwd]);
|
|
18802
|
+
let outcome = commandOutcomes.get(key);
|
|
18803
|
+
if (outcome === undefined || "failure" in outcome && !outcome.timedOut && Date.now() - outcome.at >= FAILURE_KEPT_MS) {
|
|
18804
|
+
outcome = { ...runCommand(command, env, cwd), at: Date.now() };
|
|
18805
|
+
commandOutcomes.set(key, outcome);
|
|
18806
|
+
}
|
|
18807
|
+
if ("failure" in outcome)
|
|
18808
|
+
throw new Error(`${filePath}: ${field}'s command ${outcome.failure}`);
|
|
18809
|
+
return outcome.value;
|
|
18810
|
+
}
|
|
18811
|
+
function statDevice(directory) {
|
|
18812
|
+
try {
|
|
18813
|
+
return statSync2(directory).dev;
|
|
18814
|
+
} catch {
|
|
18815
|
+
return;
|
|
18816
|
+
}
|
|
18817
|
+
}
|
|
18818
|
+
function moveNotice(oldFile, newFile, move, deviceOf) {
|
|
18819
|
+
const oldDevice = deviceOf(path.dirname(oldFile));
|
|
18820
|
+
let ancestor = path.dirname(newFile);
|
|
18821
|
+
let newDevice = deviceOf(ancestor);
|
|
18822
|
+
while (newDevice === undefined && path.dirname(ancestor) !== ancestor) {
|
|
18823
|
+
ancestor = path.dirname(ancestor);
|
|
18824
|
+
newDevice = deviceOf(ancestor);
|
|
18825
|
+
}
|
|
18826
|
+
if (oldDevice !== undefined && newDevice !== undefined && oldDevice !== newDevice) {
|
|
18827
|
+
return `${oldFile} is not read, and is on another filesystem than ${newFile} (a mount): create ${newFile}, and move ${oldFile} on the machine that owns it`;
|
|
18828
|
+
}
|
|
18829
|
+
return `${oldFile} is not read: ${move}`;
|
|
18830
|
+
}
|
|
18831
|
+
function oldUserFileNotice(filePath, env, deviceOf) {
|
|
18832
|
+
const home = nonEmpty(env.HOME) ?? homedir2();
|
|
18833
|
+
const old = path.join(home, ".config", "opencode", "envoy.json");
|
|
18834
|
+
if (!existsSync(old))
|
|
18835
|
+
return;
|
|
18836
|
+
const selected = path.resolve(filePath);
|
|
18837
|
+
const userFile = path.resolve(home, ".config", "legion", "envoy.json");
|
|
18838
|
+
if (selected === userFile) {
|
|
18839
|
+
return moveNotice(old, userFile, "mkdir -p ~/.config/legion && mv ~/.config/opencode/envoy.json ~/.config/legion/", deviceOf);
|
|
18840
|
+
}
|
|
18841
|
+
const profileFile = PLUGIN_HARNESSES.map((harness) => namedDirectory(harness, env)).some((directory) => directory !== undefined && path.isAbsolute(directory) && selected === path.resolve(directory, "envoy.json"));
|
|
18842
|
+
if (profileFile) {
|
|
18843
|
+
return `${old} is not read for this profile; copy or link it to ${filePath}, or write that file`;
|
|
18844
|
+
}
|
|
18845
|
+
const configHome = nonEmpty(env.XDG_CONFIG_HOME);
|
|
18846
|
+
const neutral = neutralConfig(env);
|
|
18847
|
+
if (configHome !== undefined && path.isAbsolute(configHome) && "path" in neutral && selected === neutral.path) {
|
|
18848
|
+
return `${old} is not read under XDG_CONFIG_HOME=${configHome}`;
|
|
18849
|
+
}
|
|
18850
|
+
return;
|
|
18851
|
+
}
|
|
18852
|
+
function missingConfigNotice(filePath, missing, off) {
|
|
18853
|
+
const link = missing.link === null ? "" : ` (a link to ${missing.link}, which does not exist)`;
|
|
18854
|
+
const old = missing.oldUserFileNotice === undefined ? "" : `; ${missing.oldUserFileNotice}`;
|
|
18855
|
+
return `no envoy.json at ${filePath}${link}: ${off}${old}`;
|
|
18856
|
+
}
|
|
18857
|
+
function readEnvoyConfig(filePath, env, options) {
|
|
18858
|
+
const deviceOf = options.deviceOf ?? statDevice;
|
|
18859
|
+
const chosen = parseFile(filePath, EnvoyFileSchema, false);
|
|
18860
|
+
if (chosen.kind === "invalid")
|
|
18861
|
+
return { kind: "failed", path: filePath, error: chosen.reason };
|
|
18862
|
+
const resolvedChosen = path.resolve(filePath);
|
|
18863
|
+
const checkoutPath = path.resolve(options.cwd, ".envoy", "envoy.json");
|
|
18864
|
+
const checkoutFile = checkoutPath === resolvedChosen ? { kind: "absent", link: null } : parseFile(checkoutPath, CheckoutFileSchema, true);
|
|
18865
|
+
const own = chosen.kind === "valid" ? chosen.settings : {};
|
|
18866
|
+
let checkout = null;
|
|
18867
|
+
if (checkoutFile.kind === "invalid") {
|
|
18868
|
+
checkout = { kind: "invalid", path: checkoutPath, error: checkoutFile.reason };
|
|
18869
|
+
} else if (checkoutFile.kind === "valid") {
|
|
18870
|
+
checkout = {
|
|
18871
|
+
kind: "valid",
|
|
18872
|
+
path: checkoutPath,
|
|
18873
|
+
dispatch: checkoutFile.settings.dispatch ?? {},
|
|
18874
|
+
envoyError: Object.hasOwn(checkoutFile.settings, "envoy") ? `${checkoutPath}: envoy is not read from a checkout's envoy.json; put it in ${filePath}` : null
|
|
18875
|
+
};
|
|
18876
|
+
}
|
|
18877
|
+
const oldCheckout = path.resolve(options.cwd, ".opencode", "envoy.json");
|
|
18878
|
+
const warnings = oldCheckout !== resolvedChosen && !existsSync(checkoutPath) && existsSync(oldCheckout) ? [
|
|
18879
|
+
moveNotice(oldCheckout, checkoutPath, `mkdir -p ${path.dirname(checkoutPath)} && mv ${oldCheckout} ${checkoutPath}`, deviceOf)
|
|
18880
|
+
] : [];
|
|
18881
|
+
const unreadOldUserFile = chosen.kind === "absent" ? oldUserFileNotice(filePath, env, deviceOf) : undefined;
|
|
18882
|
+
return {
|
|
18883
|
+
kind: "read",
|
|
18884
|
+
path: filePath,
|
|
18885
|
+
dispatch: own.dispatch ?? {},
|
|
18886
|
+
envoy: own.envoy ?? {},
|
|
18887
|
+
checkout,
|
|
18888
|
+
missing: chosen.kind === "absent" ? {
|
|
18889
|
+
link: chosen.link,
|
|
18890
|
+
...unreadOldUserFile === undefined ? {} : { oldUserFileNotice: unreadOldUserFile }
|
|
18891
|
+
} : null,
|
|
18892
|
+
warnings
|
|
18893
|
+
};
|
|
18894
|
+
}
|
|
18895
|
+
|
|
18896
|
+
// ../envoy-client/src/bearer.ts
|
|
18897
|
+
function bearerSource(env, section, written) {
|
|
18898
|
+
const pointer = nonEmpty(env[`${section}_TOKEN_FILE`]);
|
|
18899
|
+
if (pointer !== undefined) {
|
|
18900
|
+
return { kind: "variable", token: readSecretFile(`${section}_TOKEN_FILE`, pointer) };
|
|
18901
|
+
}
|
|
18902
|
+
const plain = nonEmpty(env[`${section}_TOKEN`]);
|
|
18903
|
+
if (plain !== undefined) {
|
|
18904
|
+
if (!sendable(plain)) {
|
|
18905
|
+
throw new Error(`${section}_TOKEN holds whitespace or a control character`);
|
|
18906
|
+
}
|
|
18907
|
+
return { kind: "variable", token: plain };
|
|
18908
|
+
}
|
|
18909
|
+
const bearer = written();
|
|
18910
|
+
return bearer === undefined ? { kind: "none" } : { kind: "file", ...bearer };
|
|
18911
|
+
}
|
|
18912
|
+
function fileBearer(bearer, url2, section, env) {
|
|
18913
|
+
const named = httpUrl2(url2)?.origin;
|
|
18914
|
+
const paired = httpUrl2(bearer.server)?.origin;
|
|
18915
|
+
if (named !== paired) {
|
|
18916
|
+
throw new Error(`${section}_URL names ${named}, but ${bearer.path}'s ${bearer.field} is for ${paired}; set ${section}_TOKEN or ${section}_TOKEN_FILE for it`);
|
|
18917
|
+
}
|
|
18918
|
+
if (bearer.checkout && bearer.token.startsWith("!")) {
|
|
18919
|
+
throw new Error(`${bearer.path}: ${bearer.field} is a command, which a checkout's envoy.json cannot run`);
|
|
18920
|
+
}
|
|
18921
|
+
if (bearer.token === "") {
|
|
18922
|
+
throw new Error(`${bearer.field} in ${bearer.path} must be a non-empty bearer token`);
|
|
18923
|
+
}
|
|
18924
|
+
return fileValue(bearer.token, bearer.field, bearer.path, env);
|
|
18925
|
+
}
|
|
18926
|
+
|
|
18927
|
+
// ../envoy-client/src/dispatch-config.ts
|
|
18928
|
+
var DEFAULT_SERVER_URL = "http://localhost:8766";
|
|
18929
|
+
function checkoutPair(checkout, serverUrl) {
|
|
18930
|
+
const { token } = checkout.dispatch;
|
|
18931
|
+
if (token === undefined) {
|
|
18932
|
+
throw new Error(`${checkout.path}: dispatch.serverUrl requires dispatch.token in the same file`);
|
|
18933
|
+
}
|
|
18934
|
+
return {
|
|
18935
|
+
kind: "file",
|
|
18936
|
+
token,
|
|
18937
|
+
field: "dispatch.token",
|
|
18938
|
+
path: checkout.path,
|
|
18939
|
+
server: serverUrl,
|
|
18940
|
+
checkout: true
|
|
18941
|
+
};
|
|
18942
|
+
}
|
|
18943
|
+
function writtenToken(config2, checkout) {
|
|
18944
|
+
if (checkout?.dispatch.token !== undefined) {
|
|
18945
|
+
throw new Error(`${checkout.path}: dispatch.token requires dispatch.serverUrl in the same file`);
|
|
18946
|
+
}
|
|
18947
|
+
const own = config2.dispatch;
|
|
18948
|
+
if (own.token === undefined)
|
|
18949
|
+
return;
|
|
18950
|
+
const server = own.serverUrl ?? DEFAULT_SERVER_URL;
|
|
18951
|
+
return {
|
|
18952
|
+
token: own.token,
|
|
18953
|
+
field: "dispatch.token",
|
|
18954
|
+
path: config2.path,
|
|
18955
|
+
server,
|
|
18956
|
+
checkout: false
|
|
18957
|
+
};
|
|
18958
|
+
}
|
|
18959
|
+
function dispatchSettings(env, config2) {
|
|
18960
|
+
if (config2.kind === "failed")
|
|
18961
|
+
return offWithError(config2.error);
|
|
18962
|
+
const explicitUrl = nonEmpty(env.DISPATCH_URL);
|
|
18963
|
+
const checkoutFile = explicitUrl === undefined ? config2.checkout : null;
|
|
18964
|
+
if (checkoutFile?.kind === "invalid")
|
|
18965
|
+
return offWithError(checkoutFile.error);
|
|
18966
|
+
const checkout = checkoutFile?.kind === "valid" ? checkoutFile : null;
|
|
18967
|
+
const own = config2.dispatch;
|
|
18968
|
+
if (own.enabled === false) {
|
|
18969
|
+
return offWithNotice(`${config2.path} sets dispatch.enabled to false: Dispatch is off`);
|
|
18970
|
+
}
|
|
18971
|
+
if (checkout?.dispatch.enabled === false) {
|
|
18972
|
+
return offWithNotice(`${checkout.path} sets dispatch.enabled to false: Dispatch is off`);
|
|
18973
|
+
}
|
|
18974
|
+
const checkoutServer = checkout?.dispatch.serverUrl;
|
|
18975
|
+
const addressed = explicitUrl !== undefined || checkoutServer !== undefined || own.serverUrl !== undefined || own.enabled === true;
|
|
18976
|
+
if (!addressed) {
|
|
18977
|
+
return offWithNotice(config2.missing === null ? `${config2.path} names no Dispatch server, and DISPATCH_URL is not set: Dispatch is off` : missingConfigNotice(config2.path, config2.missing, "Dispatch is off"));
|
|
18978
|
+
}
|
|
18979
|
+
if (explicitUrl !== undefined && httpUrl2(explicitUrl) === undefined) {
|
|
18980
|
+
return offWithError("DISPATCH_URL must be an http or https URL");
|
|
18981
|
+
}
|
|
18982
|
+
try {
|
|
18983
|
+
const source = checkout !== null && checkoutServer !== undefined ? checkoutPair(checkout, checkoutServer) : bearerSource(env, "DISPATCH", () => writtenToken(config2, checkout));
|
|
18984
|
+
if (source.kind === "none") {
|
|
18985
|
+
return offWithError(explicitUrl === undefined ? `${config2.path}: dispatch is enabled and no token is: set dispatch.token, or DISPATCH_TOKEN or DISPATCH_TOKEN_FILE` : `DISPATCH_URL is set and no token is: set DISPATCH_TOKEN or DISPATCH_TOKEN_FILE, or dispatch.token in ${config2.path}`);
|
|
18986
|
+
}
|
|
18987
|
+
const url2 = (explicitUrl ?? (source.kind === "file" ? source.server : own.serverUrl ?? DEFAULT_SERVER_URL)).replace(/\/+$/, "");
|
|
18988
|
+
const token = source.kind === "variable" ? source.token : fileBearer(source, url2, "DISPATCH", env);
|
|
18989
|
+
return { enabled: true, url: url2, token };
|
|
18990
|
+
} catch (error48) {
|
|
18991
|
+
return offWithError(messageFor(error48));
|
|
18992
|
+
}
|
|
18993
|
+
}
|
|
18994
|
+
|
|
18995
|
+
// ../envoy-client/src/session-settings.ts
|
|
18996
|
+
function sessionConfigFile(harness, env) {
|
|
18997
|
+
try {
|
|
18998
|
+
return { path: envoyConfigPath(harness, env), error: null };
|
|
18999
|
+
} catch (error48) {
|
|
19000
|
+
return { path: null, error: messageFor(error48) };
|
|
19001
|
+
}
|
|
19002
|
+
}
|
|
19003
|
+
function handedConfigFile(env) {
|
|
19004
|
+
try {
|
|
19005
|
+
const handed = handedConfigPath(env);
|
|
19006
|
+
return handed === undefined ? null : { path: handed, error: null };
|
|
19007
|
+
} catch (error48) {
|
|
19008
|
+
return { path: null, error: messageFor(error48) };
|
|
19009
|
+
}
|
|
19010
|
+
}
|
|
19011
|
+
function sessionDispatchSettings(file2, env, options) {
|
|
19012
|
+
if (file2.path === null)
|
|
19013
|
+
return { dispatch: offWithError(file2.error), warnings: [], missing: null };
|
|
19014
|
+
const config2 = readEnvoyConfig(file2.path, env, options);
|
|
19015
|
+
return {
|
|
19016
|
+
dispatch: dispatchSettings(env, config2),
|
|
19017
|
+
warnings: config2.kind === "read" ? config2.warnings : [],
|
|
19018
|
+
missing: config2.kind === "read" ? config2.missing : null
|
|
19019
|
+
};
|
|
19020
|
+
}
|
|
19021
|
+
|
|
18606
19022
|
// ../envoy-client/src/dispatch-cli.ts
|
|
18607
19023
|
var OK = 0;
|
|
18608
19024
|
var REFUSED = 1;
|
|
18609
19025
|
var USAGE = 2;
|
|
18610
|
-
var HOSTS =
|
|
19026
|
+
var HOSTS = ["omp", "claude", "opencode"];
|
|
18611
19027
|
var CLAUDE_OUTPUT_MAX = 25000;
|
|
18612
19028
|
var STATE_WRITE_FAILED = "dispatch: Dispatch took the call, but this session's state";
|
|
18613
19029
|
function stateWriteFailed(problem) {
|
|
@@ -18675,8 +19091,8 @@ async function runDispatchCli(argv, rawEnv, io) {
|
|
|
18675
19091
|
print(new ToolInputError(parsed.tool, parsed.problems).message);
|
|
18676
19092
|
return REFUSED;
|
|
18677
19093
|
}
|
|
18678
|
-
const hostName = env.DISPATCH_HOST;
|
|
18679
|
-
const host =
|
|
19094
|
+
const hostName = env.DISPATCH_HOST ?? (env.CLAUDE_CODE_CHILD_SESSION === "1" ? "claude" : undefined);
|
|
19095
|
+
const host = HOSTS.find((name) => name === hostName);
|
|
18680
19096
|
if (host === undefined) {
|
|
18681
19097
|
print(hostName === undefined ? "dispatch: DISPATCH_HOST is not set. The host plugin sets it in an agent's shell; to run dispatch by hand, set DISPATCH_HOST and DISPATCH_SESSION_ID yourself." : `dispatch: DISPATCH_HOST is ${JSON.stringify(hostName)}, not omp, claude or opencode.`);
|
|
18682
19098
|
return USAGE;
|
|
@@ -18698,17 +19114,19 @@ async function runDispatchCli(argv, rawEnv, io) {
|
|
|
18698
19114
|
print(JSON.stringify(parsed.args, null, 2));
|
|
18699
19115
|
return OK;
|
|
18700
19116
|
}
|
|
18701
|
-
|
|
18702
|
-
|
|
18703
|
-
|
|
18704
|
-
} catch (error48) {
|
|
18705
|
-
print(`dispatch: ${messageFor(error48)}`);
|
|
19117
|
+
const file2 = (env.DISPATCH_HOST === undefined ? null : handedConfigFile(env)) ?? sessionConfigFile(host, env);
|
|
19118
|
+
if (file2.path === null) {
|
|
19119
|
+
print(`dispatch: ${file2.error}`);
|
|
18706
19120
|
return USAGE;
|
|
18707
19121
|
}
|
|
18708
|
-
|
|
18709
|
-
|
|
19122
|
+
const { dispatch: resolution, warnings } = sessionDispatchSettings(file2, env, { cwd: io.cwd });
|
|
19123
|
+
for (const warning of warnings)
|
|
19124
|
+
print(`dispatch: ${warning}`);
|
|
19125
|
+
if (!resolution.enabled) {
|
|
19126
|
+
print(resolution.error === null ? `dispatch: ${resolution.notice}` : `dispatch: dispatch config: ${resolution.error}`);
|
|
18710
19127
|
return USAGE;
|
|
18711
19128
|
}
|
|
19129
|
+
const config2 = resolution;
|
|
18712
19130
|
let follows;
|
|
18713
19131
|
try {
|
|
18714
19132
|
pruneSessions(sessionStateRoot(env), Date.now());
|