cotal-ai 0.95.0 → 0.95.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.
@@ -27,6 +27,23 @@ export async function connectCheckViaDaemon(opts) {
27
27
  sock.destroy();
28
28
  }
29
29
  }
30
+ /** Ask the daemon to submit its own service registration for `space` and report the space's answer. */
31
+ export async function registerViaDaemon(opts) {
32
+ const sock = await openVerified({ ...opts, timeoutMs: 5000 });
33
+ try {
34
+ sock.write(encode({ v: DAEMON_PROTOCOL_VERSION, id: randomUUID().replaceAll("-", ""), op: "register", space: opts.space }));
35
+ // Readiness read plus the stock register call, each bounded by the stock client (30 s).
36
+ const reply = JSON.parse(await readLine(sock, 65_000));
37
+ if (!reply.ok)
38
+ return { ok: false, code: reply.error.code, message: reply.error.message };
39
+ if ("event" in reply || !("outcome" in reply.result) || !("space" in reply.result))
40
+ return { ok: false, code: "BAD_REPLY", message: "the daemon answered the registration with something else" };
41
+ return { ok: true, result: reply.result };
42
+ }
43
+ finally {
44
+ sock.destroy();
45
+ }
46
+ }
30
47
  function connectTo(path, timeoutMs) {
31
48
  return new Promise((resolve, reject) => {
32
49
  const s = netConnect(path);
@@ -3,13 +3,15 @@
3
3
  * in an `implementations/*` package so it can later wire `@cotal-ai/auth` and `@cotal-ai/manager`
4
4
  * without one implementation importing another (AGENTS.md, repository map).
5
5
  *
6
- * This slice serves a local, authenticated endpoint for status, sign-in and a connection check, and
7
- * nothing else: no registration, no Manager, no mesh or Cloud connection, no service install. Linux
6
+ * This slice serves a local, authenticated endpoint for status, sign-in, a connection check and a
7
+ * registration request, and nothing else: no activation or serving, no Manager, no mesh or Cloud
8
+ * connection, no service install. Linux
8
9
  * only. Sign-in runs inside the
9
10
  * daemon through stock `establishIdpSession`, so the session lands in the machine-home Cotal
10
11
  * directory where `cotal login` keeps it, and no token ever crosses the local endpoint. The
11
12
  * connection check (`connect.ts`) reads the space's public account identity and keeps the daemon's
12
- * own per-space identity, never a Manager's. It is a
13
+ * own per-space identity, never a Manager's. The registration request (`register.ts`) sends one stock
14
+ * `register` for that identity and reports the space's answer; approval stays with the space owner. It is a
13
15
  * foreground foundation, not a resilient machine service: a daemon killed without cleanup leaves
14
16
  * its socket behind, and the next start refuses until the operator removes it (see the
15
17
  * ALREADY_RUNNING remedy). `state: "ready"` means the local control listener is bound and
@@ -24,7 +26,7 @@ import { peerCredentials } from "@cotal-ai/seat";
24
26
  import { normalizeIdpUrl } from "@cotal-ai/auth";
25
27
  import { EndpointRefusal, daemonPaths } from "./endpoint.js";
26
28
  import { serveDaemon } from "./server.js";
27
- import { connectCheckViaDaemon, loginViaDaemon, queryDaemon } from "./client.js";
29
+ import { connectCheckViaDaemon, loginViaDaemon, queryDaemon, registerViaDaemon } from "./client.js";
28
30
  export const daemonFlags = [
29
31
  jsonFlag,
30
32
  plainFlag,
@@ -187,6 +189,47 @@ async function connectCheckCommand(args) {
187
189
  ] },
188
190
  ] }));
189
191
  }
192
+ /** Ask the running daemon to submit its own service registration for a space. The space owner's
193
+ * approval and the operator's name binding happen elsewhere; this never activates or serves. */
194
+ async function registerCommand(args) {
195
+ const presentation = presentationOf(args.values);
196
+ const space = args.values.space;
197
+ if (typeof space !== "string" || !space)
198
+ throw new CommandFailure("refused", "USAGE_INVALID", "cotal daemon register needs --space <s>");
199
+ let reply;
200
+ try {
201
+ reply = await registerViaDaemon({ paths: daemonPaths(), peerUid, space });
202
+ }
203
+ catch (e) {
204
+ throw asFailure(e);
205
+ }
206
+ if (!reply.ok)
207
+ throw new CommandFailure("refused", "USAGE_INVALID", `the daemon refused the registration request: ${reply.code} ${reply.message}`, { native: { code: reply.code } });
208
+ const r = reply.result;
209
+ const native = { native: { code: (r.outcome === "refused" && "reason" in r ? r.reason : r.outcome).toUpperCase().replaceAll("-", "_") } };
210
+ if (r.outcome === "not-registered")
211
+ throw new CommandFailure("refused", "TARGET_NOT_FOUND", r.message, native);
212
+ if (r.outcome === "not-user-auth" || r.outcome === "unsupported-by-space")
213
+ throw new CommandFailure("refused", "UNSUPPORTED", r.message, native);
214
+ if (r.outcome === "not-signed-in")
215
+ throw new CommandFailure("refused", "AUTH_REQUIRED", r.message, native);
216
+ if (r.outcome === "unreachable")
217
+ throw new CommandFailure("refused", "DEPENDENCY_UNAVAILABLE", r.message, native);
218
+ if (r.outcome === "account-mismatch" || r.outcome === "conflict")
219
+ throw new CommandFailure("refused", "CONFLICT", r.message, native);
220
+ // Only a refusal carrying the space's stock reason is a denial; one without is the daemon's own failure.
221
+ if (r.outcome === "refused")
222
+ throw new CommandFailure("refused", "reason" in r ? "PERMISSION_DENIED" : "DEPENDENCY_UNAVAILABLE", r.message, native);
223
+ if (r.outcome !== "approval-required" && r.outcome !== "pending" && r.outcome !== "registered")
224
+ throw new CommandFailure("unknown", "OPERATION_UNKNOWN", r.message, native);
225
+ const title = r.outcome === "approval-required" ? "Waiting for the space owner" : r.outcome === "pending" ? "Registration in progress" : "Registered, not connected";
226
+ writeResult(presentation, { command: "daemon register", target: { space }, result: r }, () => ({ sections: [
227
+ { title, blocks: [
228
+ { kind: "fields", rows: [["Space", r.space], ["Service", r.endpoint], ["Instance", r.instanceId], ["Request", r.requestId]] },
229
+ { kind: "lines", lines: [{ text: r.message, tone: "hint" }] },
230
+ ] },
231
+ ] }));
232
+ }
190
233
  async function serve() {
191
234
  let running;
192
235
  try {
@@ -214,23 +257,26 @@ async function run(args) {
214
257
  return login(args);
215
258
  if (mode === "connect-check")
216
259
  return connectCheckCommand(args);
217
- throw new CommandFailure("refused", "USAGE_INVALID", mode === undefined ? "cotal daemon needs a mode: status, serve, login or connect-check" : `unknown daemon mode ${JSON.stringify(mode)}`, { remedy: "cotal daemon status" });
260
+ if (mode === "register")
261
+ return registerCommand(args);
262
+ throw new CommandFailure("refused", "USAGE_INVALID", mode === undefined ? "cotal daemon needs a mode: status, serve, login, connect-check or register" : `unknown daemon mode ${JSON.stringify(mode)}`, { remedy: "cotal daemon status" });
218
263
  }
219
264
  const daemonCommand = {
220
265
  kind: "command",
221
266
  name: "daemon",
222
267
  group: "Manager",
223
- summary: "the local machine daemon (Linux): serve its control endpoint, read its status, sign it in, check whether it can connect to a space",
224
- positionals: "<status|serve|login|connect-check>",
268
+ summary: "the local machine daemon (Linux): serve its control endpoint, read its status, sign it in, check and request its own registration with a space",
269
+ positionals: "<status|serve|login|connect-check|register>",
225
270
  flags: daemonFlags,
226
271
  subcommands: [
227
272
  { name: "status", summary: "ask the running daemon for its status over the local endpoint", flagNames: ["json", "plain"], outputs: ["json", "plain"], reads: true, prepareMeshTarget: false },
228
273
  { 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 },
229
274
  { 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 },
230
275
  { 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 },
276
+ { 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 },
231
277
  ],
232
278
  prepareMeshTarget: false,
233
- 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"],
279
+ 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"],
234
280
  run,
235
281
  };
236
282
  registry.register(daemonCommand);
@@ -13,13 +13,14 @@
13
13
  */
14
14
  import { normalizeIdpUrl } from "@cotal-ai/auth";
15
15
  import { DAEMON_PROTOCOL_VERSION, MAX_LINE_BYTES } from "./endpoint.js";
16
- export const DAEMON_OPS = ["status", "login-start", "login-cancel", "connect-check"];
16
+ export const DAEMON_OPS = ["status", "login-start", "login-cancel", "connect-check", "register"];
17
17
  /** The fields each op takes besides `v`, `id` and `op`. Required ones must be present. */
18
18
  const OP_FIELDS = {
19
19
  status: { required: [], optional: [] },
20
20
  "login-start": { required: ["idp"], optional: ["clientId"] },
21
21
  "login-cancel": { required: ["attempt"], optional: [] },
22
22
  "connect-check": { required: ["space"], optional: [] },
23
+ register: { required: ["space"], optional: [] },
23
24
  };
24
25
  const ENVELOPE_KEYS = new Set(["v", "id", "op"]);
25
26
  const ID_PATTERN = /^[A-Za-z0-9_-]{1,64}$/;
@@ -64,7 +65,7 @@ export function parseRequest(line) {
64
65
  return bad(id, "attempt must be the 22-character attempt id from login-prompt");
65
66
  return { ok: true, request: { v, id, op, attempt: obj.attempt } };
66
67
  }
67
- if (op === "connect-check") {
68
+ if (op === "connect-check" || op === "register") {
68
69
  if (typeof obj.space !== "string" || !SPACE_PATTERN.test(obj.space))
69
70
  return bad(id, "space must be 1-64 characters of [A-Za-z0-9_-]");
70
71
  return { ok: true, request: { v, id, op, space: obj.space } };
@@ -0,0 +1,138 @@
1
+ /**
2
+ * The daemon's own service registration (A2b.2). It submits ONE stock `register` request for the
3
+ * daemon's A2b.1 identity and reports the stock answer. It never activates, serves, holds a bearer,
4
+ * decides, binds a name or grants a caller, and it never touches a Manager.
5
+ *
6
+ * It runs the A2b.1 readiness check first. Any outcome other than `ready-to-register` is returned
7
+ * unchanged, so a space without service endpoints gets 0 POST and no identity.
8
+ *
9
+ * Request identity follows the stock rule (docs/embedding.md, "Service authority contracts"): one
10
+ * requestId per instance and body. It is persisted with the body digest before the first send, through
11
+ * stock `claimIdentityRecord`, and replayed unchanged on every later call, restarts included. The host
12
+ * bounds an instance to 64 distinct requests, so a fresh key is never minted for the same instance. A
13
+ * different body for the same instance is refused locally.
14
+ *
15
+ * The request goes through the registered auth provider's `serviceEndpointAuthority` with no proof
16
+ * argument, so the stock client presents the machine's signed-in IdP proof itself. The daemon never
17
+ * reads or relays that proof.
18
+ */
19
+ import { randomBytes } from "node:crypto";
20
+ import { join } from "node:path";
21
+ import { contractDigest, resolveAuthProvider, ServiceAuthorityTransportError, singleDocumentClosure, VOID_SCHEMA_DIGEST } from "@cotal-ai/core";
22
+ import { claimIdentityRecord, findMesh, homeCotalDir, spaceSegment, userAuthStateDir, workspaceSecretStore } from "@cotal-ai/workspace";
23
+ import { connectCheck, DAEMON_ENDPOINT } from "./connect.js";
24
+ /** The daemon's contract: one cluster with one read command, declared for the later telemetry slice.
25
+ * It is ephemeral, untargeted and ungoverned, the only shape the service door admits. Nothing serves it yet. */
26
+ const CLUSTER = { urn: DAEMON_ENDPOINT, revision: 1, attributes: [], events: [],
27
+ commands: [{ name: "machine-status", class: "ephemeral", targeted: false, capability: "read", inputDigest: VOID_SCHEMA_DIGEST, outputDigest: VOID_SCHEMA_DIGEST }] };
28
+ const closure = singleDocumentClosure(CLUSTER);
29
+ const REGISTRATION = { clusterDigests: [closure.closureDigest], contractArtifacts: [closure.manifest, CLUSTER] };
30
+ const KIND = "cotal-daemon-register-request";
31
+ export function daemonRegisterFile(space, home = homeCotalDir()) {
32
+ return join(home, "daemon", spaceSegment(space), "register-request.json");
33
+ }
34
+ const parseRecord = (space) => (raw) => {
35
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw))
36
+ return undefined;
37
+ const o = raw;
38
+ if (Object.keys(o).sort().join(",") !== "bodyDigest,instanceId,kind,requestId,space,v")
39
+ return undefined;
40
+ if (o.v !== 1 || o.kind !== KIND || o.space !== space || typeof o.instanceId !== "string" || typeof o.bodyDigest !== "string")
41
+ return undefined;
42
+ if (typeof o.requestId !== "string" || !/^[0-9a-f]{32}$/.test(o.requestId))
43
+ return undefined;
44
+ return o;
45
+ };
46
+ /** Fixed sentences for the stock register denial codes (`service-endpoint-authority.ts` register
47
+ * door and its host). Any other code is reported as {@link UNRECOGNIZED}, so no remote text crosses
48
+ * the endpoint. */
49
+ const DENIED = {
50
+ "proposal-refused": "the space owner refused this daemon; a new proposal needs a new request, which this command does not send",
51
+ "proposal-expired": "the space owner did not decide before the proposal expired; a new proposal needs a new request, which this command does not send",
52
+ "approval-revoked": "the space owner revoked this daemon's approval",
53
+ "permission-denied": "the signed-in account may not register services in this space",
54
+ conflict: "the space already holds a different registration for this daemon's instance",
55
+ expired: "the space's approval for this daemon has expired",
56
+ "bad-request": "the space refused the registration request as malformed",
57
+ "closure-invalid": "the space refused the daemon's contract",
58
+ "unsupported-runtime": "the space refused the daemon's contract",
59
+ unsupported: "the space does not support this registration",
60
+ unauthenticated: "the space did not accept this machine's sign-in",
61
+ "rate-limited": "the space is rate-limiting registrations; try again later",
62
+ "resource-exhausted": "the space has no room left for this daemon's requests",
63
+ };
64
+ const UNRECOGNIZED = "unrecognized";
65
+ /** The stock register pending phase (`approved-unregistered`); any other is reported as {@link UNRECOGNIZED}. */
66
+ const PHASES = new Set(["approved-unregistered"]);
67
+ /** A stock proposal id: 64 lowercase hex characters (`service-host-ops.ts`, `hexN(proposalId, 64)`). */
68
+ const PROPOSAL_ID = /^[0-9a-f]{64}$/;
69
+ const NOT_SIGNED_IN = "this machine's sign-in was not accepted by the space's provider (expired, revoked or missing); run `cotal daemon login --idp <url>`";
70
+ const NOT_SENT = "the daemon could not send the registration request; nothing was sent and nothing is connected";
71
+ const UNUSABLE = "the space answered the registration with something the daemon does not use; run the same command again";
72
+ export async function registerDaemon(space, home = homeCotalDir()) {
73
+ const ready = await connectCheck(space, home);
74
+ if (ready.outcome !== "ready-to-register")
75
+ return ready;
76
+ const entry = findMesh(space);
77
+ const base = {
78
+ v: 1, kind: "service-endpoint-authority", operation: "register", space,
79
+ accountPublicKey: ready.accountPublicKey, endpoint: ready.endpoint, instanceId: ready.instanceId,
80
+ registration: REGISTRATION, expected: { state: "unregistered" },
81
+ };
82
+ const bodyDigest = contractDigest(base);
83
+ const record = claimIdentityRecord(daemonRegisterFile(space, home), "daemon register request", parseRecord(space), () => ({
84
+ v: 1, kind: KIND, space, instanceId: ready.instanceId, requestId: randomBytes(16).toString("hex"), bodyDigest,
85
+ }));
86
+ if (record.instanceId !== ready.instanceId || record.bodyDigest !== bodyDigest)
87
+ return { outcome: "conflict", space, requestId: record.requestId,
88
+ message: "this daemon already sent a different registration for this space; nothing was sent. Confirm the change before removing its register request file by hand" };
89
+ const request = { ...base, requestId: record.requestId };
90
+ const ids = { space, requestId: record.requestId, endpoint: ready.endpoint, instanceId: ready.instanceId };
91
+ let result;
92
+ try {
93
+ const provider = resolveAuthProvider();
94
+ if (!provider.serviceEndpointAuthority)
95
+ return { outcome: "unsupported-by-space", space, message: "the space's auth service does not host service endpoints, so the daemon cannot register with it" };
96
+ result = (await provider.serviceEndpointAuthority({ store: workspaceSecretStore(entry.root), dir: userAuthStateDir(entry.root, space, { readOnly: true }), request })).result;
97
+ }
98
+ catch (e) {
99
+ const status = e.httpStatus;
100
+ if (status === 404)
101
+ return { outcome: "unsupported-by-space", space, message: "the space's auth service does not host service endpoints, so the daemon cannot register with it" };
102
+ // Only the stock transport's own unknown outcome may have reached the space.
103
+ if (e instanceof ServiceAuthorityTransportError && e.outcome === "unknown")
104
+ return { outcome: "unconfirmed", ...ids,
105
+ message: "the space did not confirm the registration request; run the same command again to replay it unchanged" };
106
+ // Everything else failed before any send. A sign-in the IdP rejected (stock `fetchIdpJwt`'s fixed
107
+ // 401 sentence; it carries no code) or none at all (`signed-out`) needs a new sign-in.
108
+ if (e.code === "signed-out" || /rejected the cached session/.test(e.message ?? ""))
109
+ return { outcome: "not-signed-in", space, message: NOT_SIGNED_IN };
110
+ return { outcome: "refused", space, message: NOT_SENT };
111
+ }
112
+ return registerAnswer(result, ids);
113
+ }
114
+ /** The daemon's outcome for one stock register answer. */
115
+ export function registerAnswer(result, ids) {
116
+ switch (result.state) {
117
+ case "approval-required":
118
+ if (!PROPOSAL_ID.test(result.proposalId))
119
+ return { outcome: "unconfirmed", ...ids, message: UNUSABLE };
120
+ return { outcome: "approval-required", ...ids, proposalId: result.proposalId,
121
+ message: "the space owner must approve this daemon and the space operator must bind its service name; nothing is connected" };
122
+ case "pending":
123
+ return { outcome: "pending", ...ids, phase: PHASES.has(result.phase) ? result.phase : UNRECOGNIZED, message: "the space is processing an approved registration; run the same command again" };
124
+ case "registered": {
125
+ const w = result.witness;
126
+ return { outcome: "registered", ...ids,
127
+ witness: { accountPublicKey: w.accountPublicKey, space: w.space, endpoint: w.endpoint, instanceId: w.instanceId, registrationRevision: w.registrationRevision, processEpoch: w.processEpoch },
128
+ message: "registered with the space; not activated or serving, so this machine is not connected yet" };
129
+ }
130
+ case "denied": {
131
+ const known = Object.hasOwn(DENIED, result.reason);
132
+ return { outcome: "refused", ...ids, reason: known ? result.reason : UNRECOGNIZED,
133
+ message: known ? DENIED[result.reason] : "the space refused the registration for a reason the daemon does not recognize" };
134
+ }
135
+ default:
136
+ return { outcome: "unconfirmed", ...ids, message: UNUSABLE };
137
+ }
138
+ }
@@ -16,6 +16,7 @@ import { DAEMON_PROTOCOL_VERSION, MAX_LINE_BYTES, EndpointRefusal, assertLinux,
16
16
  import { encode, parseRequest } from "./protocol.js";
17
17
  import { createLoginCoordinator } from "./login.js";
18
18
  import { connectCheck } from "./connect.js";
19
+ import { registerDaemon } from "./register.js";
19
20
  import { homeCotalDir } from "@cotal-ai/workspace";
20
21
  /** Requests one connection may send. A sign-in needs two (start, cancel); status polling a few. */
21
22
  export const MAX_REQUESTS_PER_CONNECTION = 64;
@@ -41,7 +42,7 @@ export async function serveDaemon(opts) {
41
42
  const { paths } = opts;
42
43
  const dirOutcome = ensureDir(paths.dir, uid);
43
44
  refuseExisting(paths.socket);
44
- const counters = { accepted: 0, peerRejected: 0, framesDecoded: 0, requestsRefused: 0, statusAnswered: 0, loginsStarted: 0, loginsRefused: 0, cancelsAnswered: 0, connectChecksAnswered: 0 };
45
+ const counters = { accepted: 0, peerRejected: 0, framesDecoded: 0, requestsRefused: 0, statusAnswered: 0, loginsStarted: 0, loginsRefused: 0, cancelsAnswered: 0, connectChecksAnswered: 0, registersAnswered: 0 };
45
46
  const machineHome = opts.sessionDir ?? homeCotalDir();
46
47
  const login = opts.login ?? createLoginCoordinator({ sessionDir: machineHome });
47
48
  const startedAt = new Date();
@@ -119,6 +120,14 @@ export async function serveDaemon(opts) {
119
120
  });
120
121
  return;
121
122
  }
123
+ if (req.op === "register") {
124
+ void registerDaemon(req.space, machineHome).catch(() => ({ outcome: "refused", space: req.space, message: "the daemon could not complete the registration request; nothing is connected" }))
125
+ .then((result) => {
126
+ counters.registersAnswered++;
127
+ return write({ v: DAEMON_PROTOCOL_VERSION, id: req.id, ok: true, result });
128
+ });
129
+ return;
130
+ }
122
131
  if (req.op === "login-cancel") {
123
132
  void login.cancel(req.attempt).then((result) => {
124
133
  counters.cancelsAnswered++;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cotal-ai",
3
- "version": "0.95.0",
3
+ "version": "0.95.1",
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.95.0",
37
- "@cotal-ai/cli": "0.95.0",
38
- "@cotal-ai/connector-core": "0.95.0",
39
- "@cotal-ai/core": "0.95.0",
40
- "@cotal-ai/delivery": "0.95.0",
41
- "@cotal-ai/manager": "0.95.0",
42
- "@cotal-ai/runtime": "0.95.0",
43
- "@cotal-ai/seat": "0.95.0",
44
- "@cotal-ai/workspace": "0.95.0"
36
+ "@cotal-ai/auth": "0.95.1",
37
+ "@cotal-ai/cli": "0.95.1",
38
+ "@cotal-ai/connector-core": "0.95.1",
39
+ "@cotal-ai/core": "0.95.1",
40
+ "@cotal-ai/delivery": "0.95.1",
41
+ "@cotal-ai/manager": "0.95.1",
42
+ "@cotal-ai/runtime": "0.95.1",
43
+ "@cotal-ai/seat": "0.95.1",
44
+ "@cotal-ai/workspace": "0.95.1"
45
45
  },
46
46
  "devDependencies": {
47
- "@cotal-ai/connector-claude-code": "0.95.0",
48
- "@cotal-ai/connector-codex": "0.95.0",
49
- "@cotal-ai/connector-hermes": "0.95.0",
50
- "@cotal-ai/connector-jcode": "0.95.0",
51
- "@cotal-ai/connector-opencode": "0.95.0",
52
- "@cotal-ai/linear": "0.95.0",
53
- "@cotal-ai/pi": "0.95.0",
47
+ "@cotal-ai/connector-claude-code": "0.95.1",
48
+ "@cotal-ai/connector-codex": "0.95.1",
49
+ "@cotal-ai/connector-hermes": "0.95.1",
50
+ "@cotal-ai/connector-jcode": "0.95.1",
51
+ "@cotal-ai/connector-opencode": "0.95.1",
52
+ "@cotal-ai/linear": "0.95.1",
53
+ "@cotal-ai/pi": "0.95.1",
54
54
  "@cotal-ai/smoke-kit": "0.0.0",
55
- "@cotal-ai/web": "0.95.0",
55
+ "@cotal-ai/web": "0.95.1",
56
56
  "@nats-io/jetstream": "^3.4.0",
57
57
  "@nats-io/transport-node": "^3.4.0",
58
58
  "typescript": "^5.9.3"