@arcanemachine/inter-agent-opencode 0.1.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/CHANGELOG.md +5 -0
- package/LICENSE.md +21 -0
- package/README.md +146 -0
- package/dist/client.d.ts +73 -0
- package/dist/client.js +409 -0
- package/dist/config.d.ts +47 -0
- package/dist/config.js +349 -0
- package/dist/errors.d.ts +35 -0
- package/dist/errors.js +95 -0
- package/dist/inbox.d.ts +31 -0
- package/dist/inbox.js +94 -0
- package/dist/protocol.d.ts +132 -0
- package/dist/protocol.js +255 -0
- package/dist/server.d.ts +49 -0
- package/dist/server.js +231 -0
- package/dist/state.d.ts +91 -0
- package/dist/state.js +1306 -0
- package/dist/tui.d.ts +115 -0
- package/dist/tui.js +996 -0
- package/package.json +40 -0
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { type ProtocolErrorCode } from "./errors.js";
|
|
2
|
+
export declare const AUTH_METHOD = "hmac-sha256";
|
|
3
|
+
export declare const SERVER_PROOF_DOMAIN = "inter-agent/server-proof/v1";
|
|
4
|
+
export declare const CLIENT_PROOF_DOMAIN = "inter-agent/client-proof/v1";
|
|
5
|
+
export declare const DEFAULT_DIRECT_TEXT_MAX: number;
|
|
6
|
+
export declare const DEFAULT_BROADCAST_TEXT_MAX: number;
|
|
7
|
+
export type JsonObject = Record<string, unknown>;
|
|
8
|
+
export type ProtocolFrame = JsonObject & {
|
|
9
|
+
op: string;
|
|
10
|
+
};
|
|
11
|
+
export type Hello = ProtocolFrame & {
|
|
12
|
+
op: "hello";
|
|
13
|
+
auth: {
|
|
14
|
+
method: typeof AUTH_METHOD;
|
|
15
|
+
client_nonce: string;
|
|
16
|
+
};
|
|
17
|
+
role: "agent" | "control";
|
|
18
|
+
session_id: string;
|
|
19
|
+
name?: string;
|
|
20
|
+
label: string | null;
|
|
21
|
+
capabilities: JsonObject;
|
|
22
|
+
};
|
|
23
|
+
export type AuthChallenge = ProtocolFrame & {
|
|
24
|
+
op: "auth_challenge";
|
|
25
|
+
method: typeof AUTH_METHOD;
|
|
26
|
+
server_nonce: string;
|
|
27
|
+
server_proof: string;
|
|
28
|
+
};
|
|
29
|
+
export type AuthResponse = ProtocolFrame & {
|
|
30
|
+
op: "auth_response";
|
|
31
|
+
client_proof: string;
|
|
32
|
+
};
|
|
33
|
+
export type Welcome = ProtocolFrame & {
|
|
34
|
+
op: "welcome";
|
|
35
|
+
session_id: string;
|
|
36
|
+
assigned_name: string;
|
|
37
|
+
capabilities: JsonObject;
|
|
38
|
+
};
|
|
39
|
+
export type Message = ProtocolFrame & {
|
|
40
|
+
op: "msg";
|
|
41
|
+
msg_id: string;
|
|
42
|
+
from: string;
|
|
43
|
+
from_name: string;
|
|
44
|
+
ts: string;
|
|
45
|
+
to?: string | null;
|
|
46
|
+
channel?: string;
|
|
47
|
+
text?: string;
|
|
48
|
+
custom_type?: string;
|
|
49
|
+
payload?: unknown;
|
|
50
|
+
};
|
|
51
|
+
export type ProtocolErrorFrame = ProtocolFrame & {
|
|
52
|
+
op: "error";
|
|
53
|
+
code: ProtocolErrorCode;
|
|
54
|
+
message: string;
|
|
55
|
+
};
|
|
56
|
+
export type SendFrame = ProtocolFrame & {
|
|
57
|
+
op: "send";
|
|
58
|
+
to: string;
|
|
59
|
+
text: string;
|
|
60
|
+
from_name?: string;
|
|
61
|
+
};
|
|
62
|
+
export type BroadcastFrame = ProtocolFrame & {
|
|
63
|
+
op: "broadcast";
|
|
64
|
+
text: string;
|
|
65
|
+
from_name?: string;
|
|
66
|
+
};
|
|
67
|
+
export type ListFrame = ProtocolFrame & {
|
|
68
|
+
op: "list";
|
|
69
|
+
};
|
|
70
|
+
export type SessionInfo = {
|
|
71
|
+
session_id: string;
|
|
72
|
+
name: string;
|
|
73
|
+
label: string | null;
|
|
74
|
+
};
|
|
75
|
+
export type ListOkFrame = ProtocolFrame & {
|
|
76
|
+
op: "list_ok";
|
|
77
|
+
sessions: SessionInfo[];
|
|
78
|
+
};
|
|
79
|
+
export declare function generateNonce(): string;
|
|
80
|
+
export declare function buildHello(input: {
|
|
81
|
+
role: "agent" | "control";
|
|
82
|
+
sessionId: string;
|
|
83
|
+
name?: string;
|
|
84
|
+
label?: string | null;
|
|
85
|
+
capabilities?: JsonObject;
|
|
86
|
+
clientNonce?: string;
|
|
87
|
+
}): Hello;
|
|
88
|
+
export declare function canonicalHelloTranscript(hello: JsonObject): string;
|
|
89
|
+
export declare function canonicalJson(value: unknown): string;
|
|
90
|
+
export declare function hmacProof(secret: string, domain: string, input: {
|
|
91
|
+
clientNonce: string;
|
|
92
|
+
serverNonce: string;
|
|
93
|
+
hello: JsonObject;
|
|
94
|
+
}): string;
|
|
95
|
+
export declare function serverProof(secret: string, input: {
|
|
96
|
+
clientNonce: string;
|
|
97
|
+
serverNonce: string;
|
|
98
|
+
hello: JsonObject;
|
|
99
|
+
}): string;
|
|
100
|
+
export declare function clientProof(secret: string, input: {
|
|
101
|
+
clientNonce: string;
|
|
102
|
+
serverNonce: string;
|
|
103
|
+
hello: JsonObject;
|
|
104
|
+
}): string;
|
|
105
|
+
export declare function verifyProof(actual: string, expected: string): boolean;
|
|
106
|
+
export declare function verifyServerProof(proof: string, secret: string, input: {
|
|
107
|
+
clientNonce: string;
|
|
108
|
+
serverNonce: string;
|
|
109
|
+
hello: JsonObject;
|
|
110
|
+
}): boolean;
|
|
111
|
+
export declare function verifyClientProof(proof: string, secret: string, input: {
|
|
112
|
+
clientNonce: string;
|
|
113
|
+
serverNonce: string;
|
|
114
|
+
hello: JsonObject;
|
|
115
|
+
}): boolean;
|
|
116
|
+
export declare function buildAuthResponse(secret: string, input: {
|
|
117
|
+
clientNonce: string;
|
|
118
|
+
serverNonce: string;
|
|
119
|
+
hello: JsonObject;
|
|
120
|
+
}): AuthResponse;
|
|
121
|
+
export declare function validateName(value: unknown): value is string;
|
|
122
|
+
export declare function utf8ByteLength(value: string): number;
|
|
123
|
+
export declare function validateText(value: unknown, maxBytes: number): value is string;
|
|
124
|
+
export declare function decodeFrame(data: unknown): string;
|
|
125
|
+
export declare function parseFrame(data: unknown): ProtocolFrame;
|
|
126
|
+
export declare function parseHello(frame: ProtocolFrame): Hello;
|
|
127
|
+
export declare function parseAuthChallenge(frame: ProtocolFrame): AuthChallenge;
|
|
128
|
+
export declare function parseWelcome(frame: ProtocolFrame): Welcome;
|
|
129
|
+
export declare function parseError(frame: ProtocolFrame): ProtocolErrorFrame;
|
|
130
|
+
export declare function parseListOk(frame: ProtocolFrame): ListOkFrame;
|
|
131
|
+
export declare function parseMessage(frame: ProtocolFrame): Message;
|
|
132
|
+
export declare function isRecord(value: unknown): value is JsonObject;
|
package/dist/protocol.js
ADDED
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import { createHmac, randomBytes, timingSafeEqual } from "node:crypto";
|
|
2
|
+
import { TextDecoder } from "node:util";
|
|
3
|
+
import { ProtocolError, isProtocolErrorCode, } from "./errors.js";
|
|
4
|
+
export const AUTH_METHOD = "hmac-sha256";
|
|
5
|
+
export const SERVER_PROOF_DOMAIN = "inter-agent/server-proof/v1";
|
|
6
|
+
export const CLIENT_PROOF_DOMAIN = "inter-agent/client-proof/v1";
|
|
7
|
+
export const DEFAULT_DIRECT_TEXT_MAX = 2 * 1024 * 1024;
|
|
8
|
+
export const DEFAULT_BROADCAST_TEXT_MAX = 512 * 1024;
|
|
9
|
+
const decoder = new TextDecoder("utf-8", { fatal: true });
|
|
10
|
+
export function generateNonce() {
|
|
11
|
+
return randomBytes(32).toString("base64url");
|
|
12
|
+
}
|
|
13
|
+
export function buildHello(input) {
|
|
14
|
+
const hello = {
|
|
15
|
+
op: "hello",
|
|
16
|
+
auth: {
|
|
17
|
+
method: AUTH_METHOD,
|
|
18
|
+
client_nonce: input.clientNonce ?? generateNonce(),
|
|
19
|
+
},
|
|
20
|
+
role: input.role,
|
|
21
|
+
session_id: input.sessionId,
|
|
22
|
+
label: input.label ?? null,
|
|
23
|
+
capabilities: input.capabilities ?? {},
|
|
24
|
+
};
|
|
25
|
+
if (input.name !== undefined)
|
|
26
|
+
hello.name = input.name;
|
|
27
|
+
return hello;
|
|
28
|
+
}
|
|
29
|
+
export function canonicalHelloTranscript(hello) {
|
|
30
|
+
return canonicalJson({
|
|
31
|
+
capabilities: isRecord(hello.capabilities) ? hello.capabilities : {},
|
|
32
|
+
label: hello.label ?? null,
|
|
33
|
+
name: hello.name ?? null,
|
|
34
|
+
role: hello.role ?? null,
|
|
35
|
+
session_id: hello.session_id ?? null,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
function compareUnicodeKeys(left, right) {
|
|
39
|
+
const leftCodePoints = Array.from(left, (character) => character.codePointAt(0) ?? 0);
|
|
40
|
+
const rightCodePoints = Array.from(right, (character) => character.codePointAt(0) ?? 0);
|
|
41
|
+
const length = Math.min(leftCodePoints.length, rightCodePoints.length);
|
|
42
|
+
for (let index = 0; index < length; index += 1) {
|
|
43
|
+
const leftCodePoint = leftCodePoints[index] ?? 0;
|
|
44
|
+
const rightCodePoint = rightCodePoints[index] ?? 0;
|
|
45
|
+
if (leftCodePoint !== rightCodePoint)
|
|
46
|
+
return leftCodePoint - rightCodePoint;
|
|
47
|
+
}
|
|
48
|
+
return leftCodePoints.length - rightCodePoints.length;
|
|
49
|
+
}
|
|
50
|
+
export function canonicalJson(value) {
|
|
51
|
+
if (value === undefined)
|
|
52
|
+
return "null";
|
|
53
|
+
if (value === null ||
|
|
54
|
+
typeof value === "string" ||
|
|
55
|
+
typeof value === "boolean" ||
|
|
56
|
+
typeof value === "number") {
|
|
57
|
+
return JSON.stringify(value);
|
|
58
|
+
}
|
|
59
|
+
if (Array.isArray(value))
|
|
60
|
+
return `[${value.map((item) => canonicalJson(item)).join(",")}]`;
|
|
61
|
+
if (isRecord(value)) {
|
|
62
|
+
return `{${Object.keys(value)
|
|
63
|
+
.sort(compareUnicodeKeys)
|
|
64
|
+
.map((key) => `${JSON.stringify(key)}:${canonicalJson(value[key])}`)
|
|
65
|
+
.join(",")}}`;
|
|
66
|
+
}
|
|
67
|
+
return "null";
|
|
68
|
+
}
|
|
69
|
+
function proofMessage(domain, clientNonce, serverNonce, hello) {
|
|
70
|
+
return `${domain}\n${clientNonce}\n${serverNonce}\n${canonicalHelloTranscript(hello)}`;
|
|
71
|
+
}
|
|
72
|
+
export function hmacProof(secret, domain, input) {
|
|
73
|
+
return createHmac("sha256", secret)
|
|
74
|
+
.update(proofMessage(domain, input.clientNonce, input.serverNonce, input.hello), "utf8")
|
|
75
|
+
.digest("hex");
|
|
76
|
+
}
|
|
77
|
+
export function serverProof(secret, input) {
|
|
78
|
+
return hmacProof(secret, SERVER_PROOF_DOMAIN, input);
|
|
79
|
+
}
|
|
80
|
+
export function clientProof(secret, input) {
|
|
81
|
+
return hmacProof(secret, CLIENT_PROOF_DOMAIN, input);
|
|
82
|
+
}
|
|
83
|
+
export function verifyProof(actual, expected) {
|
|
84
|
+
const left = Buffer.from(actual, "utf8");
|
|
85
|
+
const right = Buffer.from(expected, "utf8");
|
|
86
|
+
return left.length === right.length && timingSafeEqual(left, right);
|
|
87
|
+
}
|
|
88
|
+
export function verifyServerProof(proof, secret, input) {
|
|
89
|
+
return verifyProof(proof, serverProof(secret, input));
|
|
90
|
+
}
|
|
91
|
+
export function verifyClientProof(proof, secret, input) {
|
|
92
|
+
return verifyProof(proof, clientProof(secret, input));
|
|
93
|
+
}
|
|
94
|
+
export function buildAuthResponse(secret, input) {
|
|
95
|
+
return { op: "auth_response", client_proof: clientProof(secret, input) };
|
|
96
|
+
}
|
|
97
|
+
export function validateName(value) {
|
|
98
|
+
return typeof value === "string" && /^[a-z0-9][a-z0-9-]{0,39}$/.test(value);
|
|
99
|
+
}
|
|
100
|
+
export function utf8ByteLength(value) {
|
|
101
|
+
return Buffer.byteLength(value, "utf8");
|
|
102
|
+
}
|
|
103
|
+
export function validateText(value, maxBytes) {
|
|
104
|
+
return typeof value === "string" && utf8ByteLength(value) <= maxBytes;
|
|
105
|
+
}
|
|
106
|
+
export function decodeFrame(data) {
|
|
107
|
+
try {
|
|
108
|
+
if (typeof data === "string")
|
|
109
|
+
return data;
|
|
110
|
+
if (data instanceof ArrayBuffer)
|
|
111
|
+
return decoder.decode(new Uint8Array(data));
|
|
112
|
+
if (ArrayBuffer.isView(data))
|
|
113
|
+
return decoder.decode(new Uint8Array(data.buffer, data.byteOffset, data.byteLength));
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
throw new ProtocolError("websocket frame is not valid UTF-8");
|
|
117
|
+
}
|
|
118
|
+
throw new ProtocolError("websocket frame must be UTF-8 text or binary data");
|
|
119
|
+
}
|
|
120
|
+
export function parseFrame(data) {
|
|
121
|
+
let parsed;
|
|
122
|
+
try {
|
|
123
|
+
parsed = JSON.parse(decodeFrame(data));
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
throw new ProtocolError("websocket frame is not valid UTF-8 JSON");
|
|
127
|
+
}
|
|
128
|
+
if (!isRecord(parsed) || typeof parsed.op !== "string")
|
|
129
|
+
throw new ProtocolError("protocol frame must be a JSON object with op");
|
|
130
|
+
return parsed;
|
|
131
|
+
}
|
|
132
|
+
export function parseHello(frame) {
|
|
133
|
+
if (frame.op !== "hello" ||
|
|
134
|
+
!isRecord(frame.auth) ||
|
|
135
|
+
frame.auth.method !== AUTH_METHOD ||
|
|
136
|
+
typeof frame.auth.client_nonce !== "string" ||
|
|
137
|
+
!frame.auth.client_nonce) {
|
|
138
|
+
throw new ProtocolError("invalid hello frame");
|
|
139
|
+
}
|
|
140
|
+
if (frame.role !== "agent" && frame.role !== "control")
|
|
141
|
+
throw new ProtocolError("invalid hello role");
|
|
142
|
+
if (typeof frame.session_id !== "string" || !frame.session_id)
|
|
143
|
+
throw new ProtocolError("invalid hello session_id");
|
|
144
|
+
if (frame.role === "agent" && !validateName(frame.name))
|
|
145
|
+
throw new ProtocolError("invalid hello name");
|
|
146
|
+
if (frame.name !== undefined && typeof frame.name !== "string")
|
|
147
|
+
throw new ProtocolError("invalid hello name");
|
|
148
|
+
if (frame.label !== undefined &&
|
|
149
|
+
frame.label !== null &&
|
|
150
|
+
typeof frame.label !== "string")
|
|
151
|
+
throw new ProtocolError("invalid hello label");
|
|
152
|
+
if (!isRecord(frame.capabilities))
|
|
153
|
+
throw new ProtocolError("invalid hello capabilities");
|
|
154
|
+
return {
|
|
155
|
+
...frame,
|
|
156
|
+
label: frame.label === undefined ? null : frame.label,
|
|
157
|
+
capabilities: frame.capabilities,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
export function parseAuthChallenge(frame) {
|
|
161
|
+
if (frame.op !== "auth_challenge" ||
|
|
162
|
+
frame.method !== AUTH_METHOD ||
|
|
163
|
+
typeof frame.server_nonce !== "string" ||
|
|
164
|
+
!frame.server_nonce ||
|
|
165
|
+
typeof frame.server_proof !== "string" ||
|
|
166
|
+
!frame.server_proof) {
|
|
167
|
+
throw new ProtocolError("invalid auth challenge");
|
|
168
|
+
}
|
|
169
|
+
return frame;
|
|
170
|
+
}
|
|
171
|
+
export function parseWelcome(frame) {
|
|
172
|
+
if (frame.op !== "welcome" ||
|
|
173
|
+
typeof frame.session_id !== "string" ||
|
|
174
|
+
typeof frame.assigned_name !== "string" ||
|
|
175
|
+
!isRecord(frame.capabilities)) {
|
|
176
|
+
throw new ProtocolError("invalid welcome frame");
|
|
177
|
+
}
|
|
178
|
+
const core = frame.capabilities.core;
|
|
179
|
+
if (!isRecord(core) ||
|
|
180
|
+
core.version !== "0.1" ||
|
|
181
|
+
frame.capabilities.channels !== true ||
|
|
182
|
+
frame.capabilities.rate_limit !== false) {
|
|
183
|
+
throw new ProtocolError("invalid welcome capabilities");
|
|
184
|
+
}
|
|
185
|
+
return frame;
|
|
186
|
+
}
|
|
187
|
+
export function parseError(frame) {
|
|
188
|
+
if (frame.op !== "error" ||
|
|
189
|
+
!isProtocolErrorCode(frame.code) ||
|
|
190
|
+
typeof frame.message !== "string")
|
|
191
|
+
throw new ProtocolError("invalid protocol error frame");
|
|
192
|
+
return frame;
|
|
193
|
+
}
|
|
194
|
+
export function parseListOk(frame) {
|
|
195
|
+
if (frame.op !== "list_ok" || !Array.isArray(frame.sessions))
|
|
196
|
+
throw new ProtocolError("invalid list response");
|
|
197
|
+
const sessions = frame.sessions.map((value) => {
|
|
198
|
+
if (!isRecord(value) ||
|
|
199
|
+
typeof value.session_id !== "string" ||
|
|
200
|
+
typeof value.name !== "string" ||
|
|
201
|
+
(value.label !== null && typeof value.label !== "string")) {
|
|
202
|
+
throw new ProtocolError("invalid list session");
|
|
203
|
+
}
|
|
204
|
+
return {
|
|
205
|
+
session_id: value.session_id,
|
|
206
|
+
name: value.name,
|
|
207
|
+
label: value.label,
|
|
208
|
+
};
|
|
209
|
+
});
|
|
210
|
+
return { ...frame, sessions };
|
|
211
|
+
}
|
|
212
|
+
export function parseMessage(frame) {
|
|
213
|
+
if (frame.op !== "msg" ||
|
|
214
|
+
typeof frame.msg_id !== "string" ||
|
|
215
|
+
typeof frame.from !== "string" ||
|
|
216
|
+
typeof frame.from_name !== "string" ||
|
|
217
|
+
typeof frame.ts !== "string") {
|
|
218
|
+
throw new ProtocolError("invalid message frame");
|
|
219
|
+
}
|
|
220
|
+
if ("to" in frame && frame.to !== null && typeof frame.to !== "string")
|
|
221
|
+
throw new ProtocolError("invalid message target");
|
|
222
|
+
if ("channel" in frame &&
|
|
223
|
+
(typeof frame.channel !== "string" || !validateName(frame.channel)))
|
|
224
|
+
throw new ProtocolError("invalid message channel");
|
|
225
|
+
const hasText = "text" in frame;
|
|
226
|
+
const hasCustom = "custom_type" in frame || "payload" in frame;
|
|
227
|
+
if (hasText === hasCustom)
|
|
228
|
+
throw new ProtocolError("message must contain exactly one payload form");
|
|
229
|
+
if (hasText && typeof frame.text !== "string")
|
|
230
|
+
throw new ProtocolError("invalid message text");
|
|
231
|
+
if (hasCustom) {
|
|
232
|
+
if (typeof frame.custom_type !== "string" || frame.custom_type.length === 0)
|
|
233
|
+
throw new ProtocolError("invalid custom message type");
|
|
234
|
+
if (!("payload" in frame))
|
|
235
|
+
throw new ProtocolError("custom message payload is missing");
|
|
236
|
+
if (!isJsonValue(frame.payload))
|
|
237
|
+
throw new ProtocolError("invalid custom message payload");
|
|
238
|
+
}
|
|
239
|
+
return frame;
|
|
240
|
+
}
|
|
241
|
+
function isJsonValue(value) {
|
|
242
|
+
if (value === null || typeof value === "string" || typeof value === "boolean")
|
|
243
|
+
return true;
|
|
244
|
+
if (typeof value === "number")
|
|
245
|
+
return Number.isFinite(value);
|
|
246
|
+
if (Array.isArray(value))
|
|
247
|
+
return value.every((item) => isJsonValue(item));
|
|
248
|
+
if (isRecord(value))
|
|
249
|
+
return Object.values(value).every((item) => isJsonValue(item));
|
|
250
|
+
return false;
|
|
251
|
+
}
|
|
252
|
+
export function isRecord(value) {
|
|
253
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
254
|
+
}
|
|
255
|
+
//# sourceMappingURL=protocol.js.map
|
package/dist/server.d.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { PluginModule } from "@opencode-ai/plugin";
|
|
2
|
+
import { type ToolContext, type ToolResult } from "@opencode-ai/plugin";
|
|
3
|
+
import { type WebSocketFactory } from "./client.js";
|
|
4
|
+
type ServerToolOptions = {
|
|
5
|
+
websocketFactory?: WebSocketFactory;
|
|
6
|
+
};
|
|
7
|
+
export declare function createServerTools(options?: ServerToolOptions): {
|
|
8
|
+
inter_agent_send: {
|
|
9
|
+
description: string;
|
|
10
|
+
args: {
|
|
11
|
+
to: import("zod").ZodString;
|
|
12
|
+
text: import("zod").ZodString;
|
|
13
|
+
};
|
|
14
|
+
execute(args: {
|
|
15
|
+
to: string;
|
|
16
|
+
text: string;
|
|
17
|
+
}, context: ToolContext): Promise<ToolResult>;
|
|
18
|
+
};
|
|
19
|
+
inter_agent_broadcast: {
|
|
20
|
+
description: string;
|
|
21
|
+
args: {
|
|
22
|
+
text: import("zod").ZodString;
|
|
23
|
+
};
|
|
24
|
+
execute(args: {
|
|
25
|
+
text: string;
|
|
26
|
+
}, context: ToolContext): Promise<ToolResult>;
|
|
27
|
+
};
|
|
28
|
+
inter_agent_list: {
|
|
29
|
+
description: string;
|
|
30
|
+
args: {};
|
|
31
|
+
execute(args: Record<string, never>, context: ToolContext): Promise<ToolResult>;
|
|
32
|
+
};
|
|
33
|
+
inter_agent_status: {
|
|
34
|
+
description: string;
|
|
35
|
+
args: {};
|
|
36
|
+
execute(args: Record<string, never>, context: ToolContext): Promise<ToolResult>;
|
|
37
|
+
};
|
|
38
|
+
inter_agent_read_messages: {
|
|
39
|
+
description: string;
|
|
40
|
+
args: {
|
|
41
|
+
count: import("zod").ZodOptional<import("zod").ZodNumber>;
|
|
42
|
+
};
|
|
43
|
+
execute(args: {
|
|
44
|
+
count?: number | undefined;
|
|
45
|
+
}, context: ToolContext): Promise<ToolResult>;
|
|
46
|
+
};
|
|
47
|
+
};
|
|
48
|
+
declare const server: PluginModule;
|
|
49
|
+
export default server;
|
package/dist/server.js
ADDED
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import { tool } from "@opencode-ai/plugin";
|
|
2
|
+
import { assertSupportedEndpoint, resolveEndpoint, resolveSecret, } from "./config.js";
|
|
3
|
+
import { broadcast, listSessions, sendDirect, } from "./client.js";
|
|
4
|
+
import { DEFAULT_BROADCAST_TEXT_MAX, DEFAULT_DIRECT_TEXT_MAX, validateName, validateText, } from "./protocol.js";
|
|
5
|
+
import { canonicalWorkspacePath, hashScope, readPreferences, resolveLease, workspaceKey, } from "./state.js";
|
|
6
|
+
import { readInboxFile } from "./inbox.js";
|
|
7
|
+
const DEFAULT_INBOX_COUNT = 20;
|
|
8
|
+
const MAX_INBOX_COUNT = 100;
|
|
9
|
+
function result(title, value, metadata = {}) {
|
|
10
|
+
return {
|
|
11
|
+
title,
|
|
12
|
+
output: JSON.stringify(value),
|
|
13
|
+
metadata,
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
function contextWorkspacePath(context) {
|
|
17
|
+
const path = context.worktree || context.directory;
|
|
18
|
+
if (!path)
|
|
19
|
+
throw new Error("OpenCode project scope is unavailable");
|
|
20
|
+
return canonicalWorkspacePath(path);
|
|
21
|
+
}
|
|
22
|
+
function scopeFor(context, endpoint) {
|
|
23
|
+
const workspacePath = contextWorkspacePath(context);
|
|
24
|
+
return {
|
|
25
|
+
endpoint,
|
|
26
|
+
workspacePath,
|
|
27
|
+
workspaceHash: workspaceKey(workspacePath),
|
|
28
|
+
sessionHash: hashScope(context.sessionID),
|
|
29
|
+
sessionID: context.sessionID,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
function exactLease(lease, scope) {
|
|
33
|
+
return (lease.workspacePath === scope.workspacePath &&
|
|
34
|
+
lease.workspaceHash === scope.workspaceHash &&
|
|
35
|
+
lease.openCodeSessionID === scope.sessionID &&
|
|
36
|
+
lease.sessionHash === scope.sessionHash &&
|
|
37
|
+
lease.host === scope.endpoint.host &&
|
|
38
|
+
lease.port === scope.endpoint.port &&
|
|
39
|
+
lease.tls === scope.endpoint.tls &&
|
|
40
|
+
validateName(lease.name));
|
|
41
|
+
}
|
|
42
|
+
function unavailableSession() {
|
|
43
|
+
return new Error("this OpenCode session is disconnected or unavailable; connect it before using inter-agent messaging");
|
|
44
|
+
}
|
|
45
|
+
function preferenceMatchesLease(preferences, lease) {
|
|
46
|
+
return (!preferences ||
|
|
47
|
+
(preferences.name === lease.name && preferences.label === lease.label));
|
|
48
|
+
}
|
|
49
|
+
async function sessionScope(context) {
|
|
50
|
+
return scopeFor(context, await resolveEndpoint());
|
|
51
|
+
}
|
|
52
|
+
async function callerScope(context) {
|
|
53
|
+
const scope = await sessionScope(context);
|
|
54
|
+
assertSupportedEndpoint(scope.endpoint);
|
|
55
|
+
const resolution = resolveLease(scope.endpoint.dataDir, {
|
|
56
|
+
workspacePath: scope.workspacePath,
|
|
57
|
+
openCodeSessionID: scope.sessionID,
|
|
58
|
+
});
|
|
59
|
+
if (resolution.check !== "fresh" ||
|
|
60
|
+
!resolution.lease ||
|
|
61
|
+
!exactLease(resolution.lease, scope))
|
|
62
|
+
throw unavailableSession();
|
|
63
|
+
const preferences = readPreferences(scope.endpoint.dataDir, scope.workspaceHash, scope.sessionHash, {
|
|
64
|
+
workspacePath: scope.workspacePath,
|
|
65
|
+
openCodeSessionID: scope.sessionID,
|
|
66
|
+
});
|
|
67
|
+
if (!preferenceMatchesLease(preferences, resolution.lease))
|
|
68
|
+
throw unavailableSession();
|
|
69
|
+
return { ...scope, lease: resolution.lease };
|
|
70
|
+
}
|
|
71
|
+
function operationOptions(context, endpoint, options) {
|
|
72
|
+
return {
|
|
73
|
+
endpoint,
|
|
74
|
+
secret: resolveSecret().secret,
|
|
75
|
+
signal: context.abort,
|
|
76
|
+
websocketFactory: options.websocketFactory,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
function validateCount(count) {
|
|
80
|
+
const value = count === undefined ? DEFAULT_INBOX_COUNT : count;
|
|
81
|
+
if (typeof value !== "number" ||
|
|
82
|
+
!Number.isInteger(value) ||
|
|
83
|
+
value < 1 ||
|
|
84
|
+
value > MAX_INBOX_COUNT)
|
|
85
|
+
throw new Error("count must be an integer between 1 and 100");
|
|
86
|
+
return value;
|
|
87
|
+
}
|
|
88
|
+
function sessionsValue(sessions) {
|
|
89
|
+
return {
|
|
90
|
+
sessions: sessions.map((session) => ({
|
|
91
|
+
session_id: session.session_id,
|
|
92
|
+
name: session.name,
|
|
93
|
+
label: session.label,
|
|
94
|
+
})),
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
function statusValue(scope, lease, leaseState, server, inboxCount, name, label) {
|
|
98
|
+
return {
|
|
99
|
+
endpoint: `${scope.endpoint.host}:${scope.endpoint.port}`,
|
|
100
|
+
supported: scope.endpoint.supported,
|
|
101
|
+
server,
|
|
102
|
+
session: lease ? "connected" : "disconnected",
|
|
103
|
+
name,
|
|
104
|
+
label,
|
|
105
|
+
lease: leaseState,
|
|
106
|
+
inbox: inboxCount,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
async function executeSend(args, context, options) {
|
|
110
|
+
if (!validateName(args.to))
|
|
111
|
+
throw new Error("to must be a valid inter-agent name");
|
|
112
|
+
if (typeof args.text !== "string" ||
|
|
113
|
+
args.text.length === 0 ||
|
|
114
|
+
!validateText(args.text, DEFAULT_DIRECT_TEXT_MAX))
|
|
115
|
+
throw new Error("text is empty or exceeds the direct message limit");
|
|
116
|
+
const caller = await callerScope(context);
|
|
117
|
+
await sendDirect(args.to, args.text, caller.lease.name, operationOptions(context, caller.endpoint, options));
|
|
118
|
+
return result("Inter-agent message sent", {
|
|
119
|
+
sent: true,
|
|
120
|
+
to: args.to,
|
|
121
|
+
from_name: caller.lease.name,
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
async function executeBroadcast(args, context, options) {
|
|
125
|
+
if (typeof args.text !== "string" ||
|
|
126
|
+
args.text.length === 0 ||
|
|
127
|
+
!validateText(args.text, DEFAULT_BROADCAST_TEXT_MAX))
|
|
128
|
+
throw new Error("text is empty or exceeds the broadcast message limit");
|
|
129
|
+
const caller = await callerScope(context);
|
|
130
|
+
await broadcast(args.text, caller.lease.name, operationOptions(context, caller.endpoint, options));
|
|
131
|
+
return result("Inter-agent broadcast sent", {
|
|
132
|
+
sent: true,
|
|
133
|
+
from_name: caller.lease.name,
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
async function executeList(context, options) {
|
|
137
|
+
const scope = await sessionScope(context);
|
|
138
|
+
assertSupportedEndpoint(scope.endpoint);
|
|
139
|
+
const listed = await listSessions(operationOptions(context, scope.endpoint, options));
|
|
140
|
+
return result("Inter-agent sessions", sessionsValue(listed.sessions), {
|
|
141
|
+
count: listed.sessions.length,
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
async function executeStatus(context, options) {
|
|
145
|
+
const scope = await sessionScope(context);
|
|
146
|
+
const resolution = resolveLease(scope.endpoint.dataDir, {
|
|
147
|
+
workspacePath: scope.workspacePath,
|
|
148
|
+
openCodeSessionID: scope.sessionID,
|
|
149
|
+
});
|
|
150
|
+
const preferences = readPreferences(scope.endpoint.dataDir, scope.workspaceHash, scope.sessionHash, {
|
|
151
|
+
workspacePath: scope.workspacePath,
|
|
152
|
+
openCodeSessionID: scope.sessionID,
|
|
153
|
+
});
|
|
154
|
+
const lease = resolution.check === "fresh" &&
|
|
155
|
+
resolution.lease &&
|
|
156
|
+
exactLease(resolution.lease, scope) &&
|
|
157
|
+
preferenceMatchesLease(preferences, resolution.lease)
|
|
158
|
+
? resolution.lease
|
|
159
|
+
: undefined;
|
|
160
|
+
let server = "unreachable";
|
|
161
|
+
if (scope.endpoint.supported) {
|
|
162
|
+
try {
|
|
163
|
+
await listSessions(operationOptions(context, scope.endpoint, options));
|
|
164
|
+
server = "reachable";
|
|
165
|
+
}
|
|
166
|
+
catch {
|
|
167
|
+
server = "unreachable";
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
const inboxCount = readInboxFile(scope.endpoint.dataDir, scope.workspaceHash, scope.sessionHash).messages.length;
|
|
171
|
+
const value = statusValue(scope, lease, lease ? "fresh" : resolution.check, server, inboxCount, lease?.name ?? preferences?.name ?? null, lease?.label ?? preferences?.label ?? null);
|
|
172
|
+
return result("Inter-agent status", value, {
|
|
173
|
+
session: value.session,
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
async function executeInbox(args, context) {
|
|
177
|
+
const count = validateCount(args.count);
|
|
178
|
+
const scope = await sessionScope(context);
|
|
179
|
+
const messages = readInboxFile(scope.endpoint.dataDir, scope.workspaceHash, scope.sessionHash).messages.slice(-count);
|
|
180
|
+
return result("Inter-agent inbox", { messages }, { count: messages.length });
|
|
181
|
+
}
|
|
182
|
+
export function createServerTools(options = {}) {
|
|
183
|
+
return {
|
|
184
|
+
inter_agent_send: tool({
|
|
185
|
+
description: "Send a direct inter-agent message from this OpenCode session.",
|
|
186
|
+
args: {
|
|
187
|
+
to: tool.schema.string().describe("Recipient agent name"),
|
|
188
|
+
text: tool.schema.string().describe("Message text"),
|
|
189
|
+
},
|
|
190
|
+
execute: (args, context) => executeSend(args, context, options),
|
|
191
|
+
}),
|
|
192
|
+
inter_agent_broadcast: tool({
|
|
193
|
+
description: "Broadcast an inter-agent message to everyone only when explicitly requested.",
|
|
194
|
+
args: {
|
|
195
|
+
text: tool.schema.string().describe("Message text"),
|
|
196
|
+
},
|
|
197
|
+
execute: (args, context) => executeBroadcast(args, context, options),
|
|
198
|
+
}),
|
|
199
|
+
inter_agent_list: tool({
|
|
200
|
+
description: "List currently connected inter-agent sessions.",
|
|
201
|
+
args: {},
|
|
202
|
+
execute: (_args, context) => executeList(context, options),
|
|
203
|
+
}),
|
|
204
|
+
inter_agent_status: tool({
|
|
205
|
+
description: "Show inter-agent status for this exact OpenCode session.",
|
|
206
|
+
args: {},
|
|
207
|
+
execute: (_args, context) => executeStatus(context, options),
|
|
208
|
+
}),
|
|
209
|
+
inter_agent_read_messages: tool({
|
|
210
|
+
description: "Read this exact OpenCode session's durable inter-agent inbox.",
|
|
211
|
+
args: {
|
|
212
|
+
count: tool.schema
|
|
213
|
+
.number()
|
|
214
|
+
.int()
|
|
215
|
+
.min(1)
|
|
216
|
+
.max(MAX_INBOX_COUNT)
|
|
217
|
+
.optional()
|
|
218
|
+
.describe("Maximum messages to return (1-100)"),
|
|
219
|
+
},
|
|
220
|
+
execute: (args, context) => executeInbox(args, context),
|
|
221
|
+
}),
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
const server = {
|
|
225
|
+
id: "inter-agent",
|
|
226
|
+
async server(_input) {
|
|
227
|
+
return { tool: createServerTools() };
|
|
228
|
+
},
|
|
229
|
+
};
|
|
230
|
+
export default server;
|
|
231
|
+
//# sourceMappingURL=server.js.map
|