@sjawhar/opencode-legion-envoy 6.4.2 → 6.5.1
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 +13 -36
- package/bin/dispatch +6 -0
- package/dist/dispatch.js +18648 -0
- package/dist/src/server.js +238 -3570
- package/package.json +4 -3
- package/skills/dispatch/SKILL.md +85 -97
- package/skills/dispatch/references/api.md +4 -4
- package/skills/dispatch/references/asks.md +34 -54
- package/skills/dispatch/references/document-edits.md +24 -21
- package/skills/dispatch/references/documents.md +75 -72
- package/skills/dispatch/references/examples.md +37 -67
- package/skills/dispatch/references/issues.md +48 -48
- package/skills/dispatch/references/messages.md +18 -25
- package/skills/dispatch/references/reading.md +10 -10
- package/skills/dispatch-brainstorming/SKILL.md +19 -15
- package/skills/dispatch-first/SKILL.md +7 -7
- package/skills/envoy/SKILL.md +3 -2
- package/skills/legion-architect/SKILL.md +47 -49
- package/skills/legion-controller/SKILL.md +38 -32
- package/skills/legion-retro/SKILL.md +7 -8
- package/skills/legion-worker/SKILL.md +20 -15
- package/skills/legion-worker/references/merge-gate.md +3 -3
- package/src/server.ts +54 -36
package/src/server.ts
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
|
-
import { existsSync } from "node:fs";
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
|
-
import { agentSubject,
|
|
4
|
+
import { agentSubject, zodSchemaApi } from "@legion/contracts";
|
|
5
5
|
import { envoyDefaultsFromEnvironment } from "@legion/envoy-client/defaults";
|
|
6
6
|
import { resolveDispatchConfig } from "@legion/envoy-client/dispatch-config";
|
|
7
|
-
import { executeDispatchTool } from "@legion/envoy-client/dispatch-execute";
|
|
8
7
|
import { dispatchFirstSkillFile } from "@legion/envoy-client/dispatch-first";
|
|
9
8
|
import { machineID } from "@legion/envoy-client/machine";
|
|
10
9
|
import {
|
|
@@ -13,7 +12,7 @@ import {
|
|
|
13
12
|
toMessageMetadata,
|
|
14
13
|
} from "@legion/envoy-client/tool-contract";
|
|
15
14
|
import { createEnvoyClient } from "@legion/envoy-client/transport";
|
|
16
|
-
import {
|
|
15
|
+
import { tool } from "@opencode-ai/plugin/tool";
|
|
17
16
|
import { logger } from "./log";
|
|
18
17
|
import { resolvePort } from "./port";
|
|
19
18
|
|
|
@@ -29,7 +28,37 @@ const skillsDirectory = [
|
|
|
29
28
|
path.resolve(moduleDirectory, "../../../skills"),
|
|
30
29
|
].find((dir) => existsSync(dir));
|
|
31
30
|
|
|
32
|
-
|
|
31
|
+
const PACKAGE_NAME = "@sjawhar/opencode-legion-envoy";
|
|
32
|
+
|
|
33
|
+
/** The package root: the nearest directory above `from` whose package.json names this package.
|
|
34
|
+
* The packed server.js sits in dist/src/, the repo's server.ts in src/. A manifest on the way up
|
|
35
|
+
* that is missing, unreadable or not JSON is passed over. */
|
|
36
|
+
function packageRoot(from: string): string {
|
|
37
|
+
for (let directory = from; ; directory = path.dirname(directory)) {
|
|
38
|
+
if (manifestName(path.join(directory, "package.json")) === PACKAGE_NAME) return directory;
|
|
39
|
+
if (path.dirname(directory) === directory) {
|
|
40
|
+
throw new Error(`envoy: no ${PACKAGE_NAME} package.json above ${from}`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** A manifest's `name`, or undefined when the file is missing, unreadable or not JSON. */
|
|
46
|
+
function manifestName(manifest: string): unknown {
|
|
47
|
+
try {
|
|
48
|
+
const parsed: unknown = JSON.parse(readFileSync(manifest, "utf8"));
|
|
49
|
+
return typeof parsed === "object" && parsed !== null && "name" in parsed
|
|
50
|
+
? parsed.name
|
|
51
|
+
: undefined;
|
|
52
|
+
} catch {
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Holds the `dispatch` shim the `shell.env` hook puts first on every shell command's PATH. */
|
|
58
|
+
const PLUGIN_BIN = path.join(packageRoot(moduleDirectory), "bin");
|
|
59
|
+
|
|
60
|
+
/** A spec by its name, typed so a misspelled name fails `tsc` rather than the plugin's load. */
|
|
61
|
+
function toolSpec(name: (typeof envoyToolSpecs)[number]["name"]): ToolSpec {
|
|
33
62
|
const spec = envoyToolSpecs.find((candidate) => candidate.name === name);
|
|
34
63
|
if (spec === undefined) throw new Error(`envoy tool contract has no ${name}`);
|
|
35
64
|
return spec;
|
|
@@ -46,12 +75,12 @@ const sessionsSpec = toolSpec("envoy_sessions");
|
|
|
46
75
|
|
|
47
76
|
export default async (input: { serverUrl: URL }) => {
|
|
48
77
|
const cwd = process.cwd();
|
|
49
|
-
// Dispatch configuration never prevents Envoy from loading.
|
|
50
|
-
//
|
|
78
|
+
// Dispatch configuration never prevents Envoy from loading. A Dispatch the configuration
|
|
79
|
+
// cannot reach is visible in the host log, including normal unconfigured installs.
|
|
51
80
|
const dispatchConfig = resolveDispatchConfig(process.env, { cwd });
|
|
52
81
|
if (!dispatchConfig.enabled) {
|
|
53
82
|
logger.warn(
|
|
54
|
-
`envoy:
|
|
83
|
+
`envoy: Dispatch disabled — ${dispatchConfig.error ?? "no Dispatch URL configured"}`
|
|
55
84
|
);
|
|
56
85
|
}
|
|
57
86
|
// With Dispatch configured, every session carries the dispatch-first skill as an instruction
|
|
@@ -182,33 +211,6 @@ export default async (input: { serverUrl: URL }) => {
|
|
|
182
211
|
clearInterval(heartbeatInterval);
|
|
183
212
|
});
|
|
184
213
|
|
|
185
|
-
// Native Dispatch tools are present only when the shared configuration has
|
|
186
|
-
// both a server URL and bearer token. Each tool returns its details as
|
|
187
|
-
// OpenCode metadata so the post-execution hook can subscribe to its topic.
|
|
188
|
-
const dispatchTools: Record<string, ToolDefinition> = {};
|
|
189
|
-
if (dispatchConfig.enabled) {
|
|
190
|
-
for (const spec of dispatchToolSpecs) {
|
|
191
|
-
dispatchTools[spec.name] = tool({
|
|
192
|
-
description: spec.description,
|
|
193
|
-
args: spec.arguments(zodSchemaApi(tool.schema)) as never,
|
|
194
|
-
async execute(args, ctx) {
|
|
195
|
-
ctx.metadata({ title: "Dispatch" });
|
|
196
|
-
const result = await executeDispatchTool({
|
|
197
|
-
tool: spec.name,
|
|
198
|
-
args: args as Record<string, unknown>,
|
|
199
|
-
cwd: ctx.directory,
|
|
200
|
-
host: "opencode",
|
|
201
|
-
sessionId: ctx.sessionID,
|
|
202
|
-
sessionTitle: (await fetchTitle(ctx.sessionID)) ?? undefined,
|
|
203
|
-
config: dispatchConfig,
|
|
204
|
-
env: process.env,
|
|
205
|
-
});
|
|
206
|
-
return { title: "Dispatch", output: result.text, metadata: result.details };
|
|
207
|
-
},
|
|
208
|
-
});
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
|
|
212
214
|
return {
|
|
213
215
|
config: (
|
|
214
216
|
cfg: { skills?: { paths?: string[] }; instructions?: string[] } & Record<string, unknown>
|
|
@@ -226,6 +228,23 @@ export default async (input: { serverUrl: URL }) => {
|
|
|
226
228
|
if (!cfg.instructions.includes(dispatchFirst)) cfg.instructions.push(dispatchFirst);
|
|
227
229
|
}
|
|
228
230
|
},
|
|
231
|
+
// The `dispatch` command in every shell command: first on PATH, and told which OpenCode
|
|
232
|
+
// session runs it and that session's title.
|
|
233
|
+
"shell.env": async (
|
|
234
|
+
input: { cwd: string; sessionID?: string; callID?: string },
|
|
235
|
+
output: { env: Record<string, string> }
|
|
236
|
+
) => {
|
|
237
|
+
output.env.PATH = `${PLUGIN_BIN}${path.delimiter}${output.env.PATH ?? process.env.PATH ?? ""}`;
|
|
238
|
+
output.env.DISPATCH_HOST = "opencode";
|
|
239
|
+
// OpenCode spreads output.env over its own process.env, so an absent key keeps the inherited value.
|
|
240
|
+
output.env.DISPATCH_SESSION_ID = "";
|
|
241
|
+
output.env.DISPATCH_SESSION_TITLE = "";
|
|
242
|
+
if (input.sessionID) {
|
|
243
|
+
output.env.DISPATCH_SESSION_ID = input.sessionID;
|
|
244
|
+
const title = trackedSessions.get(input.sessionID)?.title;
|
|
245
|
+
if (title) output.env.DISPATCH_SESSION_TITLE = title;
|
|
246
|
+
}
|
|
247
|
+
},
|
|
229
248
|
event: async ({
|
|
230
249
|
event,
|
|
231
250
|
}: {
|
|
@@ -295,7 +314,6 @@ export default async (input: { serverUrl: URL }) => {
|
|
|
295
314
|
clearInterval(heartbeatInterval);
|
|
296
315
|
},
|
|
297
316
|
tool: {
|
|
298
|
-
...dispatchTools,
|
|
299
317
|
envoy_subscribe: tool({
|
|
300
318
|
description: subscribeSpec.description,
|
|
301
319
|
args: subscribeSpec.arguments(zodSchemaApi(tool.schema)) as never,
|