@akshar5/cohall 0.5.6 → 0.6.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.
- package/CHANGELOG.md +15 -0
- package/README.md +77 -1
- package/bin/cohall.js +1255 -497
- package/bin/cohall.js.map +15 -13
- package/docs/install.md +23 -3
- package/docs/integrations.md +5 -0
- package/package.json +1 -1
package/bin/cohall.js
CHANGED
|
@@ -16,10 +16,10 @@ var __export = (target, all) => {
|
|
|
16
16
|
var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
17
17
|
|
|
18
18
|
// packages/protocol/src/index.ts
|
|
19
|
-
import { Schema } from "effect";
|
|
20
|
-
var version = "0.
|
|
19
|
+
import { Effect, Schema } from "effect";
|
|
20
|
+
var version = "0.6.1", maxSocketPayloadBytes, 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
|
-
})).pipe(Schema.brand(name)), DeviceId, ThreadId, MessageId, TaskId, AuthSessionId, OperationId, Timestamp, Platform, Provider, DeviceStatus, TaskStatus, OperationStatus, TaskTraceEventKind, MessageRole, ConnectionRole, AuthSession, CreatePairingInput, PairingCredential, ExchangePairingInput, SessionCredential, PairingResult, Capability, Workspace, Device, minimumDeviceOperationVersion = "0.5.0", parsedVersion = (value) => {
|
|
22
|
+
})).pipe(Schema.brand(name)), BotId, Bot, DeviceId, ThreadId, MessageId, TaskId, AuthSessionId, OperationId, Timestamp, Platform, Provider, DeviceStatus, TaskStatus, OperationStatus, TaskTraceEventKind, MessageRole, ConnectionRole, AuthSession, CreatePairingInput, PairingCredential, ExchangePairingInput, SessionCredential, PairingResult, Capability, Workspace, Device, minimumDeviceOperationVersion = "0.5.0", parsedVersion = (value) => {
|
|
23
23
|
const match = /^v?(\d+)\.(\d+)\.(\d+)(-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/.exec(value);
|
|
24
24
|
if (match === null) {
|
|
25
25
|
return;
|
|
@@ -50,10 +50,25 @@ var version = "0.5.6", maxSocketPayloadBytes, bounded = (maxLength) => Schema.St
|
|
|
50
50
|
return;
|
|
51
51
|
}
|
|
52
52
|
throw new Error(`All-device upgrades require Cohall ${minimumDeviceOperationVersion} or newer on every device. Upgrade individually first: ${unsupported.map((device) => `${device.name} (${device.version})`).join(", ")}`);
|
|
53
|
-
}, Thread, Message,
|
|
53
|
+
}, Thread, Message, validBotTarget = (input) => {
|
|
54
|
+
const provider = input.provider ?? (input.botId === undefined ? "codex" : "grok-bot");
|
|
55
|
+
if (provider === "grok-bot" && input.workspace !== undefined) {
|
|
56
|
+
return "Grok Bots use their own computer context; omit workspace";
|
|
57
|
+
}
|
|
58
|
+
return provider === "grok-bot" === (input.botId !== undefined) ? undefined : "The grok-bot provider requires botId; other providers cannot select a bot";
|
|
59
|
+
}, Task, taskSlot = (task) => task.provider === "grok-bot" ? `grok-bot:${task.botId}` : "cli", upgradeVersion, CreateUpgradeOperationsInput, DeviceOperation, TaskStatusCounts, ProviderUsage, DeviceUsage, UsageSummary, TaskTraceEvent, TaskTrace, ThreadContext, CreateTaskInput, SocketEvent, ErrorResponse, terminalTaskStatuses, isTerminalTask = (task) => terminalTaskStatuses.has(task.status), now = () => Timestamp.make(new Date().toISOString()), makeDeviceId = () => DeviceId.make(crypto.randomUUID()), makeThreadId = () => ThreadId.make(crypto.randomUUID()), makeMessageId = () => MessageId.make(crypto.randomUUID()), makeTaskId = () => TaskId.make(crypto.randomUUID()), makeAuthSessionId = () => AuthSessionId.make(crypto.randomUUID()), makeOperationId = () => OperationId.make(crypto.randomUUID()), decodeCreatePairingInput, decodeExchangePairingInput, decodeCreateTaskInput = (input) => Schema.decodeUnknownEffect(CreateTaskInput)(input).pipe(Effect.map((task) => ({
|
|
60
|
+
...task,
|
|
61
|
+
provider: task.provider ?? (task.botId === undefined ? "codex" : "grok-bot")
|
|
62
|
+
}))), decodeCreateUpgradeOperationsInput, decodeDevice, decodeSocketEvent;
|
|
54
63
|
var init_src = __esm(() => {
|
|
55
64
|
maxSocketPayloadBytes = 1024 * 1024;
|
|
56
65
|
count = Schema.Int.check(Schema.isBetween({ minimum: 0, maximum: Number.MAX_SAFE_INTEGER }));
|
|
66
|
+
BotId = bounded(256).pipe(Schema.brand("BotId"));
|
|
67
|
+
Bot = Schema.Struct({
|
|
68
|
+
id: BotId,
|
|
69
|
+
name: bounded(128),
|
|
70
|
+
description: Schema.optionalKey(optionalText(512))
|
|
71
|
+
});
|
|
57
72
|
DeviceId = uuid("DeviceId");
|
|
58
73
|
ThreadId = uuid("ThreadId");
|
|
59
74
|
MessageId = uuid("MessageId");
|
|
@@ -62,7 +77,7 @@ var init_src = __esm(() => {
|
|
|
62
77
|
OperationId = uuid("OperationId");
|
|
63
78
|
Timestamp = isoTimestamp("Timestamp");
|
|
64
79
|
Platform = Schema.Literals(["darwin", "linux", "windows", "unknown"]);
|
|
65
|
-
Provider = Schema.Literals(["codex", "claude-code", "opencode"]);
|
|
80
|
+
Provider = Schema.Literals(["codex", "claude-code", "opencode", "grok-bot"]);
|
|
66
81
|
DeviceStatus = Schema.Literals(["online", "busy", "offline"]);
|
|
67
82
|
TaskStatus = Schema.Literals([
|
|
68
83
|
"queued",
|
|
@@ -127,7 +142,8 @@ var init_src = __esm(() => {
|
|
|
127
142
|
platform: Platform,
|
|
128
143
|
architecture: bounded(64),
|
|
129
144
|
status: DeviceStatus,
|
|
130
|
-
providers: boundedArray(Provider,
|
|
145
|
+
providers: boundedArray(Provider, Provider.literals.length),
|
|
146
|
+
bots: Schema.optionalKey(boundedArray(Bot, 256)),
|
|
131
147
|
capabilities: boundedArray(Capability, 64),
|
|
132
148
|
workspaces: boundedArray(Workspace, 64),
|
|
133
149
|
version: bounded(32),
|
|
@@ -157,6 +173,7 @@ var init_src = __esm(() => {
|
|
|
157
173
|
prompt: bounded(131072),
|
|
158
174
|
context: Schema.optionalKey(optionalText(131072)),
|
|
159
175
|
provider: Provider,
|
|
176
|
+
botId: Schema.optionalKey(BotId),
|
|
160
177
|
status: TaskStatus,
|
|
161
178
|
sourceDeviceId: Schema.optionalKey(DeviceId),
|
|
162
179
|
targetDeviceId: DeviceId,
|
|
@@ -169,7 +186,7 @@ var init_src = __esm(() => {
|
|
|
169
186
|
updatedAt: Timestamp,
|
|
170
187
|
startedAt: Schema.optionalKey(Timestamp),
|
|
171
188
|
completedAt: Schema.optionalKey(Timestamp)
|
|
172
|
-
});
|
|
189
|
+
}).check(Schema.makeFilter(validBotTarget));
|
|
173
190
|
upgradeVersion = Schema.String.check(Schema.isPattern(/^(?:latest|v?\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?)$/, {
|
|
174
191
|
expected: "latest or an exact semantic version"
|
|
175
192
|
}));
|
|
@@ -208,12 +225,12 @@ var init_src = __esm(() => {
|
|
|
208
225
|
deviceName: bounded(128),
|
|
209
226
|
tasks: count,
|
|
210
227
|
byStatus: TaskStatusCounts,
|
|
211
|
-
byProvider: boundedArray(ProviderUsage,
|
|
228
|
+
byProvider: boundedArray(ProviderUsage, Provider.literals.length)
|
|
212
229
|
});
|
|
213
230
|
UsageSummary = Schema.Struct({
|
|
214
231
|
retainedTasks: count,
|
|
215
232
|
byStatus: TaskStatusCounts,
|
|
216
|
-
byProvider: boundedArray(ProviderUsage,
|
|
233
|
+
byProvider: boundedArray(ProviderUsage, Provider.literals.length),
|
|
217
234
|
devices: boundedArray(DeviceUsage, 256)
|
|
218
235
|
});
|
|
219
236
|
TaskTraceEvent = Schema.Struct({
|
|
@@ -226,6 +243,7 @@ var init_src = __esm(() => {
|
|
|
226
243
|
threadId: ThreadId,
|
|
227
244
|
status: TaskStatus,
|
|
228
245
|
provider: Provider,
|
|
246
|
+
botId: Schema.optionalKey(BotId),
|
|
229
247
|
sourceDeviceId: Schema.optionalKey(DeviceId),
|
|
230
248
|
targetDevice: Device,
|
|
231
249
|
parentTaskId: Schema.optionalKey(TaskId),
|
|
@@ -237,7 +255,7 @@ var init_src = __esm(() => {
|
|
|
237
255
|
events: boundedArray(TaskTraceEvent, 100),
|
|
238
256
|
truncated: Schema.Boolean,
|
|
239
257
|
error: Schema.optionalKey(optionalText(16384))
|
|
240
|
-
});
|
|
258
|
+
}).check(Schema.makeFilter(validBotTarget));
|
|
241
259
|
ThreadContext = Schema.Struct({
|
|
242
260
|
thread: Thread,
|
|
243
261
|
messages: boundedArray(Message, 20),
|
|
@@ -250,15 +268,20 @@ var init_src = __esm(() => {
|
|
|
250
268
|
prompt: bounded(131072),
|
|
251
269
|
context: Schema.optionalKey(optionalText(131072)),
|
|
252
270
|
provider: Schema.optionalKey(Provider),
|
|
271
|
+
botId: Schema.optionalKey(BotId),
|
|
253
272
|
targetDeviceId: Schema.optionalKey(DeviceId),
|
|
254
273
|
parentTaskId: Schema.optionalKey(TaskId),
|
|
255
274
|
workspace: Schema.optionalKey(bounded(4096))
|
|
256
|
-
});
|
|
275
|
+
}).check(Schema.makeFilter(validBotTarget));
|
|
257
276
|
SocketEvent = Schema.TaggedUnion({
|
|
258
277
|
Authenticate: { token: bounded(256) },
|
|
259
278
|
Connected: { serverVersion: bounded(32), connectedAt: Timestamp },
|
|
260
279
|
DeviceHello: { device: Device },
|
|
261
|
-
DeviceHeartbeat: {
|
|
280
|
+
DeviceHeartbeat: {
|
|
281
|
+
deviceId: DeviceId,
|
|
282
|
+
status: DeviceStatus,
|
|
283
|
+
bots: Schema.optionalKey(boundedArray(Bot, 256))
|
|
284
|
+
},
|
|
262
285
|
TaskAssigned: { task: Task },
|
|
263
286
|
TaskAccepted: { taskId: TaskId },
|
|
264
287
|
TaskFinished: {
|
|
@@ -287,25 +310,24 @@ var init_src = __esm(() => {
|
|
|
287
310
|
]);
|
|
288
311
|
decodeCreatePairingInput = Schema.decodeUnknownEffect(CreatePairingInput);
|
|
289
312
|
decodeExchangePairingInput = Schema.decodeUnknownEffect(ExchangePairingInput);
|
|
290
|
-
decodeCreateTaskInput = Schema.decodeUnknownEffect(CreateTaskInput);
|
|
291
313
|
decodeCreateUpgradeOperationsInput = Schema.decodeUnknownEffect(CreateUpgradeOperationsInput);
|
|
292
314
|
decodeDevice = Schema.decodeUnknownEffect(Device);
|
|
293
315
|
decodeSocketEvent = Schema.decodeUnknownEffect(SocketEvent);
|
|
294
316
|
});
|
|
295
317
|
|
|
296
318
|
// apps/relay/src/config.ts
|
|
297
|
-
import { Effect as
|
|
298
|
-
import { access as access5, chmod as
|
|
299
|
-
import { homedir as homedir5, platform as
|
|
300
|
-
import { join as
|
|
319
|
+
import { Effect as Effect12, Schema as Schema12 } from "effect";
|
|
320
|
+
import { access as access5, chmod as chmod7, mkdir as mkdir7, readFile as readFile5, writeFile as writeFile7 } from "node:fs/promises";
|
|
321
|
+
import { homedir as homedir5, platform as platform5 } from "node:os";
|
|
322
|
+
import { join as join8, resolve as resolve4 } from "node:path";
|
|
301
323
|
var RelayConfiguration, defaultDataDirectory = () => {
|
|
302
|
-
if (
|
|
303
|
-
return
|
|
324
|
+
if (platform5() === "win32") {
|
|
325
|
+
return join8(process.env.LOCALAPPDATA ?? join8(homedir5(), "AppData", "Local"), "Cohall");
|
|
304
326
|
}
|
|
305
|
-
if (
|
|
306
|
-
return
|
|
327
|
+
if (platform5() === "darwin") {
|
|
328
|
+
return join8(homedir5(), "Library", "Application Support", "Cohall", "relay");
|
|
307
329
|
}
|
|
308
|
-
return
|
|
330
|
+
return join8(process.env.XDG_DATA_HOME ?? join8(homedir5(), ".local", "share"), "cohall");
|
|
309
331
|
}, generatedToken = () => Array.from(crypto.getRandomValues(new Uint8Array(32)), (value) => value.toString(16).padStart(2, "0")).join(""), tokenFrom = async (path) => {
|
|
310
332
|
if (await access5(path).then(() => true).catch(() => false)) {
|
|
311
333
|
return (await readFile5(path, "utf8")).trim();
|
|
@@ -313,8 +335,8 @@ var RelayConfiguration, defaultDataDirectory = () => {
|
|
|
313
335
|
const token = generatedToken();
|
|
314
336
|
await writeFile7(path, `${token}
|
|
315
337
|
`, { mode: 384 });
|
|
316
|
-
if (
|
|
317
|
-
await
|
|
338
|
+
if (platform5() !== "win32") {
|
|
339
|
+
await chmod7(path, 384);
|
|
318
340
|
}
|
|
319
341
|
return token;
|
|
320
342
|
}, booleanEnvironment = (name) => {
|
|
@@ -328,16 +350,16 @@ var RelayConfiguration, defaultDataDirectory = () => {
|
|
|
328
350
|
throw new Error(`${name} must be true or false`);
|
|
329
351
|
}, isLoopback2 = (host) => host === "127.0.0.1" || host === "localhost" || host === "::1", loadEnvironmentConfiguration;
|
|
330
352
|
var init_config = __esm(() => {
|
|
331
|
-
RelayConfiguration =
|
|
332
|
-
host:
|
|
333
|
-
port:
|
|
334
|
-
token:
|
|
335
|
-
dataDirectory:
|
|
336
|
-
databasePath:
|
|
337
|
-
allowRemote:
|
|
338
|
-
historyTaskLimit:
|
|
353
|
+
RelayConfiguration = Schema12.Struct({
|
|
354
|
+
host: Schema12.NonEmptyString,
|
|
355
|
+
port: Schema12.Int.check(Schema12.isBetween({ minimum: 1, maximum: 65535 })),
|
|
356
|
+
token: Schema12.String.check(Schema12.isMinLength(32)),
|
|
357
|
+
dataDirectory: Schema12.NonEmptyString,
|
|
358
|
+
databasePath: Schema12.NonEmptyString,
|
|
359
|
+
allowRemote: Schema12.Boolean,
|
|
360
|
+
historyTaskLimit: Schema12.Int.check(Schema12.isBetween({ minimum: 100, maximum: 1e5 }))
|
|
339
361
|
});
|
|
340
|
-
loadEnvironmentConfiguration =
|
|
362
|
+
loadEnvironmentConfiguration = Effect12.tryPromise({
|
|
341
363
|
try: async () => {
|
|
342
364
|
const host = process.env.COHALL_RELAY_HOST ?? "127.0.0.1";
|
|
343
365
|
const port = Number(process.env.COHALL_RELAY_PORT ?? "8787");
|
|
@@ -345,22 +367,22 @@ var init_config = __esm(() => {
|
|
|
345
367
|
if (!isLoopback2(host) && !allowRemote) {
|
|
346
368
|
throw new Error("Remote relay binding is disabled. Use a private network or TLS reverse proxy, then set COHALL_RELAY_ALLOW_REMOTE=true.");
|
|
347
369
|
}
|
|
348
|
-
const dataDirectory =
|
|
349
|
-
await
|
|
350
|
-
if (
|
|
351
|
-
await
|
|
370
|
+
const dataDirectory = resolve4(process.env.COHALL_DATA_DIR ?? defaultDataDirectory());
|
|
371
|
+
await mkdir7(dataDirectory, { recursive: true, mode: 448 });
|
|
372
|
+
if (platform5() !== "win32") {
|
|
373
|
+
await chmod7(dataDirectory, 448);
|
|
352
374
|
}
|
|
353
|
-
const token = process.env.COHALL_TOKEN ?? await tokenFrom(
|
|
375
|
+
const token = process.env.COHALL_TOKEN ?? await tokenFrom(join8(dataDirectory, "owner-token"));
|
|
354
376
|
if (token.length < 32) {
|
|
355
377
|
throw new Error("The Cohall relay owner token must be at least 32 characters");
|
|
356
378
|
}
|
|
357
379
|
const historyTaskLimit = Number(process.env.COHALL_HISTORY_TASK_LIMIT ?? "1000");
|
|
358
|
-
return
|
|
380
|
+
return Schema12.decodeUnknownSync(RelayConfiguration)({
|
|
359
381
|
host,
|
|
360
382
|
port,
|
|
361
383
|
token,
|
|
362
384
|
dataDirectory,
|
|
363
|
-
databasePath:
|
|
385
|
+
databasePath: join8(dataDirectory, "cohall.db"),
|
|
364
386
|
allowRemote,
|
|
365
387
|
historyTaskLimit
|
|
366
388
|
});
|
|
@@ -540,18 +562,18 @@ __export(exports_store, {
|
|
|
540
562
|
RelayStore: () => exports_store,
|
|
541
563
|
PersistenceError: () => PersistenceError
|
|
542
564
|
});
|
|
543
|
-
import { Context as Context2, Effect as
|
|
565
|
+
import { Context as Context2, Effect as Effect13, Layer as Layer2, Schema as Schema13 } from "effect";
|
|
544
566
|
import { createHash as createHash2, randomBytes } from "node:crypto";
|
|
545
567
|
var PersistenceError, Service2, operationError = (operation) => (cause) => new PersistenceError({
|
|
546
568
|
operation,
|
|
547
569
|
message: cause instanceof Error ? cause.message : String(cause)
|
|
548
|
-
}),
|
|
570
|
+
}), decode2 = (operation, schema, value) => Schema13.decodeUnknownEffect(schema)(value).pipe(Effect13.mapError(operationError(operation))), threadFromRow = (row) => decode2("RelayStore.decodeThread", Thread, {
|
|
549
571
|
id: row.id,
|
|
550
572
|
title: row.title,
|
|
551
573
|
createdAt: row.created_at,
|
|
552
574
|
updatedAt: row.updated_at,
|
|
553
575
|
...row.default_device_id === null ? {} : { defaultDeviceId: row.default_device_id }
|
|
554
|
-
}), messageFromRow = (row) =>
|
|
576
|
+
}), messageFromRow = (row) => decode2("RelayStore.decodeMessage", Message, {
|
|
555
577
|
id: row.id,
|
|
556
578
|
threadId: row.thread_id,
|
|
557
579
|
role: row.role === "tool" ? "agent" : row.role,
|
|
@@ -560,7 +582,7 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
|
|
|
560
582
|
createdAt: row.created_at,
|
|
561
583
|
...row.task_id === null ? {} : { taskId: row.task_id },
|
|
562
584
|
...row.device_id === null ? {} : { deviceId: row.device_id }
|
|
563
|
-
}), deviceFromRow = (row) =>
|
|
585
|
+
}), deviceFromRow = (row) => decode2("RelayStore.decodeDevice", Device, {
|
|
564
586
|
id: row.id,
|
|
565
587
|
name: row.name,
|
|
566
588
|
hostname: row.hostname,
|
|
@@ -568,16 +590,18 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
|
|
|
568
590
|
architecture: row.architecture,
|
|
569
591
|
status: row.status,
|
|
570
592
|
providers: JSON.parse(row.providers_json),
|
|
593
|
+
...row.bots_json === null ? {} : { bots: JSON.parse(row.bots_json) },
|
|
571
594
|
capabilities: JSON.parse(row.capabilities_json),
|
|
572
595
|
workspaces: JSON.parse(row.workspaces_json),
|
|
573
596
|
version: row.version,
|
|
574
597
|
lastSeenAt: row.last_seen_at,
|
|
575
598
|
...row.connected_at === null ? {} : { connectedAt: row.connected_at }
|
|
576
|
-
}), taskFromRow = (row) =>
|
|
599
|
+
}), taskFromRow = (row) => decode2("RelayStore.decodeTask", Task, {
|
|
577
600
|
id: row.id,
|
|
578
601
|
threadId: row.thread_id,
|
|
579
602
|
prompt: row.prompt,
|
|
580
603
|
provider: row.provider,
|
|
604
|
+
...row.bot_id === null ? {} : { botId: row.bot_id },
|
|
581
605
|
status: row.status,
|
|
582
606
|
targetDeviceId: row.target_device_id,
|
|
583
607
|
createdAt: row.created_at,
|
|
@@ -591,7 +615,7 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
|
|
|
591
615
|
...row.error === null ? {} : { error: row.error },
|
|
592
616
|
...row.started_at === null ? {} : { startedAt: row.started_at },
|
|
593
617
|
...row.completed_at === null ? {} : { completedAt: row.completed_at }
|
|
594
|
-
}), operationFromRow = (row) =>
|
|
618
|
+
}), operationFromRow = (row) => decode2("RelayStore.decodeOperation", DeviceOperation, {
|
|
595
619
|
id: row.id,
|
|
596
620
|
kind: row.kind,
|
|
597
621
|
status: row.status,
|
|
@@ -611,17 +635,17 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
|
|
|
611
635
|
completed: 0,
|
|
612
636
|
failed: 0,
|
|
613
637
|
cancelled: 0
|
|
614
|
-
}), taskTraceEventFromRow = (row) =>
|
|
638
|
+
}), taskTraceEventFromRow = (row) => decode2("RelayStore.decodeTaskTraceEvent", TaskTraceEvent, {
|
|
615
639
|
kind: row.kind,
|
|
616
640
|
at: row.created_at,
|
|
617
641
|
...row.detail === null ? {} : { detail: row.detail }
|
|
618
|
-
}), rolesFromRow = (row) =>
|
|
642
|
+
}), rolesFromRow = (row) => Schema13.decodeUnknownSync(Schema13.Array(ConnectionRole))(JSON.parse(row.roles_json)), authSessionFromRow = (row) => {
|
|
619
643
|
const roles = rolesFromRow(row);
|
|
620
644
|
const role = roles[0];
|
|
621
|
-
return role === undefined ?
|
|
645
|
+
return role === undefined ? Effect13.fail(new PersistenceError({
|
|
622
646
|
operation: "RelayStore.decodeAuthSession",
|
|
623
647
|
message: `Session ${row.id} has no role`
|
|
624
|
-
})) :
|
|
648
|
+
})) : decode2("RelayStore.decodeAuthSession", AuthSession, {
|
|
625
649
|
id: row.id,
|
|
626
650
|
label: row.label,
|
|
627
651
|
role,
|
|
@@ -641,6 +665,8 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
|
|
|
641
665
|
return "Claude Code";
|
|
642
666
|
case "opencode":
|
|
643
667
|
return "OpenCode";
|
|
668
|
+
case "grok-bot":
|
|
669
|
+
return "Grok Bot";
|
|
644
670
|
}
|
|
645
671
|
}, recentWithin = (items, byteBudget) => {
|
|
646
672
|
const encoder = new TextEncoder;
|
|
@@ -713,22 +739,22 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
|
|
|
713
739
|
};
|
|
714
740
|
const queryTask = (taskId) => db.query("SELECT * FROM tasks WHERE id = ?").get(taskId);
|
|
715
741
|
const queryOperation = (operationId) => db.query("SELECT * FROM device_operations WHERE id = ?").get(operationId);
|
|
716
|
-
const getTask =
|
|
717
|
-
const row = yield*
|
|
742
|
+
const getTask = Effect13.fn("RelayStore.getTask")(function* (taskId) {
|
|
743
|
+
const row = yield* Effect13.try({
|
|
718
744
|
try: () => queryTask(taskId),
|
|
719
745
|
catch: operationError("RelayStore.getTask")
|
|
720
746
|
});
|
|
721
747
|
if (row === null) {
|
|
722
|
-
return yield*
|
|
748
|
+
return yield* Effect13.fail(new PersistenceError({
|
|
723
749
|
operation: "RelayStore.getTask",
|
|
724
750
|
message: `Unknown task ${taskId}`
|
|
725
751
|
}));
|
|
726
752
|
}
|
|
727
753
|
return yield* taskFromRow(row);
|
|
728
754
|
});
|
|
729
|
-
const traceTask =
|
|
755
|
+
const traceTask = Effect13.fn("RelayStore.traceTask")(function* (taskId) {
|
|
730
756
|
const task = yield* getTask(taskId);
|
|
731
|
-
const [deviceRow, eventRows] = yield*
|
|
757
|
+
const [deviceRow, eventRows] = yield* Effect13.try({
|
|
732
758
|
try: () => [
|
|
733
759
|
db.query("SELECT * FROM devices WHERE id = ?").get(task.targetDeviceId),
|
|
734
760
|
db.query(`SELECT * FROM (
|
|
@@ -739,20 +765,21 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
|
|
|
739
765
|
catch: operationError("RelayStore.traceTask")
|
|
740
766
|
});
|
|
741
767
|
if (deviceRow === null) {
|
|
742
|
-
return yield*
|
|
768
|
+
return yield* Effect13.fail(new PersistenceError({
|
|
743
769
|
operation: "RelayStore.traceTask.device",
|
|
744
770
|
message: `Unknown target device ${task.targetDeviceId}`
|
|
745
771
|
}));
|
|
746
772
|
}
|
|
747
773
|
const targetDevice = yield* deviceFromRow(deviceRow);
|
|
748
774
|
const selectedRows = eventRows.slice(-100);
|
|
749
|
-
const recordedEvents = yield*
|
|
775
|
+
const recordedEvents = yield* Effect13.forEach(selectedRows, taskTraceEventFromRow);
|
|
750
776
|
const events = recordedEvents.length === 0 ? derivedTraceEvents(task) : recordedEvents;
|
|
751
777
|
return TaskTrace.make({
|
|
752
778
|
taskId: task.id,
|
|
753
779
|
threadId: task.threadId,
|
|
754
780
|
status: task.status,
|
|
755
781
|
provider: task.provider,
|
|
782
|
+
...task.botId === undefined ? {} : { botId: task.botId },
|
|
756
783
|
targetDevice,
|
|
757
784
|
createdAt: task.createdAt,
|
|
758
785
|
updatedAt: task.updatedAt,
|
|
@@ -766,15 +793,15 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
|
|
|
766
793
|
...task.error === undefined ? {} : { error: task.error }
|
|
767
794
|
});
|
|
768
795
|
});
|
|
769
|
-
const listDevices =
|
|
770
|
-
const rows = yield*
|
|
796
|
+
const listDevices = Effect13.fn("RelayStore.listDevices")(function* () {
|
|
797
|
+
const rows = yield* Effect13.try({
|
|
771
798
|
try: () => db.query("SELECT * FROM devices WHERE forgotten_at IS NULL ORDER BY name COLLATE NOCASE, id").all(),
|
|
772
799
|
catch: operationError("RelayStore.listDevices")
|
|
773
800
|
});
|
|
774
|
-
return yield*
|
|
801
|
+
return yield* Effect13.forEach(rows, deviceFromRow);
|
|
775
802
|
});
|
|
776
|
-
const usage =
|
|
777
|
-
const rows = yield*
|
|
803
|
+
const usage = Effect13.fn("RelayStore.usage")(function* () {
|
|
804
|
+
const rows = yield* Effect13.try({
|
|
778
805
|
try: () => db.query(`SELECT tasks.target_device_id AS device_id, devices.name AS device_name,
|
|
779
806
|
tasks.status, tasks.provider, COUNT(*) AS tasks
|
|
780
807
|
FROM tasks
|
|
@@ -788,9 +815,9 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
|
|
|
788
815
|
const devices = new Map;
|
|
789
816
|
let retainedTasks = 0;
|
|
790
817
|
for (const row of rows) {
|
|
791
|
-
const status =
|
|
792
|
-
const provider =
|
|
793
|
-
const deviceId =
|
|
818
|
+
const status = Schema13.decodeUnknownSync(TaskStatus)(row.status);
|
|
819
|
+
const provider = Schema13.decodeUnknownSync(Provider)(row.provider);
|
|
820
|
+
const deviceId = Schema13.decodeUnknownSync(DeviceId)(row.device_id);
|
|
794
821
|
retainedTasks += row.tasks;
|
|
795
822
|
totalStatus[status] += row.tasks;
|
|
796
823
|
totalProviders.set(provider, (totalProviders.get(provider) ?? 0) + row.tasks);
|
|
@@ -820,29 +847,29 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
|
|
|
820
847
|
}))
|
|
821
848
|
});
|
|
822
849
|
});
|
|
823
|
-
const getOperation =
|
|
824
|
-
const row = yield*
|
|
850
|
+
const getOperation = Effect13.fn("RelayStore.getOperation")(function* (operationId) {
|
|
851
|
+
const row = yield* Effect13.try({
|
|
825
852
|
try: () => queryOperation(operationId),
|
|
826
853
|
catch: operationError("RelayStore.getOperation")
|
|
827
854
|
});
|
|
828
855
|
if (row === null) {
|
|
829
|
-
return yield*
|
|
856
|
+
return yield* Effect13.fail(new PersistenceError({
|
|
830
857
|
operation: "RelayStore.getOperation",
|
|
831
858
|
message: `Unknown device operation ${operationId}`
|
|
832
859
|
}));
|
|
833
860
|
}
|
|
834
861
|
return yield* operationFromRow(row);
|
|
835
862
|
});
|
|
836
|
-
const listOperations =
|
|
837
|
-
const rows = yield*
|
|
863
|
+
const listOperations = Effect13.fn("RelayStore.listOperations")(function* () {
|
|
864
|
+
const rows = yield* Effect13.try({
|
|
838
865
|
try: () => db.query("SELECT * FROM device_operations ORDER BY created_at DESC, id DESC LIMIT 50").all(),
|
|
839
866
|
catch: operationError("RelayStore.listOperations")
|
|
840
867
|
});
|
|
841
|
-
return yield*
|
|
868
|
+
return yield* Effect13.forEach(rows, operationFromRow);
|
|
842
869
|
});
|
|
843
|
-
const createUpgradeOperations =
|
|
870
|
+
const createUpgradeOperations = Effect13.fn("RelayStore.createUpgradeOperations")(function* (input) {
|
|
844
871
|
const timestamp = now();
|
|
845
|
-
const operations = yield*
|
|
872
|
+
const operations = yield* Effect13.try({
|
|
846
873
|
try: () => db.transaction(() => {
|
|
847
874
|
const devices = db.query("SELECT * FROM devices WHERE forgotten_at IS NULL ORDER BY name COLLATE NOCASE, id").all();
|
|
848
875
|
if (devices.length === 0) {
|
|
@@ -882,15 +909,15 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
|
|
|
882
909
|
});
|
|
883
910
|
return operations;
|
|
884
911
|
});
|
|
885
|
-
const pendingOperationsFor =
|
|
886
|
-
const rows = yield*
|
|
912
|
+
const pendingOperationsFor = Effect13.fn("RelayStore.pendingOperationsFor")(function* (deviceId) {
|
|
913
|
+
const rows = yield* Effect13.try({
|
|
887
914
|
try: () => db.query(`SELECT * FROM device_operations WHERE target_device_id = ? AND status = 'queued'
|
|
888
915
|
ORDER BY created_at, id LIMIT 10`).all(deviceId),
|
|
889
916
|
catch: operationError("RelayStore.pendingOperationsFor")
|
|
890
917
|
});
|
|
891
|
-
return yield*
|
|
918
|
+
return yield* Effect13.forEach(rows, operationFromRow);
|
|
892
919
|
});
|
|
893
|
-
const transitionOperation =
|
|
920
|
+
const transitionOperation = Effect13.fn("RelayStore.transitionOperation")(function* (operationId, expected, status, result, error) {
|
|
894
921
|
const current = yield* getOperation(operationId);
|
|
895
922
|
if (!expected.includes(current.status)) {
|
|
896
923
|
return current;
|
|
@@ -898,7 +925,7 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
|
|
|
898
925
|
const timestamp = now();
|
|
899
926
|
const completed = status === "completed" || status === "failed" ? timestamp : undefined;
|
|
900
927
|
const placeholders = expected.map(() => "?").join(", ");
|
|
901
|
-
yield*
|
|
928
|
+
yield* Effect13.try({
|
|
902
929
|
try: () => db.transaction(() => {
|
|
903
930
|
const updated = db.query(`UPDATE device_operations
|
|
904
931
|
SET status = ?, result = ?, error = ?, updated_at = ?, completed_at = ?
|
|
@@ -911,23 +938,23 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
|
|
|
911
938
|
});
|
|
912
939
|
return yield* getOperation(operationId);
|
|
913
940
|
});
|
|
914
|
-
const requireOperationTarget =
|
|
941
|
+
const requireOperationTarget = Effect13.fn("RelayStore.requireOperationTarget")(function* (operationId, deviceId) {
|
|
915
942
|
const operation = yield* getOperation(operationId);
|
|
916
943
|
if (operation.targetDeviceId !== deviceId) {
|
|
917
|
-
return yield*
|
|
944
|
+
return yield* Effect13.fail(new PersistenceError({
|
|
918
945
|
operation: "RelayStore.requireOperationTarget",
|
|
919
946
|
message: `Device ${deviceId} cannot update operation ${operationId}`
|
|
920
947
|
}));
|
|
921
948
|
}
|
|
922
949
|
return operation;
|
|
923
950
|
});
|
|
924
|
-
const assignOperation =
|
|
951
|
+
const assignOperation = Effect13.fn("RelayStore.assignOperation")(function* (operationId) {
|
|
925
952
|
const current = yield* getOperation(operationId);
|
|
926
953
|
if (current.status !== "queued") {
|
|
927
954
|
return current;
|
|
928
955
|
}
|
|
929
956
|
const timestamp = now();
|
|
930
|
-
yield*
|
|
957
|
+
yield* Effect13.try({
|
|
931
958
|
try: () => db.query(`UPDATE device_operations SET status = 'assigned', updated_at = ?
|
|
932
959
|
WHERE id = ? AND status = 'queued'
|
|
933
960
|
AND NOT EXISTS (
|
|
@@ -943,19 +970,19 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
|
|
|
943
970
|
});
|
|
944
971
|
return yield* getOperation(operationId);
|
|
945
972
|
});
|
|
946
|
-
const acceptOperation = (operationId, deviceId) => requireOperationTarget(operationId, deviceId).pipe(
|
|
947
|
-
const finishOperation = (operationId, deviceId, result) => requireOperationTarget(operationId, deviceId).pipe(
|
|
948
|
-
const failOperation = (operationId, deviceId, error) => requireOperationTarget(operationId, deviceId).pipe(
|
|
973
|
+
const acceptOperation = (operationId, deviceId) => requireOperationTarget(operationId, deviceId).pipe(Effect13.flatMap(() => transitionOperation(operationId, ["assigned"], "running")));
|
|
974
|
+
const finishOperation = (operationId, deviceId, result) => requireOperationTarget(operationId, deviceId).pipe(Effect13.flatMap(() => transitionOperation(operationId, ["queued", "assigned", "running"], "completed", result)));
|
|
975
|
+
const failOperation = (operationId, deviceId, error) => requireOperationTarget(operationId, deviceId).pipe(Effect13.flatMap(() => transitionOperation(operationId, ["queued", "assigned", "running"], "failed", undefined, error)));
|
|
949
976
|
const abandonOperation = (operationId) => transitionOperation(operationId, ["queued", "assigned", "running"], "failed", undefined, "Abandoned by the relay owner");
|
|
950
|
-
const requeueOperationsFor =
|
|
951
|
-
yield*
|
|
977
|
+
const requeueOperationsFor = Effect13.fn("RelayStore.requeueOperationsFor")(function* (deviceId) {
|
|
978
|
+
yield* Effect13.try({
|
|
952
979
|
try: () => db.query(`UPDATE device_operations SET status = 'queued', updated_at = ?
|
|
953
980
|
WHERE target_device_id = ? AND status IN ('assigned', 'running')`).run(now(), deviceId),
|
|
954
981
|
catch: operationError("RelayStore.requeueOperationsFor")
|
|
955
982
|
});
|
|
956
983
|
});
|
|
957
|
-
const forgetDevice =
|
|
958
|
-
const row = yield*
|
|
984
|
+
const forgetDevice = Effect13.fn("RelayStore.forgetDevice")(function* (deviceId) {
|
|
985
|
+
const row = yield* Effect13.try({
|
|
959
986
|
try: () => db.transaction(() => {
|
|
960
987
|
const device = db.query("SELECT * FROM devices WHERE id = ? AND forgotten_at IS NULL").get(deviceId);
|
|
961
988
|
if (device === null) {
|
|
@@ -982,8 +1009,8 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
|
|
|
982
1009
|
});
|
|
983
1010
|
return yield* deviceFromRow(row);
|
|
984
1011
|
});
|
|
985
|
-
const recover =
|
|
986
|
-
yield*
|
|
1012
|
+
const recover = Effect13.fn("RelayStore.recover")(function* () {
|
|
1013
|
+
yield* Effect13.try({
|
|
987
1014
|
try: () => db.transaction(() => {
|
|
988
1015
|
const timestamp = now();
|
|
989
1016
|
const interrupted = db.query("SELECT id FROM tasks WHERE status IN ('assigned', 'running')").all();
|
|
@@ -1001,52 +1028,54 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
|
|
|
1001
1028
|
catch: operationError("RelayStore.recover")
|
|
1002
1029
|
});
|
|
1003
1030
|
});
|
|
1004
|
-
const upsertDevice =
|
|
1005
|
-
yield*
|
|
1031
|
+
const upsertDevice = Effect13.fn("RelayStore.upsertDevice")(function* (device) {
|
|
1032
|
+
yield* Effect13.try({
|
|
1006
1033
|
try: () => db.query(`INSERT INTO devices (
|
|
1007
1034
|
id, name, hostname, platform, architecture, status, providers_json,
|
|
1008
|
-
capabilities_json, workspaces_json, version, last_seen_at, connected_at
|
|
1009
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1035
|
+
capabilities_json, workspaces_json, version, last_seen_at, connected_at, bots_json
|
|
1036
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1010
1037
|
ON CONFLICT(id) DO UPDATE SET
|
|
1011
1038
|
name = excluded.name, hostname = excluded.hostname, platform = excluded.platform,
|
|
1012
1039
|
architecture = excluded.architecture, status = excluded.status,
|
|
1013
|
-
providers_json = excluded.providers_json,
|
|
1040
|
+
providers_json = excluded.providers_json, bots_json = excluded.bots_json,
|
|
1041
|
+
capabilities_json = excluded.capabilities_json,
|
|
1014
1042
|
workspaces_json = excluded.workspaces_json, version = excluded.version,
|
|
1015
1043
|
last_seen_at = excluded.last_seen_at, connected_at = excluded.connected_at,
|
|
1016
|
-
forgotten_at = NULL`).run(device.id, device.name, device.hostname, device.platform, device.architecture, device.status, JSON.stringify(device.providers), JSON.stringify(device.capabilities), JSON.stringify(device.workspaces), device.version, device.lastSeenAt, device.connectedAt ?? null),
|
|
1044
|
+
forgotten_at = NULL`).run(device.id, device.name, device.hostname, device.platform, device.architecture, device.status, JSON.stringify(device.providers), JSON.stringify(device.capabilities), JSON.stringify(device.workspaces), device.version, device.lastSeenAt, device.connectedAt ?? null, device.bots === undefined ? null : JSON.stringify(device.bots)),
|
|
1017
1045
|
catch: operationError("RelayStore.upsertDevice")
|
|
1018
1046
|
});
|
|
1019
1047
|
return device;
|
|
1020
1048
|
});
|
|
1021
|
-
const updateDeviceStatus =
|
|
1049
|
+
const updateDeviceStatus = Effect13.fn("RelayStore.updateDeviceStatus")(function* (deviceId, status, bots) {
|
|
1022
1050
|
const timestamp = now();
|
|
1023
|
-
const result = yield*
|
|
1024
|
-
try: () => db.query("UPDATE devices SET status = ?, last_seen_at =
|
|
1051
|
+
const result = yield* Effect13.try({
|
|
1052
|
+
try: () => db.query("UPDATE devices SET status = ?, last_seen_at = ?, bots_json = COALESCE(?, bots_json) WHERE id = ?").run(status, timestamp, bots === undefined ? null : JSON.stringify(bots), deviceId),
|
|
1025
1053
|
catch: operationError("RelayStore.updateDeviceStatus")
|
|
1026
1054
|
});
|
|
1027
1055
|
if (result.changes !== 1) {
|
|
1028
|
-
return yield*
|
|
1056
|
+
return yield* Effect13.fail(new PersistenceError({
|
|
1029
1057
|
operation: "RelayStore.updateDeviceStatus",
|
|
1030
1058
|
message: `Unknown device ${deviceId}`
|
|
1031
1059
|
}));
|
|
1032
1060
|
}
|
|
1033
1061
|
const row = db.query("SELECT * FROM devices WHERE id = ?").get(deviceId);
|
|
1034
1062
|
if (row === null) {
|
|
1035
|
-
return yield*
|
|
1063
|
+
return yield* Effect13.fail(new PersistenceError({
|
|
1036
1064
|
operation: "RelayStore.updateDeviceStatus.read",
|
|
1037
1065
|
message: `Device ${deviceId} disappeared after update`
|
|
1038
1066
|
}));
|
|
1039
1067
|
}
|
|
1040
1068
|
return yield* deviceFromRow(row);
|
|
1041
1069
|
});
|
|
1042
|
-
const createDelegation2 =
|
|
1070
|
+
const createDelegation2 = Effect13.fn("RelayStore.createDelegation")(function* (input, targetDeviceId, sourceDeviceId, providerSessionId) {
|
|
1043
1071
|
const timestamp = now();
|
|
1044
1072
|
const threadId = input.threadId ?? makeThreadId();
|
|
1045
1073
|
const task = Task.make({
|
|
1046
1074
|
id: makeTaskId(),
|
|
1047
1075
|
threadId,
|
|
1048
1076
|
prompt: input.prompt,
|
|
1049
|
-
provider: input.provider ?? "codex",
|
|
1077
|
+
provider: input.provider ?? (input.botId === undefined ? "codex" : "grok-bot"),
|
|
1078
|
+
...input.botId === undefined ? {} : { botId: input.botId },
|
|
1050
1079
|
status: "queued",
|
|
1051
1080
|
targetDeviceId,
|
|
1052
1081
|
createdAt: timestamp,
|
|
@@ -1073,7 +1102,7 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
|
|
|
1073
1102
|
taskId: task.id,
|
|
1074
1103
|
...sourceDeviceId === undefined ? {} : { deviceId: sourceDeviceId }
|
|
1075
1104
|
});
|
|
1076
|
-
yield*
|
|
1105
|
+
yield* Effect13.try({
|
|
1077
1106
|
try: () => db.transaction(() => {
|
|
1078
1107
|
const outstanding = db.query(`SELECT COUNT(*) AS count FROM tasks WHERE target_device_id = ?
|
|
1079
1108
|
AND status IN ('queued', 'assigned', 'running', 'cancelling')`).get(targetDeviceId);
|
|
@@ -1087,9 +1116,9 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
|
|
|
1087
1116
|
}
|
|
1088
1117
|
db.query(`INSERT INTO tasks (
|
|
1089
1118
|
id, thread_id, prompt, context, provider, status, source_device_id,
|
|
1090
|
-
target_device_id, parent_task_id, workspace, provider_session_id,
|
|
1119
|
+
target_device_id, parent_task_id, workspace, provider_session_id, bot_id,
|
|
1091
1120
|
created_at, updated_at
|
|
1092
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(task.id, task.threadId, task.prompt, task.context ?? null, task.provider, task.status, task.sourceDeviceId ?? null, task.targetDeviceId, task.parentTaskId ?? null, task.workspace ?? null, task.providerSessionId ?? null, task.createdAt, task.updatedAt);
|
|
1121
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(task.id, task.threadId, task.prompt, task.context ?? null, task.provider, task.status, task.sourceDeviceId ?? null, task.targetDeviceId, task.parentTaskId ?? null, task.workspace ?? null, task.providerSessionId ?? null, task.botId ?? null, task.createdAt, task.updatedAt);
|
|
1093
1122
|
recordTaskTraceEvent(task.id, "queued", "Relay accepted the task", timestamp);
|
|
1094
1123
|
db.query(`INSERT INTO messages (
|
|
1095
1124
|
id, thread_id, role, kind, author_id, author_name, content, created_at,
|
|
@@ -1101,18 +1130,18 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
|
|
|
1101
1130
|
});
|
|
1102
1131
|
return task;
|
|
1103
1132
|
});
|
|
1104
|
-
const threadContext2 =
|
|
1105
|
-
const threadRow = yield*
|
|
1133
|
+
const threadContext2 = Effect13.fn("RelayStore.threadContext")(function* (threadId) {
|
|
1134
|
+
const threadRow = yield* Effect13.try({
|
|
1106
1135
|
try: () => db.query("SELECT * FROM threads WHERE id = ?").get(threadId),
|
|
1107
1136
|
catch: operationError("RelayStore.threadContext.thread")
|
|
1108
1137
|
});
|
|
1109
1138
|
if (threadRow === null) {
|
|
1110
|
-
return yield*
|
|
1139
|
+
return yield* Effect13.fail(new PersistenceError({
|
|
1111
1140
|
operation: "RelayStore.threadContext",
|
|
1112
1141
|
message: `Unknown thread ${threadId}`
|
|
1113
1142
|
}));
|
|
1114
1143
|
}
|
|
1115
|
-
const [messageRows, taskRows] = yield*
|
|
1144
|
+
const [messageRows, taskRows] = yield* Effect13.try({
|
|
1116
1145
|
try: () => [
|
|
1117
1146
|
db.query(`SELECT * FROM (
|
|
1118
1147
|
SELECT * FROM messages WHERE thread_id = ?
|
|
@@ -1125,10 +1154,10 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
|
|
|
1125
1154
|
],
|
|
1126
1155
|
catch: operationError("RelayStore.threadContext.items")
|
|
1127
1156
|
});
|
|
1128
|
-
const [thread, messages, tasks] = yield*
|
|
1157
|
+
const [thread, messages, tasks] = yield* Effect13.all([
|
|
1129
1158
|
threadFromRow(threadRow),
|
|
1130
|
-
|
|
1131
|
-
|
|
1159
|
+
Effect13.forEach(messageRows.slice(-20), messageFromRow),
|
|
1160
|
+
Effect13.forEach(taskRows.slice(-20), taskFromRow)
|
|
1132
1161
|
]);
|
|
1133
1162
|
const recentMessages = recentWithin(messages, 256 * 1024);
|
|
1134
1163
|
const recentTasks = recentWithin(tasks, 768 * 1024);
|
|
@@ -1139,22 +1168,22 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
|
|
|
1139
1168
|
truncated: messageRows.length > 20 || taskRows.length > 20 || recentMessages.truncated || recentTasks.truncated
|
|
1140
1169
|
});
|
|
1141
1170
|
});
|
|
1142
|
-
const pendingTasksFor =
|
|
1143
|
-
const rows = yield*
|
|
1171
|
+
const pendingTasksFor = Effect13.fn("RelayStore.pendingTasksFor")(function* (deviceId) {
|
|
1172
|
+
const rows = yield* Effect13.try({
|
|
1144
1173
|
try: () => db.query(`SELECT * FROM tasks WHERE target_device_id = ?
|
|
1145
1174
|
AND status IN ('queued', 'cancelling') ORDER BY created_at, id LIMIT 100`).all(deviceId),
|
|
1146
1175
|
catch: operationError("RelayStore.pendingTasksFor")
|
|
1147
1176
|
});
|
|
1148
|
-
return yield*
|
|
1177
|
+
return yield* Effect13.forEach(rows, taskFromRow);
|
|
1149
1178
|
});
|
|
1150
|
-
const transition =
|
|
1179
|
+
const transition = Effect13.fn("RelayStore.transition")(function* (taskId, expected, update, event) {
|
|
1151
1180
|
const current = yield* getTask(taskId);
|
|
1152
1181
|
if (!expected.includes(current.status)) {
|
|
1153
1182
|
return current;
|
|
1154
1183
|
}
|
|
1155
1184
|
const placeholders = expected.map(() => "?").join(", ");
|
|
1156
1185
|
const timestamp = now();
|
|
1157
|
-
yield*
|
|
1186
|
+
yield* Effect13.try({
|
|
1158
1187
|
try: () => db.transaction(() => {
|
|
1159
1188
|
const result = db.query(`UPDATE tasks SET status = ?, provider_session_id = ?, result = ?, error = ?,
|
|
1160
1189
|
started_at = ?, completed_at = ?, updated_at = ?
|
|
@@ -1170,24 +1199,26 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
|
|
|
1170
1199
|
});
|
|
1171
1200
|
return yield* getTask(taskId);
|
|
1172
1201
|
});
|
|
1173
|
-
const assignTask =
|
|
1202
|
+
const assignTask = Effect13.fn("RelayStore.assignTask")(function* (taskId) {
|
|
1174
1203
|
const current = yield* getTask(taskId);
|
|
1175
1204
|
if (current.status !== "queued") {
|
|
1176
1205
|
return current;
|
|
1177
1206
|
}
|
|
1178
1207
|
const timestamp = now();
|
|
1179
|
-
yield*
|
|
1208
|
+
yield* Effect13.try({
|
|
1180
1209
|
try: () => db.transaction(() => {
|
|
1181
1210
|
const result = db.query(`UPDATE tasks SET status = 'assigned', updated_at = ?
|
|
1182
1211
|
WHERE id = ? AND status = 'queued' AND NOT EXISTS (
|
|
1183
1212
|
SELECT 1 FROM tasks active
|
|
1184
1213
|
WHERE active.target_device_id = ? AND active.id <> ?
|
|
1214
|
+
AND CASE WHEN active.provider = 'grok-bot'
|
|
1215
|
+
THEN 'grok-bot:' || active.bot_id ELSE 'cli' END = ?
|
|
1185
1216
|
AND active.status IN ('assigned', 'running', 'cancelling')
|
|
1186
1217
|
) AND NOT EXISTS (
|
|
1187
1218
|
SELECT 1 FROM device_operations active
|
|
1188
1219
|
WHERE active.target_device_id = ?
|
|
1189
1220
|
AND active.status IN ('assigned', 'running')
|
|
1190
|
-
)`).run(timestamp, taskId, current.targetDeviceId, taskId, current.targetDeviceId);
|
|
1221
|
+
)`).run(timestamp, taskId, current.targetDeviceId, taskId, taskSlot(current), current.targetDeviceId);
|
|
1191
1222
|
if (result.changes === 1) {
|
|
1192
1223
|
recordTaskTraceEvent(taskId, "assigned", "Relay dispatched the task over the device WebSocket", timestamp);
|
|
1193
1224
|
}
|
|
@@ -1196,32 +1227,32 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
|
|
|
1196
1227
|
});
|
|
1197
1228
|
return yield* getTask(taskId);
|
|
1198
1229
|
});
|
|
1199
|
-
const requireTarget =
|
|
1230
|
+
const requireTarget = Effect13.fn("RelayStore.requireTarget")(function* (taskId, deviceId) {
|
|
1200
1231
|
const task = yield* getTask(taskId);
|
|
1201
1232
|
if (task.targetDeviceId !== deviceId) {
|
|
1202
|
-
return yield*
|
|
1233
|
+
return yield* Effect13.fail(new PersistenceError({
|
|
1203
1234
|
operation: "RelayStore.requireTarget",
|
|
1204
1235
|
message: `Task ${taskId} belongs to another device`
|
|
1205
1236
|
}));
|
|
1206
1237
|
}
|
|
1207
1238
|
return task;
|
|
1208
1239
|
});
|
|
1209
|
-
const terminal =
|
|
1240
|
+
const terminal = Effect13.fn("RelayStore.terminal")(function* (taskId, deviceId, status, result, error, providerSessionId) {
|
|
1210
1241
|
const current = yield* requireTarget(taskId, deviceId);
|
|
1211
1242
|
if (["completed", "failed", "cancelled"].includes(current.status)) {
|
|
1212
1243
|
return current;
|
|
1213
1244
|
}
|
|
1214
1245
|
const timestamp = now();
|
|
1215
|
-
yield*
|
|
1246
|
+
yield* Effect13.try({
|
|
1216
1247
|
try: () => db.transaction(() => {
|
|
1217
1248
|
const updated = db.query(`UPDATE tasks SET status = ?, result = ?, error = ?, provider_session_id = ?,
|
|
1218
1249
|
completed_at = ?, updated_at = ? WHERE id = ?
|
|
1219
|
-
AND status NOT IN ('completed', 'failed', 'cancelled')`).run(status, result ?? null, error ?? null, providerSessionId ?? current.providerSessionId ?? null, timestamp, timestamp, taskId);
|
|
1250
|
+
AND status NOT IN ('completed', 'failed', 'cancelled')`).run(status, result ?? null, error ?? null, current.provider === "grok-bot" ? null : providerSessionId ?? current.providerSessionId ?? null, timestamp, timestamp, taskId);
|
|
1220
1251
|
if (updated.changes !== 1) {
|
|
1221
1252
|
return;
|
|
1222
1253
|
}
|
|
1223
1254
|
recordTaskTraceEvent(taskId, status, status === "completed" ? "Target device completed the task" : status === "failed" ? "Target device reported a task failure" : "Target device acknowledged cancellation", timestamp);
|
|
1224
|
-
if (providerSessionId !== undefined) {
|
|
1255
|
+
if (current.provider !== "grok-bot" && providerSessionId !== undefined) {
|
|
1225
1256
|
db.query(`INSERT INTO provider_sessions (thread_id, device_id, provider, session_id, updated_at)
|
|
1226
1257
|
VALUES (?, ?, ?, ?, ?)
|
|
1227
1258
|
ON CONFLICT(thread_id, device_id, provider) DO UPDATE SET
|
|
@@ -1242,11 +1273,17 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
|
|
|
1242
1273
|
});
|
|
1243
1274
|
return yield* getTask(taskId);
|
|
1244
1275
|
});
|
|
1245
|
-
const requestCancellation =
|
|
1276
|
+
const requestCancellation = Effect13.fn("RelayStore.requestCancellation")(function* (taskId) {
|
|
1246
1277
|
const current = yield* getTask(taskId);
|
|
1247
1278
|
if (["completed", "failed", "cancelled"].includes(current.status)) {
|
|
1248
1279
|
return current;
|
|
1249
1280
|
}
|
|
1281
|
+
if (current.provider === "grok-bot" && current.status !== "queued") {
|
|
1282
|
+
return yield* new PersistenceError({
|
|
1283
|
+
operation: "RelayStore.requestCancellation",
|
|
1284
|
+
message: "Stop this bot in Grok Bot; its gateway cannot cancel an individual Cohall request"
|
|
1285
|
+
});
|
|
1286
|
+
}
|
|
1250
1287
|
if (current.status === "cancelling") {
|
|
1251
1288
|
return current;
|
|
1252
1289
|
}
|
|
@@ -1255,9 +1292,9 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
|
|
|
1255
1292
|
status: "cancelling"
|
|
1256
1293
|
}, { kind: "cancelling", detail: "Client requested task cancellation" });
|
|
1257
1294
|
});
|
|
1258
|
-
const requeueTasksFor =
|
|
1295
|
+
const requeueTasksFor = Effect13.fn("RelayStore.requeueTasksFor")(function* (deviceId) {
|
|
1259
1296
|
const timestamp = now();
|
|
1260
|
-
yield*
|
|
1297
|
+
yield* Effect13.try({
|
|
1261
1298
|
try: () => db.transaction(() => {
|
|
1262
1299
|
const interrupted = db.query(`SELECT id FROM tasks WHERE target_device_id = ?
|
|
1263
1300
|
AND status IN ('assigned', 'running')`).all(deviceId);
|
|
@@ -1270,42 +1307,42 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
|
|
|
1270
1307
|
catch: operationError("RelayStore.requeueTasksFor")
|
|
1271
1308
|
});
|
|
1272
1309
|
});
|
|
1273
|
-
const sessionFor =
|
|
1274
|
-
const row = yield*
|
|
1310
|
+
const sessionFor = Effect13.fn("RelayStore.sessionFor")(function* (threadId, deviceId, provider) {
|
|
1311
|
+
const row = yield* Effect13.try({
|
|
1275
1312
|
try: () => db.query(`SELECT session_id FROM provider_sessions
|
|
1276
1313
|
WHERE thread_id = ? AND device_id = ? AND provider = ?`).get(threadId, deviceId, provider),
|
|
1277
1314
|
catch: operationError("RelayStore.sessionFor")
|
|
1278
1315
|
});
|
|
1279
1316
|
return row?.session_id;
|
|
1280
1317
|
});
|
|
1281
|
-
const createPairing =
|
|
1318
|
+
const createPairing = Effect13.fn("RelayStore.createPairing")(function* (input) {
|
|
1282
1319
|
const roles = [...new Set(input.roles)];
|
|
1283
1320
|
if (roles.length === 0) {
|
|
1284
|
-
return yield*
|
|
1321
|
+
return yield* Effect13.fail(new PersistenceError({
|
|
1285
1322
|
operation: "RelayStore.createPairing",
|
|
1286
1323
|
message: "A pairing must allow at least one role"
|
|
1287
1324
|
}));
|
|
1288
1325
|
}
|
|
1289
1326
|
const token = secret("pair");
|
|
1290
1327
|
const expiresAt = new Date(Date.now() + 10 * 60 * 1000).toISOString();
|
|
1291
|
-
yield*
|
|
1328
|
+
yield* Effect13.try({
|
|
1292
1329
|
try: () => db.query(`INSERT INTO auth_pairings (token_hash, label, roles_json, expires_at, created_at)
|
|
1293
1330
|
VALUES (?, ?, ?, ?, ?)`).run(tokenHash(token), input.label, JSON.stringify(roles), expiresAt, now()),
|
|
1294
1331
|
catch: operationError("RelayStore.createPairing")
|
|
1295
1332
|
});
|
|
1296
1333
|
return PairingCredential.make({ token, expiresAt: Timestamp.make(expiresAt) });
|
|
1297
1334
|
});
|
|
1298
|
-
const exchangePairing2 =
|
|
1335
|
+
const exchangePairing2 = Effect13.fn("RelayStore.exchangePairing")(function* (token) {
|
|
1299
1336
|
const timestamp = now();
|
|
1300
1337
|
const expiresAt = new Date(Date.now() + 90 * 24 * 60 * 60 * 1000).toISOString();
|
|
1301
|
-
const created = yield*
|
|
1338
|
+
const created = yield* Effect13.try({
|
|
1302
1339
|
try: () => db.transaction(() => {
|
|
1303
1340
|
const pairing = db.query(`SELECT * FROM auth_pairings
|
|
1304
1341
|
WHERE token_hash = ? AND used_at IS NULL AND expires_at > ?`).get(tokenHash(token), timestamp);
|
|
1305
1342
|
if (pairing === null) {
|
|
1306
1343
|
throw new Error("Pairing credential is invalid, expired, or already used");
|
|
1307
1344
|
}
|
|
1308
|
-
const roles =
|
|
1345
|
+
const roles = Schema13.decodeUnknownSync(Schema13.Array(ConnectionRole))(JSON.parse(pairing.roles_json));
|
|
1309
1346
|
const deviceId = roles.includes("device") ? makeDeviceId() : undefined;
|
|
1310
1347
|
const consumed = db.query("UPDATE auth_pairings SET used_at = ? WHERE token_hash = ? AND used_at IS NULL").run(timestamp, pairing.token_hash);
|
|
1311
1348
|
if (consumed.changes !== 1) {
|
|
@@ -1333,11 +1370,11 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
|
|
|
1333
1370
|
})(),
|
|
1334
1371
|
catch: operationError("RelayStore.exchangePairing")
|
|
1335
1372
|
});
|
|
1336
|
-
const credentials = yield*
|
|
1373
|
+
const credentials = yield* Effect13.forEach(created, ({ token: sessionToken, row }) => authSessionFromRow(row).pipe(Effect13.map((session) => ({ token: sessionToken, session }))));
|
|
1337
1374
|
return PairingResult.make({ credentials });
|
|
1338
1375
|
});
|
|
1339
|
-
const authenticateSession =
|
|
1340
|
-
const row = yield*
|
|
1376
|
+
const authenticateSession = Effect13.fn("RelayStore.authenticateSession")(function* (token, role) {
|
|
1377
|
+
const row = yield* Effect13.try({
|
|
1341
1378
|
try: () => db.query(`SELECT * FROM auth_sessions
|
|
1342
1379
|
WHERE token_hash = ? AND revoked_at IS NULL AND expires_at > ?`).get(tokenHash(token), now()),
|
|
1343
1380
|
catch: operationError("RelayStore.authenticateSession")
|
|
@@ -1346,35 +1383,35 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
|
|
|
1346
1383
|
return;
|
|
1347
1384
|
}
|
|
1348
1385
|
const lastSeenAt = now();
|
|
1349
|
-
yield*
|
|
1386
|
+
yield* Effect13.try({
|
|
1350
1387
|
try: () => db.query("UPDATE auth_sessions SET last_seen_at = ? WHERE id = ?").run(lastSeenAt, row.id),
|
|
1351
1388
|
catch: operationError("RelayStore.authenticateSession.touch")
|
|
1352
1389
|
});
|
|
1353
1390
|
const session = yield* authSessionFromRow(row);
|
|
1354
1391
|
return AuthSession.make({ ...session, lastSeenAt });
|
|
1355
1392
|
});
|
|
1356
|
-
const listAuthSessions =
|
|
1357
|
-
const rows = yield*
|
|
1393
|
+
const listAuthSessions = Effect13.fn("RelayStore.listAuthSessions")(function* () {
|
|
1394
|
+
const rows = yield* Effect13.try({
|
|
1358
1395
|
try: () => db.query("SELECT * FROM auth_sessions ORDER BY created_at DESC, id LIMIT 1000").all(),
|
|
1359
1396
|
catch: operationError("RelayStore.listAuthSessions")
|
|
1360
1397
|
});
|
|
1361
|
-
return yield*
|
|
1398
|
+
return yield* Effect13.forEach(rows, authSessionFromRow);
|
|
1362
1399
|
});
|
|
1363
|
-
const revokeAuthSession =
|
|
1400
|
+
const revokeAuthSession = Effect13.fn("RelayStore.revokeAuthSession")(function* (sessionId) {
|
|
1364
1401
|
const revokedAt = now();
|
|
1365
|
-
const result = yield*
|
|
1402
|
+
const result = yield* Effect13.try({
|
|
1366
1403
|
try: () => db.query("UPDATE auth_sessions SET revoked_at = ? WHERE id = ? AND revoked_at IS NULL").run(revokedAt, sessionId),
|
|
1367
1404
|
catch: operationError("RelayStore.revokeAuthSession")
|
|
1368
1405
|
});
|
|
1369
1406
|
if (result.changes !== 1) {
|
|
1370
|
-
return yield*
|
|
1407
|
+
return yield* Effect13.fail(new PersistenceError({
|
|
1371
1408
|
operation: "RelayStore.revokeAuthSession",
|
|
1372
1409
|
message: `Unknown or already revoked session ${sessionId}`
|
|
1373
1410
|
}));
|
|
1374
1411
|
}
|
|
1375
1412
|
const row = db.query("SELECT * FROM auth_sessions WHERE id = ?").get(sessionId);
|
|
1376
1413
|
if (row === null) {
|
|
1377
|
-
return yield*
|
|
1414
|
+
return yield* Effect13.fail(new PersistenceError({
|
|
1378
1415
|
operation: "RelayStore.revokeAuthSession.read",
|
|
1379
1416
|
message: `Session ${sessionId} disappeared after revocation`
|
|
1380
1417
|
}));
|
|
@@ -1387,7 +1424,7 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
|
|
|
1387
1424
|
usage,
|
|
1388
1425
|
forgetDevice,
|
|
1389
1426
|
upsertDevice,
|
|
1390
|
-
heartbeat: (deviceId, status) => updateDeviceStatus(deviceId, status),
|
|
1427
|
+
heartbeat: (deviceId, status, bots) => updateDeviceStatus(deviceId, status, bots),
|
|
1391
1428
|
markDeviceOffline: (deviceId) => updateDeviceStatus(deviceId, "offline"),
|
|
1392
1429
|
createDelegation: createDelegation2,
|
|
1393
1430
|
getTask,
|
|
@@ -1396,7 +1433,7 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
|
|
|
1396
1433
|
pendingTasksFor,
|
|
1397
1434
|
assignTask,
|
|
1398
1435
|
rollbackAssignment: (taskId) => transition(taskId, ["assigned"], { status: "queued" }, { kind: "requeued", detail: "Target device was unavailable during dispatch" }),
|
|
1399
|
-
acceptTask: (taskId, deviceId) => requireTarget(taskId, deviceId).pipe(
|
|
1436
|
+
acceptTask: (taskId, deviceId) => requireTarget(taskId, deviceId).pipe(Effect13.flatMap(() => transition(taskId, ["assigned"], { status: "running", startedAt: now() }, { kind: "running", detail: "Target device accepted the task and started the provider" }))),
|
|
1400
1437
|
finishTask: (taskId, deviceId, result, providerSessionId) => terminal(taskId, deviceId, "completed", result, undefined, providerSessionId),
|
|
1401
1438
|
failTask: (taskId, deviceId, error) => terminal(taskId, deviceId, "failed", undefined, error),
|
|
1402
1439
|
acknowledgeCancellation: (taskId, deviceId) => terminal(taskId, deviceId, "cancelled"),
|
|
@@ -1419,7 +1456,7 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
|
|
|
1419
1456
|
listAuthSessions,
|
|
1420
1457
|
revokeAuthSession
|
|
1421
1458
|
});
|
|
1422
|
-
}, migrate = (db) =>
|
|
1459
|
+
}, migrate = (db) => Effect13.try({
|
|
1423
1460
|
try: () => {
|
|
1424
1461
|
db.exec("PRAGMA journal_mode = WAL");
|
|
1425
1462
|
db.exec("PRAGMA foreign_keys = ON");
|
|
@@ -1428,7 +1465,7 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
|
|
|
1428
1465
|
CREATE TABLE IF NOT EXISTS devices (
|
|
1429
1466
|
id TEXT PRIMARY KEY, name TEXT NOT NULL, hostname TEXT NOT NULL,
|
|
1430
1467
|
platform TEXT NOT NULL, architecture TEXT NOT NULL, status TEXT NOT NULL,
|
|
1431
|
-
providers_json TEXT NOT NULL, capabilities_json TEXT NOT NULL,
|
|
1468
|
+
providers_json TEXT NOT NULL, bots_json TEXT, capabilities_json TEXT NOT NULL,
|
|
1432
1469
|
workspaces_json TEXT NOT NULL, version TEXT NOT NULL, last_seen_at TEXT NOT NULL,
|
|
1433
1470
|
connected_at TEXT, forgotten_at TEXT
|
|
1434
1471
|
);
|
|
@@ -1446,7 +1483,7 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
|
|
|
1446
1483
|
CREATE INDEX IF NOT EXISTS messages_thread_created ON messages(thread_id, created_at);
|
|
1447
1484
|
CREATE TABLE IF NOT EXISTS tasks (
|
|
1448
1485
|
id TEXT PRIMARY KEY, thread_id TEXT NOT NULL REFERENCES threads(id) ON DELETE CASCADE,
|
|
1449
|
-
prompt TEXT NOT NULL, context TEXT, provider TEXT NOT NULL, status TEXT NOT NULL,
|
|
1486
|
+
prompt TEXT NOT NULL, context TEXT, provider TEXT NOT NULL, bot_id TEXT, status TEXT NOT NULL,
|
|
1450
1487
|
source_device_id TEXT REFERENCES devices(id),
|
|
1451
1488
|
target_device_id TEXT NOT NULL REFERENCES devices(id), parent_task_id TEXT,
|
|
1452
1489
|
workspace TEXT, provider_session_id TEXT, result TEXT, error TEXT,
|
|
@@ -1491,17 +1528,24 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
|
|
|
1491
1528
|
if (!deviceColumns.some((column) => column.name === "forgotten_at")) {
|
|
1492
1529
|
db.exec("ALTER TABLE devices ADD COLUMN forgotten_at TEXT");
|
|
1493
1530
|
}
|
|
1531
|
+
if (!deviceColumns.some((column) => column.name === "bots_json")) {
|
|
1532
|
+
db.exec("ALTER TABLE devices ADD COLUMN bots_json TEXT");
|
|
1533
|
+
}
|
|
1534
|
+
const taskColumns = db.query("PRAGMA table_info(tasks)").all();
|
|
1535
|
+
if (!taskColumns.some((column) => column.name === "bot_id")) {
|
|
1536
|
+
db.exec("ALTER TABLE tasks ADD COLUMN bot_id TEXT");
|
|
1537
|
+
}
|
|
1494
1538
|
},
|
|
1495
1539
|
catch: operationError("RelayStore.migrate")
|
|
1496
|
-
}), layer2 = (path, retainedTerminalTasks = 1000) => Layer2.effect(Service2,
|
|
1540
|
+
}), layer2 = (path, retainedTerminalTasks = 1000) => Layer2.effect(Service2, Effect13.acquireRelease(Effect13.try({
|
|
1497
1541
|
try: () => new Database(path),
|
|
1498
1542
|
catch: operationError("RelayStore.open")
|
|
1499
|
-
}).pipe(
|
|
1543
|
+
}).pipe(Effect13.tap(migrate)), (db) => Effect13.sync(() => db.close())).pipe(Effect13.map((db) => makeService(db, retainedTerminalTasks)))), layerFromDatabase = (db) => Layer2.effect(Service2, migrate(db).pipe(Effect13.map(() => makeService(db))));
|
|
1500
1544
|
var init_store = __esm(() => {
|
|
1501
1545
|
init_src();
|
|
1502
1546
|
init_database();
|
|
1503
1547
|
init_store();
|
|
1504
|
-
PersistenceError = class PersistenceError extends
|
|
1548
|
+
PersistenceError = class PersistenceError extends Schema13.TaggedErrorClass()("RelayStore.PersistenceError", { operation: Schema13.String, message: Schema13.String }) {
|
|
1505
1549
|
};
|
|
1506
1550
|
Service2 = class Service2 extends Context2.Service()("@cohall/RelayStore") {
|
|
1507
1551
|
};
|
|
@@ -1511,13 +1555,14 @@ var init_store = __esm(() => {
|
|
|
1511
1555
|
var exports_main = {};
|
|
1512
1556
|
__export(exports_main, {
|
|
1513
1557
|
runRelay: () => runRelay,
|
|
1514
|
-
relayListenOptions: () => relayListenOptions
|
|
1558
|
+
relayListenOptions: () => relayListenOptions,
|
|
1559
|
+
chooseDevice: () => chooseDevice
|
|
1515
1560
|
});
|
|
1516
|
-
import { Effect as
|
|
1561
|
+
import { Effect as Effect14, ManagedRuntime, Schema as Schema14 } from "effect";
|
|
1517
1562
|
import { createHash as createHash3, timingSafeEqual } from "node:crypto";
|
|
1518
|
-
import { access as access6, chmod as
|
|
1563
|
+
import { access as access6, chmod as chmod8 } from "node:fs/promises";
|
|
1519
1564
|
import { createServer } from "node:http";
|
|
1520
|
-
import { platform as
|
|
1565
|
+
import { platform as platform6 } from "node:os";
|
|
1521
1566
|
import { WebSocketServer } from "ws";
|
|
1522
1567
|
var RequestError, relayListenOptions = (configuration, environment = process.env, pid = process.pid) => {
|
|
1523
1568
|
const listenPid = Number(environment.LISTEN_PID);
|
|
@@ -1544,7 +1589,7 @@ var RequestError, relayListenOptions = (configuration, environment = process.env
|
|
|
1544
1589
|
if (request.method === "GET" || request.method === "HEAD") {
|
|
1545
1590
|
return Promise.resolve(undefined);
|
|
1546
1591
|
}
|
|
1547
|
-
return new Promise((
|
|
1592
|
+
return new Promise((resolve5, reject) => {
|
|
1548
1593
|
const chunks = [];
|
|
1549
1594
|
let size = 0;
|
|
1550
1595
|
const cleanup = () => {
|
|
@@ -1563,7 +1608,7 @@ var RequestError, relayListenOptions = (configuration, environment = process.env
|
|
|
1563
1608
|
};
|
|
1564
1609
|
const onEnd = () => {
|
|
1565
1610
|
cleanup();
|
|
1566
|
-
|
|
1611
|
+
resolve5(Buffer.concat(chunks, size));
|
|
1567
1612
|
};
|
|
1568
1613
|
const onError = (cause) => {
|
|
1569
1614
|
cleanup();
|
|
@@ -1612,18 +1657,18 @@ Content-Length: 0\r
|
|
|
1612
1657
|
if ("destroy" in socket && typeof socket.destroy === "function") {
|
|
1613
1658
|
socket.destroy();
|
|
1614
1659
|
}
|
|
1615
|
-
}, body = (request,
|
|
1660
|
+
}, body = (request, decode3) => Effect14.tryPromise({
|
|
1616
1661
|
try: () => request.json(),
|
|
1617
1662
|
catch: () => new RequestError({ status: 400, message: "Expected a JSON request body" })
|
|
1618
|
-
}).pipe(
|
|
1663
|
+
}).pipe(Effect14.flatMap(decode3), Effect14.mapError((cause) => cause instanceof RequestError ? cause : new RequestError({ status: 400, message: String(cause).slice(0, 2048) }))), pathId = (schema, value) => Schema14.decodeUnknownEffect(schema)(value).pipe(Effect14.mapError(() => new RequestError({ status: 400, message: `Invalid identifier: ${value}` }))), scoreDevice = (device, input, sourceDeviceId) => {
|
|
1619
1664
|
const prompt = `${input.prompt} ${input.context ?? ""}`.toLowerCase();
|
|
1620
1665
|
const needsXcode = /\b(xcode|ios|macos|swiftui|app store)\b/.test(prompt);
|
|
1621
1666
|
const needsBrowser = /\b(browser|chrome|youtube|twitter|logged[- ]?in|signed[- ]?in|x\.com)\b/.test(prompt);
|
|
1622
1667
|
return (device.status === "offline" ? 0 : 100) + (input.workspace !== undefined && device.workspaces.some((workspace) => input.workspace === workspace.path || input.workspace?.startsWith(`${workspace.path}/`)) ? 80 : 0) + (needsXcode && device.capabilities.some((capability) => capability.id === "xcode") ? 60 : 0) + (needsBrowser && device.capabilities.some((capability) => capability.id === "browser-session") ? 40 : 0) + (sourceDeviceId === device.id ? -5 : 0);
|
|
1623
|
-
}, chooseDevice = (input, sourceDeviceId) =>
|
|
1668
|
+
}, chooseDevice = (input, sourceDeviceId) => Effect14.gen(function* () {
|
|
1624
1669
|
const store = yield* exports_store.Service;
|
|
1625
|
-
const devices = yield* store.listDevices().pipe(
|
|
1626
|
-
const provider = input.provider ?? "codex";
|
|
1670
|
+
const devices = yield* store.listDevices().pipe(Effect14.mapError((cause) => new RequestError({ status: 500, message: cause.message })));
|
|
1671
|
+
const provider = input.provider ?? (input.botId === undefined ? "codex" : "grok-bot");
|
|
1627
1672
|
if (input.targetDeviceId !== undefined) {
|
|
1628
1673
|
const target = devices.find((device) => device.id === input.targetDeviceId);
|
|
1629
1674
|
if (target === undefined) {
|
|
@@ -1638,27 +1683,40 @@ Content-Length: 0\r
|
|
|
1638
1683
|
message: `${target.name} does not advertise the ${provider} provider`
|
|
1639
1684
|
});
|
|
1640
1685
|
}
|
|
1686
|
+
if (input.botId !== undefined && !target.bots?.some((bot) => bot.id === input.botId)) {
|
|
1687
|
+
return yield* new RequestError({
|
|
1688
|
+
status: 404,
|
|
1689
|
+
message: `${target.name} does not advertise bot ${input.botId}; refresh the bot list`
|
|
1690
|
+
});
|
|
1691
|
+
}
|
|
1641
1692
|
return target.id;
|
|
1642
1693
|
}
|
|
1643
|
-
const
|
|
1694
|
+
const candidates = devices.filter((device) => device.providers.includes(provider) && (input.botId === undefined || device.bots?.some((bot) => bot.id === input.botId)));
|
|
1695
|
+
if (input.botId !== undefined && candidates.length > 1) {
|
|
1696
|
+
return yield* new RequestError({
|
|
1697
|
+
status: 409,
|
|
1698
|
+
message: `Bot ${input.botId} is advertised by more than one device; select a device`
|
|
1699
|
+
});
|
|
1700
|
+
}
|
|
1701
|
+
const selected = candidates.sort((left, right) => scoreDevice(right, input, sourceDeviceId) - scoreDevice(left, input, sourceDeviceId))[0];
|
|
1644
1702
|
if (selected === undefined) {
|
|
1645
1703
|
return yield* new RequestError({
|
|
1646
1704
|
status: 409,
|
|
1647
|
-
message: `No Cohall device advertises the ${provider} provider`
|
|
1705
|
+
message: input.botId === undefined ? `No Cohall device advertises the ${provider} provider` : `No Cohall device advertises bot ${input.botId}; refresh the bot list`
|
|
1648
1706
|
});
|
|
1649
1707
|
}
|
|
1650
1708
|
return selected.id;
|
|
1651
1709
|
}), runRelay = async () => {
|
|
1652
|
-
const configuration = await
|
|
1710
|
+
const configuration = await Effect14.runPromise(loadEnvironmentConfiguration);
|
|
1653
1711
|
process.umask(63);
|
|
1654
1712
|
const runtime = ManagedRuntime.make(exports_store.layer(configuration.databasePath, configuration.historyTaskLimit));
|
|
1655
1713
|
const run2 = (effect) => runtime.runPromise(effect);
|
|
1656
|
-
await run2(
|
|
1714
|
+
await run2(Effect14.gen(function* () {
|
|
1657
1715
|
const store = yield* exports_store.Service;
|
|
1658
1716
|
yield* store.recover();
|
|
1659
1717
|
}));
|
|
1660
|
-
if (
|
|
1661
|
-
await
|
|
1718
|
+
if (platform6() !== "win32" && await access6(configuration.databasePath).then(() => true).catch(() => false)) {
|
|
1719
|
+
await chmod8(configuration.databasePath, 384);
|
|
1662
1720
|
}
|
|
1663
1721
|
const hub = new Hub;
|
|
1664
1722
|
let connections = 0;
|
|
@@ -1668,7 +1726,7 @@ Content-Length: 0\r
|
|
|
1668
1726
|
if (ownerToken2(token)) {
|
|
1669
1727
|
return "owner";
|
|
1670
1728
|
}
|
|
1671
|
-
return run2(
|
|
1729
|
+
return run2(Effect14.gen(function* () {
|
|
1672
1730
|
const store = yield* exports_store.Service;
|
|
1673
1731
|
return yield* store.authenticateSession(token, role);
|
|
1674
1732
|
})).catch(() => {
|
|
@@ -1683,7 +1741,7 @@ Content-Length: 0\r
|
|
|
1683
1741
|
return authenticate(authorization.slice("Bearer ".length), "client");
|
|
1684
1742
|
};
|
|
1685
1743
|
const dispatch = async (task) => {
|
|
1686
|
-
const assigned = await run2(
|
|
1744
|
+
const assigned = await run2(Effect14.gen(function* () {
|
|
1687
1745
|
const store = yield* exports_store.Service;
|
|
1688
1746
|
return yield* store.assignTask(task.id);
|
|
1689
1747
|
}));
|
|
@@ -1693,13 +1751,13 @@ Content-Length: 0\r
|
|
|
1693
1751
|
if (hub.sendToDevice(assigned.targetDeviceId, SocketEvent.make({ _tag: "TaskAssigned", task: assigned }))) {
|
|
1694
1752
|
return assigned;
|
|
1695
1753
|
}
|
|
1696
|
-
return run2(
|
|
1754
|
+
return run2(Effect14.gen(function* () {
|
|
1697
1755
|
const store = yield* exports_store.Service;
|
|
1698
1756
|
return yield* store.rollbackAssignment(task.id);
|
|
1699
1757
|
}));
|
|
1700
1758
|
};
|
|
1701
1759
|
const dispatchOperation = async (operation) => {
|
|
1702
|
-
const assigned = await run2(
|
|
1760
|
+
const assigned = await run2(Effect14.gen(function* () {
|
|
1703
1761
|
const store = yield* exports_store.Service;
|
|
1704
1762
|
return yield* store.assignOperation(operation.id);
|
|
1705
1763
|
}));
|
|
@@ -1709,13 +1767,22 @@ Content-Length: 0\r
|
|
|
1709
1767
|
if (hub.sendToDevice(assigned.targetDeviceId, SocketEvent.make({ _tag: "OperationAssigned", operation: assigned }))) {
|
|
1710
1768
|
return assigned;
|
|
1711
1769
|
}
|
|
1712
|
-
return run2(
|
|
1770
|
+
return run2(Effect14.gen(function* () {
|
|
1713
1771
|
const store = yield* exports_store.Service;
|
|
1714
1772
|
return yield* store.rollbackOperation(operation.id);
|
|
1715
1773
|
}));
|
|
1716
1774
|
};
|
|
1717
|
-
const
|
|
1718
|
-
const
|
|
1775
|
+
const dispatchPendingOperations = async (deviceId) => {
|
|
1776
|
+
const operations = await run2(Effect14.gen(function* () {
|
|
1777
|
+
const store = yield* exports_store.Service;
|
|
1778
|
+
return yield* store.pendingOperationsFor(deviceId);
|
|
1779
|
+
}));
|
|
1780
|
+
for (const operation of operations) {
|
|
1781
|
+
await dispatchOperation(operation);
|
|
1782
|
+
}
|
|
1783
|
+
};
|
|
1784
|
+
const dispatchPendingTasks = async (deviceId) => {
|
|
1785
|
+
const tasks = await run2(Effect14.gen(function* () {
|
|
1719
1786
|
const store = yield* exports_store.Service;
|
|
1720
1787
|
return yield* store.pendingTasksFor(deviceId);
|
|
1721
1788
|
}));
|
|
@@ -1726,13 +1793,10 @@ Content-Length: 0\r
|
|
|
1726
1793
|
await dispatch(task);
|
|
1727
1794
|
}
|
|
1728
1795
|
}
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
for (const operation of operations) {
|
|
1734
|
-
await dispatchOperation(operation);
|
|
1735
|
-
}
|
|
1796
|
+
};
|
|
1797
|
+
const dispatchPending = async (deviceId) => {
|
|
1798
|
+
await dispatchPendingOperations(deviceId);
|
|
1799
|
+
await dispatchPendingTasks(deviceId);
|
|
1736
1800
|
};
|
|
1737
1801
|
const sendError = (socket, code, message) => {
|
|
1738
1802
|
socket.send(JSON.stringify(SocketEvent.make({
|
|
@@ -1742,10 +1806,10 @@ Content-Length: 0\r
|
|
|
1742
1806
|
})));
|
|
1743
1807
|
};
|
|
1744
1808
|
const handleSocketEvent = async (socket, input) => {
|
|
1745
|
-
const event = await
|
|
1809
|
+
const event = await Effect14.runPromise(Effect14.try({
|
|
1746
1810
|
try: () => JSON.parse(socketText(input)),
|
|
1747
1811
|
catch: () => new RequestError({ status: 400, message: "Invalid socket JSON" })
|
|
1748
|
-
}).pipe(
|
|
1812
|
+
}).pipe(Effect14.flatMap(decodeSocketEvent))).catch((cause) => {
|
|
1749
1813
|
sendError(socket, "invalid_event", cause instanceof Error ? cause.message : String(cause));
|
|
1750
1814
|
return;
|
|
1751
1815
|
});
|
|
@@ -1802,11 +1866,12 @@ Content-Length: 0\r
|
|
|
1802
1866
|
socket.close(4009, "Device is already connected");
|
|
1803
1867
|
return;
|
|
1804
1868
|
}
|
|
1805
|
-
await run2(
|
|
1869
|
+
await run2(Effect14.gen(function* () {
|
|
1806
1870
|
const store = yield* exports_store.Service;
|
|
1807
1871
|
yield* store.upsertDevice(device);
|
|
1808
1872
|
}));
|
|
1809
|
-
await
|
|
1873
|
+
await dispatchPendingTasks(device.id);
|
|
1874
|
+
await dispatchPendingOperations(device.id);
|
|
1810
1875
|
return;
|
|
1811
1876
|
}
|
|
1812
1877
|
const deviceId = hub.deviceId(socket);
|
|
@@ -1820,12 +1885,12 @@ Content-Length: 0\r
|
|
|
1820
1885
|
const settleOperation = (operationId) => {
|
|
1821
1886
|
socket.send(JSON.stringify(SocketEvent.make({ _tag: "OperationSettled", operationId })));
|
|
1822
1887
|
};
|
|
1823
|
-
const processed = await run2(
|
|
1888
|
+
const processed = await run2(Effect14.gen(function* () {
|
|
1824
1889
|
const store = yield* exports_store.Service;
|
|
1825
1890
|
switch (event._tag) {
|
|
1826
1891
|
case "DeviceHeartbeat":
|
|
1827
1892
|
if (event.deviceId === deviceId) {
|
|
1828
|
-
yield* store.heartbeat(deviceId, event.status === "busy" ? "busy" : "online");
|
|
1893
|
+
yield* store.heartbeat(deviceId, event.status === "busy" ? "busy" : "online", event.bots);
|
|
1829
1894
|
}
|
|
1830
1895
|
return;
|
|
1831
1896
|
case "TaskAccepted":
|
|
@@ -1876,7 +1941,7 @@ Content-Length: 0\r
|
|
|
1876
1941
|
return json({ ok: true, version });
|
|
1877
1942
|
}
|
|
1878
1943
|
if (url.pathname === "/api/auth/pair" && request.method === "POST") {
|
|
1879
|
-
return run2(
|
|
1944
|
+
return run2(Effect14.gen(function* () {
|
|
1880
1945
|
const store = yield* exports_store.Service;
|
|
1881
1946
|
const input = yield* body(request, decodeExchangePairingInput);
|
|
1882
1947
|
return yield* store.exchangePairing(input.token);
|
|
@@ -1891,7 +1956,7 @@ Content-Length: 0\r
|
|
|
1891
1956
|
return json({ error: "Owner token required" }, 403);
|
|
1892
1957
|
}
|
|
1893
1958
|
const sourceDeviceId = principal === "owner" ? undefined : principal.deviceId;
|
|
1894
|
-
const effect =
|
|
1959
|
+
const effect = Effect14.gen(function* () {
|
|
1895
1960
|
const store = yield* exports_store.Service;
|
|
1896
1961
|
if (url.pathname === "/api/auth/pairings" && request.method === "POST") {
|
|
1897
1962
|
const input = yield* body(request, decodeCreatePairingInput);
|
|
@@ -1919,7 +1984,7 @@ Content-Length: 0\r
|
|
|
1919
1984
|
if (url.pathname === "/api/operations/upgrades" && request.method === "POST") {
|
|
1920
1985
|
const input = yield* body(request, decodeCreateUpgradeOperationsInput);
|
|
1921
1986
|
const operations = yield* store.createUpgradeOperations(input);
|
|
1922
|
-
return json(yield*
|
|
1987
|
+
return json(yield* Effect14.tryPromise(() => Promise.all(operations.map(dispatchOperation))), 201);
|
|
1923
1988
|
}
|
|
1924
1989
|
const abandonOperation = url.pathname.match(/^\/api\/operations\/([^/]+)\/abandon$/);
|
|
1925
1990
|
if (request.method === "POST" && abandonOperation?.[1] !== undefined) {
|
|
@@ -1946,9 +2011,9 @@ Content-Length: 0\r
|
|
|
1946
2011
|
if (url.pathname === "/api/tasks" && request.method === "POST") {
|
|
1947
2012
|
const input = yield* body(request, decodeCreateTaskInput);
|
|
1948
2013
|
const target = yield* chooseDevice(input, sourceDeviceId);
|
|
1949
|
-
const providerSessionId = input.threadId === undefined ? undefined : yield* store.sessionFor(input.threadId, target, input.provider ?? "codex");
|
|
2014
|
+
const providerSessionId = input.threadId === undefined || input.provider === "grok-bot" ? undefined : yield* store.sessionFor(input.threadId, target, input.provider ?? "codex");
|
|
1950
2015
|
const task2 = yield* store.createDelegation(input, target, sourceDeviceId, providerSessionId);
|
|
1951
|
-
return json(yield*
|
|
2016
|
+
return json(yield* Effect14.tryPromise(() => dispatch(task2)), 201);
|
|
1952
2017
|
}
|
|
1953
2018
|
const task = url.pathname.match(/^\/api\/tasks\/([^/]+)$/);
|
|
1954
2019
|
if (request.method === "GET" && task?.[1] !== undefined) {
|
|
@@ -1972,8 +2037,8 @@ Content-Length: 0\r
|
|
|
1972
2037
|
return json(yield* store.threadContext(yield* pathId(ThreadId, thread[1])));
|
|
1973
2038
|
}
|
|
1974
2039
|
return yield* new RequestError({ status: 404, message: "Route not found" });
|
|
1975
|
-
}).pipe(
|
|
1976
|
-
status: cause._tag !== "RelayStore.PersistenceError" ? 500 : cause.message.startsWith("Unknown") ? 404 : cause.message.includes("outstanding task limit") ? 429 : cause.message.includes("must be offline") || cause.message.includes("still has outstanding tasks") || cause.message.includes("upgrade operation in progress") || cause.message === "No Cohall devices are registered" || cause.message.startsWith("All-device operations support") ? 409 : 500,
|
|
2040
|
+
}).pipe(Effect14.mapError((cause) => cause instanceof RequestError ? cause : new RequestError({
|
|
2041
|
+
status: cause._tag !== "RelayStore.PersistenceError" ? 500 : cause.message.startsWith("Unknown") ? 404 : cause.message.includes("cannot cancel an individual Cohall request") ? 409 : cause.message.includes("outstanding task limit") ? 429 : cause.message.includes("must be offline") || cause.message.includes("still has outstanding tasks") || cause.message.includes("upgrade operation in progress") || cause.message === "No Cohall devices are registered" || cause.message.startsWith("All-device operations support") ? 409 : 500,
|
|
1977
2042
|
message: cause.message
|
|
1978
2043
|
})));
|
|
1979
2044
|
return run2(effect).catch((cause) => cause instanceof RequestError ? json({ error: cause.status === 500 ? "Relay request failed" : cause.message }, cause.status) : json({ error: "Relay request failed" }, 500));
|
|
@@ -1981,7 +2046,8 @@ Content-Length: 0\r
|
|
|
1981
2046
|
const websocketServer = new WebSocketServer({
|
|
1982
2047
|
noServer: true,
|
|
1983
2048
|
maxPayload: maxSocketPayloadBytes,
|
|
1984
|
-
perMessageDeflate: false
|
|
2049
|
+
perMessageDeflate: false,
|
|
2050
|
+
allowSynchronousEvents: false
|
|
1985
2051
|
});
|
|
1986
2052
|
websocketServer.on("connection", (rawSocket) => {
|
|
1987
2053
|
const socket = rawSocket;
|
|
@@ -2046,7 +2112,7 @@ Content-Length: 0\r
|
|
|
2046
2112
|
if (deviceId === undefined) {
|
|
2047
2113
|
return;
|
|
2048
2114
|
}
|
|
2049
|
-
run2(
|
|
2115
|
+
run2(Effect14.gen(function* () {
|
|
2050
2116
|
const store = yield* exports_store.Service;
|
|
2051
2117
|
yield* store.requeueTasksFor(deviceId);
|
|
2052
2118
|
yield* store.requeueOperationsFor(deviceId);
|
|
@@ -2074,11 +2140,11 @@ Content-Length: 0\r
|
|
|
2074
2140
|
websocketServer.emit("connection", websocket, request);
|
|
2075
2141
|
});
|
|
2076
2142
|
});
|
|
2077
|
-
await new Promise((
|
|
2143
|
+
await new Promise((resolve5, reject) => {
|
|
2078
2144
|
server.once("error", reject);
|
|
2079
2145
|
server.listen(relayListenOptions(configuration), () => {
|
|
2080
2146
|
server.off("error", reject);
|
|
2081
|
-
|
|
2147
|
+
resolve5();
|
|
2082
2148
|
});
|
|
2083
2149
|
});
|
|
2084
2150
|
const address = server.address();
|
|
@@ -2093,7 +2159,7 @@ Content-Length: 0\r
|
|
|
2093
2159
|
for (const socket of websocketServer.clients) {
|
|
2094
2160
|
socket.close(1001, "Relay shutting down");
|
|
2095
2161
|
}
|
|
2096
|
-
await new Promise((
|
|
2162
|
+
await new Promise((resolve5) => server.close(() => resolve5()));
|
|
2097
2163
|
websocketServer.close();
|
|
2098
2164
|
await runtime.dispose();
|
|
2099
2165
|
};
|
|
@@ -2105,9 +2171,9 @@ var init_main = __esm(async () => {
|
|
|
2105
2171
|
init_config();
|
|
2106
2172
|
init_hub();
|
|
2107
2173
|
init_store();
|
|
2108
|
-
RequestError = class RequestError extends
|
|
2109
|
-
status:
|
|
2110
|
-
message:
|
|
2174
|
+
RequestError = class RequestError extends Schema14.TaggedErrorClass()("Relay.RequestError", {
|
|
2175
|
+
status: Schema14.Int,
|
|
2176
|
+
message: Schema14.String
|
|
2111
2177
|
}) {
|
|
2112
2178
|
};
|
|
2113
2179
|
if (false) {}
|
|
@@ -2115,7 +2181,7 @@ var init_main = __esm(async () => {
|
|
|
2115
2181
|
|
|
2116
2182
|
// apps/device/src/main.ts
|
|
2117
2183
|
init_src();
|
|
2118
|
-
import { Effect as
|
|
2184
|
+
import { Effect as Effect15 } from "effect";
|
|
2119
2185
|
|
|
2120
2186
|
// packages/client/src/index.ts
|
|
2121
2187
|
init_src();
|
|
@@ -2129,7 +2195,7 @@ __export(exports_src, {
|
|
|
2129
2195
|
RelayDecodeError: () => RelayDecodeError,
|
|
2130
2196
|
RelayClient: () => exports_src
|
|
2131
2197
|
});
|
|
2132
|
-
import { Context, Effect, Layer, Schema as Schema2 } from "effect";
|
|
2198
|
+
import { Context, Effect as Effect2, Layer, Schema as Schema2 } from "effect";
|
|
2133
2199
|
|
|
2134
2200
|
class RelayRequestError extends Schema2.TaggedErrorClass()("RelayClient.RequestError", {
|
|
2135
2201
|
operation: Schema2.String,
|
|
@@ -2188,8 +2254,8 @@ var responseMessage = async (response, operation) => {
|
|
|
2188
2254
|
};
|
|
2189
2255
|
var make = (options) => {
|
|
2190
2256
|
const baseUrl = normalizeBaseUrl(options.baseUrl);
|
|
2191
|
-
const request = (operation, path, schema, init) =>
|
|
2192
|
-
const response = yield*
|
|
2257
|
+
const request = (operation, path, schema, init) => Effect2.gen(function* () {
|
|
2258
|
+
const response = yield* Effect2.tryPromise({
|
|
2193
2259
|
try: (signal) => fetch(`${baseUrl}${path}`, {
|
|
2194
2260
|
...init,
|
|
2195
2261
|
signal: AbortSignal.any([signal, AbortSignal.timeout(30000)]),
|
|
@@ -2205,17 +2271,17 @@ var make = (options) => {
|
|
|
2205
2271
|
})
|
|
2206
2272
|
});
|
|
2207
2273
|
if (!response.ok) {
|
|
2208
|
-
return yield*
|
|
2274
|
+
return yield* Effect2.fail(new RelayRequestError({
|
|
2209
2275
|
operation,
|
|
2210
|
-
message: yield*
|
|
2276
|
+
message: yield* Effect2.promise(() => responseMessage(response, operation)),
|
|
2211
2277
|
status: response.status
|
|
2212
2278
|
}));
|
|
2213
2279
|
}
|
|
2214
|
-
const json = yield*
|
|
2280
|
+
const json = yield* Effect2.tryPromise({
|
|
2215
2281
|
try: () => jsonBody(response, operation),
|
|
2216
2282
|
catch: (cause) => cause instanceof RelayDecodeError ? cause : new RelayDecodeError({ operation, message: String(cause) })
|
|
2217
2283
|
});
|
|
2218
|
-
return yield* Schema2.decodeUnknownEffect(schema)(json).pipe(
|
|
2284
|
+
return yield* Schema2.decodeUnknownEffect(schema)(json).pipe(Effect2.mapError((cause) => new RelayDecodeError({ operation, message: String(cause) })));
|
|
2219
2285
|
});
|
|
2220
2286
|
return Service.of({
|
|
2221
2287
|
devices: () => request("RelayClient.devices", "/api/devices", Schema2.Array(Device)),
|
|
@@ -2242,9 +2308,9 @@ var make = (options) => {
|
|
|
2242
2308
|
abandonOperation: (operationId) => request("RelayClient.abandonOperation", `/api/operations/${encodeURIComponent(operationId)}/abandon`, DeviceOperation, { method: "POST" })
|
|
2243
2309
|
});
|
|
2244
2310
|
};
|
|
2245
|
-
var exchangePairing = (baseUrl, input) =>
|
|
2311
|
+
var exchangePairing = (baseUrl, input) => Effect2.gen(function* () {
|
|
2246
2312
|
const operation = "RelayClient.exchangePairing";
|
|
2247
|
-
const response = yield*
|
|
2313
|
+
const response = yield* Effect2.tryPromise({
|
|
2248
2314
|
try: (signal) => fetch(`${normalizeBaseUrl(baseUrl)}/api/auth/pair`, {
|
|
2249
2315
|
method: "POST",
|
|
2250
2316
|
signal: AbortSignal.any([signal, AbortSignal.timeout(30000)]),
|
|
@@ -2257,17 +2323,17 @@ var exchangePairing = (baseUrl, input) => Effect.gen(function* () {
|
|
|
2257
2323
|
})
|
|
2258
2324
|
});
|
|
2259
2325
|
if (!response.ok) {
|
|
2260
|
-
return yield*
|
|
2326
|
+
return yield* Effect2.fail(new RelayRequestError({
|
|
2261
2327
|
operation,
|
|
2262
|
-
message: yield*
|
|
2328
|
+
message: yield* Effect2.promise(() => responseMessage(response, operation)),
|
|
2263
2329
|
status: response.status
|
|
2264
2330
|
}));
|
|
2265
2331
|
}
|
|
2266
|
-
const json = yield*
|
|
2332
|
+
const json = yield* Effect2.tryPromise({
|
|
2267
2333
|
try: () => jsonBody(response, operation),
|
|
2268
2334
|
catch: (cause) => cause instanceof RelayDecodeError ? cause : new RelayDecodeError({ operation, message: String(cause) })
|
|
2269
2335
|
});
|
|
2270
|
-
return yield* Schema2.decodeUnknownEffect(PairingResult)(json).pipe(
|
|
2336
|
+
return yield* Schema2.decodeUnknownEffect(PairingResult)(json).pipe(Effect2.mapError((cause) => new RelayDecodeError({ operation, message: String(cause) })));
|
|
2271
2337
|
});
|
|
2272
2338
|
var layer = (options) => Layer.succeed(Service, make(options));
|
|
2273
2339
|
|
|
@@ -2276,7 +2342,7 @@ init_src();
|
|
|
2276
2342
|
|
|
2277
2343
|
// packages/providers/src/index.ts
|
|
2278
2344
|
init_src();
|
|
2279
|
-
import { Effect as
|
|
2345
|
+
import { Effect as Effect3, Schema as Schema3 } from "effect";
|
|
2280
2346
|
import { accessSync, constants } from "node:fs";
|
|
2281
2347
|
import { chmod, mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
2282
2348
|
import { homedir, platform, tmpdir } from "node:os";
|
|
@@ -2341,8 +2407,8 @@ var findExecutable = (command) => {
|
|
|
2341
2407
|
}
|
|
2342
2408
|
return;
|
|
2343
2409
|
};
|
|
2344
|
-
var available = (provider) => findExecutable(executables[provider]) !== undefined;
|
|
2345
|
-
var availableProviders = () => Provider.literals.filter(available);
|
|
2410
|
+
var available = (provider) => provider !== "grok-bot" && findExecutable(executables[provider]) !== undefined;
|
|
2411
|
+
var availableProviders = () => Provider.literals.filter((provider) => provider !== "grok-bot").filter(available);
|
|
2346
2412
|
var boundedText = (stream, limit, onOverflow) => new Promise((resolve, reject) => {
|
|
2347
2413
|
const chunks = [];
|
|
2348
2414
|
let size = 0;
|
|
@@ -2642,7 +2708,7 @@ var terminateProcessTree = async (pid) => {
|
|
|
2642
2708
|
process.kill(-pid, "SIGKILL");
|
|
2643
2709
|
await waitForProcessGroup(pid, 1000);
|
|
2644
2710
|
};
|
|
2645
|
-
var run = (options) =>
|
|
2711
|
+
var run = (options) => Effect3.tryPromise({
|
|
2646
2712
|
try: async (signal) => {
|
|
2647
2713
|
const executable = findExecutable(executables[options.provider]);
|
|
2648
2714
|
if (executable === undefined) {
|
|
@@ -2660,7 +2726,8 @@ var run = (options) => Effect2.tryPromise({
|
|
|
2660
2726
|
env: {
|
|
2661
2727
|
...providerEnvironment(),
|
|
2662
2728
|
COHALL_PROVIDER: options.provider,
|
|
2663
|
-
COHALL_THREAD_ID: options.threadId
|
|
2729
|
+
COHALL_THREAD_ID: options.threadId,
|
|
2730
|
+
...options.taskId === undefined ? {} : { COHALL_TASK_ID: options.taskId }
|
|
2664
2731
|
},
|
|
2665
2732
|
detached: platform() !== "win32",
|
|
2666
2733
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -2735,23 +2802,22 @@ var run = (options) => Effect2.tryPromise({
|
|
|
2735
2802
|
});
|
|
2736
2803
|
|
|
2737
2804
|
// apps/device/src/cli.ts
|
|
2738
|
-
import { Effect as
|
|
2739
|
-
import { access as access4, mkdir as
|
|
2805
|
+
import { Effect as Effect9, Schema as Schema10 } from "effect";
|
|
2806
|
+
import { access as access4, mkdir as mkdir6, readFile as readFile4, stat as stat3, writeFile as writeFile6 } from "node:fs/promises";
|
|
2740
2807
|
import { homedir as homedir4 } from "node:os";
|
|
2741
|
-
import { dirname as
|
|
2808
|
+
import { dirname as dirname6, join as join7, resolve as resolve3 } from "node:path";
|
|
2742
2809
|
|
|
2743
2810
|
// skills/cohall/SKILL.md
|
|
2744
2811
|
var SKILL_default = `---
|
|
2745
2812
|
name: cohall
|
|
2746
|
-
description: Delegate
|
|
2813
|
+
description: Delegate through Cohall to another user-owned device or an existing Grok Bot. Use when the user names a Cohall target, asks to message one of their bots, or needs another machine's tools, files, or signed-in services. Carry relevant conversation context into the handoff.
|
|
2747
2814
|
---
|
|
2748
2815
|
|
|
2749
2816
|
# Cohall
|
|
2750
2817
|
|
|
2751
|
-
Cohall sends a focused task to
|
|
2818
|
+
Cohall sends a focused task to a coding agent or an existing named Grok Bot. The target
|
|
2752
2819
|
keeps its local files, credentials, browser state, provider login, skills, and
|
|
2753
|
-
permissions. The relay carries prompts, final results, and task state
|
|
2754
|
-
a lead agent.
|
|
2820
|
+
permissions. The relay carries prompts, final results, and task state.
|
|
2755
2821
|
|
|
2756
2822
|
Use the installed \`cohall\` executable when it is available. Fall back to
|
|
2757
2823
|
\`npx -y @akshar5/cohall\` only when Cohall is not installed globally.
|
|
@@ -2759,9 +2825,8 @@ Use the installed \`cohall\` executable when it is available. Fall back to
|
|
|
2759
2825
|
## Recognize cross-device requests
|
|
2760
2826
|
|
|
2761
2827
|
Treat phrases such as “run this on my Mac,” “ask \`@server\`,” or “have the Linux
|
|
2762
|
-
machine check this” as target intent.
|
|
2763
|
-
|
|
2764
|
-
devices\`, then delegate the smallest useful outcome.
|
|
2828
|
+
machine check this” as target intent. Resolve computers with \`cohall devices\`
|
|
2829
|
+
and named bots with \`cohall bots\`, then delegate the smallest useful outcome.
|
|
2765
2830
|
|
|
2766
2831
|
Good reasons to delegate include:
|
|
2767
2832
|
|
|
@@ -2805,8 +2870,9 @@ Do not delegate ordinary local work when the other device provides no advantage.
|
|
|
2805
2870
|
--context 'Why: deployment 184 failed after 15:00 UTC. Known: local checks passed and the provider status page reported elevated errors. Need: determine whether the outage explains our failure, with primary-source links and contrary evidence.'
|
|
2806
2871
|
\`\`\`
|
|
2807
2872
|
|
|
2808
|
-
|
|
2809
|
-
use Codex
|
|
2873
|
+
Coding providers are \`codex\`, \`claude-code\`, and \`opencode\`. Omit \`--provider\`
|
|
2874
|
+
to use Codex on a computer; selecting a named bot infers \`grok-bot\`.
|
|
2875
|
+
Omit \`--target\` only when Cohall may choose a matching computer.
|
|
2810
2876
|
|
|
2811
2877
|
5. The command waits by default and returns JSON. Treat work as successful only
|
|
2812
2878
|
when \`status\` is \`completed\`; use \`result\` in the current task. Report a
|
|
@@ -2822,9 +2888,43 @@ Do not delegate ordinary local work when the other device provides no advantage.
|
|
|
2822
2888
|
--prompt 'Check whether yesterday’s deployment failed for the same reason.'
|
|
2823
2889
|
\`\`\`
|
|
2824
2890
|
|
|
2825
|
-
When
|
|
2826
|
-
|
|
2827
|
-
|
|
2891
|
+
When a coding agent running as a Cohall task delegates again, \`COHALL_THREAD_ID\`
|
|
2892
|
+
and \`COHALL_TASK_ID\` carry its thread and parent task. Pass \`--thread\` and
|
|
2893
|
+
\`--parent\` explicitly from a Grok Bot using the IDs in its handoff.
|
|
2894
|
+
|
|
2895
|
+
## Message named bots
|
|
2896
|
+
|
|
2897
|
+
1. Run \`cohall bots\` and choose a listed target. Use its qualified target when
|
|
2898
|
+
names collide; discovery includes every available bot on configured hosts.
|
|
2899
|
+
2. Send the prompt and relevant context:
|
|
2900
|
+
|
|
2901
|
+
\`\`\`bash
|
|
2902
|
+
cohall send @Research --prompt 'Find three project ideas worth exploring.'
|
|
2903
|
+
cohall send '@cloud/Video ideas' --prompt 'Suggest an opening for the video.'
|
|
2904
|
+
\`\`\`
|
|
2905
|
+
|
|
2906
|
+
Named bots use their existing Grok conversation and permissions. Omit
|
|
2907
|
+
\`--workspace\`. A Cohall thread records the exchange; it does not create an
|
|
2908
|
+
isolated Grok conversation.
|
|
2909
|
+
|
|
2910
|
+
3. Reuse \`--thread <thread-id>\` for follow-ups. With an explicit thread and no
|
|
2911
|
+
target, Cohall resumes its most recent root bot task. Select a computer
|
|
2912
|
+
explicitly when delegating a child coding task:
|
|
2913
|
+
|
|
2914
|
+
\`\`\`bash
|
|
2915
|
+
cohall delegate --target @cloud --provider codex \\
|
|
2916
|
+
--thread <thread-id> --parent <task-id> --prompt 'Implement the selected idea.'
|
|
2917
|
+
\`\`\`
|
|
2918
|
+
|
|
2919
|
+
Different bots and the computer's coding agent can run concurrently. Each
|
|
2920
|
+
individual bot processes one Cohall request at a time.
|
|
2921
|
+
|
|
2922
|
+
When receiving a Cohall request inside a Grok Bot, finish with the \`cohall reply\`
|
|
2923
|
+
command supplied in the handoff. Pass the final answer through \`--message-file\`
|
|
2924
|
+
or \`--message -\` on stdin, then reply normally in chat. Use \`--error\` if unable
|
|
2925
|
+
to finish. This local receipt is what returns the result to the sending device;
|
|
2926
|
+
a chat message alone does not complete the Cohall task. The receipt survives
|
|
2927
|
+
worker restarts and works while the relay is temporarily unreachable.
|
|
2828
2928
|
|
|
2829
2929
|
## Context and safety
|
|
2830
2930
|
|
|
@@ -2860,8 +2960,10 @@ unless cancelled:
|
|
|
2860
2960
|
cohall cancel <task-id>
|
|
2861
2961
|
\`\`\`
|
|
2862
2962
|
|
|
2863
|
-
|
|
2864
|
-
the provider process has not confirmed termination yet.
|
|
2963
|
+
For coding agents, active cancellation is acknowledged by the target device;
|
|
2964
|
+
\`cancelling\` means the provider process has not confirmed termination yet.
|
|
2965
|
+
Queued bot tasks can be cancelled. Stop an active bot in Grok Bot; its gateway
|
|
2966
|
+
cannot safely cancel an individual Cohall request.
|
|
2865
2967
|
|
|
2866
2968
|
## Read shared context
|
|
2867
2969
|
|
|
@@ -2910,12 +3012,12 @@ Return the diagnostic failure to the user rather than claiming remote work ran.
|
|
|
2910
3012
|
|
|
2911
3013
|
// apps/device/src/config.ts
|
|
2912
3014
|
init_src();
|
|
2913
|
-
import { Effect as
|
|
3015
|
+
import { Effect as Effect4, Schema as Schema4 } from "effect";
|
|
2914
3016
|
import { access, chmod as chmod2, mkdir, readFile, realpath, rename, writeFile as writeFile2 } from "node:fs/promises";
|
|
2915
3017
|
import { homedir as homedir2, hostname, platform as platform2 } from "node:os";
|
|
2916
3018
|
import { dirname, join as join2, resolve } from "node:path";
|
|
2917
3019
|
var WorkspaceList = Schema4.Array(Schema4.NonEmptyString).check(Schema4.isMaxLength(64));
|
|
2918
|
-
var ProviderList = Schema4.Array(Provider).check(Schema4.isMinLength(1)).check(Schema4.isMaxLength(
|
|
3020
|
+
var ProviderList = Schema4.Array(Provider).check(Schema4.isMinLength(1)).check(Schema4.isMaxLength(Provider.literals.length));
|
|
2919
3021
|
var Sandbox = Schema4.Literals(["read-only", "workspace-write", "danger-full-access"]);
|
|
2920
3022
|
var StoredConfiguration = Schema4.Struct({
|
|
2921
3023
|
version: Schema4.Literal(1),
|
|
@@ -2927,12 +3029,14 @@ var StoredConfiguration = Schema4.Struct({
|
|
|
2927
3029
|
deviceToken: Schema4.optionalKey(Schema4.NonEmptyString),
|
|
2928
3030
|
providers: Schema4.optionalKey(ProviderList),
|
|
2929
3031
|
model: Schema4.optionalKey(Schema4.NonEmptyString),
|
|
2930
|
-
sandbox: Schema4.optionalKey(Sandbox)
|
|
3032
|
+
sandbox: Schema4.optionalKey(Sandbox),
|
|
3033
|
+
grokGateway: Schema4.optionalKey(Schema4.NonEmptyString)
|
|
2931
3034
|
});
|
|
2932
3035
|
var ClientConfiguration = Schema4.Struct({
|
|
2933
3036
|
relayUrl: Schema4.NonEmptyString,
|
|
2934
3037
|
token: Schema4.NonEmptyString,
|
|
2935
|
-
mcpThreadId: Schema4.optionalKey(Schema4.String)
|
|
3038
|
+
mcpThreadId: Schema4.optionalKey(Schema4.String),
|
|
3039
|
+
mcpTaskId: Schema4.optionalKey(Schema4.String)
|
|
2936
3040
|
});
|
|
2937
3041
|
var DeviceConfiguration = Schema4.Struct({
|
|
2938
3042
|
relayUrl: Schema4.NonEmptyString,
|
|
@@ -2942,7 +3046,8 @@ var DeviceConfiguration = Schema4.Struct({
|
|
|
2942
3046
|
workspaces: WorkspaceList,
|
|
2943
3047
|
providers: Schema4.optionalKey(ProviderList),
|
|
2944
3048
|
model: Schema4.optionalKey(Schema4.NonEmptyString),
|
|
2945
|
-
sandbox: Schema4.optionalKey(Sandbox)
|
|
3049
|
+
sandbox: Schema4.optionalKey(Sandbox),
|
|
3050
|
+
grokGateway: Schema4.optionalKey(Schema4.NonEmptyString)
|
|
2946
3051
|
});
|
|
2947
3052
|
var defaultConfigDirectory = () => {
|
|
2948
3053
|
if (platform2() === "win32") {
|
|
@@ -2978,7 +3083,7 @@ var normalizeRelayUrl = (input) => {
|
|
|
2978
3083
|
url.pathname = "/";
|
|
2979
3084
|
return url.toString().replace(/\/$/, "");
|
|
2980
3085
|
};
|
|
2981
|
-
var parseWorkspaces = (workspaceList, workspaceJson) =>
|
|
3086
|
+
var parseWorkspaces = (workspaceList, workspaceJson) => Effect4.tryPromise({
|
|
2982
3087
|
try: async () => {
|
|
2983
3088
|
const values = workspaceJson === undefined ? workspaceList.split(",").map((workspace) => workspace.trim()).filter((workspace) => workspace.length > 0) : Schema4.decodeUnknownSync(WorkspaceList)(JSON.parse(workspaceJson));
|
|
2984
3089
|
const canonical = await Promise.all(values.map((workspace) => realpath(resolve(workspace))));
|
|
@@ -3031,7 +3136,7 @@ var makeStoredConfiguration = async (input) => {
|
|
|
3031
3136
|
const existing = await readStoredConfiguration();
|
|
3032
3137
|
const relayUrl = normalizeRelayUrl(input.relayUrl);
|
|
3033
3138
|
const retainedCredentials = credentialsForRelay(existing, relayUrl);
|
|
3034
|
-
const canonicalWorkspaces = input.workspaces === undefined ? existing?.workspaces ?? [] : await
|
|
3139
|
+
const canonicalWorkspaces = input.workspaces === undefined ? existing?.workspaces ?? [] : await Effect4.runPromise(parseWorkspaces("", JSON.stringify(input.workspaces)));
|
|
3035
3140
|
const clientToken = input.clientToken ?? retainedCredentials.clientToken;
|
|
3036
3141
|
const deviceToken = input.deviceToken ?? retainedCredentials.deviceToken;
|
|
3037
3142
|
const providers = input.providers === "auto" ? undefined : input.providers ?? existing?.providers;
|
|
@@ -3045,7 +3150,8 @@ var makeStoredConfiguration = async (input) => {
|
|
|
3045
3150
|
...deviceToken === undefined ? {} : { deviceToken },
|
|
3046
3151
|
...providers === undefined ? {} : { providers },
|
|
3047
3152
|
...existing?.model === undefined ? {} : { model: existing.model },
|
|
3048
|
-
...existing?.sandbox === undefined ? {} : { sandbox: existing.sandbox }
|
|
3153
|
+
...existing?.sandbox === undefined ? {} : { sandbox: existing.sandbox },
|
|
3154
|
+
...existing?.grokGateway === undefined ? {} : { grokGateway: existing.grokGateway }
|
|
3049
3155
|
});
|
|
3050
3156
|
};
|
|
3051
3157
|
var environmentWorkspaces = async (stored) => {
|
|
@@ -3054,7 +3160,7 @@ var environmentWorkspaces = async (stored) => {
|
|
|
3054
3160
|
if (list === undefined && json === undefined) {
|
|
3055
3161
|
return stored?.workspaces ?? [];
|
|
3056
3162
|
}
|
|
3057
|
-
return
|
|
3163
|
+
return Effect4.runPromise(parseWorkspaces(list ?? "", json));
|
|
3058
3164
|
};
|
|
3059
3165
|
var environmentProvider = () => process.env.COHALL_MODEL ?? process.env.COHALL_CODEX_MODEL;
|
|
3060
3166
|
var environmentSandbox = () => {
|
|
@@ -3075,7 +3181,7 @@ var storedOrDefaults = async () => {
|
|
|
3075
3181
|
workspaces: []
|
|
3076
3182
|
});
|
|
3077
3183
|
};
|
|
3078
|
-
var loadClientConfiguration =
|
|
3184
|
+
var loadClientConfiguration = Effect4.tryPromise({
|
|
3079
3185
|
try: async () => {
|
|
3080
3186
|
const stored = await storedOrDefaults();
|
|
3081
3187
|
const relayUrl = normalizeRelayUrl(process.env.COHALL_RELAY_URL ?? stored.relayUrl);
|
|
@@ -3086,12 +3192,13 @@ var loadClientConfiguration = Effect3.tryPromise({
|
|
|
3086
3192
|
return ClientConfiguration.make({
|
|
3087
3193
|
relayUrl,
|
|
3088
3194
|
token,
|
|
3089
|
-
...process.env.COHALL_THREAD_ID === undefined ? {} : { mcpThreadId: process.env.COHALL_THREAD_ID }
|
|
3195
|
+
...process.env.COHALL_THREAD_ID === undefined ? {} : { mcpThreadId: process.env.COHALL_THREAD_ID },
|
|
3196
|
+
...process.env.COHALL_TASK_ID === undefined ? {} : { mcpTaskId: process.env.COHALL_TASK_ID }
|
|
3090
3197
|
});
|
|
3091
3198
|
},
|
|
3092
3199
|
catch: (cause) => cause instanceof Error ? cause : new Error(String(cause))
|
|
3093
3200
|
});
|
|
3094
|
-
var loadOwnerConfiguration =
|
|
3201
|
+
var loadOwnerConfiguration = Effect4.tryPromise({
|
|
3095
3202
|
try: async () => {
|
|
3096
3203
|
const stored = await storedOrDefaults();
|
|
3097
3204
|
const ownerTokenPath = join2(relayDataDirectory(), "owner-token");
|
|
@@ -3111,7 +3218,7 @@ var loadOwnerConfiguration = Effect3.tryPromise({
|
|
|
3111
3218
|
},
|
|
3112
3219
|
catch: (cause) => cause instanceof Error ? cause : new Error(String(cause))
|
|
3113
3220
|
});
|
|
3114
|
-
var loadDeviceConfiguration =
|
|
3221
|
+
var loadDeviceConfiguration = Effect4.tryPromise({
|
|
3115
3222
|
try: async () => {
|
|
3116
3223
|
const stored = await storedOrDefaults();
|
|
3117
3224
|
const relayUrl = normalizeRelayUrl(process.env.COHALL_RELAY_URL ?? stored.relayUrl);
|
|
@@ -3126,6 +3233,7 @@ var loadDeviceConfiguration = Effect3.tryPromise({
|
|
|
3126
3233
|
const model = environmentProvider() ?? stored.model;
|
|
3127
3234
|
const sandbox = environmentSandbox() ?? stored.sandbox;
|
|
3128
3235
|
const providers = environmentProviders(stored);
|
|
3236
|
+
const grokGateway = process.env.COHALL_GROK_GATEWAY ?? stored.grokGateway;
|
|
3129
3237
|
return DeviceConfiguration.make({
|
|
3130
3238
|
relayUrl,
|
|
3131
3239
|
token,
|
|
@@ -3134,7 +3242,8 @@ var loadDeviceConfiguration = Effect3.tryPromise({
|
|
|
3134
3242
|
workspaces,
|
|
3135
3243
|
...providers === undefined ? {} : { providers },
|
|
3136
3244
|
...model === undefined ? {} : { model },
|
|
3137
|
-
...sandbox === undefined ? {} : { sandbox }
|
|
3245
|
+
...sandbox === undefined ? {} : { sandbox },
|
|
3246
|
+
...grokGateway === undefined ? {} : { grokGateway: resolve(grokGateway) }
|
|
3138
3247
|
});
|
|
3139
3248
|
},
|
|
3140
3249
|
catch: (cause) => cause instanceof Error ? cause : new Error(String(cause))
|
|
@@ -3143,7 +3252,7 @@ var knownProviders = Provider.literals;
|
|
|
3143
3252
|
|
|
3144
3253
|
// apps/device/src/delegation.ts
|
|
3145
3254
|
init_src();
|
|
3146
|
-
import { Effect as
|
|
3255
|
+
import { Effect as Effect5, Schedule, Schema as Schema5 } from "effect";
|
|
3147
3256
|
|
|
3148
3257
|
class DeviceSelectionError extends Schema5.TaggedErrorClass()("Cohall.DeviceSelectionError", { message: Schema5.String, devices: Schema5.Array(Schema5.String) }) {
|
|
3149
3258
|
}
|
|
@@ -3156,50 +3265,112 @@ var TaskResult = Schema5.Struct({
|
|
|
3156
3265
|
status: TaskStatus,
|
|
3157
3266
|
provider: Provider,
|
|
3158
3267
|
target_device_id: DeviceId,
|
|
3268
|
+
bot_id: Schema5.optionalKey(BotId),
|
|
3159
3269
|
result: Schema5.optionalKey(Schema5.String),
|
|
3160
3270
|
error: Schema5.optionalKey(Schema5.String)
|
|
3161
3271
|
});
|
|
3162
3272
|
var matchingDevices = (devices, target) => {
|
|
3163
|
-
const
|
|
3164
|
-
const
|
|
3273
|
+
const value = target.replace(/^@/, "");
|
|
3274
|
+
const normalized = value.toLowerCase();
|
|
3275
|
+
const byId = devices.find((device) => device.id === value);
|
|
3165
3276
|
return byId === undefined ? devices.filter((device) => device.name.toLowerCase() === normalized || device.hostname.toLowerCase() === normalized) : [byId];
|
|
3166
3277
|
};
|
|
3278
|
+
var listBots = (devices) => devices.flatMap((device) => (device.bots ?? []).map((bot) => ({
|
|
3279
|
+
id: bot.id,
|
|
3280
|
+
name: bot.name,
|
|
3281
|
+
...bot.description === undefined ? {} : { description: bot.description },
|
|
3282
|
+
device_id: device.id,
|
|
3283
|
+
device_name: device.name,
|
|
3284
|
+
status: device.status,
|
|
3285
|
+
target: `@${device.id}/${bot.id}`
|
|
3286
|
+
})));
|
|
3287
|
+
var selectTarget = Effect5.fn("Cohall.selectTarget")(function* (devices, target) {
|
|
3288
|
+
const value = target.replace(/^@/, "");
|
|
3289
|
+
const separator = value.indexOf("/");
|
|
3290
|
+
const qualified = separator !== -1;
|
|
3291
|
+
const deviceMatches = matchingDevices(devices, qualified ? value.slice(0, separator) : value);
|
|
3292
|
+
const botValue = qualified ? value.slice(separator + 1) : value;
|
|
3293
|
+
const bots = listBots(qualified ? deviceMatches : devices);
|
|
3294
|
+
const botsById = bots.filter((bot) => bot.id === botValue);
|
|
3295
|
+
const botMatches = botsById.length > 0 ? botsById : bots.filter((bot) => bot.name.toLowerCase() === botValue.toLowerCase());
|
|
3296
|
+
const exactDevice = qualified ? undefined : deviceMatches.find((device) => device.id === value);
|
|
3297
|
+
if (exactDevice !== undefined)
|
|
3298
|
+
return { deviceId: exactDevice.id, botId: undefined };
|
|
3299
|
+
const matches = [
|
|
3300
|
+
...qualified ? [] : deviceMatches.map((device) => ({ deviceId: device.id, botId: undefined })),
|
|
3301
|
+
...botMatches.map((bot) => ({ deviceId: bot.device_id, botId: bot.id }))
|
|
3302
|
+
];
|
|
3303
|
+
const selected = matches[0];
|
|
3304
|
+
if (matches.length === 1 && selected !== undefined)
|
|
3305
|
+
return selected;
|
|
3306
|
+
return yield* new DeviceSelectionError({
|
|
3307
|
+
message: matches.length === 0 ? `No Cohall device or bot matches ${target}; use cohall devices or cohall bots` : `${target} is ambiguous; use a device ID or @device-id/bot-id from cohall bots`,
|
|
3308
|
+
devices: [
|
|
3309
|
+
...deviceMatches.map((device) => `${device.name} (@${device.id})`),
|
|
3310
|
+
...botMatches.map((bot) => `${bot.name} on ${bot.device_name} (${bot.target})`)
|
|
3311
|
+
]
|
|
3312
|
+
});
|
|
3313
|
+
});
|
|
3167
3314
|
var taskResult = (task) => TaskResult.make({
|
|
3168
3315
|
task_id: task.id,
|
|
3169
3316
|
thread_id: task.threadId,
|
|
3170
3317
|
status: task.status,
|
|
3171
3318
|
provider: task.provider,
|
|
3172
3319
|
target_device_id: task.targetDeviceId,
|
|
3320
|
+
...task.botId === undefined ? {} : { bot_id: task.botId },
|
|
3173
3321
|
...task.result === undefined ? {} : { result: task.result },
|
|
3174
3322
|
...task.error === undefined ? {} : { error: task.error }
|
|
3175
3323
|
});
|
|
3176
|
-
var createDelegation =
|
|
3324
|
+
var createDelegation = Effect5.fn("Cohall.createDelegation")(function* (client, configuration, options) {
|
|
3177
3325
|
const devices = yield* client.devices();
|
|
3178
|
-
|
|
3179
|
-
if (options.target !== undefined && matches.length !== 1) {
|
|
3326
|
+
if (devices.length === 0) {
|
|
3180
3327
|
return yield* new DeviceSelectionError({
|
|
3181
|
-
message:
|
|
3182
|
-
devices:
|
|
3328
|
+
message: "No Cohall devices are registered",
|
|
3329
|
+
devices: []
|
|
3183
3330
|
});
|
|
3184
3331
|
}
|
|
3185
|
-
|
|
3332
|
+
let target = options.target === undefined ? undefined : yield* selectTarget(devices, options.target);
|
|
3333
|
+
if (target === undefined && options.threadId !== undefined && options.provider === undefined && options.parentTaskId === undefined && configuration.mcpTaskId === undefined) {
|
|
3334
|
+
const context = yield* client.threadContext(options.threadId);
|
|
3335
|
+
const previous = context.tasks.findLast((task) => task.parentTaskId === undefined);
|
|
3336
|
+
if (previous === undefined && context.truncated) {
|
|
3337
|
+
return yield* new DeviceSelectionError({
|
|
3338
|
+
message: "Earlier thread targets are no longer in the retained context; specify --target",
|
|
3339
|
+
devices: []
|
|
3340
|
+
});
|
|
3341
|
+
}
|
|
3342
|
+
if (previous?.provider === "grok-bot" && previous.botId !== undefined) {
|
|
3343
|
+
target = yield* selectTarget(devices, `@${previous.targetDeviceId}/${previous.botId}`);
|
|
3344
|
+
}
|
|
3345
|
+
}
|
|
3346
|
+
const botId = target?.botId;
|
|
3347
|
+
if (botId !== undefined && options.provider !== undefined && options.provider !== "grok-bot") {
|
|
3186
3348
|
return yield* new DeviceSelectionError({
|
|
3187
|
-
message:
|
|
3349
|
+
message: `A bot target requires provider grok-bot, not ${options.provider}`,
|
|
3350
|
+
devices: []
|
|
3351
|
+
});
|
|
3352
|
+
}
|
|
3353
|
+
if (botId === undefined && options.provider === "grok-bot") {
|
|
3354
|
+
return yield* new DeviceSelectionError({
|
|
3355
|
+
message: "Select a bot with --target; use cohall bots to list available bots",
|
|
3188
3356
|
devices: []
|
|
3189
3357
|
});
|
|
3190
3358
|
}
|
|
3191
|
-
const
|
|
3359
|
+
const provider = botId === undefined ? options.provider : "grok-bot";
|
|
3192
3360
|
const inheritedThread = configuration.mcpThreadId;
|
|
3361
|
+
const parentTaskId = options.parentTaskId ?? (configuration.mcpTaskId === undefined ? undefined : yield* Schema5.decodeUnknownEffect(TaskId)(configuration.mcpTaskId));
|
|
3193
3362
|
return yield* client.createTask({
|
|
3194
3363
|
prompt: options.prompt,
|
|
3195
|
-
...target === undefined ? {} : { targetDeviceId: target.
|
|
3364
|
+
...target === undefined ? {} : { targetDeviceId: target.deviceId },
|
|
3365
|
+
...botId === undefined ? {} : { botId },
|
|
3196
3366
|
...options.threadId !== undefined ? { threadId: options.threadId } : inheritedThread === undefined ? {} : { threadId: ThreadId.make(inheritedThread) },
|
|
3197
3367
|
...options.context === undefined ? {} : { context: options.context },
|
|
3198
3368
|
...options.workspace === undefined ? {} : { workspace: options.workspace },
|
|
3199
|
-
...
|
|
3369
|
+
...provider === undefined ? {} : { provider },
|
|
3370
|
+
...parentTaskId === undefined ? {} : { parentTaskId }
|
|
3200
3371
|
});
|
|
3201
3372
|
});
|
|
3202
|
-
var waitForTask =
|
|
3373
|
+
var waitForTask = Effect5.fn("Cohall.waitForTask")(function* (client, initial, timeoutSeconds) {
|
|
3203
3374
|
const deadline = Date.now() + timeoutSeconds * 1000;
|
|
3204
3375
|
let task = initial;
|
|
3205
3376
|
while (!isTerminalTask(task)) {
|
|
@@ -3210,28 +3381,28 @@ var waitForTask = Effect4.fn("Cohall.waitForTask")(function* (client, initial, t
|
|
|
3210
3381
|
status: task.status
|
|
3211
3382
|
});
|
|
3212
3383
|
}
|
|
3213
|
-
yield*
|
|
3384
|
+
yield* Effect5.sleep("1 second");
|
|
3214
3385
|
task = yield* client.getTask(task.id);
|
|
3215
3386
|
}
|
|
3216
3387
|
return task;
|
|
3217
3388
|
});
|
|
3218
|
-
var followTaskTrace =
|
|
3389
|
+
var followTaskTrace = Effect5.fn("Cohall.followTaskTrace")(function* (client, taskId, onTrace) {
|
|
3219
3390
|
let revision;
|
|
3220
|
-
const poll = client.traceTask(taskId).pipe(
|
|
3391
|
+
const poll = client.traceTask(taskId).pipe(Effect5.tap((trace) => {
|
|
3221
3392
|
const latest = trace.events.at(-1);
|
|
3222
3393
|
const nextRevision = `${trace.status}:${latest?.kind ?? "none"}:${latest?.at ?? "none"}`;
|
|
3223
3394
|
if (nextRevision === revision) {
|
|
3224
|
-
return
|
|
3395
|
+
return Effect5.succeed(undefined);
|
|
3225
3396
|
}
|
|
3226
3397
|
revision = nextRevision;
|
|
3227
|
-
return
|
|
3398
|
+
return Effect5.sync(() => onTrace(trace));
|
|
3228
3399
|
}));
|
|
3229
|
-
return yield* poll.pipe(
|
|
3400
|
+
return yield* poll.pipe(Effect5.repeat({
|
|
3230
3401
|
until: (trace) => terminalTaskStatuses.has(trace.status),
|
|
3231
3402
|
schedule: Schedule.spaced("1 second")
|
|
3232
3403
|
}));
|
|
3233
3404
|
});
|
|
3234
|
-
var threadContext =
|
|
3405
|
+
var threadContext = Effect5.fn("Cohall.threadContext")(function* (client, threadId) {
|
|
3235
3406
|
return yield* client.threadContext(threadId);
|
|
3236
3407
|
});
|
|
3237
3408
|
|
|
@@ -3280,8 +3451,458 @@ var allDeviceHealth = (devices, localVersion) => {
|
|
|
3280
3451
|
};
|
|
3281
3452
|
};
|
|
3282
3453
|
|
|
3454
|
+
// apps/device/src/grok-bot.ts
|
|
3455
|
+
init_src();
|
|
3456
|
+
import { Effect as Effect6, Schedule as Schedule2, Schema as Schema7 } from "effect";
|
|
3457
|
+
import { open as open2 } from "node:fs/promises";
|
|
3458
|
+
import { isIP } from "node:net";
|
|
3459
|
+
|
|
3460
|
+
// apps/device/src/bot-replies.ts
|
|
3461
|
+
init_src();
|
|
3462
|
+
import { Schema as Schema6 } from "effect";
|
|
3463
|
+
import { randomUUID } from "node:crypto";
|
|
3464
|
+
import { chmod as chmod3, link, mkdir as mkdir2, open, unlink } from "node:fs/promises";
|
|
3465
|
+
import { dirname as dirname2, join as join3 } from "node:path";
|
|
3466
|
+
import { platform as platform3 } from "node:os";
|
|
3467
|
+
var replyLifetimeMs = 6 * 60 * 60 * 1000;
|
|
3468
|
+
var Pending = Schema6.Struct({
|
|
3469
|
+
taskId: TaskId,
|
|
3470
|
+
botId: BotId,
|
|
3471
|
+
threadId: ThreadId,
|
|
3472
|
+
deadline: Schema6.Int.check(Schema6.isGreaterThanOrEqualTo(0))
|
|
3473
|
+
});
|
|
3474
|
+
var Dispatch = Schema6.Struct({ taskId: TaskId });
|
|
3475
|
+
var Reply = Schema6.Union([
|
|
3476
|
+
Schema6.Struct({
|
|
3477
|
+
result: Schema6.String.check(Schema6.isMaxLength(131072)),
|
|
3478
|
+
error: Schema6.optionalKey(Schema6.Never)
|
|
3479
|
+
}),
|
|
3480
|
+
Schema6.Struct({
|
|
3481
|
+
error: Schema6.NonEmptyString.check(Schema6.isMaxLength(16384)),
|
|
3482
|
+
result: Schema6.optionalKey(Schema6.Never)
|
|
3483
|
+
})
|
|
3484
|
+
]);
|
|
3485
|
+
var Receipt = Schema6.Struct({
|
|
3486
|
+
taskId: TaskId,
|
|
3487
|
+
submittedAt: Schema6.Int.check(Schema6.isGreaterThanOrEqualTo(0)),
|
|
3488
|
+
reply: Reply
|
|
3489
|
+
});
|
|
3490
|
+
var isMissing = (cause) => cause instanceof Error && ("code" in cause) && cause.code === "ENOENT";
|
|
3491
|
+
var alreadyExists = (cause) => cause instanceof Error && ("code" in cause) && cause.code === "EEXIST";
|
|
3492
|
+
var paths = (taskId) => {
|
|
3493
|
+
const id = Schema6.decodeUnknownSync(TaskId)(taskId);
|
|
3494
|
+
const directory = join3(dirname2(configurationPath()), "bot-replies");
|
|
3495
|
+
return {
|
|
3496
|
+
directory,
|
|
3497
|
+
pending: join3(directory, `${id}.pending.json`),
|
|
3498
|
+
receipt: join3(directory, `${id}.reply.json`),
|
|
3499
|
+
dispatch: join3(directory, `${id}.dispatch.json`)
|
|
3500
|
+
};
|
|
3501
|
+
};
|
|
3502
|
+
var readJson = async (path, limit) => {
|
|
3503
|
+
const file = await open(path, "r").catch((cause) => {
|
|
3504
|
+
if (isMissing(cause))
|
|
3505
|
+
return;
|
|
3506
|
+
throw cause;
|
|
3507
|
+
});
|
|
3508
|
+
if (file === undefined)
|
|
3509
|
+
return;
|
|
3510
|
+
try {
|
|
3511
|
+
const buffer = Buffer.alloc(limit + 1);
|
|
3512
|
+
let size = 0;
|
|
3513
|
+
while (size < buffer.length) {
|
|
3514
|
+
const { bytesRead } = await file.read(buffer, size, buffer.length - size, size);
|
|
3515
|
+
if (bytesRead === 0)
|
|
3516
|
+
break;
|
|
3517
|
+
size += bytesRead;
|
|
3518
|
+
}
|
|
3519
|
+
if (size > limit)
|
|
3520
|
+
throw new Error("Bot reply record exceeds its size limit");
|
|
3521
|
+
return JSON.parse(buffer.subarray(0, size).toString("utf8"));
|
|
3522
|
+
} catch {
|
|
3523
|
+
throw new Error("Invalid local bot reply record");
|
|
3524
|
+
} finally {
|
|
3525
|
+
await file.close();
|
|
3526
|
+
}
|
|
3527
|
+
};
|
|
3528
|
+
var pendingReply = async (taskId) => {
|
|
3529
|
+
const value = await readJson(paths(taskId).pending, 4096);
|
|
3530
|
+
if (value === undefined)
|
|
3531
|
+
return;
|
|
3532
|
+
const decoded = Schema6.decodeUnknownResult(Pending)(value);
|
|
3533
|
+
if (decoded._tag === "Failure" || decoded.success.taskId !== taskId) {
|
|
3534
|
+
throw new Error("Invalid local bot reply manifest");
|
|
3535
|
+
}
|
|
3536
|
+
return decoded.success;
|
|
3537
|
+
};
|
|
3538
|
+
var publish = async (path, value) => {
|
|
3539
|
+
const temporary = `${path}.${randomUUID()}.tmp`;
|
|
3540
|
+
const file = await open(temporary, "wx", 384);
|
|
3541
|
+
try {
|
|
3542
|
+
try {
|
|
3543
|
+
await file.writeFile(`${JSON.stringify(value)}
|
|
3544
|
+
`, "utf8");
|
|
3545
|
+
await file.sync();
|
|
3546
|
+
} finally {
|
|
3547
|
+
await file.close();
|
|
3548
|
+
}
|
|
3549
|
+
await link(temporary, path);
|
|
3550
|
+
if (platform3() !== "win32") {
|
|
3551
|
+
const directory = await open(dirname2(path), "r");
|
|
3552
|
+
try {
|
|
3553
|
+
await directory.sync();
|
|
3554
|
+
} finally {
|
|
3555
|
+
await directory.close();
|
|
3556
|
+
}
|
|
3557
|
+
}
|
|
3558
|
+
return true;
|
|
3559
|
+
} catch (cause) {
|
|
3560
|
+
if (alreadyExists(cause))
|
|
3561
|
+
return false;
|
|
3562
|
+
throw cause;
|
|
3563
|
+
} finally {
|
|
3564
|
+
await unlink(temporary);
|
|
3565
|
+
}
|
|
3566
|
+
};
|
|
3567
|
+
var shellQuote = (value) => `'${value.replaceAll("'", "'\\''")}'`;
|
|
3568
|
+
var prepareBotReply = async (task) => {
|
|
3569
|
+
if (task.provider !== "grok-bot" || task.botId === undefined) {
|
|
3570
|
+
throw new Error("Only a named Grok Bot task can receive a bot reply");
|
|
3571
|
+
}
|
|
3572
|
+
const files = paths(task.id);
|
|
3573
|
+
await mkdir2(files.directory, { recursive: true, mode: 448 });
|
|
3574
|
+
await chmod3(files.directory, 448);
|
|
3575
|
+
const record2 = Pending.make({
|
|
3576
|
+
taskId: task.id,
|
|
3577
|
+
botId: task.botId,
|
|
3578
|
+
threadId: task.threadId,
|
|
3579
|
+
deadline: Date.now() + replyLifetimeMs
|
|
3580
|
+
});
|
|
3581
|
+
await publish(files.pending, record2);
|
|
3582
|
+
const pending = await pendingReply(task.id);
|
|
3583
|
+
if (pending === undefined || pending.botId !== task.botId || pending.threadId !== task.threadId) {
|
|
3584
|
+
throw new Error("Bot reply manifest does not match this task");
|
|
3585
|
+
}
|
|
3586
|
+
const dispatch = await readJson(files.dispatch, 4096);
|
|
3587
|
+
if (dispatch !== undefined) {
|
|
3588
|
+
const decoded = Schema6.decodeUnknownResult(Dispatch)(dispatch);
|
|
3589
|
+
if (decoded._tag === "Failure" || decoded.success.taskId !== task.id) {
|
|
3590
|
+
throw new Error("Invalid local bot dispatch record");
|
|
3591
|
+
}
|
|
3592
|
+
}
|
|
3593
|
+
return {
|
|
3594
|
+
deadline: pending.deadline,
|
|
3595
|
+
command: `COHALL_CONFIG=${shellQuote(configurationPath())} cohall`,
|
|
3596
|
+
dispatched: dispatch !== undefined
|
|
3597
|
+
};
|
|
3598
|
+
};
|
|
3599
|
+
var claimBotDispatch = async (taskId) => {
|
|
3600
|
+
const pending = await pendingReply(taskId);
|
|
3601
|
+
if (pending === undefined)
|
|
3602
|
+
throw new Error("No pending bot reply for this task");
|
|
3603
|
+
if (Date.now() >= pending.deadline)
|
|
3604
|
+
throw new Error("Bot reply deadline has expired");
|
|
3605
|
+
return publish(paths(taskId).dispatch, { taskId });
|
|
3606
|
+
};
|
|
3607
|
+
var readBotReply = async (taskId) => {
|
|
3608
|
+
const pending = await pendingReply(taskId);
|
|
3609
|
+
if (pending === undefined)
|
|
3610
|
+
return;
|
|
3611
|
+
const value = await readJson(paths(taskId).receipt, 1024 * 1024);
|
|
3612
|
+
if (value === undefined)
|
|
3613
|
+
return;
|
|
3614
|
+
const decoded = Schema6.decodeUnknownResult(Receipt)(value);
|
|
3615
|
+
if (decoded._tag === "Failure" || decoded.success.taskId !== taskId) {
|
|
3616
|
+
throw new Error("Invalid local bot reply receipt");
|
|
3617
|
+
}
|
|
3618
|
+
if (decoded.success.submittedAt >= pending.deadline) {
|
|
3619
|
+
throw new Error("Bot reply was submitted after its deadline");
|
|
3620
|
+
}
|
|
3621
|
+
return decoded.success.reply;
|
|
3622
|
+
};
|
|
3623
|
+
var writeBotReply = async (taskId, reply) => {
|
|
3624
|
+
const decoded = Schema6.decodeUnknownResult(Reply)(reply);
|
|
3625
|
+
if (decoded._tag === "Failure") {
|
|
3626
|
+
throw new Error("Provide a result of at most 131072 characters or a nonempty error of at most 16384 characters");
|
|
3627
|
+
}
|
|
3628
|
+
const pending = await pendingReply(taskId);
|
|
3629
|
+
if (pending === undefined)
|
|
3630
|
+
throw new Error("No pending bot reply for this task");
|
|
3631
|
+
const existing = await readBotReply(taskId);
|
|
3632
|
+
if (existing !== undefined) {
|
|
3633
|
+
if (JSON.stringify(existing) === JSON.stringify(decoded.success))
|
|
3634
|
+
return;
|
|
3635
|
+
throw new Error("A different reply has already been submitted for this task");
|
|
3636
|
+
}
|
|
3637
|
+
const submittedAt = Date.now();
|
|
3638
|
+
if (submittedAt >= pending.deadline)
|
|
3639
|
+
throw new Error("Bot reply deadline has expired");
|
|
3640
|
+
if (await publish(paths(taskId).receipt, { taskId, submittedAt, reply: decoded.success }))
|
|
3641
|
+
return;
|
|
3642
|
+
const winner = await readBotReply(taskId);
|
|
3643
|
+
if (JSON.stringify(winner) !== JSON.stringify(decoded.success)) {
|
|
3644
|
+
throw new Error("A different reply has already been submitted for this task");
|
|
3645
|
+
}
|
|
3646
|
+
};
|
|
3647
|
+
var cleanupBotReply = async (taskId) => {
|
|
3648
|
+
const files = paths(taskId);
|
|
3649
|
+
for (const path of [files.pending, files.receipt, files.dispatch]) {
|
|
3650
|
+
await unlink(path).catch((cause) => {
|
|
3651
|
+
if (!isMissing(cause))
|
|
3652
|
+
throw cause;
|
|
3653
|
+
});
|
|
3654
|
+
}
|
|
3655
|
+
};
|
|
3656
|
+
|
|
3657
|
+
// apps/device/src/grok-bot.ts
|
|
3658
|
+
var maxResponseBytes = 4 * 1024 * 1024;
|
|
3659
|
+
var maxDiscoveryBytes = 64 * 1024;
|
|
3660
|
+
var text2 = Schema7.NonEmptyString;
|
|
3661
|
+
var Discovery = Schema7.Struct({
|
|
3662
|
+
port: Schema7.Int.check(Schema7.isBetween({ minimum: 1, maximum: 65535 })),
|
|
3663
|
+
host: Schema7.optionalKey(text2),
|
|
3664
|
+
scheme: Schema7.optionalKey(Schema7.Literals(["http", "https"])),
|
|
3665
|
+
token: Schema7.optionalKey(Schema7.String.check(Schema7.isMaxLength(16384)))
|
|
3666
|
+
});
|
|
3667
|
+
var Agent = Schema7.Struct({
|
|
3668
|
+
id: Bot.fields.id,
|
|
3669
|
+
name: Bot.fields.name,
|
|
3670
|
+
description: Schema7.optionalKey(Schema7.String),
|
|
3671
|
+
isGroup: Schema7.optionalKey(Schema7.Boolean)
|
|
3672
|
+
});
|
|
3673
|
+
var Roster = Schema7.Array(Agent).check(Schema7.isMaxLength(1024));
|
|
3674
|
+
var Acceptance = Schema7.Union([
|
|
3675
|
+
Schema7.Struct({ outcome: Schema7.Literals(["not-found", "unknown-durability"]) }),
|
|
3676
|
+
Schema7.Struct({
|
|
3677
|
+
outcome: Schema7.Literal("found"),
|
|
3678
|
+
record: Schema7.Struct({
|
|
3679
|
+
status: Schema7.Literals(["accepted", "pending", "rejected"])
|
|
3680
|
+
})
|
|
3681
|
+
})
|
|
3682
|
+
]);
|
|
3683
|
+
var SendResult = Schema7.Struct({ accepted: Schema7.Literal(true) });
|
|
3684
|
+
|
|
3685
|
+
class GatewayError extends Error {
|
|
3686
|
+
}
|
|
3687
|
+
var decode = (schema, value, label) => {
|
|
3688
|
+
const result = Schema7.decodeUnknownResult(schema)(value);
|
|
3689
|
+
if (result._tag === "Failure")
|
|
3690
|
+
throw new GatewayError(`Grok Bot returned an invalid ${label}`);
|
|
3691
|
+
return result.success;
|
|
3692
|
+
};
|
|
3693
|
+
var readConnection = async (path) => {
|
|
3694
|
+
const value = await (async () => {
|
|
3695
|
+
const file = await open2(path, "r");
|
|
3696
|
+
try {
|
|
3697
|
+
const buffer = Buffer.alloc(maxDiscoveryBytes + 1);
|
|
3698
|
+
const { bytesRead } = await file.read(buffer, 0, buffer.length, 0);
|
|
3699
|
+
if (bytesRead > maxDiscoveryBytes)
|
|
3700
|
+
throw new Error("Discovery file exceeds its size limit");
|
|
3701
|
+
return decode(Discovery, JSON.parse(buffer.subarray(0, bytesRead).toString("utf8")), "discovery file");
|
|
3702
|
+
} finally {
|
|
3703
|
+
await file.close();
|
|
3704
|
+
}
|
|
3705
|
+
})().catch(() => {
|
|
3706
|
+
throw new Error("Cannot read Grok Bot gateway discovery; check its configured file and running gateway");
|
|
3707
|
+
});
|
|
3708
|
+
const host = value.host ?? "127.0.0.1";
|
|
3709
|
+
const normalized = host === "0.0.0.0" || host === "localhost" ? "127.0.0.1" : host === "::" || host === "::1" || host === "[::1]" ? "[::1]" : host;
|
|
3710
|
+
if (!(normalized === "[::1]" || isIP(normalized) === 4 && normalized.startsWith("127."))) {
|
|
3711
|
+
throw new Error("Grok Bot gateway must use a loopback address on this device");
|
|
3712
|
+
}
|
|
3713
|
+
return { url: `${value.scheme ?? "http"}://${normalized}:${value.port}`, token: value.token };
|
|
3714
|
+
};
|
|
3715
|
+
var rpc = async (connection, method, args, schema, signal) => {
|
|
3716
|
+
const controller = new AbortController;
|
|
3717
|
+
const deadline = setTimeout(() => controller.abort(), 20000);
|
|
3718
|
+
const combined = signal === undefined ? controller.signal : AbortSignal.any([signal, controller.signal]);
|
|
3719
|
+
try {
|
|
3720
|
+
const response = await fetch(`${connection.url}/api/${method}`, {
|
|
3721
|
+
method: "POST",
|
|
3722
|
+
redirect: "error",
|
|
3723
|
+
headers: {
|
|
3724
|
+
"content-type": "application/json",
|
|
3725
|
+
"x-sand-slim-avatars": "1",
|
|
3726
|
+
...connection.token === undefined || connection.token.length === 0 ? {} : { authorization: `Bearer ${connection.token}` }
|
|
3727
|
+
},
|
|
3728
|
+
body: JSON.stringify(args),
|
|
3729
|
+
signal: combined
|
|
3730
|
+
});
|
|
3731
|
+
if (!response.ok) {
|
|
3732
|
+
await response.body?.cancel();
|
|
3733
|
+
throw new GatewayError(`Grok Bot ${method} failed (HTTP ${response.status})`);
|
|
3734
|
+
}
|
|
3735
|
+
if (response.body === null)
|
|
3736
|
+
throw new GatewayError("Grok Bot returned an empty response");
|
|
3737
|
+
const reader = response.body.getReader();
|
|
3738
|
+
const chunks = [];
|
|
3739
|
+
let size = 0;
|
|
3740
|
+
try {
|
|
3741
|
+
for (;; ) {
|
|
3742
|
+
const chunk = await reader.read();
|
|
3743
|
+
if (chunk.done)
|
|
3744
|
+
break;
|
|
3745
|
+
size += chunk.value.byteLength;
|
|
3746
|
+
if (size > maxResponseBytes)
|
|
3747
|
+
throw new GatewayError("Grok Bot response exceeded 4 MiB");
|
|
3748
|
+
chunks.push(chunk.value);
|
|
3749
|
+
}
|
|
3750
|
+
} finally {
|
|
3751
|
+
await reader.cancel().catch(() => {
|
|
3752
|
+
return;
|
|
3753
|
+
});
|
|
3754
|
+
reader.releaseLock();
|
|
3755
|
+
}
|
|
3756
|
+
let json;
|
|
3757
|
+
try {
|
|
3758
|
+
json = JSON.parse(Buffer.concat(chunks, size).toString("utf8"));
|
|
3759
|
+
} catch {
|
|
3760
|
+
throw new GatewayError(`Grok Bot ${method} returned invalid JSON`);
|
|
3761
|
+
}
|
|
3762
|
+
return decode(schema, json, `${method} response`);
|
|
3763
|
+
} catch (cause) {
|
|
3764
|
+
if (signal?.aborted)
|
|
3765
|
+
throw new Error("Stopped waiting for Grok Bot; its run may still be active");
|
|
3766
|
+
if (cause instanceof GatewayError)
|
|
3767
|
+
throw cause;
|
|
3768
|
+
throw new Error(`Grok Bot ${method} failed; check gateway availability and compatibility`);
|
|
3769
|
+
} finally {
|
|
3770
|
+
clearTimeout(deadline);
|
|
3771
|
+
}
|
|
3772
|
+
};
|
|
3773
|
+
var discoverGrokBots = async (path) => {
|
|
3774
|
+
const connection = await readConnection(path);
|
|
3775
|
+
const roster = await rpc(connection, "listAgents", {}, Roster);
|
|
3776
|
+
const bots = roster.filter((agent) => agent.isGroup !== true);
|
|
3777
|
+
if (bots.length > 256)
|
|
3778
|
+
throw new Error("Grok Bot discovery exceeds Cohall's limit of 256 bots");
|
|
3779
|
+
if (new Set(bots.map((bot) => bot.id)).size !== bots.length) {
|
|
3780
|
+
throw new Error("Grok Bot returned duplicate bot identities");
|
|
3781
|
+
}
|
|
3782
|
+
return bots.map(({ id, name, description }) => ({
|
|
3783
|
+
id,
|
|
3784
|
+
name,
|
|
3785
|
+
...description === undefined ? {} : { description: description.slice(0, 512) }
|
|
3786
|
+
}));
|
|
3787
|
+
};
|
|
3788
|
+
var poll = (pass, intervalMs = 1000) => Effect6.tryPromise({ try: pass, catch: (cause) => cause }).pipe(Effect6.repeat({
|
|
3789
|
+
schedule: Schedule2.spaced(intervalMs),
|
|
3790
|
+
until: (value) => value !== undefined
|
|
3791
|
+
}));
|
|
3792
|
+
var timeoutMessage = "Timed out waiting for the bot to reply through Cohall; the bot may still be running.";
|
|
3793
|
+
var requireTime = (deadline) => {
|
|
3794
|
+
if (Date.now() >= deadline)
|
|
3795
|
+
throw new Error(timeoutMessage);
|
|
3796
|
+
};
|
|
3797
|
+
var finishReply = (reply) => {
|
|
3798
|
+
if (reply.error !== undefined)
|
|
3799
|
+
throw new Error(reply.error);
|
|
3800
|
+
if (reply.result === undefined)
|
|
3801
|
+
throw new Error("Grok Bot returned an invalid Cohall reply");
|
|
3802
|
+
return { result: reply.result };
|
|
3803
|
+
};
|
|
3804
|
+
var taskPrompt = (task, command) => [
|
|
3805
|
+
task.prompt,
|
|
3806
|
+
task.context === undefined ? "" : `Context:
|
|
3807
|
+
${task.context}`,
|
|
3808
|
+
`This request came through Cohall. Thread: ${task.threadId}. Task: ${task.id}.`,
|
|
3809
|
+
`You may use your own tools and delegate work to another Cohall device when the request calls for it. Discover devices with: ${command} devices`,
|
|
3810
|
+
`Delegation example: ${command} delegate --thread ${task.threadId} --parent ${task.id} --target @device --provider codex --prompt 'Concrete task' --context 'Relevant context'`,
|
|
3811
|
+
"When finished, hand the final answer back to Cohall so it reaches the requesting device. Write it to a UTF-8 file on this computer, then run:",
|
|
3812
|
+
`${command} reply ${task.id} --message-file /absolute/path/to/your-answer.txt`,
|
|
3813
|
+
`Alternatively pipe your answer into: ${command} reply ${task.id} --message -`,
|
|
3814
|
+
`If you cannot complete this task, report the reason with: ${command} reply ${task.id} --error 'Reason'`,
|
|
3815
|
+
"The reply command must succeed. Then send the same answer in this chat. A normal chat message alone does not complete the Cohall task."
|
|
3816
|
+
].filter(Boolean).join(`
|
|
3817
|
+
|
|
3818
|
+
`);
|
|
3819
|
+
var runGrokBot = async (path, task, signal) => {
|
|
3820
|
+
const existing = await readBotReply(task.id);
|
|
3821
|
+
if (existing !== undefined)
|
|
3822
|
+
return finishReply(existing);
|
|
3823
|
+
const { deadline, command, dispatched } = await prepareBotReply(task);
|
|
3824
|
+
requireTime(deadline);
|
|
3825
|
+
const agentId = task.botId;
|
|
3826
|
+
if (agentId === undefined)
|
|
3827
|
+
throw new Error("Select a Grok Bot for this task");
|
|
3828
|
+
const lookup = async (lookupSignal = signal) => {
|
|
3829
|
+
if (path === undefined)
|
|
3830
|
+
throw new Error("Configure this device's Grok Bot gateway discovery file first");
|
|
3831
|
+
return rpc(await readConnection(path), "promptAcceptanceStatus", { accountSlot: "host", agentId, clientNonce: task.id }, Acceptance, lookupSignal);
|
|
3832
|
+
};
|
|
3833
|
+
const waitForReply = (reconcile = false) => {
|
|
3834
|
+
const receipt = poll(async () => {
|
|
3835
|
+
const receipt2 = await readBotReply(task.id);
|
|
3836
|
+
if (receipt2 !== undefined)
|
|
3837
|
+
return finishReply(receipt2);
|
|
3838
|
+
requireTime(deadline);
|
|
3839
|
+
return;
|
|
3840
|
+
});
|
|
3841
|
+
if (!reconcile || path === undefined)
|
|
3842
|
+
return Effect6.runPromise(receipt, { signal });
|
|
3843
|
+
const acceptance2 = poll(async (lookupSignal) => {
|
|
3844
|
+
const current = await lookup(lookupSignal).catch(() => {
|
|
3845
|
+
return;
|
|
3846
|
+
});
|
|
3847
|
+
if (current?.outcome !== "found")
|
|
3848
|
+
return;
|
|
3849
|
+
if (current.record.status === "rejected")
|
|
3850
|
+
throw new Error("Grok Bot rejected this task; check its conversation");
|
|
3851
|
+
return current.record.status === "accepted" ? true : undefined;
|
|
3852
|
+
}, 5000).pipe(Effect6.andThen(Effect6.never));
|
|
3853
|
+
return Effect6.runPromise(Effect6.raceFirst(receipt, acceptance2), { signal });
|
|
3854
|
+
};
|
|
3855
|
+
if (dispatched)
|
|
3856
|
+
return waitForReply(true);
|
|
3857
|
+
if (path === undefined)
|
|
3858
|
+
throw new Error("Configure this device's Grok Bot gateway discovery file first");
|
|
3859
|
+
const connection = await readConnection(path);
|
|
3860
|
+
let acceptance = await lookup();
|
|
3861
|
+
if (acceptance.outcome === "unknown-durability")
|
|
3862
|
+
throw new Error("Grok Bot cannot confirm whether this task was accepted; check its conversation before retrying");
|
|
3863
|
+
if (acceptance.outcome === "not-found") {
|
|
3864
|
+
const receipt = await readBotReply(task.id);
|
|
3865
|
+
if (receipt !== undefined)
|
|
3866
|
+
return finishReply(receipt);
|
|
3867
|
+
requireTime(deadline);
|
|
3868
|
+
const roster = await rpc(connection, "listAgents", {}, Roster, signal);
|
|
3869
|
+
if (!roster.some((row) => row.id === agentId && row.isGroup !== true)) {
|
|
3870
|
+
throw new Error("The selected Grok Bot no longer exists on this device");
|
|
3871
|
+
}
|
|
3872
|
+
requireTime(deadline);
|
|
3873
|
+
acceptance = await lookup();
|
|
3874
|
+
if (acceptance.outcome === "not-found") {
|
|
3875
|
+
requireTime(deadline);
|
|
3876
|
+
if (!await claimBotDispatch(task.id)) {
|
|
3877
|
+
return waitForReply(true);
|
|
3878
|
+
}
|
|
3879
|
+
try {
|
|
3880
|
+
await rpc(connection, "sendPrompt", {
|
|
3881
|
+
agentId,
|
|
3882
|
+
prompt: taskPrompt(task, command),
|
|
3883
|
+
clientNonce: task.id,
|
|
3884
|
+
directAddressedAcceptance: true
|
|
3885
|
+
}, SendResult, signal);
|
|
3886
|
+
} catch {
|
|
3887
|
+
if (signal.aborted)
|
|
3888
|
+
throw new Error("Stopped waiting for Grok Bot; its run may still be active");
|
|
3889
|
+
const receipt2 = await readBotReply(task.id);
|
|
3890
|
+
if (receipt2 !== undefined)
|
|
3891
|
+
return finishReply(receipt2);
|
|
3892
|
+
return waitForReply(true);
|
|
3893
|
+
}
|
|
3894
|
+
return waitForReply();
|
|
3895
|
+
}
|
|
3896
|
+
}
|
|
3897
|
+
if (acceptance.outcome === "unknown-durability")
|
|
3898
|
+
throw new Error("Grok Bot cannot confirm task acceptance; the prompt was not resent");
|
|
3899
|
+
if (acceptance.outcome === "found" && acceptance.record.status === "rejected")
|
|
3900
|
+
throw new Error("Grok Bot rejected this task; check its conversation");
|
|
3901
|
+
return waitForReply(acceptance.outcome === "found" && acceptance.record.status === "pending");
|
|
3902
|
+
};
|
|
3903
|
+
|
|
3283
3904
|
// apps/device/src/setup.ts
|
|
3284
|
-
import { Effect as
|
|
3905
|
+
import { Effect as Effect7 } from "effect";
|
|
3285
3906
|
import { hostname as hostname2 } from "node:os";
|
|
3286
3907
|
import { Writable } from "node:stream";
|
|
3287
3908
|
import { createInterface } from "node:readline/promises";
|
|
@@ -3295,7 +3916,7 @@ var joinRelay = async (options) => {
|
|
|
3295
3916
|
if (!options.clientOnly && draft.workspaces.length === 0) {
|
|
3296
3917
|
throw new Error("At least one workspace is required when joining a device");
|
|
3297
3918
|
}
|
|
3298
|
-
const paired = await
|
|
3919
|
+
const paired = await Effect7.runPromise(exchangePairing(draft.relayUrl, { token: options.token }));
|
|
3299
3920
|
const clientToken = paired.credentials.find((credential) => credential.session.role === "client")?.token;
|
|
3300
3921
|
const deviceCredential = paired.credentials.find((credential) => credential.session.role === "device");
|
|
3301
3922
|
if (options.clientOnly && clientToken === undefined) {
|
|
@@ -3378,15 +3999,15 @@ var guidedSetupInput = async (options, prompter) => {
|
|
|
3378
3999
|
|
|
3379
4000
|
// apps/device/src/relay-migration.ts
|
|
3380
4001
|
init_src();
|
|
3381
|
-
import { Effect as
|
|
4002
|
+
import { Effect as Effect8, Schema as Schema9 } from "effect";
|
|
3382
4003
|
import { createHash } from "node:crypto";
|
|
3383
4004
|
import { createReadStream } from "node:fs";
|
|
3384
4005
|
import {
|
|
3385
4006
|
access as access3,
|
|
3386
|
-
chmod as
|
|
4007
|
+
chmod as chmod6,
|
|
3387
4008
|
copyFile,
|
|
3388
4009
|
lstat,
|
|
3389
|
-
mkdir as
|
|
4010
|
+
mkdir as mkdir5,
|
|
3390
4011
|
mkdtemp as mkdtemp2,
|
|
3391
4012
|
readFile as readFile3,
|
|
3392
4013
|
realpath as realpath4,
|
|
@@ -3395,24 +4016,24 @@ import {
|
|
|
3395
4016
|
stat as stat2,
|
|
3396
4017
|
writeFile as writeFile5
|
|
3397
4018
|
} from "node:fs/promises";
|
|
3398
|
-
import { basename, dirname as
|
|
4019
|
+
import { basename, dirname as dirname5, join as join6, resolve as resolve2 } from "node:path";
|
|
3399
4020
|
import { backup as sqliteBackup, DatabaseSync } from "node:sqlite";
|
|
3400
4021
|
import { WebSocket } from "ws";
|
|
3401
4022
|
|
|
3402
4023
|
// apps/device/src/service.ts
|
|
3403
4024
|
import { execFile as execFile3 } from "node:child_process";
|
|
3404
|
-
import { chmod as
|
|
4025
|
+
import { chmod as chmod5, mkdir as mkdir4, realpath as realpath3, writeFile as writeFile4 } from "node:fs/promises";
|
|
3405
4026
|
import { homedir as homedir3 } from "node:os";
|
|
3406
|
-
import { dirname as
|
|
4027
|
+
import { dirname as dirname4, join as join5 } from "node:path";
|
|
3407
4028
|
|
|
3408
4029
|
// apps/device/src/upgrade.ts
|
|
3409
|
-
import { Schema as
|
|
4030
|
+
import { Schema as Schema8 } from "effect";
|
|
3410
4031
|
import { execFile as execFile2 } from "node:child_process";
|
|
3411
4032
|
import { constants as constants2 } from "node:fs";
|
|
3412
4033
|
import {
|
|
3413
4034
|
access as access2,
|
|
3414
|
-
chmod as
|
|
3415
|
-
mkdir as
|
|
4035
|
+
chmod as chmod4,
|
|
4036
|
+
mkdir as mkdir3,
|
|
3416
4037
|
readFile as readFile2,
|
|
3417
4038
|
realpath as realpath2,
|
|
3418
4039
|
rename as rename2,
|
|
@@ -3420,18 +4041,18 @@ import {
|
|
|
3420
4041
|
stat,
|
|
3421
4042
|
writeFile as writeFile3
|
|
3422
4043
|
} from "node:fs/promises";
|
|
3423
|
-
import { delimiter as delimiter2, dirname as
|
|
4044
|
+
import { delimiter as delimiter2, dirname as dirname3, extname as extname2, isAbsolute as isAbsolute2, join as join4, relative } from "node:path";
|
|
3424
4045
|
import { platform as operatingSystem } from "node:os";
|
|
3425
4046
|
var packageName = "@akshar5/cohall";
|
|
3426
4047
|
var semanticVersion = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
|
|
3427
|
-
var PackageManager =
|
|
3428
|
-
var RestartReceipt =
|
|
3429
|
-
version:
|
|
3430
|
-
fromVersion:
|
|
4048
|
+
var PackageManager = Schema8.Literals(["npm", "bun", "pnpm"]);
|
|
4049
|
+
var RestartReceipt = Schema8.Struct({
|
|
4050
|
+
version: Schema8.String,
|
|
4051
|
+
fromVersion: Schema8.String,
|
|
3431
4052
|
packageManager: PackageManager,
|
|
3432
|
-
pendingServices:
|
|
3433
|
-
restartedServices:
|
|
3434
|
-
restartingService:
|
|
4053
|
+
pendingServices: Schema8.Array(Schema8.String),
|
|
4054
|
+
restartedServices: Schema8.Array(Schema8.String),
|
|
4055
|
+
restartingService: Schema8.optionalKey(Schema8.String)
|
|
3435
4056
|
});
|
|
3436
4057
|
var normalizePath = (path) => path.replaceAll("\\", "/");
|
|
3437
4058
|
var normalizeUpgradeTarget = (target) => {
|
|
@@ -3505,32 +4126,19 @@ var linuxSystemExecutableRoots = [
|
|
|
3505
4126
|
];
|
|
3506
4127
|
var macosHomebrewRoots = ["/opt/homebrew", "/usr/local"];
|
|
3507
4128
|
var macosAdminGroup = 80;
|
|
3508
|
-
var
|
|
4129
|
+
var isTrustedSystemPath = (platform4, path) => platform4 === "linux" && (path === "/home" || linuxSystemExecutableRoots.some((root) => containsPath(root, path) || containsPath(path, root)));
|
|
3509
4130
|
var isTrustedGroupWritablePath = (options) => {
|
|
3510
4131
|
const writableRoot = options.writableRoot;
|
|
3511
4132
|
return options.platform === "darwin" && writableRoot !== undefined && options.uid !== undefined && options.ownerUid === options.uid && options.ownerGid === macosAdminGroup && macosHomebrewRoots.some((root) => containsPath(root, writableRoot)) && containsPath(writableRoot, options.canonical) && containsPath(writableRoot, options.path);
|
|
3512
4133
|
};
|
|
3513
|
-
var
|
|
3514
|
-
const
|
|
3515
|
-
let selected;
|
|
3516
|
-
for (const path of candidates) {
|
|
3517
|
-
const available2 = await access2(path, operatingSystem() === "win32" ? constants2.F_OK : constants2.X_OK).then(() => true).catch(() => false);
|
|
3518
|
-
if (available2) {
|
|
3519
|
-
selected = path;
|
|
3520
|
-
break;
|
|
3521
|
-
}
|
|
3522
|
-
}
|
|
3523
|
-
if (selected === undefined) {
|
|
3524
|
-
throw new Error(`Could not find ${command} on PATH`);
|
|
3525
|
-
}
|
|
3526
|
-
const canonical = await realpath2(selected);
|
|
4134
|
+
var validateExecutable = async (candidate, installationRoot) => {
|
|
4135
|
+
const canonical = await realpath2(candidate);
|
|
3527
4136
|
if (operatingSystem() === "win32") {
|
|
3528
4137
|
return canonical;
|
|
3529
4138
|
}
|
|
3530
4139
|
const uid = process.getuid?.();
|
|
3531
|
-
const writableRoot =
|
|
3532
|
-
|
|
3533
|
-
for (let path = canonical;; path = dirname2(path)) {
|
|
4140
|
+
const writableRoot = installationRoot === undefined ? undefined : await realpath2(installationRoot);
|
|
4141
|
+
for (let path = canonical;; path = dirname3(path)) {
|
|
3534
4142
|
const metadata = await stat(path);
|
|
3535
4143
|
const trustedGroupWritablePath = isTrustedGroupWritablePath({
|
|
3536
4144
|
platform: operatingSystem(),
|
|
@@ -3544,16 +4152,31 @@ var trustedExecutable = async (command, options = {}) => {
|
|
|
3544
4152
|
if ((metadata.mode & 2) !== 0 || (metadata.mode & 16) !== 0 && !trustedGroupWritablePath) {
|
|
3545
4153
|
throw new Error(`Refusing executable beneath group- or world-writable path ${path}`);
|
|
3546
4154
|
}
|
|
3547
|
-
if (uid !== undefined && metadata.uid !== 0 && metadata.uid !== uid && !
|
|
4155
|
+
if (uid !== undefined && metadata.uid !== 0 && metadata.uid !== uid && !isTrustedSystemPath(operatingSystem(), path)) {
|
|
3548
4156
|
throw new Error(`Refusing executable owned by another user at ${path}`);
|
|
3549
4157
|
}
|
|
3550
|
-
const parent =
|
|
4158
|
+
const parent = dirname3(path);
|
|
3551
4159
|
if (parent === path) {
|
|
3552
4160
|
break;
|
|
3553
4161
|
}
|
|
3554
4162
|
}
|
|
3555
4163
|
return canonical;
|
|
3556
4164
|
};
|
|
4165
|
+
var trustedExecutable = async (command, options = {}) => {
|
|
4166
|
+
const candidates = isAbsolute2(command) ? [command] : (process.env.PATH ?? "").split(delimiter2).filter(Boolean).flatMap((directory) => executableNames(command).map((name) => join4(directory, name)));
|
|
4167
|
+
let failure;
|
|
4168
|
+
for (const path of candidates) {
|
|
4169
|
+
const available2 = await access2(path, operatingSystem() === "win32" ? constants2.F_OK : constants2.X_OK).then(() => true).catch(() => false);
|
|
4170
|
+
if (!available2)
|
|
4171
|
+
continue;
|
|
4172
|
+
try {
|
|
4173
|
+
return await validateExecutable(path, options.writableRoot);
|
|
4174
|
+
} catch (cause) {
|
|
4175
|
+
failure ??= cause;
|
|
4176
|
+
}
|
|
4177
|
+
}
|
|
4178
|
+
throw failure ?? new Error(`Could not find ${command} on PATH`);
|
|
4179
|
+
};
|
|
3557
4180
|
var trustedServices = async (services, resolveExecutable) => {
|
|
3558
4181
|
const resolved = new Map;
|
|
3559
4182
|
const invocation = async (value) => {
|
|
@@ -3712,15 +4335,15 @@ ${result.stderr}`);
|
|
|
3712
4335
|
throw new Error(`Active ${service.label} uses ${serviceEntrypoint}, but this Cohall CLI uses ${entrypoint}. Run the service executable's upgrade command so its installation is updated before restart.`);
|
|
3713
4336
|
}
|
|
3714
4337
|
};
|
|
3715
|
-
var readReceipt = async (path) => readFile2(path, "utf8").then((content) =>
|
|
4338
|
+
var readReceipt = async (path) => readFile2(path, "utf8").then((content) => Schema8.decodeUnknownOption(RestartReceipt)(JSON.parse(content))).then((decoded) => decoded._tag === "Some" ? decoded.value : undefined).catch(() => {
|
|
3716
4339
|
return;
|
|
3717
4340
|
});
|
|
3718
4341
|
var writeReceipt = async (path, value) => {
|
|
3719
|
-
const directory =
|
|
4342
|
+
const directory = dirname3(path);
|
|
3720
4343
|
const temporary = `${path}.${process.pid}.tmp`;
|
|
3721
|
-
await
|
|
4344
|
+
await mkdir3(directory, { recursive: true, mode: 448 });
|
|
3722
4345
|
if (operatingSystem() !== "win32") {
|
|
3723
|
-
await
|
|
4346
|
+
await chmod4(directory, 448);
|
|
3724
4347
|
}
|
|
3725
4348
|
await writeFile3(temporary, `${JSON.stringify(value, null, 2)}
|
|
3726
4349
|
`, { mode: 384 });
|
|
@@ -3728,13 +4351,13 @@ var writeReceipt = async (path, value) => {
|
|
|
3728
4351
|
};
|
|
3729
4352
|
var installedVersion = async (entrypoint) => {
|
|
3730
4353
|
const canonicalEntrypoint = await realpath2(entrypoint);
|
|
3731
|
-
const metadata = JSON.parse(await readFile2(
|
|
4354
|
+
const metadata = JSON.parse(await readFile2(join4(dirname3(dirname3(canonicalEntrypoint)), "package.json"), "utf8"));
|
|
3732
4355
|
if (typeof metadata !== "object" || metadata === null || !("name" in metadata) || metadata.name !== packageName || !("version" in metadata) || typeof metadata.version !== "string") {
|
|
3733
4356
|
throw new Error(`Could not verify the installed ${packageName} version`);
|
|
3734
4357
|
}
|
|
3735
4358
|
return metadata.version;
|
|
3736
4359
|
};
|
|
3737
|
-
var restartReceiptPath = () =>
|
|
4360
|
+
var restartReceiptPath = () => join4(dirname3(configurationPath()), "upgrade-restart.json");
|
|
3738
4361
|
var restartServices = async (runner, services, statePath, state, preserveDeviceReceipt) => {
|
|
3739
4362
|
let current = state;
|
|
3740
4363
|
for (const service of services) {
|
|
@@ -3771,6 +4394,7 @@ var restartServices = async (runner, services, statePath, state, preserveDeviceR
|
|
|
3771
4394
|
};
|
|
3772
4395
|
var reportedRestartServices = (receipt) => receipt.restartingService === undefined || receipt.restartedServices.includes(receipt.restartingService) ? receipt.restartedServices : [...receipt.restartedServices, receipt.restartingService];
|
|
3773
4396
|
var upgrade = async (options) => {
|
|
4397
|
+
const target = normalizeUpgradeTarget(options.target);
|
|
3774
4398
|
const runner = options.runner ?? defaultRunner;
|
|
3775
4399
|
const resolveExecutable = options.resolveExecutable ?? trustedExecutable;
|
|
3776
4400
|
const runtimePlatform = options.platform ?? process.platform;
|
|
@@ -3778,7 +4402,7 @@ var upgrade = async (options) => {
|
|
|
3778
4402
|
const candidates = await trustedServices(serviceCandidates(runtimePlatform, uid), resolveExecutable);
|
|
3779
4403
|
const statePath = options.statePath ?? restartReceiptPath();
|
|
3780
4404
|
const previous = await readReceipt(statePath);
|
|
3781
|
-
if (previous?.version === options.currentVersion) {
|
|
4405
|
+
if (previous?.version === options.currentVersion && (target === "latest" || target === previous.version)) {
|
|
3782
4406
|
const byId = new Map(candidates.map((service) => [service.id, service]));
|
|
3783
4407
|
const pending = previous.pendingServices.flatMap((id) => {
|
|
3784
4408
|
const service = byId.get(id);
|
|
@@ -3831,15 +4455,12 @@ var upgrade = async (options) => {
|
|
|
3831
4455
|
dry_run: false
|
|
3832
4456
|
};
|
|
3833
4457
|
}
|
|
3834
|
-
await rm2(statePath, { force: true });
|
|
3835
|
-
const target = normalizeUpgradeTarget(options.target);
|
|
3836
4458
|
const entrypoint = options.entrypoint ?? process.argv[1];
|
|
3837
4459
|
if (entrypoint === undefined) {
|
|
3838
4460
|
throw new Error("Could not resolve the Cohall executable path");
|
|
3839
4461
|
}
|
|
3840
4462
|
const canonicalEntrypoint = await realpath2(entrypoint);
|
|
3841
4463
|
const installation = packageInstallation(canonicalEntrypoint, entrypoint);
|
|
3842
|
-
const install = packageInstallCommand(installation, target);
|
|
3843
4464
|
const services = await activeServices(runner, candidates);
|
|
3844
4465
|
await assertServiceInstallations(runner, services, canonicalEntrypoint, entrypoint);
|
|
3845
4466
|
if (options.dryRun) {
|
|
@@ -3855,14 +4476,21 @@ var upgrade = async (options) => {
|
|
|
3855
4476
|
dry_run: true
|
|
3856
4477
|
};
|
|
3857
4478
|
}
|
|
3858
|
-
|
|
3859
|
-
|
|
3860
|
-
|
|
4479
|
+
let nextVersion = await installedVersion(entrypoint).catch(() => {
|
|
4480
|
+
return;
|
|
4481
|
+
});
|
|
4482
|
+
if (nextVersion === undefined || target !== nextVersion) {
|
|
4483
|
+
const install = packageInstallCommand(installation, target);
|
|
4484
|
+
const installExecutable = options.resolveExecutable === undefined ? await trustedExecutable(install.command, installation.prefix === undefined ? {} : { writableRoot: installation.prefix }) : await resolveExecutable(install.command);
|
|
4485
|
+
await checked(runner, { ...install, command: installExecutable });
|
|
4486
|
+
nextVersion = await installedVersion(entrypoint);
|
|
4487
|
+
}
|
|
3861
4488
|
if (target !== "latest" && nextVersion !== target) {
|
|
3862
4489
|
throw new Error(`Installed Cohall ${nextVersion}, expected ${target}`);
|
|
3863
4490
|
}
|
|
3864
4491
|
const upgraded = nextVersion !== options.currentVersion;
|
|
3865
4492
|
if (!options.restart || services.length === 0) {
|
|
4493
|
+
await rm2(statePath, { force: true });
|
|
3866
4494
|
return {
|
|
3867
4495
|
upgraded,
|
|
3868
4496
|
from_version: options.currentVersion,
|
|
@@ -3905,7 +4533,7 @@ var xml = (value) => value.replaceAll("&", "&").replaceAll("<", "<").repl
|
|
|
3905
4533
|
var systemdArgument = (value) => `"${value.replaceAll("\\", "\\\\").replaceAll('"', "\\\"")}"`;
|
|
3906
4534
|
var deviceServicePlan = (options) => {
|
|
3907
4535
|
if (options.platform === "linux") {
|
|
3908
|
-
const path =
|
|
4536
|
+
const path = join5(options.home, ".config", "systemd", "user", "cohall-device.service");
|
|
3909
4537
|
return {
|
|
3910
4538
|
file: {
|
|
3911
4539
|
path,
|
|
@@ -3938,7 +4566,7 @@ WantedBy=default.target
|
|
|
3938
4566
|
}
|
|
3939
4567
|
if (options.platform === "darwin") {
|
|
3940
4568
|
const label = "com.cohall.device";
|
|
3941
|
-
const path =
|
|
4569
|
+
const path = join5(options.home, "Library", "LaunchAgents", `${label}.plist`);
|
|
3942
4570
|
const domain = `gui/${options.uid ?? 0}`;
|
|
3943
4571
|
return {
|
|
3944
4572
|
file: {
|
|
@@ -4009,10 +4637,10 @@ var checked2 = async (runner, command, arguments_, allowFailure = false) => {
|
|
|
4009
4637
|
var installDeviceService = async (options = {}) => {
|
|
4010
4638
|
const entrypoint = await realpath3(options.entrypoint ?? process.argv[1] ?? "");
|
|
4011
4639
|
packageInstallation(entrypoint);
|
|
4012
|
-
const
|
|
4640
|
+
const platform4 = options.platform ?? process.platform;
|
|
4013
4641
|
const runner = options.runner ?? defaultRunner2;
|
|
4014
|
-
if (
|
|
4015
|
-
const script =
|
|
4642
|
+
if (platform4 === "win32") {
|
|
4643
|
+
const script = join5(dirname4(dirname4(entrypoint)), "deploy", "windows", "install-device.ps1");
|
|
4016
4644
|
await checked2(runner, "powershell.exe", [
|
|
4017
4645
|
"-NoProfile",
|
|
4018
4646
|
"-ExecutionPolicy",
|
|
@@ -4024,16 +4652,16 @@ var installDeviceService = async (options = {}) => {
|
|
|
4024
4652
|
}
|
|
4025
4653
|
const uid = options.uid ?? process.getuid?.();
|
|
4026
4654
|
const plan = deviceServicePlan({
|
|
4027
|
-
platform:
|
|
4655
|
+
platform: platform4,
|
|
4028
4656
|
entrypoint,
|
|
4029
4657
|
home: options.home ?? homedir3(),
|
|
4030
4658
|
...uid === undefined ? {} : { uid }
|
|
4031
4659
|
});
|
|
4032
|
-
await
|
|
4660
|
+
await mkdir4(dirname4(plan.file.path), { recursive: true, mode: 448 });
|
|
4033
4661
|
await writeFile4(plan.file.path, plan.file.content, { mode: plan.file.mode });
|
|
4034
|
-
await
|
|
4662
|
+
await chmod5(plan.file.path, plan.file.mode);
|
|
4035
4663
|
for (const [index, command] of plan.commands.entries()) {
|
|
4036
|
-
await checked2(runner, command.command, command.arguments,
|
|
4664
|
+
await checked2(runner, command.command, command.arguments, platform4 === "darwin" && index === 0);
|
|
4037
4665
|
}
|
|
4038
4666
|
return {
|
|
4039
4667
|
installed: plan.file.path,
|
|
@@ -4041,9 +4669,9 @@ var installDeviceService = async (options = {}) => {
|
|
|
4041
4669
|
};
|
|
4042
4670
|
};
|
|
4043
4671
|
var restartDeviceService = async (options = {}) => {
|
|
4044
|
-
const
|
|
4672
|
+
const platform4 = options.platform ?? process.platform;
|
|
4045
4673
|
const runner = options.runner ?? defaultRunner2;
|
|
4046
|
-
if (
|
|
4674
|
+
if (platform4 === "linux") {
|
|
4047
4675
|
const service = "cohall-device.service";
|
|
4048
4676
|
const check = await runner.run("systemctl", ["--user", "is-active", "--quiet", service], 1e4);
|
|
4049
4677
|
if (check.exitCode !== 0) {
|
|
@@ -4052,7 +4680,7 @@ var restartDeviceService = async (options = {}) => {
|
|
|
4052
4680
|
await checked2(runner, "systemctl", ["--user", "restart", service]);
|
|
4053
4681
|
return { running: true, restarted: true, service };
|
|
4054
4682
|
}
|
|
4055
|
-
if (
|
|
4683
|
+
if (platform4 === "darwin") {
|
|
4056
4684
|
const service = "com.cohall.device";
|
|
4057
4685
|
const target = `gui/${options.uid ?? process.getuid?.() ?? 0}/${service}`;
|
|
4058
4686
|
const check = await runner.run("launchctl", ["print", target], 1e4);
|
|
@@ -4062,7 +4690,7 @@ var restartDeviceService = async (options = {}) => {
|
|
|
4062
4690
|
await checked2(runner, "launchctl", ["kickstart", "-k", target]);
|
|
4063
4691
|
return { running: true, restarted: true, service };
|
|
4064
4692
|
}
|
|
4065
|
-
if (
|
|
4693
|
+
if (platform4 === "win32") {
|
|
4066
4694
|
const service = "Cohall Device";
|
|
4067
4695
|
const check = await runner.run("schtasks.exe", ["/Query", "/TN", service], 1e4);
|
|
4068
4696
|
if (check.exitCode !== 0) {
|
|
@@ -4076,10 +4704,10 @@ var restartDeviceService = async (options = {}) => {
|
|
|
4076
4704
|
};
|
|
4077
4705
|
|
|
4078
4706
|
// apps/device/src/relay-migration.ts
|
|
4079
|
-
var sha256 =
|
|
4080
|
-
var RelayBackupManifest =
|
|
4081
|
-
formatVersion:
|
|
4082
|
-
cohallVersion:
|
|
4707
|
+
var sha256 = Schema9.String.check(Schema9.isPattern(/^[a-f0-9]{64}$/, { expected: "a lowercase SHA-256 digest" }));
|
|
4708
|
+
var RelayBackupManifest = Schema9.Struct({
|
|
4709
|
+
formatVersion: Schema9.Literal(1),
|
|
4710
|
+
cohallVersion: Schema9.NonEmptyString,
|
|
4083
4711
|
createdAt: Timestamp,
|
|
4084
4712
|
databaseSha256: sha256,
|
|
4085
4713
|
ownerTokenSha256: sha256
|
|
@@ -4103,27 +4731,27 @@ var verifyBackupMember = async (path, maximumBytes) => {
|
|
|
4103
4731
|
};
|
|
4104
4732
|
var readManifest = async (path) => {
|
|
4105
4733
|
await verifyBackupMember(path, 16384);
|
|
4106
|
-
return
|
|
4734
|
+
return Schema9.decodeUnknownSync(RelayBackupManifest)(JSON.parse(await readFile3(path, "utf8")));
|
|
4107
4735
|
};
|
|
4108
4736
|
var verifyBackupDestinationParent = async (path) => {
|
|
4109
4737
|
if (process.platform === "win32") {
|
|
4110
4738
|
return;
|
|
4111
4739
|
}
|
|
4112
|
-
for (let current = await realpath4(path);; current =
|
|
4740
|
+
for (let current = await realpath4(path);; current = dirname5(current)) {
|
|
4113
4741
|
const metadata = await stat2(current);
|
|
4114
4742
|
const writableByOthers = (metadata.mode & 18) !== 0;
|
|
4115
4743
|
const sticky = (metadata.mode & 512) !== 0;
|
|
4116
4744
|
if (writableByOthers && !sticky) {
|
|
4117
4745
|
throw new Error(`Backup destination ancestors must not be group- or world-writable unless they use the sticky bit: ${current}`);
|
|
4118
4746
|
}
|
|
4119
|
-
const parent =
|
|
4747
|
+
const parent = dirname5(current);
|
|
4120
4748
|
if (parent === current) {
|
|
4121
4749
|
return;
|
|
4122
4750
|
}
|
|
4123
4751
|
}
|
|
4124
4752
|
};
|
|
4125
4753
|
var ownerToken = async (dataDirectory) => {
|
|
4126
|
-
const path =
|
|
4754
|
+
const path = join6(dataDirectory, "owner-token");
|
|
4127
4755
|
const configured = process.env.COHALL_TOKEN;
|
|
4128
4756
|
if (configured !== undefined) {
|
|
4129
4757
|
return configured;
|
|
@@ -4149,7 +4777,7 @@ var verifyDatabase = (path) => {
|
|
|
4149
4777
|
};
|
|
4150
4778
|
var backupRelay = async (destinationInput) => {
|
|
4151
4779
|
const sourceDirectory = relayDataDirectory();
|
|
4152
|
-
const sourceDatabase =
|
|
4780
|
+
const sourceDatabase = join6(sourceDirectory, "cohall.db");
|
|
4153
4781
|
if (!await exists(sourceDatabase)) {
|
|
4154
4782
|
throw new Error(`Relay database was not found at ${sourceDatabase}`);
|
|
4155
4783
|
}
|
|
@@ -4157,11 +4785,11 @@ var backupRelay = async (destinationInput) => {
|
|
|
4157
4785
|
if (await exists(destination)) {
|
|
4158
4786
|
throw new Error(`Backup destination already exists: ${destination}`);
|
|
4159
4787
|
}
|
|
4160
|
-
await verifyBackupDestinationParent(
|
|
4161
|
-
await
|
|
4788
|
+
await verifyBackupDestinationParent(dirname5(destination));
|
|
4789
|
+
await mkdir5(destination, { mode: 448 });
|
|
4162
4790
|
try {
|
|
4163
|
-
const databasePath =
|
|
4164
|
-
const ownerTokenPath =
|
|
4791
|
+
const databasePath = join6(destination, "cohall.db");
|
|
4792
|
+
const ownerTokenPath = join6(destination, "owner-token");
|
|
4165
4793
|
const database = new DatabaseSync(sourceDatabase, { readOnly: true });
|
|
4166
4794
|
try {
|
|
4167
4795
|
await sqliteBackup(database, databasePath);
|
|
@@ -4175,7 +4803,7 @@ var backupRelay = async (destinationInput) => {
|
|
|
4175
4803
|
}
|
|
4176
4804
|
await writeFile5(ownerTokenPath, `${token}
|
|
4177
4805
|
`, { mode: 384 });
|
|
4178
|
-
await Promise.all([
|
|
4806
|
+
await Promise.all([chmod6(databasePath, 384), chmod6(ownerTokenPath, 384)]);
|
|
4179
4807
|
const createdAt = Timestamp.make(new Date().toISOString());
|
|
4180
4808
|
const manifest = RelayBackupManifest.make({
|
|
4181
4809
|
formatVersion: 1,
|
|
@@ -4184,7 +4812,7 @@ var backupRelay = async (destinationInput) => {
|
|
|
4184
4812
|
databaseSha256: await digest(databasePath),
|
|
4185
4813
|
ownerTokenSha256: await digest(ownerTokenPath)
|
|
4186
4814
|
});
|
|
4187
|
-
await writeFile5(
|
|
4815
|
+
await writeFile5(join6(destination, "manifest.json"), `${JSON.stringify(manifest, null, 2)}
|
|
4188
4816
|
`, {
|
|
4189
4817
|
mode: 384
|
|
4190
4818
|
});
|
|
@@ -4201,9 +4829,9 @@ var backupRelay = async (destinationInput) => {
|
|
|
4201
4829
|
};
|
|
4202
4830
|
var restoreRelay = async (sourceInput) => {
|
|
4203
4831
|
const source = resolve2(sourceInput);
|
|
4204
|
-
const sourceDatabase =
|
|
4205
|
-
const sourceOwnerToken =
|
|
4206
|
-
const sourceManifest =
|
|
4832
|
+
const sourceDatabase = join6(source, "cohall.db");
|
|
4833
|
+
const sourceOwnerToken = join6(source, "owner-token");
|
|
4834
|
+
const sourceManifest = join6(source, "manifest.json");
|
|
4207
4835
|
for (const path of [sourceDatabase, sourceOwnerToken, sourceManifest]) {
|
|
4208
4836
|
if (!await exists(path)) {
|
|
4209
4837
|
throw new Error(`Relay backup is missing ${basename(path)}`);
|
|
@@ -4218,24 +4846,24 @@ var restoreRelay = async (sourceInput) => {
|
|
|
4218
4846
|
if (await exists(target)) {
|
|
4219
4847
|
throw new Error(`Relay data directory already exists: ${target}. Restore into a new COHALL_DATA_DIR.`);
|
|
4220
4848
|
}
|
|
4221
|
-
const parent =
|
|
4222
|
-
await
|
|
4223
|
-
const staging = await mkdtemp2(
|
|
4849
|
+
const parent = dirname5(target);
|
|
4850
|
+
await mkdir5(parent, { recursive: true, mode: 448 });
|
|
4851
|
+
const staging = await mkdtemp2(join6(parent, `.${basename(target)}.restore-`));
|
|
4224
4852
|
let backupVersion = "";
|
|
4225
4853
|
try {
|
|
4226
|
-
await
|
|
4227
|
-
const databasePath =
|
|
4228
|
-
const ownerTokenPath =
|
|
4229
|
-
const manifestPath =
|
|
4854
|
+
await chmod6(staging, 448);
|
|
4855
|
+
const databasePath = join6(staging, "cohall.db");
|
|
4856
|
+
const ownerTokenPath = join6(staging, "owner-token");
|
|
4857
|
+
const manifestPath = join6(staging, "migration-manifest.json");
|
|
4230
4858
|
await Promise.all([
|
|
4231
4859
|
copyFile(sourceDatabase, databasePath),
|
|
4232
4860
|
copyFile(sourceOwnerToken, ownerTokenPath),
|
|
4233
4861
|
copyFile(sourceManifest, manifestPath)
|
|
4234
4862
|
]);
|
|
4235
4863
|
await Promise.all([
|
|
4236
|
-
|
|
4237
|
-
|
|
4238
|
-
|
|
4864
|
+
chmod6(databasePath, 384),
|
|
4865
|
+
chmod6(ownerTokenPath, 384),
|
|
4866
|
+
chmod6(manifestPath, 384)
|
|
4239
4867
|
]);
|
|
4240
4868
|
const manifest = await readManifest(manifestPath);
|
|
4241
4869
|
if (await digest(databasePath) !== manifest.databaseSha256 || await digest(ownerTokenPath) !== manifest.ownerTokenSha256) {
|
|
@@ -4267,7 +4895,7 @@ var websocketUrl = (relayUrl) => {
|
|
|
4267
4895
|
};
|
|
4268
4896
|
var isLoopback = (url) => url.hostname === "localhost" || url.hostname === "[::1]" || /^127(?:\.\d{1,3}){3}$/.test(url.hostname);
|
|
4269
4897
|
var verifyClientCredential = async (relayUrl, token) => {
|
|
4270
|
-
await
|
|
4898
|
+
await Effect8.runPromise(exports_src.make({ baseUrl: relayUrl, token }).devices());
|
|
4271
4899
|
};
|
|
4272
4900
|
var verifyDeviceCredential = (relayUrl, token) => new Promise((resolvePromise, reject) => {
|
|
4273
4901
|
const socket = new WebSocket(websocketUrl(relayUrl), {
|
|
@@ -4295,7 +4923,7 @@ var verifyDeviceCredential = (relayUrl, token) => new Promise((resolvePromise, r
|
|
|
4295
4923
|
});
|
|
4296
4924
|
socket.on("message", (data) => {
|
|
4297
4925
|
try {
|
|
4298
|
-
const event =
|
|
4926
|
+
const event = Schema9.decodeUnknownSync(SocketEvent)(JSON.parse(data.toString()));
|
|
4299
4927
|
if (event._tag === "Connected") {
|
|
4300
4928
|
finish();
|
|
4301
4929
|
} else if (event._tag === "Error") {
|
|
@@ -4364,9 +4992,14 @@ var switchRelay = async (options) => {
|
|
|
4364
4992
|
var valueOptions = new Set([
|
|
4365
4993
|
"context",
|
|
4366
4994
|
"context-file",
|
|
4995
|
+
"grok-gateway",
|
|
4996
|
+
"error",
|
|
4367
4997
|
"label",
|
|
4368
4998
|
"model",
|
|
4999
|
+
"message",
|
|
5000
|
+
"message-file",
|
|
4369
5001
|
"name",
|
|
5002
|
+
"parent",
|
|
4370
5003
|
"prompt",
|
|
4371
5004
|
"prompt-file",
|
|
4372
5005
|
"provider",
|
|
@@ -4404,15 +5037,19 @@ Usage:
|
|
|
4404
5037
|
cohall join --relay <url> --workspace <path> [--providers list] [--token-file <path>]
|
|
4405
5038
|
cohall configure [--relay url] [--name name] [--workspace path]
|
|
4406
5039
|
[--providers codex,opencode|auto] [--model id]
|
|
5040
|
+
[--grok-gateway path]
|
|
4407
5041
|
cohall config
|
|
4408
5042
|
cohall devices
|
|
4409
|
-
cohall
|
|
4410
|
-
|
|
5043
|
+
cohall bots
|
|
5044
|
+
cohall send [@device-or-bot] [prompt] [delegate options]
|
|
5045
|
+
cohall delegate [prompt] [--target @device-or-bot] [--provider provider]
|
|
5046
|
+
[--context text] [--thread uuid] [--parent task-id] [--workspace path]
|
|
4411
5047
|
[--timeout seconds] [--no-wait]
|
|
4412
5048
|
cohall status <task-id>
|
|
4413
5049
|
cohall trace <task-id> [--follow]
|
|
4414
5050
|
cohall wait <task-id> [--timeout seconds]
|
|
4415
5051
|
cohall cancel <task-id>
|
|
5052
|
+
cohall reply <task-id> --message-file path | --message text | --message - | --error text
|
|
4416
5053
|
cohall thread <thread-id>
|
|
4417
5054
|
cohall pair [--label name] [--client-only]
|
|
4418
5055
|
cohall sessions
|
|
@@ -4434,7 +5071,7 @@ Usage:
|
|
|
4434
5071
|
cohall skill [install [agents|claude|opencode|all]]
|
|
4435
5072
|
cohall integrations
|
|
4436
5073
|
|
|
4437
|
-
Providers: codex, claude-code, opencode.
|
|
5074
|
+
Providers: codex, claude-code, opencode, grok-bot.
|
|
4438
5075
|
Configuration is stored per user; environment variables override it.`;
|
|
4439
5076
|
var parseArguments = (values) => {
|
|
4440
5077
|
const options = new Map;
|
|
@@ -4520,6 +5157,7 @@ var printLine = (value) => console.log(JSON.stringify(value));
|
|
|
4520
5157
|
var readInput = async (arguments_, name) => {
|
|
4521
5158
|
const direct = option(arguments_, name);
|
|
4522
5159
|
const path = option(arguments_, `${name}-file`);
|
|
5160
|
+
const byteLimit = name === "message" ? 4 * 131072 : 131072;
|
|
4523
5161
|
if (direct !== undefined && path !== undefined) {
|
|
4524
5162
|
throw new Error(`Use either --${name} or --${name}-file, not both`);
|
|
4525
5163
|
}
|
|
@@ -4527,14 +5165,13 @@ var readInput = async (arguments_, name) => {
|
|
|
4527
5165
|
if (!await access4(path).then(() => true).catch(() => false)) {
|
|
4528
5166
|
throw new Error(`File does not exist: ${path}`);
|
|
4529
5167
|
}
|
|
4530
|
-
if ((await stat3(path)).size >
|
|
4531
|
-
throw new Error(`${name} file exceeds
|
|
5168
|
+
if ((await stat3(path)).size > byteLimit) {
|
|
5169
|
+
throw new Error(`${name} file exceeds ${byteLimit / 1024} KiB`);
|
|
4532
5170
|
}
|
|
4533
|
-
return readFile4(path, "utf8");
|
|
4534
5171
|
}
|
|
4535
|
-
const result = direct === "-" ? await readStdin(
|
|
5172
|
+
const result = path === undefined ? direct === "-" ? await readStdin(byteLimit, name) : direct : await readFile4(path, "utf8");
|
|
4536
5173
|
if (result !== undefined && result.length > 131072) {
|
|
4537
|
-
throw new Error(`${name} exceeds
|
|
5174
|
+
throw new Error(`${name} exceeds 131072 characters`);
|
|
4538
5175
|
}
|
|
4539
5176
|
return result;
|
|
4540
5177
|
};
|
|
@@ -4573,14 +5210,14 @@ var pairingToken = async (arguments_) => {
|
|
|
4573
5210
|
return token;
|
|
4574
5211
|
};
|
|
4575
5212
|
var client = async () => {
|
|
4576
|
-
const configuration = await
|
|
5213
|
+
const configuration = await Effect9.runPromise(loadClientConfiguration);
|
|
4577
5214
|
return {
|
|
4578
5215
|
configuration,
|
|
4579
5216
|
relay: exports_src.make({ baseUrl: configuration.relayUrl, token: configuration.token })
|
|
4580
5217
|
};
|
|
4581
5218
|
};
|
|
4582
5219
|
var ownerClient = async () => {
|
|
4583
|
-
const configuration = await
|
|
5220
|
+
const configuration = await Effect9.runPromise(loadOwnerConfiguration);
|
|
4584
5221
|
return {
|
|
4585
5222
|
configuration,
|
|
4586
5223
|
relay: exports_src.make({ baseUrl: configuration.relayUrl, token: configuration.token })
|
|
@@ -4588,16 +5225,16 @@ var ownerClient = async () => {
|
|
|
4588
5225
|
};
|
|
4589
5226
|
var installSkill = async (target) => {
|
|
4590
5227
|
const roots = {
|
|
4591
|
-
agents:
|
|
4592
|
-
claude:
|
|
4593
|
-
opencode:
|
|
5228
|
+
agents: join7(homedir4(), ".agents", "skills", "cohall", "SKILL.md"),
|
|
5229
|
+
claude: join7(homedir4(), ".claude", "skills", "cohall", "SKILL.md"),
|
|
5230
|
+
opencode: join7(homedir4(), ".config", "opencode", "skills", "cohall", "SKILL.md")
|
|
4594
5231
|
};
|
|
4595
5232
|
const selected = target === "all" ? Object.values(roots) : (target in roots) ? [roots[target]] : undefined;
|
|
4596
5233
|
if (selected === undefined) {
|
|
4597
5234
|
throw new Error("Skill target must be agents, claude, opencode, or all");
|
|
4598
5235
|
}
|
|
4599
5236
|
for (const path of selected) {
|
|
4600
|
-
await
|
|
5237
|
+
await mkdir6(dirname6(path), { recursive: true });
|
|
4601
5238
|
await writeFile6(path, SKILL_default);
|
|
4602
5239
|
}
|
|
4603
5240
|
return selected;
|
|
@@ -4606,6 +5243,21 @@ var printHelp = () => console.log(help);
|
|
|
4606
5243
|
var printSkill = () => console.log(SKILL_default.trimEnd());
|
|
4607
5244
|
var runCli = async (command, raw) => {
|
|
4608
5245
|
const arguments_ = parseArguments(raw);
|
|
5246
|
+
if (command === "reply") {
|
|
5247
|
+
allowOptions(arguments_, ["message", "message-file", "error"]);
|
|
5248
|
+
const id = Schema10.decodeUnknownSync(TaskId)(identifier(arguments_, "task id"));
|
|
5249
|
+
const error = option(arguments_, "error");
|
|
5250
|
+
if (error !== undefined && (option(arguments_, "message") !== undefined || option(arguments_, "message-file") !== undefined)) {
|
|
5251
|
+
throw new Error("Use either --error or --message/--message-file, not both");
|
|
5252
|
+
}
|
|
5253
|
+
const message = error === undefined ? await readInput(arguments_, "message") : undefined;
|
|
5254
|
+
if (error === undefined && message === undefined) {
|
|
5255
|
+
throw new Error("A bot reply requires --message, --message-file, or --error");
|
|
5256
|
+
}
|
|
5257
|
+
await writeBotReply(id, error === undefined ? { result: message ?? "" } : { error });
|
|
5258
|
+
print({ submitted: true, task_id: id });
|
|
5259
|
+
return;
|
|
5260
|
+
}
|
|
4609
5261
|
if (command === "init") {
|
|
4610
5262
|
allowOptions(arguments_, [
|
|
4611
5263
|
"client-only",
|
|
@@ -4731,7 +5383,7 @@ var runCli = async (command, raw) => {
|
|
|
4731
5383
|
const { relay: relay2 } = await ownerClient();
|
|
4732
5384
|
const restart = !arguments_.options.has("no-restart");
|
|
4733
5385
|
if (arguments_.options.has("dry-run")) {
|
|
4734
|
-
const devices = await
|
|
5386
|
+
const devices = await Effect9.runPromise(relay2.devices());
|
|
4735
5387
|
assertDeviceOperationSupport(devices);
|
|
4736
5388
|
print({
|
|
4737
5389
|
dry_run: true,
|
|
@@ -4741,7 +5393,7 @@ var runCli = async (command, raw) => {
|
|
|
4741
5393
|
});
|
|
4742
5394
|
return;
|
|
4743
5395
|
}
|
|
4744
|
-
const operations = await
|
|
5396
|
+
const operations = await Effect9.runPromise(relay2.createUpgradeOperations({ target: normalizedTarget, restart }));
|
|
4745
5397
|
print({
|
|
4746
5398
|
queued: operations.length,
|
|
4747
5399
|
requested_version: normalizedTarget,
|
|
@@ -4767,10 +5419,10 @@ var runCli = async (command, raw) => {
|
|
|
4767
5419
|
if (arguments_.positionals.length !== 2 || arguments_.positionals[0] !== "abandon") {
|
|
4768
5420
|
throw new Error("Usage: cohall upgrades [abandon <operation-id>]");
|
|
4769
5421
|
}
|
|
4770
|
-
print(await
|
|
5422
|
+
print(await Effect9.runPromise(relay2.abandonOperation(Schema10.decodeUnknownSync(OperationId)(arguments_.positionals[1]))));
|
|
4771
5423
|
return;
|
|
4772
5424
|
}
|
|
4773
|
-
print(await
|
|
5425
|
+
print(await Effect9.runPromise(relay2.operations()));
|
|
4774
5426
|
return;
|
|
4775
5427
|
}
|
|
4776
5428
|
if (command === "join") {
|
|
@@ -4829,20 +5481,31 @@ var runCli = async (command, raw) => {
|
|
|
4829
5481
|
device_credential: configuration2.deviceToken !== undefined,
|
|
4830
5482
|
providers: configuration2.providers ?? "auto",
|
|
4831
5483
|
model: configuration2.model,
|
|
4832
|
-
sandbox: configuration2.sandbox
|
|
5484
|
+
sandbox: configuration2.sandbox,
|
|
5485
|
+
grok_gateway: configuration2.grokGateway
|
|
4833
5486
|
}
|
|
4834
5487
|
});
|
|
4835
5488
|
return;
|
|
4836
5489
|
}
|
|
4837
5490
|
if (command === "configure") {
|
|
4838
|
-
allowOptions(arguments_, [
|
|
5491
|
+
allowOptions(arguments_, [
|
|
5492
|
+
"grok-gateway",
|
|
5493
|
+
"model",
|
|
5494
|
+
"name",
|
|
5495
|
+
"providers",
|
|
5496
|
+
"relay",
|
|
5497
|
+
"sandbox",
|
|
5498
|
+
"workspace"
|
|
5499
|
+
]);
|
|
4839
5500
|
noPositionals(arguments_, command);
|
|
4840
5501
|
const existing = await readStoredConfiguration();
|
|
4841
5502
|
const relayUrl = normalizeRelayUrl(option(arguments_, "relay") ?? existing?.relayUrl ?? "http://127.0.0.1:8787");
|
|
4842
5503
|
const supplied = values(arguments_, "workspace");
|
|
4843
|
-
const workspaces = supplied.length === 0 ? existing?.workspaces ?? [] : await
|
|
5504
|
+
const workspaces = supplied.length === 0 ? existing?.workspaces ?? [] : await Effect9.runPromise(parseWorkspaces("", JSON.stringify(supplied)));
|
|
4844
5505
|
const sandbox = option(arguments_, "sandbox");
|
|
4845
5506
|
const model = option(arguments_, "model") ?? existing?.model;
|
|
5507
|
+
const gatewayInput = option(arguments_, "grok-gateway") ?? existing?.grokGateway;
|
|
5508
|
+
const grokGateway = gatewayInput === undefined ? undefined : resolve3(gatewayInput);
|
|
4846
5509
|
const providerInput = option(arguments_, "providers");
|
|
4847
5510
|
const providers = providerInput === undefined ? existing?.providers : providerInput === "auto" ? undefined : parseProviders(providerInput);
|
|
4848
5511
|
const configuration2 = StoredConfiguration.make({
|
|
@@ -4854,8 +5517,9 @@ var runCli = async (command, raw) => {
|
|
|
4854
5517
|
...credentialsForRelay(existing, relayUrl),
|
|
4855
5518
|
...providers === undefined ? {} : { providers },
|
|
4856
5519
|
...model === undefined ? {} : { model },
|
|
5520
|
+
...grokGateway === undefined ? {} : { grokGateway },
|
|
4857
5521
|
...(sandbox ?? existing?.sandbox) === undefined ? {} : {
|
|
4858
|
-
sandbox:
|
|
5522
|
+
sandbox: Schema10.decodeUnknownSync(Schema10.Literals(["read-only", "workspace-write", "danger-full-access"]))(sandbox ?? existing?.sandbox)
|
|
4859
5523
|
}
|
|
4860
5524
|
});
|
|
4861
5525
|
await writeStoredConfiguration(configuration2);
|
|
@@ -4898,7 +5562,7 @@ var runCli = async (command, raw) => {
|
|
|
4898
5562
|
noPositionals(arguments_, command);
|
|
4899
5563
|
if (arguments_.options.has("all")) {
|
|
4900
5564
|
const { relay: relay2 } = await client();
|
|
4901
|
-
print(allDeviceHealth(await
|
|
5565
|
+
print(allDeviceHealth(await Effect9.runPromise(relay2.devices()), version));
|
|
4902
5566
|
return;
|
|
4903
5567
|
}
|
|
4904
5568
|
const configuration2 = await readStoredConfiguration();
|
|
@@ -4911,14 +5575,18 @@ var runCli = async (command, raw) => {
|
|
|
4911
5575
|
});
|
|
4912
5576
|
const providerInput = process.env.COHALL_DEVICE_PROVIDERS;
|
|
4913
5577
|
const selectedProviders = providerInput === undefined ? configuration2?.providers : providerInput === "auto" ? undefined : parseProviders(providerInput);
|
|
4914
|
-
const providerExecutables = Object.fromEntries(Provider.literals.map((provider) => [
|
|
5578
|
+
const providerExecutables = Object.fromEntries(Provider.literals.filter((provider) => provider !== "grok-bot").map((provider) => [
|
|
4915
5579
|
provider,
|
|
4916
5580
|
findExecutable(provider === "claude-code" ? "claude" : provider) ?? "not found"
|
|
4917
5581
|
]));
|
|
5582
|
+
const grokGateway = process.env.COHALL_GROK_GATEWAY ?? configuration2?.grokGateway;
|
|
5583
|
+
const grokBots = grokGateway === undefined ? undefined : await discoverGrokBots(grokGateway).catch(() => {
|
|
5584
|
+
return;
|
|
5585
|
+
});
|
|
4918
5586
|
const clientToken = process.env.COHALL_CLIENT_TOKEN ?? storedCredentials.clientToken;
|
|
4919
5587
|
const hasDeviceCredential = process.env.COHALL_DEVICE_TOKEN !== undefined || storedCredentials.deviceToken !== undefined;
|
|
4920
5588
|
const deviceId = process.env.COHALL_DEVICE_ID ?? configuration2?.deviceId;
|
|
4921
|
-
const currentDevice = response?.ok !== true || clientToken === undefined || deviceId === undefined ? undefined : await
|
|
5589
|
+
const currentDevice = response?.ok !== true || clientToken === undefined || deviceId === undefined ? undefined : await Effect9.runPromise(exports_src.make({ baseUrl: relayUrl, token: clientToken }).devices()).then((devices) => devices.find((device) => device.id === deviceId)).catch(() => {
|
|
4922
5590
|
return;
|
|
4923
5591
|
});
|
|
4924
5592
|
const versionWarning = deviceVersionWarning(version, currentDevice?.version);
|
|
@@ -4928,7 +5596,9 @@ var runCli = async (command, raw) => {
|
|
|
4928
5596
|
...hasDeviceCredential && response?.ok === true && currentDevice?.status !== "online" && currentDevice?.status !== "busy" ? [
|
|
4929
5597
|
clientToken === undefined ? "Device status cannot be checked without a client credential" : "Device daemon is not connected or is not registered with the relay"
|
|
4930
5598
|
] : [],
|
|
4931
|
-
...selectedProviders?.flatMap((provider) =>
|
|
5599
|
+
...selectedProviders?.flatMap((provider) => provider === "grok-bot" ? grokBots === undefined ? [
|
|
5600
|
+
"Configured Grok Bot gateway is unavailable; check its discovery file and host process"
|
|
5601
|
+
] : [] : providerExecutables[provider] === "not found" ? [`Configured provider ${provider} is not installed or not on PATH`] : []) ?? []
|
|
4932
5602
|
];
|
|
4933
5603
|
print({
|
|
4934
5604
|
version,
|
|
@@ -4939,6 +5609,13 @@ var runCli = async (command, raw) => {
|
|
|
4939
5609
|
device_credential: hasDeviceCredential,
|
|
4940
5610
|
workspaces: configuration2?.workspaces ?? [],
|
|
4941
5611
|
providers: providerExecutables,
|
|
5612
|
+
...grokGateway === undefined ? {} : {
|
|
5613
|
+
grok_gateway: {
|
|
5614
|
+
path: grokGateway,
|
|
5615
|
+
reachable: grokBots !== undefined,
|
|
5616
|
+
bots: grokBots?.length ?? 0
|
|
5617
|
+
}
|
|
5618
|
+
},
|
|
4942
5619
|
provider_selection: selectedProviders ?? "auto",
|
|
4943
5620
|
provider_authentication: "checked when delegated work starts",
|
|
4944
5621
|
device_status: hasDeviceCredential === false ? "not configured" : currentDevice?.status ?? "unknown",
|
|
@@ -4951,7 +5628,7 @@ var runCli = async (command, raw) => {
|
|
|
4951
5628
|
allowOptions(arguments_, []);
|
|
4952
5629
|
noPositionals(arguments_, command);
|
|
4953
5630
|
const { relay: relay2 } = await client();
|
|
4954
|
-
print(deviceVersions(await
|
|
5631
|
+
print(deviceVersions(await Effect9.runPromise(relay2.devices()), version));
|
|
4955
5632
|
return;
|
|
4956
5633
|
}
|
|
4957
5634
|
if (command === "usage") {
|
|
@@ -4959,7 +5636,7 @@ var runCli = async (command, raw) => {
|
|
|
4959
5636
|
noPositionals(arguments_, command);
|
|
4960
5637
|
const { relay: relay2 } = await client();
|
|
4961
5638
|
print({
|
|
4962
|
-
...await
|
|
5639
|
+
...await Effect9.runPromise(relay2.usage()),
|
|
4963
5640
|
scope: "Retained Cohall task activity; provider token and billing usage are not available."
|
|
4964
5641
|
});
|
|
4965
5642
|
return;
|
|
@@ -4970,7 +5647,7 @@ var runCli = async (command, raw) => {
|
|
|
4970
5647
|
allowOptions(arguments_, ["client-only", "label"]);
|
|
4971
5648
|
noPositionals(arguments_, command);
|
|
4972
5649
|
const roles = arguments_.options.has("client-only") ? ["client"] : ["client", "device"];
|
|
4973
|
-
const credential = await
|
|
5650
|
+
const credential = await Effect9.runPromise(relay2.createPairing({ label: option(arguments_, "label") ?? "Cohall device", roles }));
|
|
4974
5651
|
print({
|
|
4975
5652
|
relay_url: configuration2.relayUrl,
|
|
4976
5653
|
pairing_token: credential.token,
|
|
@@ -4982,30 +5659,37 @@ var runCli = async (command, raw) => {
|
|
|
4982
5659
|
if (command === "sessions") {
|
|
4983
5660
|
allowOptions(arguments_, []);
|
|
4984
5661
|
noPositionals(arguments_, command);
|
|
4985
|
-
print(await
|
|
5662
|
+
print(await Effect9.runPromise(relay2.authSessions()));
|
|
4986
5663
|
return;
|
|
4987
5664
|
}
|
|
4988
5665
|
if (command === "forget") {
|
|
4989
5666
|
allowOptions(arguments_, []);
|
|
4990
|
-
print(await
|
|
5667
|
+
print(await Effect9.runPromise(relay2.forgetDevice(Schema10.decodeUnknownSync(DeviceId)(identifier(arguments_, "device id")))));
|
|
4991
5668
|
return;
|
|
4992
5669
|
}
|
|
4993
5670
|
allowOptions(arguments_, []);
|
|
4994
|
-
print(await
|
|
5671
|
+
print(await Effect9.runPromise(relay2.revokeAuthSession(Schema10.decodeUnknownSync(AuthSessionId)(identifier(arguments_, "session id")))));
|
|
4995
5672
|
return;
|
|
4996
5673
|
}
|
|
4997
5674
|
const { configuration, relay } = await client();
|
|
4998
5675
|
if (command === "devices") {
|
|
4999
5676
|
allowOptions(arguments_, []);
|
|
5000
5677
|
noPositionals(arguments_, command);
|
|
5001
|
-
print(await
|
|
5678
|
+
print(await Effect9.runPromise(relay.devices()));
|
|
5679
|
+
return;
|
|
5680
|
+
}
|
|
5681
|
+
if (command === "bots") {
|
|
5682
|
+
allowOptions(arguments_, []);
|
|
5683
|
+
noPositionals(arguments_, command);
|
|
5684
|
+
print(listBots(await Effect9.runPromise(relay.devices())));
|
|
5002
5685
|
return;
|
|
5003
5686
|
}
|
|
5004
|
-
if (command === "delegate") {
|
|
5687
|
+
if (command === "delegate" || command === "send") {
|
|
5005
5688
|
allowOptions(arguments_, [
|
|
5006
5689
|
"context",
|
|
5007
5690
|
"context-file",
|
|
5008
5691
|
"no-wait",
|
|
5692
|
+
"parent",
|
|
5009
5693
|
"prompt",
|
|
5010
5694
|
"prompt-file",
|
|
5011
5695
|
"provider",
|
|
@@ -5014,66 +5698,73 @@ var runCli = async (command, raw) => {
|
|
|
5014
5698
|
"timeout",
|
|
5015
5699
|
"workspace"
|
|
5016
5700
|
]);
|
|
5701
|
+
const positionalTarget = command === "send" && arguments_.positionals[0]?.startsWith("@") ? arguments_.positionals[0] : undefined;
|
|
5702
|
+
const positionals = positionalTarget === undefined ? arguments_.positionals : arguments_.positionals.slice(1);
|
|
5703
|
+
if (positionalTarget !== undefined && option(arguments_, "target") !== undefined) {
|
|
5704
|
+
throw new Error("Use either a positional target or --target, not both");
|
|
5705
|
+
}
|
|
5017
5706
|
if (option(arguments_, "prompt") === "-" && option(arguments_, "context") === "-") {
|
|
5018
5707
|
throw new Error("Only one of --prompt and --context may read from stdin");
|
|
5019
5708
|
}
|
|
5020
5709
|
const suppliedPrompt = await readInput(arguments_, "prompt");
|
|
5021
|
-
const prompt = suppliedPrompt ??
|
|
5710
|
+
const prompt = suppliedPrompt ?? positionals.join(" ");
|
|
5022
5711
|
if (prompt.trim().length === 0) {
|
|
5023
5712
|
throw new Error("A prompt is required");
|
|
5024
5713
|
}
|
|
5025
|
-
if (suppliedPrompt !== undefined &&
|
|
5714
|
+
if (suppliedPrompt !== undefined && positionals.length > 0) {
|
|
5026
5715
|
throw new Error("Use either a positional prompt or --prompt, not both");
|
|
5027
5716
|
}
|
|
5028
5717
|
const context = await readInput(arguments_, "context");
|
|
5029
|
-
const target = option(arguments_, "target");
|
|
5718
|
+
const target = positionalTarget ?? option(arguments_, "target");
|
|
5030
5719
|
const thread = option(arguments_, "thread");
|
|
5031
5720
|
const workspace = option(arguments_, "workspace");
|
|
5032
5721
|
const provider = option(arguments_, "provider");
|
|
5033
|
-
const
|
|
5722
|
+
const parent = option(arguments_, "parent");
|
|
5723
|
+
const task = await Effect9.runPromise(createDelegation(relay, configuration, {
|
|
5034
5724
|
prompt,
|
|
5035
5725
|
...target === undefined ? {} : { target },
|
|
5036
5726
|
...context === undefined ? {} : { context },
|
|
5037
|
-
...thread === undefined ? {} : { threadId:
|
|
5727
|
+
...thread === undefined ? {} : { threadId: Schema10.decodeUnknownSync(ThreadId)(thread) },
|
|
5728
|
+
...parent === undefined ? {} : { parentTaskId: Schema10.decodeUnknownSync(TaskId)(parent) },
|
|
5038
5729
|
...workspace === undefined ? {} : { workspace },
|
|
5039
|
-
...provider === undefined ? {} : { provider:
|
|
5730
|
+
...provider === undefined ? {} : { provider: Schema10.decodeUnknownSync(Provider)(provider) }
|
|
5040
5731
|
}));
|
|
5041
|
-
print(taskResult(arguments_.options.has("no-wait") ? task : await
|
|
5732
|
+
print(taskResult(arguments_.options.has("no-wait") ? task : await Effect9.runPromise(waitForTask(relay, task, timeout(arguments_)))));
|
|
5042
5733
|
return;
|
|
5043
5734
|
}
|
|
5044
5735
|
if (command === "status") {
|
|
5045
5736
|
allowOptions(arguments_, []);
|
|
5046
|
-
const id =
|
|
5047
|
-
print(taskResult(await
|
|
5737
|
+
const id = Schema10.decodeUnknownSync(TaskId)(identifier(arguments_, "task id"));
|
|
5738
|
+
print(taskResult(await Effect9.runPromise(relay.getTask(id))));
|
|
5048
5739
|
return;
|
|
5049
5740
|
}
|
|
5050
5741
|
if (command === "trace") {
|
|
5051
5742
|
allowOptions(arguments_, ["follow"]);
|
|
5052
|
-
const id =
|
|
5743
|
+
const id = Schema10.decodeUnknownSync(TaskId)(identifier(arguments_, "task id"));
|
|
5053
5744
|
if (arguments_.options.has("follow")) {
|
|
5054
|
-
await
|
|
5745
|
+
await Effect9.runPromise(followTaskTrace(relay, id, printLine));
|
|
5055
5746
|
return;
|
|
5056
5747
|
}
|
|
5057
|
-
print(await
|
|
5748
|
+
print(await Effect9.runPromise(relay.traceTask(id)));
|
|
5058
5749
|
return;
|
|
5059
5750
|
}
|
|
5060
5751
|
if (command === "wait") {
|
|
5061
5752
|
allowOptions(arguments_, ["timeout"]);
|
|
5062
|
-
const id =
|
|
5063
|
-
const task = await
|
|
5064
|
-
print(taskResult(await
|
|
5753
|
+
const id = Schema10.decodeUnknownSync(TaskId)(identifier(arguments_, "task id"));
|
|
5754
|
+
const task = await Effect9.runPromise(relay.getTask(id));
|
|
5755
|
+
print(taskResult(await Effect9.runPromise(waitForTask(relay, task, timeout(arguments_)))));
|
|
5065
5756
|
return;
|
|
5066
5757
|
}
|
|
5067
5758
|
if (command === "cancel") {
|
|
5068
5759
|
allowOptions(arguments_, []);
|
|
5069
|
-
const id =
|
|
5070
|
-
print(taskResult(await
|
|
5760
|
+
const id = Schema10.decodeUnknownSync(TaskId)(identifier(arguments_, "task id"));
|
|
5761
|
+
print(taskResult(await Effect9.runPromise(relay.cancelTask(id))));
|
|
5071
5762
|
return;
|
|
5072
5763
|
}
|
|
5073
5764
|
if (command === "thread") {
|
|
5074
5765
|
allowOptions(arguments_, []);
|
|
5075
|
-
const id =
|
|
5076
|
-
print(await
|
|
5766
|
+
const id = Schema10.decodeUnknownSync(ThreadId)(identifier(arguments_, "thread id"));
|
|
5767
|
+
print(await Effect9.runPromise(threadContext(relay, id)));
|
|
5077
5768
|
return;
|
|
5078
5769
|
}
|
|
5079
5770
|
throw new Error(`Unknown Cohall command: ${command}`);
|
|
@@ -5081,15 +5772,15 @@ var runCli = async (command, raw) => {
|
|
|
5081
5772
|
|
|
5082
5773
|
// apps/device/src/daemon.ts
|
|
5083
5774
|
init_src();
|
|
5084
|
-
import { Effect as
|
|
5775
|
+
import { Effect as Effect10, Schedule as Schedule3, Schema as Schema11 } from "effect";
|
|
5085
5776
|
import { constants as constants3 } from "node:fs";
|
|
5086
|
-
import { open, realpath as realpath5, stat as stat4 } from "node:fs/promises";
|
|
5087
|
-
import { arch, hostname as hostname3, platform as
|
|
5777
|
+
import { open as open3, realpath as realpath5, stat as stat4 } from "node:fs/promises";
|
|
5778
|
+
import { arch, hostname as hostname3, platform as platform4 } from "node:os";
|
|
5088
5779
|
import { basename as basename2, isAbsolute as isAbsolute3, relative as relative2 } from "node:path";
|
|
5089
5780
|
import { WebSocket as WebSocket2 } from "ws";
|
|
5090
5781
|
var maxQueuedRelayMessages = 8;
|
|
5091
5782
|
|
|
5092
|
-
class DeviceConnectionError extends
|
|
5783
|
+
class DeviceConnectionError extends Schema11.TaggedErrorClass()("Device.ConnectionError", { message: Schema11.String }) {
|
|
5093
5784
|
}
|
|
5094
5785
|
var socketUrl = (configuration) => {
|
|
5095
5786
|
const url = new URL(configuration.relayUrl);
|
|
@@ -5105,15 +5796,17 @@ var providerLabel = (provider) => {
|
|
|
5105
5796
|
return "Claude Code";
|
|
5106
5797
|
case "opencode":
|
|
5107
5798
|
return "OpenCode";
|
|
5799
|
+
case "grok-bot":
|
|
5800
|
+
return "Grok Bot";
|
|
5108
5801
|
}
|
|
5109
5802
|
};
|
|
5110
5803
|
var capabilities = (providers) => {
|
|
5111
5804
|
const values2 = providers.map((provider) => ({
|
|
5112
5805
|
id: provider,
|
|
5113
5806
|
label: providerLabel(provider),
|
|
5114
|
-
detail: `${providerLabel(provider)} executable detected; authentication is checked when work starts`
|
|
5807
|
+
detail: provider === "grok-bot" ? "Named bots through this computer's local Grok Bot gateway" : `${providerLabel(provider)} executable detected; authentication is checked when work starts`
|
|
5115
5808
|
}));
|
|
5116
|
-
if (findExecutable("google-chrome") !== undefined || findExecutable("chromium") !== undefined ||
|
|
5809
|
+
if (findExecutable("google-chrome") !== undefined || findExecutable("chromium") !== undefined || platform4() === "darwin") {
|
|
5117
5810
|
values2.push({ id: "browser-session", label: "Signed-in browser" });
|
|
5118
5811
|
}
|
|
5119
5812
|
if (findExecutable("xcodebuild") !== undefined) {
|
|
@@ -5125,10 +5818,13 @@ var capabilities = (providers) => {
|
|
|
5125
5818
|
return values2;
|
|
5126
5819
|
};
|
|
5127
5820
|
var selectProviders = (installed, configured) => configured === undefined ? installed : configured.filter((provider) => installed.includes(provider));
|
|
5128
|
-
var describeDevice = (configuration, status = "online") => {
|
|
5129
|
-
const operatingSystem2 =
|
|
5821
|
+
var describeDevice = (configuration, status = "online", bots = []) => {
|
|
5822
|
+
const operatingSystem2 = platform4();
|
|
5130
5823
|
const platformName = operatingSystem2 === "darwin" || operatingSystem2 === "linux" ? operatingSystem2 : operatingSystem2 === "win32" ? "windows" : "unknown";
|
|
5131
|
-
const installed =
|
|
5824
|
+
const installed = [
|
|
5825
|
+
...availableProviders(),
|
|
5826
|
+
...configuration.grokGateway === undefined ? [] : ["grok-bot"]
|
|
5827
|
+
];
|
|
5132
5828
|
const providers = selectProviders(installed, configuration.providers);
|
|
5133
5829
|
return Device.make({
|
|
5134
5830
|
id: configuration.id,
|
|
@@ -5138,6 +5834,7 @@ var describeDevice = (configuration, status = "online") => {
|
|
|
5138
5834
|
architecture: arch(),
|
|
5139
5835
|
status,
|
|
5140
5836
|
providers,
|
|
5837
|
+
...providers.includes("grok-bot") ? { bots } : {},
|
|
5141
5838
|
capabilities: capabilities(providers),
|
|
5142
5839
|
workspaces: configuration.workspaces.map((path) => ({
|
|
5143
5840
|
path,
|
|
@@ -5164,9 +5861,9 @@ var allowedWorkspace = async (configuration, requested) => {
|
|
|
5164
5861
|
};
|
|
5165
5862
|
var openAllowedWorkspace = async (configuration, requested) => {
|
|
5166
5863
|
const path = await allowedWorkspace(configuration, requested);
|
|
5167
|
-
const operatingSystem2 =
|
|
5864
|
+
const operatingSystem2 = platform4();
|
|
5168
5865
|
const flags = constants3.O_RDONLY | (operatingSystem2 === "win32" ? 0 : constants3.O_DIRECTORY | constants3.O_NOFOLLOW);
|
|
5169
|
-
const handle = await
|
|
5866
|
+
const handle = await open3(path, flags);
|
|
5170
5867
|
const identity = await handle.stat();
|
|
5171
5868
|
if (!identity.isDirectory()) {
|
|
5172
5869
|
await handle.close();
|
|
@@ -5249,15 +5946,47 @@ var rememberOperation = (state, operationId) => {
|
|
|
5249
5946
|
}
|
|
5250
5947
|
};
|
|
5251
5948
|
var sessionKey = (task) => `${task.threadId}:${task.provider}`;
|
|
5949
|
+
var drain = (configuration, state) => {
|
|
5950
|
+
if (state.operation !== undefined) {
|
|
5951
|
+
return;
|
|
5952
|
+
}
|
|
5953
|
+
if (state.tasks.size === 0) {
|
|
5954
|
+
const operation = state.operationQueue.shift();
|
|
5955
|
+
if (operation !== undefined) {
|
|
5956
|
+
executeOperation(configuration, state, operation);
|
|
5957
|
+
return;
|
|
5958
|
+
}
|
|
5959
|
+
}
|
|
5960
|
+
const occupied = new Set([...state.tasks.values()].map(({ task }) => taskSlot(task)));
|
|
5961
|
+
for (let index = 0;index < state.queue.length; ) {
|
|
5962
|
+
const next = state.queue[index];
|
|
5963
|
+
if (next === undefined || occupied.has(taskSlot(next))) {
|
|
5964
|
+
index += 1;
|
|
5965
|
+
continue;
|
|
5966
|
+
}
|
|
5967
|
+
state.queue.splice(index, 1);
|
|
5968
|
+
occupied.add(taskSlot(next));
|
|
5969
|
+
execute(configuration, state, next);
|
|
5970
|
+
}
|
|
5971
|
+
};
|
|
5252
5972
|
var execute = (configuration, state, task) => {
|
|
5253
5973
|
if (state.tasks.has(task.id) || state.completed.has(task.id)) {
|
|
5254
5974
|
return;
|
|
5255
5975
|
}
|
|
5256
5976
|
const controller = new AbortController;
|
|
5257
|
-
state.tasks.set(task.id, controller);
|
|
5977
|
+
state.tasks.set(task.id, { task, controller });
|
|
5258
5978
|
send(state, SocketEvent.make({ _tag: "TaskAccepted", taskId: task.id }));
|
|
5259
|
-
const workflow =
|
|
5260
|
-
|
|
5979
|
+
const workflow = Effect10.gen(function* () {
|
|
5980
|
+
if (task.provider === "grok-bot") {
|
|
5981
|
+
return yield* Effect10.tryPromise({
|
|
5982
|
+
try: (signal) => runGrokBot(configuration.grokGateway, task, signal),
|
|
5983
|
+
catch: (cause) => new ProviderRunError({
|
|
5984
|
+
provider: task.provider,
|
|
5985
|
+
message: cause instanceof Error ? cause.message : String(cause)
|
|
5986
|
+
})
|
|
5987
|
+
});
|
|
5988
|
+
}
|
|
5989
|
+
const workspace = yield* Effect10.tryPromise({
|
|
5261
5990
|
try: () => openAllowedWorkspace(configuration, task.workspace),
|
|
5262
5991
|
catch: (cause) => new ProviderRunError({
|
|
5263
5992
|
provider: task.provider,
|
|
@@ -5268,17 +5997,18 @@ var execute = (configuration, state, task) => {
|
|
|
5268
5997
|
return yield* run({
|
|
5269
5998
|
provider: task.provider,
|
|
5270
5999
|
threadId: task.threadId,
|
|
6000
|
+
taskId: task.id,
|
|
5271
6001
|
prompt: promptFor(task, configuration.name),
|
|
5272
6002
|
cwd: workspace.cwd,
|
|
5273
6003
|
beforeSpawn: workspace.validate,
|
|
5274
6004
|
...sessionId === undefined ? {} : { sessionId },
|
|
5275
6005
|
...configuration.model === undefined ? {} : { model: configuration.model },
|
|
5276
6006
|
...configuration.sandbox === undefined ? {} : { sandbox: configuration.sandbox }
|
|
5277
|
-
}).pipe(
|
|
6007
|
+
}).pipe(Effect10.ensuring(Effect10.promise(() => workspace.close().catch(() => {
|
|
5278
6008
|
return;
|
|
5279
6009
|
}))));
|
|
5280
6010
|
});
|
|
5281
|
-
|
|
6011
|
+
Effect10.runPromise(workflow, { signal: controller.signal }).then((result) => {
|
|
5282
6012
|
if (result.sessionId !== undefined) {
|
|
5283
6013
|
state.sessions.set(sessionKey(task), result.sessionId);
|
|
5284
6014
|
}
|
|
@@ -5298,15 +6028,7 @@ var execute = (configuration, state, task) => {
|
|
|
5298
6028
|
}));
|
|
5299
6029
|
}).finally(() => {
|
|
5300
6030
|
state.tasks.delete(task.id);
|
|
5301
|
-
|
|
5302
|
-
if (operation !== undefined) {
|
|
5303
|
-
executeOperation(configuration, state, operation);
|
|
5304
|
-
return;
|
|
5305
|
-
}
|
|
5306
|
-
const next = state.queue.shift();
|
|
5307
|
-
if (next !== undefined && state.operation === undefined) {
|
|
5308
|
-
execute(configuration, state, next);
|
|
5309
|
-
}
|
|
6031
|
+
drain(configuration, state);
|
|
5310
6032
|
});
|
|
5311
6033
|
};
|
|
5312
6034
|
var executeOperation = (configuration, state, operation) => {
|
|
@@ -5331,15 +6053,7 @@ var executeOperation = (configuration, state, operation) => {
|
|
|
5331
6053
|
}));
|
|
5332
6054
|
}).finally(() => {
|
|
5333
6055
|
state.operation = undefined;
|
|
5334
|
-
|
|
5335
|
-
if (pending !== undefined) {
|
|
5336
|
-
executeOperation(configuration, state, pending);
|
|
5337
|
-
return;
|
|
5338
|
-
}
|
|
5339
|
-
const next = state.queue.shift();
|
|
5340
|
-
if (next !== undefined && state.tasks.size === 0) {
|
|
5341
|
-
execute(configuration, state, next);
|
|
5342
|
-
}
|
|
6056
|
+
drain(configuration, state);
|
|
5343
6057
|
});
|
|
5344
6058
|
};
|
|
5345
6059
|
var scheduleOperation = (configuration, state, operation) => {
|
|
@@ -5370,7 +6084,7 @@ var schedule = (configuration, state, task) => {
|
|
|
5370
6084
|
if (task.providerSessionId !== undefined) {
|
|
5371
6085
|
state.sessions.set(sessionKey(task), task.providerSessionId);
|
|
5372
6086
|
}
|
|
5373
|
-
if (state.tasks.
|
|
6087
|
+
if ([...state.tasks.values()].some(({ task: active }) => taskSlot(active) === taskSlot(task)) || state.operation !== undefined) {
|
|
5374
6088
|
if (state.queue.length >= 100) {
|
|
5375
6089
|
state.socket?.close(4008, "Task queue limit reached");
|
|
5376
6090
|
return;
|
|
@@ -5383,7 +6097,9 @@ var schedule = (configuration, state, task) => {
|
|
|
5383
6097
|
var cancel = (state, taskId) => {
|
|
5384
6098
|
const running = state.tasks.get(taskId);
|
|
5385
6099
|
if (running !== undefined) {
|
|
5386
|
-
running.
|
|
6100
|
+
if (running.task.provider !== "grok-bot") {
|
|
6101
|
+
running.controller.abort();
|
|
6102
|
+
}
|
|
5387
6103
|
return;
|
|
5388
6104
|
}
|
|
5389
6105
|
const index = state.queue.findIndex((task) => task.id === taskId);
|
|
@@ -5393,18 +6109,39 @@ var cancel = (state, taskId) => {
|
|
|
5393
6109
|
remember(state, taskId);
|
|
5394
6110
|
sendTerminal(state, taskId, SocketEvent.make({ _tag: "TaskCancelled", taskId }));
|
|
5395
6111
|
};
|
|
5396
|
-
var connect = (configuration, state) =>
|
|
6112
|
+
var connect = (configuration, state) => Effect10.tryPromise({
|
|
5397
6113
|
try: (signal) => new Promise((complete) => {
|
|
5398
6114
|
const socket = new WebSocket2(socketUrl(configuration), {
|
|
5399
6115
|
maxPayload: maxSocketPayloadBytes,
|
|
5400
|
-
perMessageDeflate: false
|
|
6116
|
+
perMessageDeflate: false,
|
|
6117
|
+
allowSynchronousEvents: false
|
|
5401
6118
|
});
|
|
6119
|
+
let bots = [];
|
|
6120
|
+
let refreshing = false;
|
|
6121
|
+
const refreshBots = async () => {
|
|
6122
|
+
if (refreshing || configuration.grokGateway === undefined || configuration.providers !== undefined && !configuration.providers.includes("grok-bot")) {
|
|
6123
|
+
return;
|
|
6124
|
+
}
|
|
6125
|
+
refreshing = true;
|
|
6126
|
+
bots = await discoverGrokBots(configuration.grokGateway).catch(() => []);
|
|
6127
|
+
refreshing = false;
|
|
6128
|
+
if (!closed) {
|
|
6129
|
+
send(state, SocketEvent.make({
|
|
6130
|
+
_tag: "DeviceHeartbeat",
|
|
6131
|
+
deviceId: configuration.id,
|
|
6132
|
+
status: state.tasks.size > 0 || state.operation !== undefined ? "busy" : "online",
|
|
6133
|
+
bots
|
|
6134
|
+
}));
|
|
6135
|
+
}
|
|
6136
|
+
};
|
|
5402
6137
|
const heartbeat = setInterval(() => {
|
|
5403
6138
|
send(state, SocketEvent.make({
|
|
5404
6139
|
_tag: "DeviceHeartbeat",
|
|
5405
6140
|
deviceId: configuration.id,
|
|
5406
|
-
status: state.tasks.size > 0 || state.operation !== undefined ? "busy" : "online"
|
|
6141
|
+
status: state.tasks.size > 0 || state.operation !== undefined ? "busy" : "online",
|
|
6142
|
+
...configuration.grokGateway === undefined ? {} : { bots }
|
|
5407
6143
|
}));
|
|
6144
|
+
refreshBots();
|
|
5408
6145
|
}, 15000);
|
|
5409
6146
|
let closed = false;
|
|
5410
6147
|
let queuedMessages = 0;
|
|
@@ -5436,16 +6173,16 @@ var connect = (configuration, state) => Effect8.tryPromise({
|
|
|
5436
6173
|
if (closed) {
|
|
5437
6174
|
return;
|
|
5438
6175
|
}
|
|
5439
|
-
const
|
|
5440
|
-
const event = await
|
|
5441
|
-
try: () => JSON.parse(
|
|
6176
|
+
const text3 = Buffer.isBuffer(message) ? message.toString("utf8") : message instanceof ArrayBuffer ? Buffer.from(message).toString("utf8") : Array.isArray(message) ? Buffer.concat(message).toString("utf8") : Buffer.from(message).toString("utf8");
|
|
6177
|
+
const event = await Effect10.runPromise(Effect10.try({
|
|
6178
|
+
try: () => JSON.parse(text3),
|
|
5442
6179
|
catch: () => new DeviceConnectionError({ message: "Relay sent invalid JSON" })
|
|
5443
|
-
}).pipe(
|
|
6180
|
+
}).pipe(Effect10.flatMap(decodeSocketEvent)));
|
|
5444
6181
|
if (event._tag === "Connected") {
|
|
5445
6182
|
state.socket = socket;
|
|
5446
6183
|
socket.send(JSON.stringify(SocketEvent.make({
|
|
5447
6184
|
_tag: "DeviceHello",
|
|
5448
|
-
device: describeDevice(configuration, state.tasks.size > 0 || state.operation !== undefined ? "busy" : "online")
|
|
6185
|
+
device: describeDevice(configuration, state.tasks.size > 0 || state.operation !== undefined ? "busy" : "online", bots)
|
|
5449
6186
|
})));
|
|
5450
6187
|
for (const payload of state.terminal.values()) {
|
|
5451
6188
|
socket.send(payload);
|
|
@@ -5453,6 +6190,7 @@ var connect = (configuration, state) => Effect8.tryPromise({
|
|
|
5453
6190
|
for (const payload of state.operationTerminal.values()) {
|
|
5454
6191
|
socket.send(payload);
|
|
5455
6192
|
}
|
|
6193
|
+
refreshBots();
|
|
5456
6194
|
return;
|
|
5457
6195
|
}
|
|
5458
6196
|
if (event._tag === "TaskAssigned") {
|
|
@@ -5465,6 +6203,9 @@ var connect = (configuration, state) => Effect8.tryPromise({
|
|
|
5465
6203
|
}
|
|
5466
6204
|
if (event._tag === "TaskSettled") {
|
|
5467
6205
|
state.terminal.delete(event.taskId);
|
|
6206
|
+
cleanupBotReply(event.taskId).catch(() => {
|
|
6207
|
+
console.error(`Could not remove local bot reply for settled task ${event.taskId}`);
|
|
6208
|
+
});
|
|
5468
6209
|
return;
|
|
5469
6210
|
}
|
|
5470
6211
|
if (event._tag === "OperationAssigned") {
|
|
@@ -5505,9 +6246,9 @@ var runDaemon = (configuration) => {
|
|
|
5505
6246
|
operationTerminal: new Map,
|
|
5506
6247
|
completedOperations: new Set
|
|
5507
6248
|
};
|
|
5508
|
-
return connect(configuration, state).pipe(
|
|
6249
|
+
return connect(configuration, state).pipe(Effect10.repeat({ schedule: Schedule3.spaced("2 seconds") }), Effect10.ensuring(Effect10.sync(() => {
|
|
5509
6250
|
state.socket?.close();
|
|
5510
|
-
for (const controller of state.tasks.values()) {
|
|
6251
|
+
for (const { controller } of state.tasks.values()) {
|
|
5511
6252
|
controller.abort();
|
|
5512
6253
|
}
|
|
5513
6254
|
})));
|
|
@@ -5517,7 +6258,7 @@ var runDaemon = (configuration) => {
|
|
|
5517
6258
|
init_src();
|
|
5518
6259
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
5519
6260
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
5520
|
-
import { Effect as
|
|
6261
|
+
import { Effect as Effect11 } from "effect";
|
|
5521
6262
|
import * as z from "zod/v4";
|
|
5522
6263
|
var output = (value) => ({
|
|
5523
6264
|
content: [{ type: "text", text: JSON.stringify(value, null, 2) }]
|
|
@@ -5532,41 +6273,58 @@ var runMcp = async (configuration) => {
|
|
|
5532
6273
|
title: "List Cohall devices",
|
|
5533
6274
|
description: "List devices, their availability, local providers, capabilities, and allowed workspaces.",
|
|
5534
6275
|
inputSchema: {}
|
|
5535
|
-
}, async () => output(await
|
|
6276
|
+
}, async () => output(await Effect11.runPromise(client2.devices())));
|
|
6277
|
+
server.registerTool("list_bots", {
|
|
6278
|
+
title: "List Cohall bots",
|
|
6279
|
+
description: "List named Grok Bots on paired devices, their host availability, and unambiguous targets for delegate. Bot names can also be used when unique.",
|
|
6280
|
+
inputSchema: {}
|
|
6281
|
+
}, async () => output(listBots(await Effect11.runPromise(client2.devices()))));
|
|
5536
6282
|
server.registerTool("delegate", {
|
|
5537
|
-
title: "
|
|
5538
|
-
description: "Send focused work to
|
|
6283
|
+
title: "Send work to a device or bot",
|
|
6284
|
+
description: "Send focused work to a Cohall device or named Grok Bot. When the request depends on the current conversation, distill its motivation, relevant facts, prior findings, constraints, and desired decision into context; Cohall cannot read the host transcript. Never forward unrelated transcript content. Reuse thread_id for related follow-ups. Grok Bots use their existing conversation; a Cohall thread does not isolate it.",
|
|
5539
6285
|
inputSchema: {
|
|
5540
6286
|
prompt: z.string().min(1).max(131072),
|
|
5541
|
-
target: z.string().optional().describe("Device name,
|
|
5542
|
-
provider: z.enum(
|
|
6287
|
+
target: z.string().optional().describe("Device name, hostname or ID; @BotName; or @device/BotName. Use list_bots targets to disambiguate."),
|
|
6288
|
+
provider: z.enum(Provider.literals).optional().describe("Inferred as grok-bot for a bot target; otherwise defaults to codex."),
|
|
5543
6289
|
context: z.string().max(131072).optional().describe("Distilled context the target needs: why the user is asking, relevant facts and prior findings, constraints, and the intended decision. The calling agent must supply this when the prompt depends on its conversation; omit it only for a self-contained task."),
|
|
5544
6290
|
thread_id: z.string().uuid().optional(),
|
|
5545
|
-
|
|
6291
|
+
parent_task_id: z.string().uuid().optional().describe("The task delegating this work. Inherited from COHALL_TASK_ID when present."),
|
|
6292
|
+
workspace: z.string().max(4096).optional().describe("Coding provider workspace. Omit for named Grok Bots."),
|
|
5546
6293
|
wait: z.boolean().default(true),
|
|
5547
6294
|
timeout_seconds: z.number().int().min(5).max(86400).default(900)
|
|
5548
6295
|
}
|
|
5549
|
-
}, async ({
|
|
5550
|
-
|
|
6296
|
+
}, async ({
|
|
6297
|
+
prompt,
|
|
6298
|
+
target,
|
|
6299
|
+
provider,
|
|
6300
|
+
context,
|
|
6301
|
+
thread_id,
|
|
6302
|
+
parent_task_id,
|
|
6303
|
+
workspace,
|
|
6304
|
+
wait,
|
|
6305
|
+
timeout_seconds
|
|
6306
|
+
}) => {
|
|
6307
|
+
const task = await Effect11.runPromise(createDelegation(client2, configuration, {
|
|
5551
6308
|
prompt,
|
|
5552
|
-
provider,
|
|
6309
|
+
...provider === undefined ? {} : { provider },
|
|
5553
6310
|
...target === undefined ? {} : { target },
|
|
5554
6311
|
...context === undefined ? {} : { context },
|
|
5555
6312
|
...thread_id === undefined ? {} : { threadId: ThreadId.make(thread_id) },
|
|
6313
|
+
...parent_task_id === undefined ? {} : { parentTaskId: TaskId.make(parent_task_id) },
|
|
5556
6314
|
...workspace === undefined ? {} : { workspace }
|
|
5557
6315
|
}));
|
|
5558
|
-
return output(taskResult(wait ? await
|
|
6316
|
+
return output(taskResult(wait ? await Effect11.runPromise(waitForTask(client2, task, timeout_seconds)) : task));
|
|
5559
6317
|
});
|
|
5560
6318
|
server.registerTool("task_status", {
|
|
5561
6319
|
title: "Get delegated task status",
|
|
5562
6320
|
description: "Read the current state and result of a Cohall task.",
|
|
5563
6321
|
inputSchema: { task_id: z.string().uuid() }
|
|
5564
|
-
}, async ({ task_id }) => output(taskResult(await
|
|
6322
|
+
}, async ({ task_id }) => output(taskResult(await Effect11.runPromise(client2.getTask(TaskId.make(task_id))))));
|
|
5565
6323
|
server.registerTool("task_trace", {
|
|
5566
6324
|
title: "Trace a delegated task",
|
|
5567
6325
|
description: "Read the redacted relay and device lifecycle for troubleshooting a Cohall task.",
|
|
5568
6326
|
inputSchema: { task_id: z.string().uuid() }
|
|
5569
|
-
}, async ({ task_id }) => output(await
|
|
6327
|
+
}, async ({ task_id }) => output(await Effect11.runPromise(client2.traceTask(TaskId.make(task_id)))));
|
|
5570
6328
|
server.registerTool("wait_task", {
|
|
5571
6329
|
title: "Wait for delegated work",
|
|
5572
6330
|
description: "Wait for a Cohall task to finish and return its final result.",
|
|
@@ -5575,19 +6333,19 @@ var runMcp = async (configuration) => {
|
|
|
5575
6333
|
timeout_seconds: z.number().int().min(5).max(86400).default(900)
|
|
5576
6334
|
}
|
|
5577
6335
|
}, async ({ task_id, timeout_seconds }) => {
|
|
5578
|
-
const task = await
|
|
5579
|
-
return output(taskResult(await
|
|
6336
|
+
const task = await Effect11.runPromise(client2.getTask(TaskId.make(task_id)));
|
|
6337
|
+
return output(taskResult(await Effect11.runPromise(waitForTask(client2, task, timeout_seconds))));
|
|
5580
6338
|
});
|
|
5581
6339
|
server.registerTool("cancel_task", {
|
|
5582
6340
|
title: "Cancel delegated work",
|
|
5583
|
-
description: "Request cancellation
|
|
6341
|
+
description: "Request cancellation of coding work or a queued Bot task. Active Grok Bot tasks must be stopped in Grok Bot.",
|
|
5584
6342
|
inputSchema: { task_id: z.string().uuid() }
|
|
5585
|
-
}, async ({ task_id }) => output(taskResult(await
|
|
6343
|
+
}, async ({ task_id }) => output(taskResult(await Effect11.runPromise(client2.cancelTask(TaskId.make(task_id))))));
|
|
5586
6344
|
server.registerTool("thread_context", {
|
|
5587
6345
|
title: "Read a Cohall thread",
|
|
5588
6346
|
description: "Read shared task prompts and final results for a cross-device thread.",
|
|
5589
6347
|
inputSchema: { thread_id: z.string().uuid() }
|
|
5590
|
-
}, async ({ thread_id }) => output(await
|
|
6348
|
+
}, async ({ thread_id }) => output(await Effect11.runPromise(threadContext(client2, ThreadId.make(thread_id)))));
|
|
5591
6349
|
await server.connect(new StdioServerTransport);
|
|
5592
6350
|
};
|
|
5593
6351
|
|
|
@@ -5616,12 +6374,12 @@ var main = async () => {
|
|
|
5616
6374
|
return;
|
|
5617
6375
|
}
|
|
5618
6376
|
if (command === "device") {
|
|
5619
|
-
const configuration = await
|
|
5620
|
-
await
|
|
6377
|
+
const configuration = await Effect15.runPromise(loadDeviceConfiguration);
|
|
6378
|
+
await Effect15.runPromise(runDaemon(configuration));
|
|
5621
6379
|
return;
|
|
5622
6380
|
}
|
|
5623
6381
|
if (command === "mcp") {
|
|
5624
|
-
const configuration = await
|
|
6382
|
+
const configuration = await Effect15.runPromise(loadClientConfiguration);
|
|
5625
6383
|
await runMcp(configuration);
|
|
5626
6384
|
return;
|
|
5627
6385
|
}
|
|
@@ -5632,4 +6390,4 @@ await main().catch((cause) => {
|
|
|
5632
6390
|
process.exitCode = 1;
|
|
5633
6391
|
});
|
|
5634
6392
|
|
|
5635
|
-
//# debugId=
|
|
6393
|
+
//# debugId=78963F23872A4D6564756E2164756E21
|