cotal-ai 0.97.1 → 0.99.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/daemon/client.js +22 -0
- package/dist/daemon/command.js +60 -20
- package/dist/daemon/endpoint.js +12 -0
- package/dist/daemon/server.js +30 -0
- package/package.json +18 -18
- package/seeded-connectors/claude/dist/mcp.cjs +6 -6
- package/seeded-connectors/claude/package.json +1 -1
- package/seeded-connectors/codex/dist/host.js +6 -6
- package/seeded-connectors/codex/package.json +1 -1
- package/seeded-connectors/hermes/dist/launch.js +6 -6
- package/seeded-connectors/hermes/package.json +1 -1
- package/seeded-connectors/hermes/plugin/cotal/_sidecar/standalone.cjs +6 -6
- package/seeded-connectors/jcode/dist/host.js +6 -6
- package/seeded-connectors/jcode/dist/mcp.js +6 -6
- package/seeded-connectors/jcode/package.json +1 -1
- package/seeded-connectors/opencode/dist/plugin/index.js +6 -6
- package/seeded-connectors/opencode/package.json +1 -1
- package/seeded-connectors/pi/dist/index.js +6 -6
- package/seeded-connectors/pi/dist/standalone.js +6 -6
- package/seeded-connectors/pi/package.json +1 -1
- package/seeded-connectors/web/package.json +1 -1
package/dist/daemon/client.js
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import { connect as netConnect } from "node:net";
|
|
10
10
|
import { randomUUID } from "node:crypto";
|
|
11
|
+
import { inspectLock, processStartToken } from "@cotal-ai/core";
|
|
11
12
|
import { DAEMON_PROTOCOL_VERSION, MAX_LINE_BYTES, EndpointRefusal, assertLinux, checkDir, checkSocket } from "./endpoint.js";
|
|
12
13
|
import { encode } from "./protocol.js";
|
|
13
14
|
/** Ask the daemon whether it could register with `space`. Nothing is registered either way. */
|
|
@@ -129,7 +130,11 @@ async function openVerified(opts) {
|
|
|
129
130
|
assertLinux();
|
|
130
131
|
const uid = opts.uid ?? process.geteuid();
|
|
131
132
|
const { paths } = opts;
|
|
133
|
+
if (paths.lock !== undefined && opts.peerPid === undefined)
|
|
134
|
+
throw new Error("an explicit daemon home needs the peer pid check");
|
|
132
135
|
try {
|
|
136
|
+
if (paths.parent !== undefined)
|
|
137
|
+
checkDir(paths.parent, uid);
|
|
133
138
|
checkDir(paths.dir, uid);
|
|
134
139
|
checkSocket(paths.socket, uid);
|
|
135
140
|
}
|
|
@@ -151,6 +156,23 @@ async function openVerified(opts) {
|
|
|
151
156
|
sock.destroy();
|
|
152
157
|
throw new EndpointRefusal("PEER_UID_MISMATCH", `the process answering at ${paths.socket} runs as uid ${peer}, not ${uid}; nothing was sent`);
|
|
153
158
|
}
|
|
159
|
+
if (paths.lock !== undefined) {
|
|
160
|
+
// Same uid is not enough for an explicit daemon home: the answering process must be the one
|
|
161
|
+
// holding that home's lifetime lock, the same process (pid and start token) the lock recorded.
|
|
162
|
+
let pid;
|
|
163
|
+
try {
|
|
164
|
+
pid = opts.peerPid(sock);
|
|
165
|
+
}
|
|
166
|
+
catch {
|
|
167
|
+
pid = undefined;
|
|
168
|
+
}
|
|
169
|
+
const held = inspectLock(paths.lock);
|
|
170
|
+
const owner = held.state === "active" ? held.owner : undefined;
|
|
171
|
+
if (pid === undefined || owner === undefined || owner.pid !== pid || owner.start === undefined || processStartToken(pid) !== owner.start) {
|
|
172
|
+
sock.destroy();
|
|
173
|
+
throw new EndpointRefusal("PEER_NOT_LOCK_OWNER", `the process answering at ${paths.socket} is not the daemon holding ${paths.lock}; nothing was sent`);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
154
176
|
return sock;
|
|
155
177
|
}
|
|
156
178
|
/** Ask the daemon to sign in, show its prompt, and return its single result. */
|
package/dist/daemon/command.js
CHANGED
|
@@ -19,14 +19,14 @@
|
|
|
19
19
|
* ALREADY_RUNNING remedy). `state: "ready"` means the local control listener is bound and
|
|
20
20
|
* verified; it says nothing about any mesh or Cloud connection.
|
|
21
21
|
*/
|
|
22
|
-
import { readFileSync } from "node:fs";
|
|
23
|
-
import { dirname, join } from "node:path";
|
|
22
|
+
import { readFileSync, realpathSync } from "node:fs";
|
|
23
|
+
import { dirname, isAbsolute, join } from "node:path";
|
|
24
24
|
import { fileURLToPath } from "node:url";
|
|
25
25
|
import { registry } from "@cotal-ai/core";
|
|
26
|
-
import { CommandFailure, jsonFlag, plainFlag, presentationOf, renderReport, spaceFlag, terminalWidth, writeResult } from "@cotal-ai/workspace";
|
|
26
|
+
import { CommandFailure, homeCotalDir, jsonFlag, plainFlag, presentationOf, renderReport, spaceFlag, terminalWidth, writeResult } from "@cotal-ai/workspace";
|
|
27
27
|
import { peerCredentials } from "@cotal-ai/seat";
|
|
28
28
|
import { normalizeIdpUrl } from "@cotal-ai/auth";
|
|
29
|
-
import { EndpointRefusal, daemonPaths } from "./endpoint.js";
|
|
29
|
+
import { EndpointRefusal, checkDir, daemonPaths, daemonPathsFor } from "./endpoint.js";
|
|
30
30
|
import { serveDaemon } from "./server.js";
|
|
31
31
|
import { connectCheckViaDaemon, loginViaDaemon, queryDaemon, registerViaDaemon, serviceViaDaemon } from "./client.js";
|
|
32
32
|
export const daemonFlags = [
|
|
@@ -34,6 +34,7 @@ export const daemonFlags = [
|
|
|
34
34
|
plainFlag,
|
|
35
35
|
{ name: "idp", type: "string", value: "<auth base URL>", description: "login: the provider's auth base URL, e.g. <origin>/api/auth" },
|
|
36
36
|
{ name: "client-id", type: "string", value: "<id>", description: "login: the OAuth device-flow client id" },
|
|
37
|
+
{ name: "daemon-home", type: "string", value: "<abs dir>", description: "run or reach the daemon whose state lives in this directory (absolute, yours, mode 0700, not the Cotal home): its own sign-in, identity and registration, its own socket and lifetime lock. Without it, the default daemon" },
|
|
37
38
|
spaceFlag,
|
|
38
39
|
];
|
|
39
40
|
/** This binary's own version: the nearest package.json above this module (bin/ in a source run). */
|
|
@@ -54,6 +55,37 @@ export function binVersion() {
|
|
|
54
55
|
throw new Error("cannot determine the cotal-ai version: no cotal-ai package.json above the daemon module");
|
|
55
56
|
}
|
|
56
57
|
const peerUid = (sock) => peerCredentials(sock).uid;
|
|
58
|
+
const peerPid = (sock) => peerCredentials(sock).pid;
|
|
59
|
+
function target(args) {
|
|
60
|
+
const raw = args.values["daemon-home"];
|
|
61
|
+
if (raw === undefined)
|
|
62
|
+
return { paths: daemonPaths() };
|
|
63
|
+
if (!raw || !isAbsolute(raw))
|
|
64
|
+
throw new CommandFailure("refused", "USAGE_INVALID", "--daemon-home needs an absolute directory path");
|
|
65
|
+
checkDir(raw);
|
|
66
|
+
const home = realpathSync(raw);
|
|
67
|
+
let cotalHome;
|
|
68
|
+
try {
|
|
69
|
+
cotalHome = realpathSync(homeCotalDir());
|
|
70
|
+
}
|
|
71
|
+
catch (e) {
|
|
72
|
+
if (e.code !== "ENOENT")
|
|
73
|
+
throw e;
|
|
74
|
+
}
|
|
75
|
+
if (home === cotalHome)
|
|
76
|
+
throw new CommandFailure("refused", "USAGE_INVALID", "--daemon-home cannot be the Cotal home; name a separate directory for this daemon's state");
|
|
77
|
+
return { paths: daemonPathsFor(home), home };
|
|
78
|
+
}
|
|
79
|
+
function resolveTarget(args) {
|
|
80
|
+
try {
|
|
81
|
+
return target(args);
|
|
82
|
+
}
|
|
83
|
+
catch (e) {
|
|
84
|
+
if (e.code === "ENOENT")
|
|
85
|
+
throw new CommandFailure("refused", "TARGET_NOT_FOUND", "the --daemon-home directory does not exist; create it with mode 0700 first");
|
|
86
|
+
throw asFailure(e);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
57
89
|
const REFUSAL = {
|
|
58
90
|
UNSUPPORTED_PLATFORM: { code: "UNSUPPORTED" },
|
|
59
91
|
NO_RUNTIME_DIR: { code: "TARGET_UNAVAILABLE", remedy: "run from a logged-in session with a per-user runtime directory (systemd-logind sets XDG_RUNTIME_DIR)" },
|
|
@@ -67,6 +99,8 @@ const REFUSAL = {
|
|
|
67
99
|
SOCKET_WRONG_OWNER: { code: "PERMISSION_DENIED", remedy: "inspect the path; nothing was removed" },
|
|
68
100
|
SOCKET_WRONG_MODE: { code: "PERMISSION_DENIED", remedy: "inspect the socket; nothing was sent" },
|
|
69
101
|
PEER_UID_MISMATCH: { code: "PERMISSION_DENIED", remedy: "another user's process holds the daemon endpoint; nothing was sent" },
|
|
102
|
+
PEER_NOT_LOCK_OWNER: { code: "PERMISSION_DENIED", remedy: "the process at this daemon home's socket is not the daemon holding its lock; nothing was sent" },
|
|
103
|
+
LOCK_HELD: { code: "CONFLICT", remedy: "a daemon already serves this daemon home; run cotal daemon status with the same --daemon-home" },
|
|
70
104
|
};
|
|
71
105
|
function asFailure(e) {
|
|
72
106
|
if (!(e instanceof EndpointRefusal))
|
|
@@ -79,9 +113,10 @@ function statusView(s) {
|
|
|
79
113
|
}
|
|
80
114
|
async function status(args) {
|
|
81
115
|
const presentation = presentationOf(args.values);
|
|
116
|
+
const { paths } = resolveTarget(args);
|
|
82
117
|
let reply;
|
|
83
118
|
try {
|
|
84
|
-
reply = await queryDaemon({ paths
|
|
119
|
+
reply = await queryDaemon({ paths, peerUid, peerPid });
|
|
85
120
|
}
|
|
86
121
|
catch (e) {
|
|
87
122
|
throw asFailure(e);
|
|
@@ -107,13 +142,14 @@ async function login(args) {
|
|
|
107
142
|
}
|
|
108
143
|
if (values["client-id"] !== undefined && !values["client-id"].trim())
|
|
109
144
|
throw new CommandFailure("refused", "USAGE_INVALID", "--client-id cannot be empty");
|
|
145
|
+
const { paths } = resolveTarget(args);
|
|
110
146
|
const abort = new AbortController();
|
|
111
147
|
const onSignal = () => abort.abort();
|
|
112
148
|
process.once("SIGINT", onSignal);
|
|
113
149
|
let reply;
|
|
114
150
|
try {
|
|
115
151
|
reply = await loginViaDaemon({
|
|
116
|
-
paths
|
|
152
|
+
paths, peerUid, peerPid, idp, signal: abort.signal,
|
|
117
153
|
...(values["client-id"] !== undefined ? { clientId: values["client-id"] } : {}),
|
|
118
154
|
onPrompt: (p) => {
|
|
119
155
|
process.stderr.write(`${renderReport({ sections: [{ title: "Approve sign-in", blocks: [
|
|
@@ -161,9 +197,10 @@ async function connectCheckCommand(args) {
|
|
|
161
197
|
const space = args.values.space;
|
|
162
198
|
if (typeof space !== "string" || !space)
|
|
163
199
|
throw new CommandFailure("refused", "USAGE_INVALID", "cotal daemon connect-check needs --space <s>");
|
|
200
|
+
const { paths } = resolveTarget(args);
|
|
164
201
|
let reply;
|
|
165
202
|
try {
|
|
166
|
-
reply = await connectCheckViaDaemon({ paths
|
|
203
|
+
reply = await connectCheckViaDaemon({ paths, peerUid, peerPid, space });
|
|
167
204
|
}
|
|
168
205
|
catch (e) {
|
|
169
206
|
throw asFailure(e);
|
|
@@ -198,9 +235,10 @@ async function registerCommand(args) {
|
|
|
198
235
|
const space = args.values.space;
|
|
199
236
|
if (typeof space !== "string" || !space)
|
|
200
237
|
throw new CommandFailure("refused", "USAGE_INVALID", "cotal daemon register needs --space <s>");
|
|
238
|
+
const { paths } = resolveTarget(args);
|
|
201
239
|
let reply;
|
|
202
240
|
try {
|
|
203
|
-
reply = await registerViaDaemon({ paths
|
|
241
|
+
reply = await registerViaDaemon({ paths, peerUid, peerPid, space });
|
|
204
242
|
}
|
|
205
243
|
catch (e) {
|
|
206
244
|
throw asFailure(e);
|
|
@@ -239,9 +277,10 @@ async function serviceCommand(args, op) {
|
|
|
239
277
|
const space = args.values.space;
|
|
240
278
|
if (typeof space !== "string" || !space)
|
|
241
279
|
throw new CommandFailure("refused", "USAGE_INVALID", `cotal daemon ${op} needs --space <s>`);
|
|
280
|
+
const { paths } = resolveTarget(args);
|
|
242
281
|
let reply;
|
|
243
282
|
try {
|
|
244
|
-
reply = await serviceViaDaemon({ paths
|
|
283
|
+
reply = await serviceViaDaemon({ paths, peerUid, peerPid, space, op });
|
|
245
284
|
}
|
|
246
285
|
catch (e) {
|
|
247
286
|
throw asFailure(e);
|
|
@@ -269,10 +308,11 @@ async function serviceCommand(args, op) {
|
|
|
269
308
|
] },
|
|
270
309
|
] }));
|
|
271
310
|
}
|
|
272
|
-
async function serve() {
|
|
311
|
+
async function serve(args) {
|
|
312
|
+
const { paths, home } = resolveTarget(args);
|
|
273
313
|
let running;
|
|
274
314
|
try {
|
|
275
|
-
running = await serveDaemon({ paths
|
|
315
|
+
running = await serveDaemon({ paths, version: binVersion(), peerUid, log: (l) => process.stderr.write(`${l}\n`), ...(home !== undefined ? { sessionDir: home } : {}) });
|
|
276
316
|
}
|
|
277
317
|
catch (e) {
|
|
278
318
|
throw asFailure(e);
|
|
@@ -291,7 +331,7 @@ async function run(args) {
|
|
|
291
331
|
if (mode === "status")
|
|
292
332
|
return status(args);
|
|
293
333
|
if (mode === "serve")
|
|
294
|
-
return serve();
|
|
334
|
+
return serve(args);
|
|
295
335
|
if (mode === "login")
|
|
296
336
|
return login(args);
|
|
297
337
|
if (mode === "connect-check")
|
|
@@ -310,14 +350,14 @@ const daemonCommand = {
|
|
|
310
350
|
positionals: "<status|serve|login|connect-check|register|service-connect|service-disconnect|service-status>",
|
|
311
351
|
flags: daemonFlags,
|
|
312
352
|
subcommands: [
|
|
313
|
-
{ name: "status", summary: "ask the running daemon for its status over the local endpoint", flagNames: ["json", "plain"], outputs: ["json", "plain"], reads: true, prepareMeshTarget: false },
|
|
314
|
-
{ name: "serve", summary: "run the daemon in the foreground and serve its local endpoint (refusals honor --json)", flagNames: ["json", "plain"], outputs: ["json", "plain"], prepareMeshTarget: false },
|
|
315
|
-
{ name: "login", summary: "sign the running daemon in to a provider with device approval; the daemon keeps the session", flagNames: ["idp", "client-id", "json", "plain"], outputs: ["json", "plain"], prepareMeshTarget: false },
|
|
316
|
-
{ name: "connect-check", summary: "ask the running daemon whether it could register with a space as its own service; registers nothing", flagNames: ["space", "json", "plain"], outputs: ["json", "plain"], reads: true, prepareMeshTarget: false },
|
|
317
|
-
{ name: "register", summary: "ask the running daemon to request its own service registration with a space; the space owner must approve it, and it does not connect", flagNames: ["space", "json", "plain"], outputs: ["json", "plain"], prepareMeshTarget: false },
|
|
318
|
-
{ name: "service-connect", summary: "connect the running daemon to a space as its own approved service; succeeds only when the broker accepts the connection and the space accepts it as ready", flagNames: ["space", "json", "plain"], outputs: ["json", "plain"], prepareMeshTarget: false },
|
|
319
|
-
{ name: "service-disconnect", summary: "close the running daemon's service connection to a space", flagNames: ["space", "json", "plain"], outputs: ["json", "plain"], prepareMeshTarget: false },
|
|
320
|
-
{ name: "service-status", summary: "read the running daemon's service connection to a space, without contacting it", flagNames: ["space", "json", "plain"], outputs: ["json", "plain"], reads: true, prepareMeshTarget: false },
|
|
353
|
+
{ name: "status", summary: "ask the running daemon for its status over the local endpoint", flagNames: ["daemon-home", "json", "plain"], outputs: ["json", "plain"], reads: true, prepareMeshTarget: false },
|
|
354
|
+
{ name: "serve", summary: "run the daemon in the foreground and serve its local endpoint (refusals honor --json)", flagNames: ["daemon-home", "json", "plain"], outputs: ["json", "plain"], prepareMeshTarget: false },
|
|
355
|
+
{ name: "login", summary: "sign the running daemon in to a provider with device approval; the daemon keeps the session", flagNames: ["idp", "client-id", "daemon-home", "json", "plain"], outputs: ["json", "plain"], prepareMeshTarget: false },
|
|
356
|
+
{ name: "connect-check", summary: "ask the running daemon whether it could register with a space as its own service; registers nothing", flagNames: ["space", "daemon-home", "json", "plain"], outputs: ["json", "plain"], reads: true, prepareMeshTarget: false },
|
|
357
|
+
{ name: "register", summary: "ask the running daemon to request its own service registration with a space; the space owner must approve it, and it does not connect", flagNames: ["space", "daemon-home", "json", "plain"], outputs: ["json", "plain"], prepareMeshTarget: false },
|
|
358
|
+
{ name: "service-connect", summary: "connect the running daemon to a space as its own approved service; succeeds only when the broker accepts the connection and the space accepts it as ready", flagNames: ["space", "daemon-home", "json", "plain"], outputs: ["json", "plain"], prepareMeshTarget: false },
|
|
359
|
+
{ name: "service-disconnect", summary: "close the running daemon's service connection to a space", flagNames: ["space", "daemon-home", "json", "plain"], outputs: ["json", "plain"], prepareMeshTarget: false },
|
|
360
|
+
{ name: "service-status", summary: "read the running daemon's service connection to a space, without contacting it", flagNames: ["space", "daemon-home", "json", "plain"], outputs: ["json", "plain"], reads: true, prepareMeshTarget: false },
|
|
321
361
|
],
|
|
322
362
|
prepareMeshTarget: false,
|
|
323
363
|
examples: ["cotal daemon serve", "cotal daemon status", "cotal daemon status --json", "cotal daemon login --idp https://example.org/api/auth", "cotal daemon connect-check --space acme", "cotal daemon register --space acme", "cotal daemon service-connect --space acme", "cotal daemon service-status --space acme"],
|
package/dist/daemon/endpoint.js
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
* this uid, or not exactly 0700 is refused, never chmod-ed. Any pre-existing entry at the socket
|
|
12
12
|
* path is refused, never removed, so this code never unlinks a socket it did not create.
|
|
13
13
|
*/
|
|
14
|
+
import { createHash } from "node:crypto";
|
|
14
15
|
import { lstatSync, mkdirSync } from "node:fs";
|
|
15
16
|
import { join } from "node:path";
|
|
16
17
|
/** Wire protocol version. A request naming any other version is refused before any effect. */
|
|
@@ -35,6 +36,17 @@ export function daemonPaths(env = process.env) {
|
|
|
35
36
|
const dir = join(runtime, "cotal");
|
|
36
37
|
return { dir, socket: join(dir, "daemon.sock") };
|
|
37
38
|
}
|
|
39
|
+
/**
|
|
40
|
+
* The endpoint of the daemon whose home is `daemonHome` (an absolute, canonical path):
|
|
41
|
+
* `$XDG_RUNTIME_DIR/cotal/home-<16 hex of sha256(daemonHome)>/daemon.sock`, with its lifetime lock
|
|
42
|
+
* at `<daemonHome>/daemon.lock`. Derived, so client and server agree with no extra record, and
|
|
43
|
+
* disjoint from the legacy socket.
|
|
44
|
+
*/
|
|
45
|
+
export function daemonPathsFor(daemonHome, env = process.env) {
|
|
46
|
+
const base = daemonPaths(env);
|
|
47
|
+
const dir = join(base.dir, `home-${createHash("sha256").update(daemonHome).digest("hex").slice(0, 16)}`);
|
|
48
|
+
return { parent: base.dir, dir, socket: join(dir, "daemon.sock"), lock: join(daemonHome, "daemon.lock") };
|
|
49
|
+
}
|
|
38
50
|
export function assertLinux(platform = process.platform) {
|
|
39
51
|
if (platform !== "linux") {
|
|
40
52
|
throw new EndpointRefusal("UNSUPPORTED_PLATFORM", `the local daemon endpoint is Linux only (this is ${platform})`);
|
package/dist/daemon/server.js
CHANGED
|
@@ -19,6 +19,7 @@ import { connectCheck } from "./connect.js";
|
|
|
19
19
|
import { registerDaemon } from "./register.js";
|
|
20
20
|
import { createServiceConnections } from "./service.js";
|
|
21
21
|
import { homeCotalDir } from "@cotal-ai/workspace";
|
|
22
|
+
import { acquireLock } from "@cotal-ai/core";
|
|
22
23
|
/** Requests one connection may send. A sign-in needs two (start, cancel); status polling a few. */
|
|
23
24
|
export const MAX_REQUESTS_PER_CONNECTION = 64;
|
|
24
25
|
function listen(server, path) {
|
|
@@ -39,8 +40,33 @@ function listen(server, path) {
|
|
|
39
40
|
export async function serveDaemon(opts) {
|
|
40
41
|
assertLinux();
|
|
41
42
|
const uid = opts.uid ?? process.geteuid();
|
|
43
|
+
const { paths } = opts;
|
|
44
|
+
// An explicit daemon home holds its lifetime lock before touching any endpoint path, so a second
|
|
45
|
+
// serve for the same home refuses here and never reaches the socket. The stock lock reclaims a dead
|
|
46
|
+
// owner's record; a stale socket is still refused below and never removed.
|
|
47
|
+
let lock;
|
|
48
|
+
if (paths.lock !== undefined) {
|
|
49
|
+
const path = paths.lock;
|
|
50
|
+
lock = acquireLock(path, {
|
|
51
|
+
waitMs: 0,
|
|
52
|
+
label: "cotal daemon",
|
|
53
|
+
onTimeout: (owner) => new EndpointRefusal("LOCK_HELD", `${path} is held by a live process (pid ${owner.pid}): a daemon already serves this daemon home. Nothing was changed`),
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
try {
|
|
57
|
+
return await serveLocked(opts, uid, lock);
|
|
58
|
+
}
|
|
59
|
+
catch (e) {
|
|
60
|
+
// Every failed start releases the lock it took. A started daemon releases it only in close().
|
|
61
|
+
lock?.release();
|
|
62
|
+
throw e;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
async function serveLocked(opts, uid, lock) {
|
|
42
66
|
const log = opts.log ?? (() => { });
|
|
43
67
|
const { paths } = opts;
|
|
68
|
+
if (paths.parent !== undefined)
|
|
69
|
+
ensureDir(paths.parent, uid);
|
|
44
70
|
const dirOutcome = ensureDir(paths.dir, uid);
|
|
45
71
|
refuseExisting(paths.socket);
|
|
46
72
|
const counters = { accepted: 0, peerRejected: 0, framesDecoded: 0, requestsRefused: 0, statusAnswered: 0, loginsStarted: 0, loginsRefused: 0, cancelsAnswered: 0, connectChecksAnswered: 0, registersAnswered: 0, serviceAnswered: 0 };
|
|
@@ -259,6 +285,10 @@ export async function serveDaemon(opts) {
|
|
|
259
285
|
if (e.code !== "ENOENT")
|
|
260
286
|
throw e;
|
|
261
287
|
}
|
|
288
|
+
// The lock is released only once this daemon's own endpoint is gone. If its socket is still
|
|
289
|
+
// published (the removal above threw), the lock stays held until this process exits, so no
|
|
290
|
+
// other serve can start beside a live endpoint.
|
|
291
|
+
lock?.release();
|
|
262
292
|
})();
|
|
263
293
|
return closed;
|
|
264
294
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cotal-ai",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.99.0",
|
|
4
4
|
"description": "Cotal — lateral agent coordination over NATS. Run `npx cotal-ai` for the CLI.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"keywords": [
|
|
@@ -33,26 +33,26 @@
|
|
|
33
33
|
"node": ">=22"
|
|
34
34
|
},
|
|
35
35
|
"dependencies": {
|
|
36
|
-
"@cotal-ai/auth": "0.
|
|
37
|
-
"@cotal-ai/cli": "0.
|
|
38
|
-
"@cotal-ai/connector-core": "0.
|
|
39
|
-
"@cotal-ai/core": "0.
|
|
40
|
-
"@cotal-ai/delivery": "0.
|
|
41
|
-
"@cotal-ai/manager": "0.
|
|
42
|
-
"@cotal-ai/runtime": "0.
|
|
43
|
-
"@cotal-ai/seat": "0.
|
|
44
|
-
"@cotal-ai/workspace": "0.
|
|
36
|
+
"@cotal-ai/auth": "0.99.0",
|
|
37
|
+
"@cotal-ai/cli": "0.99.0",
|
|
38
|
+
"@cotal-ai/connector-core": "0.99.0",
|
|
39
|
+
"@cotal-ai/core": "0.99.0",
|
|
40
|
+
"@cotal-ai/delivery": "0.99.0",
|
|
41
|
+
"@cotal-ai/manager": "0.99.0",
|
|
42
|
+
"@cotal-ai/runtime": "0.99.0",
|
|
43
|
+
"@cotal-ai/seat": "0.99.0",
|
|
44
|
+
"@cotal-ai/workspace": "0.99.0"
|
|
45
45
|
},
|
|
46
46
|
"devDependencies": {
|
|
47
|
-
"@cotal-ai/connector-claude-code": "0.
|
|
48
|
-
"@cotal-ai/connector-codex": "0.
|
|
49
|
-
"@cotal-ai/connector-hermes": "0.
|
|
50
|
-
"@cotal-ai/connector-jcode": "0.
|
|
51
|
-
"@cotal-ai/connector-opencode": "0.
|
|
52
|
-
"@cotal-ai/linear": "0.
|
|
53
|
-
"@cotal-ai/pi": "0.
|
|
47
|
+
"@cotal-ai/connector-claude-code": "0.99.0",
|
|
48
|
+
"@cotal-ai/connector-codex": "0.99.0",
|
|
49
|
+
"@cotal-ai/connector-hermes": "0.99.0",
|
|
50
|
+
"@cotal-ai/connector-jcode": "0.99.0",
|
|
51
|
+
"@cotal-ai/connector-opencode": "0.99.0",
|
|
52
|
+
"@cotal-ai/linear": "0.99.0",
|
|
53
|
+
"@cotal-ai/pi": "0.99.0",
|
|
54
54
|
"@cotal-ai/smoke-kit": "0.0.0",
|
|
55
|
-
"@cotal-ai/web": "0.
|
|
55
|
+
"@cotal-ai/web": "0.99.0",
|
|
56
56
|
"@nats-io/jetstream": "^3.4.0",
|
|
57
57
|
"@nats-io/transport-node": "^3.4.0",
|
|
58
58
|
"typescript": "^5.9.3"
|