@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.
@@ -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";
@@ -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>;
@@ -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
- }