@schoolai/shipyard 0.1.0 → 0.5.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/dist/{auth-BJMJG73E.js → auth-YRHZ3SMD.js} +3 -3
- package/dist/{chunk-WB5DGKI3.js → chunk-6S523TH3.js} +2 -1
- package/dist/{chunk-WB5DGKI3.js.map → chunk-6S523TH3.js.map} +1 -1
- package/dist/{chunk-HOG3H5HO.js → chunk-CQBP5B4G.js} +106 -13
- package/dist/{chunk-HOG3H5HO.js.map → chunk-CQBP5B4G.js.map} +1 -1
- package/dist/{chunk-4KTYP247.js → chunk-H3TM7MVJ.js} +2 -2
- package/dist/index.js +2176 -204
- package/dist/index.js.map +1 -1
- package/dist/{login-YE3CJW2C.js → login-625HP2EN.js} +8 -6
- package/dist/login-625HP2EN.js.map +1 -0
- package/dist/{logout-5JRAQEQC.js → logout-L6EMSGQU.js} +3 -3
- package/package.json +1 -1
- package/dist/login-YE3CJW2C.js.map +0 -1
- /package/dist/{auth-BJMJG73E.js.map → auth-YRHZ3SMD.js.map} +0 -0
- /package/dist/{chunk-4KTYP247.js.map → chunk-H3TM7MVJ.js.map} +0 -0
- /package/dist/{logout-5JRAQEQC.js.map → logout-L6EMSGQU.js.map} +0 -0
package/dist/index.js
CHANGED
|
@@ -1,15 +1,25 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
+
AuthGitHubCallbackRequestSchema,
|
|
4
|
+
AuthGitHubCallbackResponseSchema,
|
|
5
|
+
AuthVerifyResponseSchema,
|
|
6
|
+
BRANCH_NAME_PATTERN,
|
|
7
|
+
CollabCreateRequestSchema,
|
|
8
|
+
CollabCreateResponseSchema,
|
|
9
|
+
ErrorResponseSchema,
|
|
10
|
+
HealthResponseSchema,
|
|
3
11
|
PermissionModeSchema,
|
|
4
12
|
PersonalRoomConnection,
|
|
5
|
-
ROUTES
|
|
6
|
-
|
|
13
|
+
ROUTES,
|
|
14
|
+
ValidationErrorResponseSchema
|
|
15
|
+
} from "./chunk-CQBP5B4G.js";
|
|
7
16
|
import {
|
|
17
|
+
getShipyardHome,
|
|
8
18
|
validateEnv
|
|
9
|
-
} from "./chunk-
|
|
19
|
+
} from "./chunk-6S523TH3.js";
|
|
10
20
|
|
|
11
21
|
// src/index.ts
|
|
12
|
-
import { mkdir as
|
|
22
|
+
import { mkdir as mkdir4 } from "fs/promises";
|
|
13
23
|
import { homedir as homedir3 } from "os";
|
|
14
24
|
import { resolve as resolve2 } from "path";
|
|
15
25
|
import { parseArgs } from "util";
|
|
@@ -3255,6 +3265,9 @@ function changeRef(ref, fn) {
|
|
|
3255
3265
|
internals.getDoc().commit();
|
|
3256
3266
|
return ref;
|
|
3257
3267
|
}
|
|
3268
|
+
function getLoroDoc(docOrRef) {
|
|
3269
|
+
return loro(docOrRef).doc;
|
|
3270
|
+
}
|
|
3258
3271
|
function createPathSelector(segments) {
|
|
3259
3272
|
return {
|
|
3260
3273
|
__resultType: void 0,
|
|
@@ -11179,6 +11192,14 @@ var EpochDocumentSchema = Shape.doc({
|
|
|
11179
11192
|
version: Shape.plain.number()
|
|
11180
11193
|
})
|
|
11181
11194
|
});
|
|
11195
|
+
var SUPPORTED_IMAGE_MEDIA_TYPES = ["image/png", "image/jpeg", "image/gif", "image/webp"];
|
|
11196
|
+
var ImageSourceShape = Shape.plain.discriminatedUnion("type", {
|
|
11197
|
+
base64: Shape.plain.struct({
|
|
11198
|
+
type: Shape.plain.string("base64"),
|
|
11199
|
+
mediaType: Shape.plain.string(),
|
|
11200
|
+
data: Shape.plain.string()
|
|
11201
|
+
})
|
|
11202
|
+
});
|
|
11182
11203
|
var ContentBlockShape = Shape.plain.discriminatedUnion("type", {
|
|
11183
11204
|
text: Shape.plain.struct({
|
|
11184
11205
|
type: Shape.plain.string("text"),
|
|
@@ -11201,6 +11222,11 @@ var ContentBlockShape = Shape.plain.discriminatedUnion("type", {
|
|
|
11201
11222
|
thinking: Shape.plain.struct({
|
|
11202
11223
|
type: Shape.plain.string("thinking"),
|
|
11203
11224
|
text: Shape.plain.string()
|
|
11225
|
+
}),
|
|
11226
|
+
image: Shape.plain.struct({
|
|
11227
|
+
type: Shape.plain.string("image"),
|
|
11228
|
+
id: Shape.plain.string(),
|
|
11229
|
+
source: ImageSourceShape
|
|
11204
11230
|
})
|
|
11205
11231
|
});
|
|
11206
11232
|
var A2A_TASK_STATES = [
|
|
@@ -11215,6 +11241,9 @@ var A2A_TASK_STATES = [
|
|
|
11215
11241
|
var SESSION_STATES = ["pending", "active", "completed", "failed", "interrupted"];
|
|
11216
11242
|
var REASONING_EFFORTS = ["low", "medium", "high"];
|
|
11217
11243
|
var PERMISSION_MODES = ["default", "accept-edits", "plan", "bypass"];
|
|
11244
|
+
var ANTHROPIC_AUTH_STATUSES = ["authenticated", "unauthenticated", "unknown"];
|
|
11245
|
+
var ANTHROPIC_AUTH_METHODS = ["api-key", "oauth", "none"];
|
|
11246
|
+
var ANTHROPIC_LOGIN_STATUSES = ["starting", "waiting", "done", "error"];
|
|
11218
11247
|
var MessageShape = Shape.plain.struct({
|
|
11219
11248
|
messageId: Shape.plain.string(),
|
|
11220
11249
|
role: Shape.plain.string("user", "assistant"),
|
|
@@ -11281,6 +11310,17 @@ var DiffCommentShape = Shape.plain.struct({
|
|
|
11281
11310
|
createdAt: Shape.plain.number(),
|
|
11282
11311
|
resolvedAt: Shape.plain.number().nullable()
|
|
11283
11312
|
});
|
|
11313
|
+
var PlanCommentShape = Shape.plain.struct({
|
|
11314
|
+
commentId: Shape.plain.string(),
|
|
11315
|
+
planId: Shape.plain.string(),
|
|
11316
|
+
from: Shape.plain.number(),
|
|
11317
|
+
to: Shape.plain.number(),
|
|
11318
|
+
body: Shape.plain.string(),
|
|
11319
|
+
authorType: Shape.plain.string(...COMMENT_AUTHOR_TYPES),
|
|
11320
|
+
authorId: Shape.plain.string(),
|
|
11321
|
+
createdAt: Shape.plain.number(),
|
|
11322
|
+
resolvedAt: Shape.plain.number().nullable()
|
|
11323
|
+
});
|
|
11284
11324
|
var TaskDocumentSchema = Shape.doc({
|
|
11285
11325
|
meta: Shape.struct({
|
|
11286
11326
|
id: Shape.plain.string(),
|
|
@@ -11293,7 +11333,9 @@ var TaskDocumentSchema = Shape.doc({
|
|
|
11293
11333
|
sessions: Shape.list(SessionEntryShape),
|
|
11294
11334
|
diffState: DiffStateShape,
|
|
11295
11335
|
plans: Shape.list(PlanVersionShape),
|
|
11296
|
-
|
|
11336
|
+
planEditorDocs: Shape.record(Shape.any()),
|
|
11337
|
+
diffComments: Shape.record(DiffCommentShape),
|
|
11338
|
+
planComments: Shape.record(PlanCommentShape)
|
|
11297
11339
|
});
|
|
11298
11340
|
var TOOL_RISK_LEVELS = ["low", "medium", "high"];
|
|
11299
11341
|
var PERMISSION_DECISIONS = ["approved", "denied"];
|
|
@@ -11335,8 +11377,26 @@ var TaskIndexEntryShape = Shape.struct({
|
|
|
11335
11377
|
createdAt: Shape.plain.number(),
|
|
11336
11378
|
updatedAt: Shape.plain.number()
|
|
11337
11379
|
});
|
|
11380
|
+
var WORKTREE_SETUP_STATUSES = ["running", "done", "failed"];
|
|
11381
|
+
var WorktreeSetupStatusShape = Shape.struct({
|
|
11382
|
+
status: Shape.plain.string(...WORKTREE_SETUP_STATUSES),
|
|
11383
|
+
machineId: Shape.plain.string(),
|
|
11384
|
+
startedAt: Shape.plain.number(),
|
|
11385
|
+
completedAt: Shape.plain.number().nullable(),
|
|
11386
|
+
exitCode: Shape.plain.number().nullable(),
|
|
11387
|
+
signal: Shape.plain.string().nullable(),
|
|
11388
|
+
pid: Shape.plain.number().nullable()
|
|
11389
|
+
});
|
|
11390
|
+
var WorktreeScriptShape = Shape.plain.struct({
|
|
11391
|
+
script: Shape.plain.string()
|
|
11392
|
+
});
|
|
11393
|
+
var UserSettingsShape = Shape.struct({
|
|
11394
|
+
worktreeScripts: Shape.record(WorktreeScriptShape)
|
|
11395
|
+
});
|
|
11338
11396
|
var TaskIndexDocumentSchema = Shape.doc({
|
|
11339
|
-
taskIndex: Shape.record(TaskIndexEntryShape)
|
|
11397
|
+
taskIndex: Shape.record(TaskIndexEntryShape),
|
|
11398
|
+
worktreeSetupStatus: Shape.record(WorktreeSetupStatusShape),
|
|
11399
|
+
userSettings: UserSettingsShape
|
|
11340
11400
|
});
|
|
11341
11401
|
var ReasoningCapabilityShape = Shape.plain.struct({
|
|
11342
11402
|
efforts: Shape.plain.array(Shape.plain.string(...REASONING_EFFORTS)),
|
|
@@ -11354,14 +11414,68 @@ var GitRepoInfoShape = Shape.plain.struct({
|
|
|
11354
11414
|
branch: Shape.plain.string(),
|
|
11355
11415
|
remote: Shape.plain.string().nullable()
|
|
11356
11416
|
});
|
|
11417
|
+
var AnthropicAuthShape = Shape.plain.struct({
|
|
11418
|
+
status: Shape.plain.string(...ANTHROPIC_AUTH_STATUSES),
|
|
11419
|
+
method: Shape.plain.string(...ANTHROPIC_AUTH_METHODS),
|
|
11420
|
+
email: Shape.plain.string().nullable()
|
|
11421
|
+
});
|
|
11357
11422
|
var MachineCapabilitiesEphemeral = Shape.plain.struct({
|
|
11358
11423
|
models: Shape.plain.array(ModelInfoShape),
|
|
11359
11424
|
environments: Shape.plain.array(GitRepoInfoShape),
|
|
11360
11425
|
permissionModes: Shape.plain.array(Shape.plain.string(...PERMISSION_MODES)),
|
|
11361
|
-
homeDir: Shape.plain.string().nullable()
|
|
11426
|
+
homeDir: Shape.plain.string().nullable(),
|
|
11427
|
+
anthropicAuth: AnthropicAuthShape.nullable()
|
|
11428
|
+
});
|
|
11429
|
+
var EnhancePromptRequestEphemeral = Shape.plain.struct({
|
|
11430
|
+
machineId: Shape.plain.string(),
|
|
11431
|
+
prompt: Shape.plain.string(),
|
|
11432
|
+
requestedAt: Shape.plain.number()
|
|
11433
|
+
});
|
|
11434
|
+
var EnhancePromptResponseEphemeral = Shape.plain.struct({
|
|
11435
|
+
status: Shape.plain.string("streaming", "done", "error"),
|
|
11436
|
+
text: Shape.plain.string(),
|
|
11437
|
+
error: Shape.plain.string().nullable()
|
|
11438
|
+
});
|
|
11439
|
+
var WorktreeCreateRequestEphemeral = Shape.plain.struct({
|
|
11440
|
+
machineId: Shape.plain.string(),
|
|
11441
|
+
sourceRepoPath: Shape.plain.string(),
|
|
11442
|
+
branchName: Shape.plain.string(),
|
|
11443
|
+
baseRef: Shape.plain.string(),
|
|
11444
|
+
setupScript: Shape.plain.string().nullable(),
|
|
11445
|
+
requestedAt: Shape.plain.number()
|
|
11446
|
+
});
|
|
11447
|
+
var WorktreeCreateResponseEphemeral = Shape.plain.struct({
|
|
11448
|
+
status: Shape.plain.string("creating-worktree", "copying-files", "running-setup-script", "refreshing-environments", "done", "error"),
|
|
11449
|
+
detail: Shape.plain.string().nullable(),
|
|
11450
|
+
worktreePath: Shape.plain.string().nullable(),
|
|
11451
|
+
branchName: Shape.plain.string().nullable(),
|
|
11452
|
+
setupScriptStarted: Shape.plain.boolean().nullable(),
|
|
11453
|
+
warnings: Shape.plain.array(Shape.plain.string()).nullable(),
|
|
11454
|
+
error: Shape.plain.string().nullable()
|
|
11455
|
+
});
|
|
11456
|
+
var WorktreeSetupResultEphemeral = Shape.plain.struct({
|
|
11457
|
+
exitCode: Shape.plain.number().nullable(),
|
|
11458
|
+
signal: Shape.plain.string().nullable(),
|
|
11459
|
+
worktreePath: Shape.plain.string()
|
|
11460
|
+
});
|
|
11461
|
+
var AnthropicLoginRequestEphemeral = Shape.plain.struct({
|
|
11462
|
+
machineId: Shape.plain.string(),
|
|
11463
|
+
requestedAt: Shape.plain.number()
|
|
11464
|
+
});
|
|
11465
|
+
var AnthropicLoginResponseEphemeral = Shape.plain.struct({
|
|
11466
|
+
status: Shape.plain.string(...ANTHROPIC_LOGIN_STATUSES),
|
|
11467
|
+
loginUrl: Shape.plain.string().nullable(),
|
|
11468
|
+
error: Shape.plain.string().nullable()
|
|
11362
11469
|
});
|
|
11363
11470
|
var ROOM_EPHEMERAL_DECLARATIONS = {
|
|
11364
|
-
capabilities: MachineCapabilitiesEphemeral
|
|
11471
|
+
capabilities: MachineCapabilitiesEphemeral,
|
|
11472
|
+
enhancePromptReqs: EnhancePromptRequestEphemeral,
|
|
11473
|
+
enhancePromptResps: EnhancePromptResponseEphemeral,
|
|
11474
|
+
worktreeCreateReqs: WorktreeCreateRequestEphemeral,
|
|
11475
|
+
worktreeCreateResps: WorktreeCreateResponseEphemeral,
|
|
11476
|
+
worktreeSetupResps: WorktreeSetupResultEphemeral,
|
|
11477
|
+
anthropicLoginReqs: AnthropicLoginRequestEphemeral,
|
|
11478
|
+
anthropicLoginResps: AnthropicLoginResponseEphemeral
|
|
11365
11479
|
};
|
|
11366
11480
|
|
|
11367
11481
|
// src/file-storage-adapter.ts
|
|
@@ -11440,6 +11554,11 @@ var FileStorageAdapter = class extends StorageAdapter {
|
|
|
11440
11554
|
}
|
|
11441
11555
|
};
|
|
11442
11556
|
|
|
11557
|
+
// src/lifecycle.ts
|
|
11558
|
+
import { unlinkSync } from "fs";
|
|
11559
|
+
import { readFile as readFile2, unlink as unlink2, writeFile as writeFile2 } from "fs/promises";
|
|
11560
|
+
import { join as join2 } from "path";
|
|
11561
|
+
|
|
11443
11562
|
// src/logger.ts
|
|
11444
11563
|
import pino from "pino";
|
|
11445
11564
|
var logger = pino({
|
|
@@ -11451,11 +11570,24 @@ function createChildLogger(context) {
|
|
|
11451
11570
|
}
|
|
11452
11571
|
|
|
11453
11572
|
// src/lifecycle.ts
|
|
11573
|
+
function isProcessAlive(pid) {
|
|
11574
|
+
try {
|
|
11575
|
+
process.kill(pid, 0);
|
|
11576
|
+
return true;
|
|
11577
|
+
} catch {
|
|
11578
|
+
return false;
|
|
11579
|
+
}
|
|
11580
|
+
}
|
|
11581
|
+
function isEnoent(err) {
|
|
11582
|
+
return typeof err === "object" && err !== null && "code" in err && err.code === "ENOENT";
|
|
11583
|
+
}
|
|
11454
11584
|
var LifecycleManager = class {
|
|
11455
11585
|
#abortControllers = /* @__PURE__ */ new Set();
|
|
11456
11586
|
#shutdownCallbacks = [];
|
|
11457
11587
|
#isShuttingDown = false;
|
|
11588
|
+
// biome-ignore lint/suspicious/noExplicitAny: process event handlers have heterogeneous signatures
|
|
11458
11589
|
#signalHandlers = [];
|
|
11590
|
+
#pidFilePath = null;
|
|
11459
11591
|
constructor() {
|
|
11460
11592
|
const termHandler = () => void this.#shutdown("SIGTERM");
|
|
11461
11593
|
const intHandler = () => void this.#shutdown("SIGINT");
|
|
@@ -11465,6 +11597,32 @@ var LifecycleManager = class {
|
|
|
11465
11597
|
{ signal: "SIGTERM", handler: termHandler },
|
|
11466
11598
|
{ signal: "SIGINT", handler: intHandler }
|
|
11467
11599
|
];
|
|
11600
|
+
const exceptionHandler = (error) => {
|
|
11601
|
+
try {
|
|
11602
|
+
logger.error({ error }, "Uncaught exception \u2014 initiating shutdown");
|
|
11603
|
+
} catch {
|
|
11604
|
+
}
|
|
11605
|
+
void this.#shutdown("uncaughtException");
|
|
11606
|
+
};
|
|
11607
|
+
const rejectionHandler = (reason) => {
|
|
11608
|
+
try {
|
|
11609
|
+
let jsonStr;
|
|
11610
|
+
try {
|
|
11611
|
+
jsonStr = JSON.stringify(reason);
|
|
11612
|
+
} catch {
|
|
11613
|
+
jsonStr = "(not serializable)";
|
|
11614
|
+
}
|
|
11615
|
+
const detail = reason instanceof Error ? { message: reason.message, stack: reason.stack } : { inspected: `${String(reason)} ${jsonStr}` };
|
|
11616
|
+
logger.error(detail, "Unhandled rejection (non-fatal)");
|
|
11617
|
+
} catch {
|
|
11618
|
+
}
|
|
11619
|
+
};
|
|
11620
|
+
process.on("uncaughtException", exceptionHandler);
|
|
11621
|
+
process.on("unhandledRejection", rejectionHandler);
|
|
11622
|
+
this.#signalHandlers.push(
|
|
11623
|
+
{ signal: "uncaughtException", handler: exceptionHandler },
|
|
11624
|
+
{ signal: "unhandledRejection", handler: rejectionHandler }
|
|
11625
|
+
);
|
|
11468
11626
|
}
|
|
11469
11627
|
destroy() {
|
|
11470
11628
|
for (const { signal, handler } of this.#signalHandlers) {
|
|
@@ -11474,6 +11632,43 @@ var LifecycleManager = class {
|
|
|
11474
11632
|
this.#isShuttingDown = false;
|
|
11475
11633
|
this.#shutdownCallbacks = [];
|
|
11476
11634
|
this.#abortControllers.clear();
|
|
11635
|
+
this.#removePidFileSync();
|
|
11636
|
+
}
|
|
11637
|
+
async acquirePidFile(shipyardHome) {
|
|
11638
|
+
const pidFilePath = join2(shipyardHome, "daemon.pid");
|
|
11639
|
+
try {
|
|
11640
|
+
const existing = await readFile2(pidFilePath, "utf-8");
|
|
11641
|
+
const pid = Number.parseInt(existing.trim(), 10);
|
|
11642
|
+
if (!Number.isNaN(pid) && isProcessAlive(pid)) {
|
|
11643
|
+
logger.error(
|
|
11644
|
+
{ pid, pidFile: pidFilePath },
|
|
11645
|
+
"Another daemon is already running. Stop it first or remove the stale PID file."
|
|
11646
|
+
);
|
|
11647
|
+
process.exit(1);
|
|
11648
|
+
}
|
|
11649
|
+
logger.info({ stalePid: pid, pidFile: pidFilePath }, "Removing stale PID file");
|
|
11650
|
+
} catch (err) {
|
|
11651
|
+
if (!isEnoent(err)) throw err;
|
|
11652
|
+
}
|
|
11653
|
+
await writeFile2(pidFilePath, String(process.pid), { mode: 420 });
|
|
11654
|
+
this.#pidFilePath = pidFilePath;
|
|
11655
|
+
logger.info({ pid: process.pid, pidFile: pidFilePath }, "PID file acquired");
|
|
11656
|
+
}
|
|
11657
|
+
#removePidFileSync() {
|
|
11658
|
+
if (!this.#pidFilePath) return;
|
|
11659
|
+
try {
|
|
11660
|
+
unlinkSync(this.#pidFilePath);
|
|
11661
|
+
} catch {
|
|
11662
|
+
}
|
|
11663
|
+
this.#pidFilePath = null;
|
|
11664
|
+
}
|
|
11665
|
+
async #removePidFile() {
|
|
11666
|
+
if (!this.#pidFilePath) return;
|
|
11667
|
+
try {
|
|
11668
|
+
await unlink2(this.#pidFilePath);
|
|
11669
|
+
} catch {
|
|
11670
|
+
}
|
|
11671
|
+
this.#pidFilePath = null;
|
|
11477
11672
|
}
|
|
11478
11673
|
/**
|
|
11479
11674
|
* Create an AbortController tracked by this manager.
|
|
@@ -11499,17 +11694,35 @@ var LifecycleManager = class {
|
|
|
11499
11694
|
if (this.#isShuttingDown) return;
|
|
11500
11695
|
this.#isShuttingDown = true;
|
|
11501
11696
|
logger.info({ signal }, "Shutdown signal received");
|
|
11697
|
+
const HARD_KILL_MS = 15e3;
|
|
11698
|
+
const forceExit = setTimeout(() => {
|
|
11699
|
+
logger.error("Graceful shutdown timed out, forcing exit");
|
|
11700
|
+
process.exit(1);
|
|
11701
|
+
}, HARD_KILL_MS);
|
|
11702
|
+
forceExit.unref();
|
|
11502
11703
|
for (const controller of this.#abortControllers) {
|
|
11503
11704
|
controller.abort();
|
|
11504
11705
|
}
|
|
11505
11706
|
this.#abortControllers.clear();
|
|
11707
|
+
const CALLBACK_TIMEOUT_MS = 5e3;
|
|
11506
11708
|
for (const callback of this.#shutdownCallbacks) {
|
|
11709
|
+
let timeoutId;
|
|
11507
11710
|
try {
|
|
11508
|
-
await
|
|
11711
|
+
await Promise.race([
|
|
11712
|
+
callback(),
|
|
11713
|
+
new Promise((_, reject) => {
|
|
11714
|
+
timeoutId = setTimeout(
|
|
11715
|
+
() => reject(new Error("Shutdown callback timed out")),
|
|
11716
|
+
CALLBACK_TIMEOUT_MS
|
|
11717
|
+
);
|
|
11718
|
+
})
|
|
11719
|
+
]).finally(() => clearTimeout(timeoutId));
|
|
11509
11720
|
} catch (error) {
|
|
11510
11721
|
logger.error({ error }, "Error during shutdown callback");
|
|
11511
11722
|
}
|
|
11512
11723
|
}
|
|
11724
|
+
await this.#removePidFile();
|
|
11725
|
+
clearTimeout(forceExit);
|
|
11513
11726
|
logger.info("Shutdown complete");
|
|
11514
11727
|
await new Promise((resolve3) => setTimeout(resolve3, 100));
|
|
11515
11728
|
process.exit(0);
|
|
@@ -11517,9 +11730,11 @@ var LifecycleManager = class {
|
|
|
11517
11730
|
};
|
|
11518
11731
|
|
|
11519
11732
|
// src/serve.ts
|
|
11520
|
-
import {
|
|
11733
|
+
import { spawn as spawn3 } from "child_process";
|
|
11734
|
+
import { mkdir as mkdir3 } from "fs/promises";
|
|
11521
11735
|
import { homedir as homedir2, hostname as hostname2 } from "os";
|
|
11522
11736
|
import { resolve } from "path";
|
|
11737
|
+
import { query as query2 } from "@anthropic-ai/claude-agent-sdk";
|
|
11523
11738
|
|
|
11524
11739
|
// ../../node_modules/.pnpm/@loro-extended+adapter-webrtc@5.4.2_loro-crdt@1.10.5/node_modules/@loro-extended/adapter-webrtc/dist/index.js
|
|
11525
11740
|
var WebRtcDataChannelAdapter = class extends Adapter {
|
|
@@ -11755,15 +11970,307 @@ var WebRtcDataChannelAdapter = class extends Adapter {
|
|
|
11755
11970
|
}
|
|
11756
11971
|
};
|
|
11757
11972
|
|
|
11973
|
+
// ../../packages/session/src/client.ts
|
|
11974
|
+
var SignalingClientError = class extends Error {
|
|
11975
|
+
constructor(code, message, status, details) {
|
|
11976
|
+
super(message);
|
|
11977
|
+
this.code = code;
|
|
11978
|
+
this.status = status;
|
|
11979
|
+
this.details = details;
|
|
11980
|
+
this.name = "SignalingClientError";
|
|
11981
|
+
}
|
|
11982
|
+
};
|
|
11983
|
+
var SignalingClientValidationError = class extends Error {
|
|
11984
|
+
constructor(message, response) {
|
|
11985
|
+
super(message);
|
|
11986
|
+
this.response = response;
|
|
11987
|
+
this.name = "SignalingClientValidationError";
|
|
11988
|
+
}
|
|
11989
|
+
};
|
|
11990
|
+
var SignalingClient = class {
|
|
11991
|
+
/**
|
|
11992
|
+
* Creates a new SignalingClient instance.
|
|
11993
|
+
*
|
|
11994
|
+
* @param baseUrl - Base URL of the signaling server (e.g., 'https://signaling.shipyard.dev')
|
|
11995
|
+
* @param options - Optional configuration
|
|
11996
|
+
*/
|
|
11997
|
+
constructor(baseUrl, options = {}) {
|
|
11998
|
+
this.baseUrl = baseUrl;
|
|
11999
|
+
this.fetch = options.fetch ?? globalThis.fetch.bind(globalThis);
|
|
12000
|
+
this.defaultHeaders = options.defaultHeaders ?? {};
|
|
12001
|
+
}
|
|
12002
|
+
fetch;
|
|
12003
|
+
defaultHeaders;
|
|
12004
|
+
/**
|
|
12005
|
+
* Check server health.
|
|
12006
|
+
*
|
|
12007
|
+
* GET /health
|
|
12008
|
+
*
|
|
12009
|
+
* @returns Health status with service name and environment
|
|
12010
|
+
* @throws {SignalingClientError} If the server returns an error
|
|
12011
|
+
* @throws {SignalingClientValidationError} If the response doesn't match expected schema
|
|
12012
|
+
*/
|
|
12013
|
+
async health() {
|
|
12014
|
+
const response = await this.fetch(`${this.baseUrl}${ROUTES.HEALTH}`, {
|
|
12015
|
+
method: "GET",
|
|
12016
|
+
headers: this.defaultHeaders
|
|
12017
|
+
});
|
|
12018
|
+
const data = await response.json();
|
|
12019
|
+
if (!response.ok) {
|
|
12020
|
+
this.throwError(response.status, data);
|
|
12021
|
+
}
|
|
12022
|
+
const result = HealthResponseSchema.safeParse(data);
|
|
12023
|
+
if (!result.success) {
|
|
12024
|
+
throw new SignalingClientValidationError(
|
|
12025
|
+
`Invalid health response: ${result.error.message}`,
|
|
12026
|
+
data
|
|
12027
|
+
);
|
|
12028
|
+
}
|
|
12029
|
+
return result.data;
|
|
12030
|
+
}
|
|
12031
|
+
/**
|
|
12032
|
+
* Verify a JWT against the database.
|
|
12033
|
+
*
|
|
12034
|
+
* GET /auth/verify
|
|
12035
|
+
*
|
|
12036
|
+
* Checks both cryptographic validity and that the user still exists in the database.
|
|
12037
|
+
* Does NOT throw on 401 — returns `{ valid: false, reason }` instead.
|
|
12038
|
+
*
|
|
12039
|
+
* @param token - Shipyard JWT to verify
|
|
12040
|
+
* @returns Verification result with user info (if valid) or failure reason
|
|
12041
|
+
* @throws {SignalingClientError} If the server returns a non-401 error
|
|
12042
|
+
* @throws {SignalingClientValidationError} If the response doesn't match expected schema
|
|
12043
|
+
*/
|
|
12044
|
+
async verify(token) {
|
|
12045
|
+
const response = await this.fetch(`${this.baseUrl}${ROUTES.AUTH_VERIFY}`, {
|
|
12046
|
+
method: "GET",
|
|
12047
|
+
headers: {
|
|
12048
|
+
...this.defaultHeaders,
|
|
12049
|
+
Authorization: `Bearer ${token}`
|
|
12050
|
+
}
|
|
12051
|
+
});
|
|
12052
|
+
const data = await response.json();
|
|
12053
|
+
if (!response.ok && response.status !== 401) {
|
|
12054
|
+
this.throwError(response.status, data);
|
|
12055
|
+
}
|
|
12056
|
+
const result = AuthVerifyResponseSchema.safeParse(data);
|
|
12057
|
+
if (!result.success) {
|
|
12058
|
+
throw new SignalingClientValidationError(
|
|
12059
|
+
`Invalid verify response: ${result.error.message}`,
|
|
12060
|
+
data
|
|
12061
|
+
);
|
|
12062
|
+
}
|
|
12063
|
+
return result.data;
|
|
12064
|
+
}
|
|
12065
|
+
/**
|
|
12066
|
+
* Exchange a GitHub OAuth code for a Shipyard JWT.
|
|
12067
|
+
*
|
|
12068
|
+
* POST /auth/github/callback
|
|
12069
|
+
*
|
|
12070
|
+
* @param request - OAuth code and redirect URI
|
|
12071
|
+
* @returns JWT token and user info
|
|
12072
|
+
* @throws {SignalingClientError} If authentication fails
|
|
12073
|
+
* @throws {SignalingClientValidationError} If request/response doesn't match expected schema
|
|
12074
|
+
*/
|
|
12075
|
+
async authGitHubCallback(request) {
|
|
12076
|
+
const requestResult = AuthGitHubCallbackRequestSchema.safeParse(request);
|
|
12077
|
+
if (!requestResult.success) {
|
|
12078
|
+
throw new SignalingClientValidationError(
|
|
12079
|
+
`Invalid request: ${requestResult.error.message}`,
|
|
12080
|
+
request
|
|
12081
|
+
);
|
|
12082
|
+
}
|
|
12083
|
+
const response = await this.fetch(`${this.baseUrl}${ROUTES.AUTH_GITHUB_CALLBACK}`, {
|
|
12084
|
+
method: "POST",
|
|
12085
|
+
headers: {
|
|
12086
|
+
...this.defaultHeaders,
|
|
12087
|
+
"Content-Type": "application/json"
|
|
12088
|
+
},
|
|
12089
|
+
body: JSON.stringify(requestResult.data)
|
|
12090
|
+
});
|
|
12091
|
+
const data = await response.json();
|
|
12092
|
+
if (!response.ok) {
|
|
12093
|
+
this.throwError(response.status, data);
|
|
12094
|
+
}
|
|
12095
|
+
const result = AuthGitHubCallbackResponseSchema.safeParse(data);
|
|
12096
|
+
if (!result.success) {
|
|
12097
|
+
throw new SignalingClientValidationError(
|
|
12098
|
+
`Invalid auth response: ${result.error.message}`,
|
|
12099
|
+
data
|
|
12100
|
+
);
|
|
12101
|
+
}
|
|
12102
|
+
return result.data;
|
|
12103
|
+
}
|
|
12104
|
+
/**
|
|
12105
|
+
* Create a new collaboration room.
|
|
12106
|
+
*
|
|
12107
|
+
* POST /collab/create
|
|
12108
|
+
*
|
|
12109
|
+
* Requires authentication - use `withAuth()` to create an authenticated client.
|
|
12110
|
+
*
|
|
12111
|
+
* @param request - Task ID and optional expiration
|
|
12112
|
+
* @param token - Shipyard JWT for authentication
|
|
12113
|
+
* @returns Pre-signed WebSocket URL and room info
|
|
12114
|
+
* @throws {SignalingClientError} If authorization fails or request is invalid
|
|
12115
|
+
* @throws {SignalingClientValidationError} If request/response doesn't match expected schema
|
|
12116
|
+
*/
|
|
12117
|
+
async createCollab(request, token) {
|
|
12118
|
+
const requestResult = CollabCreateRequestSchema.safeParse(request);
|
|
12119
|
+
if (!requestResult.success) {
|
|
12120
|
+
throw new SignalingClientValidationError(
|
|
12121
|
+
`Invalid request: ${requestResult.error.message}`,
|
|
12122
|
+
request
|
|
12123
|
+
);
|
|
12124
|
+
}
|
|
12125
|
+
const response = await this.fetch(`${this.baseUrl}${ROUTES.COLLAB_CREATE}`, {
|
|
12126
|
+
method: "POST",
|
|
12127
|
+
headers: {
|
|
12128
|
+
...this.defaultHeaders,
|
|
12129
|
+
"Content-Type": "application/json",
|
|
12130
|
+
Authorization: `Bearer ${token}`
|
|
12131
|
+
},
|
|
12132
|
+
body: JSON.stringify(requestResult.data)
|
|
12133
|
+
});
|
|
12134
|
+
const data = await response.json();
|
|
12135
|
+
if (!response.ok) {
|
|
12136
|
+
this.throwError(response.status, data);
|
|
12137
|
+
}
|
|
12138
|
+
const result = CollabCreateResponseSchema.safeParse(data);
|
|
12139
|
+
if (!result.success) {
|
|
12140
|
+
throw new SignalingClientValidationError(
|
|
12141
|
+
`Invalid collab create response: ${result.error.message}`,
|
|
12142
|
+
data
|
|
12143
|
+
);
|
|
12144
|
+
}
|
|
12145
|
+
return result.data;
|
|
12146
|
+
}
|
|
12147
|
+
/**
|
|
12148
|
+
* Build a WebSocket URL for connecting to a personal room.
|
|
12149
|
+
*
|
|
12150
|
+
* Note: This method only builds the URL. Use a WebSocket library to connect.
|
|
12151
|
+
*
|
|
12152
|
+
* @param userId - The user ID to connect as
|
|
12153
|
+
* @param token - Shipyard JWT for authentication
|
|
12154
|
+
* @returns WebSocket URL for connecting to the personal room
|
|
12155
|
+
*/
|
|
12156
|
+
buildPersonalRoomUrl(userId, token) {
|
|
12157
|
+
const wsBaseUrl = this.baseUrl.replace(/^http:/, "ws:").replace(/^https:/, "wss:");
|
|
12158
|
+
return `${wsBaseUrl}/personal/${encodeURIComponent(userId)}?token=${encodeURIComponent(token)}`;
|
|
12159
|
+
}
|
|
12160
|
+
/**
|
|
12161
|
+
* Build a WebSocket URL for connecting to a collaboration room.
|
|
12162
|
+
*
|
|
12163
|
+
* Note: This method only builds the URL. Use a WebSocket library to connect.
|
|
12164
|
+
* The token should be extracted from the pre-signed URL returned by createCollab().
|
|
12165
|
+
*
|
|
12166
|
+
* @param roomId - The room ID to connect to
|
|
12167
|
+
* @param presignedToken - The pre-signed URL token from createCollab()
|
|
12168
|
+
* @param userToken - Optional user JWT for authenticated users
|
|
12169
|
+
* @returns WebSocket URL for connecting to the collab room
|
|
12170
|
+
*/
|
|
12171
|
+
buildCollabRoomUrl(roomId, presignedToken, userToken) {
|
|
12172
|
+
const wsBaseUrl = this.baseUrl.replace(/^http:/, "ws:").replace(/^https:/, "wss:");
|
|
12173
|
+
let url = `${wsBaseUrl}/collab/${encodeURIComponent(roomId)}?token=${encodeURIComponent(presignedToken)}`;
|
|
12174
|
+
if (userToken) {
|
|
12175
|
+
url += `&userToken=${encodeURIComponent(userToken)}`;
|
|
12176
|
+
}
|
|
12177
|
+
return url;
|
|
12178
|
+
}
|
|
12179
|
+
/**
|
|
12180
|
+
* Extract the pre-signed token from a collab URL.
|
|
12181
|
+
*
|
|
12182
|
+
* @param presignedUrl - The full pre-signed URL from createCollab()
|
|
12183
|
+
* @returns The token query parameter value, or null if not found
|
|
12184
|
+
*/
|
|
12185
|
+
extractTokenFromPresignedUrl(presignedUrl) {
|
|
12186
|
+
try {
|
|
12187
|
+
const url = new URL(presignedUrl);
|
|
12188
|
+
return url.searchParams.get("token");
|
|
12189
|
+
} catch {
|
|
12190
|
+
return null;
|
|
12191
|
+
}
|
|
12192
|
+
}
|
|
12193
|
+
/**
|
|
12194
|
+
* Create an authenticated client that automatically includes the token.
|
|
12195
|
+
*
|
|
12196
|
+
* @param token - Shipyard JWT to include in all requests
|
|
12197
|
+
* @returns A new SignalingClient configured with the auth token
|
|
12198
|
+
*/
|
|
12199
|
+
withAuth(token) {
|
|
12200
|
+
return new AuthenticatedSignalingClient(this.baseUrl, token, {
|
|
12201
|
+
fetch: this.fetch,
|
|
12202
|
+
defaultHeaders: this.defaultHeaders
|
|
12203
|
+
});
|
|
12204
|
+
}
|
|
12205
|
+
/**
|
|
12206
|
+
* Parse error response and throw appropriate error.
|
|
12207
|
+
*/
|
|
12208
|
+
throwError(status, data) {
|
|
12209
|
+
const validationResult = ValidationErrorResponseSchema.safeParse(data);
|
|
12210
|
+
if (validationResult.success) {
|
|
12211
|
+
throw new SignalingClientError(
|
|
12212
|
+
validationResult.data.error,
|
|
12213
|
+
validationResult.data.message,
|
|
12214
|
+
status,
|
|
12215
|
+
validationResult.data.details
|
|
12216
|
+
);
|
|
12217
|
+
}
|
|
12218
|
+
const errorResult = ErrorResponseSchema.safeParse(data);
|
|
12219
|
+
if (errorResult.success) {
|
|
12220
|
+
throw new SignalingClientError(errorResult.data.error, errorResult.data.message, status);
|
|
12221
|
+
}
|
|
12222
|
+
throw new SignalingClientError("unknown_error", `Server returned status ${status}`, status);
|
|
12223
|
+
}
|
|
12224
|
+
};
|
|
12225
|
+
var AuthenticatedSignalingClient = class extends SignalingClient {
|
|
12226
|
+
constructor(baseUrl, token, options = {}) {
|
|
12227
|
+
super(baseUrl, options);
|
|
12228
|
+
this.token = token;
|
|
12229
|
+
}
|
|
12230
|
+
async createCollab(request, _token) {
|
|
12231
|
+
return super.createCollab(request, this.token);
|
|
12232
|
+
}
|
|
12233
|
+
buildPersonalRoomUrl(userId, _token) {
|
|
12234
|
+
return super.buildPersonalRoomUrl(userId, this.token);
|
|
12235
|
+
}
|
|
12236
|
+
/**
|
|
12237
|
+
* Build a WebSocket URL for connecting to a collaboration room with user authentication.
|
|
12238
|
+
*
|
|
12239
|
+
* @param roomId - The room ID to connect to
|
|
12240
|
+
* @param presignedToken - The pre-signed URL token from createCollab()
|
|
12241
|
+
* @returns WebSocket URL for connecting to the collab room
|
|
12242
|
+
*/
|
|
12243
|
+
buildCollabRoomUrl(roomId, presignedToken) {
|
|
12244
|
+
return super.buildCollabRoomUrl(roomId, presignedToken, this.token);
|
|
12245
|
+
}
|
|
12246
|
+
/**
|
|
12247
|
+
* Verify the stored token against the database.
|
|
12248
|
+
*
|
|
12249
|
+
* Convenience wrapper around verify() using the authenticated client's token.
|
|
12250
|
+
*/
|
|
12251
|
+
async verifyToken() {
|
|
12252
|
+
return this.verify(this.token);
|
|
12253
|
+
}
|
|
12254
|
+
/**
|
|
12255
|
+
* Get the authentication token.
|
|
12256
|
+
*
|
|
12257
|
+
* @returns The JWT token used for authentication
|
|
12258
|
+
*/
|
|
12259
|
+
getToken() {
|
|
12260
|
+
return this.token;
|
|
12261
|
+
}
|
|
12262
|
+
};
|
|
12263
|
+
|
|
11758
12264
|
// src/branch-watcher.ts
|
|
11759
12265
|
import { watch } from "fs";
|
|
11760
|
-
import {
|
|
12266
|
+
import { readFile as readFile3, stat } from "fs/promises";
|
|
12267
|
+
import { isAbsolute, join as join4 } from "path";
|
|
11761
12268
|
|
|
11762
12269
|
// src/capabilities.ts
|
|
11763
12270
|
import { execFile } from "child_process";
|
|
11764
12271
|
import { readdir as readdir2 } from "fs/promises";
|
|
11765
12272
|
import { homedir } from "os";
|
|
11766
|
-
import { basename, join as
|
|
12273
|
+
import { basename, join as join3 } from "path";
|
|
11767
12274
|
var TIMEOUT_MS = 5e3;
|
|
11768
12275
|
function run(command, args, cwd) {
|
|
11769
12276
|
return new Promise((resolve3, reject) => {
|
|
@@ -12033,7 +12540,7 @@ async function findGitRepos(dir, depth = 0) {
|
|
|
12033
12540
|
if (!entry.isDirectory()) continue;
|
|
12034
12541
|
if (entry.name.startsWith(".")) continue;
|
|
12035
12542
|
if (EXCLUDE_DIRS.has(entry.name)) continue;
|
|
12036
|
-
promises.push(findGitRepos(
|
|
12543
|
+
promises.push(findGitRepos(join3(dir, entry.name), depth + 1));
|
|
12037
12544
|
}
|
|
12038
12545
|
const results = await Promise.all(promises);
|
|
12039
12546
|
return results.flat();
|
|
@@ -12064,14 +12571,48 @@ async function detectEnvironments() {
|
|
|
12064
12571
|
const repoInfos = await Promise.all(repoPaths.map(getRepoMetadata));
|
|
12065
12572
|
return repoInfos.filter((info) => info !== null);
|
|
12066
12573
|
}
|
|
12574
|
+
async function detectAnthropicAuth() {
|
|
12575
|
+
if (process.env.ANTHROPIC_API_KEY) {
|
|
12576
|
+
return { status: "authenticated", method: "api-key" };
|
|
12577
|
+
}
|
|
12578
|
+
try {
|
|
12579
|
+
const stdout = await run("claude", ["auth", "status", "--json"], void 0);
|
|
12580
|
+
const parsed = JSON.parse(stdout);
|
|
12581
|
+
if (parsed.loggedIn) {
|
|
12582
|
+
return { status: "authenticated", method: "oauth", email: parsed.email };
|
|
12583
|
+
}
|
|
12584
|
+
return { status: "unauthenticated", method: "none" };
|
|
12585
|
+
} catch {
|
|
12586
|
+
return { status: "unknown", method: "none" };
|
|
12587
|
+
}
|
|
12588
|
+
}
|
|
12067
12589
|
async function detectCapabilities() {
|
|
12068
|
-
const [models, environments] = await Promise.all([
|
|
12590
|
+
const [models, environments, anthropicAuth] = await Promise.all([
|
|
12591
|
+
detectModels(),
|
|
12592
|
+
detectEnvironments(),
|
|
12593
|
+
detectAnthropicAuth()
|
|
12594
|
+
]);
|
|
12069
12595
|
const permissionModes = [...PermissionModeSchema.options];
|
|
12070
|
-
return { models, environments, permissionModes, homeDir: homedir() };
|
|
12596
|
+
return { models, environments, permissionModes, homeDir: homedir(), anthropicAuth };
|
|
12071
12597
|
}
|
|
12072
12598
|
|
|
12073
12599
|
// src/branch-watcher.ts
|
|
12074
12600
|
var DEBOUNCE_MS = 500;
|
|
12601
|
+
async function resolveHeadPath(repoPath) {
|
|
12602
|
+
const gitPath = join4(repoPath, ".git");
|
|
12603
|
+
const gitStat = await stat(gitPath);
|
|
12604
|
+
if (gitStat.isDirectory()) {
|
|
12605
|
+
return join4(gitPath, "HEAD");
|
|
12606
|
+
}
|
|
12607
|
+
const content = await readFile3(gitPath, "utf-8");
|
|
12608
|
+
const match = content.trim().match(/^gitdir:\s*(.+)$/);
|
|
12609
|
+
if (!match?.[1]) {
|
|
12610
|
+
throw new Error(`Invalid .git file at ${gitPath}`);
|
|
12611
|
+
}
|
|
12612
|
+
const gitDir = match[1];
|
|
12613
|
+
const resolvedGitDir = isAbsolute(gitDir) ? gitDir : join4(repoPath, gitDir);
|
|
12614
|
+
return join4(resolvedGitDir, "HEAD");
|
|
12615
|
+
}
|
|
12075
12616
|
function createBranchWatcher(options) {
|
|
12076
12617
|
const log = logger.child({ component: "branch-watcher" });
|
|
12077
12618
|
const watchers = /* @__PURE__ */ new Map();
|
|
@@ -12080,11 +12621,18 @@ function createBranchWatcher(options) {
|
|
|
12080
12621
|
let environments = [...options.environments];
|
|
12081
12622
|
for (const env of environments) {
|
|
12082
12623
|
branches.set(env.path, env.branch);
|
|
12083
|
-
startWatching(env.path);
|
|
12624
|
+
void startWatching(env.path);
|
|
12084
12625
|
}
|
|
12085
12626
|
log.info({ count: environments.length }, "Branch watcher started");
|
|
12086
|
-
function startWatching(repoPath) {
|
|
12087
|
-
|
|
12627
|
+
async function startWatching(repoPath) {
|
|
12628
|
+
let headPath;
|
|
12629
|
+
try {
|
|
12630
|
+
headPath = await resolveHeadPath(repoPath);
|
|
12631
|
+
} catch (err) {
|
|
12632
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
12633
|
+
log.debug({ repoPath, err: msg }, "Failed to resolve HEAD path, skipping watch");
|
|
12634
|
+
return;
|
|
12635
|
+
}
|
|
12088
12636
|
try {
|
|
12089
12637
|
const watcher = watch(headPath, () => {
|
|
12090
12638
|
debouncedCheck(repoPath);
|
|
@@ -12096,7 +12644,7 @@ function createBranchWatcher(options) {
|
|
|
12096
12644
|
watchers.set(repoPath, watcher);
|
|
12097
12645
|
} catch (err) {
|
|
12098
12646
|
const msg = err instanceof Error ? err.message : String(err);
|
|
12099
|
-
log.debug({ repoPath, err: msg }, "Failed to watch
|
|
12647
|
+
log.debug({ repoPath, err: msg }, "Failed to watch HEAD");
|
|
12100
12648
|
}
|
|
12101
12649
|
}
|
|
12102
12650
|
function removeWatcher(repoPath) {
|
|
@@ -12157,10 +12705,57 @@ function createBranchWatcher(options) {
|
|
|
12157
12705
|
}
|
|
12158
12706
|
watchers.clear();
|
|
12159
12707
|
log.info("Branch watcher closed");
|
|
12708
|
+
},
|
|
12709
|
+
addEnvironment(repoPath, branch) {
|
|
12710
|
+
if (watchers.has(repoPath)) {
|
|
12711
|
+
log.debug({ repoPath }, "Already watching, skipping addEnvironment");
|
|
12712
|
+
return;
|
|
12713
|
+
}
|
|
12714
|
+
branches.set(repoPath, branch);
|
|
12715
|
+
environments.push({
|
|
12716
|
+
path: repoPath,
|
|
12717
|
+
name: repoPath.split("/").pop() ?? repoPath,
|
|
12718
|
+
branch,
|
|
12719
|
+
remote: void 0
|
|
12720
|
+
});
|
|
12721
|
+
void startWatching(repoPath);
|
|
12722
|
+
log.info({ repoPath, branch }, "Added environment to branch watcher");
|
|
12160
12723
|
}
|
|
12161
12724
|
};
|
|
12162
12725
|
}
|
|
12163
12726
|
|
|
12727
|
+
// src/crash-recovery.ts
|
|
12728
|
+
function recoverOrphanedTask(taskDoc, log) {
|
|
12729
|
+
const json = taskDoc.toJSON();
|
|
12730
|
+
const { status } = json.meta;
|
|
12731
|
+
if (status !== "working" && status !== "starting" && status !== "input-required") {
|
|
12732
|
+
return false;
|
|
12733
|
+
}
|
|
12734
|
+
const sessions = json.sessions;
|
|
12735
|
+
let lastActiveIdx = -1;
|
|
12736
|
+
for (let i = sessions.length - 1; i >= 0; i--) {
|
|
12737
|
+
const s = sessions[i];
|
|
12738
|
+
if (s?.status === "active" || s?.status === "pending") {
|
|
12739
|
+
lastActiveIdx = i;
|
|
12740
|
+
break;
|
|
12741
|
+
}
|
|
12742
|
+
}
|
|
12743
|
+
change(taskDoc, (draft) => {
|
|
12744
|
+
if (lastActiveIdx >= 0) {
|
|
12745
|
+
const session = draft.sessions.get(lastActiveIdx);
|
|
12746
|
+
if (session) {
|
|
12747
|
+
session.status = "interrupted";
|
|
12748
|
+
session.completedAt = Date.now();
|
|
12749
|
+
session.error = "Daemon process exited unexpectedly";
|
|
12750
|
+
}
|
|
12751
|
+
}
|
|
12752
|
+
draft.meta.status = "failed";
|
|
12753
|
+
draft.meta.updatedAt = Date.now();
|
|
12754
|
+
});
|
|
12755
|
+
log.info({ previousStatus: status }, "Recovered orphaned task after daemon crash");
|
|
12756
|
+
return true;
|
|
12757
|
+
}
|
|
12758
|
+
|
|
12164
12759
|
// src/peer-manager.ts
|
|
12165
12760
|
var ICE_SERVERS = [{ urls: "stun:stun.l.google.com:19302" }];
|
|
12166
12761
|
function machineIdToPeerId(machineId) {
|
|
@@ -12198,9 +12793,14 @@ function createPeerManager(config) {
|
|
|
12198
12793
|
};
|
|
12199
12794
|
pc.ondatachannel = (event) => {
|
|
12200
12795
|
const channel = event.channel;
|
|
12201
|
-
if (channel.label
|
|
12202
|
-
|
|
12203
|
-
|
|
12796
|
+
if (channel.label?.startsWith("terminal-io:")) {
|
|
12797
|
+
const taskId = channel.label.slice("terminal-io:".length);
|
|
12798
|
+
if (!taskId) {
|
|
12799
|
+
logger.warn({ machineId }, "Terminal channel with empty taskId, ignoring");
|
|
12800
|
+
return;
|
|
12801
|
+
}
|
|
12802
|
+
logger.info({ machineId, taskId }, "Terminal data channel received");
|
|
12803
|
+
config.onTerminalChannel?.(machineId, event.channel, taskId);
|
|
12204
12804
|
} else {
|
|
12205
12805
|
logger.info({ machineId }, "Data channel received from browser");
|
|
12206
12806
|
config.webrtcAdapter.attachDataChannel(
|
|
@@ -12258,6 +12858,13 @@ function createPeerManager(config) {
|
|
|
12258
12858
|
return pc;
|
|
12259
12859
|
})();
|
|
12260
12860
|
pendingCreates.set(fromMachineId, promise);
|
|
12861
|
+
const HANDSHAKE_TIMEOUT_MS = 3e4;
|
|
12862
|
+
setTimeout(() => {
|
|
12863
|
+
if (pendingCreates.get(fromMachineId) === promise) {
|
|
12864
|
+
pendingCreates.delete(fromMachineId);
|
|
12865
|
+
logger.warn({ fromMachineId }, "WebRTC handshake timed out");
|
|
12866
|
+
}
|
|
12867
|
+
}, HANDSHAKE_TIMEOUT_MS);
|
|
12261
12868
|
await promise;
|
|
12262
12869
|
},
|
|
12263
12870
|
async handleAnswer(fromMachineId, answer) {
|
|
@@ -12296,73 +12903,381 @@ function createPeerManager(config) {
|
|
|
12296
12903
|
pc.close();
|
|
12297
12904
|
}
|
|
12298
12905
|
peers.clear();
|
|
12906
|
+
pendingCreates.clear();
|
|
12299
12907
|
}
|
|
12300
12908
|
};
|
|
12301
12909
|
}
|
|
12302
12910
|
|
|
12303
|
-
// src/
|
|
12304
|
-
|
|
12305
|
-
|
|
12306
|
-
|
|
12307
|
-
|
|
12308
|
-
|
|
12309
|
-
|
|
12310
|
-
|
|
12311
|
-
|
|
12312
|
-
|
|
12313
|
-
|
|
12314
|
-
|
|
12315
|
-
|
|
12316
|
-
|
|
12317
|
-
|
|
12318
|
-
|
|
12319
|
-
|
|
12320
|
-
|
|
12321
|
-
|
|
12322
|
-
|
|
12323
|
-
|
|
12324
|
-
|
|
12325
|
-
|
|
12326
|
-
|
|
12327
|
-
|
|
12328
|
-
|
|
12329
|
-
|
|
12330
|
-
|
|
12331
|
-
|
|
12332
|
-
|
|
12333
|
-
|
|
12334
|
-
|
|
12335
|
-
cwd: options.cwd,
|
|
12336
|
-
env
|
|
12337
|
-
});
|
|
12338
|
-
} catch (err) {
|
|
12339
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
12340
|
-
log.error({ err: msg, shell }, "Failed to spawn PTY");
|
|
12341
|
-
throw new Error(`Failed to spawn PTY: ${msg}`);
|
|
12911
|
+
// src/plan-editor/format-plan-feedback.ts
|
|
12912
|
+
function formatPlanFeedbackForClaudeCode(original, edited, comments, generalFeedback) {
|
|
12913
|
+
const sections = [];
|
|
12914
|
+
if (generalFeedback) {
|
|
12915
|
+
sections.push("## General Feedback\n");
|
|
12916
|
+
sections.push(generalFeedback);
|
|
12917
|
+
sections.push("");
|
|
12918
|
+
}
|
|
12919
|
+
if (comments.length > 0) {
|
|
12920
|
+
appendCommentSection(sections, comments, edited);
|
|
12921
|
+
}
|
|
12922
|
+
if (original !== edited) {
|
|
12923
|
+
sections.push("## Edits Made\n");
|
|
12924
|
+
sections.push("The user edited the following sections of the plan:\n");
|
|
12925
|
+
sections.push("```diff");
|
|
12926
|
+
sections.push(computeLineDiff(original, edited));
|
|
12927
|
+
sections.push("```");
|
|
12928
|
+
sections.push("");
|
|
12929
|
+
}
|
|
12930
|
+
return sections.join("\n").trim();
|
|
12931
|
+
}
|
|
12932
|
+
function appendCommentSection(sections, comments, edited) {
|
|
12933
|
+
sections.push("## Inline Comments\n");
|
|
12934
|
+
sections.push("The user left the following comments on specific parts of the plan:\n");
|
|
12935
|
+
const editedLines = edited.split("\n");
|
|
12936
|
+
const sorted = [...comments].sort((a, b) => a.from - b.from);
|
|
12937
|
+
for (const comment of sorted) {
|
|
12938
|
+
const nearbyLine = findNearestLine(editedLines, comment.from);
|
|
12939
|
+
if (nearbyLine) {
|
|
12940
|
+
sections.push(`> Near "${truncate(nearbyLine, 60)}": ${comment.body}`);
|
|
12941
|
+
} else {
|
|
12942
|
+
sections.push(`> ${comment.body}`);
|
|
12342
12943
|
}
|
|
12343
|
-
isAlive = true;
|
|
12344
|
-
process2.onData((data) => {
|
|
12345
|
-
for (const cb of dataCallbacks) {
|
|
12346
|
-
cb(data);
|
|
12347
|
-
}
|
|
12348
|
-
});
|
|
12349
|
-
process2.onExit(({ exitCode, signal }) => {
|
|
12350
|
-
log.info({ exitCode, signal, pid: process2?.pid }, "PTY exited");
|
|
12351
|
-
isAlive = false;
|
|
12352
|
-
clearKillTimer();
|
|
12353
|
-
for (const cb of exitCallbacks) {
|
|
12354
|
-
cb(exitCode, signal);
|
|
12355
|
-
}
|
|
12356
|
-
});
|
|
12357
|
-
log.info({ pid: process2.pid }, "PTY spawned");
|
|
12358
12944
|
}
|
|
12359
|
-
|
|
12360
|
-
|
|
12361
|
-
|
|
12945
|
+
sections.push("");
|
|
12946
|
+
}
|
|
12947
|
+
function findNearestLine(lines, charOffset) {
|
|
12948
|
+
let pos = 0;
|
|
12949
|
+
for (const line of lines) {
|
|
12950
|
+
if (pos + line.length >= charOffset) {
|
|
12951
|
+
const trimmed = line.trim();
|
|
12952
|
+
if (trimmed.length > 0) return trimmed;
|
|
12362
12953
|
}
|
|
12363
|
-
|
|
12954
|
+
pos += line.length + 1;
|
|
12364
12955
|
}
|
|
12365
|
-
|
|
12956
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
12957
|
+
const trimmed = lines[i]?.trim();
|
|
12958
|
+
if (trimmed && trimmed.length > 0) return trimmed;
|
|
12959
|
+
}
|
|
12960
|
+
return null;
|
|
12961
|
+
}
|
|
12962
|
+
function truncate(text, maxLen) {
|
|
12963
|
+
if (text.length <= maxLen) return text;
|
|
12964
|
+
return `${text.slice(0, maxLen - 3)}...`;
|
|
12965
|
+
}
|
|
12966
|
+
function computeLineDiff(original, edited) {
|
|
12967
|
+
const oldLines = original.split("\n");
|
|
12968
|
+
const newLines = edited.split("\n");
|
|
12969
|
+
const result = [];
|
|
12970
|
+
let oi = 0;
|
|
12971
|
+
let ni = 0;
|
|
12972
|
+
while (oi < oldLines.length && ni < newLines.length) {
|
|
12973
|
+
if (oldLines[oi] === newLines[ni]) {
|
|
12974
|
+
result.push(` ${oldLines[oi]}`);
|
|
12975
|
+
oi++;
|
|
12976
|
+
ni++;
|
|
12977
|
+
} else {
|
|
12978
|
+
const consumed = consumeMismatch(oldLines, newLines, oi, ni, result);
|
|
12979
|
+
oi = consumed.oi;
|
|
12980
|
+
ni = consumed.ni;
|
|
12981
|
+
}
|
|
12982
|
+
}
|
|
12983
|
+
appendRemainder(result, oldLines, oi, "-");
|
|
12984
|
+
appendRemainder(result, newLines, ni, "+");
|
|
12985
|
+
return result.join("\n");
|
|
12986
|
+
}
|
|
12987
|
+
function consumeMismatch(oldLines, newLines, oi, ni, result) {
|
|
12988
|
+
const newInOld = findNext(oldLines, newLines[ni], oi);
|
|
12989
|
+
const oldInNew = findNext(newLines, oldLines[oi], ni);
|
|
12990
|
+
if (newInOld !== null && (oldInNew === null || newInOld - oi <= oldInNew - ni)) {
|
|
12991
|
+
while (oi < newInOld) {
|
|
12992
|
+
result.push(`-${oldLines[oi]}`);
|
|
12993
|
+
oi++;
|
|
12994
|
+
}
|
|
12995
|
+
} else if (oldInNew !== null) {
|
|
12996
|
+
while (ni < oldInNew) {
|
|
12997
|
+
result.push(`+${newLines[ni]}`);
|
|
12998
|
+
ni++;
|
|
12999
|
+
}
|
|
13000
|
+
} else {
|
|
13001
|
+
result.push(`-${oldLines[oi]}`);
|
|
13002
|
+
result.push(`+${newLines[ni]}`);
|
|
13003
|
+
oi++;
|
|
13004
|
+
ni++;
|
|
13005
|
+
}
|
|
13006
|
+
return { oi, ni };
|
|
13007
|
+
}
|
|
13008
|
+
function appendRemainder(result, lines, start, prefix) {
|
|
13009
|
+
for (let i = start; i < lines.length; i++) {
|
|
13010
|
+
result.push(`${prefix}${lines[i]}`);
|
|
13011
|
+
}
|
|
13012
|
+
}
|
|
13013
|
+
function findNext(lines, target, from) {
|
|
13014
|
+
if (target === void 0) return null;
|
|
13015
|
+
const lookAhead = 5;
|
|
13016
|
+
for (let i = from; i < Math.min(from + lookAhead, lines.length); i++) {
|
|
13017
|
+
if (lines[i] === target) return i;
|
|
13018
|
+
}
|
|
13019
|
+
return null;
|
|
13020
|
+
}
|
|
13021
|
+
|
|
13022
|
+
// src/plan-editor/init-plan-editor-doc.ts
|
|
13023
|
+
import { EditorState } from "@tiptap/pm/state";
|
|
13024
|
+
import { LoroMap as LoroMap3 } from "loro-crdt";
|
|
13025
|
+
import { updateLoroToPmState } from "loro-prosemirror";
|
|
13026
|
+
|
|
13027
|
+
// src/plan-editor/schema.ts
|
|
13028
|
+
import { getSchema, Mark } from "@tiptap/core";
|
|
13029
|
+
import StarterKit from "@tiptap/starter-kit";
|
|
13030
|
+
import MarkdownIt from "markdown-it";
|
|
13031
|
+
import { MarkdownParser, MarkdownSerializer } from "prosemirror-markdown";
|
|
13032
|
+
var CommentMark = Mark.create({
|
|
13033
|
+
name: "comment",
|
|
13034
|
+
addAttributes() {
|
|
13035
|
+
return { commentId: { default: null } };
|
|
13036
|
+
}
|
|
13037
|
+
});
|
|
13038
|
+
var planEditorSchema = getSchema([StarterKit.configure({ undoRedo: false }), CommentMark]);
|
|
13039
|
+
var configuredDocs = /* @__PURE__ */ new WeakSet();
|
|
13040
|
+
function configurePlanEditorTextStyles(loroDoc) {
|
|
13041
|
+
if (configuredDocs.has(loroDoc)) return;
|
|
13042
|
+
configuredDocs.add(loroDoc);
|
|
13043
|
+
const config = {};
|
|
13044
|
+
for (const [name, markType] of Object.entries(planEditorSchema.marks)) {
|
|
13045
|
+
const expand = markType.spec.inclusive !== false ? "after" : "none";
|
|
13046
|
+
config[name] = { expand };
|
|
13047
|
+
}
|
|
13048
|
+
loroDoc.configTextStyle(config);
|
|
13049
|
+
}
|
|
13050
|
+
var planEditorParser = new MarkdownParser(
|
|
13051
|
+
planEditorSchema,
|
|
13052
|
+
MarkdownIt("commonmark", { html: false }).enable("strikethrough").disable("image"),
|
|
13053
|
+
{
|
|
13054
|
+
blockquote: { block: "blockquote" },
|
|
13055
|
+
paragraph: { block: "paragraph" },
|
|
13056
|
+
list_item: { block: "listItem" },
|
|
13057
|
+
bullet_list: { block: "bulletList" },
|
|
13058
|
+
ordered_list: {
|
|
13059
|
+
block: "orderedList",
|
|
13060
|
+
getAttrs: (tok) => ({ start: Number(tok.attrGet("start") ?? 1) })
|
|
13061
|
+
},
|
|
13062
|
+
heading: {
|
|
13063
|
+
block: "heading",
|
|
13064
|
+
getAttrs: (tok) => ({ level: Number(tok.tag.slice(1)) })
|
|
13065
|
+
},
|
|
13066
|
+
code_block: { block: "codeBlock", noCloseToken: true },
|
|
13067
|
+
fence: {
|
|
13068
|
+
block: "codeBlock",
|
|
13069
|
+
getAttrs: (tok) => ({ language: tok.info || null }),
|
|
13070
|
+
noCloseToken: true
|
|
13071
|
+
},
|
|
13072
|
+
hr: { node: "horizontalRule" },
|
|
13073
|
+
hardbreak: { node: "hardBreak" },
|
|
13074
|
+
link: {
|
|
13075
|
+
mark: "link",
|
|
13076
|
+
getAttrs: (tok) => ({ href: tok.attrGet("href"), target: tok.attrGet("target") })
|
|
13077
|
+
},
|
|
13078
|
+
em: { mark: "italic" },
|
|
13079
|
+
strong: { mark: "bold" },
|
|
13080
|
+
code_inline: { mark: "code", noCloseToken: true },
|
|
13081
|
+
s: { mark: "strike" }
|
|
13082
|
+
}
|
|
13083
|
+
);
|
|
13084
|
+
function backticksFor(node, side) {
|
|
13085
|
+
const ticks = /`+/g;
|
|
13086
|
+
let len = 0;
|
|
13087
|
+
if (node.isText && node.text) {
|
|
13088
|
+
for (const match of node.text.matchAll(ticks)) {
|
|
13089
|
+
len = Math.max(len, match[0].length);
|
|
13090
|
+
}
|
|
13091
|
+
}
|
|
13092
|
+
let result = len > 0 && side > 0 ? " `" : "`";
|
|
13093
|
+
for (let i = 0; i < len; i++) result += "`";
|
|
13094
|
+
if (len > 0 && side < 0) result += " ";
|
|
13095
|
+
return result;
|
|
13096
|
+
}
|
|
13097
|
+
var planEditorSerializer = new MarkdownSerializer(
|
|
13098
|
+
{
|
|
13099
|
+
blockquote(state, node) {
|
|
13100
|
+
state.wrapBlock("> ", null, node, () => state.renderContent(node));
|
|
13101
|
+
},
|
|
13102
|
+
codeBlock(state, node) {
|
|
13103
|
+
const info = String(node.attrs.language ?? "");
|
|
13104
|
+
state.write(`\`\`\`${info}
|
|
13105
|
+
`);
|
|
13106
|
+
state.text(node.textContent, false);
|
|
13107
|
+
state.ensureNewLine();
|
|
13108
|
+
state.write("```");
|
|
13109
|
+
state.closeBlock(node);
|
|
13110
|
+
},
|
|
13111
|
+
heading(state, node) {
|
|
13112
|
+
state.write(`${state.repeat("#", Number(node.attrs.level))} `);
|
|
13113
|
+
state.renderInline(node);
|
|
13114
|
+
state.closeBlock(node);
|
|
13115
|
+
},
|
|
13116
|
+
horizontalRule(state, node) {
|
|
13117
|
+
state.write("---");
|
|
13118
|
+
state.closeBlock(node);
|
|
13119
|
+
},
|
|
13120
|
+
bulletList(state, node) {
|
|
13121
|
+
state.renderList(node, " ", () => "- ");
|
|
13122
|
+
},
|
|
13123
|
+
orderedList(state, node) {
|
|
13124
|
+
const start = Number(node.attrs.start) || 1;
|
|
13125
|
+
const maxW = String(start + node.childCount - 1).length;
|
|
13126
|
+
const space = state.repeat(" ", maxW + 2);
|
|
13127
|
+
state.renderList(node, space, (i) => {
|
|
13128
|
+
const nStr = String(start + i);
|
|
13129
|
+
return `${state.repeat(" ", maxW - nStr.length)}${nStr}. `;
|
|
13130
|
+
});
|
|
13131
|
+
},
|
|
13132
|
+
listItem(state, node) {
|
|
13133
|
+
state.renderContent(node);
|
|
13134
|
+
},
|
|
13135
|
+
paragraph(state, node) {
|
|
13136
|
+
state.renderInline(node);
|
|
13137
|
+
state.closeBlock(node);
|
|
13138
|
+
},
|
|
13139
|
+
hardBreak(state, node, parent, index) {
|
|
13140
|
+
for (let i = index + 1; i < parent.childCount; i++) {
|
|
13141
|
+
if (parent.child(i).type !== node.type) {
|
|
13142
|
+
state.write("\\\n");
|
|
13143
|
+
return;
|
|
13144
|
+
}
|
|
13145
|
+
}
|
|
13146
|
+
},
|
|
13147
|
+
text(state, node) {
|
|
13148
|
+
state.text(node.text ?? "");
|
|
13149
|
+
}
|
|
13150
|
+
},
|
|
13151
|
+
{
|
|
13152
|
+
italic: { open: "*", close: "*", mixable: true, expelEnclosingWhitespace: true },
|
|
13153
|
+
bold: { open: "**", close: "**", mixable: true, expelEnclosingWhitespace: true },
|
|
13154
|
+
strike: { open: "~~", close: "~~", mixable: true, expelEnclosingWhitespace: true },
|
|
13155
|
+
code: {
|
|
13156
|
+
open(_state, _mark, parent, index) {
|
|
13157
|
+
return backticksFor(parent.child(index), -1);
|
|
13158
|
+
},
|
|
13159
|
+
close(_state, _mark, parent, index) {
|
|
13160
|
+
return backticksFor(parent.child(index - 1), 1);
|
|
13161
|
+
},
|
|
13162
|
+
escape: false
|
|
13163
|
+
},
|
|
13164
|
+
link: {
|
|
13165
|
+
open: "[",
|
|
13166
|
+
close(_state, mark) {
|
|
13167
|
+
const href = String(mark.attrs.href ?? "");
|
|
13168
|
+
const title = mark.attrs.title ? ` "${String(mark.attrs.title)}"` : "";
|
|
13169
|
+
return `](${href}${title})`;
|
|
13170
|
+
}
|
|
13171
|
+
},
|
|
13172
|
+
comment: { open: "", close: "" }
|
|
13173
|
+
},
|
|
13174
|
+
{ hardBreakNodeName: "hardBreak", strict: false }
|
|
13175
|
+
);
|
|
13176
|
+
|
|
13177
|
+
// src/plan-editor/init-plan-editor-doc.ts
|
|
13178
|
+
function initPlanEditorDoc(loroDoc, planId, markdown) {
|
|
13179
|
+
try {
|
|
13180
|
+
const pmDoc = planEditorParser.parse(markdown);
|
|
13181
|
+
const planEditorDocs = loroDoc.getMap("planEditorDocs");
|
|
13182
|
+
const planContainer = planEditorDocs.setContainer(planId, new LoroMap3());
|
|
13183
|
+
configurePlanEditorTextStyles(loroDoc);
|
|
13184
|
+
const editorState = EditorState.create({ doc: pmDoc, schema: planEditorSchema });
|
|
13185
|
+
const mapping = /* @__PURE__ */ new Map();
|
|
13186
|
+
updateLoroToPmState(loroDoc, mapping, editorState, planContainer.id);
|
|
13187
|
+
loroDoc.commit();
|
|
13188
|
+
return true;
|
|
13189
|
+
} catch (error) {
|
|
13190
|
+
logger.warn({ planId, error }, "initPlanEditorDoc failed");
|
|
13191
|
+
return false;
|
|
13192
|
+
}
|
|
13193
|
+
}
|
|
13194
|
+
|
|
13195
|
+
// src/plan-editor/serialize-plan-editor-doc.ts
|
|
13196
|
+
import { LoroMap as LoroMap4 } from "loro-crdt";
|
|
13197
|
+
import { createNodeFromLoroObj } from "loro-prosemirror";
|
|
13198
|
+
function tryCreateNode(raw, mapping) {
|
|
13199
|
+
try {
|
|
13200
|
+
return createNodeFromLoroObj(planEditorSchema, raw, mapping);
|
|
13201
|
+
} catch {
|
|
13202
|
+
return null;
|
|
13203
|
+
}
|
|
13204
|
+
}
|
|
13205
|
+
function serializePlanEditorDoc(loroDoc, planId) {
|
|
13206
|
+
const planEditorDocs = loroDoc.getMap("planEditorDocs");
|
|
13207
|
+
const raw = planEditorDocs.get(planId);
|
|
13208
|
+
if (!(raw instanceof LoroMap4)) return "";
|
|
13209
|
+
const mapping = /* @__PURE__ */ new Map();
|
|
13210
|
+
const pmNode = tryCreateNode(raw, mapping);
|
|
13211
|
+
if (!pmNode) return "";
|
|
13212
|
+
if (pmNode.childCount === 0 || pmNode.childCount === 1 && pmNode.firstChild?.textContent === "") {
|
|
13213
|
+
return "";
|
|
13214
|
+
}
|
|
13215
|
+
return planEditorSerializer.serialize(pmNode, { tightLists: true });
|
|
13216
|
+
}
|
|
13217
|
+
|
|
13218
|
+
// src/pty-manager.ts
|
|
13219
|
+
import * as pty from "node-pty";
|
|
13220
|
+
var KILL_TIMEOUT_MS = 5e3;
|
|
13221
|
+
var DEFAULT_COLS = 80;
|
|
13222
|
+
var DEFAULT_ROWS = 24;
|
|
13223
|
+
function createPtyManager() {
|
|
13224
|
+
const log = createChildLogger({ mode: "pty" });
|
|
13225
|
+
let process2 = null;
|
|
13226
|
+
let isAlive = false;
|
|
13227
|
+
let killTimer = null;
|
|
13228
|
+
const dataCallbacks = [];
|
|
13229
|
+
const exitCallbacks = [];
|
|
13230
|
+
function getDefaultShell() {
|
|
13231
|
+
return globalThis.process.env.SHELL ?? "/bin/zsh";
|
|
13232
|
+
}
|
|
13233
|
+
function spawn4(options) {
|
|
13234
|
+
if (isAlive) {
|
|
13235
|
+
throw new Error("PTY already spawned. Call kill() or dispose() first.");
|
|
13236
|
+
}
|
|
13237
|
+
const shell = options.shell ?? getDefaultShell();
|
|
13238
|
+
const cols = options.cols ?? DEFAULT_COLS;
|
|
13239
|
+
const rows = options.rows ?? DEFAULT_ROWS;
|
|
13240
|
+
const env = {};
|
|
13241
|
+
for (const [key, val] of Object.entries({ ...globalThis.process.env, ...options.env })) {
|
|
13242
|
+
if (val !== void 0) env[key] = val;
|
|
13243
|
+
}
|
|
13244
|
+
log.info({ shell, cwd: options.cwd, cols, rows }, "Spawning PTY");
|
|
13245
|
+
try {
|
|
13246
|
+
process2 = pty.spawn(shell, ["--login"], {
|
|
13247
|
+
name: "xterm-256color",
|
|
13248
|
+
cols,
|
|
13249
|
+
rows,
|
|
13250
|
+
cwd: options.cwd,
|
|
13251
|
+
env
|
|
13252
|
+
});
|
|
13253
|
+
} catch (err) {
|
|
13254
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
13255
|
+
log.error({ err: msg, shell }, "Failed to spawn PTY");
|
|
13256
|
+
throw new Error(`Failed to spawn PTY: ${msg}`);
|
|
13257
|
+
}
|
|
13258
|
+
isAlive = true;
|
|
13259
|
+
process2.onData((data) => {
|
|
13260
|
+
for (const cb of dataCallbacks) {
|
|
13261
|
+
cb(data);
|
|
13262
|
+
}
|
|
13263
|
+
});
|
|
13264
|
+
process2.onExit(({ exitCode, signal }) => {
|
|
13265
|
+
log.info({ exitCode, signal, pid: process2?.pid }, "PTY exited");
|
|
13266
|
+
isAlive = false;
|
|
13267
|
+
clearKillTimer();
|
|
13268
|
+
for (const cb of exitCallbacks) {
|
|
13269
|
+
cb(exitCode, signal);
|
|
13270
|
+
}
|
|
13271
|
+
});
|
|
13272
|
+
log.info({ pid: process2.pid }, "PTY spawned");
|
|
13273
|
+
}
|
|
13274
|
+
function write(data) {
|
|
13275
|
+
if (!process2 || !isAlive) {
|
|
13276
|
+
throw new Error("PTY is not running");
|
|
13277
|
+
}
|
|
13278
|
+
process2.write(data);
|
|
13279
|
+
}
|
|
13280
|
+
function resize(cols, rows) {
|
|
12366
13281
|
if (!process2 || !isAlive) {
|
|
12367
13282
|
throw new Error("PTY is not running");
|
|
12368
13283
|
}
|
|
@@ -12407,7 +13322,7 @@ function createPtyManager() {
|
|
|
12407
13322
|
get alive() {
|
|
12408
13323
|
return isAlive;
|
|
12409
13324
|
},
|
|
12410
|
-
spawn:
|
|
13325
|
+
spawn: spawn4,
|
|
12411
13326
|
write,
|
|
12412
13327
|
resize,
|
|
12413
13328
|
onData,
|
|
@@ -12480,6 +13395,49 @@ var StreamingInputController = class {
|
|
|
12480
13395
|
};
|
|
12481
13396
|
|
|
12482
13397
|
// src/session-manager.ts
|
|
13398
|
+
var IDLE_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
13399
|
+
var IDLE_CHECK_INTERVAL_MS = 3e4;
|
|
13400
|
+
var SAFE_MEDIA_TYPES = new Set(SUPPORTED_IMAGE_MEDIA_TYPES);
|
|
13401
|
+
function isSafeMediaType(value) {
|
|
13402
|
+
return SAFE_MEDIA_TYPES.has(value);
|
|
13403
|
+
}
|
|
13404
|
+
function toSdkContent(blocks) {
|
|
13405
|
+
const imageBlocks = [];
|
|
13406
|
+
const textBlocks = [];
|
|
13407
|
+
for (const block of blocks) {
|
|
13408
|
+
if (block.type === "image") imageBlocks.push(block);
|
|
13409
|
+
else if (block.type === "text") textBlocks.push(block);
|
|
13410
|
+
}
|
|
13411
|
+
const result = [];
|
|
13412
|
+
for (let i = 0; i < imageBlocks.length; i++) {
|
|
13413
|
+
const img = imageBlocks[i];
|
|
13414
|
+
if (!img) continue;
|
|
13415
|
+
if (img.source.type !== "base64") {
|
|
13416
|
+
logger.warn({ sourceType: img.source.type }, "Skipping image with unsupported source type");
|
|
13417
|
+
continue;
|
|
13418
|
+
}
|
|
13419
|
+
if (!isSafeMediaType(img.source.mediaType)) {
|
|
13420
|
+
logger.warn(
|
|
13421
|
+
{ mediaType: img.source.mediaType },
|
|
13422
|
+
"Skipping image with unsupported media type"
|
|
13423
|
+
);
|
|
13424
|
+
continue;
|
|
13425
|
+
}
|
|
13426
|
+
result.push({ type: "text", text: `Attachment ${i + 1}:` });
|
|
13427
|
+
result.push({
|
|
13428
|
+
type: "image",
|
|
13429
|
+
source: {
|
|
13430
|
+
type: "base64",
|
|
13431
|
+
media_type: img.source.mediaType,
|
|
13432
|
+
data: img.source.data
|
|
13433
|
+
}
|
|
13434
|
+
});
|
|
13435
|
+
}
|
|
13436
|
+
for (const block of textBlocks) {
|
|
13437
|
+
result.push({ type: "text", text: block.text });
|
|
13438
|
+
}
|
|
13439
|
+
return result;
|
|
13440
|
+
}
|
|
12483
13441
|
function safeStringify(value) {
|
|
12484
13442
|
try {
|
|
12485
13443
|
return JSON.stringify(value);
|
|
@@ -12525,6 +13483,17 @@ function parseSdkBlock(block, parentToolUseId) {
|
|
|
12525
13483
|
return parseToolResultBlock(block, parentToolUseId);
|
|
12526
13484
|
case "thinking":
|
|
12527
13485
|
return typeof block.thinking === "string" ? { type: "thinking", text: block.thinking } : null;
|
|
13486
|
+
case "image": {
|
|
13487
|
+
const source = block.source;
|
|
13488
|
+
if (source && source.type === "base64" && typeof source.media_type === "string" && typeof source.data === "string") {
|
|
13489
|
+
return {
|
|
13490
|
+
type: "image",
|
|
13491
|
+
id: typeof block.id === "string" ? block.id : nanoid(),
|
|
13492
|
+
source: { type: "base64", mediaType: source.media_type, data: source.data }
|
|
13493
|
+
};
|
|
13494
|
+
}
|
|
13495
|
+
return null;
|
|
13496
|
+
}
|
|
12528
13497
|
default:
|
|
12529
13498
|
return null;
|
|
12530
13499
|
}
|
|
@@ -12563,6 +13532,24 @@ var SessionManager = class {
|
|
|
12563
13532
|
}
|
|
12564
13533
|
return null;
|
|
12565
13534
|
}
|
|
13535
|
+
/**
|
|
13536
|
+
* Extract the latest user message as content blocks (text + image).
|
|
13537
|
+
* Used when sending messages to Claude so images are included.
|
|
13538
|
+
* Filters out tool_use/tool_result/thinking blocks that aren't
|
|
13539
|
+
* relevant when constructing a new prompt.
|
|
13540
|
+
*/
|
|
13541
|
+
getLatestUserContentBlocks() {
|
|
13542
|
+
const conversation = this.#taskDoc.toJSON().conversation;
|
|
13543
|
+
for (let i = conversation.length - 1; i >= 0; i--) {
|
|
13544
|
+
const msg = conversation[i];
|
|
13545
|
+
if (msg?.role === "user") {
|
|
13546
|
+
return msg.content.filter(
|
|
13547
|
+
(block) => block.type === "text" || block.type === "image"
|
|
13548
|
+
);
|
|
13549
|
+
}
|
|
13550
|
+
}
|
|
13551
|
+
return null;
|
|
13552
|
+
}
|
|
12566
13553
|
/**
|
|
12567
13554
|
* Determine whether to resume an existing session or start fresh.
|
|
12568
13555
|
*
|
|
@@ -12614,7 +13601,7 @@ var SessionManager = class {
|
|
|
12614
13601
|
});
|
|
12615
13602
|
this.#notifyStatusChange("starting");
|
|
12616
13603
|
const controller = new StreamingInputController();
|
|
12617
|
-
controller.push(opts.prompt);
|
|
13604
|
+
controller.push(typeof opts.prompt === "string" ? opts.prompt : toSdkContent(opts.prompt));
|
|
12618
13605
|
const response = query({
|
|
12619
13606
|
prompt: controller.iterable(),
|
|
12620
13607
|
options: {
|
|
@@ -12649,7 +13636,7 @@ var SessionManager = class {
|
|
|
12649
13636
|
if (!this.#inputController || this.#inputController.isDone) {
|
|
12650
13637
|
throw new Error("No active streaming session to send follow-up to");
|
|
12651
13638
|
}
|
|
12652
|
-
this.#inputController.push(prompt);
|
|
13639
|
+
this.#inputController.push(typeof prompt === "string" ? prompt : toSdkContent(prompt));
|
|
12653
13640
|
change(this.#taskDoc, (draft) => {
|
|
12654
13641
|
draft.meta.status = "working";
|
|
12655
13642
|
draft.meta.updatedAt = Date.now();
|
|
@@ -12709,7 +13696,7 @@ var SessionManager = class {
|
|
|
12709
13696
|
});
|
|
12710
13697
|
this.#notifyStatusChange("starting");
|
|
12711
13698
|
const controller = new StreamingInputController();
|
|
12712
|
-
controller.push(prompt);
|
|
13699
|
+
controller.push(typeof prompt === "string" ? prompt : toSdkContent(prompt));
|
|
12713
13700
|
const response = query({
|
|
12714
13701
|
prompt: controller.iterable(),
|
|
12715
13702
|
options: {
|
|
@@ -12738,8 +13725,22 @@ var SessionManager = class {
|
|
|
12738
13725
|
}
|
|
12739
13726
|
async #processMessages(response, sessionId) {
|
|
12740
13727
|
let agentSessionId = "";
|
|
13728
|
+
let lastMessageAt = Date.now();
|
|
13729
|
+
let idleTimedOut = false;
|
|
13730
|
+
const idleTimer = setInterval(() => {
|
|
13731
|
+
if (Date.now() - lastMessageAt >= IDLE_TIMEOUT_MS) {
|
|
13732
|
+
clearInterval(idleTimer);
|
|
13733
|
+
idleTimedOut = true;
|
|
13734
|
+
logger.warn(
|
|
13735
|
+
{ sessionId, idleMs: Date.now() - lastMessageAt },
|
|
13736
|
+
"Session idle timeout, closing"
|
|
13737
|
+
);
|
|
13738
|
+
response.close();
|
|
13739
|
+
}
|
|
13740
|
+
}, IDLE_CHECK_INTERVAL_MS);
|
|
12741
13741
|
try {
|
|
12742
13742
|
for await (const message of response) {
|
|
13743
|
+
lastMessageAt = Date.now();
|
|
12743
13744
|
const result = this.#handleMessage(message, sessionId, agentSessionId);
|
|
12744
13745
|
if (result.agentSessionId) {
|
|
12745
13746
|
agentSessionId = result.agentSessionId;
|
|
@@ -12749,24 +13750,26 @@ var SessionManager = class {
|
|
|
12749
13750
|
}
|
|
12750
13751
|
}
|
|
12751
13752
|
} catch (error) {
|
|
12752
|
-
const
|
|
12753
|
-
this.#markFailed(sessionId,
|
|
13753
|
+
const errorMsg2 = idleTimedOut ? "Session idle timeout exceeded" : error instanceof Error ? error.message : String(error);
|
|
13754
|
+
this.#markFailed(sessionId, errorMsg2);
|
|
12754
13755
|
return {
|
|
12755
13756
|
sessionId,
|
|
12756
13757
|
agentSessionId,
|
|
12757
13758
|
status: "failed",
|
|
12758
|
-
error:
|
|
13759
|
+
error: errorMsg2
|
|
12759
13760
|
};
|
|
12760
13761
|
} finally {
|
|
13762
|
+
clearInterval(idleTimer);
|
|
12761
13763
|
this.#inputController = null;
|
|
12762
13764
|
this.#activeQuery = null;
|
|
12763
13765
|
}
|
|
12764
|
-
|
|
13766
|
+
const errorMsg = idleTimedOut ? "Session idle timeout exceeded" : "Session ended without result message";
|
|
13767
|
+
this.#markFailed(sessionId, errorMsg);
|
|
12765
13768
|
return {
|
|
12766
13769
|
sessionId,
|
|
12767
13770
|
agentSessionId,
|
|
12768
13771
|
status: "failed",
|
|
12769
|
-
error:
|
|
13772
|
+
error: errorMsg
|
|
12770
13773
|
};
|
|
12771
13774
|
}
|
|
12772
13775
|
/**
|
|
@@ -12779,7 +13782,27 @@ var SessionManager = class {
|
|
|
12779
13782
|
return sessions.findIndex((s) => s.sessionId === sessionId);
|
|
12780
13783
|
}
|
|
12781
13784
|
#handleMessage(message, sessionId, agentSessionId) {
|
|
12782
|
-
|
|
13785
|
+
switch (message.type) {
|
|
13786
|
+
case "system":
|
|
13787
|
+
return this.#handleSystemMessage(message, sessionId);
|
|
13788
|
+
case "assistant":
|
|
13789
|
+
return this.#handleAssistantMsg(message, sessionId);
|
|
13790
|
+
case "user":
|
|
13791
|
+
if (!("isReplay" in message && message.isReplay)) {
|
|
13792
|
+
this.#appendUserToolResults(message);
|
|
13793
|
+
}
|
|
13794
|
+
return {};
|
|
13795
|
+
case "result":
|
|
13796
|
+
return { sessionResult: this.#handleResult(message, sessionId, agentSessionId) };
|
|
13797
|
+
case "tool_progress":
|
|
13798
|
+
this.#handleToolProgress(message);
|
|
13799
|
+
return {};
|
|
13800
|
+
default:
|
|
13801
|
+
return {};
|
|
13802
|
+
}
|
|
13803
|
+
}
|
|
13804
|
+
#handleSystemMessage(message, sessionId) {
|
|
13805
|
+
if ("subtype" in message && message.subtype === "init") {
|
|
12783
13806
|
const initSessionId = message.session_id;
|
|
12784
13807
|
if ("model" in message && typeof message.model === "string") {
|
|
12785
13808
|
this.#currentModel = message.model;
|
|
@@ -12797,24 +13820,27 @@ var SessionManager = class {
|
|
|
12797
13820
|
this.#notifyStatusChange("working");
|
|
12798
13821
|
return { agentSessionId: initSessionId };
|
|
12799
13822
|
}
|
|
12800
|
-
if (message.
|
|
12801
|
-
|
|
12802
|
-
|
|
12803
|
-
|
|
12804
|
-
|
|
12805
|
-
return {};
|
|
12806
|
-
}
|
|
12807
|
-
if (message.type === "user" && !("isReplay" in message && message.isReplay)) {
|
|
12808
|
-
this.#appendUserToolResults(message);
|
|
12809
|
-
return {};
|
|
13823
|
+
if ("subtype" in message && message.subtype === "task_notification") {
|
|
13824
|
+
const taskId = "task_id" in message ? message.task_id : "unknown";
|
|
13825
|
+
const status = "status" in message ? message.status : "unknown";
|
|
13826
|
+
const summary = "summary" in message ? message.summary : "";
|
|
13827
|
+
logger.info({ taskId, status, summary }, "Received task_notification from subagent");
|
|
12810
13828
|
}
|
|
12811
|
-
|
|
12812
|
-
|
|
12813
|
-
|
|
12814
|
-
|
|
13829
|
+
return {};
|
|
13830
|
+
}
|
|
13831
|
+
#handleAssistantMsg(message, sessionId) {
|
|
13832
|
+
if ("error" in message && message.error) {
|
|
13833
|
+
logger.warn({ error: message.error, sessionId }, "Assistant message carried an error");
|
|
12815
13834
|
}
|
|
13835
|
+
this.#appendAssistantMessage(message);
|
|
12816
13836
|
return {};
|
|
12817
13837
|
}
|
|
13838
|
+
#handleToolProgress(message) {
|
|
13839
|
+
const toolName = "tool_name" in message ? message.tool_name : "unknown";
|
|
13840
|
+
const toolUseId = "tool_use_id" in message ? message.tool_use_id : "unknown";
|
|
13841
|
+
const elapsedSeconds = "elapsed_time_seconds" in message ? message.elapsed_time_seconds : 0;
|
|
13842
|
+
logger.debug({ toolName, toolUseId, elapsedSeconds }, "tool_progress heartbeat");
|
|
13843
|
+
}
|
|
12818
13844
|
/**
|
|
12819
13845
|
* Extract tool_result blocks from SDK user messages (which carry tool outputs)
|
|
12820
13846
|
* and append them to the last assistant conversation entry so the UI can
|
|
@@ -12912,9 +13938,10 @@ var SessionManager = class {
|
|
|
12912
13938
|
);
|
|
12913
13939
|
continue;
|
|
12914
13940
|
}
|
|
13941
|
+
const planId = nanoid();
|
|
12915
13942
|
change(this.#taskDoc, (draft) => {
|
|
12916
13943
|
draft.plans.push({
|
|
12917
|
-
planId
|
|
13944
|
+
planId,
|
|
12918
13945
|
toolUseId: block.toolUseId,
|
|
12919
13946
|
markdown: planMarkdown,
|
|
12920
13947
|
reviewStatus: "pending",
|
|
@@ -12922,7 +13949,18 @@ var SessionManager = class {
|
|
|
12922
13949
|
createdAt: Date.now()
|
|
12923
13950
|
});
|
|
12924
13951
|
});
|
|
12925
|
-
|
|
13952
|
+
const loroDoc = getLoroDoc(this.#taskDoc);
|
|
13953
|
+
const initOk = initPlanEditorDoc(loroDoc, planId, planMarkdown);
|
|
13954
|
+
if (!initOk) {
|
|
13955
|
+
logger.warn(
|
|
13956
|
+
{ planId, toolUseId: block.toolUseId },
|
|
13957
|
+
"Failed to initialize plan editor doc from markdown"
|
|
13958
|
+
);
|
|
13959
|
+
}
|
|
13960
|
+
logger.info(
|
|
13961
|
+
{ toolUseId: block.toolUseId, planId },
|
|
13962
|
+
"Extracted plan from ExitPlanMode tool call"
|
|
13963
|
+
);
|
|
12926
13964
|
}
|
|
12927
13965
|
}
|
|
12928
13966
|
#handleResult(message, sessionId, agentSessionId) {
|
|
@@ -13016,8 +14054,9 @@ async function createSignalingHandle(env, log) {
|
|
|
13016
14054
|
if (!env.SHIPYARD_SIGNALING_URL) {
|
|
13017
14055
|
return null;
|
|
13018
14056
|
}
|
|
13019
|
-
const
|
|
13020
|
-
const
|
|
14057
|
+
const devSuffix = env.SHIPYARD_DEV ? "-dev" : "";
|
|
14058
|
+
const machineId = env.SHIPYARD_MACHINE_ID ?? `${hostname()}${devSuffix}`;
|
|
14059
|
+
const machineName = env.SHIPYARD_MACHINE_NAME ?? `${hostname()}${devSuffix}`;
|
|
13021
14060
|
const wsUrl = new URL(env.SHIPYARD_SIGNALING_URL);
|
|
13022
14061
|
if (!wsUrl.pathname.includes("/personal/")) {
|
|
13023
14062
|
if (!env.SHIPYARD_USER_ID) {
|
|
@@ -13053,40 +14092,280 @@ async function createSignalingHandle(env, log) {
|
|
|
13053
14092
|
return { signaling, connection, capabilities };
|
|
13054
14093
|
}
|
|
13055
14094
|
|
|
13056
|
-
// src/
|
|
13057
|
-
function
|
|
13058
|
-
|
|
14095
|
+
// src/worktree-cleanup.ts
|
|
14096
|
+
function isPidAlive(pid) {
|
|
14097
|
+
try {
|
|
14098
|
+
process.kill(pid, 0);
|
|
14099
|
+
return true;
|
|
14100
|
+
} catch {
|
|
14101
|
+
return false;
|
|
14102
|
+
}
|
|
13059
14103
|
}
|
|
13060
|
-
|
|
13061
|
-
|
|
13062
|
-
|
|
13063
|
-
|
|
13064
|
-
|
|
13065
|
-
|
|
13066
|
-
|
|
13067
|
-
|
|
13068
|
-
|
|
13069
|
-
|
|
13070
|
-
|
|
13071
|
-
const
|
|
13072
|
-
if (
|
|
13073
|
-
|
|
14104
|
+
function classifyEntries(entries, localMachineId, cutoff) {
|
|
14105
|
+
const staleKeys = [];
|
|
14106
|
+
const orphanedEntries = [];
|
|
14107
|
+
for (const [path, entry] of Object.entries(entries)) {
|
|
14108
|
+
if (entry.status !== "running") {
|
|
14109
|
+
if (entry.completedAt && entry.completedAt < cutoff) {
|
|
14110
|
+
staleKeys.push(path);
|
|
14111
|
+
}
|
|
14112
|
+
continue;
|
|
14113
|
+
}
|
|
14114
|
+
if (entry.machineId === localMachineId) {
|
|
14115
|
+
const alive = entry.pid != null && isPidAlive(entry.pid);
|
|
14116
|
+
if (!alive) {
|
|
14117
|
+
orphanedEntries.push({ path, entry });
|
|
14118
|
+
}
|
|
13074
14119
|
}
|
|
13075
14120
|
}
|
|
13076
|
-
|
|
13077
|
-
|
|
13078
|
-
|
|
13079
|
-
|
|
14121
|
+
return { staleKeys, orphanedEntries };
|
|
14122
|
+
}
|
|
14123
|
+
function cleanupStaleSetupEntries(roomDoc, localMachineId, log) {
|
|
14124
|
+
const SEVEN_DAYS_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
14125
|
+
const cutoff = Date.now() - SEVEN_DAYS_MS;
|
|
14126
|
+
try {
|
|
14127
|
+
const json = roomDoc.toJSON();
|
|
14128
|
+
const entries = json.worktreeSetupStatus ?? {};
|
|
14129
|
+
const { staleKeys, orphanedEntries } = classifyEntries(entries, localMachineId, cutoff);
|
|
14130
|
+
if (staleKeys.length === 0 && orphanedEntries.length === 0) return;
|
|
14131
|
+
change(roomDoc, (draft) => {
|
|
14132
|
+
for (const key of staleKeys) {
|
|
14133
|
+
draft.worktreeSetupStatus.delete(key);
|
|
14134
|
+
}
|
|
14135
|
+
for (const { path, entry } of orphanedEntries) {
|
|
14136
|
+
draft.worktreeSetupStatus.set(path, {
|
|
14137
|
+
...entry,
|
|
14138
|
+
status: "failed",
|
|
14139
|
+
completedAt: Date.now()
|
|
14140
|
+
});
|
|
14141
|
+
}
|
|
14142
|
+
});
|
|
14143
|
+
if (staleKeys.length > 0) {
|
|
14144
|
+
log.info({ count: staleKeys.length }, "Cleaned up stale worktree setup status entries");
|
|
14145
|
+
}
|
|
14146
|
+
if (orphanedEntries.length > 0) {
|
|
14147
|
+
log.info(
|
|
14148
|
+
{ count: orphanedEntries.length, paths: orphanedEntries.map((e) => e.path) },
|
|
14149
|
+
"Marked orphaned running setup entries as failed"
|
|
14150
|
+
);
|
|
14151
|
+
}
|
|
14152
|
+
} catch (err) {
|
|
14153
|
+
log.warn({ err }, "Failed to clean up stale worktree setup status entries");
|
|
14154
|
+
}
|
|
14155
|
+
}
|
|
14156
|
+
|
|
14157
|
+
// src/worktree-command.ts
|
|
14158
|
+
import { execFile as execFile2, spawn as spawn2 } from "child_process";
|
|
14159
|
+
import { closeSync, openSync } from "fs";
|
|
14160
|
+
import { access, chmod, constants, copyFile, mkdir as mkdir2, writeFile as writeFile3 } from "fs/promises";
|
|
14161
|
+
import { dirname as dirname2, isAbsolute as isAbsolute2, join as join5 } from "path";
|
|
14162
|
+
var GIT_TIMEOUT_MS = 3e4;
|
|
14163
|
+
function isErrnoException(err) {
|
|
14164
|
+
return err instanceof Error && "code" in err;
|
|
14165
|
+
}
|
|
14166
|
+
var MAX_BUFFER = 10 * 1024 * 1024;
|
|
14167
|
+
function runGit(args, cwd) {
|
|
14168
|
+
return new Promise((resolve3, reject) => {
|
|
14169
|
+
execFile2(
|
|
14170
|
+
"git",
|
|
14171
|
+
args,
|
|
14172
|
+
{ timeout: GIT_TIMEOUT_MS, cwd, maxBuffer: MAX_BUFFER },
|
|
14173
|
+
(error, stdout) => {
|
|
14174
|
+
if (error) reject(error);
|
|
14175
|
+
else resolve3(stdout.trim());
|
|
14176
|
+
}
|
|
14177
|
+
);
|
|
14178
|
+
});
|
|
14179
|
+
}
|
|
14180
|
+
var COPY_EXCLUDE_PATTERNS = [
|
|
14181
|
+
"node_modules",
|
|
14182
|
+
".turbo",
|
|
14183
|
+
".data/",
|
|
14184
|
+
"/dist/",
|
|
14185
|
+
"/build/",
|
|
14186
|
+
".next",
|
|
14187
|
+
".DS_Store",
|
|
14188
|
+
"*.log",
|
|
14189
|
+
".eslintcache"
|
|
14190
|
+
];
|
|
14191
|
+
function matchesPattern(filePath, pattern) {
|
|
14192
|
+
const segments = filePath.split("/");
|
|
14193
|
+
if (pattern.startsWith("*")) return filePath.endsWith(pattern.slice(1));
|
|
14194
|
+
let bare = pattern;
|
|
14195
|
+
if (bare.startsWith("/")) bare = bare.slice(1);
|
|
14196
|
+
if (bare.endsWith("/")) bare = bare.slice(0, -1);
|
|
14197
|
+
return segments.some((seg) => seg === bare);
|
|
14198
|
+
}
|
|
14199
|
+
function shouldExclude(filePath) {
|
|
14200
|
+
return COPY_EXCLUDE_PATTERNS.some((pattern) => matchesPattern(filePath, pattern));
|
|
14201
|
+
}
|
|
14202
|
+
async function refExists(ref, cwd) {
|
|
14203
|
+
try {
|
|
14204
|
+
await runGit(["rev-parse", "--verify", ref], cwd);
|
|
14205
|
+
return true;
|
|
14206
|
+
} catch {
|
|
14207
|
+
return false;
|
|
14208
|
+
}
|
|
14209
|
+
}
|
|
14210
|
+
async function assertWorktreeNotExists(worktreePath) {
|
|
14211
|
+
try {
|
|
14212
|
+
await access(worktreePath, constants.F_OK);
|
|
14213
|
+
} catch (err) {
|
|
14214
|
+
if (isErrnoException(err) && err.code === "ENOENT") return;
|
|
14215
|
+
return;
|
|
14216
|
+
}
|
|
14217
|
+
throw new Error(`Worktree already exists at ${worktreePath}`);
|
|
14218
|
+
}
|
|
14219
|
+
async function addWorktree(worktreePath, branchName, baseRef, cwd) {
|
|
14220
|
+
const remoteExists = await refExists(`origin/${branchName}`, cwd);
|
|
14221
|
+
if (remoteExists) {
|
|
13080
14222
|
try {
|
|
13081
|
-
|
|
13082
|
-
|
|
13083
|
-
const lastUserMsg = [...json.conversation].reverse().find((m) => m.role === "user");
|
|
13084
|
-
if (lastUserMsg?.cwd) return lastUserMsg.cwd;
|
|
14223
|
+
await runGit(["worktree", "add", worktreePath, branchName], cwd);
|
|
14224
|
+
return;
|
|
13085
14225
|
} catch {
|
|
14226
|
+
await runGit(
|
|
14227
|
+
["worktree", "add", worktreePath, "-b", branchName, `origin/${branchName}`],
|
|
14228
|
+
cwd
|
|
14229
|
+
);
|
|
14230
|
+
return;
|
|
13086
14231
|
}
|
|
13087
14232
|
}
|
|
13088
|
-
|
|
14233
|
+
const localExists = await refExists(branchName, cwd);
|
|
14234
|
+
if (localExists) {
|
|
14235
|
+
await runGit(["worktree", "add", worktreePath, branchName], cwd);
|
|
14236
|
+
return;
|
|
14237
|
+
}
|
|
14238
|
+
const normalizedRef = baseRef.startsWith("origin/") ? baseRef.slice("origin/".length) : baseRef;
|
|
14239
|
+
await runGit(["worktree", "add", worktreePath, "-b", branchName, `origin/${normalizedRef}`], cwd);
|
|
13089
14240
|
}
|
|
14241
|
+
async function copyIgnoredFiles(sourceRepoPath, worktreePath) {
|
|
14242
|
+
const warnings = [];
|
|
14243
|
+
const ignoredOutput = await runGit(
|
|
14244
|
+
["ls-files", "--others", "--ignored", "--exclude-standard"],
|
|
14245
|
+
sourceRepoPath
|
|
14246
|
+
);
|
|
14247
|
+
if (!ignoredOutput) return warnings;
|
|
14248
|
+
const files = ignoredOutput.split("\n").filter((f) => f && !shouldExclude(f));
|
|
14249
|
+
for (const file of files) {
|
|
14250
|
+
try {
|
|
14251
|
+
const destPath = join5(worktreePath, file);
|
|
14252
|
+
await mkdir2(dirname2(destPath), { recursive: true });
|
|
14253
|
+
await copyFile(join5(sourceRepoPath, file), destPath);
|
|
14254
|
+
} catch (err) {
|
|
14255
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
14256
|
+
warnings.push(`Failed to copy ${file}: ${msg}`);
|
|
14257
|
+
}
|
|
14258
|
+
}
|
|
14259
|
+
return warnings;
|
|
14260
|
+
}
|
|
14261
|
+
async function launchSetupScript(worktreePath, setupScript) {
|
|
14262
|
+
if (!setupScript) return null;
|
|
14263
|
+
const shipyardDir = join5(worktreePath, ".shipyard");
|
|
14264
|
+
const scriptPath = join5(shipyardDir, "worktree-setup.sh");
|
|
14265
|
+
const logPath = join5(shipyardDir, "worktree-setup.log");
|
|
14266
|
+
let fullScript;
|
|
14267
|
+
if (setupScript.startsWith("#!")) {
|
|
14268
|
+
const firstNewline = setupScript.indexOf("\n");
|
|
14269
|
+
const shebang = setupScript.slice(0, firstNewline + 1);
|
|
14270
|
+
const rest = setupScript.slice(firstNewline + 1);
|
|
14271
|
+
fullScript = `${shebang}set -e
|
|
14272
|
+
${rest}`;
|
|
14273
|
+
} else {
|
|
14274
|
+
fullScript = `#!/bin/sh
|
|
14275
|
+
set -e
|
|
14276
|
+
${setupScript}`;
|
|
14277
|
+
}
|
|
14278
|
+
await mkdir2(shipyardDir, { recursive: true });
|
|
14279
|
+
await writeFile3(scriptPath, fullScript, "utf-8");
|
|
14280
|
+
await chmod(scriptPath, 493);
|
|
14281
|
+
const logFd = openSync(logPath, "w");
|
|
14282
|
+
try {
|
|
14283
|
+
const child = spawn2(scriptPath, [], {
|
|
14284
|
+
cwd: worktreePath,
|
|
14285
|
+
stdio: ["ignore", logFd, logFd],
|
|
14286
|
+
detached: true
|
|
14287
|
+
});
|
|
14288
|
+
return child;
|
|
14289
|
+
} finally {
|
|
14290
|
+
closeSync(logFd);
|
|
14291
|
+
}
|
|
14292
|
+
}
|
|
14293
|
+
async function createWorktree(opts) {
|
|
14294
|
+
const { sourceRepoPath, branchName, baseRef, setupScript, onProgress } = opts;
|
|
14295
|
+
validateWorktreeInputs(sourceRepoPath, branchName, baseRef);
|
|
14296
|
+
const worktreeParent = `${sourceRepoPath}-wt`;
|
|
14297
|
+
const worktreePath = join5(worktreeParent, branchName);
|
|
14298
|
+
onProgress("creating-worktree", `Creating worktree at ${worktreePath}`);
|
|
14299
|
+
await mkdir2(worktreeParent, { recursive: true });
|
|
14300
|
+
await assertWorktreeNotExists(worktreePath);
|
|
14301
|
+
try {
|
|
14302
|
+
await runGit(["fetch", "origin", baseRef], sourceRepoPath);
|
|
14303
|
+
} catch {
|
|
14304
|
+
}
|
|
14305
|
+
await addWorktree(worktreePath, branchName, baseRef, sourceRepoPath);
|
|
14306
|
+
onProgress("copying-files", "Copying ignored files from source repo");
|
|
14307
|
+
let warnings = [];
|
|
14308
|
+
try {
|
|
14309
|
+
warnings = await copyIgnoredFiles(sourceRepoPath, worktreePath);
|
|
14310
|
+
onProgress("copying-files", `Copied ignored files (${warnings.length} warnings)`);
|
|
14311
|
+
} catch (err) {
|
|
14312
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
14313
|
+
warnings.push(`Failed to list ignored files: ${msg}`);
|
|
14314
|
+
}
|
|
14315
|
+
const setupChild = await launchSetupScript(worktreePath, setupScript);
|
|
14316
|
+
const setupScriptStarted = setupChild !== null;
|
|
14317
|
+
if (setupScriptStarted) {
|
|
14318
|
+
onProgress("running-setup-script", "Launched worktree setup script");
|
|
14319
|
+
}
|
|
14320
|
+
return { worktreePath, branchName, setupScriptStarted, setupChild, warnings };
|
|
14321
|
+
}
|
|
14322
|
+
var BASE_REF_PATTERN = /^[a-zA-Z0-9][a-zA-Z0-9/_.-]*$/;
|
|
14323
|
+
function validateWorktreeInputs(sourceRepoPath, branchName, baseRef) {
|
|
14324
|
+
if (!isAbsolute2(sourceRepoPath)) {
|
|
14325
|
+
throw new Error(`sourceRepoPath must be an absolute path, got: ${sourceRepoPath}`);
|
|
14326
|
+
}
|
|
14327
|
+
if (sourceRepoPath.includes("..")) {
|
|
14328
|
+
throw new Error('sourceRepoPath must not contain ".." path traversal segments');
|
|
14329
|
+
}
|
|
14330
|
+
if (branchName.includes("..")) {
|
|
14331
|
+
throw new Error('branchName must not contain ".." path traversal segments');
|
|
14332
|
+
}
|
|
14333
|
+
if (branchName.startsWith("-") || branchName.startsWith(".")) {
|
|
14334
|
+
throw new Error('branchName must not start with "-" or "."');
|
|
14335
|
+
}
|
|
14336
|
+
if (!BRANCH_NAME_PATTERN.test(branchName)) {
|
|
14337
|
+
throw new Error(
|
|
14338
|
+
`branchName contains invalid characters. Must match ${BRANCH_NAME_PATTERN.source}`
|
|
14339
|
+
);
|
|
14340
|
+
}
|
|
14341
|
+
if (baseRef.startsWith("-")) {
|
|
14342
|
+
throw new Error('baseRef must not start with "-"');
|
|
14343
|
+
}
|
|
14344
|
+
if (baseRef.includes("..")) {
|
|
14345
|
+
throw new Error('baseRef must not contain ".." path traversal segments');
|
|
14346
|
+
}
|
|
14347
|
+
if (!BASE_REF_PATTERN.test(baseRef)) {
|
|
14348
|
+
throw new Error(`baseRef contains invalid characters. Must match ${BASE_REF_PATTERN.source}`);
|
|
14349
|
+
}
|
|
14350
|
+
}
|
|
14351
|
+
|
|
14352
|
+
// src/serve.ts
|
|
14353
|
+
function assertNever(x) {
|
|
14354
|
+
throw new Error(`Unhandled message type: ${JSON.stringify(x)}`);
|
|
14355
|
+
}
|
|
14356
|
+
var processedRequestIds = /* @__PURE__ */ new Set();
|
|
14357
|
+
var pendingCleanupTimers = /* @__PURE__ */ new Set();
|
|
14358
|
+
function scheduleEphemeralCleanup(fn, delayMs) {
|
|
14359
|
+
const timer = setTimeout(() => {
|
|
14360
|
+
pendingCleanupTimers.delete(timer);
|
|
14361
|
+
fn();
|
|
14362
|
+
}, delayMs);
|
|
14363
|
+
pendingCleanupTimers.add(timer);
|
|
14364
|
+
}
|
|
14365
|
+
var CONTROL_PREFIX = "\0\0";
|
|
14366
|
+
var TERMINAL_BUFFER_MAX_BYTES = 1048576;
|
|
14367
|
+
var TERMINAL_OPEN_TIMEOUT_MS = 1e4;
|
|
14368
|
+
var TERMINAL_CWD_TIMEOUT_MS = 5e3;
|
|
13090
14369
|
var TaskEphemeralDeclarations = {
|
|
13091
14370
|
permReqs: PermissionRequestEphemeral,
|
|
13092
14371
|
permResps: PermissionResponseEphemeral
|
|
@@ -13097,7 +14376,23 @@ async function serve(env) {
|
|
|
13097
14376
|
process.exit(1);
|
|
13098
14377
|
}
|
|
13099
14378
|
const log = createChildLogger({ mode: "serve" });
|
|
14379
|
+
if (env.SHIPYARD_USER_TOKEN && env.SHIPYARD_SIGNALING_URL) {
|
|
14380
|
+
const client = new SignalingClient(env.SHIPYARD_SIGNALING_URL);
|
|
14381
|
+
try {
|
|
14382
|
+
const result = await client.verify(env.SHIPYARD_USER_TOKEN);
|
|
14383
|
+
if (!result.valid) {
|
|
14384
|
+
logger.error(
|
|
14385
|
+
`Auth token is no longer valid (${result.reason}). Run \`shipyard login\` to re-authenticate.`
|
|
14386
|
+
);
|
|
14387
|
+
process.exit(1);
|
|
14388
|
+
}
|
|
14389
|
+
log.info("Token verified against session server");
|
|
14390
|
+
} catch {
|
|
14391
|
+
log.warn("Could not verify token with session server, proceeding anyway");
|
|
14392
|
+
}
|
|
14393
|
+
}
|
|
13100
14394
|
const lifecycle = new LifecycleManager();
|
|
14395
|
+
await lifecycle.acquirePidFile(getShipyardHome());
|
|
13101
14396
|
const handle = await createSignalingHandle(env, log);
|
|
13102
14397
|
if (!handle) {
|
|
13103
14398
|
logger.error("SHIPYARD_SIGNALING_URL is required for serve mode");
|
|
@@ -13106,9 +14401,11 @@ async function serve(env) {
|
|
|
13106
14401
|
const { signaling, connection, capabilities } = handle;
|
|
13107
14402
|
const activeTasks = /* @__PURE__ */ new Map();
|
|
13108
14403
|
const watchedTasks = /* @__PURE__ */ new Map();
|
|
13109
|
-
const
|
|
14404
|
+
const taskHandles = /* @__PURE__ */ new Map();
|
|
14405
|
+
const devSuffix = env.SHIPYARD_DEV ? "-dev" : "";
|
|
14406
|
+
const machineId = env.SHIPYARD_MACHINE_ID ?? `${hostname2()}${devSuffix}`;
|
|
13110
14407
|
const dataDir = resolve(env.SHIPYARD_DATA_DIR.replace("~", homedir2()));
|
|
13111
|
-
await
|
|
14408
|
+
await mkdir3(dataDir, { recursive: true, mode: 448 });
|
|
13112
14409
|
const storage = new FileStorageAdapter(dataDir);
|
|
13113
14410
|
const webrtcAdapter = new WebRtcDataChannelAdapter();
|
|
13114
14411
|
const repo = new Repo({
|
|
@@ -13132,17 +14429,18 @@ async function serve(env) {
|
|
|
13132
14429
|
candidate
|
|
13133
14430
|
});
|
|
13134
14431
|
},
|
|
13135
|
-
onTerminalChannel(fromMachineId, rawChannel) {
|
|
14432
|
+
onTerminalChannel(fromMachineId, rawChannel, taskId) {
|
|
13136
14433
|
const channel = rawChannel;
|
|
13137
|
-
const
|
|
13138
|
-
const
|
|
14434
|
+
const terminalKey = `${fromMachineId}:${taskId}`;
|
|
14435
|
+
const termLog = createChildLogger({ mode: `terminal:${fromMachineId}:${taskId}` });
|
|
14436
|
+
const existingPty = terminalPtys.get(terminalKey);
|
|
13139
14437
|
if (existingPty) {
|
|
13140
14438
|
termLog.info("Disposing existing PTY for reconnecting machine");
|
|
13141
14439
|
existingPty.dispose();
|
|
13142
|
-
terminalPtys.delete(
|
|
14440
|
+
terminalPtys.delete(terminalKey);
|
|
13143
14441
|
}
|
|
13144
14442
|
const ptyManager = createPtyManager();
|
|
13145
|
-
terminalPtys.set(
|
|
14443
|
+
terminalPtys.set(terminalKey, ptyManager);
|
|
13146
14444
|
let ptySpawned = false;
|
|
13147
14445
|
let channelOpen = channel.readyState === "open";
|
|
13148
14446
|
const pendingBuffer = [];
|
|
@@ -13153,7 +14451,7 @@ async function serve(env) {
|
|
|
13153
14451
|
clearTimeout(openTimeout);
|
|
13154
14452
|
clearTimeout(cwdTimeout);
|
|
13155
14453
|
ptyManager.dispose();
|
|
13156
|
-
terminalPtys.delete(
|
|
14454
|
+
terminalPtys.delete(terminalKey);
|
|
13157
14455
|
if (channel.readyState === "open") {
|
|
13158
14456
|
channel.close();
|
|
13159
14457
|
}
|
|
@@ -13205,7 +14503,7 @@ async function serve(env) {
|
|
|
13205
14503
|
} catch (err) {
|
|
13206
14504
|
termLog.error({ err }, "Failed to spawn terminal PTY");
|
|
13207
14505
|
channel.close();
|
|
13208
|
-
terminalPtys.delete(
|
|
14506
|
+
terminalPtys.delete(terminalKey);
|
|
13209
14507
|
return;
|
|
13210
14508
|
}
|
|
13211
14509
|
ptyManager.onData((data) => {
|
|
@@ -13216,7 +14514,7 @@ async function serve(env) {
|
|
|
13216
14514
|
if (channel.readyState === "open") {
|
|
13217
14515
|
channel.close();
|
|
13218
14516
|
}
|
|
13219
|
-
terminalPtys.delete(
|
|
14517
|
+
terminalPtys.delete(terminalKey);
|
|
13220
14518
|
});
|
|
13221
14519
|
for (const input of preSpawnInputBuffer) {
|
|
13222
14520
|
try {
|
|
@@ -13232,8 +14530,8 @@ async function serve(env) {
|
|
|
13232
14530
|
}
|
|
13233
14531
|
const cwdTimeout = setTimeout(() => {
|
|
13234
14532
|
if (!ptySpawned) {
|
|
13235
|
-
termLog.info("No cwd control message received, falling back to
|
|
13236
|
-
const fallbackCwd =
|
|
14533
|
+
termLog.info("No cwd control message received, falling back to $HOME");
|
|
14534
|
+
const fallbackCwd = process.env.HOME ?? process.cwd();
|
|
13237
14535
|
spawnPty(fallbackCwd);
|
|
13238
14536
|
}
|
|
13239
14537
|
}, TERMINAL_CWD_TIMEOUT_MS);
|
|
@@ -13273,7 +14571,9 @@ async function serve(env) {
|
|
|
13273
14571
|
clearTimeout(openTimeout);
|
|
13274
14572
|
clearTimeout(cwdTimeout);
|
|
13275
14573
|
ptyManager.dispose();
|
|
13276
|
-
terminalPtys.
|
|
14574
|
+
if (terminalPtys.get(terminalKey) === ptyManager) {
|
|
14575
|
+
terminalPtys.delete(terminalKey);
|
|
14576
|
+
}
|
|
13277
14577
|
};
|
|
13278
14578
|
}
|
|
13279
14579
|
});
|
|
@@ -13294,7 +14594,12 @@ async function serve(env) {
|
|
|
13294
14594
|
remote: e.remote ?? null
|
|
13295
14595
|
})),
|
|
13296
14596
|
permissionModes: caps.permissionModes,
|
|
13297
|
-
homeDir: caps.homeDir ?? null
|
|
14597
|
+
homeDir: caps.homeDir ?? null,
|
|
14598
|
+
anthropicAuth: caps.anthropicAuth ? {
|
|
14599
|
+
status: caps.anthropicAuth.status,
|
|
14600
|
+
method: caps.anthropicAuth.method,
|
|
14601
|
+
email: caps.anthropicAuth.email ?? null
|
|
14602
|
+
} : null
|
|
13298
14603
|
};
|
|
13299
14604
|
roomHandle.capabilities.set(machineId, value);
|
|
13300
14605
|
log.info({ machineId }, "Published capabilities to room ephemeral");
|
|
@@ -13307,6 +14612,165 @@ async function serve(env) {
|
|
|
13307
14612
|
publishCapabilities(capabilities);
|
|
13308
14613
|
}
|
|
13309
14614
|
});
|
|
14615
|
+
const typedRoomHandle = roomHandle;
|
|
14616
|
+
const typedRoomDoc = roomHandle.doc;
|
|
14617
|
+
cleanupStaleSetupEntries(typedRoomDoc, machineId, log);
|
|
14618
|
+
typedRoomHandle.enhancePromptReqs.subscribe(({ key: requestId, value, source }) => {
|
|
14619
|
+
if (source !== "remote") return;
|
|
14620
|
+
if (!value) return;
|
|
14621
|
+
if (value.machineId !== machineId) return;
|
|
14622
|
+
if (processedRequestIds.has(requestId)) return;
|
|
14623
|
+
processedRequestIds.add(requestId);
|
|
14624
|
+
const enhLog = createChildLogger({ mode: `enhance-prompt-ephemeral:${requestId}` });
|
|
14625
|
+
enhLog.info(
|
|
14626
|
+
{ promptLen: value.prompt.length },
|
|
14627
|
+
"Received enhance-prompt request via ephemeral"
|
|
14628
|
+
);
|
|
14629
|
+
if (capabilities.anthropicAuth?.status !== "authenticated") {
|
|
14630
|
+
enhLog.error("Not authenticated with Anthropic");
|
|
14631
|
+
typedRoomHandle.enhancePromptResps.set(requestId, {
|
|
14632
|
+
status: "error",
|
|
14633
|
+
text: "",
|
|
14634
|
+
error: "Not authenticated with Anthropic. Run 'claude auth login' or set ANTHROPIC_API_KEY."
|
|
14635
|
+
});
|
|
14636
|
+
scheduleEphemeralCleanup(() => {
|
|
14637
|
+
typedRoomHandle.enhancePromptReqs.delete(requestId);
|
|
14638
|
+
typedRoomHandle.enhancePromptResps.delete(requestId);
|
|
14639
|
+
}, EPHEMERAL_CLEANUP_DELAY_MS);
|
|
14640
|
+
processedRequestIds.delete(requestId);
|
|
14641
|
+
return;
|
|
14642
|
+
}
|
|
14643
|
+
const abortController = lifecycle.createAbortController();
|
|
14644
|
+
const timeout = setTimeout(() => abortController.abort(), ENHANCE_PROMPT_TIMEOUT_MS);
|
|
14645
|
+
runEnhancePromptEphemeral(value.prompt, requestId, abortController, typedRoomHandle, enhLog).catch((err) => {
|
|
14646
|
+
enhLog.error({ err }, "Enhance prompt ephemeral failed");
|
|
14647
|
+
}).finally(() => {
|
|
14648
|
+
clearTimeout(timeout);
|
|
14649
|
+
abortController.abort();
|
|
14650
|
+
processedRequestIds.delete(requestId);
|
|
14651
|
+
});
|
|
14652
|
+
});
|
|
14653
|
+
typedRoomHandle.worktreeCreateReqs.subscribe(({ key: requestId, value, source }) => {
|
|
14654
|
+
if (source !== "remote") return;
|
|
14655
|
+
if (!value) return;
|
|
14656
|
+
if (value.machineId !== machineId) return;
|
|
14657
|
+
if (processedRequestIds.has(requestId)) return;
|
|
14658
|
+
processedRequestIds.add(requestId);
|
|
14659
|
+
const wtLog = createChildLogger({ mode: `worktree-create-ephemeral:${requestId}` });
|
|
14660
|
+
wtLog.info(
|
|
14661
|
+
{
|
|
14662
|
+
sourceRepoPath: value.sourceRepoPath,
|
|
14663
|
+
branchName: value.branchName,
|
|
14664
|
+
baseRef: value.baseRef
|
|
14665
|
+
},
|
|
14666
|
+
"Received worktree-create request via ephemeral"
|
|
14667
|
+
);
|
|
14668
|
+
const roomJson = typedRoomDoc.toJSON();
|
|
14669
|
+
const crdtScriptEntry = roomJson?.userSettings?.worktreeScripts?.[value.sourceRepoPath];
|
|
14670
|
+
const resolvedSetupScript = value.setupScript ?? crdtScriptEntry?.script ?? null;
|
|
14671
|
+
runWorktreeCreateEphemeral(
|
|
14672
|
+
requestId,
|
|
14673
|
+
value.sourceRepoPath,
|
|
14674
|
+
value.branchName,
|
|
14675
|
+
value.baseRef,
|
|
14676
|
+
resolvedSetupScript,
|
|
14677
|
+
typedRoomHandle,
|
|
14678
|
+
typedRoomDoc,
|
|
14679
|
+
machineId,
|
|
14680
|
+
capabilities,
|
|
14681
|
+
publishCapabilities,
|
|
14682
|
+
branchWatcher,
|
|
14683
|
+
wtLog
|
|
14684
|
+
).catch((err) => {
|
|
14685
|
+
wtLog.error({ err }, "Worktree create ephemeral handler failed");
|
|
14686
|
+
}).finally(() => {
|
|
14687
|
+
processedRequestIds.delete(requestId);
|
|
14688
|
+
});
|
|
14689
|
+
});
|
|
14690
|
+
typedRoomHandle.anthropicLoginReqs.subscribe(({ key: requestId, value, source }) => {
|
|
14691
|
+
if (source !== "remote") return;
|
|
14692
|
+
if (!value) return;
|
|
14693
|
+
if (value.machineId !== machineId) return;
|
|
14694
|
+
if (processedRequestIds.has(requestId)) return;
|
|
14695
|
+
processedRequestIds.add(requestId);
|
|
14696
|
+
const loginLog = createChildLogger({ mode: `anthropic-login:${requestId}` });
|
|
14697
|
+
loginLog.info("Received Anthropic login request via ephemeral");
|
|
14698
|
+
typedRoomHandle.anthropicLoginResps.set(requestId, {
|
|
14699
|
+
status: "starting",
|
|
14700
|
+
loginUrl: null,
|
|
14701
|
+
error: null
|
|
14702
|
+
});
|
|
14703
|
+
const child = spawn3("claude", ["auth", "login"], {
|
|
14704
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
14705
|
+
});
|
|
14706
|
+
let stdout = "";
|
|
14707
|
+
child.stdout?.on("data", (chunk) => {
|
|
14708
|
+
const text = chunk.toString();
|
|
14709
|
+
stdout += text;
|
|
14710
|
+
const urlMatch = text.match(/https:\/\/\S+/);
|
|
14711
|
+
if (urlMatch) {
|
|
14712
|
+
typedRoomHandle.anthropicLoginResps.set(requestId, {
|
|
14713
|
+
status: "waiting",
|
|
14714
|
+
loginUrl: urlMatch[0],
|
|
14715
|
+
error: null
|
|
14716
|
+
});
|
|
14717
|
+
}
|
|
14718
|
+
});
|
|
14719
|
+
child.stderr?.on("data", (chunk) => {
|
|
14720
|
+
const text = chunk.toString();
|
|
14721
|
+
stdout += text;
|
|
14722
|
+
const urlMatch = text.match(/https:\/\/\S+/);
|
|
14723
|
+
if (urlMatch) {
|
|
14724
|
+
typedRoomHandle.anthropicLoginResps.set(requestId, {
|
|
14725
|
+
status: "waiting",
|
|
14726
|
+
loginUrl: urlMatch[0],
|
|
14727
|
+
error: null
|
|
14728
|
+
});
|
|
14729
|
+
}
|
|
14730
|
+
});
|
|
14731
|
+
child.on("exit", (exitCode) => {
|
|
14732
|
+
if (exitCode === 0) {
|
|
14733
|
+
loginLog.info("Anthropic login completed successfully");
|
|
14734
|
+
detectAnthropicAuth().then((authStatus) => {
|
|
14735
|
+
capabilities.anthropicAuth = authStatus;
|
|
14736
|
+
publishCapabilities(capabilities);
|
|
14737
|
+
loginLog.info({ authStatus }, "Re-detected auth after login");
|
|
14738
|
+
}).catch((err) => {
|
|
14739
|
+
loginLog.warn({ err }, "Failed to re-detect auth after login");
|
|
14740
|
+
});
|
|
14741
|
+
typedRoomHandle.anthropicLoginResps.set(requestId, {
|
|
14742
|
+
status: "done",
|
|
14743
|
+
loginUrl: null,
|
|
14744
|
+
error: null
|
|
14745
|
+
});
|
|
14746
|
+
} else {
|
|
14747
|
+
loginLog.error({ exitCode, stdout }, "Anthropic login failed");
|
|
14748
|
+
typedRoomHandle.anthropicLoginResps.set(requestId, {
|
|
14749
|
+
status: "error",
|
|
14750
|
+
loginUrl: null,
|
|
14751
|
+
error: `Login failed (exit code ${exitCode})`
|
|
14752
|
+
});
|
|
14753
|
+
}
|
|
14754
|
+
scheduleEphemeralCleanup(() => {
|
|
14755
|
+
typedRoomHandle.anthropicLoginReqs.delete(requestId);
|
|
14756
|
+
typedRoomHandle.anthropicLoginResps.delete(requestId);
|
|
14757
|
+
}, EPHEMERAL_CLEANUP_DELAY_MS);
|
|
14758
|
+
processedRequestIds.delete(requestId);
|
|
14759
|
+
});
|
|
14760
|
+
child.on("error", (err) => {
|
|
14761
|
+
loginLog.error({ err: err.message }, "Failed to spawn claude auth login");
|
|
14762
|
+
typedRoomHandle.anthropicLoginResps.set(requestId, {
|
|
14763
|
+
status: "error",
|
|
14764
|
+
loginUrl: null,
|
|
14765
|
+
error: `Failed to start login: ${err.message}`
|
|
14766
|
+
});
|
|
14767
|
+
scheduleEphemeralCleanup(() => {
|
|
14768
|
+
typedRoomHandle.anthropicLoginReqs.delete(requestId);
|
|
14769
|
+
typedRoomHandle.anthropicLoginResps.delete(requestId);
|
|
14770
|
+
}, EPHEMERAL_CLEANUP_DELAY_MS);
|
|
14771
|
+
processedRequestIds.delete(requestId);
|
|
14772
|
+
});
|
|
14773
|
+
});
|
|
13310
14774
|
connection.onStateChange((state) => {
|
|
13311
14775
|
log.info({ state }, "Connection state changed");
|
|
13312
14776
|
});
|
|
@@ -13316,13 +14780,18 @@ async function serve(env) {
|
|
|
13316
14780
|
signaling,
|
|
13317
14781
|
connection,
|
|
13318
14782
|
repo,
|
|
13319
|
-
roomDoc:
|
|
14783
|
+
roomDoc: typedRoomDoc,
|
|
14784
|
+
roomHandle: typedRoomHandle,
|
|
13320
14785
|
lifecycle,
|
|
13321
14786
|
activeTasks,
|
|
13322
14787
|
watchedTasks,
|
|
14788
|
+
taskHandles,
|
|
13323
14789
|
peerManager,
|
|
13324
14790
|
env,
|
|
13325
|
-
machineId
|
|
14791
|
+
machineId,
|
|
14792
|
+
capabilities,
|
|
14793
|
+
publishCapabilities,
|
|
14794
|
+
branchWatcher
|
|
13326
14795
|
});
|
|
13327
14796
|
});
|
|
13328
14797
|
connection.connect();
|
|
@@ -13347,6 +14816,9 @@ async function serve(env) {
|
|
|
13347
14816
|
unsub();
|
|
13348
14817
|
}
|
|
13349
14818
|
watchedTasks.clear();
|
|
14819
|
+
taskHandles.clear();
|
|
14820
|
+
for (const timer of pendingCleanupTimers) clearTimeout(timer);
|
|
14821
|
+
pendingCleanupTimers.clear();
|
|
13350
14822
|
for (const [id, ptyMgr] of terminalPtys) {
|
|
13351
14823
|
ptyMgr.dispose();
|
|
13352
14824
|
terminalPtys.delete(id);
|
|
@@ -13504,6 +14976,21 @@ function handleMessage(msg, ctx) {
|
|
|
13504
14976
|
});
|
|
13505
14977
|
break;
|
|
13506
14978
|
}
|
|
14979
|
+
case "enhance-prompt-request":
|
|
14980
|
+
handleEnhancePrompt(msg, ctx);
|
|
14981
|
+
break;
|
|
14982
|
+
case "enhance-prompt-chunk":
|
|
14983
|
+
case "enhance-prompt-done":
|
|
14984
|
+
ctx.log.debug({ type: msg.type }, "Enhance prompt echo");
|
|
14985
|
+
break;
|
|
14986
|
+
case "worktree-create-request":
|
|
14987
|
+
handleWorktreeCreate(msg, ctx);
|
|
14988
|
+
break;
|
|
14989
|
+
case "worktree-create-progress":
|
|
14990
|
+
case "worktree-create-done":
|
|
14991
|
+
case "worktree-create-error":
|
|
14992
|
+
ctx.log.debug({ type: msg.type }, "Worktree create echo");
|
|
14993
|
+
break;
|
|
13507
14994
|
case "authenticated":
|
|
13508
14995
|
case "agent-joined":
|
|
13509
14996
|
case "agent-left":
|
|
@@ -13518,25 +15005,29 @@ function handleMessage(msg, ctx) {
|
|
|
13518
15005
|
function handleNotifyTask(msg, ctx) {
|
|
13519
15006
|
const { taskId, requestId } = msg;
|
|
13520
15007
|
const taskLog = createChildLogger({ mode: "serve", taskId });
|
|
13521
|
-
if (
|
|
13522
|
-
taskLog.error("
|
|
15008
|
+
if (ctx.capabilities.anthropicAuth?.status !== "authenticated") {
|
|
15009
|
+
taskLog.error("Not authenticated with Anthropic");
|
|
13523
15010
|
ctx.connection.send({
|
|
13524
15011
|
type: "task-ack",
|
|
13525
15012
|
requestId,
|
|
13526
15013
|
taskId,
|
|
13527
15014
|
accepted: false,
|
|
13528
|
-
error: "
|
|
15015
|
+
error: "Not authenticated with Anthropic. Run 'claude auth login' or set ANTHROPIC_API_KEY."
|
|
13529
15016
|
});
|
|
13530
15017
|
return;
|
|
13531
15018
|
}
|
|
13532
15019
|
if (ctx.watchedTasks.has(taskId)) {
|
|
13533
|
-
taskLog.debug("Task already watched,
|
|
15020
|
+
taskLog.debug("Task already watched, re-checking for new work");
|
|
13534
15021
|
ctx.connection.send({
|
|
13535
15022
|
type: "task-ack",
|
|
13536
15023
|
requestId,
|
|
13537
15024
|
taskId,
|
|
13538
15025
|
accepted: true
|
|
13539
15026
|
});
|
|
15027
|
+
const taskHandle = ctx.taskHandles.get(taskId);
|
|
15028
|
+
if (taskHandle) {
|
|
15029
|
+
onTaskDocChanged(taskId, taskHandle, taskLog, ctx);
|
|
15030
|
+
}
|
|
13540
15031
|
return;
|
|
13541
15032
|
}
|
|
13542
15033
|
taskLog.info("Received notify-task, starting watch");
|
|
@@ -13551,6 +15042,424 @@ function handleNotifyTask(msg, ctx) {
|
|
|
13551
15042
|
taskLog.error({ err: errMsg }, "Failed to start watching task document");
|
|
13552
15043
|
});
|
|
13553
15044
|
}
|
|
15045
|
+
var ENHANCE_PROMPT_TIMEOUT_MS = 3e4;
|
|
15046
|
+
var ENHANCE_SYSTEM_PROMPT = `You are a prompt rewriter that transforms rough user requests into clear, specific instructions for an AI coding assistant (Claude Code). The enhanced prompt will be sent AS the user's message to that assistant.
|
|
15047
|
+
|
|
15048
|
+
RULES:
|
|
15049
|
+
- Write in imperative/request form ("Build...", "Create...", "Add...") \u2014 NEVER first person ("I'll...", "Let me...")
|
|
15050
|
+
- Return ONLY the rewritten prompt. No preamble, explanation, or markdown formatting.
|
|
15051
|
+
- Keep the user's core intent intact. Do not add features they didn't imply.
|
|
15052
|
+
- Expand vague ideas into concrete specifics: name technologies, list features, set constraints.
|
|
15053
|
+
- If the user names a technology, keep it. If they don't, pick sensible defaults.
|
|
15054
|
+
- Target 2-5x the input length. Be concise and direct \u2014 no filler, hype, or superlatives.
|
|
15055
|
+
- Do NOT add project setup boilerplate (e.g., "initialize a git repo", "set up CI/CD") unless asked.
|
|
15056
|
+
- Structure as a single paragraph or short bullet list, whichever is clearer.
|
|
15057
|
+
|
|
15058
|
+
WHAT TO ADD:
|
|
15059
|
+
- Specific features the request implies but doesn't list
|
|
15060
|
+
- Technology choices when unspecified (framework, API, library)
|
|
15061
|
+
- Concrete parameters (sizes, ranges, counts) instead of vague adjectives
|
|
15062
|
+
- UI/UX details when building something visual
|
|
15063
|
+
- Scope boundaries to prevent over-building
|
|
15064
|
+
|
|
15065
|
+
EXAMPLES:
|
|
15066
|
+
|
|
15067
|
+
Input: "make a cool hello world beat app"
|
|
15068
|
+
Output: "Build a browser-based beat maker with a 4x4 pad grid that plays drum sounds on click. Include kick, snare, hi-hat, and clap samples using the Web Audio API. Add a step sequencer with play/pause, a tempo slider (60-200 BPM), and visual feedback on active pads. Keep it to a single page with a dark theme."
|
|
15069
|
+
|
|
15070
|
+
Input: "fix the login bug"
|
|
15071
|
+
Output: "Investigate and fix the login bug. Check the authentication flow for common issues: expired tokens, incorrect credential validation, session handling errors, or CORS misconfigurations. Add error logging if missing and verify the fix works for both valid and invalid credential cases."
|
|
15072
|
+
|
|
15073
|
+
Input: "add dark mode"
|
|
15074
|
+
Output: "Add a dark mode toggle to the app. Use CSS custom properties for theme colors, persist the user's preference in localStorage, and respect the system prefers-color-scheme setting as the default. Ensure all existing components have adequate contrast in both themes."
|
|
15075
|
+
|
|
15076
|
+
Input: "make a landing page for my saas"
|
|
15077
|
+
Output: "Build a responsive landing page with: hero section with headline, subheadline, and CTA button; features grid (3-4 cards with icons); pricing section with 2-3 tier cards; FAQ accordion; and a footer with links. Use a clean, modern design with consistent spacing. Make it mobile-first with a sticky header."`;
|
|
15078
|
+
function handleEnhancePrompt(msg, ctx) {
|
|
15079
|
+
const { requestId, prompt } = msg;
|
|
15080
|
+
if (processedRequestIds.has(requestId)) return;
|
|
15081
|
+
processedRequestIds.add(requestId);
|
|
15082
|
+
const enhanceLog = createChildLogger({ mode: "enhance-prompt" });
|
|
15083
|
+
if (ctx.capabilities.anthropicAuth?.status !== "authenticated") {
|
|
15084
|
+
enhanceLog.error("Not authenticated with Anthropic");
|
|
15085
|
+
ctx.connection.send({
|
|
15086
|
+
type: "error",
|
|
15087
|
+
code: "not_authenticated",
|
|
15088
|
+
message: "Not authenticated with Anthropic. Run 'claude auth login' or set ANTHROPIC_API_KEY.",
|
|
15089
|
+
requestId
|
|
15090
|
+
});
|
|
15091
|
+
processedRequestIds.delete(requestId);
|
|
15092
|
+
return;
|
|
15093
|
+
}
|
|
15094
|
+
const abortController = ctx.lifecycle.createAbortController();
|
|
15095
|
+
const timeout = setTimeout(() => abortController.abort(), ENHANCE_PROMPT_TIMEOUT_MS);
|
|
15096
|
+
runEnhancePrompt(prompt, requestId, abortController, ctx, enhanceLog).catch((err) => {
|
|
15097
|
+
enhanceLog.error({ err }, "Enhance prompt failed");
|
|
15098
|
+
}).finally(() => {
|
|
15099
|
+
clearTimeout(timeout);
|
|
15100
|
+
abortController.abort();
|
|
15101
|
+
processedRequestIds.delete(requestId);
|
|
15102
|
+
});
|
|
15103
|
+
}
|
|
15104
|
+
function extractTextChunks(rawContent) {
|
|
15105
|
+
if (!Array.isArray(rawContent)) return [];
|
|
15106
|
+
const chunks = [];
|
|
15107
|
+
for (const block of rawContent) {
|
|
15108
|
+
const b = block;
|
|
15109
|
+
if (b.type === "text" && typeof b.text === "string") {
|
|
15110
|
+
chunks.push(b.text);
|
|
15111
|
+
}
|
|
15112
|
+
}
|
|
15113
|
+
return chunks;
|
|
15114
|
+
}
|
|
15115
|
+
async function runEnhancePrompt(prompt, requestId, abortController, ctx, log) {
|
|
15116
|
+
let fullText = "";
|
|
15117
|
+
try {
|
|
15118
|
+
const response = query2({
|
|
15119
|
+
prompt,
|
|
15120
|
+
options: {
|
|
15121
|
+
maxTurns: 1,
|
|
15122
|
+
allowedTools: [],
|
|
15123
|
+
systemPrompt: ENHANCE_SYSTEM_PROMPT,
|
|
15124
|
+
abortController
|
|
15125
|
+
}
|
|
15126
|
+
});
|
|
15127
|
+
for await (const message of response) {
|
|
15128
|
+
if (message.type !== "assistant") continue;
|
|
15129
|
+
for (const text of extractTextChunks(message.message.content)) {
|
|
15130
|
+
fullText += text;
|
|
15131
|
+
ctx.connection.send({
|
|
15132
|
+
type: "enhance-prompt-chunk",
|
|
15133
|
+
requestId,
|
|
15134
|
+
text
|
|
15135
|
+
});
|
|
15136
|
+
}
|
|
15137
|
+
}
|
|
15138
|
+
ctx.connection.send({
|
|
15139
|
+
type: "enhance-prompt-done",
|
|
15140
|
+
requestId,
|
|
15141
|
+
fullText
|
|
15142
|
+
});
|
|
15143
|
+
log.info({ promptLen: prompt.length, resultLen: fullText.length }, "Prompt enhanced");
|
|
15144
|
+
} catch (err) {
|
|
15145
|
+
if (abortController.signal.aborted) {
|
|
15146
|
+
log.warn("Prompt enhancement aborted");
|
|
15147
|
+
} else {
|
|
15148
|
+
log.error({ err }, "Prompt enhancement failed");
|
|
15149
|
+
}
|
|
15150
|
+
ctx.connection.send({
|
|
15151
|
+
type: "error",
|
|
15152
|
+
code: "enhance_failed",
|
|
15153
|
+
message: err instanceof Error ? err.message : "Prompt enhancement failed",
|
|
15154
|
+
requestId
|
|
15155
|
+
});
|
|
15156
|
+
}
|
|
15157
|
+
}
|
|
15158
|
+
function handleWorktreeCreate(msg, ctx) {
|
|
15159
|
+
const { requestId, sourceRepoPath, branchName, baseRef } = msg;
|
|
15160
|
+
if (processedRequestIds.has(requestId)) return;
|
|
15161
|
+
processedRequestIds.add(requestId);
|
|
15162
|
+
const wtLog = createChildLogger({ mode: "worktree-create" });
|
|
15163
|
+
const roomJson = ctx.roomDoc.toJSON();
|
|
15164
|
+
const scriptEntry = roomJson?.userSettings?.worktreeScripts?.[sourceRepoPath];
|
|
15165
|
+
const setupScript = scriptEntry?.script ?? null;
|
|
15166
|
+
wtLog.info({ sourceRepoPath, branchName, baseRef }, "Starting worktree creation");
|
|
15167
|
+
runWorktreeCreate(requestId, sourceRepoPath, branchName, baseRef, setupScript, ctx, wtLog).catch(
|
|
15168
|
+
(err) => {
|
|
15169
|
+
wtLog.error({ err }, "Worktree create handler failed");
|
|
15170
|
+
}
|
|
15171
|
+
);
|
|
15172
|
+
}
|
|
15173
|
+
function monitorSetupChild(child, requestId, worktreePath, machineId, roomHandle, roomDoc, startedAt, log) {
|
|
15174
|
+
child.on("exit", (exitCode, signal) => {
|
|
15175
|
+
const status = exitCode === 0 ? "done" : "failed";
|
|
15176
|
+
log.info({ worktreePath, exitCode, signal, status }, "Setup script exited");
|
|
15177
|
+
change(roomDoc, (draft) => {
|
|
15178
|
+
draft.worktreeSetupStatus.set(worktreePath, {
|
|
15179
|
+
status,
|
|
15180
|
+
machineId,
|
|
15181
|
+
startedAt,
|
|
15182
|
+
completedAt: Date.now(),
|
|
15183
|
+
exitCode: exitCode ?? null,
|
|
15184
|
+
signal: signal ?? null,
|
|
15185
|
+
pid: child.pid ?? null
|
|
15186
|
+
});
|
|
15187
|
+
});
|
|
15188
|
+
try {
|
|
15189
|
+
roomHandle.worktreeSetupResps.set(requestId, {
|
|
15190
|
+
exitCode: exitCode ?? null,
|
|
15191
|
+
signal: signal ?? null,
|
|
15192
|
+
worktreePath
|
|
15193
|
+
});
|
|
15194
|
+
} catch (err) {
|
|
15195
|
+
log.warn({ err }, "Failed to publish setup result to ephemeral");
|
|
15196
|
+
}
|
|
15197
|
+
scheduleEphemeralCleanup(() => {
|
|
15198
|
+
roomHandle.worktreeSetupResps.delete(requestId);
|
|
15199
|
+
}, EPHEMERAL_CLEANUP_DELAY_MS);
|
|
15200
|
+
});
|
|
15201
|
+
child.on("error", (err) => {
|
|
15202
|
+
log.warn({ err: err.message }, "Setup script spawn error");
|
|
15203
|
+
change(roomDoc, (draft) => {
|
|
15204
|
+
draft.worktreeSetupStatus.set(worktreePath, {
|
|
15205
|
+
status: "failed",
|
|
15206
|
+
machineId,
|
|
15207
|
+
startedAt,
|
|
15208
|
+
completedAt: Date.now(),
|
|
15209
|
+
exitCode: null,
|
|
15210
|
+
signal: null,
|
|
15211
|
+
pid: child.pid ?? null
|
|
15212
|
+
});
|
|
15213
|
+
});
|
|
15214
|
+
roomHandle.worktreeSetupResps.set(requestId, {
|
|
15215
|
+
exitCode: null,
|
|
15216
|
+
signal: null,
|
|
15217
|
+
worktreePath
|
|
15218
|
+
});
|
|
15219
|
+
scheduleEphemeralCleanup(() => {
|
|
15220
|
+
roomHandle.worktreeSetupResps.delete(requestId);
|
|
15221
|
+
}, EPHEMERAL_CLEANUP_DELAY_MS);
|
|
15222
|
+
});
|
|
15223
|
+
child.unref();
|
|
15224
|
+
}
|
|
15225
|
+
async function runWorktreeCreate(requestId, sourceRepoPath, branchName, baseRef, setupScript, ctx, log) {
|
|
15226
|
+
try {
|
|
15227
|
+
const result = await createWorktree({
|
|
15228
|
+
sourceRepoPath,
|
|
15229
|
+
branchName,
|
|
15230
|
+
baseRef,
|
|
15231
|
+
setupScript,
|
|
15232
|
+
onProgress(step, detail) {
|
|
15233
|
+
ctx.connection.send({
|
|
15234
|
+
type: "worktree-create-progress",
|
|
15235
|
+
requestId,
|
|
15236
|
+
// eslint-disable-next-line no-restricted-syntax -- step string from createWorktree matches the schema enum
|
|
15237
|
+
step,
|
|
15238
|
+
detail
|
|
15239
|
+
});
|
|
15240
|
+
}
|
|
15241
|
+
});
|
|
15242
|
+
ctx.connection.send({
|
|
15243
|
+
type: "worktree-create-progress",
|
|
15244
|
+
requestId,
|
|
15245
|
+
step: "refreshing-environments",
|
|
15246
|
+
detail: "Re-detecting git environments"
|
|
15247
|
+
});
|
|
15248
|
+
try {
|
|
15249
|
+
const newEnvs = await detectEnvironments();
|
|
15250
|
+
ctx.capabilities.environments = newEnvs;
|
|
15251
|
+
ctx.publishCapabilities(ctx.capabilities);
|
|
15252
|
+
const repoMeta = await getRepoMetadata(result.worktreePath);
|
|
15253
|
+
if (repoMeta) {
|
|
15254
|
+
ctx.branchWatcher.addEnvironment(result.worktreePath, repoMeta.branch);
|
|
15255
|
+
}
|
|
15256
|
+
} catch (err) {
|
|
15257
|
+
log.warn({ err }, "Failed to refresh environments after worktree creation");
|
|
15258
|
+
}
|
|
15259
|
+
ctx.connection.send({
|
|
15260
|
+
type: "worktree-create-done",
|
|
15261
|
+
requestId,
|
|
15262
|
+
worktreePath: result.worktreePath,
|
|
15263
|
+
branchName: result.branchName,
|
|
15264
|
+
setupScriptStarted: result.setupScriptStarted,
|
|
15265
|
+
warnings: result.warnings.length > 0 ? result.warnings : void 0
|
|
15266
|
+
});
|
|
15267
|
+
if (result.setupChild) {
|
|
15268
|
+
const startedAt = Date.now();
|
|
15269
|
+
change(ctx.roomDoc, (draft) => {
|
|
15270
|
+
draft.worktreeSetupStatus.set(result.worktreePath, {
|
|
15271
|
+
status: "running",
|
|
15272
|
+
machineId: ctx.machineId,
|
|
15273
|
+
startedAt,
|
|
15274
|
+
completedAt: null,
|
|
15275
|
+
exitCode: null,
|
|
15276
|
+
signal: null,
|
|
15277
|
+
pid: result.setupChild?.pid ?? null
|
|
15278
|
+
});
|
|
15279
|
+
});
|
|
15280
|
+
monitorSetupChild(
|
|
15281
|
+
result.setupChild,
|
|
15282
|
+
requestId,
|
|
15283
|
+
result.worktreePath,
|
|
15284
|
+
ctx.machineId,
|
|
15285
|
+
ctx.roomHandle,
|
|
15286
|
+
ctx.roomDoc,
|
|
15287
|
+
startedAt,
|
|
15288
|
+
log
|
|
15289
|
+
);
|
|
15290
|
+
}
|
|
15291
|
+
log.info(
|
|
15292
|
+
{
|
|
15293
|
+
worktreePath: result.worktreePath,
|
|
15294
|
+
setupScriptStarted: result.setupScriptStarted,
|
|
15295
|
+
warningCount: result.warnings.length
|
|
15296
|
+
},
|
|
15297
|
+
"Worktree created successfully"
|
|
15298
|
+
);
|
|
15299
|
+
} catch (err) {
|
|
15300
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
15301
|
+
log.error({ err: message }, "Worktree creation failed");
|
|
15302
|
+
ctx.connection.send({
|
|
15303
|
+
type: "worktree-create-error",
|
|
15304
|
+
requestId,
|
|
15305
|
+
message
|
|
15306
|
+
});
|
|
15307
|
+
} finally {
|
|
15308
|
+
processedRequestIds.delete(requestId);
|
|
15309
|
+
}
|
|
15310
|
+
}
|
|
15311
|
+
var EPHEMERAL_CLEANUP_DELAY_MS = 5e3;
|
|
15312
|
+
async function runEnhancePromptEphemeral(prompt, requestId, abortController, roomHandle, log) {
|
|
15313
|
+
let fullText = "";
|
|
15314
|
+
try {
|
|
15315
|
+
const response = query2({
|
|
15316
|
+
prompt,
|
|
15317
|
+
options: {
|
|
15318
|
+
maxTurns: 1,
|
|
15319
|
+
allowedTools: [],
|
|
15320
|
+
systemPrompt: ENHANCE_SYSTEM_PROMPT,
|
|
15321
|
+
abortController
|
|
15322
|
+
}
|
|
15323
|
+
});
|
|
15324
|
+
for await (const message of response) {
|
|
15325
|
+
if (message.type !== "assistant") continue;
|
|
15326
|
+
for (const text of extractTextChunks(message.message.content)) {
|
|
15327
|
+
fullText += text;
|
|
15328
|
+
roomHandle.enhancePromptResps.set(requestId, {
|
|
15329
|
+
status: "streaming",
|
|
15330
|
+
text: fullText,
|
|
15331
|
+
error: null
|
|
15332
|
+
});
|
|
15333
|
+
}
|
|
15334
|
+
}
|
|
15335
|
+
roomHandle.enhancePromptResps.set(requestId, {
|
|
15336
|
+
status: "done",
|
|
15337
|
+
text: fullText,
|
|
15338
|
+
error: null
|
|
15339
|
+
});
|
|
15340
|
+
log.info(
|
|
15341
|
+
{ promptLen: prompt.length, resultLen: fullText.length },
|
|
15342
|
+
"Prompt enhanced via ephemeral"
|
|
15343
|
+
);
|
|
15344
|
+
} catch (err) {
|
|
15345
|
+
if (abortController.signal.aborted) {
|
|
15346
|
+
log.warn("Prompt enhancement aborted (ephemeral)");
|
|
15347
|
+
} else {
|
|
15348
|
+
log.error({ err }, "Prompt enhancement failed (ephemeral)");
|
|
15349
|
+
}
|
|
15350
|
+
const errorMessage = err instanceof Error ? err.message : "Prompt enhancement failed";
|
|
15351
|
+
roomHandle.enhancePromptResps.set(requestId, {
|
|
15352
|
+
status: "error",
|
|
15353
|
+
text: "",
|
|
15354
|
+
error: errorMessage
|
|
15355
|
+
});
|
|
15356
|
+
} finally {
|
|
15357
|
+
scheduleEphemeralCleanup(() => {
|
|
15358
|
+
roomHandle.enhancePromptReqs.delete(requestId);
|
|
15359
|
+
roomHandle.enhancePromptResps.delete(requestId);
|
|
15360
|
+
}, EPHEMERAL_CLEANUP_DELAY_MS);
|
|
15361
|
+
}
|
|
15362
|
+
}
|
|
15363
|
+
async function runWorktreeCreateEphemeral(requestId, sourceRepoPath, branchName, baseRef, setupScript, roomHandle, roomDoc, localMachineId, caps, publishCaps, watcher, log) {
|
|
15364
|
+
try {
|
|
15365
|
+
const result = await createWorktree({
|
|
15366
|
+
sourceRepoPath,
|
|
15367
|
+
branchName,
|
|
15368
|
+
baseRef,
|
|
15369
|
+
setupScript,
|
|
15370
|
+
onProgress(step, detail) {
|
|
15371
|
+
roomHandle.worktreeCreateResps.set(requestId, {
|
|
15372
|
+
// eslint-disable-next-line no-restricted-syntax -- step string from createWorktree matches the schema enum
|
|
15373
|
+
status: step,
|
|
15374
|
+
detail: detail ?? null,
|
|
15375
|
+
worktreePath: null,
|
|
15376
|
+
branchName: null,
|
|
15377
|
+
setupScriptStarted: null,
|
|
15378
|
+
warnings: null,
|
|
15379
|
+
error: null
|
|
15380
|
+
});
|
|
15381
|
+
}
|
|
15382
|
+
});
|
|
15383
|
+
roomHandle.worktreeCreateResps.set(requestId, {
|
|
15384
|
+
status: "refreshing-environments",
|
|
15385
|
+
detail: "Re-detecting git environments",
|
|
15386
|
+
worktreePath: null,
|
|
15387
|
+
branchName: null,
|
|
15388
|
+
setupScriptStarted: null,
|
|
15389
|
+
warnings: null,
|
|
15390
|
+
error: null
|
|
15391
|
+
});
|
|
15392
|
+
try {
|
|
15393
|
+
const newEnvs = await detectEnvironments();
|
|
15394
|
+
caps.environments = newEnvs;
|
|
15395
|
+
publishCaps(caps);
|
|
15396
|
+
const repoMeta = await getRepoMetadata(result.worktreePath);
|
|
15397
|
+
if (repoMeta) {
|
|
15398
|
+
watcher.addEnvironment(result.worktreePath, repoMeta.branch);
|
|
15399
|
+
}
|
|
15400
|
+
} catch (err) {
|
|
15401
|
+
log.warn({ err }, "Failed to refresh environments after worktree creation (ephemeral)");
|
|
15402
|
+
}
|
|
15403
|
+
roomHandle.worktreeCreateResps.set(requestId, {
|
|
15404
|
+
status: "done",
|
|
15405
|
+
detail: null,
|
|
15406
|
+
worktreePath: result.worktreePath,
|
|
15407
|
+
branchName: result.branchName,
|
|
15408
|
+
setupScriptStarted: result.setupScriptStarted,
|
|
15409
|
+
warnings: result.warnings.length > 0 ? result.warnings : null,
|
|
15410
|
+
error: null
|
|
15411
|
+
});
|
|
15412
|
+
if (result.setupChild) {
|
|
15413
|
+
const startedAt = Date.now();
|
|
15414
|
+
change(roomDoc, (draft) => {
|
|
15415
|
+
draft.worktreeSetupStatus.set(result.worktreePath, {
|
|
15416
|
+
status: "running",
|
|
15417
|
+
machineId: localMachineId,
|
|
15418
|
+
startedAt,
|
|
15419
|
+
completedAt: null,
|
|
15420
|
+
exitCode: null,
|
|
15421
|
+
signal: null,
|
|
15422
|
+
pid: result.setupChild?.pid ?? null
|
|
15423
|
+
});
|
|
15424
|
+
});
|
|
15425
|
+
monitorSetupChild(
|
|
15426
|
+
result.setupChild,
|
|
15427
|
+
requestId,
|
|
15428
|
+
result.worktreePath,
|
|
15429
|
+
localMachineId,
|
|
15430
|
+
roomHandle,
|
|
15431
|
+
roomDoc,
|
|
15432
|
+
startedAt,
|
|
15433
|
+
log
|
|
15434
|
+
);
|
|
15435
|
+
}
|
|
15436
|
+
log.info(
|
|
15437
|
+
{
|
|
15438
|
+
worktreePath: result.worktreePath,
|
|
15439
|
+
setupScriptStarted: result.setupScriptStarted,
|
|
15440
|
+
warningCount: result.warnings.length
|
|
15441
|
+
},
|
|
15442
|
+
"Worktree created successfully via ephemeral"
|
|
15443
|
+
);
|
|
15444
|
+
} catch (err) {
|
|
15445
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
15446
|
+
log.error({ err: message }, "Worktree creation failed (ephemeral)");
|
|
15447
|
+
roomHandle.worktreeCreateResps.set(requestId, {
|
|
15448
|
+
status: "error",
|
|
15449
|
+
detail: null,
|
|
15450
|
+
worktreePath: null,
|
|
15451
|
+
branchName: null,
|
|
15452
|
+
setupScriptStarted: null,
|
|
15453
|
+
warnings: null,
|
|
15454
|
+
error: message
|
|
15455
|
+
});
|
|
15456
|
+
} finally {
|
|
15457
|
+
scheduleEphemeralCleanup(() => {
|
|
15458
|
+
roomHandle.worktreeCreateReqs.delete(requestId);
|
|
15459
|
+
roomHandle.worktreeCreateResps.delete(requestId);
|
|
15460
|
+
}, EPHEMERAL_CLEANUP_DELAY_MS);
|
|
15461
|
+
}
|
|
15462
|
+
}
|
|
13554
15463
|
async function watchTaskDocument(taskId, taskLog, ctx) {
|
|
13555
15464
|
const epoch = DEFAULT_EPOCH;
|
|
13556
15465
|
const taskDocId = buildDocumentId("task", taskId, epoch);
|
|
@@ -13561,6 +15470,14 @@ async function watchTaskDocument(taskId, taskLog, ctx) {
|
|
|
13561
15470
|
} catch {
|
|
13562
15471
|
taskLog.debug({ taskDocId }, "No existing task data in storage");
|
|
13563
15472
|
}
|
|
15473
|
+
try {
|
|
15474
|
+
await taskHandle.waitForSync({ kind: "network", timeout: 3e3 });
|
|
15475
|
+
} catch {
|
|
15476
|
+
taskLog.debug({ taskDocId }, "Network sync timed out (browser may not be connected yet)");
|
|
15477
|
+
}
|
|
15478
|
+
if (recoverOrphanedTask(taskHandle.doc, taskLog)) {
|
|
15479
|
+
updateTaskInIndex(ctx.roomDoc, taskId, { status: "failed", updatedAt: Date.now() });
|
|
15480
|
+
}
|
|
13564
15481
|
const json = taskHandle.doc.toJSON();
|
|
13565
15482
|
const lastUserMsg = [...json.conversation].reverse().find((m) => m.role === "user");
|
|
13566
15483
|
const initialCwd = lastUserMsg?.cwd ?? process.cwd();
|
|
@@ -13571,10 +15488,10 @@ async function watchTaskDocument(taskId, taskLog, ctx) {
|
|
|
13571
15488
|
taskLog.info({ taskDocId, opCount: opCountBefore }, "Doc state before subscribe");
|
|
13572
15489
|
const unsubscribe = taskHandle.subscribe((event) => {
|
|
13573
15490
|
taskLog.info({ taskDocId, eventBy: event.by }, "Subscription event received");
|
|
13574
|
-
if (event.by === "local") return;
|
|
13575
15491
|
onTaskDocChanged(taskId, taskHandle, taskLog, ctx);
|
|
13576
15492
|
});
|
|
13577
15493
|
ctx.watchedTasks.set(taskId, unsubscribe);
|
|
15494
|
+
ctx.taskHandles.set(taskId, taskHandle);
|
|
13578
15495
|
taskLog.info({ taskDocId }, "Subscribed to task document changes");
|
|
13579
15496
|
const opCountAfter = taskHandle.loroDoc.opCount();
|
|
13580
15497
|
taskLog.info({ taskDocId, opCount: opCountAfter }, "Doc state after subscribe");
|
|
@@ -13587,17 +15504,25 @@ async function watchTaskDocument(taskId, taskLog, ctx) {
|
|
|
13587
15504
|
onTaskDocChanged(taskId, taskHandle, taskLog, ctx);
|
|
13588
15505
|
}
|
|
13589
15506
|
}
|
|
13590
|
-
function handleFollowUp(activeTask, taskLog) {
|
|
15507
|
+
function handleFollowUp(activeTask, conversationLen, taskLog) {
|
|
13591
15508
|
if (!activeTask) return;
|
|
15509
|
+
if (conversationLen <= activeTask.lastDispatchedConvLen) {
|
|
15510
|
+
taskLog.debug(
|
|
15511
|
+
{ conversationLen, lastDispatched: activeTask.lastDispatchedConvLen },
|
|
15512
|
+
"Conversation unchanged since last dispatch, skipping duplicate"
|
|
15513
|
+
);
|
|
15514
|
+
return;
|
|
15515
|
+
}
|
|
13592
15516
|
if (!activeTask.sessionManager.isStreaming) {
|
|
13593
15517
|
taskLog.debug("Task already running but not streaming, skipping");
|
|
13594
15518
|
return;
|
|
13595
15519
|
}
|
|
13596
|
-
const
|
|
13597
|
-
if (
|
|
15520
|
+
const contentBlocks = activeTask.sessionManager.getLatestUserContentBlocks();
|
|
15521
|
+
if (contentBlocks && contentBlocks.length > 0) {
|
|
13598
15522
|
try {
|
|
13599
15523
|
taskLog.info("Sending follow-up to active streaming session");
|
|
13600
|
-
activeTask.
|
|
15524
|
+
activeTask.lastDispatchedConvLen = conversationLen;
|
|
15525
|
+
activeTask.sessionManager.sendFollowUp(contentBlocks);
|
|
13601
15526
|
} catch (err) {
|
|
13602
15527
|
taskLog.warn({ err }, "Failed to send follow-up to streaming session");
|
|
13603
15528
|
}
|
|
@@ -13619,7 +15544,7 @@ function onTaskDocChanged(taskId, taskHandle, taskLog, ctx) {
|
|
|
13619
15544
|
const conversation2 = json.conversation;
|
|
13620
15545
|
const lastMessage2 = conversation2[conversation2.length - 1];
|
|
13621
15546
|
if (lastMessage2?.role === "user") {
|
|
13622
|
-
handleFollowUp(ctx.activeTasks.get(taskId), taskLog);
|
|
15547
|
+
handleFollowUp(ctx.activeTasks.get(taskId), conversation2.length, taskLog);
|
|
13623
15548
|
}
|
|
13624
15549
|
const activeLastUserMsg = [...conversation2].reverse().find((m) => m.role === "user");
|
|
13625
15550
|
const activeCwd = activeLastUserMsg?.cwd ?? process.cwd();
|
|
@@ -13650,7 +15575,12 @@ function onTaskDocChanged(taskId, taskHandle, taskLog, ctx) {
|
|
|
13650
15575
|
updateTaskInIndex(ctx.roomDoc, taskId, { status, updatedAt: Date.now() });
|
|
13651
15576
|
};
|
|
13652
15577
|
const manager = new SessionManager(doc, onStatusChange);
|
|
13653
|
-
const activeTask = {
|
|
15578
|
+
const activeTask = {
|
|
15579
|
+
taskId,
|
|
15580
|
+
abortController,
|
|
15581
|
+
sessionManager: manager,
|
|
15582
|
+
lastDispatchedConvLen: conversation.length
|
|
15583
|
+
};
|
|
13654
15584
|
ctx.activeTasks.set(taskId, activeTask);
|
|
13655
15585
|
ctx.signaling.updateStatus("running", taskId);
|
|
13656
15586
|
const turnStartRefPromise = captureTreeSnapshot(cwd);
|
|
@@ -13689,6 +15619,8 @@ function onTaskDocChanged(taskId, taskHandle, taskLog, ctx) {
|
|
|
13689
15619
|
turnStartRefPromise,
|
|
13690
15620
|
abortController,
|
|
13691
15621
|
ctx
|
|
15622
|
+
}).catch((cleanupErr) => {
|
|
15623
|
+
taskLog.warn({ err: cleanupErr }, "cleanupTaskRun failed");
|
|
13692
15624
|
})
|
|
13693
15625
|
);
|
|
13694
15626
|
}
|
|
@@ -13722,11 +15654,6 @@ async function cleanupTaskRun(opts) {
|
|
|
13722
15654
|
taskHandle.permResps.delete(key);
|
|
13723
15655
|
}
|
|
13724
15656
|
ctx.activeTasks.delete(taskId);
|
|
13725
|
-
const unsub = ctx.watchedTasks.get(taskId);
|
|
13726
|
-
if (unsub) {
|
|
13727
|
-
unsub();
|
|
13728
|
-
ctx.watchedTasks.delete(taskId);
|
|
13729
|
-
}
|
|
13730
15657
|
ctx.signaling.updateStatus("idle");
|
|
13731
15658
|
}
|
|
13732
15659
|
function clearDebouncedTimer(timers, taskId) {
|
|
@@ -13763,6 +15690,44 @@ function toPermissionResult(decision, input, suggestions, message) {
|
|
|
13763
15690
|
message: message ?? "User denied permission"
|
|
13764
15691
|
};
|
|
13765
15692
|
}
|
|
15693
|
+
function resolveExitPlanMode(taskHandle, taskLog, toolUseID, value) {
|
|
15694
|
+
const plans = taskHandle.doc.toJSON().plans;
|
|
15695
|
+
const planIndex = plans.findIndex((p) => p.toolUseId === toolUseID);
|
|
15696
|
+
if (planIndex < 0) return;
|
|
15697
|
+
const plan = plans[planIndex];
|
|
15698
|
+
if (!plan) return;
|
|
15699
|
+
const reviewStatus = value.decision === "approved" ? "approved" : "changes-requested";
|
|
15700
|
+
const editedMarkdown = serializePlanEditorDoc(taskHandle.loroDoc, plan.planId);
|
|
15701
|
+
const allComments = taskHandle.doc.toJSON().planComments;
|
|
15702
|
+
const planComments = Object.values(allComments).filter(
|
|
15703
|
+
(c) => c.planId === plan.planId && c.resolvedAt === null
|
|
15704
|
+
);
|
|
15705
|
+
const richFeedback = formatPlanFeedbackForClaudeCode(
|
|
15706
|
+
plan.markdown,
|
|
15707
|
+
editedMarkdown || plan.markdown,
|
|
15708
|
+
planComments,
|
|
15709
|
+
value.message ?? null
|
|
15710
|
+
);
|
|
15711
|
+
change(taskHandle.doc, (draft) => {
|
|
15712
|
+
const draftPlan = draft.plans.get(planIndex);
|
|
15713
|
+
if (draftPlan) {
|
|
15714
|
+
draftPlan.reviewStatus = reviewStatus;
|
|
15715
|
+
draftPlan.reviewFeedback = (richFeedback || value.message) ?? null;
|
|
15716
|
+
}
|
|
15717
|
+
});
|
|
15718
|
+
if (richFeedback) {
|
|
15719
|
+
value.message = richFeedback;
|
|
15720
|
+
}
|
|
15721
|
+
taskLog.info(
|
|
15722
|
+
{
|
|
15723
|
+
toolUseID,
|
|
15724
|
+
reviewStatus,
|
|
15725
|
+
hasFeedback: !!richFeedback,
|
|
15726
|
+
hasEdits: editedMarkdown !== plan.markdown
|
|
15727
|
+
},
|
|
15728
|
+
"Updated plan reviewStatus in CRDT with rich feedback"
|
|
15729
|
+
);
|
|
15730
|
+
}
|
|
13766
15731
|
function resolvePermissionResponse(ctx) {
|
|
13767
15732
|
const { taskHandle, roomDoc, taskId, taskLog, toolName, toolUseID, input, suggestions, value } = ctx;
|
|
13768
15733
|
taskHandle.permReqs.delete(toolUseID);
|
|
@@ -13773,22 +15738,7 @@ function resolvePermissionResponse(ctx) {
|
|
|
13773
15738
|
});
|
|
13774
15739
|
updateTaskInIndex(roomDoc, taskId, { status: "working", updatedAt: Date.now() });
|
|
13775
15740
|
if (toolName === "ExitPlanMode") {
|
|
13776
|
-
|
|
13777
|
-
const planIndex = plans.findIndex((p) => p.toolUseId === toolUseID);
|
|
13778
|
-
if (planIndex >= 0) {
|
|
13779
|
-
const reviewStatus = value.decision === "approved" ? "approved" : "changes-requested";
|
|
13780
|
-
change(taskHandle.doc, (draft) => {
|
|
13781
|
-
const plan = draft.plans.get(planIndex);
|
|
13782
|
-
if (plan) {
|
|
13783
|
-
plan.reviewStatus = reviewStatus;
|
|
13784
|
-
plan.reviewFeedback = value.message ?? null;
|
|
13785
|
-
}
|
|
13786
|
-
});
|
|
13787
|
-
taskLog.info(
|
|
13788
|
-
{ toolUseID, reviewStatus, hasFeedback: !!value.message },
|
|
13789
|
-
"Updated plan reviewStatus in CRDT"
|
|
13790
|
-
);
|
|
13791
|
-
}
|
|
15741
|
+
resolveExitPlanMode(taskHandle, taskLog, toolUseID, value);
|
|
13792
15742
|
}
|
|
13793
15743
|
taskLog.info(
|
|
13794
15744
|
{
|
|
@@ -13837,19 +15787,35 @@ function buildCanUseTool(taskHandle, taskLog, roomDoc, taskId) {
|
|
|
13837
15787
|
"Permission request sent to browser"
|
|
13838
15788
|
);
|
|
13839
15789
|
return new Promise((resolve3) => {
|
|
15790
|
+
const PERMISSION_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
15791
|
+
let settled = false;
|
|
13840
15792
|
let unsub;
|
|
13841
|
-
const
|
|
15793
|
+
const settle = (result) => {
|
|
15794
|
+
if (settled) return;
|
|
15795
|
+
settled = true;
|
|
15796
|
+
clearTimeout(timeout);
|
|
13842
15797
|
unsub?.();
|
|
15798
|
+
signal.removeEventListener("abort", onAbort);
|
|
13843
15799
|
taskHandle.permReqs.delete(toolUseID);
|
|
13844
|
-
|
|
15800
|
+
change(taskHandle.doc, (draft) => {
|
|
15801
|
+
draft.meta.status = "working";
|
|
15802
|
+
draft.meta.updatedAt = Date.now();
|
|
15803
|
+
});
|
|
15804
|
+
updateTaskInIndex(roomDoc, taskId, { status: "working", updatedAt: Date.now() });
|
|
15805
|
+
resolve3(result);
|
|
15806
|
+
};
|
|
15807
|
+
const timeout = setTimeout(() => {
|
|
15808
|
+
taskLog.warn({ toolName, toolUseID }, "Permission request timed out");
|
|
15809
|
+
settle({ behavior: "deny", message: "Permission request timed out" });
|
|
15810
|
+
}, PERMISSION_TIMEOUT_MS);
|
|
15811
|
+
const onAbort = () => {
|
|
15812
|
+
settle({ behavior: "deny", message: "Task was aborted" });
|
|
13845
15813
|
};
|
|
13846
15814
|
signal.addEventListener("abort", onAbort, { once: true });
|
|
13847
15815
|
unsub = taskHandle.permResps.subscribe(({ key, value, source }) => {
|
|
13848
15816
|
if (source === "local") return;
|
|
13849
15817
|
if (key !== toolUseID || !value) return;
|
|
13850
|
-
|
|
13851
|
-
signal.removeEventListener("abort", onAbort);
|
|
13852
|
-
resolve3(
|
|
15818
|
+
settle(
|
|
13853
15819
|
resolvePermissionResponse({
|
|
13854
15820
|
taskHandle,
|
|
13855
15821
|
roomDoc,
|
|
@@ -13880,11 +15846,16 @@ async function runTask(opts) {
|
|
|
13880
15846
|
abortController,
|
|
13881
15847
|
log
|
|
13882
15848
|
} = opts;
|
|
13883
|
-
const
|
|
13884
|
-
if (!
|
|
15849
|
+
const contentBlocks = manager.getLatestUserContentBlocks();
|
|
15850
|
+
if (!contentBlocks || contentBlocks.length === 0) {
|
|
13885
15851
|
throw new Error(`No user message found in task ${taskId}`);
|
|
13886
15852
|
}
|
|
13887
|
-
|
|
15853
|
+
const textPreview = contentBlocks.filter((b) => b.type === "text").map((b) => b.text).join(" ").slice(0, 100);
|
|
15854
|
+
const imageCount = contentBlocks.filter((b) => b.type === "image").length;
|
|
15855
|
+
log.info(
|
|
15856
|
+
{ prompt: textPreview || "(images only)", imageCount },
|
|
15857
|
+
"Running task with prompt from CRDT"
|
|
15858
|
+
);
|
|
13888
15859
|
const canUseTool = permissionMode === "bypassPermissions" ? void 0 : buildCanUseTool(taskHandle, log, roomDoc, taskId);
|
|
13889
15860
|
const stderr = (data) => {
|
|
13890
15861
|
const trimmed = data.trim();
|
|
@@ -13898,7 +15869,7 @@ async function runTask(opts) {
|
|
|
13898
15869
|
const resumeInfo = manager.shouldResume();
|
|
13899
15870
|
if (resumeInfo.resume && resumeInfo.sessionId) {
|
|
13900
15871
|
log.info({ sessionId: resumeInfo.sessionId }, "Resuming existing session");
|
|
13901
|
-
return manager.resumeSession(resumeInfo.sessionId,
|
|
15872
|
+
return manager.resumeSession(resumeInfo.sessionId, contentBlocks, {
|
|
13902
15873
|
abortController,
|
|
13903
15874
|
machineId,
|
|
13904
15875
|
model,
|
|
@@ -13910,7 +15881,7 @@ async function runTask(opts) {
|
|
|
13910
15881
|
});
|
|
13911
15882
|
}
|
|
13912
15883
|
return manager.createSession({
|
|
13913
|
-
prompt,
|
|
15884
|
+
prompt: contentBlocks,
|
|
13914
15885
|
cwd,
|
|
13915
15886
|
machineId,
|
|
13916
15887
|
model,
|
|
@@ -13962,8 +15933,12 @@ function parseCliArgs() {
|
|
|
13962
15933
|
" -s, --serve Run in serve mode (signaling + spawn-agent)",
|
|
13963
15934
|
" -h, --help Show this help",
|
|
13964
15935
|
"",
|
|
15936
|
+
"Authentication:",
|
|
15937
|
+
" Run `claude auth login` to authenticate with Anthropic (primary method).",
|
|
15938
|
+
" Alternatively, set ANTHROPIC_API_KEY for CI/headless environments.",
|
|
15939
|
+
"",
|
|
13965
15940
|
"Environment:",
|
|
13966
|
-
" ANTHROPIC_API_KEY API key for Claude (
|
|
15941
|
+
" ANTHROPIC_API_KEY API key for Claude (optional, overrides OAuth)",
|
|
13967
15942
|
" SHIPYARD_DEV Set to 1 for dev mode (uses ~/.shipyard-dev/)",
|
|
13968
15943
|
" SHIPYARD_DATA_DIR Data directory (overridden by --data-dir)",
|
|
13969
15944
|
" LOG_LEVEL Log level: debug, info, warn, error (default: info)",
|
|
@@ -13994,7 +15969,7 @@ async function setupSignaling(env, log) {
|
|
|
13994
15969
|
return handle;
|
|
13995
15970
|
}
|
|
13996
15971
|
async function setupRepo(dataDir) {
|
|
13997
|
-
await
|
|
15972
|
+
await mkdir4(dataDir, { recursive: true, mode: 448 });
|
|
13998
15973
|
const storage = new FileStorageAdapter(dataDir);
|
|
13999
15974
|
const repo = new Repo({
|
|
14000
15975
|
identity: { name: "shipyard-daemon" },
|
|
@@ -14061,13 +16036,13 @@ function handleResult(log, result, startTime) {
|
|
|
14061
16036
|
async function handleSubcommand() {
|
|
14062
16037
|
const subcommand = process.argv[2];
|
|
14063
16038
|
if (subcommand === "login") {
|
|
14064
|
-
const { loginCommand } = await import("./login-
|
|
16039
|
+
const { loginCommand } = await import("./login-625HP2EN.js");
|
|
14065
16040
|
const hasCheck = process.argv.includes("--check");
|
|
14066
16041
|
await loginCommand({ check: hasCheck });
|
|
14067
16042
|
return true;
|
|
14068
16043
|
}
|
|
14069
16044
|
if (subcommand === "logout") {
|
|
14070
|
-
const { logoutCommand } = await import("./logout-
|
|
16045
|
+
const { logoutCommand } = await import("./logout-L6EMSGQU.js");
|
|
14071
16046
|
await logoutCommand();
|
|
14072
16047
|
return true;
|
|
14073
16048
|
}
|
|
@@ -14075,7 +16050,7 @@ async function handleSubcommand() {
|
|
|
14075
16050
|
}
|
|
14076
16051
|
async function loadAuthFromConfig(env) {
|
|
14077
16052
|
if (env.SHIPYARD_USER_TOKEN) return;
|
|
14078
|
-
const { loadAuthToken } = await import("./auth-
|
|
16053
|
+
const { loadAuthToken } = await import("./auth-YRHZ3SMD.js");
|
|
14079
16054
|
const auth = await loadAuthToken();
|
|
14080
16055
|
if (auth.status === "ok") {
|
|
14081
16056
|
env.SHIPYARD_USER_TOKEN = auth.token;
|
|
@@ -14091,15 +16066,11 @@ async function loadAuthFromConfig(env) {
|
|
|
14091
16066
|
}
|
|
14092
16067
|
logger.warn("No auth token found. Run `shipyard login` to authenticate.");
|
|
14093
16068
|
}
|
|
14094
|
-
function validateTaskArgs(args
|
|
16069
|
+
function validateTaskArgs(args) {
|
|
14095
16070
|
if (!args.prompt && !args.resume) {
|
|
14096
16071
|
logger.error("Either --prompt, --resume, or --serve is required. Use --help for usage.");
|
|
14097
16072
|
process.exit(1);
|
|
14098
16073
|
}
|
|
14099
|
-
if (!env.ANTHROPIC_API_KEY) {
|
|
14100
|
-
logger.error("ANTHROPIC_API_KEY is required when running tasks. Use --help for usage.");
|
|
14101
|
-
process.exit(1);
|
|
14102
|
-
}
|
|
14103
16074
|
}
|
|
14104
16075
|
function createCleanup(signalingHandle, lifecycle, repo) {
|
|
14105
16076
|
let cleanedUp = false;
|
|
@@ -14126,13 +16097,14 @@ async function main() {
|
|
|
14126
16097
|
if (args.serve) {
|
|
14127
16098
|
return serve(env);
|
|
14128
16099
|
}
|
|
14129
|
-
validateTaskArgs(args
|
|
16100
|
+
validateTaskArgs(args);
|
|
14130
16101
|
const dataDir = resolve2(args.dataDir ?? env.SHIPYARD_DATA_DIR.replace("~", homedir3()));
|
|
14131
16102
|
const taskId = args.taskId ?? generateTaskId();
|
|
14132
16103
|
const log = createChildLogger({ taskId });
|
|
14133
16104
|
log.info({ dataDir, prompt: args.prompt, resume: args.resume }, "Starting daemon");
|
|
14134
16105
|
const repo = await setupRepo(dataDir);
|
|
14135
16106
|
const lifecycle = new LifecycleManager();
|
|
16107
|
+
await lifecycle.acquirePidFile(getShipyardHome());
|
|
14136
16108
|
const signalingHandle = await setupSignaling(env, log);
|
|
14137
16109
|
const cleanup = createCleanup(signalingHandle, lifecycle, repo);
|
|
14138
16110
|
lifecycle.onShutdown(async () => {
|