@sjawhar/opencode-legion-envoy 0.9.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -0
- package/dist/src/server.js +817 -169
- package/package.json +4 -3
- package/skills/dispatch/SKILL.md +196 -45
- package/src/server.ts +73 -26
- package/dist/bin/dispatch-mcp-shim.js +0 -4889
- package/src/config/index.ts +0 -69
- package/src/config/schema.ts +0 -21
- package/src/dispatch-mcp.ts +0 -113
package/src/config/index.ts
DELETED
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
-
import os from "node:os";
|
|
3
|
-
import path from "node:path";
|
|
4
|
-
import { messageFor } from "@legion/envoy-client/errors";
|
|
5
|
-
import { type EnvoyConfig, EnvoyConfigSchema } from "./schema";
|
|
6
|
-
|
|
7
|
-
export interface LoadEnvoyConfigOptions {
|
|
8
|
-
homeDir?: string;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
/** A present but unusable envoy.json. The plugin refuses to load rather than run with dispatch silently off. */
|
|
12
|
-
export class EnvoyConfigError extends Error {
|
|
13
|
-
readonly filePath: string;
|
|
14
|
-
constructor(filePath: string, detail: string) {
|
|
15
|
-
super(`[envoy-plugin] invalid config at ${filePath}: ${detail}`);
|
|
16
|
-
this.name = "EnvoyConfigError";
|
|
17
|
-
this.filePath = filePath;
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
function readConfigFile(filePath: string): EnvoyConfig | null {
|
|
22
|
-
if (!existsSync(filePath)) return null;
|
|
23
|
-
let raw: unknown;
|
|
24
|
-
try {
|
|
25
|
-
raw = JSON.parse(readFileSync(filePath, "utf-8")) as unknown;
|
|
26
|
-
} catch (error) {
|
|
27
|
-
throw new EnvoyConfigError(filePath, messageFor(error));
|
|
28
|
-
}
|
|
29
|
-
const parsed = EnvoyConfigSchema.safeParse(raw);
|
|
30
|
-
if (!parsed.success) {
|
|
31
|
-
const issues = parsed.error.issues
|
|
32
|
-
.map((issue) => `${issue.path.join(".")}: ${issue.message}`)
|
|
33
|
-
.join(", ");
|
|
34
|
-
throw new EnvoyConfigError(filePath, issues);
|
|
35
|
-
}
|
|
36
|
-
return parsed.data;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function mergeConfig(base: EnvoyConfig, override: EnvoyConfig): EnvoyConfig {
|
|
40
|
-
return {
|
|
41
|
-
...base,
|
|
42
|
-
...override,
|
|
43
|
-
dispatch:
|
|
44
|
-
base.dispatch || override.dispatch
|
|
45
|
-
? {
|
|
46
|
-
...base.dispatch,
|
|
47
|
-
...override.dispatch,
|
|
48
|
-
}
|
|
49
|
-
: undefined,
|
|
50
|
-
};
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
export async function loadEnvoyConfig(
|
|
54
|
-
directory: string,
|
|
55
|
-
options: LoadEnvoyConfigOptions = {}
|
|
56
|
-
): Promise<EnvoyConfig> {
|
|
57
|
-
const homeDir = options.homeDir ?? os.homedir();
|
|
58
|
-
const userConfigPath = path.join(homeDir, ".config", "opencode", "envoy.json");
|
|
59
|
-
const repoConfigPath = path.join(directory, ".opencode", "envoy.json");
|
|
60
|
-
|
|
61
|
-
let merged: EnvoyConfig = {};
|
|
62
|
-
const userConfig = readConfigFile(userConfigPath);
|
|
63
|
-
if (userConfig) merged = mergeConfig(merged, userConfig);
|
|
64
|
-
const repoConfig = readConfigFile(repoConfigPath);
|
|
65
|
-
if (repoConfig) merged = mergeConfig(merged, repoConfig);
|
|
66
|
-
return merged;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
export type { DispatchConfig, EnvoyConfig } from "./schema";
|
package/src/config/schema.ts
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
import { tool } from "@opencode-ai/plugin";
|
|
2
|
-
|
|
3
|
-
const z = tool.schema;
|
|
4
|
-
|
|
5
|
-
export const DispatchConfigSchema = z
|
|
6
|
-
.object({
|
|
7
|
-
enabled: z.boolean().optional(),
|
|
8
|
-
serverUrl: z.string().url().optional(),
|
|
9
|
-
})
|
|
10
|
-
.strict();
|
|
11
|
-
|
|
12
|
-
export const EnvoyConfigSchema = z
|
|
13
|
-
.object({
|
|
14
|
-
$schema: z.string().optional(),
|
|
15
|
-
natsUrls: z.array(z.string()).optional(),
|
|
16
|
-
dispatch: DispatchConfigSchema.optional(),
|
|
17
|
-
})
|
|
18
|
-
.passthrough();
|
|
19
|
-
|
|
20
|
-
export type DispatchConfig = ReturnType<typeof DispatchConfigSchema.parse>;
|
|
21
|
-
export type EnvoyConfig = ReturnType<typeof EnvoyConfigSchema.parse>;
|
package/src/dispatch-mcp.ts
DELETED
|
@@ -1,113 +0,0 @@
|
|
|
1
|
-
import { existsSync } from "node:fs";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import type { DispatchConfig } from "./config";
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* OpenCode local MCP config shape that we inject into `config.mcp`. We use
|
|
7
|
-
* `type: "local"` (subprocess via stdio) instead of `type: "remote"` so we
|
|
8
|
-
* can rotate the GitHub bearer transparently — the StreamableHTTPClient
|
|
9
|
-
* transport snapshots static headers once at construction, which would
|
|
10
|
-
* break MCP calls after the gh-app installation token expires (~1h).
|
|
11
|
-
*
|
|
12
|
-
* The shim subprocess mints a fresh token via `gh auth token` per request
|
|
13
|
-
* (with a 50-minute in-memory cache), so OpenCode never sees an expired
|
|
14
|
-
* token. The user's `gh` shim handles per-CWD profile selection via the
|
|
15
|
-
* project's `.git/config` `[gh-app "<profile>"]` block.
|
|
16
|
-
*/
|
|
17
|
-
export interface DispatchMcpEntry {
|
|
18
|
-
type: "local";
|
|
19
|
-
command: string[];
|
|
20
|
-
environment: Record<string, string>;
|
|
21
|
-
enabled: true;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export interface BuildDispatchMcpEntryOptions {
|
|
25
|
-
dispatch: DispatchConfig | undefined;
|
|
26
|
-
/**
|
|
27
|
-
* Absolute path to the shim entry script. Defaults to the colocated
|
|
28
|
-
* `bin/dispatch-mcp-shim.ts` next to this module. Override in tests.
|
|
29
|
-
*/
|
|
30
|
-
shimPath?: string;
|
|
31
|
-
/**
|
|
32
|
-
* Command used to launch the shim. Defaults to `bun`. Override in tests
|
|
33
|
-
* or when a different runtime is desired (e.g. `node` with a compiled
|
|
34
|
-
* shim).
|
|
35
|
-
*/
|
|
36
|
-
runtime?: string;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
const DEFAULT_SERVER_URL = "http://localhost:8766";
|
|
40
|
-
|
|
41
|
-
function defaultShimPath(): string {
|
|
42
|
-
// Source layout: this module runs from src/ and the shim wrapper lives at
|
|
43
|
-
// ../bin/dispatch-mcp-shim.ts. Packed layout: this module is bundled to
|
|
44
|
-
// dist/src/server.js and the self-contained shim bundle lives at
|
|
45
|
-
// dist/bin/dispatch-mcp-shim.js — same ../bin relationship, built artifact.
|
|
46
|
-
const packageRoot = path.join(import.meta.dir, "..");
|
|
47
|
-
const candidates = [
|
|
48
|
-
path.join(packageRoot, "bin", "dispatch-mcp-shim.js"),
|
|
49
|
-
path.join(packageRoot, "bin", "dispatch-mcp-shim.ts"),
|
|
50
|
-
];
|
|
51
|
-
const found = candidates.find((candidate) => existsSync(candidate));
|
|
52
|
-
if (!found) {
|
|
53
|
-
throw new Error(`dispatch MCP shim not found; tried: ${candidates.join(", ")}`);
|
|
54
|
-
}
|
|
55
|
-
return found;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* Build the OpenCode `mcp.envoy` entry. Returns null when dispatch
|
|
60
|
-
* is not enabled in envoy.json.
|
|
61
|
-
*
|
|
62
|
-
* Token availability is NOT validated here — the shim subprocess handles
|
|
63
|
-
* token fetching at request time. If `gh auth token` fails inside the
|
|
64
|
-
* shim, the affected MCP request returns a JSON-RPC error with a helpful
|
|
65
|
-
* message; other MCP servers continue to work.
|
|
66
|
-
*/
|
|
67
|
-
export function buildDispatchMcpEntry(opts: BuildDispatchMcpEntryOptions): DispatchMcpEntry | null {
|
|
68
|
-
if (!opts.dispatch?.enabled) return null;
|
|
69
|
-
|
|
70
|
-
const baseUrl = (opts.dispatch.serverUrl ?? DEFAULT_SERVER_URL).replace(/\/+$/, "");
|
|
71
|
-
const shimPath = opts.shimPath ?? defaultShimPath();
|
|
72
|
-
const runtime = opts.runtime ?? "bun";
|
|
73
|
-
|
|
74
|
-
return {
|
|
75
|
-
type: "local",
|
|
76
|
-
command: [runtime, shimPath],
|
|
77
|
-
environment: {
|
|
78
|
-
DISPATCH_MCP_URL: `${baseUrl}/mcp`,
|
|
79
|
-
},
|
|
80
|
-
enabled: true,
|
|
81
|
-
};
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/**
|
|
85
|
-
* Inject the envoy MCP entry into an OpenCode `cfg` object. Returns a
|
|
86
|
-
* structured result instead of logging directly so the behavior is pure and
|
|
87
|
-
* testable; the caller forwards `warning` to the plugin logger when present.
|
|
88
|
-
*
|
|
89
|
-
* Idempotent on the plugin's own re-writes: when the existing `cfg.mcp.envoy`
|
|
90
|
-
* deep-equals the entry we'd inject, this is a silent no-op. OpenCode's
|
|
91
|
-
* InstanceState invalidation can re-run the plugin's config hook against a
|
|
92
|
-
* Config-service cfg that still carries our prior mutation; without the
|
|
93
|
-
* idempotency check that legitimate re-entry path produces a TUI stderr
|
|
94
|
-
* alarm. A warning still fires when the existing entry is genuinely
|
|
95
|
-
* different from ours (a user override the plugin must not clobber).
|
|
96
|
-
*/
|
|
97
|
-
export function injectEnvoyMcp(
|
|
98
|
-
cfg: { mcp?: Record<string, unknown> } & Record<string, unknown>,
|
|
99
|
-
entry: DispatchMcpEntry
|
|
100
|
-
): { warning?: string } {
|
|
101
|
-
cfg.mcp = cfg.mcp ?? {};
|
|
102
|
-
const existing = (cfg.mcp as Record<string, unknown>).envoy;
|
|
103
|
-
if (existing !== undefined) {
|
|
104
|
-
if (JSON.stringify(existing) === JSON.stringify(entry)) {
|
|
105
|
-
return {};
|
|
106
|
-
}
|
|
107
|
-
return {
|
|
108
|
-
warning: "[envoy-plugin] envoy MCP entry already present in config; not overriding",
|
|
109
|
-
};
|
|
110
|
-
}
|
|
111
|
-
(cfg.mcp as Record<string, unknown>).envoy = entry;
|
|
112
|
-
return {};
|
|
113
|
-
}
|