@couralabs/claudin 0.1.6 → 0.1.7

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.
Files changed (2) hide show
  1. package/claudin.js +100 -28
  2. package/package.json +1 -1
package/claudin.js CHANGED
@@ -1,5 +1,16 @@
1
1
  #!/usr/bin/env bun
2
2
  // @bun
3
+
4
+ // packages/daemon/src/auth/connectionCredentials.ts
5
+ import { randomBytes } from "crypto";
6
+ var createRunId = () => crypto.randomUUID();
7
+ var createAccessToken = () => randomBytes(32).toString("base64url");
8
+ function appendCredentials(url, { token, runId }) {
9
+ const target = new URL(url);
10
+ target.searchParams.set("token", token);
11
+ target.searchParams.set("runId", runId);
12
+ return target.toString();
13
+ }
3
14
  // node_modules/.bun/zod@4.6.7/node_modules/zod/v4/core/util.js
4
15
  function getEnumValues(entries) {
5
16
  const numericValues = Object.values(entries).filter((v) => typeof v === "number");
@@ -7550,8 +7561,8 @@ class DaemonClient {
7550
7561
  this.socket = socket;
7551
7562
  socket.addEventListener("message", (event) => this.notifyListeners(String(event.data)));
7552
7563
  }
7553
- static async connect(url) {
7554
- const socket = new WebSocket(url);
7564
+ static async connect(url, credentials) {
7565
+ const socket = new WebSocket(appendCredentials(url, credentials));
7555
7566
  await new Promise((resolve, reject) => {
7556
7567
  socket.addEventListener("open", () => resolve(), { once: true });
7557
7568
  socket.addEventListener("error", () => reject(new Error(`Cannot reach ${url}`)), {
@@ -8816,8 +8827,10 @@ class PermissionGate {
8816
8827
  }
8817
8828
  open(kind, toolUseId, input, signal, announcement) {
8818
8829
  return new Promise((settle) => {
8819
- this.pending.set(toolUseId, { kind, input, settle });
8820
- signal.addEventListener("abort", () => this.dismiss(kind, toolUseId), { once: true });
8830
+ const onAbort = () => this.dismiss(kind, toolUseId);
8831
+ const detach = () => signal.removeEventListener("abort", onAbort);
8832
+ this.pending.set(toolUseId, { kind, input, settle, detach });
8833
+ signal.addEventListener("abort", onAbort, { once: true });
8821
8834
  this.emit(announcement);
8822
8835
  });
8823
8836
  }
@@ -8832,6 +8845,7 @@ class PermissionGate {
8832
8845
  if (pending?.kind !== kind)
8833
8846
  return;
8834
8847
  this.pending.delete(toolUseId);
8848
+ pending.detach();
8835
8849
  return pending;
8836
8850
  }
8837
8851
  listIds(kind) {
@@ -9211,7 +9225,6 @@ class AgentSession {
9211
9225
  try {
9212
9226
  yield* sink;
9213
9227
  } finally {
9214
- this.pumping = false;
9215
9228
  this.listenGates(() => {});
9216
9229
  }
9217
9230
  }
@@ -9306,9 +9319,11 @@ class AgentSession {
9306
9319
  if (this.pendingTurns === 0 && this.liveTaskIds.size === 0)
9307
9320
  break;
9308
9321
  }
9322
+ this.pumping = false;
9309
9323
  sink.end();
9310
9324
  } catch (error) {
9311
9325
  this.pendingTurns = 0;
9326
+ this.pumping = false;
9312
9327
  sink.end(error);
9313
9328
  }
9314
9329
  }
@@ -9332,14 +9347,13 @@ class AgentSessionManager {
9332
9347
  }
9333
9348
  createSession(options = {}) {
9334
9349
  const sessionId = options.sessionId ?? crypto.randomUUID();
9335
- this.sessions.set(sessionId, new AgentSession(sessionId, { ...options, sessionId }, this.runtime));
9350
+ this.replaceSession(sessionId, { ...options, sessionId });
9336
9351
  return sessionId;
9337
9352
  }
9338
9353
  async resumeSession(sessionId, options = {}) {
9339
9354
  if (!await this.runtime.getInfo(sessionId))
9340
9355
  return this.createSession({ ...options, sessionId });
9341
- const resumed = new AgentSession(sessionId, { ...options, resume: sessionId }, this.runtime);
9342
- this.sessions.set(sessionId, resumed);
9356
+ this.replaceSession(sessionId, { ...options, resume: sessionId });
9343
9357
  return sessionId;
9344
9358
  }
9345
9359
  async forkSession(sessionId, forkOptions, options = {}) {
@@ -9354,6 +9368,10 @@ class AgentSessionManager {
9354
9368
  for (const sessionId of [...this.sessions.keys()])
9355
9369
  this.closeSession(sessionId);
9356
9370
  }
9371
+ replaceSession(sessionId, options) {
9372
+ this.closeSession(sessionId);
9373
+ this.sessions.set(sessionId, new AgentSession(sessionId, options, this.runtime));
9374
+ }
9357
9375
  requireSession(sessionId) {
9358
9376
  const session = this.sessions.get(sessionId);
9359
9377
  if (!session)
@@ -9503,6 +9521,31 @@ class ReplayClient {
9503
9521
  import { once } from "events";
9504
9522
  import { WebSocketServer } from "ws";
9505
9523
 
9524
+ // packages/daemon/src/auth/checkConnection.ts
9525
+ import { timingSafeEqual } from "crypto";
9526
+ var isSameSecret = (given, expected) => {
9527
+ const givenBytes = Buffer.from(given);
9528
+ const expectedBytes = Buffer.from(expected);
9529
+ return givenBytes.length === expectedBytes.length && timingSafeEqual(givenBytes, expectedBytes);
9530
+ };
9531
+ function readRunId(request) {
9532
+ return new URL(request.url ?? "/", "ws://127.0.0.1").searchParams.get("runId");
9533
+ }
9534
+ function checkConnection(request, token, liveRunIds) {
9535
+ if (request.headers.origin)
9536
+ return { status: 403, reason: "Browser origins are not allowed" };
9537
+ const params = new URL(request.url ?? "/", "ws://127.0.0.1").searchParams;
9538
+ if (!isSameSecret(params.get("token") ?? "", token)) {
9539
+ return { status: 401, reason: "Invalid access token" };
9540
+ }
9541
+ const runId = params.get("runId");
9542
+ if (!runId)
9543
+ return { status: 400, reason: "Missing run id" };
9544
+ if (liveRunIds.has(runId))
9545
+ return { status: 409, reason: "Run id already connected" };
9546
+ return;
9547
+ }
9548
+
9506
9549
  // packages/daemon/src/persistence/rateLimitCache.ts
9507
9550
  import { mkdir as mkdir2, readFile, writeFile } from "fs/promises";
9508
9551
  import { homedir as homedir4 } from "os";
@@ -11360,11 +11403,18 @@ var workspaceHandlers = {
11360
11403
 
11361
11404
  // packages/daemon/src/server/handleRequest.ts
11362
11405
  var handlers = { ...sessionHandlers, ...agentHandlers, ...workspaceHandlers, ...optionsHandlers };
11406
+ var SESSION_SWITCH_TYPES = new Set([
11407
+ "session.created",
11408
+ "session.restored",
11409
+ "session.forked"
11410
+ ]);
11363
11411
  async function* handleRequest(context, request) {
11364
- await context.session.dispatch(request);
11412
+ const origin = context.session;
11413
+ await origin.dispatch(request);
11365
11414
  const handler = handlers[request.type];
11366
11415
  for await (const response of handler(context, request)) {
11367
- await context.session.dispatch(response);
11416
+ const target = SESSION_SWITCH_TYPES.has(response.type) ? context.session : origin;
11417
+ await target.dispatch(response);
11368
11418
  yield response;
11369
11419
  }
11370
11420
  }
@@ -11382,7 +11432,12 @@ function publishEvent(clients, origin, event, toOrigin = true) {
11382
11432
  }
11383
11433
 
11384
11434
  // packages/daemon/src/server/startDaemon.ts
11385
- var DEFAULT_PORT = 7777;
11435
+ var LOOPBACK_HOST = "127.0.0.1";
11436
+ async function reportFailure(context, server, socket, error) {
11437
+ const failure = { type: "request.failed", reason: String(error) };
11438
+ await context.session.dispatch(failure).catch(() => {});
11439
+ publishEvent(server.clients, socket, failure);
11440
+ }
11386
11441
  async function answerMessage(context, server, socket, data) {
11387
11442
  try {
11388
11443
  const request = requestEventSchema.parse(JSON.parse(data));
@@ -11391,15 +11446,27 @@ async function answerMessage(context, server, socket, data) {
11391
11446
  publishEvent(server.clients, socket, response);
11392
11447
  }
11393
11448
  } catch (error) {
11394
- const failure = { type: "request.failed", reason: String(error) };
11395
- await context.session.dispatch(failure);
11396
- publishEvent(server.clients, socket, failure);
11449
+ await reportFailure(context, server, socket, error);
11397
11450
  }
11398
11451
  }
11399
- async function startDaemon(port = DEFAULT_PORT, context) {
11452
+ async function startDaemon(port, token, context) {
11400
11453
  const daemonContext = context ?? await createDaemonContext();
11401
- const server = new WebSocketServer({ port });
11402
- server.on("connection", (socket) => {
11454
+ const liveRunIds = new Set;
11455
+ const server = new WebSocketServer({
11456
+ port,
11457
+ host: LOOPBACK_HOST,
11458
+ verifyClient: ({ req }, respond) => {
11459
+ const rejection = checkConnection(req, token, liveRunIds);
11460
+ if (rejection)
11461
+ respond(false, rejection.status, rejection.reason);
11462
+ else
11463
+ respond(true);
11464
+ }
11465
+ });
11466
+ server.on("connection", (socket, request) => {
11467
+ const runId = readRunId(request) ?? "";
11468
+ liveRunIds.add(runId);
11469
+ socket.once("close", () => liveRunIds.delete(runId));
11403
11470
  socket.on("message", (data) => answerMessage(daemonContext, server, socket, data.toString()));
11404
11471
  });
11405
11472
  server.once("close", () => daemonContext.agents.closeAll());
@@ -24302,7 +24369,8 @@ function useColor() {
24302
24369
  var program = new Command;
24303
24370
 
24304
24371
  // apps/cli/src/index.ts
24305
- var DEFAULT_PORT2 = "7777";
24372
+ var DEFAULT_PORT = "7777";
24373
+ var runId = createRunId();
24306
24374
  var program2 = new Command().name("claudin").enablePositionalOptions();
24307
24375
  var printResumeHint = (sessionId) => {
24308
24376
  if (!sessionId)
@@ -24316,10 +24384,12 @@ var exitOnDestroy = (renderer) => {
24316
24384
  renderer.on("destroy", () => process.exit(0));
24317
24385
  };
24318
24386
  var openContext = (sessionId) => createDaemonContext(undefined, sessionId).catch((error) => program2.error(error.message));
24319
- var daemonUrl = (port) => `ws://localhost:${Number(port)}`;
24320
- program2.option("-p, --port <port>", "daemon port", DEFAULT_PORT2).option("-s, --session <id>", "resume a previous session").action(async (opts) => {
24321
- const server = await startDaemon(Number(opts.port), await openContext(opts.session));
24322
- const renderer = await startTui(await DaemonClient.connect(daemonUrl(opts.port)), {
24387
+ var daemonUrl = (port) => `ws://127.0.0.1:${Number(port)}`;
24388
+ program2.option("-p, --port <port>", "daemon port", DEFAULT_PORT).option("-s, --session <id>", "resume a previous session").action(async (opts) => {
24389
+ const token = createAccessToken();
24390
+ const server = await startDaemon(Number(opts.port), token, await openContext(opts.session));
24391
+ const client = await DaemonClient.connect(daemonUrl(opts.port), { token, runId });
24392
+ const renderer = await startTui(client, {
24323
24393
  resumeSessionId: opts.session,
24324
24394
  onExit: printResumeHint
24325
24395
  });
@@ -24329,14 +24399,16 @@ program2.option("-p, --port <port>", "daemon port", DEFAULT_PORT2).option("-s, -
24329
24399
  server.close(() => process.exit(0));
24330
24400
  });
24331
24401
  });
24332
- program2.command("daemon").description("Run the daemon without a UI so terminals can attach to it").option("-p, --port <port>", "daemon port", DEFAULT_PORT2).option("-s, --session <id>", "serve a previous session").action(async (opts) => {
24333
- await startDaemon(Number(opts.port), await openContext(opts.session));
24334
- process.stdout.write(`Daemon on ${daemonUrl(opts.port)}
24335
- Attach with: claudin attach -p ${opts.port}
24402
+ program2.command("daemon").description("Run the daemon without a UI so terminals can attach to it").option("-p, --port <port>", "daemon port", DEFAULT_PORT).option("-s, --session <id>", "serve a previous session").action(async (opts) => {
24403
+ const token = createAccessToken();
24404
+ await startDaemon(Number(opts.port), token, await openContext(opts.session));
24405
+ process.stdout.write(`Daemon on ${daemonUrl(opts.port)} (run ${runId})
24406
+ Attach with: claudin attach -p ${opts.port} --token ${token}
24336
24407
  `);
24337
24408
  });
24338
- program2.command("attach").description("Attach another terminal to a running daemon's live session").option("-p, --port <port>", "daemon port", DEFAULT_PORT2).action(async (opts) => {
24339
- const client = await DaemonClient.connect(daemonUrl(opts.port)).catch((error) => program2.error(error.message));
24409
+ program2.command("attach").description("Attach another terminal to a running daemon's live session").option("-p, --port <port>", "daemon port", DEFAULT_PORT).option("-t, --token <token>", "access token printed by the daemon (or CLAUDIN_TOKEN)").action(async (opts) => {
24410
+ const token = opts.token ?? process.env.CLAUDIN_TOKEN ?? program2.error("An access token is required: pass --token or set CLAUDIN_TOKEN");
24411
+ const client = await DaemonClient.connect(daemonUrl(opts.port), { token, runId }).catch((error) => program2.error(error.message));
24340
24412
  const renderer = await startTui(client, { attach: true, onExit: printResumeHint });
24341
24413
  client.onDisconnect(() => renderer.destroy());
24342
24414
  exitOnDestroy(renderer);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@couralabs/claudin",
3
- "version": "0.1.6",
3
+ "version": "0.1.7",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "claudin": "claudin.js"