@akshar5/cohall 0.4.4 → 0.4.6
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 +14 -0
- package/README.md +53 -2
- package/bin/cohall.js +122 -14
- package/bin/cohall.js.map +8 -8
- package/deploy/systemd/cohall-relay.service +3 -1
- package/deploy/systemd/cohall-relay.socket +11 -0
- package/docs/install.md +8 -5
- package/docs/integrations.md +1 -3
- package/docs/releasing.md +0 -1
- package/docs/services.md +50 -5
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.4.6](https://github.com/AksharP5/cohall/compare/v0.4.5...v0.4.6) (2026-08-08)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
* **skill:** explain cross-device workflows and recovery ([#34](https://github.com/AksharP5/cohall/issues/34)) ([0bba7b9](https://github.com/AksharP5/cohall/commit/0bba7b9645d2c1f577332ebb21a885b973fc6e80))
|
|
9
|
+
|
|
10
|
+
## [0.4.5](https://github.com/AksharP5/cohall/compare/v0.4.4...v0.4.5) (2026-08-07)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Bug Fixes
|
|
14
|
+
|
|
15
|
+
* **relay:** keep upgrades available and forget stale devices ([#32](https://github.com/AksharP5/cohall/issues/32)) ([d5b054d](https://github.com/AksharP5/cohall/commit/d5b054db37b8e2737af7540cac4f74f64e457a7f))
|
|
16
|
+
|
|
3
17
|
## [0.4.4](https://github.com/AksharP5/cohall/compare/v0.4.3...v0.4.4) (2026-08-07)
|
|
4
18
|
|
|
5
19
|
|
package/README.md
CHANGED
|
@@ -36,6 +36,27 @@ session IDs. It does not plan work, copy device credentials, or SSH into a
|
|
|
36
36
|
machine. Each device runs its own provider CLI with its existing local login,
|
|
37
37
|
configuration, skills, MCP servers, permissions, and workspace access.
|
|
38
38
|
|
|
39
|
+
## What to use it for
|
|
40
|
+
|
|
41
|
+
Once the skill is installed, requests in an ordinary agent thread can be as
|
|
42
|
+
direct as:
|
|
43
|
+
|
|
44
|
+
- “Ask `@macbook` to build the iOS app in Xcode and diagnose the signing error.”
|
|
45
|
+
- “Have `@devbox` reproduce this failure against its Docker services.”
|
|
46
|
+
- “Use `@archlinux`'s signed-in browser to inspect the failed deployment.”
|
|
47
|
+
- “Queue the full test suite on `@devbox`; keep working here and report back.”
|
|
48
|
+
|
|
49
|
+
The current agent turns that request into a focused Cohall task and incorporates
|
|
50
|
+
the result when it returns. `@macbook` is a device selector understood through
|
|
51
|
+
the installed skill, not special chat syntax built into your harness.
|
|
52
|
+
|
|
53
|
+
T3 Connect is a remote interface to a T3 Code environment: use it when you want
|
|
54
|
+
to browse that machine's projects, terminal, files, and diffs yourself. Cohall
|
|
55
|
+
instead lets the agent in your current Codex, Claude Code, OpenCode, T3Code, or
|
|
56
|
+
other harness delegate an outcome to an agent on another machine. It provides
|
|
57
|
+
cross-harness CLI/MCP access, durable offline queues, resumable agent threads,
|
|
58
|
+
and redacted task traces. The two tools can be used together.
|
|
59
|
+
|
|
39
60
|
## Quick start
|
|
40
61
|
|
|
41
62
|
Run Cohall anywhere Node.js 24 or newer is installed. Use whichever JavaScript
|
|
@@ -91,6 +112,26 @@ npx -y @akshar5/cohall device
|
|
|
91
112
|
then writes a per-user configuration file with Unix mode `0600`. Workspace roots
|
|
92
113
|
must already exist and are resolved to canonical paths.
|
|
93
114
|
|
|
115
|
+
## Availability and restarts
|
|
116
|
+
|
|
117
|
+
The relay must be reachable to accept new tasks or return status. Once accepted,
|
|
118
|
+
a task is stored in SQLite and survives relay or target-device restarts. Work for
|
|
119
|
+
an offline target waits on the relay and starts when that device reconnects.
|
|
120
|
+
Interrupted running work is re-queued with at-least-once delivery, so prompts
|
|
121
|
+
that cause external changes should be safe to retry.
|
|
122
|
+
|
|
123
|
+
The packaged service definitions reconnect automatically:
|
|
124
|
+
|
|
125
|
+
- the Linux relay starts at boot through systemd socket and service units;
|
|
126
|
+
- a Linux device starts with its user service, or at boot when user lingering is
|
|
127
|
+
enabled;
|
|
128
|
+
- a macOS device starts when that user logs in through its LaunchAgent;
|
|
129
|
+
- a Windows device starts at user logon through Task Scheduler.
|
|
130
|
+
|
|
131
|
+
Powered-off devices do not run work. A powered-off relay cannot accept new work,
|
|
132
|
+
but tasks already written to its persistent data directory remain there for its
|
|
133
|
+
next start. See [service setup](docs/services.md) for installation and checks.
|
|
134
|
+
|
|
94
135
|
## Use from an agent
|
|
95
136
|
|
|
96
137
|
Install the same embedded skill for Codex, Claude Code, and OpenCode:
|
|
@@ -226,8 +267,13 @@ the same npm, Bun, or pnpm installation and restarts only active Cohall relay
|
|
|
226
267
|
and device jobs. Active jobs restart even when the installed files are already
|
|
227
268
|
current, ensuring an older loaded process is replaced. Cohall refuses to restart
|
|
228
269
|
a service configured to use a different global installation; run that service's
|
|
229
|
-
executable directly or update its service definition first.
|
|
230
|
-
|
|
270
|
+
executable directly or update its service definition first.
|
|
271
|
+
|
|
272
|
+
- `cohall upgrade --dry-run` previews the plan.
|
|
273
|
+
- `cohall upgrade --no-restart` updates files without restarting services.
|
|
274
|
+
|
|
275
|
+
Linux relays can use the packaged systemd socket unit so new connections remain
|
|
276
|
+
available while the relay process is replaced; see [service setup](docs/services.md).
|
|
231
277
|
Environment variables override stored values:
|
|
232
278
|
|
|
233
279
|
| Variable | Purpose |
|
|
@@ -250,6 +296,11 @@ Environment variables override stored values:
|
|
|
250
296
|
| `COHALL_RELAY_ALLOW_REMOTE` | Explicit non-loopback binding opt-in |
|
|
251
297
|
|
|
252
298
|
The owner token can create pairing credentials, list sessions, and revoke them.
|
|
299
|
+
`cohall forget <device-id>` removes an offline device from discovery after
|
|
300
|
+
confirming it has no outstanding tasks, revokes its device credential, and
|
|
301
|
+
preserves its completed task history. On the relay host, owner commands read the
|
|
302
|
+
protected local owner-token file automatically; remote owner commands require
|
|
303
|
+
`COHALL_TOKEN`.
|
|
253
304
|
Ordinary client and device credentials are role-separated, device-bound where
|
|
254
305
|
applicable, expiring, and stored only as SHA-256 hashes by the relay.
|
|
255
306
|
|
package/bin/cohall.js
CHANGED
|
@@ -17,7 +17,7 @@ var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
|
17
17
|
|
|
18
18
|
// packages/protocol/src/index.ts
|
|
19
19
|
import { Schema } from "effect";
|
|
20
|
-
var version = "0.4.
|
|
20
|
+
var version = "0.4.6", 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)), uuid = (name) => Schema.String.check(Schema.isUUID(4)).pipe(Schema.brand(name)), isoTimestamp = (name) => Schema.String.check(Schema.isPattern(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,9})?Z$/, {
|
|
21
21
|
expected: "an ISO-8601 UTC timestamp"
|
|
22
22
|
})).pipe(Schema.brand(name)), DeviceId, ThreadId, MessageId, TaskId, AuthSessionId, Timestamp, Platform, Provider, DeviceStatus, TaskStatus, TaskTraceEventKind, MessageRole, ConnectionRole, AuthSession, CreatePairingInput, PairingCredential, ExchangePairingInput, SessionCredential, PairingResult, Capability, Workspace, Device, Thread, Message, Task, 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()), decodeCreatePairingInput, decodeExchangePairingInput, decodeCreateTaskInput, decodeDevice, decodeSocketEvent;
|
|
23
23
|
var init_src = __esm(() => {
|
|
@@ -330,6 +330,9 @@ class Hub {
|
|
|
330
330
|
}
|
|
331
331
|
}
|
|
332
332
|
}
|
|
333
|
+
closeDevice(deviceId) {
|
|
334
|
+
this.#devices.get(deviceId)?.close(4003, "Device forgotten");
|
|
335
|
+
}
|
|
333
336
|
hasDevice(deviceId) {
|
|
334
337
|
return this.#devices.get(deviceId)?.readyState === WebSocket2.OPEN;
|
|
335
338
|
}
|
|
@@ -588,11 +591,35 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
|
|
|
588
591
|
});
|
|
589
592
|
const listDevices = Effect9.fn("RelayStore.listDevices")(function* () {
|
|
590
593
|
const rows = yield* Effect9.try({
|
|
591
|
-
try: () => db.query("SELECT * FROM devices ORDER BY name COLLATE NOCASE, id").all(),
|
|
594
|
+
try: () => db.query("SELECT * FROM devices WHERE forgotten_at IS NULL ORDER BY name COLLATE NOCASE, id").all(),
|
|
592
595
|
catch: operationError("RelayStore.listDevices")
|
|
593
596
|
});
|
|
594
597
|
return yield* Effect9.forEach(rows, deviceFromRow);
|
|
595
598
|
});
|
|
599
|
+
const forgetDevice = Effect9.fn("RelayStore.forgetDevice")(function* (deviceId) {
|
|
600
|
+
const row = yield* Effect9.try({
|
|
601
|
+
try: () => db.transaction(() => {
|
|
602
|
+
const device = db.query("SELECT * FROM devices WHERE id = ? AND forgotten_at IS NULL").get(deviceId);
|
|
603
|
+
if (device === null) {
|
|
604
|
+
throw new Error(`Unknown device ${deviceId}`);
|
|
605
|
+
}
|
|
606
|
+
if (device.status !== "offline") {
|
|
607
|
+
throw new Error(`Device ${deviceId} must be offline before it can be forgotten`);
|
|
608
|
+
}
|
|
609
|
+
const outstanding = db.query(`SELECT COUNT(*) AS count FROM tasks
|
|
610
|
+
WHERE target_device_id = ? AND status NOT IN ('completed', 'failed', 'cancelled')`).get(deviceId);
|
|
611
|
+
if ((outstanding?.count ?? 0) > 0) {
|
|
612
|
+
throw new Error(`Device ${deviceId} still has outstanding tasks`);
|
|
613
|
+
}
|
|
614
|
+
const timestamp = now();
|
|
615
|
+
db.query("UPDATE devices SET forgotten_at = ? WHERE id = ?").run(timestamp, deviceId);
|
|
616
|
+
db.query("UPDATE auth_sessions SET revoked_at = ? WHERE bound_device_id = ? AND revoked_at IS NULL").run(timestamp, deviceId);
|
|
617
|
+
return device;
|
|
618
|
+
})(),
|
|
619
|
+
catch: operationError("RelayStore.forgetDevice")
|
|
620
|
+
});
|
|
621
|
+
return yield* deviceFromRow(row);
|
|
622
|
+
});
|
|
596
623
|
const recover = Effect9.fn("RelayStore.recover")(function* () {
|
|
597
624
|
yield* Effect9.try({
|
|
598
625
|
try: () => db.transaction(() => {
|
|
@@ -620,7 +647,8 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
|
|
|
620
647
|
architecture = excluded.architecture, status = excluded.status,
|
|
621
648
|
providers_json = excluded.providers_json, capabilities_json = excluded.capabilities_json,
|
|
622
649
|
workspaces_json = excluded.workspaces_json, version = excluded.version,
|
|
623
|
-
last_seen_at = excluded.last_seen_at, connected_at = excluded.connected_at
|
|
650
|
+
last_seen_at = excluded.last_seen_at, connected_at = excluded.connected_at,
|
|
651
|
+
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),
|
|
624
652
|
catch: operationError("RelayStore.upsertDevice")
|
|
625
653
|
});
|
|
626
654
|
return device;
|
|
@@ -983,6 +1011,7 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
|
|
|
983
1011
|
return Service2.of({
|
|
984
1012
|
recover,
|
|
985
1013
|
listDevices,
|
|
1014
|
+
forgetDevice,
|
|
986
1015
|
upsertDevice,
|
|
987
1016
|
heartbeat: (deviceId, status) => updateDeviceStatus(deviceId, status),
|
|
988
1017
|
markDeviceOffline: (deviceId) => updateDeviceStatus(deviceId, "offline"),
|
|
@@ -1017,7 +1046,7 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
|
|
|
1017
1046
|
platform TEXT NOT NULL, architecture TEXT NOT NULL, status TEXT NOT NULL,
|
|
1018
1047
|
providers_json TEXT NOT NULL, capabilities_json TEXT NOT NULL,
|
|
1019
1048
|
workspaces_json TEXT NOT NULL, version TEXT NOT NULL, last_seen_at TEXT NOT NULL,
|
|
1020
|
-
connected_at TEXT
|
|
1049
|
+
connected_at TEXT, forgotten_at TEXT
|
|
1021
1050
|
);
|
|
1022
1051
|
CREATE TABLE IF NOT EXISTS threads (
|
|
1023
1052
|
id TEXT PRIMARY KEY, title TEXT NOT NULL, created_at TEXT NOT NULL,
|
|
@@ -1065,6 +1094,10 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
|
|
|
1065
1094
|
);
|
|
1066
1095
|
CREATE INDEX IF NOT EXISTS auth_sessions_token ON auth_sessions(token_hash);
|
|
1067
1096
|
`);
|
|
1097
|
+
const deviceColumns = db.query("PRAGMA table_info(devices)").all();
|
|
1098
|
+
if (!deviceColumns.some((column) => column.name === "forgotten_at")) {
|
|
1099
|
+
db.exec("ALTER TABLE devices ADD COLUMN forgotten_at TEXT");
|
|
1100
|
+
}
|
|
1068
1101
|
},
|
|
1069
1102
|
catch: operationError("RelayStore.migrate")
|
|
1070
1103
|
}), layer2 = (path) => Layer2.effect(Service2, Effect9.acquireRelease(Effect9.try({
|
|
@@ -1084,7 +1117,8 @@ var init_store = __esm(() => {
|
|
|
1084
1117
|
// apps/relay/src/main.ts
|
|
1085
1118
|
var exports_main = {};
|
|
1086
1119
|
__export(exports_main, {
|
|
1087
|
-
runRelay: () => runRelay
|
|
1120
|
+
runRelay: () => runRelay,
|
|
1121
|
+
relayListenOptions: () => relayListenOptions
|
|
1088
1122
|
});
|
|
1089
1123
|
import { Effect as Effect10, ManagedRuntime, Schema as Schema11 } from "effect";
|
|
1090
1124
|
import { createHash as createHash2, timingSafeEqual } from "node:crypto";
|
|
@@ -1092,7 +1126,22 @@ import { access as access4, chmod as chmod5 } from "node:fs/promises";
|
|
|
1092
1126
|
import { createServer } from "node:http";
|
|
1093
1127
|
import { platform as platform5 } from "node:os";
|
|
1094
1128
|
import { WebSocketServer } from "ws";
|
|
1095
|
-
var RequestError,
|
|
1129
|
+
var RequestError, relayListenOptions = (configuration, environment = process.env, pid = process.pid) => {
|
|
1130
|
+
const listenPid = Number(environment.LISTEN_PID);
|
|
1131
|
+
const listenFds = Number(environment.LISTEN_FDS);
|
|
1132
|
+
if (listenPid !== pid || !Number.isInteger(listenFds) || listenFds < 1) {
|
|
1133
|
+
return { host: configuration.host, port: configuration.port };
|
|
1134
|
+
}
|
|
1135
|
+
const descriptorNames = environment.LISTEN_FDNAMES?.split(":") ?? [];
|
|
1136
|
+
const namedIndex = descriptorNames.indexOf("cohall-relay");
|
|
1137
|
+
if (namedIndex >= 0 && namedIndex < listenFds) {
|
|
1138
|
+
return { fd: 3 + namedIndex };
|
|
1139
|
+
}
|
|
1140
|
+
if (listenFds === 1) {
|
|
1141
|
+
return { fd: 3 };
|
|
1142
|
+
}
|
|
1143
|
+
throw new Error("Cohall received multiple systemd sockets but none was named cohall-relay");
|
|
1144
|
+
}, json = (value, status = 200) => Response.json(value, {
|
|
1096
1145
|
status,
|
|
1097
1146
|
headers: {
|
|
1098
1147
|
"cache-control": "no-store",
|
|
@@ -1414,6 +1463,22 @@ Content-Length: 0\r
|
|
|
1414
1463
|
if (url.pathname === "/api/devices" && request.method === "GET") {
|
|
1415
1464
|
return json(yield* store.listDevices());
|
|
1416
1465
|
}
|
|
1466
|
+
const forgetDevice = url.pathname.match(/^\/api\/devices\/([^/]+)\/forget$/);
|
|
1467
|
+
if (request.method === "POST" && forgetDevice?.[1] !== undefined) {
|
|
1468
|
+
if (principal !== "owner") {
|
|
1469
|
+
return yield* new RequestError({ status: 403, message: "Owner token required" });
|
|
1470
|
+
}
|
|
1471
|
+
const id = yield* pathId(DeviceId, forgetDevice[1]);
|
|
1472
|
+
if (hub.hasDevice(id)) {
|
|
1473
|
+
return yield* new RequestError({
|
|
1474
|
+
status: 409,
|
|
1475
|
+
message: `Device ${id} must be offline before it can be forgotten`
|
|
1476
|
+
});
|
|
1477
|
+
}
|
|
1478
|
+
const forgotten = yield* store.forgetDevice(id);
|
|
1479
|
+
hub.closeDevice(id);
|
|
1480
|
+
return json(forgotten);
|
|
1481
|
+
}
|
|
1417
1482
|
if (url.pathname === "/api/tasks" && request.method === "POST") {
|
|
1418
1483
|
const input = yield* body(request, decodeCreateTaskInput);
|
|
1419
1484
|
const target = yield* chooseDevice(input, sourceDeviceId);
|
|
@@ -1444,7 +1509,7 @@ Content-Length: 0\r
|
|
|
1444
1509
|
}
|
|
1445
1510
|
return yield* new RequestError({ status: 404, message: "Route not found" });
|
|
1446
1511
|
}).pipe(Effect10.mapError((cause) => cause instanceof RequestError ? cause : new RequestError({
|
|
1447
|
-
status: cause._tag !== "RelayStore.PersistenceError" ? 500 : cause.message.startsWith("Unknown") ? 404 : cause.message.includes("outstanding task limit") ? 429 : 500,
|
|
1512
|
+
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") ? 409 : 500,
|
|
1448
1513
|
message: cause.message
|
|
1449
1514
|
})));
|
|
1450
1515
|
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));
|
|
@@ -1543,12 +1608,14 @@ Content-Length: 0\r
|
|
|
1543
1608
|
});
|
|
1544
1609
|
await new Promise((resolve3, reject) => {
|
|
1545
1610
|
server.once("error", reject);
|
|
1546
|
-
server.listen(configuration
|
|
1611
|
+
server.listen(relayListenOptions(configuration), () => {
|
|
1547
1612
|
server.off("error", reject);
|
|
1548
1613
|
resolve3();
|
|
1549
1614
|
});
|
|
1550
1615
|
});
|
|
1551
|
-
|
|
1616
|
+
const address = server.address();
|
|
1617
|
+
const listener = typeof address === "string" ? address : address?.family === "IPv6" ? `[${address.address}]:${address.port}` : `${address?.address ?? configuration.host}:${address?.port ?? configuration.port}`;
|
|
1618
|
+
console.log(`Cohall relay listening on http://${listener}`);
|
|
1552
1619
|
let stopping = false;
|
|
1553
1620
|
const shutdown = async () => {
|
|
1554
1621
|
if (stopping) {
|
|
@@ -1684,6 +1751,7 @@ var make = (options) => {
|
|
|
1684
1751
|
});
|
|
1685
1752
|
return Service.of({
|
|
1686
1753
|
devices: () => request("RelayClient.devices", "/api/devices", Schema2.Array(Device)),
|
|
1754
|
+
forgetDevice: (deviceId) => request("RelayClient.forgetDevice", `/api/devices/${encodeURIComponent(deviceId)}/forget`, Device, { method: "POST" }),
|
|
1687
1755
|
createTask: (input) => request("RelayClient.createTask", "/api/tasks", Task, {
|
|
1688
1756
|
method: "POST",
|
|
1689
1757
|
body: JSON.stringify(input)
|
|
@@ -2160,6 +2228,22 @@ a lead agent.
|
|
|
2160
2228
|
Use the installed \`cohall\` executable when it is available. Fall back to
|
|
2161
2229
|
\`npx -y @akshar5/cohall\` only when Cohall is not installed globally.
|
|
2162
2230
|
|
|
2231
|
+
## Recognize cross-device requests
|
|
2232
|
+
|
|
2233
|
+
Treat phrases such as “run this on my Mac,” “ask \`@devbox\`,” or “have the Linux
|
|
2234
|
+
machine check this” as target intent. The \`@name\` form is a Cohall device
|
|
2235
|
+
selector, not chat syntax supplied by the harness. Resolve it with \`cohall
|
|
2236
|
+
devices\`, then delegate the smallest useful outcome.
|
|
2237
|
+
|
|
2238
|
+
Good reasons to delegate include:
|
|
2239
|
+
|
|
2240
|
+
- Xcode, simulators, signing state, or a signed-in browser on a Mac;
|
|
2241
|
+
- Docker services, deployment access, or a repository checkout on a server;
|
|
2242
|
+
- long tests that can run remotely while useful local work continues;
|
|
2243
|
+
- any machine-local tool, file, login, or network access unavailable here.
|
|
2244
|
+
|
|
2245
|
+
Do not delegate ordinary local work when the other device provides no advantage.
|
|
2246
|
+
|
|
2163
2247
|
## Delegate work
|
|
2164
2248
|
|
|
2165
2249
|
1. List devices unless the target is already explicit:
|
|
@@ -2224,6 +2308,12 @@ cohall status <task-id>
|
|
|
2224
2308
|
cohall wait <task-id> --timeout 1800
|
|
2225
2309
|
\`\`\`
|
|
2226
2310
|
|
|
2311
|
+
An accepted task for an offline target waits durably on the relay and dispatches
|
|
2312
|
+
when the device reconnects. Accepted tasks survive relay and device restarts.
|
|
2313
|
+
The relay itself must be reachable to accept a new task. Interrupted execution
|
|
2314
|
+
uses at-least-once delivery and may run again, so make consequential prompts safe
|
|
2315
|
+
to retry.
|
|
2316
|
+
|
|
2227
2317
|
The timeout error includes the task ID and last known status. The task continues
|
|
2228
2318
|
unless cancelled:
|
|
2229
2319
|
|
|
@@ -2323,6 +2413,15 @@ var defaultConfigDirectory = () => {
|
|
|
2323
2413
|
}
|
|
2324
2414
|
return join2(process.env.XDG_CONFIG_HOME ?? join2(homedir2(), ".config"), "cohall");
|
|
2325
2415
|
};
|
|
2416
|
+
var defaultRelayDataDirectory = () => {
|
|
2417
|
+
if (platform2() === "win32") {
|
|
2418
|
+
return join2(process.env.LOCALAPPDATA ?? join2(homedir2(), "AppData", "Local"), "Cohall");
|
|
2419
|
+
}
|
|
2420
|
+
if (platform2() === "darwin") {
|
|
2421
|
+
return join2(homedir2(), "Library", "Application Support", "Cohall", "relay");
|
|
2422
|
+
}
|
|
2423
|
+
return join2(process.env.XDG_DATA_HOME ?? join2(homedir2(), ".local", "share"), "cohall");
|
|
2424
|
+
};
|
|
2326
2425
|
var configurationPath = () => resolve(process.env.COHALL_CONFIG ?? join2(defaultConfigDirectory(), "config.json"));
|
|
2327
2426
|
var normalizeRelayUrl = (input) => {
|
|
2328
2427
|
const url = new URL(input);
|
|
@@ -2454,9 +2553,12 @@ var loadClientConfiguration = Effect3.tryPromise({
|
|
|
2454
2553
|
var loadOwnerConfiguration = Effect3.tryPromise({
|
|
2455
2554
|
try: async () => {
|
|
2456
2555
|
const stored = await storedOrDefaults();
|
|
2457
|
-
const
|
|
2458
|
-
|
|
2459
|
-
|
|
2556
|
+
const ownerTokenPath = join2(resolve(process.env.COHALL_DATA_DIR ?? defaultRelayDataDirectory()), "owner-token");
|
|
2557
|
+
const token = process.env.COHALL_TOKEN ?? await readFile(ownerTokenPath, "utf8").then((value) => value.trim()).catch(() => {
|
|
2558
|
+
return;
|
|
2559
|
+
});
|
|
2560
|
+
if (token === undefined || token.length === 0) {
|
|
2561
|
+
throw new Error("No owner credential. Run this command on the relay host or set COHALL_TOKEN.");
|
|
2460
2562
|
}
|
|
2461
2563
|
return ClientConfiguration.make({
|
|
2462
2564
|
relayUrl: normalizeRelayUrl(process.env.COHALL_RELAY_URL ?? stored.relayUrl),
|
|
@@ -3013,6 +3115,7 @@ Usage:
|
|
|
3013
3115
|
cohall pair [--label name] [--client-only]
|
|
3014
3116
|
cohall sessions
|
|
3015
3117
|
cohall revoke <session-id>
|
|
3118
|
+
cohall forget <device-id>
|
|
3016
3119
|
cohall doctor
|
|
3017
3120
|
cohall upgrade [--to version|latest] [--no-restart] [--dry-run]
|
|
3018
3121
|
cohall device
|
|
@@ -3386,7 +3489,7 @@ var runCli = async (command, raw) => {
|
|
|
3386
3489
|
});
|
|
3387
3490
|
return;
|
|
3388
3491
|
}
|
|
3389
|
-
if (command === "pair" || command === "sessions" || command === "revoke") {
|
|
3492
|
+
if (command === "pair" || command === "sessions" || command === "revoke" || command === "forget") {
|
|
3390
3493
|
const { configuration: configuration2, relay: relay2 } = await ownerClient();
|
|
3391
3494
|
if (command === "pair") {
|
|
3392
3495
|
allowOptions(arguments_, ["client-only", "label"]);
|
|
@@ -3407,6 +3510,11 @@ var runCli = async (command, raw) => {
|
|
|
3407
3510
|
print(await Effect5.runPromise(relay2.authSessions()));
|
|
3408
3511
|
return;
|
|
3409
3512
|
}
|
|
3513
|
+
if (command === "forget") {
|
|
3514
|
+
allowOptions(arguments_, []);
|
|
3515
|
+
print(await Effect5.runPromise(relay2.forgetDevice(Schema7.decodeUnknownSync(DeviceId)(identifier(arguments_, "device id")))));
|
|
3516
|
+
return;
|
|
3517
|
+
}
|
|
3410
3518
|
allowOptions(arguments_, []);
|
|
3411
3519
|
print(await Effect5.runPromise(relay2.revokeAuthSession(Schema7.decodeUnknownSync(AuthSessionId)(identifier(arguments_, "session id")))));
|
|
3412
3520
|
return;
|
|
@@ -3925,4 +4033,4 @@ await main().catch((cause) => {
|
|
|
3925
4033
|
process.exitCode = 1;
|
|
3926
4034
|
});
|
|
3927
4035
|
|
|
3928
|
-
//# debugId=
|
|
4036
|
+
//# debugId=0FE6295B18AAAAEC64756E2164756E21
|