@sjawhar/pi-envoy 9.7.0 → 10.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +51 -17
- package/dist/dispatch.js +643 -225
- package/dist/envoy.js +870 -317
- package/dist/skills/dispatch/references/api.md +16 -3
- package/dist/skills/dispatch/references/asks.md +3 -3
- package/dist/skills/dispatch/references/issues.md +5 -1
- package/package.json +1 -1
package/dist/dispatch.js
CHANGED
|
@@ -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, "");
|
|
@@ -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})`;
|
|
@@ -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 {
|
|
@@ -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());
|