@sjawhar/opencode-legion-envoy 0.1.0-alpha.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 +21 -0
- package/dist/index.js +12508 -0
- package/package.json +21 -0
- package/scripts/sync-host.sh +8 -0
- package/src/index.ts +177 -0
- package/tsconfig.json +11 -0
package/package.json
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@sjawhar/opencode-legion-envoy",
|
|
3
|
+
"version": "0.1.0-alpha.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"build": "bun build src/index.ts --outdir dist --target bun --format esm",
|
|
9
|
+
"typecheck": "bunx tsc --noEmit",
|
|
10
|
+
"test": "bun test",
|
|
11
|
+
"lint": "bunx biome check src/"
|
|
12
|
+
},
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"@opencode-ai/plugin": "^1.1.19"
|
|
15
|
+
},
|
|
16
|
+
"devDependencies": {
|
|
17
|
+
"@biomejs/biome": "^2.3.14",
|
|
18
|
+
"@types/bun": "latest",
|
|
19
|
+
"typescript": "^5.3.0"
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
host="${1:?usage: sync-host.sh user@host}"
|
|
5
|
+
root="$(cd "$(dirname "$0")/../../.." && pwd)"
|
|
6
|
+
|
|
7
|
+
rsync -az "$root/packages/envoy-plugin/dist/" "$host:~/legion/default/packages/envoy-plugin/dist/"
|
|
8
|
+
rsync -az "$HOME/.dotfiles/opencode/plugins/envoy.ts" "$host:~/.dotfiles/opencode/plugins/envoy.ts"
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { readFileSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { tool } from "@opencode-ai/plugin/tool";
|
|
4
|
+
|
|
5
|
+
const root = process.env.ENVOY_URL ?? "http://127.0.0.1:9020";
|
|
6
|
+
|
|
7
|
+
async function call(path: string, init?: RequestInit) {
|
|
8
|
+
const res = await fetch(`${root}${path}`, init);
|
|
9
|
+
const text = await res.text();
|
|
10
|
+
if (!res.ok) throw new Error(text || `${res.status}`);
|
|
11
|
+
return text;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export default async (input: { serverUrl: URL }) => {
|
|
15
|
+
const shellPid = process.env.OC_SHELL_PID;
|
|
16
|
+
const registryDir = process.env.OC_REGISTRY;
|
|
17
|
+
const file = shellPid && registryDir ? `${registryDir}/${shellPid}.json` : null;
|
|
18
|
+
let activeSessionID: string | null = null;
|
|
19
|
+
|
|
20
|
+
const update = (patch: Record<string, unknown>) => {
|
|
21
|
+
if (!file) return;
|
|
22
|
+
try {
|
|
23
|
+
const data = JSON.parse(readFileSync(file, "utf-8"));
|
|
24
|
+
Object.assign(data, patch);
|
|
25
|
+
writeFileSync(file, `${JSON.stringify(data)}\n`);
|
|
26
|
+
} catch {}
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const currentPort = () => {
|
|
30
|
+
const value = Number.parseInt(input.serverUrl.port, 10);
|
|
31
|
+
if (Number.isFinite(value) && value > 0) return value;
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
const output = execFileSync("ss", ["-tlnp"], { encoding: "utf-8" });
|
|
35
|
+
for (const line of output.split("\n")) {
|
|
36
|
+
if (!line.includes(`pid=${process.pid}`)) continue;
|
|
37
|
+
const parts = line.trim().split(/\s+/);
|
|
38
|
+
const local = parts[3];
|
|
39
|
+
const match = local?.match(/:(\d+)$/);
|
|
40
|
+
if (!match) continue;
|
|
41
|
+
const port = Number.parseInt(match[1], 10);
|
|
42
|
+
if (Number.isFinite(port) && port > 0) return port;
|
|
43
|
+
}
|
|
44
|
+
} catch {}
|
|
45
|
+
|
|
46
|
+
return null;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const syncPort = () => {
|
|
50
|
+
const value = currentPort();
|
|
51
|
+
if (!value) return false;
|
|
52
|
+
update({ port: value });
|
|
53
|
+
return true;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const fetchSession = async (sessionID: string) => {
|
|
57
|
+
try {
|
|
58
|
+
const res = await fetch(`${input.serverUrl}/session/${sessionID}`);
|
|
59
|
+
if (!res.ok) return null;
|
|
60
|
+
const session = await res.json();
|
|
61
|
+
return { id: session.id, title: session.title || "" };
|
|
62
|
+
} catch {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
if (file) {
|
|
68
|
+
syncPort();
|
|
69
|
+
const timer = setInterval(() => {
|
|
70
|
+
if (syncPort()) clearInterval(timer);
|
|
71
|
+
}, 1000);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
event: file
|
|
76
|
+
? async ({ event }: { event: { type?: string; properties?: Record<string, unknown> } }) => {
|
|
77
|
+
syncPort();
|
|
78
|
+
|
|
79
|
+
if (
|
|
80
|
+
event.type === "session.status" &&
|
|
81
|
+
(event.properties?.status as { type?: string } | undefined)?.type === "busy"
|
|
82
|
+
) {
|
|
83
|
+
const sessionID = event.properties?.sessionID as string | undefined;
|
|
84
|
+
if (sessionID && sessionID !== activeSessionID) {
|
|
85
|
+
activeSessionID = sessionID;
|
|
86
|
+
const session = await fetchSession(sessionID);
|
|
87
|
+
if (session) update({ session });
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (event.type === "session.updated") {
|
|
92
|
+
const info = event.properties?.info as { id?: string; title?: string } | undefined;
|
|
93
|
+
if (info && info.id === activeSessionID) {
|
|
94
|
+
update({ session: { id: info.id, title: info.title || "" } });
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (event.type === "session.idle") {
|
|
99
|
+
const sessionID = event.properties?.sessionID as string | undefined;
|
|
100
|
+
if (sessionID && sessionID === activeSessionID) {
|
|
101
|
+
const session = await fetchSession(sessionID);
|
|
102
|
+
if (session) update({ session });
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
: undefined,
|
|
107
|
+
tool: {
|
|
108
|
+
envoy_subscribe: tool({
|
|
109
|
+
description: "Subscribe this session to envoy notification topics",
|
|
110
|
+
args: {
|
|
111
|
+
topics: tool.schema
|
|
112
|
+
.array(tool.schema.string())
|
|
113
|
+
.describe("NATS-style topic patterns to subscribe to"),
|
|
114
|
+
},
|
|
115
|
+
async execute(args, ctx) {
|
|
116
|
+
ctx.metadata({ title: "Envoy subscribe" });
|
|
117
|
+
return call("/v1/interests/subscribe", {
|
|
118
|
+
method: "POST",
|
|
119
|
+
headers: { "Content-Type": "application/json" },
|
|
120
|
+
body: JSON.stringify({
|
|
121
|
+
session_id: ctx.sessionID,
|
|
122
|
+
dir: ctx.directory,
|
|
123
|
+
topics: args.topics,
|
|
124
|
+
}),
|
|
125
|
+
});
|
|
126
|
+
},
|
|
127
|
+
}),
|
|
128
|
+
envoy_unsubscribe: tool({
|
|
129
|
+
description: "Unsubscribe this session from envoy topics, or all if omitted",
|
|
130
|
+
args: {
|
|
131
|
+
topics: tool.schema
|
|
132
|
+
.array(tool.schema.string())
|
|
133
|
+
.optional()
|
|
134
|
+
.describe("Topics to remove, or omit to remove all"),
|
|
135
|
+
},
|
|
136
|
+
async execute(args, ctx) {
|
|
137
|
+
ctx.metadata({ title: "Envoy unsubscribe" });
|
|
138
|
+
return call("/v1/interests/unsubscribe", {
|
|
139
|
+
method: "POST",
|
|
140
|
+
headers: { "Content-Type": "application/json" },
|
|
141
|
+
body: JSON.stringify({
|
|
142
|
+
session_id: ctx.sessionID,
|
|
143
|
+
topics: args.topics ?? [],
|
|
144
|
+
}),
|
|
145
|
+
});
|
|
146
|
+
},
|
|
147
|
+
}),
|
|
148
|
+
envoy_list: tool({
|
|
149
|
+
description: "List envoy subscriptions for this session",
|
|
150
|
+
args: {},
|
|
151
|
+
async execute(_args, ctx) {
|
|
152
|
+
ctx.metadata({ title: "Envoy list" });
|
|
153
|
+
return call(`/v1/interests/${ctx.sessionID}`);
|
|
154
|
+
},
|
|
155
|
+
}),
|
|
156
|
+
envoy_send: tool({
|
|
157
|
+
description: "Send an envoy agent-to-agent message to another session",
|
|
158
|
+
args: {
|
|
159
|
+
target_session: tool.schema.string().describe("Target session ID"),
|
|
160
|
+
message: tool.schema.string().describe("Message to deliver"),
|
|
161
|
+
},
|
|
162
|
+
async execute(args, ctx) {
|
|
163
|
+
ctx.metadata({ title: "Envoy send" });
|
|
164
|
+
return call("/v1/messages/send", {
|
|
165
|
+
method: "POST",
|
|
166
|
+
headers: { "Content-Type": "application/json" },
|
|
167
|
+
body: JSON.stringify({
|
|
168
|
+
source_session: ctx.sessionID,
|
|
169
|
+
target_session: args.target_session,
|
|
170
|
+
message: args.message,
|
|
171
|
+
}),
|
|
172
|
+
});
|
|
173
|
+
},
|
|
174
|
+
}),
|
|
175
|
+
},
|
|
176
|
+
};
|
|
177
|
+
};
|