@akshar5/cohall 0.10.1 → 0.11.0
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 +18 -0
- package/README.md +3 -1
- package/bin/cohall.js +532 -133
- package/bin/cohall.js.map +13 -12
- package/docs/install.md +15 -3
- package/docs/integrations.md +31 -2
- package/docs/onboarding.md +116 -0
- package/docs/services.md +4 -0
- package/package.json +1 -1
package/bin/cohall.js
CHANGED
|
@@ -17,7 +17,7 @@ var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
|
17
17
|
|
|
18
18
|
// packages/protocol/src/index.ts
|
|
19
19
|
import { Effect, Schema } from "effect";
|
|
20
|
-
var version = "0.
|
|
20
|
+
var version = "0.11.0", maxSocketPayloadBytes, maxAttachmentBytes, maxTaskAttachments = 2, maxProgressNoteBytes = 1024, bounded = (maxLength) => Schema.String.check(Schema.isMinLength(1), Schema.isMaxLength(maxLength)), optionalText = (maxLength) => Schema.String.check(Schema.isMaxLength(maxLength)), boundedArray = (schema, maxLength) => Schema.Array(schema).check(Schema.isMaxLength(maxLength)), count, uuid = (name) => Schema.String.check(Schema.isUUID(4)).pipe(Schema.brand(name)), isoTimestamp = (name) => Schema.String.check(Schema.isPattern(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,9})?Z$/, {
|
|
21
21
|
expected: "an ISO-8601 UTC timestamp"
|
|
22
22
|
})).pipe(Schema.brand(name)), BotId, Bot, DeviceId, ThreadId, MessageId, TaskId, TaskRequestId, TaskRunId, ClarificationId, AuthSessionId, OperationId, Timestamp, TaskDeadline, taskDeadlineError = "Task deadline exceeded", taskDeadlinePassed = (task, time = Date.now()) => task.expiresAt !== undefined && Date.parse(task.expiresAt) <= time, Platform, Provider, DeviceStatus, TaskStatus, OperationStatus, TaskTraceEventKind, MessageRole, ConnectionRole, AuthSession, CreatePairingInput, PairingCredential, ExchangePairingInput, SessionCredential, PairingResult, Capability, Workspace, Device, maxDevicePageDevices = 16, maxDevicePageResponseBytes, DevicePage, minimumDeviceOperationVersion = "0.5.0", parsedVersion = (value) => {
|
|
23
23
|
const match = /^v?(\d+)\.(\d+)\.(\d+)(-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/.exec(value);
|
|
@@ -420,25 +420,25 @@ var init_src = __esm(() => {
|
|
|
420
420
|
});
|
|
421
421
|
|
|
422
422
|
// apps/relay/src/config.ts
|
|
423
|
-
import { Effect as Effect13, Schema as
|
|
424
|
-
import { access as access5, chmod as chmod7, mkdir as
|
|
423
|
+
import { Effect as Effect13, Schema as Schema15 } from "effect";
|
|
424
|
+
import { access as access5, chmod as chmod7, mkdir as mkdir9, readFile as readFile4, writeFile as writeFile10 } from "node:fs/promises";
|
|
425
425
|
import { homedir as homedir5, platform as platform5 } from "node:os";
|
|
426
|
-
import { isAbsolute as isAbsolute6, join as
|
|
426
|
+
import { isAbsolute as isAbsolute6, join as join10, resolve as resolve5 } from "node:path";
|
|
427
427
|
var RelayConfiguration, defaultDataDirectory = () => {
|
|
428
428
|
if (platform5() === "win32") {
|
|
429
|
-
return
|
|
429
|
+
return join10(process.env.LOCALAPPDATA ?? join10(homedir5(), "AppData", "Local"), "Cohall");
|
|
430
430
|
}
|
|
431
431
|
if (platform5() === "darwin") {
|
|
432
|
-
return
|
|
432
|
+
return join10(homedir5(), "Library", "Application Support", "Cohall", "relay");
|
|
433
433
|
}
|
|
434
434
|
const dataHome = process.env.XDG_DATA_HOME;
|
|
435
|
-
return
|
|
435
|
+
return join10(dataHome !== undefined && isAbsolute6(dataHome) ? dataHome : join10(homedir5(), ".local", "share"), "cohall");
|
|
436
436
|
}, generatedToken = () => Array.from(crypto.getRandomValues(new Uint8Array(32)), (value) => value.toString(16).padStart(2, "0")).join(""), tokenFrom = async (path) => {
|
|
437
437
|
if (await access5(path).then(() => true).catch(() => false)) {
|
|
438
438
|
return (await readFile4(path, "utf8")).trim();
|
|
439
439
|
}
|
|
440
440
|
const token = generatedToken();
|
|
441
|
-
await
|
|
441
|
+
await writeFile10(path, `${token}
|
|
442
442
|
`, { mode: 384 });
|
|
443
443
|
if (platform5() !== "win32") {
|
|
444
444
|
await chmod7(path, 384);
|
|
@@ -455,14 +455,14 @@ var RelayConfiguration, defaultDataDirectory = () => {
|
|
|
455
455
|
throw new Error(`${name} must be true or false`);
|
|
456
456
|
}, isLoopback2 = (host) => host === "127.0.0.1" || host === "localhost" || host === "::1", loadEnvironmentConfiguration;
|
|
457
457
|
var init_config = __esm(() => {
|
|
458
|
-
RelayConfiguration =
|
|
459
|
-
host:
|
|
460
|
-
port:
|
|
461
|
-
token:
|
|
462
|
-
dataDirectory:
|
|
463
|
-
databasePath:
|
|
464
|
-
allowRemote:
|
|
465
|
-
historyTaskLimit:
|
|
458
|
+
RelayConfiguration = Schema15.Struct({
|
|
459
|
+
host: Schema15.NonEmptyString,
|
|
460
|
+
port: Schema15.Int.check(Schema15.isBetween({ minimum: 1, maximum: 65535 })),
|
|
461
|
+
token: Schema15.String.check(Schema15.isMinLength(32)),
|
|
462
|
+
dataDirectory: Schema15.NonEmptyString,
|
|
463
|
+
databasePath: Schema15.NonEmptyString,
|
|
464
|
+
allowRemote: Schema15.Boolean,
|
|
465
|
+
historyTaskLimit: Schema15.Int.check(Schema15.isBetween({ minimum: 100, maximum: 1e5 }))
|
|
466
466
|
});
|
|
467
467
|
loadEnvironmentConfiguration = Effect13.tryPromise({
|
|
468
468
|
try: async () => {
|
|
@@ -473,21 +473,21 @@ var init_config = __esm(() => {
|
|
|
473
473
|
throw new Error("Remote relay binding is disabled. Use a private network or TLS reverse proxy, then set COHALL_RELAY_ALLOW_REMOTE=true.");
|
|
474
474
|
}
|
|
475
475
|
const dataDirectory = resolve5(process.env.COHALL_DATA_DIR ?? defaultDataDirectory());
|
|
476
|
-
await
|
|
476
|
+
await mkdir9(dataDirectory, { recursive: true, mode: 448 });
|
|
477
477
|
if (platform5() !== "win32") {
|
|
478
478
|
await chmod7(dataDirectory, 448);
|
|
479
479
|
}
|
|
480
|
-
const token = process.env.COHALL_TOKEN ?? await tokenFrom(
|
|
480
|
+
const token = process.env.COHALL_TOKEN ?? await tokenFrom(join10(dataDirectory, "owner-token"));
|
|
481
481
|
if (token.length < 32) {
|
|
482
482
|
throw new Error("The Cohall relay owner token must be at least 32 characters");
|
|
483
483
|
}
|
|
484
484
|
const historyTaskLimit = Number(process.env.COHALL_HISTORY_TASK_LIMIT ?? "1000");
|
|
485
|
-
return
|
|
485
|
+
return Schema15.decodeUnknownSync(RelayConfiguration)({
|
|
486
486
|
host,
|
|
487
487
|
port,
|
|
488
488
|
token,
|
|
489
489
|
dataDirectory,
|
|
490
|
-
databasePath:
|
|
490
|
+
databasePath: join10(dataDirectory, "cohall.db"),
|
|
491
491
|
allowRemote,
|
|
492
492
|
historyTaskLimit
|
|
493
493
|
});
|
|
@@ -673,12 +673,12 @@ __export(exports_store, {
|
|
|
673
673
|
RelayStore: () => exports_store,
|
|
674
674
|
PersistenceError: () => PersistenceError
|
|
675
675
|
});
|
|
676
|
-
import { Context as Context2, Effect as Effect14, Layer as Layer2, Schema as
|
|
676
|
+
import { Context as Context2, Effect as Effect14, Layer as Layer2, Schema as Schema16 } from "effect";
|
|
677
677
|
import { createHash as createHash2, randomBytes } from "node:crypto";
|
|
678
678
|
var PersistenceError, TaskProgressError, TaskInputError, TaskDeadlineError, TaskRequestError, Service2, operationError = (operation) => (cause) => new PersistenceError({
|
|
679
679
|
operation,
|
|
680
680
|
message: cause instanceof Error ? cause.message : String(cause)
|
|
681
|
-
}), decode2 = (operation, schema, value) =>
|
|
681
|
+
}), decode2 = (operation, schema, value) => Schema16.decodeUnknownEffect(schema)(value).pipe(Effect14.mapError(operationError(operation))), threadFromRow = (row) => decode2("RelayStore.decodeThread", Thread, {
|
|
682
682
|
id: row.id,
|
|
683
683
|
title: row.title,
|
|
684
684
|
createdAt: row.created_at,
|
|
@@ -777,7 +777,7 @@ var PersistenceError, TaskProgressError, TaskInputError, TaskDeadlineError, Task
|
|
|
777
777
|
kind: row.kind,
|
|
778
778
|
at: row.created_at,
|
|
779
779
|
...row.detail === null ? {} : { detail: row.detail }
|
|
780
|
-
}), rolesFromRow = (row) =>
|
|
780
|
+
}), rolesFromRow = (row) => Schema16.decodeUnknownSync(Schema16.Array(ConnectionRole))(JSON.parse(row.roles_json)), authSessionFromRow = (row) => {
|
|
781
781
|
const roles = rolesFromRow(row);
|
|
782
782
|
const role = roles[0];
|
|
783
783
|
return role === undefined ? Effect14.fail(new PersistenceError({
|
|
@@ -1115,9 +1115,9 @@ var PersistenceError, TaskProgressError, TaskInputError, TaskDeadlineError, Task
|
|
|
1115
1115
|
const devices = new Map;
|
|
1116
1116
|
let retainedTasks = 0;
|
|
1117
1117
|
for (const row of rows) {
|
|
1118
|
-
const status =
|
|
1119
|
-
const provider =
|
|
1120
|
-
const deviceId =
|
|
1118
|
+
const status = Schema16.decodeUnknownSync(TaskStatus)(row.status);
|
|
1119
|
+
const provider = Schema16.decodeUnknownSync(Provider)(row.provider);
|
|
1120
|
+
const deviceId = Schema16.decodeUnknownSync(DeviceId)(row.device_id);
|
|
1121
1121
|
retainedTasks += row.tasks;
|
|
1122
1122
|
totalStatus[status] += row.tasks;
|
|
1123
1123
|
totalProviders.set(provider, (totalProviders.get(provider) ?? 0) + row.tasks);
|
|
@@ -1909,7 +1909,7 @@ var PersistenceError, TaskProgressError, TaskInputError, TaskDeadlineError, Task
|
|
|
1909
1909
|
if (pairing === null) {
|
|
1910
1910
|
throw new Error("Pairing credential is invalid, expired, or already used");
|
|
1911
1911
|
}
|
|
1912
|
-
const roles =
|
|
1912
|
+
const roles = Schema16.decodeUnknownSync(Schema16.Array(ConnectionRole))(JSON.parse(pairing.roles_json));
|
|
1913
1913
|
const deviceId = roles.includes("device") ? makeDeviceId() : undefined;
|
|
1914
1914
|
const consumed = db.query("UPDATE auth_pairings SET used_at = ? WHERE token_hash = ? AND used_at IS NULL").run(timestamp, pairing.token_hash);
|
|
1915
1915
|
if (consumed.changes !== 1) {
|
|
@@ -2183,19 +2183,19 @@ var init_store = __esm(() => {
|
|
|
2183
2183
|
init_src();
|
|
2184
2184
|
init_database();
|
|
2185
2185
|
init_store();
|
|
2186
|
-
PersistenceError = class PersistenceError extends
|
|
2187
|
-
operation:
|
|
2188
|
-
message:
|
|
2189
|
-
missingRecord:
|
|
2186
|
+
PersistenceError = class PersistenceError extends Schema16.TaggedErrorClass()("RelayStore.PersistenceError", {
|
|
2187
|
+
operation: Schema16.String,
|
|
2188
|
+
message: Schema16.String,
|
|
2189
|
+
missingRecord: Schema16.optionalKey(Schema16.Literals(["task", "operation"]))
|
|
2190
2190
|
}) {
|
|
2191
2191
|
};
|
|
2192
|
-
TaskProgressError = class TaskProgressError extends
|
|
2192
|
+
TaskProgressError = class TaskProgressError extends Schema16.TaggedErrorClass()("RelayStore.TaskProgressError", { status: Schema16.Literals([403, 409]), message: Schema16.String }) {
|
|
2193
2193
|
};
|
|
2194
|
-
TaskInputError = class TaskInputError extends
|
|
2194
|
+
TaskInputError = class TaskInputError extends Schema16.TaggedErrorClass()("RelayStore.TaskInputError", { status: Schema16.Literals([403, 409]), message: Schema16.String }) {
|
|
2195
2195
|
};
|
|
2196
|
-
TaskDeadlineError = class TaskDeadlineError extends
|
|
2196
|
+
TaskDeadlineError = class TaskDeadlineError extends Schema16.TaggedErrorClass()("RelayStore.TaskDeadlineError", { status: Schema16.Literals([400, 409]), message: Schema16.String }) {
|
|
2197
2197
|
};
|
|
2198
|
-
TaskRequestError = class TaskRequestError extends
|
|
2198
|
+
TaskRequestError = class TaskRequestError extends Schema16.TaggedErrorClass()("RelayStore.TaskRequestError", { status: Schema16.Literals([409, 410]), message: Schema16.String }) {
|
|
2199
2199
|
};
|
|
2200
2200
|
Service2 = class Service2 extends Context2.Service()("@cohall/RelayStore") {
|
|
2201
2201
|
};
|
|
@@ -2211,7 +2211,7 @@ __export(exports_main, {
|
|
|
2211
2211
|
canReadTaskAttachments: () => canReadTaskAttachments,
|
|
2212
2212
|
canDispatchTaskToDevice: () => canDispatchTaskToDevice
|
|
2213
2213
|
});
|
|
2214
|
-
import { Effect as Effect15, ManagedRuntime, Schema as
|
|
2214
|
+
import { Effect as Effect15, ManagedRuntime, Schema as Schema17, Schedule as Schedule4 } from "effect";
|
|
2215
2215
|
import { createHash as createHash3, timingSafeEqual } from "node:crypto";
|
|
2216
2216
|
import { access as access6, chmod as chmod8 } from "node:fs/promises";
|
|
2217
2217
|
import { createServer } from "node:http";
|
|
@@ -2314,7 +2314,7 @@ Content-Length: 0\r
|
|
|
2314
2314
|
}, body = (request, decode3) => Effect15.tryPromise({
|
|
2315
2315
|
try: () => request.json(),
|
|
2316
2316
|
catch: () => new RequestError({ status: 400, message: "Expected a JSON request body" })
|
|
2317
|
-
}).pipe(Effect15.flatMap(decode3), Effect15.mapError((cause) => cause instanceof RequestError ? cause : new RequestError({ status: 400, message: String(cause).slice(0, 2048) }))), pathId = (schema, value) =>
|
|
2317
|
+
}).pipe(Effect15.flatMap(decode3), Effect15.mapError((cause) => cause instanceof RequestError ? cause : new RequestError({ status: 400, message: String(cause).slice(0, 2048) }))), pathId = (schema, value) => Schema17.decodeUnknownEffect(schema)(value).pipe(Effect15.mapError(() => new RequestError({ status: 400, message: `Invalid identifier: ${value}` }))), scoreDevice = (device, input, sourceDeviceId) => {
|
|
2318
2318
|
const prompt = `${input.prompt} ${input.context ?? ""}`.toLowerCase();
|
|
2319
2319
|
const needsXcode = /\b(xcode|ios|macos|swiftui|app store)\b/.test(prompt);
|
|
2320
2320
|
const needsBrowser = /\b(browser|chrome|youtube|twitter|logged[- ]?in|signed[- ]?in|x\.com)\b/.test(prompt);
|
|
@@ -2713,6 +2713,14 @@ Content-Length: 0\r
|
|
|
2713
2713
|
return yield* store.exchangePairing(input.token);
|
|
2714
2714
|
})).then((value) => json(value, 201)).catch(() => json({ error: "Pairing credential is invalid, expired, or already used" }, 401));
|
|
2715
2715
|
}
|
|
2716
|
+
if (url.pathname === "/api/device/credential" && request.method === "GET") {
|
|
2717
|
+
const authorization = request.headers.get("authorization");
|
|
2718
|
+
const token = authorization?.startsWith("Bearer ") === true ? authorization.slice("Bearer ".length) : undefined;
|
|
2719
|
+
if (token === undefined || await authenticate(token, "device") === undefined) {
|
|
2720
|
+
return json({ error: "Unauthorized" }, 401);
|
|
2721
|
+
}
|
|
2722
|
+
return json({ ok: true });
|
|
2723
|
+
}
|
|
2716
2724
|
const attachmentRoute = request.method === "GET" && /^\/api\/tasks\/[^/]+\/attachments(?:\/[^/]+)?$/.test(url.pathname);
|
|
2717
2725
|
const progressRoute = request.method === "POST" && /^\/api\/tasks\/[^/]+\/progress$/.test(url.pathname);
|
|
2718
2726
|
const inputRoute = request.method === "POST" && /^\/api\/tasks\/[^/]+\/input$/.test(url.pathname);
|
|
@@ -3048,9 +3056,9 @@ var init_main = __esm(async () => {
|
|
|
3048
3056
|
init_config();
|
|
3049
3057
|
init_hub();
|
|
3050
3058
|
init_store();
|
|
3051
|
-
RequestError = class RequestError extends
|
|
3052
|
-
status:
|
|
3053
|
-
message:
|
|
3059
|
+
RequestError = class RequestError extends Schema17.TaggedErrorClass()("Relay.RequestError", {
|
|
3060
|
+
status: Schema17.Int,
|
|
3061
|
+
message: Schema17.String
|
|
3054
3062
|
}) {
|
|
3055
3063
|
};
|
|
3056
3064
|
maxTaskRequestBodyBytes = 3 * 1024 * 1024;
|
|
@@ -3174,6 +3182,7 @@ var make = (options) => {
|
|
|
3174
3182
|
});
|
|
3175
3183
|
const devicePage = (after) => request("RelayClient.devices", `/api/devices/page${after === undefined ? "" : `?after=${encodeURIComponent(after)}`}`, DevicePage, undefined, maxDevicePageResponseBytes);
|
|
3176
3184
|
return Service.of({
|
|
3185
|
+
verifyDeviceCredential: () => request("RelayClient.verifyDeviceCredential", "/api/device/credential", Schema2.Struct({ ok: Schema2.Literal(true) })).pipe(Effect2.asVoid),
|
|
3177
3186
|
devices: () => Effect2.gen(function* () {
|
|
3178
3187
|
const first = yield* devicePage().pipe(Effect2.catch((cause) => cause instanceof RelayRequestError && cause.status === 404 ? request("RelayClient.devices", "/api/devices", Schema2.Array(Device)) : Effect2.fail(cause)));
|
|
3179
3188
|
if (!("devices" in first))
|
|
@@ -3317,29 +3326,30 @@ var make = (options) => {
|
|
|
3317
3326
|
};
|
|
3318
3327
|
var exchangePairing = (baseUrl, input) => Effect2.gen(function* () {
|
|
3319
3328
|
const operation = "RelayClient.exchangePairing";
|
|
3320
|
-
const
|
|
3321
|
-
try: (signal) =>
|
|
3322
|
-
|
|
3323
|
-
|
|
3324
|
-
|
|
3325
|
-
|
|
3326
|
-
|
|
3327
|
-
|
|
3329
|
+
const json = yield* Effect2.tryPromise({
|
|
3330
|
+
try: async (signal) => {
|
|
3331
|
+
const response = await fetch(`${normalizeBaseUrl(baseUrl)}/api/auth/pair`, {
|
|
3332
|
+
method: "POST",
|
|
3333
|
+
signal: AbortSignal.any([signal, AbortSignal.timeout(30000)]),
|
|
3334
|
+
headers: { "content-type": "application/json" },
|
|
3335
|
+
body: JSON.stringify(input)
|
|
3336
|
+
});
|
|
3337
|
+
if (!response.ok) {
|
|
3338
|
+
throw new RelayRequestError({
|
|
3339
|
+
operation,
|
|
3340
|
+
message: await responseMessage(response, operation),
|
|
3341
|
+
status: response.status
|
|
3342
|
+
});
|
|
3343
|
+
}
|
|
3344
|
+
return jsonBody(response, operation).catch((cause) => {
|
|
3345
|
+
throw cause instanceof RelayDecodeError ? cause : new RelayDecodeError({ operation, message: String(cause) });
|
|
3346
|
+
});
|
|
3347
|
+
},
|
|
3348
|
+
catch: (cause) => cause instanceof RelayRequestError || cause instanceof RelayDecodeError ? cause : new RelayRequestError({
|
|
3328
3349
|
operation,
|
|
3329
3350
|
message: cause instanceof Error ? cause.message : String(cause)
|
|
3330
3351
|
})
|
|
3331
3352
|
});
|
|
3332
|
-
if (!response.ok) {
|
|
3333
|
-
return yield* Effect2.fail(new RelayRequestError({
|
|
3334
|
-
operation,
|
|
3335
|
-
message: yield* Effect2.promise(() => responseMessage(response, operation)),
|
|
3336
|
-
status: response.status
|
|
3337
|
-
}));
|
|
3338
|
-
}
|
|
3339
|
-
const json = yield* Effect2.tryPromise({
|
|
3340
|
-
try: () => jsonBody(response, operation),
|
|
3341
|
-
catch: (cause) => cause instanceof RelayDecodeError ? cause : new RelayDecodeError({ operation, message: String(cause) })
|
|
3342
|
-
});
|
|
3343
3353
|
return yield* Schema2.decodeUnknownEffect(PairingResult)(json).pipe(Effect2.mapError((cause) => new RelayDecodeError({ operation, message: String(cause) })));
|
|
3344
3354
|
});
|
|
3345
3355
|
var layer = (options) => Layer.succeed(Service, make(options));
|
|
@@ -3893,10 +3903,10 @@ var run = (options) => Effect3.callback((resume, signal) => {
|
|
|
3893
3903
|
});
|
|
3894
3904
|
|
|
3895
3905
|
// apps/device/src/cli.ts
|
|
3896
|
-
import { Effect as Effect10, Schema as
|
|
3897
|
-
import { access as access4, mkdir as
|
|
3906
|
+
import { Effect as Effect10, Schema as Schema12 } from "effect";
|
|
3907
|
+
import { access as access4, mkdir as mkdir8, open as open5, stat as stat4, writeFile as writeFile8 } from "node:fs/promises";
|
|
3898
3908
|
import { homedir as homedir4 } from "node:os";
|
|
3899
|
-
import { dirname as dirname6, join as
|
|
3909
|
+
import { dirname as dirname6, join as join9, resolve as resolve4 } from "node:path";
|
|
3900
3910
|
|
|
3901
3911
|
// skills/cohall/SKILL.md
|
|
3902
3912
|
var SKILL_default = `---
|
|
@@ -4162,6 +4172,125 @@ cohall devices
|
|
|
4162
4172
|
Return the diagnostic failure to the user rather than claiming remote work ran.
|
|
4163
4173
|
`;
|
|
4164
4174
|
|
|
4175
|
+
// docs/onboarding.md
|
|
4176
|
+
var onboarding_default = `# Set up Cohall with your agent
|
|
4177
|
+
|
|
4178
|
+
These instructions are for the agent doing the setup. Read the path that matches
|
|
4179
|
+
the owner's request, complete the verification steps, and report what works.
|
|
4180
|
+
\`cohall onboard\` prints this same guide without changing the machine.
|
|
4181
|
+
|
|
4182
|
+
## Choose the role
|
|
4183
|
+
|
|
4184
|
+
- A relay address and pairing token mean **join an existing relay**.
|
|
4185
|
+
- A request to host a relay means **host a relay** on an always-on machine.
|
|
4186
|
+
- If neither is clear, ask whether this machine should host or join, and obtain
|
|
4187
|
+
the relay address before pairing. A laptop that sleeps is a poor relay host.
|
|
4188
|
+
- For an existing installation, inspect \`cohall config\` and run \`cohall doctor\`
|
|
4189
|
+
before repairing it. Reuse working credentials and workspace settings.
|
|
4190
|
+
|
|
4191
|
+
Detect the OS, Node.js version, package manager, installed provider CLIs, and
|
|
4192
|
+
existing workspace roots. Cohall requires Node.js 24 or newer. Follow
|
|
4193
|
+
[installation](https://github.com/AksharP5/cohall/blob/main/docs/install.md) for package commands and
|
|
4194
|
+
[services](https://github.com/AksharP5/cohall/blob/main/docs/services.md) for the current OS. Use the owner's authorization for
|
|
4195
|
+
installation and autostart; obtain it when missing. Provider login requires the
|
|
4196
|
+
owner's account and should stay on the machine that runs that provider.
|
|
4197
|
+
|
|
4198
|
+
Keep tokens out of command arguments, shell history, logs, and setup briefs.
|
|
4199
|
+
Read them through a hidden terminal prompt, stdin, or a private token file.
|
|
4200
|
+
|
|
4201
|
+
## Host a relay
|
|
4202
|
+
|
|
4203
|
+
1. Choose a machine that stays awake and a persistent relay data directory.
|
|
4204
|
+
Install Cohall globally so its service has a stable executable path.
|
|
4205
|
+
2. Start \`cohall relay\` once in the foreground. The default listener is
|
|
4206
|
+
\`127.0.0.1:8787\`; the relay creates a private owner token in its data directory
|
|
4207
|
+
when none is supplied. Preserve that directory and include it in backups.
|
|
4208
|
+
3. Make the relay reachable through the owner's private Tailscale network or
|
|
4209
|
+
HTTPS reverse proxy. Confirm the address from a joining machine with
|
|
4210
|
+
\`GET /api/health\`. Plain HTTP is suitable only inside an independently
|
|
4211
|
+
encrypted private network, never as a public listener.
|
|
4212
|
+
4. Configure relay autostart. The packaged relay service instructions cover
|
|
4213
|
+
Linux; macOS and Windows relay hosts need an owner-approved supervisor.
|
|
4214
|
+
\`cohall service install\` installs a **device worker**, not a relay.
|
|
4215
|
+
Prepare the managed relay with the same data and owner configuration. Stop
|
|
4216
|
+
the foreground relay to release its address, start the managed service, and
|
|
4217
|
+
verify relay health before pairing devices.
|
|
4218
|
+
5. On the relay host, create a ten-minute, single-use pairing token for each
|
|
4219
|
+
joining machine:
|
|
4220
|
+
|
|
4221
|
+
\`\`\`bash
|
|
4222
|
+
cohall pair --label "Workstation"
|
|
4223
|
+
\`\`\`
|
|
4224
|
+
|
|
4225
|
+
Set \`COHALL_RELAY_URL\` to the reachable address when creating the pairing so
|
|
4226
|
+
its \`join_instructions\` use that address. The owner token is read from the
|
|
4227
|
+
relay data directory on the host; a service using a different user or data
|
|
4228
|
+
directory needs the matching owner configuration. Transfer the pairing token
|
|
4229
|
+
privately and send the returned join instructions separately.
|
|
4230
|
+
|
|
4231
|
+
6. If this host should also run agent work, pair it as another device and
|
|
4232
|
+
complete the joining path below. Hosting a relay alone does not register a
|
|
4233
|
+
worker or install its agent integration.
|
|
4234
|
+
|
|
4235
|
+
## Join an existing relay
|
|
4236
|
+
|
|
4237
|
+
1. Confirm the relay address and the role. A worker runs providers against its
|
|
4238
|
+
own allowed workspaces; a client-only installation submits work elsewhere.
|
|
4239
|
+
Request a fresh token if the supplied one has expired or was already used.
|
|
4240
|
+
2. Choose existing workspace roots and installed providers for a worker.
|
|
4241
|
+
Confirm provider login on this machine. Use \`--providers codex\`,
|
|
4242
|
+
\`--providers claude-code\`, or \`--providers opencode\` to limit selection;
|
|
4243
|
+
\`auto\` enables every detected provider. Grok Bots have a separate
|
|
4244
|
+
[gateway setup](https://github.com/AksharP5/cohall/blob/main/docs/grok-bot.md).
|
|
4245
|
+
3. Run guided setup with the confirmed address:
|
|
4246
|
+
|
|
4247
|
+
\`\`\`bash
|
|
4248
|
+
cohall init --relay https://your-relay.example
|
|
4249
|
+
\`\`\`
|
|
4250
|
+
|
|
4251
|
+
A terminal prompts for the pairing token without echoing it. Scripts and
|
|
4252
|
+
agents without an interactive terminal supply it on stdin or with
|
|
4253
|
+
\`--token-file\`; see the [pairing examples](https://github.com/AksharP5/cohall/blob/main/docs/install.md#pair-a-machine).
|
|
4254
|
+
Client-only setup adds \`--client-only\` and uses a client-only pairing.
|
|
4255
|
+
\`init\` writes configuration and installs the Cohall skill for Codex,
|
|
4256
|
+
Claude Code, and OpenCode. It preserves existing settings during repair.
|
|
4257
|
+
|
|
4258
|
+
4. For a worker, run \`cohall device\` in the foreground or install its service
|
|
4259
|
+
after a global package installation. \`cohall init --service\` can install the
|
|
4260
|
+
worker service during setup. A client-only installation needs no device
|
|
4261
|
+
service. For Grok Bots, use the gateway instructions to verify availability.
|
|
4262
|
+
5. Use the CLI and installed skill in a harness that can run commands. If the
|
|
4263
|
+
owner prefers MCP, run \`cohall integrations\` and follow the entry for the
|
|
4264
|
+
actual harness in [agent integrations](https://github.com/AksharP5/cohall/blob/main/docs/integrations.md). Configure exactly
|
|
4265
|
+
one Cohall server for that configuration, restart or reconnect the harness,
|
|
4266
|
+
and ask it to call \`list_devices\`. Installing a skill does not configure MCP.
|
|
4267
|
+
|
|
4268
|
+
## Verify before calling setup complete
|
|
4269
|
+
|
|
4270
|
+
1. Run \`cohall doctor\`. Confirm relay access and client authentication. A worker
|
|
4271
|
+
should be online or busy with the intended providers and workspaces.
|
|
4272
|
+
\`doctor --all\` checks the other registered workers.
|
|
4273
|
+
2. If using MCP, check the real host connection in the doctor report after
|
|
4274
|
+
asking the harness to call \`list_devices\`. A successful server self-test
|
|
4275
|
+
alone does not establish that the harness loaded it.
|
|
4276
|
+
3. From the requesting client, run a harmless delegation to the intended
|
|
4277
|
+
worker, using its target from \`cohall devices\` and an advertised provider:
|
|
4278
|
+
|
|
4279
|
+
\`\`\`bash
|
|
4280
|
+
cohall delegate --target @workstation --provider codex --timeout 60 \\
|
|
4281
|
+
"Reply with exactly cohall-ready. Do not inspect or change files."
|
|
4282
|
+
\`\`\`
|
|
4283
|
+
|
|
4284
|
+
Require a completed result containing \`cohall-ready\`. Provider detection
|
|
4285
|
+
alone does not prove authentication or execution works. For a Grok Bot,
|
|
4286
|
+
use its target from \`cohall bots\` and \`--provider grok-bot\`.
|
|
4287
|
+
|
|
4288
|
+
4. Report the relay host/address, this machine's role, selected providers and
|
|
4289
|
+
workspaces, autostart state, integration used, and checks that passed.
|
|
4290
|
+
Identify any remaining login, permission, or connectivity step. Include no
|
|
4291
|
+
tokens or private task content.
|
|
4292
|
+
`;
|
|
4293
|
+
|
|
4165
4294
|
// apps/device/src/config.ts
|
|
4166
4295
|
init_src();
|
|
4167
4296
|
import { Effect as Effect4, Schema as Schema4 } from "effect";
|
|
@@ -5229,9 +5358,15 @@ var terminalPrompter = () => {
|
|
|
5229
5358
|
close: () => terminal.close()
|
|
5230
5359
|
};
|
|
5231
5360
|
};
|
|
5361
|
+
var setupRelayUrl = (input) => {
|
|
5362
|
+
if (input === undefined || input.trim().length === 0) {
|
|
5363
|
+
throw new Error("Relay URL is required. Run `cohall onboard` to choose a relay host or join an existing relay.");
|
|
5364
|
+
}
|
|
5365
|
+
return normalizeRelayUrl(input);
|
|
5366
|
+
};
|
|
5232
5367
|
var guidedSetupInput = async (options, prompter) => {
|
|
5233
5368
|
const existing = await readStoredConfiguration();
|
|
5234
|
-
const relayUrl =
|
|
5369
|
+
const relayUrl = setupRelayUrl(options.relayUrl ?? await prompter.answer("Relay URL", existing?.relayUrl ?? ""));
|
|
5235
5370
|
const credentials = credentialsForRelay(existing, relayUrl);
|
|
5236
5371
|
const hasRequiredCredentials = credentials.clientToken !== undefined && (options.clientOnly || credentials.deviceToken !== undefined);
|
|
5237
5372
|
let workspaces = options.clientOnly ? existing?.workspaces ?? [] : options.workspaces;
|
|
@@ -6489,7 +6624,7 @@ var isLoopback = (url) => url.hostname === "localhost" || url.hostname === "[::1
|
|
|
6489
6624
|
var verifyClientCredential = async (relayUrl, token) => {
|
|
6490
6625
|
await Effect8.runPromise(exports_src.make({ baseUrl: relayUrl, token }).devices());
|
|
6491
6626
|
};
|
|
6492
|
-
var
|
|
6627
|
+
var verifyLegacyDeviceCredential = (relayUrl, token) => new Promise((resolvePromise, reject) => {
|
|
6493
6628
|
const socket = new WebSocket(websocketUrl(relayUrl), {
|
|
6494
6629
|
handshakeTimeout: 1e4,
|
|
6495
6630
|
maxPayload: 1024 * 1024,
|
|
@@ -6528,6 +6663,12 @@ var verifyDeviceCredential = (relayUrl, token) => new Promise((resolvePromise, r
|
|
|
6528
6663
|
socket.once("error", (cause) => finish(cause));
|
|
6529
6664
|
socket.once("close", () => finish(new Error("New relay rejected the stored device credential")));
|
|
6530
6665
|
});
|
|
6666
|
+
var verifyDeviceCredential = (relayUrl, token) => Effect8.runPromise(exports_src.make({ baseUrl: relayUrl, token }).verifyDeviceCredential()).catch((cause) => {
|
|
6667
|
+
if (cause instanceof RelayRequestError && (cause.status === 401 || cause.status === 404)) {
|
|
6668
|
+
return verifyLegacyDeviceCredential(relayUrl, token);
|
|
6669
|
+
}
|
|
6670
|
+
throw cause;
|
|
6671
|
+
});
|
|
6531
6672
|
var switchRelay = async (options) => {
|
|
6532
6673
|
if (process.env.COHALL_RELAY_URL !== undefined) {
|
|
6533
6674
|
throw new Error("Unset COHALL_RELAY_URL before changing the stored relay URL");
|
|
@@ -6623,7 +6764,7 @@ var readInputAttachments = async (paths2) => {
|
|
|
6623
6764
|
if (!source.isFile()) {
|
|
6624
6765
|
throw new Error(`Attachment ${name} must be a regular file`);
|
|
6625
6766
|
}
|
|
6626
|
-
const handle = await open3(path, constants3.O_RDONLY | (process.platform === "win32" ? 0 : constants3.O_NOFOLLOW));
|
|
6767
|
+
const handle = await open3(path, constants3.O_RDONLY | (process.platform === "win32" ? 0 : constants3.O_NOFOLLOW | constants3.O_NONBLOCK));
|
|
6627
6768
|
try {
|
|
6628
6769
|
const metadata = await handle.stat();
|
|
6629
6770
|
if (!metadata.isFile() || metadata.dev !== source.dev || metadata.ino !== source.ino || metadata.size < 1 || metadata.size > maxAttachmentBytes) {
|
|
@@ -6680,7 +6821,7 @@ var collectOutputs = async (directory) => {
|
|
|
6680
6821
|
throw new Error(`Output ${name} repeats another file name`);
|
|
6681
6822
|
}
|
|
6682
6823
|
names.add(key);
|
|
6683
|
-
const handle = await open3(join7(directory, name), constants3.O_RDONLY | (process.platform === "win32" ? 0 : constants3.O_NOFOLLOW));
|
|
6824
|
+
const handle = await open3(join7(directory, name), constants3.O_RDONLY | (process.platform === "win32" ? 0 : constants3.O_NOFOLLOW | constants3.O_NONBLOCK));
|
|
6684
6825
|
try {
|
|
6685
6826
|
const metadata = await handle.stat();
|
|
6686
6827
|
const pathMetadata = await lstat2(join7(directory, name));
|
|
@@ -6706,6 +6847,248 @@ import {
|
|
|
6706
6847
|
getDefaultEnvironment,
|
|
6707
6848
|
StdioClientTransport
|
|
6708
6849
|
} from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
6850
|
+
|
|
6851
|
+
// apps/device/src/mcp-host-diagnostics.ts
|
|
6852
|
+
init_src();
|
|
6853
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
6854
|
+
import { Schema as Schema11 } from "effect";
|
|
6855
|
+
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
6856
|
+
import { constants as constants4 } from "node:fs";
|
|
6857
|
+
import { lstat as lstat3, mkdir as mkdir7, open as open4, opendir, rename as rename4, rm as rm5, writeFile as writeFile7 } from "node:fs/promises";
|
|
6858
|
+
import { join as join8 } from "node:path";
|
|
6859
|
+
var maxRecords = 8;
|
|
6860
|
+
var maxRecordBytes = 4096;
|
|
6861
|
+
var retentionMs = 7 * 24 * 60 * 60 * 1000;
|
|
6862
|
+
var recordName = /^[\da-f]{8}-[\da-f]{4}-4[\da-f]{3}-[89ab][\da-f]{3}-[\da-f]{12}\.json$/;
|
|
6863
|
+
var text3 = Schema11.String.check(Schema11.isMaxLength(128));
|
|
6864
|
+
var count2 = Schema11.Int.check(Schema11.isBetween({ minimum: 0, maximum: Number.MAX_SAFE_INTEGER }));
|
|
6865
|
+
var McpHostSession = Schema11.Struct({
|
|
6866
|
+
schema_version: Schema11.Literal(1),
|
|
6867
|
+
launch_id: Schema11.String.check(Schema11.isUUID(4)),
|
|
6868
|
+
launched_at: Timestamp,
|
|
6869
|
+
server_version: text3,
|
|
6870
|
+
initialized_at: Schema11.optionalKey(Timestamp),
|
|
6871
|
+
client: Schema11.optionalKey(Schema11.Struct({ name: text3, version: text3 })),
|
|
6872
|
+
tools_list: Schema11.optionalKey(Schema11.Struct({ at: Timestamp, tool_count: count2 })),
|
|
6873
|
+
tools_call_count: count2,
|
|
6874
|
+
last_tool_call_at: Schema11.optionalKey(Timestamp),
|
|
6875
|
+
closed_at: Schema11.optionalKey(Timestamp)
|
|
6876
|
+
});
|
|
6877
|
+
var storagePath = (configPath) => `${configPath}.mcp-hosts`;
|
|
6878
|
+
var recordsIn = async (directory) => {
|
|
6879
|
+
const records = [];
|
|
6880
|
+
let entries = 0;
|
|
6881
|
+
for await (const entry of await opendir(directory)) {
|
|
6882
|
+
if (++entries > 64)
|
|
6883
|
+
throw new Error("MCP host diagnostics directory exceeded its entry limit");
|
|
6884
|
+
if (!recordName.test(entry.name))
|
|
6885
|
+
continue;
|
|
6886
|
+
const path = join8(directory, entry.name);
|
|
6887
|
+
const metadata = await lstat3(path).catch((cause) => {
|
|
6888
|
+
if (cause instanceof Error && "code" in cause && cause.code === "ENOENT")
|
|
6889
|
+
return;
|
|
6890
|
+
throw cause;
|
|
6891
|
+
});
|
|
6892
|
+
if (metadata !== undefined)
|
|
6893
|
+
records.push({ path, modified: metadata.mtimeMs });
|
|
6894
|
+
}
|
|
6895
|
+
return records.sort((left, right) => right.modified - left.modified || left.path.localeCompare(right.path));
|
|
6896
|
+
};
|
|
6897
|
+
var readMcpHostDiagnostics = async (configPath = configurationPath()) => {
|
|
6898
|
+
const path = storagePath(configPath);
|
|
6899
|
+
const warnings = [];
|
|
6900
|
+
let records;
|
|
6901
|
+
try {
|
|
6902
|
+
records = await recordsIn(path);
|
|
6903
|
+
} catch (cause) {
|
|
6904
|
+
if (cause instanceof Error && "code" in cause && cause.code === "ENOENT") {
|
|
6905
|
+
return { status: "not_observed", path, sessions: [], warnings };
|
|
6906
|
+
}
|
|
6907
|
+
return {
|
|
6908
|
+
status: "unavailable",
|
|
6909
|
+
path,
|
|
6910
|
+
sessions: [],
|
|
6911
|
+
warnings,
|
|
6912
|
+
error: `Cannot read MCP host diagnostics at ${path}. Restore access to this diagnostics directory, then restart the Cohall MCP connection in your host.`
|
|
6913
|
+
};
|
|
6914
|
+
}
|
|
6915
|
+
const sessions = [];
|
|
6916
|
+
let invalid = 0;
|
|
6917
|
+
for (const record2 of records.filter((record3) => record3.modified >= Date.now() - retentionMs).slice(0, maxRecords)) {
|
|
6918
|
+
try {
|
|
6919
|
+
const handle = await open4(record2.path, constants4.O_RDONLY | (process.platform === "win32" ? 0 : constants4.O_NOFOLLOW | constants4.O_NONBLOCK));
|
|
6920
|
+
try {
|
|
6921
|
+
const metadata = await handle.stat();
|
|
6922
|
+
if (!metadata.isFile() || metadata.size > maxRecordBytes)
|
|
6923
|
+
throw new Error("Invalid record");
|
|
6924
|
+
const data = await readBoundedFile(handle, maxRecordBytes);
|
|
6925
|
+
if (data.length > maxRecordBytes)
|
|
6926
|
+
throw new Error("Invalid record");
|
|
6927
|
+
sessions.push(Schema11.decodeUnknownSync(McpHostSession)(JSON.parse(data.toString("utf8"))));
|
|
6928
|
+
} finally {
|
|
6929
|
+
await handle.close();
|
|
6930
|
+
}
|
|
6931
|
+
} catch (cause) {
|
|
6932
|
+
if (!(cause instanceof Error && ("code" in cause) && cause.code === "ENOENT"))
|
|
6933
|
+
invalid += 1;
|
|
6934
|
+
}
|
|
6935
|
+
}
|
|
6936
|
+
if (invalid > 0) {
|
|
6937
|
+
warnings.push(`${invalid} MCP host diagnostics record(s) could not be read. Reset the diagnostics directory ${path}, then restart the Cohall MCP connection in your host.`);
|
|
6938
|
+
}
|
|
6939
|
+
if (sessions.length === 0 && invalid > 0) {
|
|
6940
|
+
return {
|
|
6941
|
+
status: "unavailable",
|
|
6942
|
+
path,
|
|
6943
|
+
sessions,
|
|
6944
|
+
warnings,
|
|
6945
|
+
error: "No valid MCP host diagnostics records could be read"
|
|
6946
|
+
};
|
|
6947
|
+
}
|
|
6948
|
+
const latest = sessions[0];
|
|
6949
|
+
if (latest !== undefined) {
|
|
6950
|
+
if (latest.initialized_at === undefined) {
|
|
6951
|
+
warnings.push(`${latest.closed_at === undefined ? "An MCP launch was recorded" : "A recorded MCP launch ended"} without client initialization. If your host still lacks Cohall tools, check its MCP command and selected config, then restart its Cohall connection.`);
|
|
6952
|
+
} else if (latest.tools_list?.tool_count === 0) {
|
|
6953
|
+
warnings.push("A recorded MCP launch returned an empty tools list. If Cohall tools are missing, check the host's Cohall MCP command and update or restart its connection.");
|
|
6954
|
+
} else if (latest.tools_list === undefined && latest.tools_call_count === 0) {
|
|
6955
|
+
warnings.push("An MCP host initialized Cohall but no tool discovery or calls were observed. If Cohall tools are missing, refresh or restart its MCP connection in that host.");
|
|
6956
|
+
}
|
|
6957
|
+
}
|
|
6958
|
+
return {
|
|
6959
|
+
status: sessions.some((session) => session.initialized_at !== undefined) ? "observed" : "not_observed",
|
|
6960
|
+
path,
|
|
6961
|
+
sessions,
|
|
6962
|
+
warnings
|
|
6963
|
+
};
|
|
6964
|
+
};
|
|
6965
|
+
var createMcpHostDiagnostics = (server) => {
|
|
6966
|
+
const transport = new StdioServerTransport;
|
|
6967
|
+
if (process.env.COHALL_MCP_SELF_TEST === "1")
|
|
6968
|
+
return transport;
|
|
6969
|
+
const directory = storagePath(configurationPath());
|
|
6970
|
+
let session = McpHostSession.make({
|
|
6971
|
+
schema_version: 1,
|
|
6972
|
+
launch_id: randomUUID2(),
|
|
6973
|
+
launched_at: now(),
|
|
6974
|
+
server_version: version,
|
|
6975
|
+
tools_call_count: 0
|
|
6976
|
+
});
|
|
6977
|
+
const path = join8(directory, `${session.launch_id}.json`);
|
|
6978
|
+
let dirty = true;
|
|
6979
|
+
let disabled = false;
|
|
6980
|
+
let stopped = false;
|
|
6981
|
+
let writing;
|
|
6982
|
+
let timer;
|
|
6983
|
+
const schedule = () => {
|
|
6984
|
+
if (disabled || stopped || timer !== undefined)
|
|
6985
|
+
return;
|
|
6986
|
+
timer = setTimeout(() => {
|
|
6987
|
+
timer = undefined;
|
|
6988
|
+
flush();
|
|
6989
|
+
}, 250);
|
|
6990
|
+
timer.unref();
|
|
6991
|
+
};
|
|
6992
|
+
const flush = () => {
|
|
6993
|
+
if (writing !== undefined)
|
|
6994
|
+
return writing;
|
|
6995
|
+
if (!dirty || disabled)
|
|
6996
|
+
return Promise.resolve();
|
|
6997
|
+
dirty = false;
|
|
6998
|
+
const snapshot = session;
|
|
6999
|
+
writing = (async () => {
|
|
7000
|
+
await mkdir7(directory, { recursive: true, mode: 448 });
|
|
7001
|
+
await recordsIn(directory);
|
|
7002
|
+
const temporary = `${path}.tmp`;
|
|
7003
|
+
try {
|
|
7004
|
+
const json = `${JSON.stringify(snapshot)}
|
|
7005
|
+
`;
|
|
7006
|
+
if (Buffer.byteLength(json) > maxRecordBytes)
|
|
7007
|
+
throw new Error("Record exceeded its limit");
|
|
7008
|
+
await writeFile7(temporary, json, { flag: "wx", mode: 384 });
|
|
7009
|
+
await rename4(temporary, path);
|
|
7010
|
+
} finally {
|
|
7011
|
+
await rm5(temporary, { force: true });
|
|
7012
|
+
}
|
|
7013
|
+
const records = await recordsIn(directory);
|
|
7014
|
+
const expired = records.filter((record2, index) => index >= maxRecords || record2.modified < Date.now() - retentionMs);
|
|
7015
|
+
await Promise.all(expired.map((record2) => rm5(record2.path, { force: true })));
|
|
7016
|
+
})().catch((cause) => {
|
|
7017
|
+
disabled = true;
|
|
7018
|
+
const reason = cause instanceof Error ? "code" in cause && typeof cause.code === "string" ? cause.code : cause.message : "Unknown filesystem error";
|
|
7019
|
+
console.error(`Cohall MCP host diagnostics are unavailable (${reason.replace(/[\r\n]/g, " ").slice(0, 160)}). Check diagnostics storage access; MCP tools remain available.`);
|
|
7020
|
+
}).finally(() => {
|
|
7021
|
+
writing = undefined;
|
|
7022
|
+
if (dirty)
|
|
7023
|
+
schedule();
|
|
7024
|
+
});
|
|
7025
|
+
return writing;
|
|
7026
|
+
};
|
|
7027
|
+
const changed = () => {
|
|
7028
|
+
dirty = true;
|
|
7029
|
+
schedule();
|
|
7030
|
+
};
|
|
7031
|
+
const shutdown = async () => {
|
|
7032
|
+
if (stopped)
|
|
7033
|
+
return;
|
|
7034
|
+
stopped = true;
|
|
7035
|
+
clearTimeout(timer);
|
|
7036
|
+
session = { ...session, closed_at: now() };
|
|
7037
|
+
dirty = true;
|
|
7038
|
+
await writing;
|
|
7039
|
+
await flush();
|
|
7040
|
+
};
|
|
7041
|
+
flush();
|
|
7042
|
+
process.once("beforeExit", () => void shutdown());
|
|
7043
|
+
const initialized = server.oninitialized;
|
|
7044
|
+
server.oninitialized = () => {
|
|
7045
|
+
initialized?.();
|
|
7046
|
+
const client = server.getClientVersion();
|
|
7047
|
+
session = {
|
|
7048
|
+
...session,
|
|
7049
|
+
initialized_at: now(),
|
|
7050
|
+
...client === undefined ? {} : { client: { name: client.name.slice(0, 128), version: client.version.slice(0, 128) } }
|
|
7051
|
+
};
|
|
7052
|
+
changed();
|
|
7053
|
+
};
|
|
7054
|
+
const closed = server.onclose;
|
|
7055
|
+
server.onclose = () => {
|
|
7056
|
+
closed?.();
|
|
7057
|
+
shutdown();
|
|
7058
|
+
};
|
|
7059
|
+
const pendingLists = new Set;
|
|
7060
|
+
const start = transport.start.bind(transport);
|
|
7061
|
+
transport.start = async () => {
|
|
7062
|
+
const receive = transport.onmessage;
|
|
7063
|
+
transport.onmessage = (message) => {
|
|
7064
|
+
if ("id" in message && message.id !== undefined && "method" in message) {
|
|
7065
|
+
if (message.method === "tools/list" && pendingLists.size < 32)
|
|
7066
|
+
pendingLists.add(message.id);
|
|
7067
|
+
if (message.method === "tools/call") {
|
|
7068
|
+
session = {
|
|
7069
|
+
...session,
|
|
7070
|
+
tools_call_count: Math.min(session.tools_call_count + 1, Number.MAX_SAFE_INTEGER),
|
|
7071
|
+
last_tool_call_at: now()
|
|
7072
|
+
};
|
|
7073
|
+
changed();
|
|
7074
|
+
}
|
|
7075
|
+
}
|
|
7076
|
+
receive?.(message);
|
|
7077
|
+
};
|
|
7078
|
+
await start();
|
|
7079
|
+
};
|
|
7080
|
+
const send = transport.send.bind(transport);
|
|
7081
|
+
transport.send = async (message) => {
|
|
7082
|
+
await send(message);
|
|
7083
|
+
if ("id" in message && message.id !== undefined && pendingLists.delete(message.id) && "result" in message && Array.isArray(message.result.tools)) {
|
|
7084
|
+
session = { ...session, tools_list: { at: now(), tool_count: message.result.tools.length } };
|
|
7085
|
+
changed();
|
|
7086
|
+
}
|
|
7087
|
+
};
|
|
7088
|
+
return transport;
|
|
7089
|
+
};
|
|
7090
|
+
|
|
7091
|
+
// apps/device/src/mcp-diagnostics.ts
|
|
6709
7092
|
var checkMcp = async (entrypoint, relayUrl, token) => {
|
|
6710
7093
|
const client = new Client({ name: "cohall-doctor", version });
|
|
6711
7094
|
const transport = new StdioClientTransport({
|
|
@@ -6714,6 +7097,7 @@ var checkMcp = async (entrypoint, relayUrl, token) => {
|
|
|
6714
7097
|
env: {
|
|
6715
7098
|
...getDefaultEnvironment(),
|
|
6716
7099
|
COHALL_CONFIG: configurationPath(),
|
|
7100
|
+
COHALL_MCP_SELF_TEST: "1",
|
|
6717
7101
|
COHALL_RELAY_URL: relayUrl,
|
|
6718
7102
|
COHALL_CLIENT_TOKEN: token
|
|
6719
7103
|
},
|
|
@@ -6784,6 +7168,7 @@ var aliases = new Map([
|
|
|
6784
7168
|
var help = `Cohall connects agents running on your own devices.
|
|
6785
7169
|
|
|
6786
7170
|
Usage:
|
|
7171
|
+
cohall onboard
|
|
6787
7172
|
cohall init [--relay url] [--workspace path] [--providers list] [--service]
|
|
6788
7173
|
cohall join --relay <url> --workspace <path> [--providers list] [--token-file <path>]
|
|
6789
7174
|
cohall configure [--relay url] [--name name] [--workspace path]
|
|
@@ -6914,7 +7299,7 @@ var printLine = (value) => console.log(JSON.stringify(value));
|
|
|
6914
7299
|
var readTextFile = async (path, limit, exceeded) => {
|
|
6915
7300
|
if ((await stat4(path)).size > limit)
|
|
6916
7301
|
throw new Error(exceeded);
|
|
6917
|
-
const file = await
|
|
7302
|
+
const file = await open5(path, "r");
|
|
6918
7303
|
try {
|
|
6919
7304
|
const data = await readBoundedFile(file, limit);
|
|
6920
7305
|
if (data.length > limit)
|
|
@@ -6989,17 +7374,17 @@ var ownerClient = async () => {
|
|
|
6989
7374
|
};
|
|
6990
7375
|
var installSkill = async (target) => {
|
|
6991
7376
|
const roots = {
|
|
6992
|
-
agents:
|
|
6993
|
-
claude:
|
|
6994
|
-
opencode:
|
|
7377
|
+
agents: join9(homedir4(), ".agents", "skills", "cohall", "SKILL.md"),
|
|
7378
|
+
claude: join9(homedir4(), ".claude", "skills", "cohall", "SKILL.md"),
|
|
7379
|
+
opencode: join9(homedir4(), ".config", "opencode", "skills", "cohall", "SKILL.md")
|
|
6995
7380
|
};
|
|
6996
7381
|
const selected = target === "all" ? Object.values(roots) : (target in roots) ? [roots[target]] : undefined;
|
|
6997
7382
|
if (selected === undefined) {
|
|
6998
7383
|
throw new Error("Skill target must be agents, claude, opencode, or all");
|
|
6999
7384
|
}
|
|
7000
7385
|
for (const path of selected) {
|
|
7001
|
-
await
|
|
7002
|
-
await
|
|
7386
|
+
await mkdir8(dirname6(path), { recursive: true });
|
|
7387
|
+
await writeFile8(path, SKILL_default);
|
|
7003
7388
|
}
|
|
7004
7389
|
return selected;
|
|
7005
7390
|
};
|
|
@@ -7007,13 +7392,19 @@ var printHelp = () => console.log(help);
|
|
|
7007
7392
|
var printSkill = () => console.log(SKILL_default.trimEnd());
|
|
7008
7393
|
var runCli = async (command, raw) => {
|
|
7009
7394
|
const arguments_ = parseArguments(raw);
|
|
7395
|
+
if (command === "onboard") {
|
|
7396
|
+
allowOptions(arguments_, []);
|
|
7397
|
+
noPositionals(arguments_, command);
|
|
7398
|
+
console.log(onboarding_default.trimEnd());
|
|
7399
|
+
return;
|
|
7400
|
+
}
|
|
7010
7401
|
if (command === "reply") {
|
|
7011
7402
|
allowOptions(arguments_, ["message", "message-file", "error", "question", "run-id"]);
|
|
7012
|
-
const id =
|
|
7403
|
+
const id = Schema12.decodeUnknownSync(TaskId)(identifier(arguments_, "task id"));
|
|
7013
7404
|
const error = option(arguments_, "error");
|
|
7014
7405
|
const question = option(arguments_, "question");
|
|
7015
7406
|
const rawRunId = option(arguments_, "run-id");
|
|
7016
|
-
const runId = rawRunId === undefined ? undefined :
|
|
7407
|
+
const runId = rawRunId === undefined ? undefined : Schema12.decodeUnknownSync(TaskRunId)(rawRunId);
|
|
7017
7408
|
if ((error !== undefined || question !== undefined) && (option(arguments_, "message") !== undefined || option(arguments_, "message-file") !== undefined || error !== undefined && question !== undefined)) {
|
|
7018
7409
|
throw new Error("Use one of --question, --error, or --message/--message-file");
|
|
7019
7410
|
}
|
|
@@ -7021,7 +7412,7 @@ var runCli = async (command, raw) => {
|
|
|
7021
7412
|
if (error === undefined && question === undefined && message === undefined) {
|
|
7022
7413
|
throw new Error("A bot reply requires --message, --message-file, --error, or --question");
|
|
7023
7414
|
}
|
|
7024
|
-
const reply = question !== undefined ? { question:
|
|
7415
|
+
const reply = question !== undefined ? { question: Schema12.decodeUnknownSync(RequestTaskInput.fields.question)(question) } : error === undefined ? { result: message ?? "" } : { error };
|
|
7025
7416
|
await writeBotReply(id, reply, runId);
|
|
7026
7417
|
print({ submitted: true, task_id: id });
|
|
7027
7418
|
return;
|
|
@@ -7037,6 +7428,9 @@ var runCli = async (command, raw) => {
|
|
|
7037
7428
|
"workspace"
|
|
7038
7429
|
]);
|
|
7039
7430
|
noPositionals(arguments_, command);
|
|
7431
|
+
if (arguments_.options.has("client-only") && arguments_.options.has("service")) {
|
|
7432
|
+
throw new Error("A client-only setup cannot install a device service");
|
|
7433
|
+
}
|
|
7040
7434
|
const token = option(arguments_, "token-file") === undefined ? undefined : await pairingToken(arguments_);
|
|
7041
7435
|
const interactive = process.stdin.isTTY === true && process.stderr.isTTY === true;
|
|
7042
7436
|
const relayUrl = option(arguments_, "relay");
|
|
@@ -7095,7 +7489,7 @@ var runCli = async (command, raw) => {
|
|
|
7095
7489
|
workspaces: result.configuration.workspaces,
|
|
7096
7490
|
skills: installedSkills,
|
|
7097
7491
|
...service === undefined ? {} : { service },
|
|
7098
|
-
next: service === undefined && !arguments_.options.has("client-only") ? "Run `cohall service install` for background availability, or `cohall device` now." : "Run `cohall doctor
|
|
7492
|
+
next: service === undefined && !arguments_.options.has("client-only") ? "Run `cohall service install` for background availability, or `cohall device` now. Then run `cohall doctor` and the delegation check in `cohall onboard`." : "Run `cohall doctor`, then the delegation check in `cohall onboard`."
|
|
7099
7493
|
});
|
|
7100
7494
|
} finally {
|
|
7101
7495
|
prompter.close();
|
|
@@ -7187,7 +7581,7 @@ var runCli = async (command, raw) => {
|
|
|
7187
7581
|
if (arguments_.positionals.length !== 2 || arguments_.positionals[0] !== "abandon") {
|
|
7188
7582
|
throw new Error("Usage: cohall upgrades [abandon <operation-id>]");
|
|
7189
7583
|
}
|
|
7190
|
-
print(await Effect10.runPromise(relay2.abandonOperation(
|
|
7584
|
+
print(await Effect10.runPromise(relay2.abandonOperation(Schema12.decodeUnknownSync(OperationId)(arguments_.positionals[1]))));
|
|
7191
7585
|
return;
|
|
7192
7586
|
}
|
|
7193
7587
|
print(await Effect10.runPromise(relay2.operations()));
|
|
@@ -7203,9 +7597,9 @@ var runCli = async (command, raw) => {
|
|
|
7203
7597
|
"workspace"
|
|
7204
7598
|
]);
|
|
7205
7599
|
noPositionals(arguments_, command);
|
|
7206
|
-
const token = await pairingToken(arguments_);
|
|
7207
|
-
const relayUrl = normalizeRelayUrl(option(arguments_, "relay") ?? "http://127.0.0.1:8787");
|
|
7208
7600
|
const existing = await readStoredConfiguration();
|
|
7601
|
+
const relayUrl = setupRelayUrl(option(arguments_, "relay") ?? existing?.relayUrl);
|
|
7602
|
+
const token = await pairingToken(arguments_);
|
|
7209
7603
|
const suppliedWorkspaces = values(arguments_, "workspace");
|
|
7210
7604
|
const workspaces = suppliedWorkspaces.length === 0 ? existing?.workspaces : suppliedWorkspaces;
|
|
7211
7605
|
const deviceName = option(arguments_, "name");
|
|
@@ -7287,7 +7681,7 @@ var runCli = async (command, raw) => {
|
|
|
7287
7681
|
...model === undefined ? {} : { model },
|
|
7288
7682
|
...grokGateway === undefined ? {} : { grokGateway },
|
|
7289
7683
|
...(sandbox ?? existing?.sandbox) === undefined ? {} : {
|
|
7290
|
-
sandbox:
|
|
7684
|
+
sandbox: Schema12.decodeUnknownSync(Schema12.Literals(["read-only", "workspace-write", "danger-full-access"]))(sandbox ?? existing?.sandbox)
|
|
7291
7685
|
}
|
|
7292
7686
|
});
|
|
7293
7687
|
await writeStoredConfiguration(configuration2);
|
|
@@ -7352,6 +7746,7 @@ var runCli = async (command, raw) => {
|
|
|
7352
7746
|
return;
|
|
7353
7747
|
});
|
|
7354
7748
|
const clientToken = process.env.COHALL_CLIENT_TOKEN ?? storedCredentials.clientToken;
|
|
7749
|
+
const mcpHost = await readMcpHostDiagnostics();
|
|
7355
7750
|
const mcp = clientToken === undefined ? { status: "not configured" } : await checkMcp(process.argv[1] ?? "", relayUrl, clientToken);
|
|
7356
7751
|
const hasDeviceCredential = process.env.COHALL_DEVICE_TOKEN !== undefined || storedCredentials.deviceToken !== undefined;
|
|
7357
7752
|
const deviceId = process.env.COHALL_DEVICE_ID ?? configuration2?.deviceId;
|
|
@@ -7373,6 +7768,8 @@ var runCli = async (command, raw) => {
|
|
|
7373
7768
|
...response?.ok === true ? [] : ["Relay is unreachable"],
|
|
7374
7769
|
...versionWarning === undefined ? [] : [versionWarning],
|
|
7375
7770
|
...mcp.status === "error" ? [`MCP check failed: ${mcp.error}`] : [],
|
|
7771
|
+
...mcpHost.warnings,
|
|
7772
|
+
...mcpHost.status === "unavailable" ? [`MCP host diagnostics unavailable: ${mcpHost.error}`] : [],
|
|
7376
7773
|
...clientAuthentication.status === "error" ? [clientAuthentication.error] : [],
|
|
7377
7774
|
...hasDeviceCredential && response?.ok === true && clientAuthentication.status !== "error" && currentDevice?.status !== "online" && currentDevice?.status !== "busy" ? [
|
|
7378
7775
|
clientToken === undefined ? "Device status cannot be checked without a client credential" : "Device daemon is not connected or is not registered with the relay"
|
|
@@ -7388,7 +7785,8 @@ var runCli = async (command, raw) => {
|
|
|
7388
7785
|
config_path: configurationPath(),
|
|
7389
7786
|
client_credential: process.env.COHALL_CLIENT_TOKEN !== undefined || storedCredentials.clientToken !== undefined,
|
|
7390
7787
|
client_authentication: clientAuthentication.status === "ok" ? { status: "ok" } : clientAuthentication,
|
|
7391
|
-
mcp,
|
|
7788
|
+
mcp: { ...mcp, scope: "server self-test" },
|
|
7789
|
+
mcp_host: mcpHost,
|
|
7392
7790
|
device_credential: hasDeviceCredential,
|
|
7393
7791
|
workspaces: configuration2?.workspaces ?? [],
|
|
7394
7792
|
providers: providerExecutables,
|
|
@@ -7435,7 +7833,8 @@ var runCli = async (command, raw) => {
|
|
|
7435
7833
|
relay_url: configuration2.relayUrl,
|
|
7436
7834
|
pairing_token: credential.token,
|
|
7437
7835
|
expires_at: credential.expiresAt,
|
|
7438
|
-
roles
|
|
7836
|
+
roles,
|
|
7837
|
+
join_instructions: `Join my existing Cohall relay at ${configuration2.relayUrl} as ${arguments_.options.has("client-only") ? "a client that submits work only" : "a worker and client"}. Follow https://github.com/AksharP5/cohall/blob/main/docs/onboarding.md, "Join an existing relay". I will provide the one-time pairing token separately; read it through a hidden terminal prompt, stdin, or a token file. ${arguments_.options.has("client-only") ? "Use cohall init --client-only; no device service is needed." : "Choose existing workspace roots and installed providers before running cohall init."} Finish the verification steps and report what passed.`
|
|
7439
7838
|
});
|
|
7440
7839
|
return;
|
|
7441
7840
|
}
|
|
@@ -7447,11 +7846,11 @@ var runCli = async (command, raw) => {
|
|
|
7447
7846
|
}
|
|
7448
7847
|
if (command === "forget") {
|
|
7449
7848
|
allowOptions(arguments_, []);
|
|
7450
|
-
print(await Effect10.runPromise(relay2.forgetDevice(
|
|
7849
|
+
print(await Effect10.runPromise(relay2.forgetDevice(Schema12.decodeUnknownSync(DeviceId)(identifier(arguments_, "device id")))));
|
|
7451
7850
|
return;
|
|
7452
7851
|
}
|
|
7453
7852
|
allowOptions(arguments_, []);
|
|
7454
|
-
print(await Effect10.runPromise(relay2.revokeAuthSession(
|
|
7853
|
+
print(await Effect10.runPromise(relay2.revokeAuthSession(Schema12.decodeUnknownSync(AuthSessionId)(identifier(arguments_, "session id")))));
|
|
7455
7854
|
return;
|
|
7456
7855
|
}
|
|
7457
7856
|
const { configuration, relay } = await client();
|
|
@@ -7460,20 +7859,20 @@ var runCli = async (command, raw) => {
|
|
|
7460
7859
|
const rawQuestion = option(arguments_, "question");
|
|
7461
7860
|
if (rawQuestion === undefined)
|
|
7462
7861
|
throw new Error("--question is required");
|
|
7463
|
-
const question =
|
|
7464
|
-
const id = arguments_.positionals.length === 0 ? undefined :
|
|
7862
|
+
const question = Schema12.decodeUnknownSync(RequestTaskInput.fields.question)(rawQuestion);
|
|
7863
|
+
const id = arguments_.positionals.length === 0 ? undefined : Schema12.decodeUnknownSync(TaskId)(identifier(arguments_, "task id"));
|
|
7465
7864
|
const rawRunId = option(arguments_, "run-id");
|
|
7466
|
-
const runId = rawRunId === undefined ? undefined :
|
|
7865
|
+
const runId = rawRunId === undefined ? undefined : Schema12.decodeUnknownSync(TaskRunId)(rawRunId);
|
|
7467
7866
|
print(await Effect10.runPromise(requestInput(relay, configuration, question, id, runId)));
|
|
7468
7867
|
return;
|
|
7469
7868
|
}
|
|
7470
7869
|
if (command === "answer") {
|
|
7471
7870
|
allowOptions(arguments_, ["request-id", "message", "message-file"]);
|
|
7472
|
-
const id =
|
|
7871
|
+
const id = Schema12.decodeUnknownSync(TaskId)(identifier(arguments_, "task id"));
|
|
7473
7872
|
const requestId = option(arguments_, "request-id");
|
|
7474
7873
|
if (requestId === undefined)
|
|
7475
7874
|
throw new Error("--request-id is required; use the input request id from status or inbox");
|
|
7476
|
-
const input =
|
|
7875
|
+
const input = Schema12.decodeUnknownSync(AnswerTaskInput)({
|
|
7477
7876
|
requestId,
|
|
7478
7877
|
answer: await readInput(arguments_, "message")
|
|
7479
7878
|
});
|
|
@@ -7488,8 +7887,8 @@ var runCli = async (command, raw) => {
|
|
|
7488
7887
|
const note = option(arguments_, "message");
|
|
7489
7888
|
if (note === undefined)
|
|
7490
7889
|
throw new Error("--message is required");
|
|
7491
|
-
const input =
|
|
7492
|
-
print(await Effect10.runPromise(relay.reportTaskProgress(
|
|
7890
|
+
const input = Schema12.decodeUnknownSync(TaskProgressInput)({ note });
|
|
7891
|
+
print(await Effect10.runPromise(relay.reportTaskProgress(Schema12.decodeUnknownSync(TaskId)(rawId), input)));
|
|
7493
7892
|
return;
|
|
7494
7893
|
}
|
|
7495
7894
|
if (command === "devices") {
|
|
@@ -7549,15 +7948,15 @@ var runCli = async (command, raw) => {
|
|
|
7549
7948
|
const requestId = option(arguments_, "request-id");
|
|
7550
7949
|
const task = await Effect10.runPromise(createDelegation(relay, configuration, {
|
|
7551
7950
|
prompt,
|
|
7552
|
-
...requestId === undefined ? {} : { requestId:
|
|
7951
|
+
...requestId === undefined ? {} : { requestId: Schema12.decodeUnknownSync(TaskRequestId)(requestId) },
|
|
7553
7952
|
...target === undefined ? {} : { target },
|
|
7554
7953
|
...context === undefined ? {} : { context },
|
|
7555
|
-
...thread === undefined ? {} : { threadId:
|
|
7556
|
-
...parent === undefined ? {} : { parentTaskId:
|
|
7954
|
+
...thread === undefined ? {} : { threadId: Schema12.decodeUnknownSync(ThreadId)(thread) },
|
|
7955
|
+
...parent === undefined ? {} : { parentTaskId: Schema12.decodeUnknownSync(TaskId)(parent) },
|
|
7557
7956
|
...workspace === undefined ? {} : { workspace },
|
|
7558
|
-
...provider === undefined ? {} : { provider:
|
|
7957
|
+
...provider === undefined ? {} : { provider: Schema12.decodeUnknownSync(Provider)(provider) },
|
|
7559
7958
|
...attachments2.length === 0 ? {} : { attachments: attachments2 },
|
|
7560
|
-
...deadline === undefined ? {} : { expiresAt:
|
|
7959
|
+
...deadline === undefined ? {} : { expiresAt: Schema12.decodeUnknownSync(TaskDeadline)(deadline) }
|
|
7561
7960
|
}));
|
|
7562
7961
|
const completed = arguments_.options.has("no-wait") ? task : await Effect10.runPromise(waitForTask(relay, task, waitSeconds));
|
|
7563
7962
|
print(arguments_.options.has("no-wait") ? taskResult(completed) : await acknowledgedTaskResult(relay, completed));
|
|
@@ -7568,7 +7967,7 @@ var runCli = async (command, raw) => {
|
|
|
7568
7967
|
if (arguments_.positionals[0] === "ack") {
|
|
7569
7968
|
if (arguments_.positionals.length !== 2)
|
|
7570
7969
|
throw new Error("Usage: cohall inbox ack <task-id>");
|
|
7571
|
-
const id =
|
|
7970
|
+
const id = Schema12.decodeUnknownSync(TaskId)(arguments_.positionals[1]);
|
|
7572
7971
|
print(await Effect10.runPromise(relay.acknowledgeCompletion(id)));
|
|
7573
7972
|
return;
|
|
7574
7973
|
}
|
|
@@ -7578,13 +7977,13 @@ var runCli = async (command, raw) => {
|
|
|
7578
7977
|
}
|
|
7579
7978
|
if (command === "status") {
|
|
7580
7979
|
allowOptions(arguments_, []);
|
|
7581
|
-
const id =
|
|
7980
|
+
const id = Schema12.decodeUnknownSync(TaskId)(identifier(arguments_, "task id"));
|
|
7582
7981
|
print(taskResult(await Effect10.runPromise(relay.getTask(id))));
|
|
7583
7982
|
return;
|
|
7584
7983
|
}
|
|
7585
7984
|
if (command === "attachments") {
|
|
7586
7985
|
allowOptions(arguments_, []);
|
|
7587
|
-
const id =
|
|
7986
|
+
const id = Schema12.decodeUnknownSync(TaskId)(identifier(arguments_, "task id"));
|
|
7588
7987
|
print(await Effect10.runPromise(relay.listAttachments(id)));
|
|
7589
7988
|
return;
|
|
7590
7989
|
}
|
|
@@ -7597,18 +7996,18 @@ var runCli = async (command, raw) => {
|
|
|
7597
7996
|
if (destination === undefined) {
|
|
7598
7997
|
throw new Error("--output is required");
|
|
7599
7998
|
}
|
|
7600
|
-
const id =
|
|
7601
|
-
const name =
|
|
7999
|
+
const id = Schema12.decodeUnknownSync(TaskId)(arguments_.positionals[0]);
|
|
8000
|
+
const name = Schema12.decodeUnknownSync(AttachmentName)(arguments_.positionals[1]);
|
|
7602
8001
|
const directionInput = option(arguments_, "direction");
|
|
7603
|
-
const direction = directionInput === undefined ? undefined :
|
|
8002
|
+
const direction = directionInput === undefined ? undefined : Schema12.decodeUnknownSync(AttachmentDirection)(directionInput);
|
|
7604
8003
|
const data = await Effect10.runPromise(relay.readAttachment(id, name, direction));
|
|
7605
|
-
await
|
|
8004
|
+
await writeFile8(destination, data, { flag: "wx", mode: 384 });
|
|
7606
8005
|
print({ task_id: id, name, path: resolve4(destination), bytes: data.length });
|
|
7607
8006
|
return;
|
|
7608
8007
|
}
|
|
7609
8008
|
if (command === "trace") {
|
|
7610
8009
|
allowOptions(arguments_, ["follow"]);
|
|
7611
|
-
const id =
|
|
8010
|
+
const id = Schema12.decodeUnknownSync(TaskId)(identifier(arguments_, "task id"));
|
|
7612
8011
|
if (arguments_.options.has("follow")) {
|
|
7613
8012
|
await Effect10.runPromise(followTaskTrace(relay, id, printLine));
|
|
7614
8013
|
return;
|
|
@@ -7618,19 +8017,19 @@ var runCli = async (command, raw) => {
|
|
|
7618
8017
|
}
|
|
7619
8018
|
if (command === "wait") {
|
|
7620
8019
|
allowOptions(arguments_, ["timeout"]);
|
|
7621
|
-
const id =
|
|
8020
|
+
const id = Schema12.decodeUnknownSync(TaskId)(identifier(arguments_, "task id"));
|
|
7622
8021
|
print(taskResult(await Effect10.runPromise(waitForTask(relay, id, timeout(arguments_)))));
|
|
7623
8022
|
return;
|
|
7624
8023
|
}
|
|
7625
8024
|
if (command === "cancel") {
|
|
7626
8025
|
allowOptions(arguments_, []);
|
|
7627
|
-
const id =
|
|
8026
|
+
const id = Schema12.decodeUnknownSync(TaskId)(identifier(arguments_, "task id"));
|
|
7628
8027
|
print(taskResult(await Effect10.runPromise(relay.cancelTask(id))));
|
|
7629
8028
|
return;
|
|
7630
8029
|
}
|
|
7631
8030
|
if (command === "thread") {
|
|
7632
8031
|
allowOptions(arguments_, []);
|
|
7633
|
-
const id =
|
|
8032
|
+
const id = Schema12.decodeUnknownSync(ThreadId)(identifier(arguments_, "thread id"));
|
|
7634
8033
|
print(await Effect10.runPromise(threadContext(relay, id)));
|
|
7635
8034
|
return;
|
|
7636
8035
|
}
|
|
@@ -7639,15 +8038,15 @@ var runCli = async (command, raw) => {
|
|
|
7639
8038
|
|
|
7640
8039
|
// apps/device/src/daemon.ts
|
|
7641
8040
|
init_src();
|
|
7642
|
-
import { Effect as Effect11, Schedule as Schedule3, Schema as
|
|
7643
|
-
import { constants as
|
|
7644
|
-
import { open as
|
|
8041
|
+
import { Effect as Effect11, Schedule as Schedule3, Schema as Schema13 } from "effect";
|
|
8042
|
+
import { constants as constants5 } from "node:fs";
|
|
8043
|
+
import { open as open6, realpath as realpath4, stat as stat5 } from "node:fs/promises";
|
|
7645
8044
|
import { arch, hostname as hostname3, platform as platform4 } from "node:os";
|
|
7646
|
-
import { basename as basename3, isAbsolute as isAbsolute5, relative as relative2 } from "node:path";
|
|
8045
|
+
import { basename as basename3, isAbsolute as isAbsolute5, relative as relative2, sep } from "node:path";
|
|
7647
8046
|
import { WebSocket as WebSocket2 } from "ws";
|
|
7648
8047
|
var maxQueuedRelayMessages = 8;
|
|
7649
8048
|
|
|
7650
|
-
class DeviceConnectionError extends
|
|
8049
|
+
class DeviceConnectionError extends Schema13.TaggedErrorClass()("Device.ConnectionError", { message: Schema13.String }) {
|
|
7651
8050
|
}
|
|
7652
8051
|
var socketUrl = (configuration) => {
|
|
7653
8052
|
const url = new URL(configuration.relayUrl);
|
|
@@ -7724,7 +8123,7 @@ var allowedWorkspace = async (configuration, requested) => {
|
|
|
7724
8123
|
const candidate = await realpath4(selected);
|
|
7725
8124
|
const allowed = configuration.workspaces.some((root) => {
|
|
7726
8125
|
const child = relative2(root, candidate);
|
|
7727
|
-
return child
|
|
8126
|
+
return child !== ".." && !child.startsWith(`..${sep}`) && !isAbsolute5(child);
|
|
7728
8127
|
});
|
|
7729
8128
|
if (!allowed) {
|
|
7730
8129
|
throw new Error(`Workspace ${candidate} is outside this device's configured workspace roots`);
|
|
@@ -7734,8 +8133,8 @@ var allowedWorkspace = async (configuration, requested) => {
|
|
|
7734
8133
|
var openAllowedWorkspace = async (configuration, requested) => {
|
|
7735
8134
|
const path = await allowedWorkspace(configuration, requested);
|
|
7736
8135
|
const operatingSystem2 = platform4();
|
|
7737
|
-
const flags =
|
|
7738
|
-
const handle = await
|
|
8136
|
+
const flags = constants5.O_RDONLY | (operatingSystem2 === "win32" ? 0 : constants5.O_DIRECTORY | constants5.O_NOFOLLOW);
|
|
8137
|
+
const handle = await open6(path, flags);
|
|
7739
8138
|
const identity = await handle.stat();
|
|
7740
8139
|
if (!identity.isDirectory()) {
|
|
7741
8140
|
await handle.close();
|
|
@@ -8168,9 +8567,9 @@ var connect = (configuration, state) => Effect11.tryPromise({
|
|
|
8168
8567
|
if (closed) {
|
|
8169
8568
|
return;
|
|
8170
8569
|
}
|
|
8171
|
-
const
|
|
8570
|
+
const text4 = 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");
|
|
8172
8571
|
const event = await Effect11.runPromise(Effect11.try({
|
|
8173
|
-
try: () => JSON.parse(
|
|
8572
|
+
try: () => JSON.parse(text4),
|
|
8174
8573
|
catch: () => new DeviceConnectionError({ message: "Relay sent invalid JSON" })
|
|
8175
8574
|
}).pipe(Effect11.flatMap(decodeSocketEvent)));
|
|
8176
8575
|
if (event._tag === "Connected") {
|
|
@@ -8263,10 +8662,9 @@ var runDaemon = (configuration) => {
|
|
|
8263
8662
|
// apps/device/src/mcp.ts
|
|
8264
8663
|
init_src();
|
|
8265
8664
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
8266
|
-
import {
|
|
8267
|
-
import { Effect as Effect12, Schema as Schema13 } from "effect";
|
|
8665
|
+
import { Effect as Effect12, Schema as Schema14 } from "effect";
|
|
8268
8666
|
import * as z from "zod/v4";
|
|
8269
|
-
import { writeFile as
|
|
8667
|
+
import { writeFile as writeFile9 } from "node:fs/promises";
|
|
8270
8668
|
|
|
8271
8669
|
// apps/device/src/mcp-build-notice.ts
|
|
8272
8670
|
import { execFile as execFile3 } from "node:child_process";
|
|
@@ -8336,7 +8734,7 @@ var runMcp = async (configuration) => {
|
|
|
8336
8734
|
run_id: z.string().uuid().optional(),
|
|
8337
8735
|
question: z.string().min(1).max(maxClarificationBytes)
|
|
8338
8736
|
}
|
|
8339
|
-
}, async ({ task_id, run_id, question }) => output(await Effect12.runPromise(requestInput(client2, configuration,
|
|
8737
|
+
}, async ({ task_id, run_id, question }) => output(await Effect12.runPromise(requestInput(client2, configuration, Schema14.decodeUnknownSync(RequestTaskInput.fields.question)(question), task_id === undefined ? undefined : Schema14.decodeUnknownSync(TaskId)(task_id), run_id === undefined ? undefined : Schema14.decodeUnknownSync(TaskRunId)(run_id)))));
|
|
8340
8738
|
server.registerTool("task_answer", {
|
|
8341
8739
|
title: "Answer a worker's question",
|
|
8342
8740
|
description: "Answer the current clarification and resume the same task. Use input_request.id from task_status or delegate as request_id, or inputRequest.id from completion_inbox. Only the original requester or relay owner can answer. Use wait_task to collect the resumed result.",
|
|
@@ -8345,7 +8743,7 @@ var runMcp = async (configuration) => {
|
|
|
8345
8743
|
request_id: z.string().uuid(),
|
|
8346
8744
|
answer: z.string().min(1).max(maxClarificationBytes)
|
|
8347
8745
|
}
|
|
8348
|
-
}, async ({ task_id, request_id, answer }) => output(taskResult(await Effect12.runPromise(client2.answerTaskInput(
|
|
8746
|
+
}, async ({ task_id, request_id, answer }) => output(taskResult(await Effect12.runPromise(client2.answerTaskInput(Schema14.decodeUnknownSync(TaskId)(task_id), Schema14.decodeUnknownSync(AnswerTaskInput)({ requestId: request_id, answer }))))));
|
|
8349
8747
|
server.registerTool("task_progress", {
|
|
8350
8748
|
title: "Report task progress",
|
|
8351
8749
|
description: `Replace the latest progress note for a running task on this device. Use brief milestones, up to ${maxProgressNoteBytes} UTF-8 bytes. Inherits the delegated task ID when omitted.`,
|
|
@@ -8357,8 +8755,8 @@ var runMcp = async (configuration) => {
|
|
|
8357
8755
|
const id = task_id ?? configuration.mcpTaskId;
|
|
8358
8756
|
if (id === undefined)
|
|
8359
8757
|
throw new Error("Task id is required outside delegated work");
|
|
8360
|
-
const input =
|
|
8361
|
-
return output(await Effect12.runPromise(client2.reportTaskProgress(
|
|
8758
|
+
const input = Schema14.decodeUnknownSync(TaskProgressInput)({ note });
|
|
8759
|
+
return output(await Effect12.runPromise(client2.reportTaskProgress(Schema14.decodeUnknownSync(TaskId)(id), input)));
|
|
8362
8760
|
});
|
|
8363
8761
|
server.registerTool("list_devices", {
|
|
8364
8762
|
title: "List Cohall devices",
|
|
@@ -8404,7 +8802,7 @@ var runMcp = async (configuration) => {
|
|
|
8404
8802
|
const attachments2 = await readInputAttachments(attachment_paths ?? []);
|
|
8405
8803
|
const task = await Effect12.runPromise(createDelegation(client2, configuration, {
|
|
8406
8804
|
prompt,
|
|
8407
|
-
...request_id === undefined ? {} : { requestId:
|
|
8805
|
+
...request_id === undefined ? {} : { requestId: Schema14.decodeUnknownSync(TaskRequestId)(request_id) },
|
|
8408
8806
|
...provider === undefined ? {} : { provider },
|
|
8409
8807
|
...target === undefined ? {} : { target },
|
|
8410
8808
|
...context === undefined ? {} : { context },
|
|
@@ -8412,7 +8810,7 @@ var runMcp = async (configuration) => {
|
|
|
8412
8810
|
...parent_task_id === undefined ? {} : { parentTaskId: TaskId.make(parent_task_id) },
|
|
8413
8811
|
...workspace === undefined ? {} : { workspace },
|
|
8414
8812
|
...attachments2.length === 0 ? {} : { attachments: attachments2 },
|
|
8415
|
-
...deadline === undefined ? {} : { expiresAt:
|
|
8813
|
+
...deadline === undefined ? {} : { expiresAt: Schema14.decodeUnknownSync(TaskDeadline)(deadline) }
|
|
8416
8814
|
}), { signal });
|
|
8417
8815
|
const completed = wait ? await Effect12.runPromise(waitForTask(client2, task, timeout_seconds), { signal }) : task;
|
|
8418
8816
|
return output(wait ? await acknowledgedTaskResult(client2, completed, signal) : taskResult(completed));
|
|
@@ -8446,11 +8844,12 @@ var runMcp = async (configuration) => {
|
|
|
8446
8844
|
output_path: z.string(),
|
|
8447
8845
|
direction: z.enum(["input", "output"]).optional()
|
|
8448
8846
|
}
|
|
8449
|
-
}, async ({ task_id, name, output_path, direction }) => {
|
|
8450
|
-
const safeName =
|
|
8451
|
-
const safeDirection = direction === undefined ? undefined :
|
|
8452
|
-
const data = await Effect12.runPromise(client2.readAttachment(TaskId.make(task_id), safeName, safeDirection));
|
|
8453
|
-
|
|
8847
|
+
}, async ({ task_id, name, output_path, direction }, { signal }) => {
|
|
8848
|
+
const safeName = Schema14.decodeUnknownSync(AttachmentName)(name);
|
|
8849
|
+
const safeDirection = direction === undefined ? undefined : Schema14.decodeUnknownSync(AttachmentDirection)(direction);
|
|
8850
|
+
const data = await Effect12.runPromise(client2.readAttachment(TaskId.make(task_id), safeName, safeDirection), { signal });
|
|
8851
|
+
signal.throwIfAborted();
|
|
8852
|
+
await writeFile9(output_path, data, { flag: "wx", mode: 384, signal });
|
|
8454
8853
|
return output({ task_id, name: safeName, output_path, bytes: data.length });
|
|
8455
8854
|
});
|
|
8456
8855
|
server.registerTool("task_trace", {
|
|
@@ -8480,7 +8879,7 @@ var runMcp = async (configuration) => {
|
|
|
8480
8879
|
description: "Read shared task prompts and final results for a cross-device thread.",
|
|
8481
8880
|
inputSchema: { thread_id: z.string().uuid() }
|
|
8482
8881
|
}, async ({ thread_id }) => output(await Effect12.runPromise(threadContext(client2, ThreadId.make(thread_id)))));
|
|
8483
|
-
await server.connect(
|
|
8882
|
+
await server.connect(createMcpHostDiagnostics(server.server));
|
|
8484
8883
|
};
|
|
8485
8884
|
|
|
8486
8885
|
// apps/device/src/main.ts
|
|
@@ -8536,4 +8935,4 @@ await main().catch((cause) => {
|
|
|
8536
8935
|
process.exitCode = 1;
|
|
8537
8936
|
});
|
|
8538
8937
|
|
|
8539
|
-
//# debugId=
|
|
8938
|
+
//# debugId=6DD87AF6E4752A1764756E2164756E21
|