@akshar5/cohall 0.4.0 → 0.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +17 -0
- package/README.md +4 -1
- package/bin/cohall.js +97 -25
- package/bin/cohall.js.map +6 -6
- package/docs/install.md +5 -2
- package/docs/services.md +6 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.4.2](https://github.com/AksharP5/cohall/compare/v0.4.1...v0.4.2) (2026-08-07)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
* **upgrade:** detect service installation mismatches ([19bf066](https://github.com/AksharP5/cohall/commit/19bf06674d6fac87576f804e92d863d5c673dda0))
|
|
9
|
+
|
|
10
|
+
## [0.4.1](https://github.com/AksharP5/cohall/compare/v0.4.0...v0.4.1) (2026-08-07)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Bug Fixes
|
|
14
|
+
|
|
15
|
+
* **device:** bound relay message buffering ([15b975e](https://github.com/AksharP5/cohall/commit/15b975eb658ac8b1140ed0481ab4726543265ca4))
|
|
16
|
+
* **providers:** bound JSON events while streaming ([7e0a4fc](https://github.com/AksharP5/cohall/commit/7e0a4fccc1b68e630b0b8bb553bbb66b84795798))
|
|
17
|
+
* **providers:** preserve custom config for nested delegation ([#21](https://github.com/AksharP5/cohall/issues/21)) ([63c95f8](https://github.com/AksharP5/cohall/commit/63c95f84f00ad03ad8d017696a5a87cbb8c6c05d))
|
|
18
|
+
* **upgrade:** restart already-current services ([a2c775f](https://github.com/AksharP5/cohall/commit/a2c775fe48a4dffda54ea7537f34b1a2b6d1e6ed))
|
|
19
|
+
|
|
3
20
|
## [0.4.0](https://github.com/AksharP5/cohall/compare/v0.3.4...v0.4.0) (2026-08-07)
|
|
4
21
|
|
|
5
22
|
|
package/README.md
CHANGED
|
@@ -223,7 +223,10 @@ status, provider selection, executable paths, and version information.
|
|
|
223
223
|
|
|
224
224
|
For a global installation used by services, `cohall upgrade` updates through
|
|
225
225
|
the same npm, Bun, or pnpm installation and restarts only active Cohall relay
|
|
226
|
-
and device jobs.
|
|
226
|
+
and device jobs. Active jobs restart even when the installed files are already
|
|
227
|
+
current, ensuring an older loaded process is replaced. Cohall refuses to restart
|
|
228
|
+
a service configured to use a different global installation; run that service's
|
|
229
|
+
executable directly or update its service definition first. Use `cohall upgrade --dry-run` to inspect the plan or
|
|
227
230
|
`cohall upgrade --no-restart` to update files without restarting services.
|
|
228
231
|
Environment variables override stored values:
|
|
229
232
|
|
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.2", 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(() => {
|
|
@@ -1742,7 +1742,6 @@ import { chmod, mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
|
1742
1742
|
import { homedir, platform, tmpdir } from "node:os";
|
|
1743
1743
|
import { delimiter, extname, isAbsolute, join } from "node:path";
|
|
1744
1744
|
import { spawn } from "node:child_process";
|
|
1745
|
-
import { createInterface } from "node:readline";
|
|
1746
1745
|
|
|
1747
1746
|
class ProviderUnavailableError extends Schema3.TaggedErrorClass()("CohallProvider.Unavailable", { provider: Provider, message: Schema3.String }) {
|
|
1748
1747
|
}
|
|
@@ -1759,7 +1758,7 @@ var executables = {
|
|
|
1759
1758
|
opencode: "opencode"
|
|
1760
1759
|
};
|
|
1761
1760
|
var providerEnvironment = () => ({
|
|
1762
|
-
...Object.fromEntries(Object.entries(process.env).filter(([name]) => !name.startsWith("COHALL_"))),
|
|
1761
|
+
...Object.fromEntries(Object.entries(process.env).filter(([name]) => !name.startsWith("COHALL_") || name === "COHALL_CONFIG")),
|
|
1763
1762
|
PATH: executableDirectories().join(delimiter)
|
|
1764
1763
|
});
|
|
1765
1764
|
var record = (value) => typeof value === "object" && value !== null && !Array.isArray(value) ? Object.fromEntries(Object.entries(value)) : undefined;
|
|
@@ -1927,22 +1926,43 @@ var prepareCommand = async (options) => {
|
|
|
1927
1926
|
}
|
|
1928
1927
|
};
|
|
1929
1928
|
var forEachJsonEvent = async (stdout, onEvent) => {
|
|
1930
|
-
const
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1929
|
+
const limit = 1024 * 1024;
|
|
1930
|
+
let chunks = [];
|
|
1931
|
+
let size = 0;
|
|
1932
|
+
const append = (chunk) => {
|
|
1933
|
+
if (size + chunk.byteLength > limit) {
|
|
1934
|
+
throw new Error("Provider event exceeded 1024 KiB");
|
|
1935
|
+
}
|
|
1936
|
+
if (chunk.byteLength > 0) {
|
|
1937
|
+
chunks.push(chunk);
|
|
1938
|
+
size += chunk.byteLength;
|
|
1939
|
+
}
|
|
1940
|
+
};
|
|
1941
|
+
const emit = () => {
|
|
1942
|
+
const line = Buffer.concat(chunks, size).toString("utf8");
|
|
1943
|
+
chunks = [];
|
|
1944
|
+
size = 0;
|
|
1945
|
+
if (line.trim().length === 0) {
|
|
1946
|
+
return;
|
|
1947
|
+
}
|
|
1948
|
+
const event = record(JSON.parse(line));
|
|
1949
|
+
if (event !== undefined) {
|
|
1950
|
+
onEvent(event);
|
|
1951
|
+
}
|
|
1952
|
+
};
|
|
1953
|
+
for await (const chunk of stdout) {
|
|
1954
|
+
const bytes = Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk));
|
|
1955
|
+
let start = 0;
|
|
1956
|
+
for (let newline = bytes.indexOf(10, start);newline !== -1; ) {
|
|
1957
|
+
append(bytes.subarray(start, newline));
|
|
1958
|
+
emit();
|
|
1959
|
+
start = newline + 1;
|
|
1960
|
+
newline = bytes.indexOf(10, start);
|
|
1943
1961
|
}
|
|
1944
|
-
|
|
1945
|
-
|
|
1962
|
+
append(bytes.subarray(start));
|
|
1963
|
+
}
|
|
1964
|
+
if (size > 0) {
|
|
1965
|
+
emit();
|
|
1946
1966
|
}
|
|
1947
1967
|
};
|
|
1948
1968
|
var parseCodex = async (stdout, existingSession) => {
|
|
@@ -2596,6 +2616,7 @@ var normalizeUpgradeTarget = (target) => {
|
|
|
2596
2616
|
}
|
|
2597
2617
|
return normalized;
|
|
2598
2618
|
};
|
|
2619
|
+
var deviceVersionWarning = (cliVersion, deviceVersion) => deviceVersion === undefined || deviceVersion === cliVersion ? undefined : `Running device daemon uses Cohall ${deviceVersion}, while this CLI is ${cliVersion}. Run upgrade through the executable configured by the device service.`;
|
|
2599
2620
|
var packageInstallation = (canonicalEntrypoint, entrypoint = canonicalEntrypoint) => {
|
|
2600
2621
|
const path = normalizePath(canonicalEntrypoint);
|
|
2601
2622
|
const marker = `/node_modules/${packageName}/`;
|
|
@@ -2645,6 +2666,13 @@ var serviceCandidates = (runtimePlatform, uid) => {
|
|
|
2645
2666
|
manager,
|
|
2646
2667
|
device,
|
|
2647
2668
|
check: { command: "systemctl", arguments: [...user, "is-active", "--quiet", unit] },
|
|
2669
|
+
entrypoint: {
|
|
2670
|
+
inspect: {
|
|
2671
|
+
command: "systemctl",
|
|
2672
|
+
arguments: [...user, "show", "--property=ExecStart", "--value", unit]
|
|
2673
|
+
},
|
|
2674
|
+
parse: (output) => output.match(/\bpath=(.*?)\s*;/)?.[1]?.trim()
|
|
2675
|
+
},
|
|
2648
2676
|
restart: [{ command: "systemctl", arguments: [...user, "restart", unit] }]
|
|
2649
2677
|
};
|
|
2650
2678
|
};
|
|
@@ -2663,6 +2691,10 @@ var serviceCandidates = (runtimePlatform, uid) => {
|
|
|
2663
2691
|
device,
|
|
2664
2692
|
check: { command: "launchctl", arguments: ["print", `${domain}/${label}`] },
|
|
2665
2693
|
activeOutput: /\bstate\s*=\s*running\b/,
|
|
2694
|
+
entrypoint: {
|
|
2695
|
+
inspect: { command: "launchctl", arguments: ["print", `${domain}/${label}`] },
|
|
2696
|
+
parse: (output) => output.match(/^\s*program\s*=\s*(.+)$/m)?.[1]?.trim()
|
|
2697
|
+
},
|
|
2666
2698
|
restart: [{ command: "launchctl", arguments: ["kickstart", "-k", `${domain}/${label}`] }]
|
|
2667
2699
|
});
|
|
2668
2700
|
return [launchd("com.cohall.relay", false), launchd("com.cohall.device", true)];
|
|
@@ -2727,6 +2759,26 @@ ${result.stderr}`))) {
|
|
|
2727
2759
|
}
|
|
2728
2760
|
return active;
|
|
2729
2761
|
};
|
|
2762
|
+
var assertServiceInstallations = async (runner, services, canonicalEntrypoint, entrypoint) => {
|
|
2763
|
+
for (const service of services) {
|
|
2764
|
+
if (service.entrypoint === undefined) {
|
|
2765
|
+
continue;
|
|
2766
|
+
}
|
|
2767
|
+
const result = await checked(runner, service.entrypoint.inspect, 1e4);
|
|
2768
|
+
const serviceEntrypoint = service.entrypoint.parse(`${result.stdout}
|
|
2769
|
+
${result.stderr}`);
|
|
2770
|
+
if (serviceEntrypoint === undefined) {
|
|
2771
|
+
throw new Error(`Could not determine the executable used by active ${service.label}`);
|
|
2772
|
+
}
|
|
2773
|
+
const canonicalServiceEntrypoint = await realpath2(serviceEntrypoint).catch((cause) => {
|
|
2774
|
+
throw new Error(`Could not resolve the executable used by active ${service.label}: ${cause instanceof Error ? cause.message : String(cause)}`);
|
|
2775
|
+
});
|
|
2776
|
+
if (canonicalServiceEntrypoint === canonicalEntrypoint) {
|
|
2777
|
+
continue;
|
|
2778
|
+
}
|
|
2779
|
+
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.`);
|
|
2780
|
+
}
|
|
2781
|
+
};
|
|
2730
2782
|
var readReceipt = async (path) => readFile2(path, "utf8").then((content) => Schema6.decodeUnknownOption(RestartReceipt)(JSON.parse(content))).then((decoded) => decoded._tag === "Some" ? decoded.value : undefined).catch(() => {
|
|
2731
2783
|
return;
|
|
2732
2784
|
});
|
|
@@ -2827,9 +2879,11 @@ var upgrade = async (options) => {
|
|
|
2827
2879
|
if (entrypoint === undefined) {
|
|
2828
2880
|
throw new Error("Could not resolve the Cohall executable path");
|
|
2829
2881
|
}
|
|
2830
|
-
const
|
|
2882
|
+
const canonicalEntrypoint = await realpath2(entrypoint);
|
|
2883
|
+
const installation = packageInstallation(canonicalEntrypoint, entrypoint);
|
|
2831
2884
|
const install = packageInstallCommand(installation, target);
|
|
2832
2885
|
const services = await activeServices(runner, candidates);
|
|
2886
|
+
await assertServiceInstallations(runner, services, canonicalEntrypoint, entrypoint);
|
|
2833
2887
|
if (options.dryRun) {
|
|
2834
2888
|
return {
|
|
2835
2889
|
upgraded: false,
|
|
@@ -2849,7 +2903,7 @@ var upgrade = async (options) => {
|
|
|
2849
2903
|
throw new Error(`Installed Cohall ${nextVersion}, expected ${target}`);
|
|
2850
2904
|
}
|
|
2851
2905
|
const upgraded = nextVersion !== options.currentVersion;
|
|
2852
|
-
if (!
|
|
2906
|
+
if (!options.restart || services.length === 0) {
|
|
2853
2907
|
return {
|
|
2854
2908
|
upgraded,
|
|
2855
2909
|
from_version: options.currentVersion,
|
|
@@ -2857,7 +2911,7 @@ var upgrade = async (options) => {
|
|
|
2857
2911
|
requested_version: target,
|
|
2858
2912
|
package_manager: installation.manager,
|
|
2859
2913
|
services_restarted: [],
|
|
2860
|
-
services_pending_restart:
|
|
2914
|
+
services_pending_restart: options.restart ? [] : services.map((service) => service.id),
|
|
2861
2915
|
resumed_after_restart: false,
|
|
2862
2916
|
dry_run: false
|
|
2863
2917
|
};
|
|
@@ -2873,7 +2927,7 @@ var upgrade = async (options) => {
|
|
|
2873
2927
|
const completed = await restartServices(runner, services, statePath, initial);
|
|
2874
2928
|
await rm2(statePath, { force: true });
|
|
2875
2929
|
return {
|
|
2876
|
-
upgraded
|
|
2930
|
+
upgraded,
|
|
2877
2931
|
from_version: options.currentVersion,
|
|
2878
2932
|
installed_version: nextVersion,
|
|
2879
2933
|
requested_version: target,
|
|
@@ -3279,8 +3333,10 @@ var runCli = async (command, raw) => {
|
|
|
3279
3333
|
const currentDevice = response?.ok !== true || clientToken === undefined || deviceId === undefined ? undefined : await Effect5.runPromise(exports_src.make({ baseUrl: relayUrl, token: clientToken }).devices()).then((devices) => devices.find((device) => device.id === deviceId)).catch(() => {
|
|
3280
3334
|
return;
|
|
3281
3335
|
});
|
|
3336
|
+
const versionWarning = deviceVersionWarning(version, currentDevice?.version);
|
|
3282
3337
|
const warnings = [
|
|
3283
3338
|
...response?.ok === true ? [] : ["Relay is unreachable"],
|
|
3339
|
+
...versionWarning === undefined ? [] : [versionWarning],
|
|
3284
3340
|
...hasDeviceCredential && response?.ok === true && currentDevice?.status !== "online" && currentDevice?.status !== "busy" ? [
|
|
3285
3341
|
clientToken === undefined ? "Device status cannot be checked without a client credential" : "Device daemon is not connected or is not registered with the relay"
|
|
3286
3342
|
] : [],
|
|
@@ -3420,6 +3476,8 @@ import { realpath as realpath3 } from "node:fs/promises";
|
|
|
3420
3476
|
import { arch, hostname as hostname2, platform as platform3 } from "node:os";
|
|
3421
3477
|
import { basename, isAbsolute as isAbsolute2, relative } from "node:path";
|
|
3422
3478
|
import { WebSocket } from "ws";
|
|
3479
|
+
var maxRelayPayloadBytes = 256 * 1024;
|
|
3480
|
+
var maxQueuedRelayMessages = 8;
|
|
3423
3481
|
|
|
3424
3482
|
class DeviceConnectionError extends Schema8.TaggedErrorClass()("Device.ConnectionError", { message: Schema8.String }) {
|
|
3425
3483
|
}
|
|
@@ -3618,7 +3676,10 @@ var cancel = (state, taskId) => {
|
|
|
3618
3676
|
};
|
|
3619
3677
|
var connect = (configuration, state) => Effect6.tryPromise({
|
|
3620
3678
|
try: (signal) => new Promise((complete) => {
|
|
3621
|
-
const socket = new WebSocket(socketUrl(configuration)
|
|
3679
|
+
const socket = new WebSocket(socketUrl(configuration), {
|
|
3680
|
+
maxPayload: maxRelayPayloadBytes,
|
|
3681
|
+
perMessageDeflate: false
|
|
3682
|
+
});
|
|
3622
3683
|
const heartbeat = setInterval(() => {
|
|
3623
3684
|
send(state, SocketEvent.make({
|
|
3624
3685
|
_tag: "DeviceHeartbeat",
|
|
@@ -3627,6 +3688,7 @@ var connect = (configuration, state) => Effect6.tryPromise({
|
|
|
3627
3688
|
}));
|
|
3628
3689
|
}, 15000);
|
|
3629
3690
|
let closed = false;
|
|
3691
|
+
let queuedMessages = 0;
|
|
3630
3692
|
const close = () => {
|
|
3631
3693
|
if (closed) {
|
|
3632
3694
|
return;
|
|
@@ -3646,8 +3708,16 @@ var connect = (configuration, state) => Effect6.tryPromise({
|
|
|
3646
3708
|
socket.send(JSON.stringify(SocketEvent.make({ _tag: "Authenticate", token: configuration.token })));
|
|
3647
3709
|
});
|
|
3648
3710
|
socket.on("message", (message) => {
|
|
3649
|
-
|
|
3711
|
+
if (queuedMessages >= maxQueuedRelayMessages) {
|
|
3712
|
+
socket.close(4008, "Message queue limit reached");
|
|
3713
|
+
return;
|
|
3714
|
+
}
|
|
3715
|
+
queuedMessages += 1;
|
|
3650
3716
|
state.processing = state.processing.then(async () => {
|
|
3717
|
+
if (closed) {
|
|
3718
|
+
return;
|
|
3719
|
+
}
|
|
3720
|
+
const text2 = 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");
|
|
3651
3721
|
const event = await Effect6.runPromise(Effect6.try({
|
|
3652
3722
|
try: () => JSON.parse(text2),
|
|
3653
3723
|
catch: () => new DeviceConnectionError({ message: "Relay sent invalid JSON" })
|
|
@@ -3680,6 +3750,8 @@ var connect = (configuration, state) => Effect6.tryPromise({
|
|
|
3680
3750
|
}
|
|
3681
3751
|
}).catch((cause) => {
|
|
3682
3752
|
console.error(`Invalid relay event: ${cause instanceof Error ? cause.message : String(cause)}`);
|
|
3753
|
+
}).finally(() => {
|
|
3754
|
+
queuedMessages = Math.max(0, queuedMessages - 1);
|
|
3683
3755
|
});
|
|
3684
3756
|
});
|
|
3685
3757
|
socket.once("close", close);
|
|
@@ -3826,4 +3898,4 @@ await main().catch((cause) => {
|
|
|
3826
3898
|
process.exitCode = 1;
|
|
3827
3899
|
});
|
|
3828
3900
|
|
|
3829
|
-
//# debugId=
|
|
3901
|
+
//# debugId=D69D718E229B95B264756E2164756E21
|