@cotal-ai/connector-opencode 0.69.0 → 0.70.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/dist/extension.d.ts +1 -1
- package/dist/index.js +8 -2
- package/dist/launch.d.ts +14 -0
- package/dist/launch.d.ts.map +1 -0
- package/dist/launch.js +195 -0
- package/dist/opencode-line.d.ts +1 -1
- package/dist/plugin/index.js +2084 -2066
- package/dist/plugin.d.ts.map +1 -1
- package/dist/plugin2.d.ts.map +1 -1
- package/dist/serve.js +18 -8
- package/package.json +4 -4
package/dist/extension.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type Connector } from "@cotal-ai/core";
|
|
2
2
|
/**
|
|
3
3
|
* The OpenCode connector: launches a watchable `opencode` TUI bound to the agent's session, using
|
|
4
|
-
* OpenCode's client/server split (see
|
|
4
|
+
* OpenCode's client/server split (see launch.ts). The Cotal mesh bridge runs as an in-process plugin
|
|
5
5
|
* inside a headless `opencode serve`: it holds the {@link MeshAgent}, registers the cotal_* tools
|
|
6
6
|
* natively (from the shared specs, at parity with Claude Code), reports presence off the event bus,
|
|
7
7
|
* and owns ONE session it drives — injecting each incoming peer batch through the authenticated
|
package/dist/index.js
CHANGED
|
@@ -14,7 +14,10 @@ import { registry as registry3 } from "@cotal-ai/core";
|
|
|
14
14
|
import { DEFAULT_SERVER, DEV_OWNER, LAUNCH_MATERIAL_ENV, discardLaunchMaterial, assertLifecycleToken, assertValidChannel, channelInAllow, credsClaims, eventChannel, idFromCreds, isConcreteChannel, loadAgentFile, parseJoinLink, readLaunchMaterial, resolveReadAcl } from "@cotal-ai/core";
|
|
15
15
|
|
|
16
16
|
// ../connector-core/dist/agent.js
|
|
17
|
-
import { normalizeMentions, RUN_LAUNCH_DEADLINE_MS, subjectMatches, isConcreteChannel as isConcreteChannel2, assertValidChannel as assertValidChannel2, channelInAllow as channelInAllow2, resolvePeer as resolvePeerInRoster, CotalEndpoint, BASELINE_LIFECYCLE_ENDPOINT as BASELINE_LIFECYCLE_ENDPOINT2, BIND_SPLIT_REISSUES, assertLifecycleToken as assertLifecycleToken3, EpEnvelopeError, isPublishPermissionDenied, unansweredRequest, renderLifecycleBlocked, bearerCommandFailure, partsToText, routeToken } from "@cotal-ai/core";
|
|
17
|
+
import { normalizeMentions, RUN_LAUNCH_DEADLINE_MS, subjectMatches, isConcreteChannel as isConcreteChannel2, assertValidChannel as assertValidChannel2, channelInAllow as channelInAllow2, resolvePeer as resolvePeerInRoster, peerLabel as peerLabel2, CotalEndpoint, BASELINE_LIFECYCLE_ENDPOINT as BASELINE_LIFECYCLE_ENDPOINT2, BIND_SPLIT_REISSUES, assertLifecycleToken as assertLifecycleToken3, EpEnvelopeError, isPublishPermissionDenied, unansweredRequest, renderLifecycleBlocked, controlReplyFrom, bearerCommandFailure, partsToText, routeToken } from "@cotal-ai/core";
|
|
18
|
+
|
|
19
|
+
// ../connector-core/dist/framing.js
|
|
20
|
+
import { peerLabel } from "@cotal-ai/core";
|
|
18
21
|
|
|
19
22
|
// ../connector-core/dist/manager-call.js
|
|
20
23
|
import { BASELINE_LIFECYCLE_ENDPOINT, assertLifecycleToken as assertLifecycleToken2, dialerFor, invokeCommand, isPermissionDenied, issuedUserCaller, replyRefusedBeforeEffect, resolveService, standaloneConnectOpts } from "@cotal-ai/core";
|
|
@@ -14855,7 +14858,10 @@ function date4(params) {
|
|
|
14855
14858
|
config(en_default());
|
|
14856
14859
|
|
|
14857
14860
|
// ../connector-core/dist/tool-specs.js
|
|
14858
|
-
import { isConcreteChannel as isConcreteChannel3, channelInAllow as channelInAllow3, AmbiguousPeerError, assertLifecycleToken as assertLifecycleToken4, isPermissionDenied as isPermissionDenied2, renderLifecycleBlocked as renderLifecycleBlocked2, LANG_PROBLEM_DETAIL_KIND, formatAge, presenceAges } from "@cotal-ai/core";
|
|
14861
|
+
import { isConcreteChannel as isConcreteChannel3, channelInAllow as channelInAllow3, AmbiguousPeerError, peerLabel as peerLabel4, assertLifecycleToken as assertLifecycleToken4, isPermissionDenied as isPermissionDenied2, renderLifecycleBlocked as renderLifecycleBlocked2, LANG_PROBLEM_DETAIL_KIND, formatAge, presenceAges } from "@cotal-ai/core";
|
|
14862
|
+
|
|
14863
|
+
// ../connector-core/dist/orientation.js
|
|
14864
|
+
import { peerLabel as peerLabel3 } from "@cotal-ai/core";
|
|
14859
14865
|
|
|
14860
14866
|
// ../connector-core/dist/docs.js
|
|
14861
14867
|
var STOP = new Set("a an and are as at be by do for from has how in is it of on or that the this to use using was what when where which who with you your".split(" "));
|
package/dist/launch.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export declare const BIN: string;
|
|
2
|
+
/** The foreground TUI's argv, built from the server it attaches to. Its env carries the server's
|
|
3
|
+
* basic-auth credentials, so the argv never needs the secret. */
|
|
4
|
+
export type TuiArgv = (server: {
|
|
5
|
+
url: string;
|
|
6
|
+
session: string;
|
|
7
|
+
line: 1 | 2;
|
|
8
|
+
}) => [command: string, ...args: string[]];
|
|
9
|
+
/** Extra env for the server, built from inputs the caller writes into the agent's home. It runs only
|
|
10
|
+
* once no seat of this name is running, so a refused launch never rewrites files a running seat's
|
|
11
|
+
* server still reads. */
|
|
12
|
+
export type ServeEnv = (agentHome: string) => NodeJS.ProcessEnv;
|
|
13
|
+
export declare function launch(tuiArgv: TuiArgv, serveEnv?: ServeEnv): Promise<void>;
|
|
14
|
+
//# sourceMappingURL=launch.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"launch.d.ts","sourceRoot":"","sources":["../src/launch.ts"],"names":[],"mappings":"AAgDA,eAAO,MAAM,GAAG,QAAuB,CAAC;AAqExC;kEACkE;AAClE,MAAM,MAAM,OAAO,GAAG,CAAC,MAAM,EAAE;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,CAAC,GAAG,CAAC,CAAA;CAAE,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,MAAM,EAAE,CAAC,CAAC;AAEtH;;0BAE0B;AAC1B,MAAM,MAAM,QAAQ,GAAG,CAAC,SAAS,EAAE,MAAM,KAAK,MAAM,CAAC,UAAU,CAAC;AAEhE,wBAAsB,MAAM,CAAC,OAAO,EAAE,OAAO,EAAE,QAAQ,CAAC,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CA+IjF"}
|
package/dist/launch.js
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
// src/launch.ts
|
|
2
|
+
import { execFileSync, spawn } from "node:child_process";
|
|
3
|
+
import { createServer } from "node:net";
|
|
4
|
+
import { once } from "node:events";
|
|
5
|
+
import { randomBytes } from "node:crypto";
|
|
6
|
+
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
7
|
+
import { delimiter, join } from "node:path";
|
|
8
|
+
|
|
9
|
+
// src/opencode-line.ts
|
|
10
|
+
function opencodeLine(raw, bin) {
|
|
11
|
+
const m = raw.match(/(?:^|v)(\d+)\.\d+/);
|
|
12
|
+
const major = m ? Number(m[1]) : NaN;
|
|
13
|
+
if (major === 1 || major === 2) return major;
|
|
14
|
+
throw new Error(
|
|
15
|
+
`opencode connector: unsupported OpenCode version "${raw}" from ${bin}; this connector supports the 1.x (opencode-ai) and 2.x (@opencode/cli) lines`
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// src/launch.ts
|
|
20
|
+
function resolveOpencodeBin() {
|
|
21
|
+
const override = process.env.COTAL_OPENCODE_BIN?.trim() || process.env.COTAL_OPENCODE_RESOLVED_BIN?.trim();
|
|
22
|
+
if (override) return override;
|
|
23
|
+
if (process.platform !== "win32") return "opencode";
|
|
24
|
+
for (const dir of (process.env.PATH ?? "").split(delimiter)) {
|
|
25
|
+
if (!dir) continue;
|
|
26
|
+
for (const exe of [join(dir, "opencode.exe"), join(dir, "node_modules", "opencode-ai", "bin", "opencode.exe")]) {
|
|
27
|
+
if (existsSync(exe)) return exe;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return "opencode";
|
|
31
|
+
}
|
|
32
|
+
var BIN = resolveOpencodeBin();
|
|
33
|
+
var USERNAME = "opencode";
|
|
34
|
+
var SECRET = process.env.OPENCODE_SERVER_PASSWORD || randomBytes(24).toString("hex");
|
|
35
|
+
async function freePort() {
|
|
36
|
+
const srv = createServer();
|
|
37
|
+
srv.listen(0, "127.0.0.1");
|
|
38
|
+
await once(srv, "listening");
|
|
39
|
+
const port = srv.address().port;
|
|
40
|
+
await new Promise((r) => srv.close(() => r()));
|
|
41
|
+
return port;
|
|
42
|
+
}
|
|
43
|
+
async function terminate(child) {
|
|
44
|
+
if (child.exitCode !== null || child.signalCode !== null) return;
|
|
45
|
+
child.kill("SIGTERM");
|
|
46
|
+
const dead = await Promise.race([
|
|
47
|
+
once(child, "exit").then(() => true),
|
|
48
|
+
new Promise((r) => setTimeout(() => r(false), 3e3))
|
|
49
|
+
]);
|
|
50
|
+
if (!dead) {
|
|
51
|
+
child.kill("SIGKILL");
|
|
52
|
+
await once(child, "exit");
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
function processCommand(pid) {
|
|
56
|
+
try {
|
|
57
|
+
if (process.platform === "win32") {
|
|
58
|
+
return execFileSync("powershell.exe", [
|
|
59
|
+
"-NoProfile",
|
|
60
|
+
"-NonInteractive",
|
|
61
|
+
"-Command",
|
|
62
|
+
`(Get-CimInstance Win32_Process -Filter 'ProcessId = ${pid}').CommandLine`
|
|
63
|
+
], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
64
|
+
}
|
|
65
|
+
return execFileSync("ps", ["-p", String(pid), "-o", "command="], {
|
|
66
|
+
encoding: "utf8",
|
|
67
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
68
|
+
}).trim();
|
|
69
|
+
} catch {
|
|
70
|
+
return void 0;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
function isLiveOpencodeServe(pid) {
|
|
74
|
+
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
75
|
+
try {
|
|
76
|
+
process.kill(pid, 0);
|
|
77
|
+
} catch {
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
const cmd = processCommand(pid);
|
|
81
|
+
if (!cmd) return true;
|
|
82
|
+
return /\bserve\b/.test(cmd) && (/(?:^|[\\/\s])opencode(?:\.exe)?(?:\s|$)/i.test(cmd) || /--hostname\s+127\.0\.0\.1\b/.test(cmd) && /--port\s+\d+\b/.test(cmd));
|
|
83
|
+
}
|
|
84
|
+
async function launch(tuiArgv, serveEnv) {
|
|
85
|
+
const port = process.env.COTAL_OPENCODE_PORT?.trim() || String(await freePort());
|
|
86
|
+
const url = `http://127.0.0.1:${port}`;
|
|
87
|
+
const name = process.env.COTAL_NAME?.trim() || "agent";
|
|
88
|
+
const dataRoot = process.env.COTAL_OPENCODE_HOME?.trim();
|
|
89
|
+
if (!dataRoot) throw new Error("COTAL_OPENCODE_HOME is not set \u2014 the connector must pin the agent's data root");
|
|
90
|
+
const agentHome = join(dataRoot, ".cotal", "opencode", name);
|
|
91
|
+
const dbPath = join(agentHome, "opencode.db");
|
|
92
|
+
const pidFile = join(agentHome, "serve.pid");
|
|
93
|
+
let recorded;
|
|
94
|
+
try {
|
|
95
|
+
recorded = readFileSync(pidFile, "utf8");
|
|
96
|
+
} catch (e) {
|
|
97
|
+
if (e.code !== "ENOENT") throw e;
|
|
98
|
+
}
|
|
99
|
+
if (recorded !== void 0) {
|
|
100
|
+
const pid = Number(recorded);
|
|
101
|
+
if (isLiveOpencodeServe(pid))
|
|
102
|
+
throw new Error(`agent "${name}" is already running (opencode serve pid ${pid}) \u2014 kill it first`);
|
|
103
|
+
rmSync(pidFile, { force: true });
|
|
104
|
+
}
|
|
105
|
+
const versionRaw = execFileSync(BIN, ["--version"], { encoding: "utf8" }).trim();
|
|
106
|
+
const line = opencodeLine(versionRaw, BIN);
|
|
107
|
+
mkdirSync(agentHome, { recursive: true });
|
|
108
|
+
const serve = spawn(BIN, ["serve", "--hostname", "127.0.0.1", "--port", port], {
|
|
109
|
+
env: {
|
|
110
|
+
...process.env,
|
|
111
|
+
...serveEnv?.(agentHome),
|
|
112
|
+
COTAL_OPENCODE_SERVER_URL: url,
|
|
113
|
+
COTAL_OPENCODE_LINE: String(line),
|
|
114
|
+
OPENCODE_SERVER_USERNAME: USERNAME,
|
|
115
|
+
OPENCODE_SERVER_PASSWORD: SECRET,
|
|
116
|
+
OPENCODE_DB: dbPath
|
|
117
|
+
},
|
|
118
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
119
|
+
});
|
|
120
|
+
writeFileSync(pidFile, String(serve.pid));
|
|
121
|
+
serve.on("exit", () => rmSync(pidFile, { force: true }));
|
|
122
|
+
let sessionId;
|
|
123
|
+
let attached = false;
|
|
124
|
+
let onSession;
|
|
125
|
+
const scan = (d) => {
|
|
126
|
+
if (!attached) process.stderr.write(d);
|
|
127
|
+
if (!sessionId) {
|
|
128
|
+
const m = d.toString().match(/\[cotal-session\] (\S+)/);
|
|
129
|
+
if (m) {
|
|
130
|
+
sessionId = m[1];
|
|
131
|
+
onSession?.(sessionId);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
serve.stdout?.on("data", scan);
|
|
136
|
+
serve.stderr?.on("data", scan);
|
|
137
|
+
serve.on("exit", (code, signal) => {
|
|
138
|
+
if (!attached) process.exit(code ?? (signal ? 1 : 0));
|
|
139
|
+
});
|
|
140
|
+
const auth = `Basic ${Buffer.from(`${USERNAME}:${SECRET}`).toString("base64")}`;
|
|
141
|
+
const pokePath = line === 2 ? "/api/config" : "/session";
|
|
142
|
+
void (async () => {
|
|
143
|
+
for (let i = 0; i < 300 && !sessionId; i++) {
|
|
144
|
+
try {
|
|
145
|
+
await fetch(`${url}${pokePath}`, { headers: { authorization: auth }, signal: AbortSignal.timeout(1500) });
|
|
146
|
+
} catch {
|
|
147
|
+
}
|
|
148
|
+
await new Promise((r) => setTimeout(r, 200));
|
|
149
|
+
}
|
|
150
|
+
})();
|
|
151
|
+
const id = await new Promise((resolve) => {
|
|
152
|
+
if (sessionId) return resolve(sessionId);
|
|
153
|
+
onSession = resolve;
|
|
154
|
+
setTimeout(() => resolve(sessionId), 6e4);
|
|
155
|
+
});
|
|
156
|
+
if (!id) {
|
|
157
|
+
process.stderr.write(
|
|
158
|
+
`[cotal-connector] serve: agent session never came up (~60s) \u2014 aborting. Check the boot log above for plugin/mesh errors (OPENCODE_DB=${dbPath})
|
|
159
|
+
`
|
|
160
|
+
);
|
|
161
|
+
await terminate(serve);
|
|
162
|
+
process.exit(1);
|
|
163
|
+
}
|
|
164
|
+
if (process.env.COTAL_SERVE_HEADLESS?.trim() === "1") {
|
|
165
|
+
process.stdout.write(`[cotal-serve] ${JSON.stringify({ port: Number(port), session: id })}
|
|
166
|
+
`);
|
|
167
|
+
for (const sig of ["SIGINT", "SIGTERM"])
|
|
168
|
+
process.on(sig, () => void terminate(serve).then(() => process.exit(0)));
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
const tuiEnv = {
|
|
172
|
+
...process.env,
|
|
173
|
+
OPENCODE_SERVER_USERNAME: USERNAME,
|
|
174
|
+
OPENCODE_SERVER_PASSWORD: SECRET,
|
|
175
|
+
OPENCODE_DB: dbPath
|
|
176
|
+
};
|
|
177
|
+
delete tuiEnv.OPENCODE_CONFIG_CONTENT;
|
|
178
|
+
for (const k of Object.keys(tuiEnv)) if (k.startsWith("COTAL_")) delete tuiEnv[k];
|
|
179
|
+
attached = true;
|
|
180
|
+
const [command, ...args] = tuiArgv({ url, session: id, line });
|
|
181
|
+
const tui = spawn(command, args, { env: tuiEnv, stdio: "inherit" });
|
|
182
|
+
for (const sig of ["SIGINT", "SIGTERM"])
|
|
183
|
+
process.on(sig, () => {
|
|
184
|
+
tui.kill(sig);
|
|
185
|
+
serve.kill(sig);
|
|
186
|
+
});
|
|
187
|
+
serve.on("exit", () => void terminate(tui));
|
|
188
|
+
tui.on("exit", (code, signal) => {
|
|
189
|
+
void terminate(serve).then(() => process.exit(code ?? (signal ? 1 : 0)));
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
export {
|
|
193
|
+
BIN,
|
|
194
|
+
launch
|
|
195
|
+
};
|
package/dist/opencode-line.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** Parses `opencode --version` output into the major protocol line: 1 (opencode-ai) or 2
|
|
2
|
-
* (@opencode/cli). Pure so both
|
|
2
|
+
* (@opencode/cli). Pure so both launch.ts (the launcher) and extension.ts (the model catalog) can
|
|
3
3
|
* detect the line from the same rule with no fallback for anything else. */
|
|
4
4
|
export declare function opencodeLine(raw: string, bin: string): 1 | 2;
|
|
5
5
|
//# sourceMappingURL=opencode-line.d.ts.map
|