@sjawhar/opencode-legion-envoy 0.6.0 → 0.6.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/package.json CHANGED
@@ -1,11 +1,14 @@
1
1
  {
2
2
  "name": "@sjawhar/opencode-legion-envoy",
3
- "version": "0.6.0",
3
+ "version": "0.6.1",
4
4
  "type": "module",
5
- "main": "src/server.ts",
5
+ "main": "dist/src/server.js",
6
6
  "exports": {
7
+ ".": {
8
+ "import": "./dist/src/server.js"
9
+ },
7
10
  "./server": {
8
- "import": "./src/server.ts"
11
+ "import": "./dist/src/server.js"
9
12
  },
10
13
  "./tui": {
11
14
  "import": "./src/tui.tsx"
@@ -15,15 +18,19 @@
15
18
  "type": "git",
16
19
  "url": "https://github.com/sjawhar/legion"
17
20
  },
18
- "types": "src/server.ts",
21
+ "files": [
22
+ "dist",
23
+ "src",
24
+ "!src/**/__tests__"
25
+ ],
19
26
  "scripts": {
27
+ "build": "bun build src/server.ts bin/dispatch-mcp-shim.ts --outdir dist --target bun --format esm --external '@opencode-ai/*'",
28
+ "prepack": "bun run build",
20
29
  "typecheck": "bunx tsc --noEmit",
21
30
  "test": "bun test",
22
31
  "lint": "bunx biome check src/"
23
32
  },
24
33
  "dependencies": {
25
- "@legion/contracts": "workspace:*",
26
- "@legion/envoy-client": "workspace:*",
27
34
  "@opencode-ai/plugin": "~1.14.46"
28
35
  },
29
36
  "peerDependencies": {
@@ -32,6 +39,8 @@
32
39
  },
33
40
  "devDependencies": {
34
41
  "@biomejs/biome": "^2.3.14",
42
+ "@legion/contracts": "0.10.0",
43
+ "@legion/envoy-client": "0.1.0",
35
44
  "@types/bun": "latest",
36
45
  "solid-js": "^1.9.0",
37
46
  "typescript": "^5.3.0"
@@ -1,3 +1,4 @@
1
+ import { existsSync } from "node:fs";
1
2
  import path from "node:path";
2
3
  import type { DispatchConfig } from "./config";
3
4
 
@@ -38,10 +39,20 @@ export interface BuildDispatchMcpEntryOptions {
38
39
  const DEFAULT_SERVER_URL = "http://localhost:8766";
39
40
 
40
41
  function defaultShimPath(): string {
41
- // import.meta.dir resolves to this file's directory in Bun, e.g.
42
- // /home/ubuntu/legion/default/packages/envoy-plugin/src go up one
43
- // level to the package root, then into bin/.
44
- return path.join(import.meta.dir, "..", "bin", "dispatch-mcp-shim.ts");
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;
45
56
  }
46
57
 
47
58
  /**
package/AGENTS.md DELETED
@@ -1,34 +0,0 @@
1
- # Envoy Plugin Package
2
-
3
- OpenCode plugin package for Legion's Envoy subsystem.
4
-
5
- ## Overview
6
-
7
- This plugin exposes the Envoy tools and maintains the live session registry metadata Envoy needs for hot delivery.
8
-
9
- It is the user-facing bridge between OpenCode sessions and Envoy transport.
10
-
11
- ## Where to look
12
-
13
- | Task | Location | Notes |
14
- | ------------------- | ---------------------- | ------------------------------------------------------------------ |
15
- | Tool definitions | `src/server.ts` | `envoy_subscribe`, `envoy_unsubscribe`, `envoy_list`, `envoy_send`, `envoy_publish`, `envoy_role_set`, `envoy_whoami`, `envoy_sessions` |
16
- | Packaging metadata | `package.json` | npm identity, `exports` map, scripts |
17
- | TUI: `/whoami` + sidebar | `src/tui.tsx` | slash command + session-id/port sidebar; loaded via the `./tui` export. Ships as `.tsx` source (no build/`dist`) — Bun transpiles it natively at load, so `@opentui/core` + `@opentui/solid` MUST be `peerDependencies` (not `devDependencies`) so the `@jsxImportSource @opentui/solid` runtime resolves in the consumer's install tree |
18
- | Host rollout helper | `scripts/sync-host.sh` | sync packed release tarball + shim to remote host |
19
- | Dispatch MCP + auto-subscribe | `src/dispatch-mcp.ts`, `src/dispatch-subscribe.ts` | injects the shim wrapper in `bin/`; shared forwarding/token code lives in `@legion/envoy-client`. `tool.execute.after` auto-subscribes dispatch callers to new thread topics. |
20
-
21
- ## Critical conventions
22
-
23
- - Tool descriptions must be self-describing enough that agents can infer correct topic formats.
24
- - Slack examples must use real `team_id` values, not workspace slugs.
25
- - This package owns the session-registry/port-backfill behavior now; do not split that back into a second plugin casually.
26
- - Keep the plugin source-of-truth here even if a dotfiles shim is still used for rollout convenience.
27
-
28
- ## Topic reminders
29
-
30
- - Agent: `notifications.agent.<session_id>`
31
- - GitHub: `notifications.github.<owner>.<repo>.<kind>`
32
- - Slack: `notifications.slack.<team_id>.<channel_id>.<message|mention>`
33
-
34
- If you are unsure what a session is subscribed to, use `envoy_list()`.
@@ -1,7 +0,0 @@
1
- #!/usr/bin/env bun
2
- // envoy-plugin local MCP shim entry point. The forwarding logic is shared
3
- // with the other Envoy adapters in @legion/envoy-client; OpenCode spawns
4
- // this wrapper via the `mcp.envoy` entry injected by src/dispatch-mcp.ts.
5
- import { runDispatchMcpShim } from "@legion/envoy-client/dispatch-mcp-shim";
6
-
7
- runDispatchMcpShim();
@@ -1,73 +0,0 @@
1
- #!/usr/bin/env bash
2
- set -euo pipefail
3
-
4
- host="${1:?usage: sync-host.sh user@host}"
5
-
6
- PLUGIN_DIR="legion/default/packages/envoy-plugin"
7
- PLUGIN_REF="file://{env:HOME}/${PLUGIN_DIR}"
8
- REPO="sjawhar/legion"
9
-
10
- # Find latest envoy release tag
11
- tag=$(gh release list --repo "$REPO" --limit 10 --json tagName \
12
- --jq '[.[] | select(.tagName | startswith("legion-envoy-"))][0].tagName')
13
- if [ -z "$tag" ]; then
14
- echo "ERROR: No legion-envoy release found" >&2
15
- exit 1
16
- fi
17
- echo "Using release: $tag"
18
-
19
- # Download plugin tarball
20
- tmpdir=$(mktemp -d)
21
- trap 'rm -rf "$tmpdir"' EXIT
22
-
23
- gh release download "$tag" \
24
- --repo "$REPO" \
25
- --pattern '*.tgz' \
26
- --dir "$tmpdir" \
27
- --clobber
28
-
29
- tgz=$(find "$tmpdir" -name '*.tgz' -print -quit)
30
- if [ -z "$tgz" ]; then
31
- echo "ERROR: No .tgz found in release $tag" >&2
32
- exit 1
33
- fi
34
- echo "Downloaded: $(basename "$tgz")"
35
-
36
- # Install on remote: extract tarball (strip package/ prefix from npm pack output)
37
- ssh "$host" "rm -rf ~/${PLUGIN_DIR} && mkdir -p ~/${PLUGIN_DIR}"
38
- scp -q "$tgz" "$host:/tmp/envoy-plugin.tgz"
39
- ssh "$host" "tar xzf /tmp/envoy-plugin.tgz --strip-components=1 -C ~/${PLUGIN_DIR} && rm /tmp/envoy-plugin.tgz"
40
- echo "Installed plugin to ~/${PLUGIN_DIR}"
41
-
42
- # Update opencode.json: replace npm package reference with file:// path
43
- # Resolves symlinks so we don't clobber dotfiles symlink structure
44
- ssh "$host" "
45
- CONFIG=\$(readlink -f \$HOME/.config/opencode/opencode.json 2>/dev/null || echo \$HOME/.config/opencode/opencode.json)
46
- if [ -f \"\$CONFIG\" ]; then
47
- jq --arg ref '$PLUGIN_REF' \\
48
- '(.plugin // []) |= [.[] | if (test(\"opencode-legion-envoy\") and (test(\"^file://\") | not)) then \$ref else . end]' \\
49
- \"\$CONFIG\" > /tmp/opencode.json.tmp && mv /tmp/opencode.json.tmp \"\$CONFIG\"
50
- echo \"Updated opencode.json: \$CONFIG\"
51
- else
52
- echo \"WARNING: opencode.json not found at \$CONFIG — add plugin manually: $PLUGIN_REF\"
53
- fi
54
- "
55
-
56
- # Update tui.json: ensure file:// ref is present in the TUI plugin list.
57
- # tui.json is a separate config file from opencode.json; opencode loads TUI
58
- # plugins (slash commands, sidebar slots) only from here.
59
- ssh "$host" "
60
- TUI_CONFIG=\$(readlink -f \$HOME/.config/opencode/tui.json 2>/dev/null || echo \$HOME/.config/opencode/tui.json)
61
- if [ -f \"\$TUI_CONFIG\" ]; then
62
- jq --arg ref '$PLUGIN_REF' \\
63
- '(.plugin // []) |= ((map(if (test(\"opencode-legion-envoy\") and (test(\"^file://\") | not)) then \$ref else . end)) | if any(. == \$ref) then . else . + [\$ref] end)' \\
64
- \"\$TUI_CONFIG\" > /tmp/tui.json.tmp && mv /tmp/tui.json.tmp \"\$TUI_CONFIG\"
65
- echo \"Updated tui.json: \$TUI_CONFIG\"
66
- else
67
- mkdir -p \$(dirname \"\$TUI_CONFIG\")
68
- jq -n --arg ref '$PLUGIN_REF' '{\"\\\$schema\":\"https://opencode.ai/tui.json\",\"plugin\":[\$ref]}' > \"\$TUI_CONFIG\"
69
- echo \"Created tui.json: \$TUI_CONFIG\"
70
- fi
71
- "
72
-
73
- echo "Done: $host envoy-plugin synced from release $tag"
@@ -1,56 +0,0 @@
1
- import { describe, expect, it, mock } from "bun:test";
2
- import { copyNative, copyToClipboard } from "../clipboard";
3
-
4
- describe("copyToClipboard", () => {
5
- it("copies via the renderer OSC 52 writer and does not fall back when it succeeds", () => {
6
- const copyToClipboardOSC52 = mock((_text: string) => true);
7
- const ok = copyToClipboard("ses_abc", { copyToClipboardOSC52 });
8
- expect(ok).toBe(true);
9
- expect(copyToClipboardOSC52).toHaveBeenCalledWith("ses_abc");
10
- });
11
- });
12
-
13
- describe("copyNative", () => {
14
- it("uses wl-copy on Linux/Wayland when available", () => {
15
- const run = mock((_cmd: string, _args: string[], _text: string) => true);
16
- const ok = copyNative("ses_abc", {
17
- os: "linux",
18
- env: { WAYLAND_DISPLAY: "wayland-0" },
19
- which: (cmd) => cmd === "wl-copy",
20
- run,
21
- });
22
- expect(ok).toBe(true);
23
- expect(run).toHaveBeenCalledWith("wl-copy", [], "ses_abc");
24
- });
25
-
26
- it("falls back to xclip on Linux/X11", () => {
27
- const run = mock((_cmd: string, _args: string[], _text: string) => true);
28
- const ok = copyNative("ses_abc", {
29
- os: "linux",
30
- env: {},
31
- which: (cmd) => cmd === "xclip",
32
- run,
33
- });
34
- expect(ok).toBe(true);
35
- expect(run).toHaveBeenCalledWith("xclip", ["-selection", "clipboard"], "ses_abc");
36
- });
37
-
38
- it("falls back to xsel when xclip is missing", () => {
39
- const run = mock((_cmd: string, _args: string[], _text: string) => true);
40
- copyNative("x", { os: "linux", env: {}, which: (cmd) => cmd === "xsel", run });
41
- expect(run).toHaveBeenCalledWith("xsel", ["--clipboard", "--input"], "x");
42
- });
43
-
44
- it("uses osascript on macOS with escaped quotes", () => {
45
- const run = mock((_cmd: string, _args: string[], _text: string) => true);
46
- copyNative('a"b\\c', { os: "darwin", which: () => true, run });
47
- expect(run).toHaveBeenCalledWith("osascript", ["-e", 'set the clipboard to "a\\"b\\\\c"'], "");
48
- });
49
-
50
- it("returns false on Linux when no clipboard tool is installed", () => {
51
- const run = mock(() => true);
52
- const ok = copyNative("x", { os: "linux", env: {}, which: () => false, run });
53
- expect(ok).toBe(false);
54
- expect(run).not.toHaveBeenCalled();
55
- });
56
- });
@@ -1,116 +0,0 @@
1
- import { describe, expect, it } from "bun:test";
2
- import { buildDispatchMcpEntry, injectEnvoyMcp } from "../dispatch-mcp";
3
-
4
- describe("buildDispatchMcpEntry", () => {
5
- it("returns null when dispatch is undefined", () => {
6
- const result = buildDispatchMcpEntry({ dispatch: undefined });
7
- expect(result).toBeNull();
8
- });
9
-
10
- it("returns null when dispatch.enabled is false", () => {
11
- const result = buildDispatchMcpEntry({
12
- dispatch: { enabled: false, serverUrl: "http://example:8766" },
13
- });
14
- expect(result).toBeNull();
15
- });
16
-
17
- it("builds a local MCP entry pointing at the shim with serverUrl in env", () => {
18
- const result = buildDispatchMcpEntry({
19
- dispatch: {
20
- enabled: true,
21
- serverUrl: "http://sami-agents-mx:8766",
22
- },
23
- shimPath: "/path/to/shim.ts",
24
- runtime: "bun",
25
- });
26
- expect(result).toEqual({
27
- type: "local",
28
- command: ["bun", "/path/to/shim.ts"],
29
- environment: {
30
- DISPATCH_MCP_URL: "http://sami-agents-mx:8766/mcp",
31
- },
32
- enabled: true,
33
- });
34
- });
35
-
36
- it("falls back to localhost:8766 when serverUrl is omitted", () => {
37
- const result = buildDispatchMcpEntry({
38
- dispatch: { enabled: true },
39
- shimPath: "/shim.ts",
40
- });
41
- expect(result?.environment.DISPATCH_MCP_URL).toBe("http://localhost:8766/mcp");
42
- });
43
-
44
- it("strips trailing slashes from serverUrl before appending /mcp", () => {
45
- const result = buildDispatchMcpEntry({
46
- dispatch: { enabled: true, serverUrl: "http://example:8766//" },
47
- shimPath: "/shim.ts",
48
- });
49
- expect(result?.environment.DISPATCH_MCP_URL).toBe("http://example:8766/mcp");
50
- });
51
-
52
- it("uses bun as the default runtime", () => {
53
- const result = buildDispatchMcpEntry({
54
- dispatch: { enabled: true },
55
- shimPath: "/shim.ts",
56
- });
57
- expect(result?.command[0]).toBe("bun");
58
- });
59
-
60
- it("uses the provided runtime override", () => {
61
- const result = buildDispatchMcpEntry({
62
- dispatch: { enabled: true },
63
- shimPath: "/shim.ts",
64
- runtime: "node",
65
- });
66
- expect(result?.command[0]).toBe("node");
67
- });
68
-
69
- it("default shim path resolves to bin/dispatch-mcp-shim.ts in the package root", () => {
70
- const result = buildDispatchMcpEntry({
71
- dispatch: { enabled: true },
72
- });
73
- expect(result?.command[1]).toContain("bin/dispatch-mcp-shim.ts");
74
- });
75
- });
76
-
77
- describe("injectEnvoyMcp", () => {
78
- const entry = {
79
- type: "local" as const,
80
- command: ["bun", "/shim.ts"],
81
- environment: { DISPATCH_MCP_URL: "http://test:8766/mcp" },
82
- enabled: true as const,
83
- };
84
-
85
- it("adds the entry to a cfg that has no mcp block yet", () => {
86
- const cfg: { mcp?: Record<string, unknown> } = {};
87
- const result = injectEnvoyMcp(cfg, entry);
88
- expect(result.warning).toBeUndefined();
89
- expect(cfg.mcp?.envoy).toEqual(entry);
90
- });
91
-
92
- it("is idempotent on its own re-write — second call does not warn", () => {
93
- const cfg: { mcp?: Record<string, unknown> } = {};
94
- injectEnvoyMcp(cfg, entry);
95
- const second = injectEnvoyMcp(cfg, entry);
96
- // No warning when the existing entry already equals what we'd inject.
97
- // This is the common case after InstanceState invalidation re-runs the
98
- // config hook against a Config-service cfg that still has our prior
99
- // mutation — silent no-op, not a TUI stderr alarm.
100
- expect(second.warning).toBeUndefined();
101
- expect(cfg.mcp?.envoy).toEqual(entry);
102
- });
103
-
104
- it("warns and preserves the existing entry when it differs from ours", () => {
105
- const userOverride = {
106
- type: "local" as const,
107
- command: ["node", "/custom-shim.js"],
108
- environment: { DISPATCH_MCP_URL: "http://other:9999/mcp" },
109
- enabled: true as const,
110
- };
111
- const cfg: { mcp?: Record<string, unknown> } = { mcp: { envoy: userOverride } };
112
- const result = injectEnvoyMcp(cfg, entry);
113
- expect(result.warning).toContain("already present");
114
- expect(cfg.mcp?.envoy).toEqual(userOverride);
115
- });
116
- });
@@ -1,66 +0,0 @@
1
- import { describe, expect, it } from "bun:test";
2
- import {
3
- dispatchSubscriptionTopic,
4
- dispatchThreadTopic,
5
- isDispatchTool,
6
- } from "../dispatch-subscribe";
7
-
8
- describe("isDispatchTool", () => {
9
- it("matches the MCP-exposed name and common separators", () => {
10
- expect(isDispatchTool("envoy_dispatch")).toBe(true);
11
- expect(isDispatchTool("dispatch")).toBe(true);
12
- expect(isDispatchTool("envoy.dispatch")).toBe(true);
13
- expect(isDispatchTool("mcp__envoy__dispatch")).toBe(true);
14
- });
15
-
16
- it("does not match unrelated tools", () => {
17
- expect(isDispatchTool("envoy_subscribe")).toBe(false);
18
- expect(isDispatchTool("bash")).toBe(false);
19
- expect(isDispatchTool("dispatcher")).toBe(false);
20
- expect(isDispatchTool("dispatch_thread")).toBe(false);
21
- });
22
- });
23
-
24
- describe("dispatchThreadTopic", () => {
25
- it("builds the wildcard thread topic", () => {
26
- expect(dispatchThreadTopic("sjawhar", "legion", 123)).toBe(
27
- "notifications.github.sjawhar.legion.issue.123.>"
28
- );
29
- });
30
- });
31
-
32
- describe("dispatchSubscriptionTopic", () => {
33
- it("derives the topic from a dispatch tool result JSON", () => {
34
- const output = JSON.stringify({
35
- thread: 742,
36
- url: "https://github.com/sjawhar/legion/issues/742",
37
- });
38
- expect(dispatchSubscriptionTopic("envoy_dispatch", output)).toBe(
39
- "notifications.github.sjawhar.legion.issue.742.>"
40
- );
41
- });
42
-
43
- it("derives owner/repo/number purely from the issue URL", () => {
44
- // Even if a stale/incorrect JSON `thread` were present, the URL is canonical.
45
- const output = '{"thread":1,"url":"https://github.com/acme/Widgets/issues/55"}';
46
- expect(dispatchSubscriptionTopic("envoy_dispatch", output)).toBe(
47
- "notifications.github.acme.Widgets.issue.55.>"
48
- );
49
- });
50
-
51
- it("returns null for non-dispatch tools even with a github URL present", () => {
52
- const output = '{"url":"https://github.com/sjawhar/legion/issues/9"}';
53
- expect(dispatchSubscriptionTopic("envoy_subscribe", output)).toBeNull();
54
- });
55
-
56
- it("returns null when the output has no github issue URL", () => {
57
- expect(dispatchSubscriptionTopic("envoy_dispatch", "created thread 5")).toBeNull();
58
- expect(dispatchSubscriptionTopic("envoy_dispatch", "")).toBeNull();
59
- expect(dispatchSubscriptionTopic("envoy_dispatch", "not json at all")).toBeNull();
60
- });
61
-
62
- it("ignores pull-request URLs (only issue threads carry dispatch replies)", () => {
63
- const output = '{"url":"https://github.com/sjawhar/legion/pull/100"}';
64
- expect(dispatchSubscriptionTopic("envoy_dispatch", output)).toBeNull();
65
- });
66
- });