@akshar5/cohall 0.10.0 → 0.10.2

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 CHANGED
@@ -1,5 +1,26 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.10.2](https://github.com/AksharP5/cohall/compare/v0.10.1...v0.10.2) (2026-10-04)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * **client:** cancel pairing response body reads ([#165](https://github.com/AksharP5/cohall/issues/165)) ([92702ee](https://github.com/AksharP5/cohall/commit/92702eed5ce582f7086fa12f45bd5acb1cc80a24))
9
+ * **device:** allow workspace names beginning with two dots ([#163](https://github.com/AksharP5/cohall/issues/163)) ([5764e37](https://github.com/AksharP5/cohall/commit/5764e37935502e861316c46b36b0e0db868a66c4))
10
+ * **device:** reject attachment FIFOs without blocking ([#162](https://github.com/AksharP5/cohall/issues/162)) ([7eae2ca](https://github.com/AksharP5/cohall/commit/7eae2ca46d1dfbbb6fa91a0236d137b52dc0f9c3))
11
+ * **mcp:** stop cancelled attachment downloads before writing ([#164](https://github.com/AksharP5/cohall/issues/164)) ([1b91dc4](https://github.com/AksharP5/cohall/commit/1b91dc453e44f72051729fe32787d33b1d275b4e))
12
+ * **relay:** change addresses while workers are connected ([#166](https://github.com/AksharP5/cohall/issues/166)) ([87c3baa](https://github.com/AksharP5/cohall/commit/87c3baa886bd887bb7aaf00d76bcea44e8cbe000))
13
+
14
+ ## [0.10.1](https://github.com/AksharP5/cohall/compare/v0.10.0...v0.10.1) (2026-10-04)
15
+
16
+
17
+ ### Bug Fixes
18
+
19
+ * **cli:** prevent oversized files from bypassing input limits ([#160](https://github.com/AksharP5/cohall/issues/160)) ([c6ef1d1](https://github.com/AksharP5/cohall/commit/c6ef1d1b31454b3baf755944cd0c6ef7021594c3))
20
+ * **config:** ignore invalid XDG directories ([#157](https://github.com/AksharP5/cohall/issues/157)) ([10e9acb](https://github.com/AksharP5/cohall/commit/10e9acbfdd2839e12b384467b568260c183a88ef))
21
+ * **providers:** preserve multipart OpenCode answers ([#161](https://github.com/AksharP5/cohall/issues/161)) ([219c988](https://github.com/AksharP5/cohall/commit/219c988ccfd5b3725242da5cbcab1ac27da63cf2))
22
+ * **relay:** keep usage available for all retained devices ([#159](https://github.com/AksharP5/cohall/issues/159)) ([a01b960](https://github.com/AksharP5/cohall/commit/a01b960437011148059871fb46d0fa14ab40361d))
23
+
3
24
  ## [0.10.0](https://github.com/AksharP5/cohall/compare/v0.9.0...v0.10.0) (2026-10-04)
4
25
 
5
26
 
package/README.md CHANGED
@@ -280,8 +280,12 @@ advertise attachment support. Named Grok Bots do not support file attachments.
280
280
  If a long result and output files exceed the 1 MiB transfer limit, the task
281
281
  completes with its text result and a notice that the files were omitted.
282
282
  Provider text results must fit 128 KiB. Codex and OpenCode JSON events are capped
283
- at 1 MiB each. If an event is discarded at that limit, the provider must return
284
- a later text answer or the task fails explicitly.
283
+ at 1 MiB each; retained OpenCode message parts use the same bound. If an event
284
+ is discarded at that limit, the provider must return
285
+ a later complete answer or the task fails explicitly. Identified OpenCode
286
+ multipart answers must follow a new message-start event; more parts from the same
287
+ message cannot recover discarded content. Older OpenCode events without part IDs
288
+ keep their last-text behavior.
285
289
 
286
290
  `--timeout` accepts 5 to 86400 seconds and defaults to 900. It limits how long
287
291
  the command waits; the task continues after that. Invalid timeout values are
@@ -412,8 +416,9 @@ cohall usage
412
416
  It and `cohall devices` include each device's current queue count and the
413
417
  creation time of its oldest queued task. Assigned, running, cancelling, and
414
418
  terminal tasks do not count as queued. Queue fields are absent with older relays.
415
- `usage` reports retained Cohall task activity by device, status, and provider;
416
- provider token counts and billing are not available to the relay.
419
+ `usage` reports retained Cohall task activity by device, status, and provider,
420
+ including forgotten devices whose tasks are still retained. Provider token counts
421
+ and billing are not available to the relay.
417
422
 
418
423
  From the relay owner account, queue a Cohall upgrade for every registered
419
424
  device and inspect progress:
package/bin/cohall.js CHANGED
@@ -17,7 +17,7 @@ var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
17
17
 
18
18
  // packages/protocol/src/index.ts
19
19
  import { Effect, Schema } from "effect";
20
- var version = "0.10.0", maxSocketPayloadBytes, maxAttachmentBytes, maxTaskAttachments = 2, maxProgressNoteBytes = 1024, bounded = (maxLength) => Schema.String.check(Schema.isMinLength(1), Schema.isMaxLength(maxLength)), optionalText = (maxLength) => Schema.String.check(Schema.isMaxLength(maxLength)), boundedArray = (schema, maxLength) => Schema.Array(schema).check(Schema.isMaxLength(maxLength)), count, uuid = (name) => Schema.String.check(Schema.isUUID(4)).pipe(Schema.brand(name)), isoTimestamp = (name) => Schema.String.check(Schema.isPattern(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,9})?Z$/, {
20
+ var version = "0.10.2", maxSocketPayloadBytes, maxAttachmentBytes, maxTaskAttachments = 2, maxProgressNoteBytes = 1024, bounded = (maxLength) => Schema.String.check(Schema.isMinLength(1), Schema.isMaxLength(maxLength)), optionalText = (maxLength) => Schema.String.check(Schema.isMaxLength(maxLength)), boundedArray = (schema, maxLength) => Schema.Array(schema).check(Schema.isMaxLength(maxLength)), count, uuid = (name) => Schema.String.check(Schema.isUUID(4)).pipe(Schema.brand(name)), isoTimestamp = (name) => Schema.String.check(Schema.isPattern(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,9})?Z$/, {
21
21
  expected: "an ISO-8601 UTC timestamp"
22
22
  })).pipe(Schema.brand(name)), BotId, Bot, DeviceId, ThreadId, MessageId, TaskId, TaskRequestId, TaskRunId, ClarificationId, AuthSessionId, OperationId, Timestamp, TaskDeadline, taskDeadlineError = "Task deadline exceeded", taskDeadlinePassed = (task, time = Date.now()) => task.expiresAt !== undefined && Date.parse(task.expiresAt) <= time, Platform, Provider, DeviceStatus, TaskStatus, OperationStatus, TaskTraceEventKind, MessageRole, ConnectionRole, AuthSession, CreatePairingInput, PairingCredential, ExchangePairingInput, SessionCredential, PairingResult, Capability, Workspace, Device, maxDevicePageDevices = 16, maxDevicePageResponseBytes, DevicePage, minimumDeviceOperationVersion = "0.5.0", parsedVersion = (value) => {
23
23
  const match = /^v?(\d+)\.(\d+)\.(\d+)(-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/.exec(value);
@@ -314,7 +314,7 @@ var init_src = __esm(() => {
314
314
  retainedTasks: count,
315
315
  byStatus: TaskStatusCounts,
316
316
  byProvider: boundedArray(ProviderUsage, Provider.literals.length),
317
- devices: boundedArray(DeviceUsage, 256)
317
+ devices: Schema.Array(DeviceUsage)
318
318
  });
319
319
  TaskTraceEvent = Schema.Struct({
320
320
  kind: TaskTraceEventKind,
@@ -421,9 +421,9 @@ var init_src = __esm(() => {
421
421
 
422
422
  // apps/relay/src/config.ts
423
423
  import { Effect as Effect13, Schema as Schema14 } from "effect";
424
- import { access as access5, chmod as chmod7, mkdir as mkdir8, readFile as readFile5, writeFile as writeFile9 } from "node:fs/promises";
424
+ import { access as access5, chmod as chmod7, mkdir as mkdir8, readFile as readFile4, writeFile as writeFile9 } from "node:fs/promises";
425
425
  import { homedir as homedir5, platform as platform5 } from "node:os";
426
- import { join as join9, resolve as resolve5 } from "node:path";
426
+ import { isAbsolute as isAbsolute6, join as join9, resolve as resolve5 } from "node:path";
427
427
  var RelayConfiguration, defaultDataDirectory = () => {
428
428
  if (platform5() === "win32") {
429
429
  return join9(process.env.LOCALAPPDATA ?? join9(homedir5(), "AppData", "Local"), "Cohall");
@@ -431,10 +431,11 @@ var RelayConfiguration, defaultDataDirectory = () => {
431
431
  if (platform5() === "darwin") {
432
432
  return join9(homedir5(), "Library", "Application Support", "Cohall", "relay");
433
433
  }
434
- return join9(process.env.XDG_DATA_HOME ?? join9(homedir5(), ".local", "share"), "cohall");
434
+ const dataHome = process.env.XDG_DATA_HOME;
435
+ return join9(dataHome !== undefined && isAbsolute6(dataHome) ? dataHome : join9(homedir5(), ".local", "share"), "cohall");
435
436
  }, generatedToken = () => Array.from(crypto.getRandomValues(new Uint8Array(32)), (value) => value.toString(16).padStart(2, "0")).join(""), tokenFrom = async (path) => {
436
437
  if (await access5(path).then(() => true).catch(() => false)) {
437
- return (await readFile5(path, "utf8")).trim();
438
+ return (await readFile4(path, "utf8")).trim();
438
439
  }
439
440
  const token = generatedToken();
440
441
  await writeFile9(path, `${token}
@@ -2712,6 +2713,14 @@ Content-Length: 0\r
2712
2713
  return yield* store.exchangePairing(input.token);
2713
2714
  })).then((value) => json(value, 201)).catch(() => json({ error: "Pairing credential is invalid, expired, or already used" }, 401));
2714
2715
  }
2716
+ if (url.pathname === "/api/device/credential" && request.method === "GET") {
2717
+ const authorization = request.headers.get("authorization");
2718
+ const token = authorization?.startsWith("Bearer ") === true ? authorization.slice("Bearer ".length) : undefined;
2719
+ if (token === undefined || await authenticate(token, "device") === undefined) {
2720
+ return json({ error: "Unauthorized" }, 401);
2721
+ }
2722
+ return json({ ok: true });
2723
+ }
2715
2724
  const attachmentRoute = request.method === "GET" && /^\/api\/tasks\/[^/]+\/attachments(?:\/[^/]+)?$/.test(url.pathname);
2716
2725
  const progressRoute = request.method === "POST" && /^\/api\/tasks\/[^/]+\/progress$/.test(url.pathname);
2717
2726
  const inputRoute = request.method === "POST" && /^\/api\/tasks\/[^/]+\/input$/.test(url.pathname);
@@ -3173,6 +3182,7 @@ var make = (options) => {
3173
3182
  });
3174
3183
  const devicePage = (after) => request("RelayClient.devices", `/api/devices/page${after === undefined ? "" : `?after=${encodeURIComponent(after)}`}`, DevicePage, undefined, maxDevicePageResponseBytes);
3175
3184
  return Service.of({
3185
+ verifyDeviceCredential: () => request("RelayClient.verifyDeviceCredential", "/api/device/credential", Schema2.Struct({ ok: Schema2.Literal(true) })).pipe(Effect2.asVoid),
3176
3186
  devices: () => Effect2.gen(function* () {
3177
3187
  const first = yield* devicePage().pipe(Effect2.catch((cause) => cause instanceof RelayRequestError && cause.status === 404 ? request("RelayClient.devices", "/api/devices", Schema2.Array(Device)) : Effect2.fail(cause)));
3178
3188
  if (!("devices" in first))
@@ -3316,29 +3326,30 @@ var make = (options) => {
3316
3326
  };
3317
3327
  var exchangePairing = (baseUrl, input) => Effect2.gen(function* () {
3318
3328
  const operation = "RelayClient.exchangePairing";
3319
- const response = yield* Effect2.tryPromise({
3320
- try: (signal) => fetch(`${normalizeBaseUrl(baseUrl)}/api/auth/pair`, {
3321
- method: "POST",
3322
- signal: AbortSignal.any([signal, AbortSignal.timeout(30000)]),
3323
- headers: { "content-type": "application/json" },
3324
- body: JSON.stringify(input)
3325
- }),
3326
- catch: (cause) => new RelayRequestError({
3329
+ const json = yield* Effect2.tryPromise({
3330
+ try: async (signal) => {
3331
+ const response = await fetch(`${normalizeBaseUrl(baseUrl)}/api/auth/pair`, {
3332
+ method: "POST",
3333
+ signal: AbortSignal.any([signal, AbortSignal.timeout(30000)]),
3334
+ headers: { "content-type": "application/json" },
3335
+ body: JSON.stringify(input)
3336
+ });
3337
+ if (!response.ok) {
3338
+ throw new RelayRequestError({
3339
+ operation,
3340
+ message: await responseMessage(response, operation),
3341
+ status: response.status
3342
+ });
3343
+ }
3344
+ return jsonBody(response, operation).catch((cause) => {
3345
+ throw cause instanceof RelayDecodeError ? cause : new RelayDecodeError({ operation, message: String(cause) });
3346
+ });
3347
+ },
3348
+ catch: (cause) => cause instanceof RelayRequestError || cause instanceof RelayDecodeError ? cause : new RelayRequestError({
3327
3349
  operation,
3328
3350
  message: cause instanceof Error ? cause.message : String(cause)
3329
3351
  })
3330
3352
  });
3331
- if (!response.ok) {
3332
- return yield* Effect2.fail(new RelayRequestError({
3333
- operation,
3334
- message: yield* Effect2.promise(() => responseMessage(response, operation)),
3335
- status: response.status
3336
- }));
3337
- }
3338
- const json = yield* Effect2.tryPromise({
3339
- try: () => jsonBody(response, operation),
3340
- catch: (cause) => cause instanceof RelayDecodeError ? cause : new RelayDecodeError({ operation, message: String(cause) })
3341
- });
3342
3353
  return yield* Schema2.decodeUnknownEffect(PairingResult)(json).pipe(Effect2.mapError((cause) => new RelayDecodeError({ operation, message: String(cause) })));
3343
3354
  });
3344
3355
  var layer = (options) => Layer.succeed(Service, make(options));
@@ -3365,6 +3376,8 @@ class ProviderRunError extends Schema3.TaggedErrorClass()("CohallProvider.RunErr
3365
3376
  exitCode: Schema3.optionalKey(Schema3.Number)
3366
3377
  }) {
3367
3378
  }
3379
+ var maxResultBytes = 131072;
3380
+ var maxJsonBytes = 1024 * 1024;
3368
3381
  var executables = {
3369
3382
  codex: "codex",
3370
3383
  "claude-code": "claude",
@@ -3541,7 +3554,6 @@ var prepareCommand = async (options) => {
3541
3554
  }
3542
3555
  };
3543
3556
  var forEachJsonEvent = async (stdout, onEvent, onOversized) => {
3544
- const limit = 1024 * 1024;
3545
3557
  let chunks = [];
3546
3558
  let size = 0;
3547
3559
  let oversized = false;
@@ -3549,7 +3561,7 @@ var forEachJsonEvent = async (stdout, onEvent, onOversized) => {
3549
3561
  if (oversized) {
3550
3562
  return;
3551
3563
  }
3552
- if (size + chunk.byteLength > limit) {
3564
+ if (size + chunk.byteLength > maxJsonBytes) {
3553
3565
  chunks = [];
3554
3566
  size = 0;
3555
3567
  oversized = true;
@@ -3653,25 +3665,69 @@ var parseOpenCode = async (stdout, existingSession) => {
3653
3665
  let result = "";
3654
3666
  let sessionId = existingSession;
3655
3667
  let discardedEvent = false;
3668
+ let messageId;
3669
+ const parts = new Map;
3670
+ let partBytes = 0;
3671
+ let oversizedMessage = false;
3672
+ const selectMessage = (id) => {
3673
+ messageId = id;
3674
+ parts.clear();
3675
+ partBytes = 0;
3676
+ oversizedMessage = false;
3677
+ result = "";
3678
+ };
3656
3679
  await forEachJsonEvent(stdout, (event) => {
3657
3680
  const failure = openCodeError(event);
3658
3681
  if (failure !== undefined) {
3659
3682
  throw new Error(failure);
3660
3683
  }
3661
3684
  sessionId = text(event.sessionID) ?? text(event.sessionId) ?? text(event.session_id) ?? text(record(event.info)?.sessionID) ?? sessionId;
3685
+ const part = record(event.part);
3686
+ const nextMessageId = text(part?.messageID);
3687
+ if (text(event.type) === "step_start" && nextMessageId !== undefined && nextMessageId !== messageId) {
3688
+ selectMessage(nextMessageId);
3689
+ discardedEvent = false;
3690
+ return;
3691
+ }
3662
3692
  if (text(event.type) !== "text")
3663
3693
  return;
3664
- const content = textFromPart(event.part) ?? textFromPart(event) ?? text(event.result);
3665
- if (content !== undefined && content.length > 0) {
3666
- result = content;
3667
- discardedEvent = false;
3694
+ const content = textFromPart(part) ?? textFromPart(event) ?? text(event.result);
3695
+ if (content === undefined)
3696
+ return;
3697
+ const partId = text(part?.id);
3698
+ if (nextMessageId !== undefined && partId !== undefined) {
3699
+ if (nextMessageId !== messageId)
3700
+ selectMessage(nextMessageId);
3701
+ if (oversizedMessage)
3702
+ return;
3703
+ const previous = parts.get(partId);
3704
+ if (content.length === 0 && previous === undefined)
3705
+ return;
3706
+ partBytes += byteLength(content) - byteLength(previous ?? "") + (previous === undefined ? byteLength(partId) : 0);
3707
+ if (partBytes > maxJsonBytes) {
3708
+ oversizedMessage = true;
3709
+ parts.clear();
3710
+ return;
3711
+ }
3712
+ parts.set(partId, content);
3713
+ return;
3668
3714
  }
3715
+ if (content.length === 0)
3716
+ return;
3717
+ selectMessage(undefined);
3718
+ result = content;
3719
+ discardedEvent = false;
3669
3720
  }, () => {
3670
3721
  discardedEvent = true;
3671
3722
  });
3672
3723
  if (discardedEvent) {
3673
3724
  throw new Error("Provider JSON event exceeded 1 MiB without a later text response");
3674
3725
  }
3726
+ if (oversizedMessage)
3727
+ throw new Error("OpenCode message exceeded 1 MiB");
3728
+ if (parts.size > 0)
3729
+ result = [...parts.values()].filter((content) => content.length > 0).join(`
3730
+ `);
3675
3731
  if (result.length === 0) {
3676
3732
  throw new Error("OpenCode produced no result; verify its authentication and use a supported project workspace");
3677
3733
  }
@@ -3683,7 +3739,7 @@ var parse = async (options, stdout, onFailure) => {
3683
3739
  case "codex":
3684
3740
  return await parseCodex(stdout, options.sessionId);
3685
3741
  case "claude-code":
3686
- return parseClaude(await boundedText(stdout, 1024 * 1024, onFailure), options.sessionId);
3742
+ return parseClaude(await boundedText(stdout, maxJsonBytes, onFailure), options.sessionId);
3687
3743
  case "opencode":
3688
3744
  return await parseOpenCode(stdout, options.sessionId);
3689
3745
  }
@@ -3808,7 +3864,7 @@ var run = (options) => Effect3.callback((resume, signal) => {
3808
3864
  exitCode
3809
3865
  });
3810
3866
  }
3811
- if (byteLength(result.result) > 131072) {
3867
+ if (byteLength(result.result) > maxResultBytes) {
3812
3868
  throw new ProviderRunError({
3813
3869
  provider: options.provider,
3814
3870
  message: "Provider result exceeded 128 KiB"
@@ -3848,7 +3904,7 @@ var run = (options) => Effect3.callback((resume, signal) => {
3848
3904
 
3849
3905
  // apps/device/src/cli.ts
3850
3906
  import { Effect as Effect10, Schema as Schema11 } from "effect";
3851
- import { access as access4, mkdir as mkdir7, readFile as readFile4, stat as stat4, writeFile as writeFile7 } from "node:fs/promises";
3907
+ import { access as access4, mkdir as mkdir7, open as open4, stat as stat4, writeFile as writeFile7 } from "node:fs/promises";
3852
3908
  import { homedir as homedir4 } from "node:os";
3853
3909
  import { dirname as dirname6, join as join8, resolve as resolve4 } from "node:path";
3854
3910
 
@@ -4121,7 +4177,7 @@ init_src();
4121
4177
  import { Effect as Effect4, Schema as Schema4 } from "effect";
4122
4178
  import { access, chmod as chmod2, mkdir, readFile, realpath, rename, stat, writeFile as writeFile2 } from "node:fs/promises";
4123
4179
  import { homedir as homedir2, hostname, platform as platform2 } from "node:os";
4124
- import { dirname, join as join2, resolve } from "node:path";
4180
+ import { dirname, isAbsolute as isAbsolute2, join as join2, resolve } from "node:path";
4125
4181
  var WorkspaceList = Schema4.Array(Schema4.NonEmptyString).check(Schema4.isMaxLength(64));
4126
4182
  var ProviderList = Schema4.Array(Provider).check(Schema4.isMinLength(1)).check(Schema4.isMaxLength(Provider.literals.length));
4127
4183
  var Sandbox = Schema4.Literals(["read-only", "workspace-write", "danger-full-access"]);
@@ -4163,7 +4219,8 @@ var defaultConfigDirectory = () => {
4163
4219
  if (platform2() === "darwin") {
4164
4220
  return join2(homedir2(), "Library", "Application Support", "Cohall");
4165
4221
  }
4166
- return join2(process.env.XDG_CONFIG_HOME ?? join2(homedir2(), ".config"), "cohall");
4222
+ const configHome = process.env.XDG_CONFIG_HOME;
4223
+ return join2(configHome !== undefined && isAbsolute2(configHome) ? configHome : join2(homedir2(), ".config"), "cohall");
4167
4224
  };
4168
4225
  var defaultRelayDataDirectory = () => {
4169
4226
  if (platform2() === "win32") {
@@ -4172,7 +4229,8 @@ var defaultRelayDataDirectory = () => {
4172
4229
  if (platform2() === "darwin") {
4173
4230
  return join2(homedir2(), "Library", "Application Support", "Cohall", "relay");
4174
4231
  }
4175
- return join2(process.env.XDG_DATA_HOME ?? join2(homedir2(), ".local", "share"), "cohall");
4232
+ const dataHome = process.env.XDG_DATA_HOME;
4233
+ return join2(dataHome !== undefined && isAbsolute2(dataHome) ? dataHome : join2(homedir2(), ".local", "share"), "cohall");
4176
4234
  };
4177
4235
  var relayDataDirectory = () => resolve(process.env.COHALL_DATA_DIR ?? defaultRelayDataDirectory());
4178
4236
  var configurationPath = () => resolve(process.env.COHALL_CONFIG ?? join2(defaultConfigDirectory(), "config.json"));
@@ -5237,7 +5295,7 @@ import { WebSocket } from "ws";
5237
5295
  import { execFile as execFile2 } from "node:child_process";
5238
5296
  import { chmod as chmod5, mkdir as mkdir4, writeFile as writeFile4 } from "node:fs/promises";
5239
5297
  import { homedir as homedir3 } from "node:os";
5240
- import { dirname as dirname4, isAbsolute as isAbsolute3, join as join5 } from "node:path";
5298
+ import { dirname as dirname4, isAbsolute as isAbsolute4, join as join5 } from "node:path";
5241
5299
 
5242
5300
  // apps/device/src/upgrade.ts
5243
5301
  import { Schema as Schema8 } from "effect";
@@ -5255,7 +5313,7 @@ import {
5255
5313
  stat as stat2,
5256
5314
  writeFile as writeFile3
5257
5315
  } from "node:fs/promises";
5258
- import { delimiter as delimiter2, dirname as dirname3, extname as extname2, isAbsolute as isAbsolute2, join as join4, relative, resolve as resolve2 } from "node:path";
5316
+ import { delimiter as delimiter2, dirname as dirname3, extname as extname2, isAbsolute as isAbsolute3, join as join4, relative, resolve as resolve2 } from "node:path";
5259
5317
  import { platform as operatingSystem } from "node:os";
5260
5318
  var packageName = "@akshar5/cohall";
5261
5319
  var minimumBunLookupVersion = "1.2.15";
@@ -5383,7 +5441,7 @@ var resolvePackageInstallation = async (entrypoint, options = {}) => {
5383
5441
  if (bun !== undefined) {
5384
5442
  const result2 = await (options.runner ?? defaultRunner).run(bun, ["pm", "ls", "--global"], 1e4);
5385
5443
  const root2 = result2.stdout.split(/\r?\n/, 1)[0]?.match(/^(.*?) node_modules \(\d+(?: installed)?\)$/)?.[1];
5386
- if (result2.exitCode === 0 && root2 !== undefined && isAbsolute2(root2) && await realpath2(join4(root2, "node_modules", packageName, "bin", "cohall.js")).catch(() => {
5444
+ if (result2.exitCode === 0 && root2 !== undefined && isAbsolute3(root2) && await realpath2(join4(root2, "node_modules", packageName, "bin", "cohall.js")).catch(() => {
5387
5445
  return;
5388
5446
  }) === canonicalEntrypoint)
5389
5447
  return {
@@ -5412,7 +5470,7 @@ var resolvePackageInstallation = async (entrypoint, options = {}) => {
5412
5470
  const reported = normalizePath(result.stdout.trim().split(/\r?\n/).at(-1)?.trim() ?? "");
5413
5471
  const root = reported.endsWith("/node_modules") ? dirname3(reported) : reported;
5414
5472
  const selected = resolve2(globalRoot);
5415
- const configured = isAbsolute2(root) ? resolve2(root) : undefined;
5473
+ const configured = isAbsolute3(root) ? resolve2(root) : undefined;
5416
5474
  if (configured === undefined || (process.platform === "win32" ? selected.toLowerCase() !== configured.toLowerCase() : selected !== configured))
5417
5475
  throw globalEntrypointError();
5418
5476
  const packageEntry = (root2) => join4(root2, "node_modules", packageName, "bin", "cohall.js");
@@ -5440,7 +5498,7 @@ var pnpmServiceDirectories = async (executable, globalRoot, runner = defaultRunn
5440
5498
  const query = async (arguments_) => {
5441
5499
  const result = await checked(runner, { command: executable, arguments: arguments_, environment: pnpmEnvironment() }, 1e4);
5442
5500
  const directory = result.stdout.trim().split(/\r?\n/).at(-1)?.trim() ?? "";
5443
- if (!isAbsolute2(directory))
5501
+ if (!isAbsolute3(directory))
5444
5502
  throw new Error(`pnpm ${arguments_.join(" ")} did not report an absolute directory`);
5445
5503
  return directory;
5446
5504
  };
@@ -5491,7 +5549,7 @@ var executableNames = (command) => {
5491
5549
  };
5492
5550
  var containsPath = (root, path) => {
5493
5551
  const child = relative(root, path);
5494
- return child === "" || !child.startsWith("..") && !isAbsolute2(child);
5552
+ return child === "" || !child.startsWith("..") && !isAbsolute3(child);
5495
5553
  };
5496
5554
  var linuxSystemExecutableRoots = [
5497
5555
  "/bin",
@@ -5543,7 +5601,7 @@ var validateExecutable = async (candidate, installationRoot, preserveSymlink = f
5543
5601
  return preserveSymlink ? resolve2(candidate) : canonical;
5544
5602
  };
5545
5603
  var trustedExecutable = async (command, options = {}) => {
5546
- const candidates = isAbsolute2(command) ? [command] : (process.env.PATH ?? "").split(delimiter2).filter(Boolean).flatMap((directory) => executableNames(command).map((name) => join4(directory, name)));
5604
+ const candidates = isAbsolute3(command) ? [command] : (process.env.PATH ?? "").split(delimiter2).filter(Boolean).flatMap((directory) => executableNames(command).map((name) => join4(directory, name)));
5547
5605
  let failure;
5548
5606
  for (const path of candidates) {
5549
5607
  const available2 = await access2(path, operatingSystem() === "win32" ? constants2.F_OK : constants2.X_OK).then(() => true).catch(() => false);
@@ -6044,7 +6102,7 @@ var deviceServicePlan = (options) => {
6044
6102
  ].join(":");
6045
6103
  if (options.platform === "linux") {
6046
6104
  const configured = process.env.XDG_CONFIG_HOME;
6047
- const configHome = configured !== undefined && isAbsolute3(configured) ? configured : join5(options.home, ".config");
6105
+ const configHome = configured !== undefined && isAbsolute4(configured) ? configured : join5(options.home, ".config");
6048
6106
  const path = join5(configHome, "systemd", "user", "cohall-device.service");
6049
6107
  return {
6050
6108
  file: {
@@ -6441,7 +6499,7 @@ var isLoopback = (url) => url.hostname === "localhost" || url.hostname === "[::1
6441
6499
  var verifyClientCredential = async (relayUrl, token) => {
6442
6500
  await Effect8.runPromise(exports_src.make({ baseUrl: relayUrl, token }).devices());
6443
6501
  };
6444
- var verifyDeviceCredential = (relayUrl, token) => new Promise((resolvePromise, reject) => {
6502
+ var verifyLegacyDeviceCredential = (relayUrl, token) => new Promise((resolvePromise, reject) => {
6445
6503
  const socket = new WebSocket(websocketUrl(relayUrl), {
6446
6504
  handshakeTimeout: 1e4,
6447
6505
  maxPayload: 1024 * 1024,
@@ -6480,6 +6538,12 @@ var verifyDeviceCredential = (relayUrl, token) => new Promise((resolvePromise, r
6480
6538
  socket.once("error", (cause) => finish(cause));
6481
6539
  socket.once("close", () => finish(new Error("New relay rejected the stored device credential")));
6482
6540
  });
6541
+ var verifyDeviceCredential = (relayUrl, token) => Effect8.runPromise(exports_src.make({ baseUrl: relayUrl, token }).verifyDeviceCredential()).catch((cause) => {
6542
+ if (cause instanceof RelayRequestError && (cause.status === 401 || cause.status === 404)) {
6543
+ return verifyLegacyDeviceCredential(relayUrl, token);
6544
+ }
6545
+ throw cause;
6546
+ });
6483
6547
  var switchRelay = async (options) => {
6484
6548
  if (process.env.COHALL_RELAY_URL !== undefined) {
6485
6549
  throw new Error("Unset COHALL_RELAY_URL before changing the stored relay URL");
@@ -6536,33 +6600,29 @@ var switchRelay = async (options) => {
6536
6600
  init_src();
6537
6601
  import { Effect as Effect9, Schema as Schema10 } from "effect";
6538
6602
  import { constants as constants3 } from "node:fs";
6539
- import {
6540
- lstat as lstat2,
6541
- mkdtemp as mkdtemp3,
6542
- mkdir as mkdir6,
6543
- open as open3,
6544
- readdir as readdir2,
6545
- rm as rm4,
6546
- writeFile as writeFile6
6547
- } from "node:fs/promises";
6603
+ import { lstat as lstat2, mkdtemp as mkdtemp3, mkdir as mkdir6, open as open3, readdir as readdir2, rm as rm4, writeFile as writeFile6 } from "node:fs/promises";
6548
6604
  import { tmpdir as tmpdir2 } from "node:os";
6549
6605
  import { basename as basename2, join as join7 } from "node:path";
6550
- var readBoundedFile = async (handle) => {
6551
- const buffer = Buffer.allocUnsafe(maxAttachmentBytes + 1);
6552
- let offset = 0;
6553
- while (offset < buffer.length) {
6606
+
6607
+ // apps/device/src/bounded-file.ts
6608
+ var readBoundedFile = async (handle, limit) => {
6609
+ const buffer = Buffer.allocUnsafe(limit + 1);
6610
+ let size = 0;
6611
+ while (size < buffer.length) {
6554
6612
  const { bytesRead } = await handle.read({
6555
6613
  buffer,
6556
- offset,
6557
- length: buffer.length - offset,
6614
+ offset: size,
6615
+ length: buffer.length - size,
6558
6616
  position: null
6559
6617
  });
6560
6618
  if (bytesRead === 0)
6561
6619
  break;
6562
- offset += bytesRead;
6620
+ size += bytesRead;
6563
6621
  }
6564
- return buffer.subarray(0, offset);
6622
+ return buffer.subarray(0, size);
6565
6623
  };
6624
+
6625
+ // apps/device/src/task-attachments.ts
6566
6626
  var readInputAttachments = async (paths2) => {
6567
6627
  if (paths2.length > maxTaskAttachments) {
6568
6628
  throw new Error(`A task supports at most ${maxTaskAttachments} input files`);
@@ -6579,13 +6639,13 @@ var readInputAttachments = async (paths2) => {
6579
6639
  if (!source.isFile()) {
6580
6640
  throw new Error(`Attachment ${name} must be a regular file`);
6581
6641
  }
6582
- const handle = await open3(path, constants3.O_RDONLY | (process.platform === "win32" ? 0 : constants3.O_NOFOLLOW));
6642
+ const handle = await open3(path, constants3.O_RDONLY | (process.platform === "win32" ? 0 : constants3.O_NOFOLLOW | constants3.O_NONBLOCK));
6583
6643
  try {
6584
6644
  const metadata = await handle.stat();
6585
6645
  if (!metadata.isFile() || metadata.dev !== source.dev || metadata.ino !== source.ino || metadata.size < 1 || metadata.size > maxAttachmentBytes) {
6586
6646
  throw new Error(`Attachment ${name} must be a regular file of 1 to ${maxAttachmentBytes} bytes`);
6587
6647
  }
6588
- const data = await readBoundedFile(handle);
6648
+ const data = await readBoundedFile(handle, maxAttachmentBytes);
6589
6649
  if (data.length < 1 || data.length > maxAttachmentBytes) {
6590
6650
  throw new Error(`Attachment ${name} changed size while reading`);
6591
6651
  }
@@ -6636,14 +6696,14 @@ var collectOutputs = async (directory) => {
6636
6696
  throw new Error(`Output ${name} repeats another file name`);
6637
6697
  }
6638
6698
  names.add(key);
6639
- const handle = await open3(join7(directory, name), constants3.O_RDONLY | (process.platform === "win32" ? 0 : constants3.O_NOFOLLOW));
6699
+ const handle = await open3(join7(directory, name), constants3.O_RDONLY | (process.platform === "win32" ? 0 : constants3.O_NOFOLLOW | constants3.O_NONBLOCK));
6640
6700
  try {
6641
6701
  const metadata = await handle.stat();
6642
6702
  const pathMetadata = await lstat2(join7(directory, name));
6643
6703
  if (!metadata.isFile() || !pathMetadata.isFile() || metadata.dev !== pathMetadata.dev || metadata.ino !== pathMetadata.ino || metadata.size < 1 || metadata.size > maxAttachmentBytes) {
6644
6704
  throw new Error(`Output ${name} must be a regular file of 1 to ${maxAttachmentBytes} bytes`);
6645
6705
  }
6646
- const data = await readBoundedFile(handle);
6706
+ const data = await readBoundedFile(handle, maxAttachmentBytes);
6647
6707
  if (data.length < 1 || data.length > maxAttachmentBytes) {
6648
6708
  throw new Error(`Output ${name} changed size while reading`);
6649
6709
  }
@@ -6867,6 +6927,19 @@ var identifier = (arguments_, label) => {
6867
6927
  };
6868
6928
  var print = (value) => console.log(JSON.stringify(value, null, 2));
6869
6929
  var printLine = (value) => console.log(JSON.stringify(value));
6930
+ var readTextFile = async (path, limit, exceeded) => {
6931
+ if ((await stat4(path)).size > limit)
6932
+ throw new Error(exceeded);
6933
+ const file = await open4(path, "r");
6934
+ try {
6935
+ const data = await readBoundedFile(file, limit);
6936
+ if (data.length > limit)
6937
+ throw new Error(exceeded);
6938
+ return data.toString("utf8");
6939
+ } finally {
6940
+ await file.close();
6941
+ }
6942
+ };
6870
6943
  var readInput = async (arguments_, name) => {
6871
6944
  const direct = option(arguments_, name);
6872
6945
  const path = option(arguments_, `${name}-file`);
@@ -6878,11 +6951,8 @@ var readInput = async (arguments_, name) => {
6878
6951
  if (!await access4(path).then(() => true).catch(() => false)) {
6879
6952
  throw new Error(`File does not exist: ${path}`);
6880
6953
  }
6881
- if ((await stat4(path)).size > byteLimit) {
6882
- throw new Error(`${name} file exceeds ${byteLimit / 1024} KiB`);
6883
- }
6884
6954
  }
6885
- const result = path === undefined ? direct === "-" ? await readStdin(byteLimit, name) : direct : await readFile4(path, "utf8");
6955
+ const result = path === undefined ? direct === "-" ? await readStdin(byteLimit, name) : direct : await readTextFile(path, byteLimit, `${name} file exceeds ${byteLimit / 1024} KiB`);
6886
6956
  if (result !== undefined && result.length > 131072) {
6887
6957
  throw new Error(`${name} exceeds 131072 characters`);
6888
6958
  }
@@ -6913,10 +6983,7 @@ var pairingToken = async (arguments_) => {
6913
6983
  if (path !== undefined && !await access4(path).then(() => true).catch(() => false)) {
6914
6984
  throw new Error(`Pairing token file does not exist: ${path}`);
6915
6985
  }
6916
- if (path !== undefined && (await stat4(path)).size > 512) {
6917
- throw new Error("Pairing token file exceeds 512 bytes");
6918
- }
6919
- const token = (path === undefined ? await readStdin(512, "Pairing token") : await readFile4(path, "utf8")).trim();
6986
+ const token = (path === undefined ? await readStdin(512, "Pairing token") : await readTextFile(path, 512, "Pairing token file exceeds 512 bytes")).trim();
6920
6987
  if (token.length === 0 || new TextEncoder().encode(token).byteLength > 256) {
6921
6988
  throw new Error("Provide a pairing token on stdin or with --token-file");
6922
6989
  }
@@ -7590,9 +7657,9 @@ var runCli = async (command, raw) => {
7590
7657
  init_src();
7591
7658
  import { Effect as Effect11, Schedule as Schedule3, Schema as Schema12 } from "effect";
7592
7659
  import { constants as constants4 } from "node:fs";
7593
- import { open as open4, realpath as realpath4, stat as stat5 } from "node:fs/promises";
7660
+ import { open as open5, realpath as realpath4, stat as stat5 } from "node:fs/promises";
7594
7661
  import { arch, hostname as hostname3, platform as platform4 } from "node:os";
7595
- import { basename as basename3, isAbsolute as isAbsolute4, relative as relative2 } from "node:path";
7662
+ import { basename as basename3, isAbsolute as isAbsolute5, relative as relative2, sep } from "node:path";
7596
7663
  import { WebSocket as WebSocket2 } from "ws";
7597
7664
  var maxQueuedRelayMessages = 8;
7598
7665
 
@@ -7673,7 +7740,7 @@ var allowedWorkspace = async (configuration, requested) => {
7673
7740
  const candidate = await realpath4(selected);
7674
7741
  const allowed = configuration.workspaces.some((root) => {
7675
7742
  const child = relative2(root, candidate);
7676
- return child === "" || !child.startsWith("..") && !isAbsolute4(child);
7743
+ return child !== ".." && !child.startsWith(`..${sep}`) && !isAbsolute5(child);
7677
7744
  });
7678
7745
  if (!allowed) {
7679
7746
  throw new Error(`Workspace ${candidate} is outside this device's configured workspace roots`);
@@ -7684,7 +7751,7 @@ var openAllowedWorkspace = async (configuration, requested) => {
7684
7751
  const path = await allowedWorkspace(configuration, requested);
7685
7752
  const operatingSystem2 = platform4();
7686
7753
  const flags = constants4.O_RDONLY | (operatingSystem2 === "win32" ? 0 : constants4.O_DIRECTORY | constants4.O_NOFOLLOW);
7687
- const handle = await open4(path, flags);
7754
+ const handle = await open5(path, flags);
7688
7755
  const identity = await handle.stat();
7689
7756
  if (!identity.isDirectory()) {
7690
7757
  await handle.close();
@@ -8395,11 +8462,12 @@ var runMcp = async (configuration) => {
8395
8462
  output_path: z.string(),
8396
8463
  direction: z.enum(["input", "output"]).optional()
8397
8464
  }
8398
- }, async ({ task_id, name, output_path, direction }) => {
8465
+ }, async ({ task_id, name, output_path, direction }, { signal }) => {
8399
8466
  const safeName = Schema13.decodeUnknownSync(AttachmentName)(name);
8400
8467
  const safeDirection = direction === undefined ? undefined : Schema13.decodeUnknownSync(AttachmentDirection)(direction);
8401
- const data = await Effect12.runPromise(client2.readAttachment(TaskId.make(task_id), safeName, safeDirection));
8402
- await writeFile8(output_path, data, { flag: "wx", mode: 384 });
8468
+ const data = await Effect12.runPromise(client2.readAttachment(TaskId.make(task_id), safeName, safeDirection), { signal });
8469
+ signal.throwIfAborted();
8470
+ await writeFile8(output_path, data, { flag: "wx", mode: 384, signal });
8403
8471
  return output({ task_id, name: safeName, output_path, bytes: data.length });
8404
8472
  });
8405
8473
  server.registerTool("task_trace", {
@@ -8485,4 +8553,4 @@ await main().catch((cause) => {
8485
8553
  process.exitCode = 1;
8486
8554
  });
8487
8555
 
8488
- //# debugId=4195FFD0F10A67E564756E2164756E21
8556
+ //# debugId=49CCA945DC24F88C64756E2164756E21