@schoolai/shipyard-mcp 0.3.2-next.523 → 0.3.2-next.527
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/README.md +6 -6
- package/apps/hook/dist/index.cjs +58 -60
- package/apps/server/dist/{chunk-2MIAN4VT.js → chunk-HFZCBGQ3.js} +85 -3
- package/apps/server/dist/{chunk-NNJ3ELQW.js → chunk-QDYV6R7L.js} +2942 -1250
- package/apps/server/dist/index.js +253 -518
- package/apps/server/dist/{input-request-manager-OSA7HKQW.js → input-request-manager-QKHCEJUU.js} +3 -8
- package/apps/server/dist/{session-registry-3ZNR5KYN.js → session-registry-CBDXMXY3.js} +1 -3
- package/package.json +1 -1
- package/apps/server/dist/chunk-4G3Y65O4.js +0 -60
- package/apps/server/dist/chunk-JSBRDJBE.js +0 -30
- package/apps/server/dist/chunk-SPYUFJJK.js +0 -212
- package/apps/server/dist/chunk-WLIAQMMT.js +0 -2779
- package/apps/server/dist/dist-STCVHI7H.js +0 -497
- package/apps/server/dist/server-identity-LMOARXHL.js +0 -14
|
@@ -1,7 +1,19 @@
|
|
|
1
1
|
import {
|
|
2
2
|
__commonJS,
|
|
3
|
-
__toESM
|
|
4
|
-
|
|
3
|
+
__toESM,
|
|
4
|
+
assertNever,
|
|
5
|
+
getSessionIdByPlanId,
|
|
6
|
+
getSessionState,
|
|
7
|
+
getSessionStateByPlanId,
|
|
8
|
+
isSessionStateApproved,
|
|
9
|
+
isSessionStateApprovedAwaitingToken,
|
|
10
|
+
isSessionStateReviewed,
|
|
11
|
+
isSessionStateSynced,
|
|
12
|
+
loadEnv,
|
|
13
|
+
logger,
|
|
14
|
+
setSessionState,
|
|
15
|
+
startPeriodicCleanup
|
|
16
|
+
} from "./chunk-HFZCBGQ3.js";
|
|
5
17
|
|
|
6
18
|
// ../../node_modules/.pnpm/lz-string@1.5.0/node_modules/lz-string/libs/lz-string.js
|
|
7
19
|
var require_lz_string = __commonJS({
|
|
@@ -306,7 +318,7 @@ var require_lz_string = __commonJS({
|
|
|
306
318
|
return compressed.charCodeAt(index);
|
|
307
319
|
});
|
|
308
320
|
},
|
|
309
|
-
_decompress: function(
|
|
321
|
+
_decompress: function(length2, resetValue, getNextValue) {
|
|
310
322
|
var dictionary = [], next, enlargeIn = 4, dictSize = 4, numBits = 3, entry = "", result = [], i, w, bits, resb, maxpower, power, c, data = { val: getNextValue(0), position: resetValue, index: 1 };
|
|
311
323
|
for (i = 0; i < 3; i += 1) {
|
|
312
324
|
dictionary[i] = i;
|
|
@@ -364,7 +376,7 @@ var require_lz_string = __commonJS({
|
|
|
364
376
|
w = c;
|
|
365
377
|
result.push(c);
|
|
366
378
|
while (true) {
|
|
367
|
-
if (data.index >
|
|
379
|
+
if (data.index > length2) {
|
|
368
380
|
return "";
|
|
369
381
|
}
|
|
370
382
|
bits = 0;
|
|
@@ -470,12 +482,6 @@ var PlanStatusValues = [
|
|
|
470
482
|
"in_progress",
|
|
471
483
|
"completed"
|
|
472
484
|
];
|
|
473
|
-
var PlanViewTabValues = [
|
|
474
|
-
"plan",
|
|
475
|
-
"activity",
|
|
476
|
-
"deliverables",
|
|
477
|
-
"changes"
|
|
478
|
-
];
|
|
479
485
|
var OriginPlatformValues = [
|
|
480
486
|
"claude-code",
|
|
481
487
|
"devin",
|
|
@@ -532,38 +538,6 @@ var ConversationVersionSchema = z.discriminatedUnion("handedOff", [ConversationV
|
|
|
532
538
|
handedOffAt: z.number(),
|
|
533
539
|
handedOffTo: z.string()
|
|
534
540
|
})]);
|
|
535
|
-
var PlanEventTypes = [
|
|
536
|
-
"plan_created",
|
|
537
|
-
"status_changed",
|
|
538
|
-
"comment_added",
|
|
539
|
-
"comment_resolved",
|
|
540
|
-
"artifact_uploaded",
|
|
541
|
-
"deliverable_linked",
|
|
542
|
-
"pr_linked",
|
|
543
|
-
"content_edited",
|
|
544
|
-
"approved",
|
|
545
|
-
"changes_requested",
|
|
546
|
-
"completed",
|
|
547
|
-
"conversation_imported",
|
|
548
|
-
"conversation_handed_off",
|
|
549
|
-
"step_completed",
|
|
550
|
-
"plan_archived",
|
|
551
|
-
"plan_unarchived",
|
|
552
|
-
"conversation_exported",
|
|
553
|
-
"plan_shared",
|
|
554
|
-
"approval_requested",
|
|
555
|
-
"input_request_created",
|
|
556
|
-
"input_request_answered",
|
|
557
|
-
"input_request_declined",
|
|
558
|
-
"agent_activity",
|
|
559
|
-
"session_token_regenerated"
|
|
560
|
-
];
|
|
561
|
-
var AgentActivityTypes = [
|
|
562
|
-
"help_request",
|
|
563
|
-
"help_request_resolved",
|
|
564
|
-
"blocker",
|
|
565
|
-
"blocker_resolved"
|
|
566
|
-
];
|
|
567
541
|
var PlanEventBaseSchema = z.object({
|
|
568
542
|
id: z.string(),
|
|
569
543
|
actor: z.string(),
|
|
@@ -728,13 +702,6 @@ var PlanEventSchema = z.discriminatedUnion("type", [
|
|
|
728
702
|
}),
|
|
729
703
|
PlanEventBaseSchema.extend({ type: z.literal("session_token_regenerated") })
|
|
730
704
|
]);
|
|
731
|
-
function isInboxWorthy(event, username, ownerId) {
|
|
732
|
-
if (!event.inboxWorthy) return false;
|
|
733
|
-
if (!event.inboxFor) return true;
|
|
734
|
-
const resolvedInboxFor = event.inboxFor === "owner" && ownerId ? ownerId : event.inboxFor;
|
|
735
|
-
if (Array.isArray(resolvedInboxFor)) return resolvedInboxFor.includes(username);
|
|
736
|
-
return resolvedInboxFor === username;
|
|
737
|
-
}
|
|
738
705
|
var PlanMetadataBaseSchema = z.object({
|
|
739
706
|
id: z.string(),
|
|
740
707
|
title: z.string(),
|
|
@@ -800,9 +767,6 @@ var LocalArtifactSchema = BaseArtifactSchema.extend({
|
|
|
800
767
|
localArtifactId: z.string()
|
|
801
768
|
});
|
|
802
769
|
var ArtifactSchema = z.discriminatedUnion("storage", [GitHubArtifactSchema, LocalArtifactSchema]);
|
|
803
|
-
function getArtifactUrl(repo, pr, planId, filename) {
|
|
804
|
-
return `https://raw.githubusercontent.com/${repo}/plan-artifacts/pr-${pr}/${planId}/${filename}`;
|
|
805
|
-
}
|
|
806
770
|
var DeliverableSchema = z.object({
|
|
807
771
|
id: z.string(),
|
|
808
772
|
text: z.string(),
|
|
@@ -879,15 +843,6 @@ var GitHubArtifactParseSchema = z.object({
|
|
|
879
843
|
storage: z.literal("github"),
|
|
880
844
|
url: z.string()
|
|
881
845
|
});
|
|
882
|
-
function createGitHubArtifact(params) {
|
|
883
|
-
const artifact = {
|
|
884
|
-
id: nanoid(),
|
|
885
|
-
...params,
|
|
886
|
-
storage: "github",
|
|
887
|
-
uploadedAt: params.uploadedAt ?? Date.now()
|
|
888
|
-
};
|
|
889
|
-
return GitHubArtifactParseSchema.parse(artifact);
|
|
890
|
-
}
|
|
891
846
|
var LocalArtifactParseSchema = z.object({
|
|
892
847
|
id: z.string(),
|
|
893
848
|
type: z.enum([
|
|
@@ -901,15 +856,6 @@ var LocalArtifactParseSchema = z.object({
|
|
|
901
856
|
storage: z.literal("local"),
|
|
902
857
|
localArtifactId: z.string()
|
|
903
858
|
});
|
|
904
|
-
function createLocalArtifact(params) {
|
|
905
|
-
const artifact = {
|
|
906
|
-
id: nanoid(),
|
|
907
|
-
...params,
|
|
908
|
-
storage: "local",
|
|
909
|
-
uploadedAt: params.uploadedAt ?? Date.now()
|
|
910
|
-
};
|
|
911
|
-
return LocalArtifactParseSchema.parse(artifact);
|
|
912
|
-
}
|
|
913
859
|
function createInitialConversationVersion(params) {
|
|
914
860
|
const version = {
|
|
915
861
|
...params,
|
|
@@ -917,19 +863,12 @@ function createInitialConversationVersion(params) {
|
|
|
917
863
|
};
|
|
918
864
|
return ConversationVersionSchema.parse(version);
|
|
919
865
|
}
|
|
920
|
-
function createHandedOffConversationVersion(params) {
|
|
921
|
-
const version = {
|
|
922
|
-
...params,
|
|
923
|
-
handedOff: true
|
|
924
|
-
};
|
|
925
|
-
return ConversationVersionSchema.parse(version);
|
|
926
|
-
}
|
|
927
866
|
|
|
928
867
|
// ../../packages/schema/dist/yjs-helpers-DzEyLz-f.mjs
|
|
929
868
|
import { z as z2 } from "zod";
|
|
930
869
|
import { nanoid as nanoid2 } from "nanoid";
|
|
931
870
|
import * as Y from "yjs";
|
|
932
|
-
function
|
|
871
|
+
function assertNever2(value) {
|
|
933
872
|
throw new Error(`Unhandled discriminated union member: ${JSON.stringify(value)}`);
|
|
934
873
|
}
|
|
935
874
|
var AgentPresenceSchema = z2.object({
|
|
@@ -1028,17 +967,6 @@ var CreateSubscriptionRequestSchema = z2.object({
|
|
|
1028
967
|
threshold: z2.number().positive().optional()
|
|
1029
968
|
});
|
|
1030
969
|
var CreateSubscriptionResponseSchema = z2.object({ clientId: z2.string() });
|
|
1031
|
-
var DEFAULT_INPUT_REQUEST_TIMEOUT_SECONDS = 1800;
|
|
1032
|
-
var InputRequestTypeValues = [
|
|
1033
|
-
"text",
|
|
1034
|
-
"multiline",
|
|
1035
|
-
"choice",
|
|
1036
|
-
"confirm",
|
|
1037
|
-
"number",
|
|
1038
|
-
"email",
|
|
1039
|
-
"date",
|
|
1040
|
-
"rating"
|
|
1041
|
-
];
|
|
1042
970
|
var InputRequestStatusValues = [
|
|
1043
971
|
"pending",
|
|
1044
972
|
"answered",
|
|
@@ -1206,17 +1134,6 @@ function createInputRequest(params) {
|
|
|
1206
1134
|
if (!parseResult.success) throw new Error(`Invalid input request: ${parseResult.error.issues[0]?.message}`);
|
|
1207
1135
|
return parseResult.data;
|
|
1208
1136
|
}
|
|
1209
|
-
function normalizeChoiceOptions(options) {
|
|
1210
|
-
return options.map((opt) => typeof opt === "string" ? {
|
|
1211
|
-
value: opt,
|
|
1212
|
-
label: opt
|
|
1213
|
-
} : {
|
|
1214
|
-
...opt,
|
|
1215
|
-
value: opt.value,
|
|
1216
|
-
label: opt.label || opt.value
|
|
1217
|
-
});
|
|
1218
|
-
}
|
|
1219
|
-
var CHOICE_DROPDOWN_THRESHOLD = 9;
|
|
1220
1137
|
var MAX_QUESTIONS_PER_REQUEST = 10;
|
|
1221
1138
|
var QuestionBaseSchema = z2.object({
|
|
1222
1139
|
message: z2.string().min(1, "Message cannot be empty"),
|
|
@@ -1369,9 +1286,6 @@ var YDOC_KEYS = {
|
|
|
1369
1286
|
LOCAL_DIFF_COMMENTS: "localDiffComments"
|
|
1370
1287
|
};
|
|
1371
1288
|
var validKeys = new Set(Object.values(YDOC_KEYS));
|
|
1372
|
-
function isValidYDocKey(key) {
|
|
1373
|
-
return validKeys.has(key);
|
|
1374
|
-
}
|
|
1375
1289
|
var CommentBodySchema = z2.union([z2.string(), z2.array(z2.unknown())]);
|
|
1376
1290
|
var ThreadCommentSchema = z2.object({
|
|
1377
1291
|
id: z2.string(),
|
|
@@ -1385,9 +1299,6 @@ var ThreadSchema = z2.object({
|
|
|
1385
1299
|
resolved: z2.boolean().optional(),
|
|
1386
1300
|
selectedText: z2.string().optional()
|
|
1387
1301
|
});
|
|
1388
|
-
function isThread(value) {
|
|
1389
|
-
return ThreadSchema.safeParse(value).success;
|
|
1390
|
-
}
|
|
1391
1302
|
function parseThreads(data) {
|
|
1392
1303
|
const threads = [];
|
|
1393
1304
|
for (const [_key, value] of Object.entries(data)) {
|
|
@@ -1421,20 +1332,6 @@ function extractMentions(body) {
|
|
|
1421
1332
|
function toUnknownArray(array) {
|
|
1422
1333
|
return array.toJSON();
|
|
1423
1334
|
}
|
|
1424
|
-
function findInputRequestById(data, requestId) {
|
|
1425
|
-
for (let i = 0; i < data.length; i++) {
|
|
1426
|
-
const item = data[i];
|
|
1427
|
-
if (item && typeof item === "object" && "id" in item && item.id === requestId) {
|
|
1428
|
-
const parsed = AnyInputRequestSchema.safeParse(item);
|
|
1429
|
-
if (parsed.success) return {
|
|
1430
|
-
rawIndex: i,
|
|
1431
|
-
request: parsed.data
|
|
1432
|
-
};
|
|
1433
|
-
return null;
|
|
1434
|
-
}
|
|
1435
|
-
}
|
|
1436
|
-
return null;
|
|
1437
|
-
}
|
|
1438
1335
|
var VALID_STATUS_TRANSITIONS = {
|
|
1439
1336
|
draft: [
|
|
1440
1337
|
"pending_review",
|
|
@@ -1539,7 +1436,7 @@ function applyStatusTransitionFields(map, transition) {
|
|
|
1539
1436
|
applyCompletedTransition(map, transition);
|
|
1540
1437
|
break;
|
|
1541
1438
|
default:
|
|
1542
|
-
|
|
1439
|
+
assertNever2(transition);
|
|
1543
1440
|
}
|
|
1544
1441
|
}
|
|
1545
1442
|
function resetPlanToDraft(ydoc, actor) {
|
|
@@ -1607,20 +1504,6 @@ function initPlanMetadata(ydoc, init) {
|
|
|
1607
1504
|
const result = getPlanMetadataWithValidation(ydoc);
|
|
1608
1505
|
if (!result.success) throw new Error(`Failed to initialize metadata: ${result.error}`);
|
|
1609
1506
|
}
|
|
1610
|
-
function getStepCompletions(ydoc) {
|
|
1611
|
-
const steps = ydoc.getMap("stepCompletions");
|
|
1612
|
-
return new Map(steps.entries());
|
|
1613
|
-
}
|
|
1614
|
-
function toggleStepCompletion(ydoc, stepId, actor) {
|
|
1615
|
-
ydoc.transact(() => {
|
|
1616
|
-
const steps = ydoc.getMap("stepCompletions");
|
|
1617
|
-
const current = steps.get(stepId) || false;
|
|
1618
|
-
steps.set(stepId, !current);
|
|
1619
|
-
}, actor ? { actor } : void 0);
|
|
1620
|
-
}
|
|
1621
|
-
function isStepCompleted(ydoc, stepId) {
|
|
1622
|
-
return ydoc.getMap("stepCompletions").get(stepId) || false;
|
|
1623
|
-
}
|
|
1624
1507
|
function getArtifacts(ydoc) {
|
|
1625
1508
|
return toUnknownArray(ydoc.getArray(YDOC_KEYS.ARTIFACTS)).map((item) => {
|
|
1626
1509
|
if (!item || typeof item !== "object") return null;
|
|
@@ -1639,40 +1522,12 @@ function addArtifact(ydoc, artifact, actor) {
|
|
|
1639
1522
|
ydoc.getArray(YDOC_KEYS.ARTIFACTS).push([validated]);
|
|
1640
1523
|
}, actor ? { actor } : void 0);
|
|
1641
1524
|
}
|
|
1642
|
-
function removeArtifact(ydoc, artifactId) {
|
|
1643
|
-
const array = ydoc.getArray(YDOC_KEYS.ARTIFACTS);
|
|
1644
|
-
const index = toUnknownArray(array).map((item) => ArtifactSchema.safeParse(item)).filter((r) => r.success).map((r) => r.data).findIndex((a) => a.id === artifactId);
|
|
1645
|
-
if (index === -1) return false;
|
|
1646
|
-
array.delete(index, 1);
|
|
1647
|
-
return true;
|
|
1648
|
-
}
|
|
1649
|
-
function getAgentPresences(ydoc) {
|
|
1650
|
-
const map = ydoc.getMap(YDOC_KEYS.PRESENCE);
|
|
1651
|
-
const result = /* @__PURE__ */ new Map();
|
|
1652
|
-
for (const [sessionId, value] of map.entries()) {
|
|
1653
|
-
const parsed = AgentPresenceSchema.safeParse(value);
|
|
1654
|
-
if (parsed.success) result.set(sessionId, parsed.data);
|
|
1655
|
-
}
|
|
1656
|
-
return result;
|
|
1657
|
-
}
|
|
1658
1525
|
function setAgentPresence(ydoc, presence, actor) {
|
|
1659
1526
|
const validated = AgentPresenceSchema.parse(presence);
|
|
1660
1527
|
ydoc.transact(() => {
|
|
1661
1528
|
ydoc.getMap(YDOC_KEYS.PRESENCE).set(validated.sessionId, validated);
|
|
1662
1529
|
}, actor ? { actor } : void 0);
|
|
1663
1530
|
}
|
|
1664
|
-
function clearAgentPresence(ydoc, sessionId) {
|
|
1665
|
-
const map = ydoc.getMap(YDOC_KEYS.PRESENCE);
|
|
1666
|
-
if (!map.has(sessionId)) return false;
|
|
1667
|
-
map.delete(sessionId);
|
|
1668
|
-
return true;
|
|
1669
|
-
}
|
|
1670
|
-
function getAgentPresence(ydoc, sessionId) {
|
|
1671
|
-
const value = ydoc.getMap(YDOC_KEYS.PRESENCE).get(sessionId);
|
|
1672
|
-
if (!value) return null;
|
|
1673
|
-
const parsed = AgentPresenceSchema.safeParse(value);
|
|
1674
|
-
return parsed.success ? parsed.data : null;
|
|
1675
|
-
}
|
|
1676
1531
|
function getDeliverables(ydoc) {
|
|
1677
1532
|
return toUnknownArray(ydoc.getArray(YDOC_KEYS.DELIVERABLES)).map((item) => DeliverableSchema.safeParse(item)).filter((result) => result.success).map((result) => result.data);
|
|
1678
1533
|
}
|
|
@@ -1701,75 +1556,6 @@ function linkArtifactToDeliverable(ydoc, deliverableId, artifactId, actor) {
|
|
|
1701
1556
|
}, actor ? { actor } : void 0);
|
|
1702
1557
|
return true;
|
|
1703
1558
|
}
|
|
1704
|
-
function getPlanOwnerId(ydoc) {
|
|
1705
|
-
const ownerId = ydoc.getMap(YDOC_KEYS.METADATA).get("ownerId");
|
|
1706
|
-
return typeof ownerId === "string" ? ownerId : null;
|
|
1707
|
-
}
|
|
1708
|
-
function isApprovalRequired(ydoc) {
|
|
1709
|
-
const map = ydoc.getMap(YDOC_KEYS.METADATA);
|
|
1710
|
-
const approvalRequired = map.get("approvalRequired");
|
|
1711
|
-
if (typeof approvalRequired === "boolean") return approvalRequired;
|
|
1712
|
-
const ownerId = map.get("ownerId");
|
|
1713
|
-
return typeof ownerId === "string" && ownerId.length > 0;
|
|
1714
|
-
}
|
|
1715
|
-
function getApprovedUsers(ydoc) {
|
|
1716
|
-
const approvedUsers = ydoc.getMap(YDOC_KEYS.METADATA).get("approvedUsers");
|
|
1717
|
-
if (!Array.isArray(approvedUsers)) return [];
|
|
1718
|
-
return approvedUsers.filter((id) => typeof id === "string");
|
|
1719
|
-
}
|
|
1720
|
-
function isUserApproved(ydoc, userId) {
|
|
1721
|
-
if (getPlanOwnerId(ydoc) === userId) return true;
|
|
1722
|
-
return getApprovedUsers(ydoc).includes(userId);
|
|
1723
|
-
}
|
|
1724
|
-
function approveUser(ydoc, userId, actor) {
|
|
1725
|
-
const currentApproved = getApprovedUsers(ydoc);
|
|
1726
|
-
if (currentApproved.includes(userId)) return;
|
|
1727
|
-
ydoc.transact(() => {
|
|
1728
|
-
const map = ydoc.getMap(YDOC_KEYS.METADATA);
|
|
1729
|
-
map.set("approvedUsers", [...currentApproved, userId]);
|
|
1730
|
-
map.set("updatedAt", Date.now());
|
|
1731
|
-
}, actor ? { actor } : void 0);
|
|
1732
|
-
}
|
|
1733
|
-
function revokeUser(ydoc, userId, actor) {
|
|
1734
|
-
if (userId === getPlanOwnerId(ydoc)) return false;
|
|
1735
|
-
const currentApproved = getApprovedUsers(ydoc);
|
|
1736
|
-
if (currentApproved.indexOf(userId) === -1) return false;
|
|
1737
|
-
ydoc.transact(() => {
|
|
1738
|
-
const map = ydoc.getMap(YDOC_KEYS.METADATA);
|
|
1739
|
-
map.set("approvedUsers", currentApproved.filter((id) => id !== userId));
|
|
1740
|
-
map.set("updatedAt", Date.now());
|
|
1741
|
-
}, actor ? { actor } : void 0);
|
|
1742
|
-
return true;
|
|
1743
|
-
}
|
|
1744
|
-
function getRejectedUsers(ydoc) {
|
|
1745
|
-
const rejectedUsers = ydoc.getMap(YDOC_KEYS.METADATA).get("rejectedUsers");
|
|
1746
|
-
if (!Array.isArray(rejectedUsers)) return [];
|
|
1747
|
-
return rejectedUsers.filter((id) => typeof id === "string");
|
|
1748
|
-
}
|
|
1749
|
-
function isUserRejected(ydoc, userId) {
|
|
1750
|
-
return getRejectedUsers(ydoc).includes(userId);
|
|
1751
|
-
}
|
|
1752
|
-
function rejectUser(ydoc, userId, actor) {
|
|
1753
|
-
if (userId === getPlanOwnerId(ydoc)) return;
|
|
1754
|
-
const currentRejected = getRejectedUsers(ydoc);
|
|
1755
|
-
const currentApproved = getApprovedUsers(ydoc);
|
|
1756
|
-
ydoc.transact(() => {
|
|
1757
|
-
const map = ydoc.getMap(YDOC_KEYS.METADATA);
|
|
1758
|
-
if (!currentRejected.includes(userId)) map.set("rejectedUsers", [...currentRejected, userId]);
|
|
1759
|
-
if (currentApproved.includes(userId)) map.set("approvedUsers", currentApproved.filter((id) => id !== userId));
|
|
1760
|
-
map.set("updatedAt", Date.now());
|
|
1761
|
-
}, actor ? { actor } : void 0);
|
|
1762
|
-
}
|
|
1763
|
-
function unrejectUser(ydoc, userId, actor) {
|
|
1764
|
-
const currentRejected = getRejectedUsers(ydoc);
|
|
1765
|
-
if (currentRejected.indexOf(userId) === -1) return false;
|
|
1766
|
-
ydoc.transact(() => {
|
|
1767
|
-
const map = ydoc.getMap(YDOC_KEYS.METADATA);
|
|
1768
|
-
map.set("rejectedUsers", currentRejected.filter((id) => id !== userId));
|
|
1769
|
-
map.set("updatedAt", Date.now());
|
|
1770
|
-
}, actor ? { actor } : void 0);
|
|
1771
|
-
return true;
|
|
1772
|
-
}
|
|
1773
1559
|
function getLinkedPRs(ydoc) {
|
|
1774
1560
|
return toUnknownArray(ydoc.getArray(YDOC_KEYS.LINKED_PRS)).map((item) => LinkedPRSchema.safeParse(item)).filter((result) => result.success).map((result) => result.data);
|
|
1775
1561
|
}
|
|
@@ -1782,126 +1568,12 @@ function linkPR(ydoc, pr, actor) {
|
|
|
1782
1568
|
array.push([validated]);
|
|
1783
1569
|
}, actor ? { actor } : void 0);
|
|
1784
1570
|
}
|
|
1785
|
-
function unlinkPR(ydoc, prNumber) {
|
|
1786
|
-
const array = ydoc.getArray(YDOC_KEYS.LINKED_PRS);
|
|
1787
|
-
const index = toUnknownArray(array).map((item) => LinkedPRSchema.safeParse(item)).filter((r) => r.success).map((r) => r.data).findIndex((p) => p.prNumber === prNumber);
|
|
1788
|
-
if (index === -1) return false;
|
|
1789
|
-
array.delete(index, 1);
|
|
1790
|
-
return true;
|
|
1791
|
-
}
|
|
1792
|
-
function getLinkedPR(ydoc, prNumber) {
|
|
1793
|
-
return getLinkedPRs(ydoc).find((pr) => pr.prNumber === prNumber) ?? null;
|
|
1794
|
-
}
|
|
1795
|
-
function updateLinkedPRStatus(ydoc, prNumber, status) {
|
|
1796
|
-
const array = ydoc.getArray(YDOC_KEYS.LINKED_PRS);
|
|
1797
|
-
const existing = toUnknownArray(array).map((item) => LinkedPRSchema.safeParse(item)).filter((r) => r.success).map((r) => r.data);
|
|
1798
|
-
const index = existing.findIndex((p) => p.prNumber === prNumber);
|
|
1799
|
-
if (index === -1) return false;
|
|
1800
|
-
const pr = existing[index];
|
|
1801
|
-
if (!pr) return false;
|
|
1802
|
-
array.delete(index, 1);
|
|
1803
|
-
array.insert(index, [{
|
|
1804
|
-
...pr,
|
|
1805
|
-
status
|
|
1806
|
-
}]);
|
|
1807
|
-
return true;
|
|
1808
|
-
}
|
|
1809
1571
|
function getPRReviewComments(ydoc) {
|
|
1810
1572
|
return toUnknownArray(ydoc.getArray(YDOC_KEYS.PR_REVIEW_COMMENTS)).map((item) => PRReviewCommentSchema.safeParse(item)).filter((result) => result.success).map((result) => result.data);
|
|
1811
1573
|
}
|
|
1812
|
-
function getPRReviewCommentsForPR(ydoc, prNumber) {
|
|
1813
|
-
return getPRReviewComments(ydoc).filter((c) => c.prNumber === prNumber);
|
|
1814
|
-
}
|
|
1815
|
-
function addPRReviewComment(ydoc, comment, actor) {
|
|
1816
|
-
const validated = PRReviewCommentSchema.parse(comment);
|
|
1817
|
-
ydoc.transact(() => {
|
|
1818
|
-
ydoc.getArray(YDOC_KEYS.PR_REVIEW_COMMENTS).push([validated]);
|
|
1819
|
-
}, actor ? { actor } : void 0);
|
|
1820
|
-
}
|
|
1821
|
-
function resolvePRReviewComment(ydoc, commentId, resolved) {
|
|
1822
|
-
const array = ydoc.getArray(YDOC_KEYS.PR_REVIEW_COMMENTS);
|
|
1823
|
-
const existing = toUnknownArray(array).map((item) => PRReviewCommentSchema.safeParse(item)).filter((r) => r.success).map((r) => r.data);
|
|
1824
|
-
const index = existing.findIndex((c) => c.id === commentId);
|
|
1825
|
-
if (index === -1) return false;
|
|
1826
|
-
const comment = existing[index];
|
|
1827
|
-
if (!comment) return false;
|
|
1828
|
-
array.delete(index, 1);
|
|
1829
|
-
array.insert(index, [{
|
|
1830
|
-
...comment,
|
|
1831
|
-
resolved
|
|
1832
|
-
}]);
|
|
1833
|
-
return true;
|
|
1834
|
-
}
|
|
1835
|
-
function removePRReviewComment(ydoc, commentId) {
|
|
1836
|
-
const array = ydoc.getArray(YDOC_KEYS.PR_REVIEW_COMMENTS);
|
|
1837
|
-
const index = toUnknownArray(array).map((item) => PRReviewCommentSchema.safeParse(item)).filter((r) => r.success).map((r) => r.data).findIndex((c) => c.id === commentId);
|
|
1838
|
-
if (index === -1) return false;
|
|
1839
|
-
array.delete(index, 1);
|
|
1840
|
-
return true;
|
|
1841
|
-
}
|
|
1842
1574
|
function getLocalDiffComments(ydoc) {
|
|
1843
1575
|
return toUnknownArray(ydoc.getArray(YDOC_KEYS.LOCAL_DIFF_COMMENTS)).map((item) => LocalDiffCommentSchema.safeParse(item)).filter((result) => result.success).map((result) => result.data);
|
|
1844
1576
|
}
|
|
1845
|
-
function getLocalDiffCommentsForFile(ydoc, path) {
|
|
1846
|
-
return getLocalDiffComments(ydoc).filter((c) => c.path === path);
|
|
1847
|
-
}
|
|
1848
|
-
function addLocalDiffComment(ydoc, comment, actor) {
|
|
1849
|
-
const validated = LocalDiffCommentSchema.parse(comment);
|
|
1850
|
-
ydoc.transact(() => {
|
|
1851
|
-
ydoc.getArray(YDOC_KEYS.LOCAL_DIFF_COMMENTS).push([validated]);
|
|
1852
|
-
}, actor ? { actor } : void 0);
|
|
1853
|
-
}
|
|
1854
|
-
function resolveLocalDiffComment(ydoc, commentId, resolved) {
|
|
1855
|
-
const array = ydoc.getArray(YDOC_KEYS.LOCAL_DIFF_COMMENTS);
|
|
1856
|
-
const existing = toUnknownArray(array).map((item) => LocalDiffCommentSchema.safeParse(item)).filter((r) => r.success).map((r) => r.data);
|
|
1857
|
-
const index = existing.findIndex((c) => c.id === commentId);
|
|
1858
|
-
if (index === -1) return false;
|
|
1859
|
-
const comment = existing[index];
|
|
1860
|
-
if (!comment) return false;
|
|
1861
|
-
array.delete(index, 1);
|
|
1862
|
-
array.insert(index, [{
|
|
1863
|
-
...comment,
|
|
1864
|
-
resolved
|
|
1865
|
-
}]);
|
|
1866
|
-
return true;
|
|
1867
|
-
}
|
|
1868
|
-
function removeLocalDiffComment(ydoc, commentId) {
|
|
1869
|
-
const array = ydoc.getArray(YDOC_KEYS.LOCAL_DIFF_COMMENTS);
|
|
1870
|
-
const index = toUnknownArray(array).map((item) => LocalDiffCommentSchema.safeParse(item)).filter((r) => r.success).map((r) => r.data).findIndex((c) => c.id === commentId);
|
|
1871
|
-
if (index === -1) return false;
|
|
1872
|
-
array.delete(index, 1);
|
|
1873
|
-
return true;
|
|
1874
|
-
}
|
|
1875
|
-
function extractViewedByFromCrdt(existingViewedBy) {
|
|
1876
|
-
const viewedBy = {};
|
|
1877
|
-
if (existingViewedBy instanceof Y.Map) {
|
|
1878
|
-
for (const [key, value] of existingViewedBy.entries()) if (typeof key === "string" && typeof value === "number") viewedBy[key] = value;
|
|
1879
|
-
} else if (existingViewedBy && typeof existingViewedBy === "object" && !Array.isArray(existingViewedBy)) {
|
|
1880
|
-
for (const [key, value] of Object.entries(existingViewedBy)) if (typeof value === "number") viewedBy[key] = value;
|
|
1881
|
-
}
|
|
1882
|
-
return viewedBy;
|
|
1883
|
-
}
|
|
1884
|
-
function markPlanAsViewed(ydoc, username) {
|
|
1885
|
-
const map = ydoc.getMap(YDOC_KEYS.METADATA);
|
|
1886
|
-
ydoc.transact(() => {
|
|
1887
|
-
const viewedBy = extractViewedByFromCrdt(map.get("viewedBy"));
|
|
1888
|
-
viewedBy[username] = Date.now();
|
|
1889
|
-
const viewedByMap = new Y.Map();
|
|
1890
|
-
for (const [user, timestamp] of Object.entries(viewedBy)) viewedByMap.set(user, timestamp);
|
|
1891
|
-
map.set("viewedBy", viewedByMap);
|
|
1892
|
-
});
|
|
1893
|
-
}
|
|
1894
|
-
function getViewedBy(ydoc) {
|
|
1895
|
-
return extractViewedByFromCrdt(ydoc.getMap(YDOC_KEYS.METADATA).get("viewedBy"));
|
|
1896
|
-
}
|
|
1897
|
-
function isPlanUnread(metadata, username, viewedBy) {
|
|
1898
|
-
const lastViewed = (viewedBy ?? {})[username];
|
|
1899
|
-
if (!lastViewed) return true;
|
|
1900
|
-
return lastViewed < metadata.updatedAt;
|
|
1901
|
-
}
|
|
1902
|
-
function getConversationVersions(ydoc) {
|
|
1903
|
-
return getPlanMetadata(ydoc)?.conversationVersions || [];
|
|
1904
|
-
}
|
|
1905
1577
|
function addConversationVersion(ydoc, version, actor) {
|
|
1906
1578
|
const validated = ConversationVersionSchema.parse(version);
|
|
1907
1579
|
ydoc.transact(() => {
|
|
@@ -1912,21 +1584,6 @@ function addConversationVersion(ydoc, version, actor) {
|
|
|
1912
1584
|
metadata.set("conversationVersions", [...versions, validated]);
|
|
1913
1585
|
}, actor ? { actor } : void 0);
|
|
1914
1586
|
}
|
|
1915
|
-
function markVersionHandedOff(ydoc, versionId, handedOffTo, actor) {
|
|
1916
|
-
const updated = getConversationVersions(ydoc).map((v) => {
|
|
1917
|
-
if (v.versionId !== versionId) return v;
|
|
1918
|
-
const handedOffVersion = {
|
|
1919
|
-
...v,
|
|
1920
|
-
handedOff: true,
|
|
1921
|
-
handedOffAt: Date.now(),
|
|
1922
|
-
handedOffTo
|
|
1923
|
-
};
|
|
1924
|
-
return ConversationVersionSchema.parse(handedOffVersion);
|
|
1925
|
-
});
|
|
1926
|
-
ydoc.transact(() => {
|
|
1927
|
-
ydoc.getMap(YDOC_KEYS.METADATA).set("conversationVersions", updated);
|
|
1928
|
-
}, actor ? { actor } : void 0);
|
|
1929
|
-
}
|
|
1930
1587
|
function logPlanEvent(ydoc, type, actor, ...args) {
|
|
1931
1588
|
const eventsArray = ydoc.getArray(YDOC_KEYS.EVENTS);
|
|
1932
1589
|
const [data, options] = args;
|
|
@@ -1981,229 +1638,6 @@ function createPlanSnapshot(ydoc, reason, actor, status, blocks) {
|
|
|
1981
1638
|
deliverables: deliverables.length > 0 ? deliverables : void 0
|
|
1982
1639
|
};
|
|
1983
1640
|
}
|
|
1984
|
-
function getLatestSnapshot(ydoc) {
|
|
1985
|
-
const snapshots = getSnapshots(ydoc);
|
|
1986
|
-
if (snapshots.length === 0) return null;
|
|
1987
|
-
return snapshots[snapshots.length - 1] ?? null;
|
|
1988
|
-
}
|
|
1989
|
-
function getValidatedTags(rawTags) {
|
|
1990
|
-
if (!Array.isArray(rawTags)) return [];
|
|
1991
|
-
return rawTags.filter((t2) => typeof t2 === "string");
|
|
1992
|
-
}
|
|
1993
|
-
function addPlanTag(ydoc, tag, actor) {
|
|
1994
|
-
ydoc.transact(() => {
|
|
1995
|
-
const map = ydoc.getMap(YDOC_KEYS.METADATA);
|
|
1996
|
-
const currentTags = getValidatedTags(map.get("tags"));
|
|
1997
|
-
const normalizedTag = tag.toLowerCase().trim();
|
|
1998
|
-
if (!normalizedTag || currentTags.includes(normalizedTag)) return;
|
|
1999
|
-
map.set("tags", [...currentTags, normalizedTag]);
|
|
2000
|
-
map.set("updatedAt", Date.now());
|
|
2001
|
-
}, actor ? { actor } : void 0);
|
|
2002
|
-
}
|
|
2003
|
-
function removePlanTag(ydoc, tag, actor) {
|
|
2004
|
-
ydoc.transact(() => {
|
|
2005
|
-
const map = ydoc.getMap(YDOC_KEYS.METADATA);
|
|
2006
|
-
const currentTags = getValidatedTags(map.get("tags"));
|
|
2007
|
-
const normalizedTag = tag.toLowerCase().trim();
|
|
2008
|
-
map.set("tags", currentTags.filter((t2) => t2 !== normalizedTag));
|
|
2009
|
-
map.set("updatedAt", Date.now());
|
|
2010
|
-
}, actor ? { actor } : void 0);
|
|
2011
|
-
}
|
|
2012
|
-
function getAllTagsFromIndex(indexEntries) {
|
|
2013
|
-
const tagSet = /* @__PURE__ */ new Set();
|
|
2014
|
-
for (const entry of indexEntries) if (entry.tags) for (const tag of entry.tags) tagSet.add(tag);
|
|
2015
|
-
return Array.from(tagSet).sort();
|
|
2016
|
-
}
|
|
2017
|
-
function archivePlan(ydoc, actorId) {
|
|
2018
|
-
const metadata = getPlanMetadata(ydoc);
|
|
2019
|
-
if (!metadata) return {
|
|
2020
|
-
success: false,
|
|
2021
|
-
error: "Plan metadata not found"
|
|
2022
|
-
};
|
|
2023
|
-
if (metadata.archivedAt) return {
|
|
2024
|
-
success: false,
|
|
2025
|
-
error: "Plan is already archived"
|
|
2026
|
-
};
|
|
2027
|
-
ydoc.transact(() => {
|
|
2028
|
-
const metadataMap = ydoc.getMap(YDOC_KEYS.METADATA);
|
|
2029
|
-
metadataMap.set("archivedAt", Date.now());
|
|
2030
|
-
metadataMap.set("archivedBy", actorId);
|
|
2031
|
-
metadataMap.set("updatedAt", Date.now());
|
|
2032
|
-
}, { actor: actorId });
|
|
2033
|
-
return { success: true };
|
|
2034
|
-
}
|
|
2035
|
-
function unarchivePlan(ydoc, actorId) {
|
|
2036
|
-
const metadata = getPlanMetadata(ydoc);
|
|
2037
|
-
if (!metadata) return {
|
|
2038
|
-
success: false,
|
|
2039
|
-
error: "Plan metadata not found"
|
|
2040
|
-
};
|
|
2041
|
-
if (!metadata.archivedAt) return {
|
|
2042
|
-
success: false,
|
|
2043
|
-
error: "Plan is not archived"
|
|
2044
|
-
};
|
|
2045
|
-
ydoc.transact(() => {
|
|
2046
|
-
const metadataMap = ydoc.getMap(YDOC_KEYS.METADATA);
|
|
2047
|
-
metadataMap.delete("archivedAt");
|
|
2048
|
-
metadataMap.delete("archivedBy");
|
|
2049
|
-
metadataMap.set("updatedAt", Date.now());
|
|
2050
|
-
}, { actor: actorId });
|
|
2051
|
-
return { success: true };
|
|
2052
|
-
}
|
|
2053
|
-
function answerInputRequest(ydoc, requestId, response, answeredBy) {
|
|
2054
|
-
const requestsArray = ydoc.getArray(YDOC_KEYS.INPUT_REQUESTS);
|
|
2055
|
-
const found = findInputRequestById(toUnknownArray(requestsArray), requestId);
|
|
2056
|
-
if (!found) return {
|
|
2057
|
-
success: false,
|
|
2058
|
-
error: "Request not found"
|
|
2059
|
-
};
|
|
2060
|
-
const { rawIndex: index, request } = found;
|
|
2061
|
-
if (request.status !== "pending") switch (request.status) {
|
|
2062
|
-
case "answered":
|
|
2063
|
-
return {
|
|
2064
|
-
success: false,
|
|
2065
|
-
error: "Request already answered",
|
|
2066
|
-
answeredBy: request.answeredBy
|
|
2067
|
-
};
|
|
2068
|
-
case "declined":
|
|
2069
|
-
return {
|
|
2070
|
-
success: false,
|
|
2071
|
-
error: "Request was declined"
|
|
2072
|
-
};
|
|
2073
|
-
case "cancelled":
|
|
2074
|
-
return {
|
|
2075
|
-
success: false,
|
|
2076
|
-
error: "Request was cancelled"
|
|
2077
|
-
};
|
|
2078
|
-
default:
|
|
2079
|
-
return {
|
|
2080
|
-
success: false,
|
|
2081
|
-
error: `Request is not pending`
|
|
2082
|
-
};
|
|
2083
|
-
}
|
|
2084
|
-
const answeredRequest = {
|
|
2085
|
-
...request,
|
|
2086
|
-
status: "answered",
|
|
2087
|
-
response,
|
|
2088
|
-
answeredAt: Date.now(),
|
|
2089
|
-
answeredBy
|
|
2090
|
-
};
|
|
2091
|
-
const validated = InputRequestSchema.parse(answeredRequest);
|
|
2092
|
-
ydoc.transact(() => {
|
|
2093
|
-
requestsArray.delete(index, 1);
|
|
2094
|
-
requestsArray.insert(index, [validated]);
|
|
2095
|
-
logPlanEvent(ydoc, "input_request_answered", answeredBy, {
|
|
2096
|
-
requestId,
|
|
2097
|
-
response,
|
|
2098
|
-
answeredBy,
|
|
2099
|
-
requestMessage: "message" in request ? request.message : void 0,
|
|
2100
|
-
requestType: request.type
|
|
2101
|
-
});
|
|
2102
|
-
});
|
|
2103
|
-
return { success: true };
|
|
2104
|
-
}
|
|
2105
|
-
function answerMultiQuestionInputRequest(ydoc, requestId, responses, answeredBy) {
|
|
2106
|
-
const requestsArray = ydoc.getArray(YDOC_KEYS.INPUT_REQUESTS);
|
|
2107
|
-
const found = findInputRequestById(toUnknownArray(requestsArray), requestId);
|
|
2108
|
-
if (!found) return {
|
|
2109
|
-
success: false,
|
|
2110
|
-
error: "Request not found"
|
|
2111
|
-
};
|
|
2112
|
-
const { rawIndex: index, request } = found;
|
|
2113
|
-
if (request.type !== "multi") return {
|
|
2114
|
-
success: false,
|
|
2115
|
-
error: "Request is not pending"
|
|
2116
|
-
};
|
|
2117
|
-
if (request.status !== "pending") switch (request.status) {
|
|
2118
|
-
case "answered":
|
|
2119
|
-
return {
|
|
2120
|
-
success: false,
|
|
2121
|
-
error: "Request already answered",
|
|
2122
|
-
answeredBy: request.answeredBy
|
|
2123
|
-
};
|
|
2124
|
-
case "declined":
|
|
2125
|
-
return {
|
|
2126
|
-
success: false,
|
|
2127
|
-
error: "Request was declined"
|
|
2128
|
-
};
|
|
2129
|
-
case "cancelled":
|
|
2130
|
-
return {
|
|
2131
|
-
success: false,
|
|
2132
|
-
error: "Request was cancelled"
|
|
2133
|
-
};
|
|
2134
|
-
default:
|
|
2135
|
-
return {
|
|
2136
|
-
success: false,
|
|
2137
|
-
error: `Request is not pending`
|
|
2138
|
-
};
|
|
2139
|
-
}
|
|
2140
|
-
const answeredRequest = {
|
|
2141
|
-
...request,
|
|
2142
|
-
status: "answered",
|
|
2143
|
-
responses,
|
|
2144
|
-
answeredAt: Date.now(),
|
|
2145
|
-
answeredBy
|
|
2146
|
-
};
|
|
2147
|
-
const validated = MultiQuestionInputRequestSchema.parse(answeredRequest);
|
|
2148
|
-
ydoc.transact(() => {
|
|
2149
|
-
requestsArray.delete(index, 1);
|
|
2150
|
-
requestsArray.insert(index, [validated]);
|
|
2151
|
-
logPlanEvent(ydoc, "input_request_answered", answeredBy, {
|
|
2152
|
-
requestId,
|
|
2153
|
-
response: responses,
|
|
2154
|
-
answeredBy,
|
|
2155
|
-
requestType: "multi"
|
|
2156
|
-
});
|
|
2157
|
-
});
|
|
2158
|
-
return { success: true };
|
|
2159
|
-
}
|
|
2160
|
-
function cancelInputRequest(ydoc, requestId) {
|
|
2161
|
-
const requestsArray = ydoc.getArray(YDOC_KEYS.INPUT_REQUESTS);
|
|
2162
|
-
const found = findInputRequestById(toUnknownArray(requestsArray), requestId);
|
|
2163
|
-
if (!found) return {
|
|
2164
|
-
success: false,
|
|
2165
|
-
error: "Request not found"
|
|
2166
|
-
};
|
|
2167
|
-
const { rawIndex: index, request } = found;
|
|
2168
|
-
if (request.status !== "pending") return {
|
|
2169
|
-
success: false,
|
|
2170
|
-
error: `Request is not pending`
|
|
2171
|
-
};
|
|
2172
|
-
const cancelledRequest = {
|
|
2173
|
-
...request,
|
|
2174
|
-
status: "cancelled"
|
|
2175
|
-
};
|
|
2176
|
-
const validated = request.type === "multi" ? MultiQuestionInputRequestSchema.parse(cancelledRequest) : InputRequestSchema.parse(cancelledRequest);
|
|
2177
|
-
ydoc.transact(() => {
|
|
2178
|
-
requestsArray.delete(index, 1);
|
|
2179
|
-
requestsArray.insert(index, [validated]);
|
|
2180
|
-
});
|
|
2181
|
-
return { success: true };
|
|
2182
|
-
}
|
|
2183
|
-
function declineInputRequest(ydoc, requestId) {
|
|
2184
|
-
const requestsArray = ydoc.getArray(YDOC_KEYS.INPUT_REQUESTS);
|
|
2185
|
-
const found = findInputRequestById(toUnknownArray(requestsArray), requestId);
|
|
2186
|
-
if (!found) return {
|
|
2187
|
-
success: false,
|
|
2188
|
-
error: "Request not found"
|
|
2189
|
-
};
|
|
2190
|
-
const { rawIndex: index, request } = found;
|
|
2191
|
-
if (request.status !== "pending") return {
|
|
2192
|
-
success: false,
|
|
2193
|
-
error: `Request is not pending`
|
|
2194
|
-
};
|
|
2195
|
-
const declinedRequest = {
|
|
2196
|
-
...request,
|
|
2197
|
-
status: "declined"
|
|
2198
|
-
};
|
|
2199
|
-
const validated = request.type === "multi" ? MultiQuestionInputRequestSchema.parse(declinedRequest) : InputRequestSchema.parse(declinedRequest);
|
|
2200
|
-
ydoc.transact(() => {
|
|
2201
|
-
requestsArray.delete(index, 1);
|
|
2202
|
-
requestsArray.insert(index, [validated]);
|
|
2203
|
-
logPlanEvent(ydoc, "input_request_declined", "User", { requestId });
|
|
2204
|
-
});
|
|
2205
|
-
return { success: true };
|
|
2206
|
-
}
|
|
2207
1641
|
function atomicRegenerateTokenIfOwner(ydoc, expectedOwnerId, newTokenHash, actor) {
|
|
2208
1642
|
let result = {
|
|
2209
1643
|
success: false,
|
|
@@ -2272,29 +1706,10 @@ var UrlEncodedPlanV2Schema = z3.object({
|
|
|
2272
1706
|
keyVersions: z3.array(UrlKeyVersionSchema).optional()
|
|
2273
1707
|
});
|
|
2274
1708
|
var UrlEncodedPlanSchema = z3.discriminatedUnion("v", [UrlEncodedPlanV1Schema, UrlEncodedPlanV2Schema]);
|
|
2275
|
-
function isUrlEncodedPlanV1(plan) {
|
|
2276
|
-
return plan.v === 1;
|
|
2277
|
-
}
|
|
2278
|
-
function isUrlEncodedPlanV2(plan) {
|
|
2279
|
-
return plan.v === 2;
|
|
2280
|
-
}
|
|
2281
1709
|
function encodePlan(plan) {
|
|
2282
1710
|
const json = JSON.stringify(plan);
|
|
2283
1711
|
return import_lz_string.default.compressToEncodedURIComponent(json);
|
|
2284
1712
|
}
|
|
2285
|
-
function decodePlan(encoded) {
|
|
2286
|
-
try {
|
|
2287
|
-
const json = import_lz_string.default.decompressFromEncodedURIComponent(encoded);
|
|
2288
|
-
if (!json) return null;
|
|
2289
|
-
const parsed = JSON.parse(json);
|
|
2290
|
-
const result = UrlEncodedPlanSchema.safeParse(parsed);
|
|
2291
|
-
if (!result.success)
|
|
2292
|
-
return null;
|
|
2293
|
-
return result.data;
|
|
2294
|
-
} catch (_error) {
|
|
2295
|
-
return null;
|
|
2296
|
-
}
|
|
2297
|
-
}
|
|
2298
1713
|
function createPlanUrl(baseUrl, plan) {
|
|
2299
1714
|
const encoded = encodePlan(plan);
|
|
2300
1715
|
const url = new URL(baseUrl);
|
|
@@ -2334,22 +1749,6 @@ function createPlanUrlWithHistory(baseUrl, plan, snapshots) {
|
|
|
2334
1749
|
keyVersions: keyVersions.length > 0 ? keyVersions : void 0
|
|
2335
1750
|
});
|
|
2336
1751
|
}
|
|
2337
|
-
function getLocationSearch() {
|
|
2338
|
-
if (typeof globalThis === "undefined") return null;
|
|
2339
|
-
if (!("location" in globalThis)) return null;
|
|
2340
|
-
const location = Object.fromEntries(Object.entries(globalThis)).location;
|
|
2341
|
-
if (typeof location !== "object" || location === null) return null;
|
|
2342
|
-
if (!("search" in location)) return null;
|
|
2343
|
-
const search = Object.fromEntries(Object.entries(location)).search;
|
|
2344
|
-
return typeof search === "string" ? search : null;
|
|
2345
|
-
}
|
|
2346
|
-
function getPlanFromUrl() {
|
|
2347
|
-
const search = getLocationSearch();
|
|
2348
|
-
if (!search) return null;
|
|
2349
|
-
const encoded = new URLSearchParams(search).get("d");
|
|
2350
|
-
if (!encoded) return null;
|
|
2351
|
-
return decodePlan(encoded);
|
|
2352
|
-
}
|
|
2353
1752
|
|
|
2354
1753
|
// ../../packages/schema/dist/index.mjs
|
|
2355
1754
|
import { z as z4 } from "zod";
|
|
@@ -2560,88 +1959,6 @@ var ClaudeCodeMessageSchema = z4.object({
|
|
|
2560
1959
|
costUSD: z4.number().optional(),
|
|
2561
1960
|
durationMs: z4.number().optional()
|
|
2562
1961
|
});
|
|
2563
|
-
function parseClaudeCodeTranscriptString(content) {
|
|
2564
|
-
const lines = content.split("\n").filter((line) => line.trim());
|
|
2565
|
-
const messages = [];
|
|
2566
|
-
const errors = [];
|
|
2567
|
-
for (let i = 0; i < lines.length; i++) {
|
|
2568
|
-
const line = lines[i];
|
|
2569
|
-
if (!line) continue;
|
|
2570
|
-
try {
|
|
2571
|
-
const parsed = JSON.parse(line);
|
|
2572
|
-
const result = ClaudeCodeMessageSchema.safeParse(parsed);
|
|
2573
|
-
if (result.success) messages.push(result.data);
|
|
2574
|
-
else errors.push({
|
|
2575
|
-
line: i + 1,
|
|
2576
|
-
error: `Validation failed: ${result.error.message}`
|
|
2577
|
-
});
|
|
2578
|
-
} catch (err) {
|
|
2579
|
-
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
2580
|
-
errors.push({
|
|
2581
|
-
line: i + 1,
|
|
2582
|
-
error: `JSON parse error: ${errorMessage}`
|
|
2583
|
-
});
|
|
2584
|
-
}
|
|
2585
|
-
}
|
|
2586
|
-
return {
|
|
2587
|
-
messages,
|
|
2588
|
-
errors
|
|
2589
|
-
};
|
|
2590
|
-
}
|
|
2591
|
-
function assertNever$1(x) {
|
|
2592
|
-
throw new Error(`Unhandled case: ${JSON.stringify(x)}`);
|
|
2593
|
-
}
|
|
2594
|
-
function convertContentBlock(block) {
|
|
2595
|
-
switch (block.type) {
|
|
2596
|
-
case "text":
|
|
2597
|
-
return [{
|
|
2598
|
-
type: "text",
|
|
2599
|
-
text: block.text
|
|
2600
|
-
}];
|
|
2601
|
-
case "tool_use":
|
|
2602
|
-
return [{
|
|
2603
|
-
type: "data",
|
|
2604
|
-
data: { toolUse: {
|
|
2605
|
-
name: block.name,
|
|
2606
|
-
id: block.id,
|
|
2607
|
-
input: block.input
|
|
2608
|
-
} }
|
|
2609
|
-
}];
|
|
2610
|
-
case "tool_result":
|
|
2611
|
-
return [{
|
|
2612
|
-
type: "data",
|
|
2613
|
-
data: { toolResult: {
|
|
2614
|
-
toolUseId: block.tool_use_id,
|
|
2615
|
-
content: block.content,
|
|
2616
|
-
isError: block.is_error ?? false
|
|
2617
|
-
} }
|
|
2618
|
-
}];
|
|
2619
|
-
default:
|
|
2620
|
-
return assertNever$1(block);
|
|
2621
|
-
}
|
|
2622
|
-
}
|
|
2623
|
-
function convertMessage(msg, contextId) {
|
|
2624
|
-
const role = msg.message.role === "user" ? "user" : "agent";
|
|
2625
|
-
const parts = msg.message.content.flatMap((block) => convertContentBlock(block));
|
|
2626
|
-
return {
|
|
2627
|
-
messageId: msg.uuid,
|
|
2628
|
-
role,
|
|
2629
|
-
parts,
|
|
2630
|
-
contextId,
|
|
2631
|
-
metadata: {
|
|
2632
|
-
timestamp: msg.timestamp,
|
|
2633
|
-
platform: "claude-code",
|
|
2634
|
-
parentMessageId: msg.parentUuid,
|
|
2635
|
-
model: msg.message.model,
|
|
2636
|
-
usage: msg.message.usage,
|
|
2637
|
-
costUSD: msg.costUSD,
|
|
2638
|
-
durationMs: msg.durationMs
|
|
2639
|
-
}
|
|
2640
|
-
};
|
|
2641
|
-
}
|
|
2642
|
-
function claudeCodeToA2A(messages, contextId) {
|
|
2643
|
-
return messages.filter((msg) => msg.type !== "summary").map((msg) => convertMessage(msg, contextId));
|
|
2644
|
-
}
|
|
2645
1962
|
function validateA2AMessages(messages) {
|
|
2646
1963
|
const valid = [];
|
|
2647
1964
|
const errors = [];
|
|
@@ -2658,39 +1975,6 @@ function validateA2AMessages(messages) {
|
|
|
2658
1975
|
errors
|
|
2659
1976
|
};
|
|
2660
1977
|
}
|
|
2661
|
-
function getFirstTextPart(parts) {
|
|
2662
|
-
return parts.filter((p) => p.type === "text")[0];
|
|
2663
|
-
}
|
|
2664
|
-
function extractTitleFromMessage(msg) {
|
|
2665
|
-
if (!msg) return "Imported Conversation";
|
|
2666
|
-
const firstPart = getFirstTextPart(msg.parts);
|
|
2667
|
-
if (!firstPart) return "Imported Conversation";
|
|
2668
|
-
const text = firstPart.text;
|
|
2669
|
-
return text.length > 50 ? `${text.slice(0, 50)}...` : text;
|
|
2670
|
-
}
|
|
2671
|
-
function isToolDataPart(part) {
|
|
2672
|
-
const data = part.data;
|
|
2673
|
-
return Boolean(data && typeof data === "object" && ("toolUse" in data || "toolResult" in data));
|
|
2674
|
-
}
|
|
2675
|
-
function countToolInteractions(parts) {
|
|
2676
|
-
return parts.filter((p) => p.type === "data").filter(isToolDataPart).length;
|
|
2677
|
-
}
|
|
2678
|
-
function summarizeMessage(msg) {
|
|
2679
|
-
const prefix = msg.role === "user" ? "User" : "Agent";
|
|
2680
|
-
const firstTextPart = getFirstTextPart(msg.parts);
|
|
2681
|
-
if (firstTextPart) return `${prefix}: ${firstTextPart.text.slice(0, 100)}${firstTextPart.text.length > 100 ? "..." : ""}`;
|
|
2682
|
-
const toolCount = countToolInteractions(msg.parts);
|
|
2683
|
-
if (toolCount > 0) return `${prefix}: [${toolCount} tool interaction(s)]`;
|
|
2684
|
-
}
|
|
2685
|
-
function summarizeA2AConversation(messages, maxMessages = 3) {
|
|
2686
|
-
const title = extractTitleFromMessage(messages.find((m) => m.role === "user"));
|
|
2687
|
-
const summaryLines = messages.slice(0, maxMessages).map(summarizeMessage).filter((line) => typeof line === "string");
|
|
2688
|
-
if (messages.length > maxMessages) summaryLines.push(`... and ${messages.length - maxMessages} more messages`);
|
|
2689
|
-
return {
|
|
2690
|
-
title,
|
|
2691
|
-
text: summaryLines.join("\n")
|
|
2692
|
-
};
|
|
2693
|
-
}
|
|
2694
1978
|
function isToolUseData(data) {
|
|
2695
1979
|
if (!data || typeof data !== "object") return false;
|
|
2696
1980
|
const d = toRecord(data);
|
|
@@ -2862,26 +2146,6 @@ function computeCommentStaleness(comment, currentHeadSha, currentLineContent) {
|
|
|
2862
2146
|
isStale: false
|
|
2863
2147
|
};
|
|
2864
2148
|
}
|
|
2865
|
-
function isLineContentStale(comment, currentLineContent) {
|
|
2866
|
-
if (!comment.lineContentHash || currentLineContent === void 0) return false;
|
|
2867
|
-
const currentHash = hashLineContent(currentLineContent);
|
|
2868
|
-
return comment.lineContentHash !== currentHash;
|
|
2869
|
-
}
|
|
2870
|
-
function withStalenessInfo(comment, currentHeadSha, currentLineContent) {
|
|
2871
|
-
const staleness = computeCommentStaleness(comment, currentHeadSha, currentLineContent);
|
|
2872
|
-
return {
|
|
2873
|
-
...comment,
|
|
2874
|
-
isStale: staleness.isStale,
|
|
2875
|
-
stalenessType: staleness.type
|
|
2876
|
-
};
|
|
2877
|
-
}
|
|
2878
|
-
function withStalenessInfoBatch(comments, currentHeadSha, lineContentMap) {
|
|
2879
|
-
return comments.map((comment) => {
|
|
2880
|
-
const key = `${comment.path}:${comment.line}`;
|
|
2881
|
-
const currentLineContent = lineContentMap?.get(key);
|
|
2882
|
-
return withStalenessInfo(comment, currentHeadSha, currentLineContent);
|
|
2883
|
-
});
|
|
2884
|
-
}
|
|
2885
2149
|
function isDiffHeader(line) {
|
|
2886
2150
|
return line.startsWith("diff --git") || line.startsWith("index ") || line.startsWith("---") || line.startsWith("+++");
|
|
2887
2151
|
}
|
|
@@ -2996,26 +2260,6 @@ function formatDiffCommentsForLLM(comments, options = {}) {
|
|
|
2996
2260
|
(${resolvedCount} resolved comment(s) not shown)`;
|
|
2997
2261
|
return output;
|
|
2998
2262
|
}
|
|
2999
|
-
function formatPRCommentsForLLM(comments, options = {}) {
|
|
3000
|
-
return formatDiffCommentsForLLM(comments, options);
|
|
3001
|
-
}
|
|
3002
|
-
function getPRCommentsSummary(comments) {
|
|
3003
|
-
const byFile = /* @__PURE__ */ new Map();
|
|
3004
|
-
let unresolved = 0;
|
|
3005
|
-
let resolved = 0;
|
|
3006
|
-
for (const comment of comments) {
|
|
3007
|
-
if (comment.resolved) resolved++;
|
|
3008
|
-
else unresolved++;
|
|
3009
|
-
const count = byFile.get(comment.path) ?? 0;
|
|
3010
|
-
byFile.set(comment.path, count + 1);
|
|
3011
|
-
}
|
|
3012
|
-
return {
|
|
3013
|
-
total: comments.length,
|
|
3014
|
-
unresolved,
|
|
3015
|
-
resolved,
|
|
3016
|
-
byFile
|
|
3017
|
-
};
|
|
3018
|
-
}
|
|
3019
2263
|
var EnvironmentContextSchema = z4.object({
|
|
3020
2264
|
projectName: z4.string().optional(),
|
|
3021
2265
|
branch: z4.string().optional(),
|
|
@@ -3031,18 +2275,6 @@ var GitHubPRResponseSchema = z4.object({
|
|
|
3031
2275
|
merged: z4.boolean(),
|
|
3032
2276
|
head: z4.object({ ref: z4.string() })
|
|
3033
2277
|
});
|
|
3034
|
-
function asPlanId(id) {
|
|
3035
|
-
return id;
|
|
3036
|
-
}
|
|
3037
|
-
function asAwarenessClientId(id) {
|
|
3038
|
-
return id;
|
|
3039
|
-
}
|
|
3040
|
-
function asWebRTCPeerId(id) {
|
|
3041
|
-
return id;
|
|
3042
|
-
}
|
|
3043
|
-
function asGitHubUsername(username) {
|
|
3044
|
-
return username;
|
|
3045
|
-
}
|
|
3046
2278
|
var ROUTES = {
|
|
3047
2279
|
REGISTRY_LIST: "/registry",
|
|
3048
2280
|
REGISTRY_REGISTER: "/register",
|
|
@@ -3083,49 +2315,6 @@ var InviteRedemptionSchema = z4.object({
|
|
|
3083
2315
|
redeemedAt: z4.number(),
|
|
3084
2316
|
tokenId: z4.string()
|
|
3085
2317
|
});
|
|
3086
|
-
function parseInviteFromUrl(url) {
|
|
3087
|
-
try {
|
|
3088
|
-
const inviteParam = new URL(url).searchParams.get("invite");
|
|
3089
|
-
if (!inviteParam) return null;
|
|
3090
|
-
const [tokenId, tokenValue] = inviteParam.split(":");
|
|
3091
|
-
if (!tokenId || !tokenValue) return null;
|
|
3092
|
-
return {
|
|
3093
|
-
tokenId,
|
|
3094
|
-
tokenValue
|
|
3095
|
-
};
|
|
3096
|
-
} catch {
|
|
3097
|
-
return null;
|
|
3098
|
-
}
|
|
3099
|
-
}
|
|
3100
|
-
function buildInviteUrl(baseUrl, planId, tokenId, tokenValue) {
|
|
3101
|
-
const normalizedBase = baseUrl.endsWith("/") ? baseUrl.slice(0, -1) : baseUrl;
|
|
3102
|
-
const url = new URL(`${normalizedBase}${ROUTES.WEB_TASK(planId)}`);
|
|
3103
|
-
url.searchParams.set("invite", `${tokenId}:${tokenValue}`);
|
|
3104
|
-
return url.toString();
|
|
3105
|
-
}
|
|
3106
|
-
function getTokenTimeRemaining(expiresAt) {
|
|
3107
|
-
const remaining = expiresAt - Date.now();
|
|
3108
|
-
if (remaining <= 0) return {
|
|
3109
|
-
expired: true,
|
|
3110
|
-
minutes: 0,
|
|
3111
|
-
formatted: "Expired"
|
|
3112
|
-
};
|
|
3113
|
-
const minutes = Math.ceil(remaining / 6e4);
|
|
3114
|
-
if (minutes >= 60) {
|
|
3115
|
-
const hours = Math.floor(minutes / 60);
|
|
3116
|
-
const mins = minutes % 60;
|
|
3117
|
-
return {
|
|
3118
|
-
expired: false,
|
|
3119
|
-
minutes,
|
|
3120
|
-
formatted: mins > 0 ? `${hours}h ${mins}m` : `${hours}h`
|
|
3121
|
-
};
|
|
3122
|
-
}
|
|
3123
|
-
return {
|
|
3124
|
-
expired: false,
|
|
3125
|
-
minutes,
|
|
3126
|
-
formatted: `${minutes}m`
|
|
3127
|
-
};
|
|
3128
|
-
}
|
|
3129
2318
|
var GitFileStatusSchema = z4.enum([
|
|
3130
2319
|
"added",
|
|
3131
2320
|
"modified",
|
|
@@ -3163,11 +2352,6 @@ var LocalChangesUnavailableSchema = z4.object({
|
|
|
3163
2352
|
message: z4.string()
|
|
3164
2353
|
});
|
|
3165
2354
|
var LocalChangesResultSchema = z4.discriminatedUnion("available", [LocalChangesResponseSchema, LocalChangesUnavailableSchema]);
|
|
3166
|
-
var P2PMessageType = {
|
|
3167
|
-
CONVERSATION_EXPORT_START: 240,
|
|
3168
|
-
CONVERSATION_CHUNK: 241,
|
|
3169
|
-
CONVERSATION_EXPORT_END: 242
|
|
3170
|
-
};
|
|
3171
2355
|
var ConversationExportStartMetaSchema = z4.object({
|
|
3172
2356
|
exportId: z4.string(),
|
|
3173
2357
|
totalChunks: z4.number().int().positive(),
|
|
@@ -3187,112 +2371,9 @@ var ConversationExportEndSchema = z4.object({
|
|
|
3187
2371
|
exportId: z4.string(),
|
|
3188
2372
|
checksum: z4.string()
|
|
3189
2373
|
});
|
|
3190
|
-
function isConversationExportStart(data) {
|
|
3191
|
-
return data.length > 0 && data[0] === P2PMessageType.CONVERSATION_EXPORT_START;
|
|
3192
|
-
}
|
|
3193
|
-
function isConversationChunk(data) {
|
|
3194
|
-
return data.length > 0 && data[0] === P2PMessageType.CONVERSATION_CHUNK;
|
|
3195
|
-
}
|
|
3196
|
-
function isConversationExportEnd(data) {
|
|
3197
|
-
return data.length > 0 && data[0] === P2PMessageType.CONVERSATION_EXPORT_END;
|
|
3198
|
-
}
|
|
3199
|
-
function isP2PConversationMessage(data) {
|
|
3200
|
-
if (data.length === 0) return false;
|
|
3201
|
-
const type = data[0];
|
|
3202
|
-
return type === P2PMessageType.CONVERSATION_EXPORT_START || type === P2PMessageType.CONVERSATION_CHUNK || type === P2PMessageType.CONVERSATION_EXPORT_END;
|
|
3203
|
-
}
|
|
3204
2374
|
var textEncoder = new TextEncoder();
|
|
3205
2375
|
var textDecoder = new TextDecoder();
|
|
3206
|
-
function encodeExportStartMessage(meta) {
|
|
3207
|
-
const jsonBytes = textEncoder.encode(JSON.stringify(meta));
|
|
3208
|
-
const result = new Uint8Array(1 + jsonBytes.length);
|
|
3209
|
-
result[0] = P2PMessageType.CONVERSATION_EXPORT_START;
|
|
3210
|
-
result.set(jsonBytes, 1);
|
|
3211
|
-
return result;
|
|
3212
|
-
}
|
|
3213
|
-
function decodeExportStartMessage(data) {
|
|
3214
|
-
if (data.length === 0 || data[0] !== P2PMessageType.CONVERSATION_EXPORT_START) throw new Error("Invalid export start message: wrong type byte");
|
|
3215
|
-
const jsonStr = textDecoder.decode(data.slice(1));
|
|
3216
|
-
const parsed = JSON.parse(jsonStr);
|
|
3217
|
-
return ConversationExportStartMetaSchema.parse(parsed);
|
|
3218
|
-
}
|
|
3219
|
-
function encodeChunkMessage(chunk) {
|
|
3220
|
-
const exportIdBytes = textEncoder.encode(chunk.exportId);
|
|
3221
|
-
const result = new Uint8Array(5 + exportIdBytes.length + 4 + chunk.data.length);
|
|
3222
|
-
let offset = 0;
|
|
3223
|
-
result[offset] = P2PMessageType.CONVERSATION_CHUNK;
|
|
3224
|
-
offset += 1;
|
|
3225
|
-
const view = new DataView(result.buffer);
|
|
3226
|
-
view.setUint32(offset, exportIdBytes.length, false);
|
|
3227
|
-
offset += 4;
|
|
3228
|
-
result.set(exportIdBytes, offset);
|
|
3229
|
-
offset += exportIdBytes.length;
|
|
3230
|
-
view.setUint32(offset, chunk.chunkIndex, false);
|
|
3231
|
-
offset += 4;
|
|
3232
|
-
result.set(chunk.data, offset);
|
|
3233
|
-
return result;
|
|
3234
|
-
}
|
|
3235
|
-
function decodeChunkMessage(data) {
|
|
3236
|
-
if (data.length < 9 || data[0] !== P2PMessageType.CONVERSATION_CHUNK) throw new Error("Invalid chunk message: too short or wrong type byte");
|
|
3237
|
-
const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
|
|
3238
|
-
let offset = 1;
|
|
3239
|
-
const exportIdLength = view.getUint32(offset, false);
|
|
3240
|
-
offset += 4;
|
|
3241
|
-
if (data.length < 9 + exportIdLength) throw new Error("Invalid chunk message: exportId extends beyond message");
|
|
3242
|
-
const exportId = textDecoder.decode(data.slice(offset, offset + exportIdLength));
|
|
3243
|
-
offset += exportIdLength;
|
|
3244
|
-
const chunkIndex = view.getUint32(offset, false);
|
|
3245
|
-
offset += 4;
|
|
3246
|
-
const chunkData = data.slice(offset);
|
|
3247
|
-
return ChunkMessageSchema.parse({
|
|
3248
|
-
exportId,
|
|
3249
|
-
chunkIndex,
|
|
3250
|
-
data: chunkData
|
|
3251
|
-
});
|
|
3252
|
-
}
|
|
3253
|
-
function encodeExportEndMessage(end) {
|
|
3254
|
-
const jsonBytes = textEncoder.encode(JSON.stringify(end));
|
|
3255
|
-
const result = new Uint8Array(1 + jsonBytes.length);
|
|
3256
|
-
result[0] = P2PMessageType.CONVERSATION_EXPORT_END;
|
|
3257
|
-
result.set(jsonBytes, 1);
|
|
3258
|
-
return result;
|
|
3259
|
-
}
|
|
3260
|
-
function decodeExportEndMessage(data) {
|
|
3261
|
-
if (data.length === 0 || data[0] !== P2PMessageType.CONVERSATION_EXPORT_END) throw new Error("Invalid export end message: wrong type byte");
|
|
3262
|
-
const jsonStr = textDecoder.decode(data.slice(1));
|
|
3263
|
-
const parsed = JSON.parse(jsonStr);
|
|
3264
|
-
return ConversationExportEndSchema.parse(parsed);
|
|
3265
|
-
}
|
|
3266
|
-
function decodeP2PMessage(data) {
|
|
3267
|
-
if (data.length === 0) throw new Error("Cannot decode empty message");
|
|
3268
|
-
const type = data[0];
|
|
3269
|
-
if (type === void 0) throw new Error("Message type byte is missing");
|
|
3270
|
-
switch (type) {
|
|
3271
|
-
case P2PMessageType.CONVERSATION_EXPORT_START:
|
|
3272
|
-
return {
|
|
3273
|
-
type: "export_start",
|
|
3274
|
-
payload: decodeExportStartMessage(data)
|
|
3275
|
-
};
|
|
3276
|
-
case P2PMessageType.CONVERSATION_CHUNK:
|
|
3277
|
-
return {
|
|
3278
|
-
type: "chunk",
|
|
3279
|
-
payload: decodeChunkMessage(data)
|
|
3280
|
-
};
|
|
3281
|
-
case P2PMessageType.CONVERSATION_EXPORT_END:
|
|
3282
|
-
return {
|
|
3283
|
-
type: "export_end",
|
|
3284
|
-
payload: decodeExportEndMessage(data)
|
|
3285
|
-
};
|
|
3286
|
-
default:
|
|
3287
|
-
throw new Error(`Unknown P2P message type: 0x${type.toString(16)}`);
|
|
3288
|
-
}
|
|
3289
|
-
}
|
|
3290
|
-
function assertNeverP2PMessage(msg) {
|
|
3291
|
-
throw new Error(`Unhandled P2P message type: ${JSON.stringify(msg)}`);
|
|
3292
|
-
}
|
|
3293
2376
|
var PLAN_INDEX_DOC_NAME = "plan-index";
|
|
3294
|
-
var PLAN_INDEX_VIEWED_BY_KEY = "viewedBy";
|
|
3295
|
-
var NON_PLAN_DB_NAMES = ["plan-index", "idb-keyval"];
|
|
3296
2377
|
var PlanIndexEntrySchema = z4.discriminatedUnion("deleted", [z4.object({
|
|
3297
2378
|
deleted: z4.literal(false),
|
|
3298
2379
|
id: z4.string(),
|
|
@@ -3314,18 +2395,6 @@ var PlanIndexEntrySchema = z4.discriminatedUnion("deleted", [z4.object({
|
|
|
3314
2395
|
deletedAt: z4.number(),
|
|
3315
2396
|
deletedBy: z4.string()
|
|
3316
2397
|
})]);
|
|
3317
|
-
function getPlanIndex(ydoc, includeArchived = false) {
|
|
3318
|
-
const plansMap = ydoc.getMap(YDOC_KEYS.PLANS);
|
|
3319
|
-
const entries = [];
|
|
3320
|
-
for (const [_id, data] of plansMap.entries()) {
|
|
3321
|
-
const result = PlanIndexEntrySchema.safeParse(data);
|
|
3322
|
-
if (result.success) {
|
|
3323
|
-
if (!includeArchived && result.data.deleted) continue;
|
|
3324
|
-
entries.push(result.data);
|
|
3325
|
-
}
|
|
3326
|
-
}
|
|
3327
|
-
return entries.sort((a, b) => b.updatedAt - a.updatedAt);
|
|
3328
|
-
}
|
|
3329
2398
|
function getPlanIndexEntry(ydoc, planId) {
|
|
3330
2399
|
const data = ydoc.getMap(YDOC_KEYS.PLANS).get(planId);
|
|
3331
2400
|
if (!data) return null;
|
|
@@ -3336,9 +2405,6 @@ function setPlanIndexEntry(ydoc, entry) {
|
|
|
3336
2405
|
const validated = PlanIndexEntrySchema.parse(entry);
|
|
3337
2406
|
ydoc.getMap(YDOC_KEYS.PLANS).set(validated.id, validated);
|
|
3338
2407
|
}
|
|
3339
|
-
function removePlanIndexEntry(ydoc, planId) {
|
|
3340
|
-
ydoc.getMap(YDOC_KEYS.PLANS).delete(planId);
|
|
3341
|
-
}
|
|
3342
2408
|
function touchPlanIndexEntry(ydoc, planId) {
|
|
3343
2409
|
const entry = getPlanIndexEntry(ydoc, planId);
|
|
3344
2410
|
if (entry) setPlanIndexEntry(ydoc, {
|
|
@@ -3346,84 +2412,6 @@ function touchPlanIndexEntry(ydoc, planId) {
|
|
|
3346
2412
|
updatedAt: Date.now()
|
|
3347
2413
|
});
|
|
3348
2414
|
}
|
|
3349
|
-
function getViewedByFromIndex(ydoc, planId) {
|
|
3350
|
-
const planViewedBy = ydoc.getMap(PLAN_INDEX_VIEWED_BY_KEY).get(planId);
|
|
3351
|
-
if (!planViewedBy || !(planViewedBy instanceof Y2.Map)) return {};
|
|
3352
|
-
const result = {};
|
|
3353
|
-
for (const [username, timestamp] of planViewedBy.entries()) if (typeof timestamp === "number") result[username] = timestamp;
|
|
3354
|
-
return result;
|
|
3355
|
-
}
|
|
3356
|
-
function updatePlanIndexViewedBy(ydoc, planId, username) {
|
|
3357
|
-
ydoc.transact(() => {
|
|
3358
|
-
const viewedByRoot = ydoc.getMap(PLAN_INDEX_VIEWED_BY_KEY);
|
|
3359
|
-
let planViewedBy = viewedByRoot.get(planId);
|
|
3360
|
-
if (!planViewedBy || !(planViewedBy instanceof Y2.Map)) {
|
|
3361
|
-
planViewedBy = new Y2.Map();
|
|
3362
|
-
viewedByRoot.set(planId, planViewedBy);
|
|
3363
|
-
}
|
|
3364
|
-
planViewedBy.set(username, Date.now());
|
|
3365
|
-
});
|
|
3366
|
-
}
|
|
3367
|
-
function clearPlanIndexViewedBy(ydoc, planId, username) {
|
|
3368
|
-
ydoc.transact(() => {
|
|
3369
|
-
const planViewedBy = ydoc.getMap(PLAN_INDEX_VIEWED_BY_KEY).get(planId);
|
|
3370
|
-
if (planViewedBy && planViewedBy instanceof Y2.Map) planViewedBy.delete(username);
|
|
3371
|
-
});
|
|
3372
|
-
}
|
|
3373
|
-
function getAllViewedByFromIndex(ydoc, planIds) {
|
|
3374
|
-
const result = {};
|
|
3375
|
-
for (const planId of planIds) result[planId] = getViewedByFromIndex(ydoc, planId);
|
|
3376
|
-
return result;
|
|
3377
|
-
}
|
|
3378
|
-
function removeViewedByFromIndex(ydoc, planId) {
|
|
3379
|
-
ydoc.getMap(PLAN_INDEX_VIEWED_BY_KEY).delete(planId);
|
|
3380
|
-
}
|
|
3381
|
-
var PLAN_INDEX_EVENT_VIEWED_BY_KEY = "event-viewedBy";
|
|
3382
|
-
function markEventAsViewed(ydoc, planId, eventId, username) {
|
|
3383
|
-
const viewedByRoot = ydoc.getMap(PLAN_INDEX_EVENT_VIEWED_BY_KEY);
|
|
3384
|
-
const rawPlanEvents = viewedByRoot.get(planId);
|
|
3385
|
-
let planEvents;
|
|
3386
|
-
if (rawPlanEvents instanceof Y2.Map) planEvents = rawPlanEvents;
|
|
3387
|
-
else {
|
|
3388
|
-
planEvents = new Y2.Map();
|
|
3389
|
-
viewedByRoot.set(planId, planEvents);
|
|
3390
|
-
}
|
|
3391
|
-
const rawEventViews = planEvents.get(eventId);
|
|
3392
|
-
let eventViews;
|
|
3393
|
-
if (rawEventViews instanceof Y2.Map) eventViews = rawEventViews;
|
|
3394
|
-
else {
|
|
3395
|
-
eventViews = new Y2.Map();
|
|
3396
|
-
planEvents.set(eventId, eventViews);
|
|
3397
|
-
}
|
|
3398
|
-
eventViews.set(username, Date.now());
|
|
3399
|
-
}
|
|
3400
|
-
function clearEventViewedBy(ydoc, planId, eventId, username) {
|
|
3401
|
-
const planEvents = ydoc.getMap(PLAN_INDEX_EVENT_VIEWED_BY_KEY).get(planId);
|
|
3402
|
-
if (!planEvents || !(planEvents instanceof Y2.Map)) return;
|
|
3403
|
-
const eventViews = planEvents.get(eventId);
|
|
3404
|
-
if (!eventViews || !(eventViews instanceof Y2.Map)) return;
|
|
3405
|
-
eventViews.delete(username);
|
|
3406
|
-
}
|
|
3407
|
-
function isEventUnread(ydoc, planId, eventId, username) {
|
|
3408
|
-
const planEvents = ydoc.getMap(PLAN_INDEX_EVENT_VIEWED_BY_KEY).get(planId);
|
|
3409
|
-
if (!planEvents || !(planEvents instanceof Y2.Map)) return true;
|
|
3410
|
-
const eventViews = planEvents.get(eventId);
|
|
3411
|
-
if (!eventViews || !(eventViews instanceof Y2.Map)) return true;
|
|
3412
|
-
return !eventViews.has(username);
|
|
3413
|
-
}
|
|
3414
|
-
function getAllEventViewedByForPlan(ydoc, planId) {
|
|
3415
|
-
const planEvents = ydoc.getMap(PLAN_INDEX_EVENT_VIEWED_BY_KEY).get(planId);
|
|
3416
|
-
if (!planEvents || !(planEvents instanceof Y2.Map)) return {};
|
|
3417
|
-
const result = {};
|
|
3418
|
-
for (const [eventId, eventViews] of planEvents.entries()) {
|
|
3419
|
-
if (!(eventViews instanceof Y2.Map)) continue;
|
|
3420
|
-
const views = {};
|
|
3421
|
-
for (const [username, timestamp] of eventViews.entries())
|
|
3422
|
-
if (typeof timestamp === "number") views[username] = timestamp;
|
|
3423
|
-
result[eventId] = views;
|
|
3424
|
-
}
|
|
3425
|
-
return result;
|
|
3426
|
-
}
|
|
3427
2415
|
function formatThreadsForLLM(threads, options = {}) {
|
|
3428
2416
|
const { includeResolved = false, selectedTextMaxLength = 100, resolveUser } = options;
|
|
3429
2417
|
const unresolvedThreads = threads.filter((t$1) => !t$1.resolved);
|
|
@@ -3457,18 +2445,17 @@ function truncate(text, maxLength) {
|
|
|
3457
2445
|
}
|
|
3458
2446
|
var TOOL_NAMES = {
|
|
3459
2447
|
ADD_ARTIFACT: "add_artifact",
|
|
3460
|
-
ADD_PR_REVIEW_COMMENT: "add_pr_review_comment",
|
|
3461
2448
|
COMPLETE_TASK: "complete_task",
|
|
3462
|
-
|
|
2449
|
+
CREATE_TASK: "create_task",
|
|
3463
2450
|
EXECUTE_CODE: "execute_code",
|
|
3464
2451
|
LINK_PR: "link_pr",
|
|
3465
2452
|
READ_DIFF_COMMENTS: "read_diff_comments",
|
|
3466
|
-
|
|
2453
|
+
READ_TASK: "read_task",
|
|
3467
2454
|
REGENERATE_SESSION_TOKEN: "regenerate_session_token",
|
|
3468
2455
|
REQUEST_USER_INPUT: "request_user_input",
|
|
3469
2456
|
SETUP_REVIEW_NOTIFICATION: "setup_review_notification",
|
|
3470
2457
|
UPDATE_BLOCK_CONTENT: "update_block_content",
|
|
3471
|
-
|
|
2458
|
+
UPDATE_TASK: "update_task"
|
|
3472
2459
|
};
|
|
3473
2460
|
var PlanIdSchema = z4.object({ planId: z4.string().min(1) });
|
|
3474
2461
|
var PlanStatusResponseSchema = z4.object({ status: z4.string() });
|
|
@@ -3662,9 +2649,6 @@ function isErrnoException(err) {
|
|
|
3662
2649
|
function hasErrorCode(err, code) {
|
|
3663
2650
|
return isErrnoException(err) && err.code === code;
|
|
3664
2651
|
}
|
|
3665
|
-
function isBuffer(value) {
|
|
3666
|
-
return Buffer.isBuffer(value);
|
|
3667
|
-
}
|
|
3668
2652
|
function createUserResolver(ydoc, fallbackLength = 8) {
|
|
3669
2653
|
const usersMap = ydoc.getMap("users");
|
|
3670
2654
|
return (userId) => {
|
|
@@ -3681,258 +2665,2966 @@ function getSignalingConnections(provider) {
|
|
|
3681
2665
|
if (hasSignalingConns(provider)) return provider.signalingConns;
|
|
3682
2666
|
return [];
|
|
3683
2667
|
}
|
|
3684
|
-
|
|
3685
|
-
|
|
2668
|
+
|
|
2669
|
+
// src/registry-server.ts
|
|
2670
|
+
import { mkdirSync, readFileSync as readFileSync2, unlinkSync } from "fs";
|
|
2671
|
+
import { readFile, unlink, writeFile as writeFile2 } from "fs/promises";
|
|
2672
|
+
import http from "http";
|
|
2673
|
+
import { homedir as homedir3 } from "os";
|
|
2674
|
+
import { join as join4, resolve, sep } from "path";
|
|
2675
|
+
import { createExpressMiddleware } from "@trpc/server/adapters/express";
|
|
2676
|
+
import express from "express";
|
|
2677
|
+
import * as decoding from "lib0/decoding";
|
|
2678
|
+
import * as encoding from "lib0/encoding";
|
|
2679
|
+
import { WebSocketServer } from "ws";
|
|
2680
|
+
import { LeveldbPersistence } from "y-leveldb";
|
|
2681
|
+
import * as awarenessProtocol from "y-protocols/awareness";
|
|
2682
|
+
import * as syncProtocol from "y-protocols/sync";
|
|
2683
|
+
import * as Y4 from "yjs";
|
|
2684
|
+
|
|
2685
|
+
// src/config/env/registry.ts
|
|
2686
|
+
import { homedir } from "os";
|
|
2687
|
+
import { join } from "path";
|
|
2688
|
+
|
|
2689
|
+
// ../../packages/shared/dist/registry-config.mjs
|
|
2690
|
+
var DEFAULT_REGISTRY_PORTS = [32191, 32192];
|
|
2691
|
+
|
|
2692
|
+
// src/config/env/registry.ts
|
|
2693
|
+
import { z as z5 } from "zod";
|
|
2694
|
+
var schema = z5.object({
|
|
2695
|
+
REGISTRY_PORT: z5.string().optional().transform((val) => {
|
|
2696
|
+
if (!val) return DEFAULT_REGISTRY_PORTS;
|
|
2697
|
+
const port = Number.parseInt(val, 10);
|
|
2698
|
+
if (Number.isNaN(port)) {
|
|
2699
|
+
throw new Error(`REGISTRY_PORT must be a valid number, got: ${val}`);
|
|
2700
|
+
}
|
|
2701
|
+
return [port];
|
|
2702
|
+
}),
|
|
2703
|
+
SHIPYARD_STATE_DIR: z5.string().optional().default(() => join(homedir(), ".shipyard"))
|
|
2704
|
+
});
|
|
2705
|
+
var registryConfig = loadEnv(schema);
|
|
2706
|
+
|
|
2707
|
+
// src/conversation-handlers.ts
|
|
2708
|
+
import { mkdir, writeFile } from "fs/promises";
|
|
2709
|
+
import { homedir as homedir2 } from "os";
|
|
2710
|
+
import { join as join2 } from "path";
|
|
2711
|
+
import { nanoid as nanoid3 } from "nanoid";
|
|
2712
|
+
async function importConversationHandler(input, ctx) {
|
|
2713
|
+
const { a2aMessages, meta } = input;
|
|
2714
|
+
if (!a2aMessages || !Array.isArray(a2aMessages)) {
|
|
2715
|
+
return {
|
|
2716
|
+
success: false,
|
|
2717
|
+
error: "Missing or invalid a2aMessages"
|
|
2718
|
+
};
|
|
2719
|
+
}
|
|
2720
|
+
if (a2aMessages.length === 0) {
|
|
2721
|
+
return {
|
|
2722
|
+
success: false,
|
|
2723
|
+
error: "a2aMessages array is empty"
|
|
2724
|
+
};
|
|
2725
|
+
}
|
|
2726
|
+
try {
|
|
2727
|
+
const { valid, errors } = validateA2AMessages(a2aMessages);
|
|
2728
|
+
if (errors.length > 0) {
|
|
2729
|
+
return {
|
|
2730
|
+
success: false,
|
|
2731
|
+
error: `Invalid A2A messages: ${errors.map((e) => e.error).join(", ")}`
|
|
2732
|
+
};
|
|
2733
|
+
}
|
|
2734
|
+
const sessionId = nanoid3();
|
|
2735
|
+
const claudeMessages = a2aToClaudeCode(valid, sessionId);
|
|
2736
|
+
const jsonl = formatAsClaudeCodeJSONL(claudeMessages);
|
|
2737
|
+
const projectName = meta?.planId ? `shipyard-${meta.planId.slice(0, 8)}` : process.cwd().split("/").pop() || "shipyard";
|
|
2738
|
+
const projectPath = join2(homedir2(), ".claude", "projects", projectName);
|
|
2739
|
+
await mkdir(projectPath, { recursive: true });
|
|
2740
|
+
const transcriptPath = join2(projectPath, `${sessionId}.jsonl`);
|
|
2741
|
+
await writeFile(transcriptPath, jsonl, "utf-8");
|
|
2742
|
+
ctx.logger.info(
|
|
2743
|
+
{
|
|
2744
|
+
sessionId,
|
|
2745
|
+
transcriptPath,
|
|
2746
|
+
messageCount: claudeMessages.length,
|
|
2747
|
+
sourcePlatform: meta?.sourcePlatform
|
|
2748
|
+
},
|
|
2749
|
+
"Created Claude Code session from imported conversation"
|
|
2750
|
+
);
|
|
2751
|
+
return {
|
|
2752
|
+
success: true,
|
|
2753
|
+
sessionId,
|
|
2754
|
+
transcriptPath,
|
|
2755
|
+
messageCount: claudeMessages.length
|
|
2756
|
+
};
|
|
2757
|
+
} catch (error) {
|
|
2758
|
+
ctx.logger.error({ error }, "Failed to import conversation");
|
|
2759
|
+
return {
|
|
2760
|
+
success: false,
|
|
2761
|
+
error: error instanceof Error ? error.message : "Unknown error"
|
|
2762
|
+
};
|
|
2763
|
+
}
|
|
3686
2764
|
}
|
|
3687
|
-
function
|
|
3688
|
-
return
|
|
2765
|
+
function createConversationHandlers() {
|
|
2766
|
+
return {
|
|
2767
|
+
importConversation: (input, ctx) => importConversationHandler(input, ctx)
|
|
2768
|
+
};
|
|
3689
2769
|
}
|
|
3690
2770
|
|
|
3691
|
-
|
|
3692
|
-
|
|
3693
|
-
|
|
3694
|
-
|
|
3695
|
-
|
|
3696
|
-
|
|
3697
|
-
|
|
3698
|
-
|
|
3699
|
-
|
|
3700
|
-
|
|
3701
|
-
|
|
3702
|
-
|
|
3703
|
-
|
|
3704
|
-
|
|
3705
|
-
|
|
3706
|
-
|
|
3707
|
-
|
|
3708
|
-
|
|
3709
|
-
|
|
3710
|
-
|
|
3711
|
-
|
|
3712
|
-
|
|
3713
|
-
|
|
3714
|
-
|
|
3715
|
-
|
|
3716
|
-
|
|
3717
|
-
|
|
3718
|
-
|
|
3719
|
-
|
|
3720
|
-
|
|
3721
|
-
|
|
3722
|
-
|
|
3723
|
-
|
|
3724
|
-
|
|
3725
|
-
|
|
3726
|
-
|
|
3727
|
-
|
|
3728
|
-
|
|
3729
|
-
|
|
3730
|
-
|
|
3731
|
-
|
|
3732
|
-
|
|
3733
|
-
|
|
3734
|
-
|
|
3735
|
-
|
|
3736
|
-
|
|
3737
|
-
|
|
3738
|
-
|
|
3739
|
-
|
|
3740
|
-
|
|
3741
|
-
|
|
3742
|
-
|
|
2771
|
+
// src/crdt-validation.ts
|
|
2772
|
+
var corruptionState = /* @__PURE__ */ new Map();
|
|
2773
|
+
var CORRUPTION_REPORT_INTERVAL_MS = 5 * 60 * 1e3;
|
|
2774
|
+
function shouldReportCorruption(planId, key) {
|
|
2775
|
+
const state = corruptionState.get(planId);
|
|
2776
|
+
const now = Date.now();
|
|
2777
|
+
if (!state) {
|
|
2778
|
+
corruptionState.set(planId, {
|
|
2779
|
+
lastReported: now,
|
|
2780
|
+
corruptedKeys: /* @__PURE__ */ new Set([key])
|
|
2781
|
+
});
|
|
2782
|
+
return true;
|
|
2783
|
+
}
|
|
2784
|
+
if (!state.corruptedKeys.has(key)) {
|
|
2785
|
+
state.corruptedKeys.add(key);
|
|
2786
|
+
state.lastReported = now;
|
|
2787
|
+
return true;
|
|
2788
|
+
}
|
|
2789
|
+
if (now - state.lastReported > CORRUPTION_REPORT_INTERVAL_MS) {
|
|
2790
|
+
state.lastReported = now;
|
|
2791
|
+
return true;
|
|
2792
|
+
}
|
|
2793
|
+
return false;
|
|
2794
|
+
}
|
|
2795
|
+
function clearCorruptionState(planId, key) {
|
|
2796
|
+
const state = corruptionState.get(planId);
|
|
2797
|
+
if (state) {
|
|
2798
|
+
state.corruptedKeys.delete(key);
|
|
2799
|
+
if (state.corruptedKeys.size === 0) {
|
|
2800
|
+
corruptionState.delete(planId);
|
|
2801
|
+
}
|
|
2802
|
+
}
|
|
2803
|
+
}
|
|
2804
|
+
function validateMetadata(doc, planId) {
|
|
2805
|
+
const result = getPlanMetadataWithValidation(doc);
|
|
2806
|
+
if (result.success) {
|
|
2807
|
+
clearCorruptionState(planId, YDOC_KEYS.METADATA);
|
|
2808
|
+
return { key: YDOC_KEYS.METADATA, valid: true };
|
|
2809
|
+
}
|
|
2810
|
+
return {
|
|
2811
|
+
key: YDOC_KEYS.METADATA,
|
|
2812
|
+
valid: false,
|
|
2813
|
+
errors: [result.error]
|
|
2814
|
+
};
|
|
2815
|
+
}
|
|
2816
|
+
function validateArray(doc, key, schema4) {
|
|
2817
|
+
const array = doc.getArray(key);
|
|
2818
|
+
const items = array.toJSON();
|
|
2819
|
+
if (items.length === 0) {
|
|
2820
|
+
return { key, valid: true, totalItems: 0, invalidItems: 0 };
|
|
2821
|
+
}
|
|
2822
|
+
const errors = [];
|
|
2823
|
+
let invalidCount = 0;
|
|
2824
|
+
for (let i = 0; i < items.length; i++) {
|
|
2825
|
+
const result = schema4.safeParse(items[i]);
|
|
2826
|
+
if (!result.success) {
|
|
2827
|
+
invalidCount++;
|
|
2828
|
+
errors.push(`Item ${i}: ${result.error?.message ?? "Unknown error"}`);
|
|
2829
|
+
}
|
|
2830
|
+
}
|
|
2831
|
+
return {
|
|
2832
|
+
key,
|
|
2833
|
+
valid: invalidCount === 0,
|
|
2834
|
+
totalItems: items.length,
|
|
2835
|
+
invalidItems: invalidCount,
|
|
2836
|
+
errors: errors.length > 0 ? errors : void 0
|
|
2837
|
+
};
|
|
2838
|
+
}
|
|
2839
|
+
function logCorruption(planId, result, origin) {
|
|
2840
|
+
if (!shouldReportCorruption(planId, result.key)) {
|
|
2841
|
+
return;
|
|
2842
|
+
}
|
|
2843
|
+
logger.error(
|
|
2844
|
+
{
|
|
2845
|
+
planId,
|
|
2846
|
+
key: result.key,
|
|
2847
|
+
totalItems: result.totalItems,
|
|
2848
|
+
invalidItems: result.invalidItems,
|
|
2849
|
+
errors: result.errors?.slice(0, 5),
|
|
2850
|
+
origin: typeof origin === "string" ? origin : void 0
|
|
2851
|
+
},
|
|
2852
|
+
"CRDT corruption detected from peer sync"
|
|
2853
|
+
);
|
|
2854
|
+
}
|
|
2855
|
+
function createArrayObserver(planId, key, schema4) {
|
|
2856
|
+
return (event, transaction) => {
|
|
2857
|
+
const doc = event.target.doc;
|
|
2858
|
+
if (!doc) return;
|
|
2859
|
+
const result = validateArray(doc, key, schema4);
|
|
2860
|
+
if (!result.valid) {
|
|
2861
|
+
logCorruption(planId, result, transaction.origin);
|
|
2862
|
+
} else {
|
|
2863
|
+
clearCorruptionState(planId, key);
|
|
2864
|
+
}
|
|
2865
|
+
};
|
|
2866
|
+
}
|
|
2867
|
+
function attachCRDTValidation(planId, doc) {
|
|
2868
|
+
doc.getMap(YDOC_KEYS.METADATA).observe((_event, transaction) => {
|
|
2869
|
+
const result = validateMetadata(doc, planId);
|
|
2870
|
+
if (!result.valid) {
|
|
2871
|
+
logCorruption(planId, result, transaction.origin);
|
|
2872
|
+
} else {
|
|
2873
|
+
clearCorruptionState(planId, YDOC_KEYS.METADATA);
|
|
2874
|
+
}
|
|
2875
|
+
});
|
|
2876
|
+
doc.getArray(YDOC_KEYS.ARTIFACTS).observe(createArrayObserver(planId, YDOC_KEYS.ARTIFACTS, ArtifactSchema));
|
|
2877
|
+
doc.getArray(YDOC_KEYS.DELIVERABLES).observe(createArrayObserver(planId, YDOC_KEYS.DELIVERABLES, DeliverableSchema));
|
|
2878
|
+
doc.getArray(YDOC_KEYS.LINKED_PRS).observe(createArrayObserver(planId, YDOC_KEYS.LINKED_PRS, LinkedPRSchema));
|
|
2879
|
+
doc.getArray(YDOC_KEYS.EVENTS).observe(createArrayObserver(planId, YDOC_KEYS.EVENTS, PlanEventSchema));
|
|
2880
|
+
doc.getArray(YDOC_KEYS.SNAPSHOTS).observe(createArrayObserver(planId, YDOC_KEYS.SNAPSHOTS, PlanSnapshotSchema));
|
|
2881
|
+
doc.getArray(YDOC_KEYS.PR_REVIEW_COMMENTS).observe(
|
|
2882
|
+
createArrayObserver(
|
|
2883
|
+
planId,
|
|
2884
|
+
YDOC_KEYS.PR_REVIEW_COMMENTS,
|
|
2885
|
+
PRReviewCommentSchema
|
|
2886
|
+
)
|
|
2887
|
+
);
|
|
2888
|
+
doc.getArray(YDOC_KEYS.INPUT_REQUESTS).observe(
|
|
2889
|
+
createArrayObserver(planId, YDOC_KEYS.INPUT_REQUESTS, InputRequestSchema)
|
|
2890
|
+
);
|
|
2891
|
+
logger.debug({ planId }, "CRDT validation observers attached");
|
|
2892
|
+
}
|
|
2893
|
+
|
|
2894
|
+
// src/git-local-changes.ts
|
|
2895
|
+
import { execSync } from "child_process";
|
|
2896
|
+
import { readFileSync } from "fs";
|
|
2897
|
+
import { isAbsolute, join as join3, normalize } from "path";
|
|
2898
|
+
function execGit(command, opts) {
|
|
2899
|
+
try {
|
|
2900
|
+
return execSync(command, {
|
|
2901
|
+
cwd: opts.cwd,
|
|
2902
|
+
encoding: "utf-8",
|
|
2903
|
+
timeout: opts.timeout ?? 5e3,
|
|
2904
|
+
maxBuffer: opts.maxBuffer,
|
|
2905
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
2906
|
+
}).trim();
|
|
2907
|
+
} catch {
|
|
2908
|
+
return null;
|
|
2909
|
+
}
|
|
2910
|
+
}
|
|
2911
|
+
function ensureGitRepo(cwd) {
|
|
2912
|
+
const isRepo = execGit("git rev-parse --is-inside-work-tree", { cwd });
|
|
2913
|
+
if (isRepo !== null) return null;
|
|
2914
|
+
logger.info({ cwd }, "Not a git repo, initializing with git init");
|
|
2915
|
+
const initResult = execGit("git init", { cwd });
|
|
2916
|
+
if (initResult === null) {
|
|
2917
|
+
logger.error({ cwd }, "Failed to initialize git repository");
|
|
2918
|
+
return {
|
|
2919
|
+
available: false,
|
|
2920
|
+
reason: "git_error",
|
|
2921
|
+
message: "Failed to initialize git repository"
|
|
2922
|
+
};
|
|
2923
|
+
}
|
|
2924
|
+
logger.info({ cwd }, "Git repository initialized");
|
|
2925
|
+
return null;
|
|
2926
|
+
}
|
|
2927
|
+
function getCurrentBranchName(cwd) {
|
|
2928
|
+
const branch = execGit("git rev-parse --abbrev-ref HEAD", { cwd });
|
|
2929
|
+
if (!branch) {
|
|
2930
|
+
logger.warn({ cwd }, "Could not get current branch");
|
|
2931
|
+
return "unknown";
|
|
2932
|
+
}
|
|
2933
|
+
if (branch === "HEAD") {
|
|
2934
|
+
return execGit("git rev-parse --short HEAD", { cwd }) ?? "unknown";
|
|
2935
|
+
}
|
|
2936
|
+
return branch;
|
|
2937
|
+
}
|
|
2938
|
+
function getGitDiff(cwd) {
|
|
2939
|
+
const headDiff = execGit("git diff HEAD", { cwd, timeout: 3e4, maxBuffer: 10 * 1024 * 1024 });
|
|
2940
|
+
if (headDiff !== null) return headDiff;
|
|
2941
|
+
logger.debug({ cwd }, "git diff HEAD failed, trying --cached");
|
|
2942
|
+
return execGit("git diff --cached", { cwd, timeout: 3e4, maxBuffer: 10 * 1024 * 1024 }) ?? "";
|
|
2943
|
+
}
|
|
2944
|
+
function mergeFilesWithStatus(staged, unstaged, diffFiles) {
|
|
2945
|
+
const allPaths = /* @__PURE__ */ new Set([
|
|
2946
|
+
...staged.map((f) => f.path),
|
|
2947
|
+
...unstaged.map((f) => f.path),
|
|
2948
|
+
...diffFiles.map((f) => f.path)
|
|
2949
|
+
]);
|
|
2950
|
+
const mergedFiles = [];
|
|
2951
|
+
for (const path of allPaths) {
|
|
2952
|
+
const diffFile = diffFiles.find((f) => f.path === path);
|
|
2953
|
+
const stagedFile = staged.find((f) => f.path === path);
|
|
2954
|
+
const unstagedFile = unstaged.find((f) => f.path === path);
|
|
2955
|
+
if (diffFile) {
|
|
2956
|
+
mergedFiles.push(diffFile);
|
|
2957
|
+
} else {
|
|
2958
|
+
const status = stagedFile?.status ?? unstagedFile?.status ?? "modified";
|
|
2959
|
+
mergedFiles.push({
|
|
2960
|
+
path,
|
|
2961
|
+
status,
|
|
2962
|
+
additions: 0,
|
|
2963
|
+
deletions: 0,
|
|
2964
|
+
patch: void 0
|
|
2965
|
+
});
|
|
2966
|
+
}
|
|
2967
|
+
}
|
|
2968
|
+
return mergedFiles.sort((a, b) => a.path.localeCompare(b.path));
|
|
2969
|
+
}
|
|
2970
|
+
function getLocalChanges(cwd) {
|
|
2971
|
+
try {
|
|
2972
|
+
const repoError = ensureGitRepo(cwd);
|
|
2973
|
+
if (repoError) return repoError;
|
|
2974
|
+
const branch = getCurrentBranchName(cwd);
|
|
2975
|
+
let headSha;
|
|
2976
|
+
try {
|
|
2977
|
+
headSha = execGit("git rev-parse HEAD", { cwd }) ?? void 0;
|
|
2978
|
+
} catch {
|
|
2979
|
+
headSha = void 0;
|
|
2980
|
+
}
|
|
2981
|
+
const statusOutput = execGit("git status --porcelain", { cwd, timeout: 1e4 }) ?? "";
|
|
2982
|
+
const { staged, unstaged, untracked } = parseGitStatus(statusOutput);
|
|
2983
|
+
const diffOutput = getGitDiff(cwd);
|
|
2984
|
+
const diffFiles = parseDiffOutput(diffOutput);
|
|
2985
|
+
const mergedFiles = mergeFilesWithStatus(staged, unstaged, diffFiles);
|
|
2986
|
+
logger.debug(
|
|
2987
|
+
{
|
|
2988
|
+
cwd,
|
|
2989
|
+
branch,
|
|
2990
|
+
headSha,
|
|
2991
|
+
stagedCount: staged.length,
|
|
2992
|
+
unstagedCount: unstaged.length,
|
|
2993
|
+
untrackedCount: untracked.length,
|
|
2994
|
+
filesCount: mergedFiles.length
|
|
2995
|
+
},
|
|
2996
|
+
"Got local changes"
|
|
2997
|
+
);
|
|
2998
|
+
return {
|
|
2999
|
+
available: true,
|
|
3000
|
+
branch,
|
|
3001
|
+
baseBranch: "HEAD",
|
|
3002
|
+
headSha,
|
|
3003
|
+
staged,
|
|
3004
|
+
unstaged,
|
|
3005
|
+
untracked,
|
|
3006
|
+
files: mergedFiles
|
|
3007
|
+
};
|
|
3008
|
+
} catch (error) {
|
|
3009
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
3010
|
+
logger.error({ error, cwd }, "Failed to get local changes");
|
|
3011
|
+
return {
|
|
3012
|
+
available: false,
|
|
3013
|
+
reason: "git_error",
|
|
3014
|
+
message: `Git error: ${message}`
|
|
3015
|
+
};
|
|
3016
|
+
}
|
|
3017
|
+
}
|
|
3018
|
+
function parseGitStatus(output) {
|
|
3019
|
+
const staged = [];
|
|
3020
|
+
const unstaged = [];
|
|
3021
|
+
const untracked = [];
|
|
3022
|
+
for (const line of output.split("\n")) {
|
|
3023
|
+
if (!line || line.length < 3) continue;
|
|
3024
|
+
const x = line[0];
|
|
3025
|
+
const y = line[1];
|
|
3026
|
+
let path = line.slice(3);
|
|
3027
|
+
if (path.includes(" -> ")) {
|
|
3028
|
+
path = path.split(" -> ")[1] ?? path;
|
|
3029
|
+
}
|
|
3030
|
+
if (x === "?" && y === "?") {
|
|
3031
|
+
untracked.push(path);
|
|
3032
|
+
continue;
|
|
3033
|
+
}
|
|
3034
|
+
if (x === "!" && y === "!") {
|
|
3035
|
+
continue;
|
|
3036
|
+
}
|
|
3037
|
+
if (x && x !== " " && x !== "?") {
|
|
3038
|
+
staged.push({
|
|
3039
|
+
path,
|
|
3040
|
+
status: parseStatusChar(x),
|
|
3041
|
+
additions: 0,
|
|
3042
|
+
deletions: 0
|
|
3043
|
+
});
|
|
3044
|
+
}
|
|
3045
|
+
if (y && y !== " " && y !== "?") {
|
|
3046
|
+
unstaged.push({
|
|
3047
|
+
path,
|
|
3048
|
+
status: parseStatusChar(y),
|
|
3049
|
+
additions: 0,
|
|
3050
|
+
deletions: 0
|
|
3051
|
+
});
|
|
3052
|
+
}
|
|
3053
|
+
}
|
|
3054
|
+
return { staged, unstaged, untracked };
|
|
3055
|
+
}
|
|
3056
|
+
function parseStatusChar(char) {
|
|
3057
|
+
switch (char) {
|
|
3058
|
+
case "A":
|
|
3059
|
+
return "added";
|
|
3060
|
+
case "M":
|
|
3061
|
+
return "modified";
|
|
3062
|
+
case "D":
|
|
3063
|
+
return "deleted";
|
|
3064
|
+
case "R":
|
|
3065
|
+
return "renamed";
|
|
3066
|
+
case "C":
|
|
3067
|
+
return "copied";
|
|
3068
|
+
case "U":
|
|
3069
|
+
return "modified";
|
|
3070
|
+
default:
|
|
3071
|
+
return "modified";
|
|
3072
|
+
}
|
|
3073
|
+
}
|
|
3074
|
+
function parseDiffOutput(diff) {
|
|
3075
|
+
const files = [];
|
|
3076
|
+
if (!diff.trim()) {
|
|
3077
|
+
return files;
|
|
3078
|
+
}
|
|
3079
|
+
const fileDiffs = diff.split(/(?=diff --git )/);
|
|
3080
|
+
for (const fileDiff of fileDiffs) {
|
|
3081
|
+
if (!fileDiff.trim()) continue;
|
|
3082
|
+
const headerMatch = fileDiff.match(/^diff --git a\/(.+?) b\/(.+)/m);
|
|
3083
|
+
if (!headerMatch) continue;
|
|
3084
|
+
const path = headerMatch[2] ?? headerMatch[1];
|
|
3085
|
+
if (!path) continue;
|
|
3086
|
+
if (fileDiff.includes("Binary files")) {
|
|
3087
|
+
files.push({
|
|
3088
|
+
path,
|
|
3089
|
+
status: detectStatus(fileDiff),
|
|
3090
|
+
additions: 0,
|
|
3091
|
+
deletions: 0,
|
|
3092
|
+
patch: void 0
|
|
3093
|
+
});
|
|
3094
|
+
continue;
|
|
3095
|
+
}
|
|
3096
|
+
let additions = 0;
|
|
3097
|
+
let deletions = 0;
|
|
3098
|
+
for (const line of fileDiff.split("\n")) {
|
|
3099
|
+
if (line.startsWith("+") && !line.startsWith("+++")) {
|
|
3100
|
+
additions++;
|
|
3101
|
+
} else if (line.startsWith("-") && !line.startsWith("---")) {
|
|
3102
|
+
deletions++;
|
|
3103
|
+
}
|
|
3104
|
+
}
|
|
3105
|
+
const patchStart = fileDiff.indexOf("@@");
|
|
3106
|
+
const patch = patchStart >= 0 ? fileDiff.slice(patchStart) : void 0;
|
|
3107
|
+
files.push({
|
|
3108
|
+
path,
|
|
3109
|
+
status: detectStatus(fileDiff),
|
|
3110
|
+
additions,
|
|
3111
|
+
deletions,
|
|
3112
|
+
patch
|
|
3113
|
+
});
|
|
3114
|
+
}
|
|
3115
|
+
return files;
|
|
3116
|
+
}
|
|
3117
|
+
function detectStatus(fileDiff) {
|
|
3118
|
+
if (fileDiff.includes("new file mode")) {
|
|
3119
|
+
return "added";
|
|
3120
|
+
}
|
|
3121
|
+
if (fileDiff.includes("deleted file mode")) {
|
|
3122
|
+
return "deleted";
|
|
3123
|
+
}
|
|
3124
|
+
if (fileDiff.includes("rename from")) {
|
|
3125
|
+
return "renamed";
|
|
3126
|
+
}
|
|
3127
|
+
if (fileDiff.includes("copy from")) {
|
|
3128
|
+
return "copied";
|
|
3129
|
+
}
|
|
3130
|
+
return "modified";
|
|
3131
|
+
}
|
|
3132
|
+
function getFileContent(cwd, filePath) {
|
|
3133
|
+
try {
|
|
3134
|
+
const normalizedPath = normalize(filePath);
|
|
3135
|
+
if (isAbsolute(normalizedPath) || normalizedPath.startsWith("..")) {
|
|
3136
|
+
return { content: null, error: "Invalid file path" };
|
|
3137
|
+
}
|
|
3138
|
+
const fullPath = join3(cwd, normalizedPath);
|
|
3139
|
+
if (!fullPath.startsWith(cwd)) {
|
|
3140
|
+
return { content: null, error: "Invalid file path" };
|
|
3141
|
+
}
|
|
3142
|
+
const content = readFileSync(fullPath, { encoding: "utf-8" });
|
|
3143
|
+
if (content.length > 10 * 1024 * 1024) {
|
|
3144
|
+
return { content: null, error: "File too large to display" };
|
|
3145
|
+
}
|
|
3146
|
+
return { content };
|
|
3147
|
+
} catch (error) {
|
|
3148
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
3149
|
+
if (message.includes("ENOENT")) {
|
|
3150
|
+
return { content: null, error: "File not found" };
|
|
3151
|
+
}
|
|
3152
|
+
if (message.includes("EISDIR")) {
|
|
3153
|
+
return { content: null, error: "Path is a directory" };
|
|
3154
|
+
}
|
|
3155
|
+
return { content: null, error: `Failed to read file: ${message}` };
|
|
3156
|
+
}
|
|
3157
|
+
}
|
|
3158
|
+
|
|
3159
|
+
// src/github-artifacts.ts
|
|
3160
|
+
import { Octokit } from "@octokit/rest";
|
|
3161
|
+
|
|
3162
|
+
// src/config/env/github.ts
|
|
3163
|
+
import { execSync as execSync2 } from "child_process";
|
|
3164
|
+
import { z as z6 } from "zod";
|
|
3165
|
+
function getTokenFromGhCli() {
|
|
3166
|
+
try {
|
|
3167
|
+
const token = execSync2("gh auth token", {
|
|
3168
|
+
encoding: "utf-8",
|
|
3169
|
+
timeout: 5e3,
|
|
3170
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
3171
|
+
}).trim();
|
|
3172
|
+
if (token) {
|
|
3173
|
+
return token;
|
|
3174
|
+
}
|
|
3175
|
+
} catch {
|
|
3176
|
+
}
|
|
3177
|
+
return null;
|
|
3178
|
+
}
|
|
3179
|
+
var schema2 = z6.object({
|
|
3180
|
+
GITHUB_USERNAME: z6.string().optional(),
|
|
3181
|
+
GITHUB_TOKEN: z6.string().optional().transform((val) => val || getTokenFromGhCli() || null),
|
|
3182
|
+
SHIPYARD_ARTIFACTS: z6.string().optional().transform((val) => {
|
|
3183
|
+
if (!val) return true;
|
|
3184
|
+
const setting = val.toLowerCase();
|
|
3185
|
+
return setting !== "disabled" && setting !== "false" && setting !== "0";
|
|
3186
|
+
})
|
|
3187
|
+
});
|
|
3188
|
+
var githubConfig = loadEnv(schema2);
|
|
3189
|
+
|
|
3190
|
+
// src/github-artifacts.ts
|
|
3191
|
+
var ARTIFACTS_BRANCH = "plan-artifacts";
|
|
3192
|
+
function parseRepoString(repo) {
|
|
3193
|
+
const parts = repo.split("/");
|
|
3194
|
+
if (parts.length !== 2 || !parts[0] || !parts[1]) {
|
|
3195
|
+
throw new Error(`Invalid repo format: "${repo}". Expected "owner/repo".`);
|
|
3196
|
+
}
|
|
3197
|
+
return { owner: parts[0], repoName: parts[1] };
|
|
3198
|
+
}
|
|
3199
|
+
function isArtifactsEnabled() {
|
|
3200
|
+
return githubConfig.SHIPYARD_ARTIFACTS;
|
|
3201
|
+
}
|
|
3202
|
+
function resolveGitHubToken() {
|
|
3203
|
+
return githubConfig.GITHUB_TOKEN;
|
|
3204
|
+
}
|
|
3205
|
+
function getErrorStatus(error) {
|
|
3206
|
+
if (!error || typeof error !== "object") return void 0;
|
|
3207
|
+
const record = Object.fromEntries(Object.entries(error));
|
|
3208
|
+
const status = record.status;
|
|
3209
|
+
return typeof status === "number" ? status : void 0;
|
|
3210
|
+
}
|
|
3211
|
+
function isAuthError(error) {
|
|
3212
|
+
const status = getErrorStatus(error);
|
|
3213
|
+
return status === 401 || status === 403;
|
|
3214
|
+
}
|
|
3215
|
+
var GitHubAuthError = class extends Error {
|
|
3216
|
+
constructor(message) {
|
|
3217
|
+
super(message);
|
|
3218
|
+
this.name = "GitHubAuthError";
|
|
3219
|
+
}
|
|
3220
|
+
};
|
|
3221
|
+
async function withTokenRetry(operation) {
|
|
3222
|
+
try {
|
|
3223
|
+
return await operation();
|
|
3224
|
+
} catch (error) {
|
|
3225
|
+
if (isAuthError(error)) {
|
|
3226
|
+
logger.info("GitHub auth error, checking token and retrying...");
|
|
3227
|
+
const newToken = resolveGitHubToken();
|
|
3228
|
+
if (!newToken) {
|
|
3229
|
+
throw new GitHubAuthError(
|
|
3230
|
+
"GitHub token expired and could not be refreshed.\n\nTo fix this, run in your terminal:\n gh auth login\n\nOr set GITHUB_TOKEN environment variable in your MCP config."
|
|
3231
|
+
);
|
|
3232
|
+
}
|
|
3233
|
+
try {
|
|
3234
|
+
return await operation();
|
|
3235
|
+
} catch (retryError) {
|
|
3236
|
+
if (isAuthError(retryError)) {
|
|
3237
|
+
throw new GitHubAuthError(
|
|
3238
|
+
"GitHub authentication failed after token refresh.\n\nYour token may not have the required permissions.\nRun: gh auth login --scopes repo\n\nOr check your GITHUB_TOKEN has repo access."
|
|
3239
|
+
);
|
|
3240
|
+
}
|
|
3241
|
+
throw retryError;
|
|
3242
|
+
}
|
|
3243
|
+
}
|
|
3244
|
+
throw error;
|
|
3245
|
+
}
|
|
3246
|
+
}
|
|
3247
|
+
function getOctokit() {
|
|
3248
|
+
const token = resolveGitHubToken();
|
|
3249
|
+
if (!token) {
|
|
3250
|
+
return null;
|
|
3251
|
+
}
|
|
3252
|
+
return new Octokit({ auth: token });
|
|
3253
|
+
}
|
|
3254
|
+
function isGitHubConfigured() {
|
|
3255
|
+
return !!resolveGitHubToken();
|
|
3256
|
+
}
|
|
3257
|
+
async function ensureArtifactsBranch(repo) {
|
|
3258
|
+
return withTokenRetry(async () => {
|
|
3259
|
+
const octokit = getOctokit();
|
|
3260
|
+
if (!octokit) {
|
|
3261
|
+
throw new Error("GITHUB_TOKEN not set");
|
|
3262
|
+
}
|
|
3263
|
+
const { owner, repoName } = parseRepoString(repo);
|
|
3264
|
+
try {
|
|
3265
|
+
await octokit.repos.getBranch({
|
|
3266
|
+
owner,
|
|
3267
|
+
repo: repoName,
|
|
3268
|
+
branch: ARTIFACTS_BRANCH
|
|
3269
|
+
});
|
|
3270
|
+
logger.debug({ repo }, "Artifacts branch exists");
|
|
3271
|
+
return;
|
|
3272
|
+
} catch (error) {
|
|
3273
|
+
if (getErrorStatus(error) !== 404) {
|
|
3274
|
+
throw error;
|
|
3275
|
+
}
|
|
3276
|
+
}
|
|
3277
|
+
logger.info({ repo }, "Creating artifacts branch");
|
|
3278
|
+
try {
|
|
3279
|
+
const { data: repoData } = await octokit.repos.get({ owner, repo: repoName });
|
|
3280
|
+
const defaultBranch = repoData.default_branch;
|
|
3281
|
+
const { data: refData } = await octokit.git.getRef({
|
|
3282
|
+
owner,
|
|
3283
|
+
repo: repoName,
|
|
3284
|
+
ref: `heads/${defaultBranch}`
|
|
3285
|
+
});
|
|
3286
|
+
await octokit.git.createRef({
|
|
3287
|
+
owner,
|
|
3288
|
+
repo: repoName,
|
|
3289
|
+
ref: `refs/heads/${ARTIFACTS_BRANCH}`,
|
|
3290
|
+
sha: refData.object.sha
|
|
3291
|
+
});
|
|
3292
|
+
logger.info({ repo, branch: ARTIFACTS_BRANCH }, "Created artifacts branch");
|
|
3293
|
+
} catch (error) {
|
|
3294
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
3295
|
+
throw new Error(
|
|
3296
|
+
`Failed to create "${ARTIFACTS_BRANCH}" branch. Please create it manually:
|
|
3297
|
+
|
|
3298
|
+
git checkout --orphan ${ARTIFACTS_BRANCH}
|
|
3299
|
+
git rm -rf .
|
|
3300
|
+
git commit --allow-empty -m "Initialize plan artifacts"
|
|
3301
|
+
git push -u origin ${ARTIFACTS_BRANCH}
|
|
3302
|
+
git checkout main
|
|
3303
|
+
|
|
3304
|
+
Error: ${message}`
|
|
3305
|
+
);
|
|
3306
|
+
}
|
|
3307
|
+
});
|
|
3308
|
+
}
|
|
3309
|
+
async function uploadArtifact(params) {
|
|
3310
|
+
return withTokenRetry(async () => {
|
|
3311
|
+
const octokit = getOctokit();
|
|
3312
|
+
if (!octokit) {
|
|
3313
|
+
throw new Error("GITHUB_TOKEN not set");
|
|
3314
|
+
}
|
|
3315
|
+
const { repo, planId, filename, content } = params;
|
|
3316
|
+
const { owner, repoName } = parseRepoString(repo);
|
|
3317
|
+
const path = `plans/${planId}/${filename}`;
|
|
3318
|
+
await ensureArtifactsBranch(repo);
|
|
3319
|
+
let existingSha;
|
|
3320
|
+
try {
|
|
3321
|
+
const { data } = await octokit.repos.getContent({
|
|
3322
|
+
owner,
|
|
3323
|
+
repo: repoName,
|
|
3324
|
+
path,
|
|
3325
|
+
ref: ARTIFACTS_BRANCH
|
|
3326
|
+
});
|
|
3327
|
+
if (!Array.isArray(data) && data.type === "file") {
|
|
3328
|
+
existingSha = data.sha;
|
|
3329
|
+
}
|
|
3330
|
+
} catch (error) {
|
|
3331
|
+
if (getErrorStatus(error) !== 404) {
|
|
3332
|
+
throw error;
|
|
3333
|
+
}
|
|
3334
|
+
}
|
|
3335
|
+
await octokit.repos.createOrUpdateFileContents({
|
|
3336
|
+
owner,
|
|
3337
|
+
repo: repoName,
|
|
3338
|
+
path,
|
|
3339
|
+
message: `Add artifact: ${filename}`,
|
|
3340
|
+
content,
|
|
3341
|
+
branch: ARTIFACTS_BRANCH,
|
|
3342
|
+
sha: existingSha
|
|
3343
|
+
});
|
|
3344
|
+
const url = `https://raw.githubusercontent.com/${repo}/${ARTIFACTS_BRANCH}/${path}`;
|
|
3345
|
+
logger.info({ repo, path, url }, "Artifact uploaded");
|
|
3346
|
+
return url;
|
|
3347
|
+
});
|
|
3348
|
+
}
|
|
3349
|
+
|
|
3350
|
+
// src/hook-handlers.ts
|
|
3351
|
+
import { ServerBlockNoteEditor } from "@blocknote/server-util";
|
|
3352
|
+
|
|
3353
|
+
// ../../packages/shared/dist/index.mjs
|
|
3354
|
+
import { createHash, randomBytes } from "crypto";
|
|
3355
|
+
function computeHash(content) {
|
|
3356
|
+
return createHash("sha256").update(content).digest("hex").slice(0, 16);
|
|
3357
|
+
}
|
|
3358
|
+
function generateSessionToken() {
|
|
3359
|
+
return randomBytes(32).toString("base64url");
|
|
3360
|
+
}
|
|
3361
|
+
function hashSessionToken(token) {
|
|
3362
|
+
return createHash("sha256").update(token).digest("hex");
|
|
3363
|
+
}
|
|
3364
|
+
var APPROVAL_LONG_POLL_TIMEOUT_MS = 1800 * 1e3;
|
|
3365
|
+
var DEFAULT_TRPC_TIMEOUT_MS = 10 * 1e3;
|
|
3366
|
+
|
|
3367
|
+
// src/hook-handlers.ts
|
|
3368
|
+
import { TRPCError as TRPCError2 } from "@trpc/server";
|
|
3369
|
+
import { nanoid as nanoid4 } from "nanoid";
|
|
3370
|
+
import open from "open";
|
|
3371
|
+
|
|
3372
|
+
// src/config/env/web.ts
|
|
3373
|
+
import { z as z7 } from "zod";
|
|
3374
|
+
var schema3 = z7.object({
|
|
3375
|
+
SHIPYARD_WEB_URL: z7.string().url().default("https://schoolai.github.io/shipyard")
|
|
3376
|
+
});
|
|
3377
|
+
var webConfig = loadEnv(schema3);
|
|
3378
|
+
|
|
3379
|
+
// src/hub-client.ts
|
|
3380
|
+
import { WebsocketProvider } from "y-websocket";
|
|
3381
|
+
import * as Y3 from "yjs";
|
|
3382
|
+
var providers = /* @__PURE__ */ new Map();
|
|
3383
|
+
var docs = /* @__PURE__ */ new Map();
|
|
3384
|
+
var hubPort = null;
|
|
3385
|
+
var initialized = false;
|
|
3386
|
+
async function initHubClient(port) {
|
|
3387
|
+
if (initialized) {
|
|
3388
|
+
logger.warn("Hub client already initialized");
|
|
3389
|
+
return;
|
|
3390
|
+
}
|
|
3391
|
+
hubPort = port;
|
|
3392
|
+
initialized = true;
|
|
3393
|
+
logger.info({ hubPort }, "Hub client initialized, will connect to registry hub");
|
|
3394
|
+
}
|
|
3395
|
+
function isHubClientInitialized() {
|
|
3396
|
+
return initialized;
|
|
3397
|
+
}
|
|
3398
|
+
async function getOrCreateDoc(docName) {
|
|
3399
|
+
const existing = docs.get(docName);
|
|
3400
|
+
if (existing) {
|
|
3401
|
+
return existing;
|
|
3402
|
+
}
|
|
3403
|
+
if (!initialized || !hubPort) {
|
|
3404
|
+
throw new Error("Hub client not initialized. Call initHubClient() first.");
|
|
3405
|
+
}
|
|
3406
|
+
const doc = new Y3.Doc();
|
|
3407
|
+
docs.set(docName, doc);
|
|
3408
|
+
const hubUrl = `ws://localhost:${hubPort}`;
|
|
3409
|
+
const provider = new WebsocketProvider(hubUrl, docName, doc, {
|
|
3410
|
+
connect: true,
|
|
3411
|
+
maxBackoffTime: 2500
|
|
3412
|
+
});
|
|
3413
|
+
providers.set(docName, provider);
|
|
3414
|
+
await new Promise((resolve2, reject) => {
|
|
3415
|
+
if (provider.synced) {
|
|
3416
|
+
logger.debug({ docName }, "Provider already synced");
|
|
3417
|
+
resolve2();
|
|
3418
|
+
return;
|
|
3419
|
+
}
|
|
3420
|
+
const onSync = (isSynced) => {
|
|
3421
|
+
if (isSynced) {
|
|
3422
|
+
logger.debug({ docName }, "Provider synced via sync event");
|
|
3423
|
+
provider.off("sync", onSync);
|
|
3424
|
+
clearTimeout(timeoutId);
|
|
3425
|
+
resolve2();
|
|
3426
|
+
}
|
|
3427
|
+
};
|
|
3428
|
+
provider.on("sync", onSync);
|
|
3429
|
+
const timeoutId = setTimeout(() => {
|
|
3430
|
+
if (!provider.synced) {
|
|
3431
|
+
provider.off("sync", onSync);
|
|
3432
|
+
logger.error({ docName, synced: provider.synced }, "Hub sync timeout - cannot proceed");
|
|
3433
|
+
reject(new Error(`Failed to sync document '${docName}' with hub within 10 seconds`));
|
|
3434
|
+
}
|
|
3435
|
+
}, 1e4);
|
|
3436
|
+
});
|
|
3437
|
+
logger.info({ docName, hubUrl }, "Connected to hub for document sync");
|
|
3438
|
+
return doc;
|
|
3439
|
+
}
|
|
3440
|
+
async function hasActiveConnections(planId) {
|
|
3441
|
+
if (!hubPort) return false;
|
|
3442
|
+
try {
|
|
3443
|
+
const res = await fetch(`http://localhost:${hubPort}${ROUTES.PLAN_HAS_CONNECTIONS(planId)}`, {
|
|
3444
|
+
signal: AbortSignal.timeout(500)
|
|
3445
|
+
});
|
|
3446
|
+
if (!res.ok) return false;
|
|
3447
|
+
const data = HasConnectionsResponseSchema.parse(await res.json());
|
|
3448
|
+
return data.hasConnections;
|
|
3449
|
+
} catch {
|
|
3450
|
+
return false;
|
|
3451
|
+
}
|
|
3452
|
+
}
|
|
3453
|
+
|
|
3454
|
+
// src/webrtc-provider.ts
|
|
3455
|
+
import wrtc from "@roamhq/wrtc";
|
|
3456
|
+
import { WebrtcProvider } from "y-webrtc";
|
|
3457
|
+
|
|
3458
|
+
// src/server-identity.ts
|
|
3459
|
+
import { execSync as execSync3 } from "child_process";
|
|
3460
|
+
import os from "os";
|
|
3461
|
+
import { basename } from "path";
|
|
3462
|
+
import { z as z8 } from "zod";
|
|
3463
|
+
var GitHubUserResponseSchema = z8.object({
|
|
3464
|
+
login: z8.string().optional()
|
|
3465
|
+
});
|
|
3466
|
+
var cachedUsername = null;
|
|
3467
|
+
var usernameResolved = false;
|
|
3468
|
+
var cachedRepoName = null;
|
|
3469
|
+
function getRepositoryFullName() {
|
|
3470
|
+
if (cachedRepoName !== null) {
|
|
3471
|
+
return cachedRepoName || null;
|
|
3472
|
+
}
|
|
3473
|
+
try {
|
|
3474
|
+
const repoName = execSync3("gh repo view --json nameWithOwner --jq .nameWithOwner", {
|
|
3475
|
+
encoding: "utf-8",
|
|
3476
|
+
timeout: 5e3,
|
|
3477
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
3478
|
+
}).trim();
|
|
3479
|
+
if (!repoName) {
|
|
3480
|
+
cachedRepoName = "";
|
|
3481
|
+
return null;
|
|
3482
|
+
}
|
|
3483
|
+
cachedRepoName = repoName;
|
|
3484
|
+
return cachedRepoName;
|
|
3485
|
+
} catch {
|
|
3486
|
+
cachedRepoName = "";
|
|
3487
|
+
return null;
|
|
3488
|
+
}
|
|
3489
|
+
}
|
|
3490
|
+
async function getGitHubUsername() {
|
|
3491
|
+
if (usernameResolved && cachedUsername) {
|
|
3492
|
+
return cachedUsername;
|
|
3493
|
+
}
|
|
3494
|
+
if (githubConfig.GITHUB_USERNAME) {
|
|
3495
|
+
cachedUsername = githubConfig.GITHUB_USERNAME;
|
|
3496
|
+
usernameResolved = true;
|
|
3497
|
+
logger.info({ username: cachedUsername }, "Using GITHUB_USERNAME from env");
|
|
3498
|
+
return cachedUsername;
|
|
3499
|
+
}
|
|
3500
|
+
if (githubConfig.GITHUB_TOKEN) {
|
|
3501
|
+
const username = await getUsernameFromToken(githubConfig.GITHUB_TOKEN);
|
|
3502
|
+
if (username) {
|
|
3503
|
+
cachedUsername = username;
|
|
3504
|
+
usernameResolved = true;
|
|
3505
|
+
logger.info({ username }, "Resolved username from GITHUB_TOKEN via API");
|
|
3506
|
+
return cachedUsername;
|
|
3507
|
+
}
|
|
3508
|
+
}
|
|
3509
|
+
const cliUsername = getUsernameFromCLI();
|
|
3510
|
+
if (cliUsername) {
|
|
3511
|
+
cachedUsername = cliUsername;
|
|
3512
|
+
usernameResolved = true;
|
|
3513
|
+
logger.info({ username: cliUsername }, "Resolved username from gh CLI");
|
|
3514
|
+
return cachedUsername;
|
|
3515
|
+
}
|
|
3516
|
+
const gitUsername = getUsernameFromGitConfig();
|
|
3517
|
+
if (gitUsername) {
|
|
3518
|
+
cachedUsername = gitUsername;
|
|
3519
|
+
usernameResolved = true;
|
|
3520
|
+
logger.warn({ username: gitUsername }, "Using git config user.name (UNVERIFIED)");
|
|
3521
|
+
return cachedUsername;
|
|
3522
|
+
}
|
|
3523
|
+
const osUsername = process.env.USER || process.env.USERNAME;
|
|
3524
|
+
if (osUsername) {
|
|
3525
|
+
cachedUsername = osUsername.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
3526
|
+
usernameResolved = true;
|
|
3527
|
+
logger.warn(
|
|
3528
|
+
{ username: cachedUsername, original: osUsername },
|
|
3529
|
+
"Using sanitized OS username (UNVERIFIED)"
|
|
3530
|
+
);
|
|
3531
|
+
return cachedUsername;
|
|
3532
|
+
}
|
|
3533
|
+
usernameResolved = true;
|
|
3534
|
+
throw new Error(
|
|
3535
|
+
'GitHub username required but could not be determined.\n\nConfigure ONE of:\n1. GITHUB_USERNAME=your-username (explicit)\n2. GITHUB_TOKEN=ghp_xxx (will fetch from API)\n3. gh auth login (uses CLI)\n4. git config --global user.name "your-username"\n5. Set USER or USERNAME environment variable\n\nFor remote agents: Use option 1 or 2'
|
|
3536
|
+
);
|
|
3537
|
+
}
|
|
3538
|
+
async function getUsernameFromToken(token) {
|
|
3539
|
+
try {
|
|
3540
|
+
const response = await fetch("https://api.github.com/user", {
|
|
3541
|
+
headers: {
|
|
3542
|
+
Authorization: `Bearer ${token}`,
|
|
3543
|
+
Accept: "application/vnd.github.v3+json",
|
|
3544
|
+
"User-Agent": "shipyard-mcp-server"
|
|
3545
|
+
},
|
|
3546
|
+
signal: AbortSignal.timeout(5e3)
|
|
3547
|
+
});
|
|
3548
|
+
if (!response.ok) return null;
|
|
3549
|
+
const user = GitHubUserResponseSchema.parse(await response.json());
|
|
3550
|
+
return user.login ?? null;
|
|
3551
|
+
} catch (error) {
|
|
3552
|
+
logger.debug({ error }, "GitHub API failed");
|
|
3553
|
+
return null;
|
|
3554
|
+
}
|
|
3555
|
+
}
|
|
3556
|
+
function getUsernameFromCLI() {
|
|
3557
|
+
try {
|
|
3558
|
+
const username = execSync3("gh api user --jq .login", {
|
|
3559
|
+
encoding: "utf-8",
|
|
3560
|
+
timeout: 5e3,
|
|
3561
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
3562
|
+
}).trim();
|
|
3563
|
+
return username || null;
|
|
3564
|
+
} catch {
|
|
3565
|
+
return null;
|
|
3566
|
+
}
|
|
3567
|
+
}
|
|
3568
|
+
function getUsernameFromGitConfig() {
|
|
3569
|
+
try {
|
|
3570
|
+
const username = execSync3("git config user.name", {
|
|
3571
|
+
encoding: "utf-8",
|
|
3572
|
+
timeout: 5e3,
|
|
3573
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
3574
|
+
}).trim();
|
|
3575
|
+
return username || null;
|
|
3576
|
+
} catch {
|
|
3577
|
+
return null;
|
|
3578
|
+
}
|
|
3579
|
+
}
|
|
3580
|
+
async function getVerifiedGitHubUsername() {
|
|
3581
|
+
if (githubConfig.GITHUB_USERNAME) {
|
|
3582
|
+
logger.info({ username: githubConfig.GITHUB_USERNAME }, "Using GITHUB_USERNAME from env");
|
|
3583
|
+
return githubConfig.GITHUB_USERNAME;
|
|
3584
|
+
}
|
|
3585
|
+
if (githubConfig.GITHUB_TOKEN) {
|
|
3586
|
+
try {
|
|
3587
|
+
const username = await getUsernameFromToken(githubConfig.GITHUB_TOKEN);
|
|
3588
|
+
if (username) {
|
|
3589
|
+
logger.info({ username }, "Got verified username from GITHUB_TOKEN");
|
|
3590
|
+
return username;
|
|
3591
|
+
}
|
|
3592
|
+
} catch (error) {
|
|
3593
|
+
logger.warn({ error }, "Failed to get username from GITHUB_TOKEN");
|
|
3594
|
+
}
|
|
3595
|
+
}
|
|
3596
|
+
try {
|
|
3597
|
+
const username = getUsernameFromCLI();
|
|
3598
|
+
if (username) {
|
|
3599
|
+
logger.info({ username }, "Got verified username from gh CLI");
|
|
3600
|
+
return username;
|
|
3601
|
+
}
|
|
3602
|
+
} catch (error) {
|
|
3603
|
+
logger.debug({ error }, "Failed to get username from gh CLI");
|
|
3604
|
+
}
|
|
3605
|
+
logger.warn("No verified GitHub authentication available");
|
|
3606
|
+
return null;
|
|
3607
|
+
}
|
|
3608
|
+
function getGitBranch() {
|
|
3609
|
+
try {
|
|
3610
|
+
return execSync3("git branch --show-current", {
|
|
3611
|
+
encoding: "utf-8",
|
|
3612
|
+
timeout: 2e3,
|
|
3613
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
3614
|
+
}).trim() || void 0;
|
|
3615
|
+
} catch {
|
|
3616
|
+
return void 0;
|
|
3617
|
+
}
|
|
3618
|
+
}
|
|
3619
|
+
function getEnvironmentContext() {
|
|
3620
|
+
return {
|
|
3621
|
+
projectName: basename(process.cwd()) || void 0,
|
|
3622
|
+
branch: getGitBranch(),
|
|
3623
|
+
hostname: os.hostname(),
|
|
3624
|
+
repo: getRepositoryFullName() || void 0
|
|
3625
|
+
};
|
|
3626
|
+
}
|
|
3627
|
+
|
|
3628
|
+
// src/webrtc-provider.ts
|
|
3629
|
+
var SIGNALING_SERVER = process.env.SIGNALING_URL || "wss://shipyard-signaling.jacob-191.workers.dev";
|
|
3630
|
+
var globalAny = globalThis;
|
|
3631
|
+
if (typeof globalAny.RTCPeerConnection === "undefined") {
|
|
3632
|
+
globalAny.RTCPeerConnection = wrtc.RTCPeerConnection;
|
|
3633
|
+
globalAny.RTCSessionDescription = wrtc.RTCSessionDescription;
|
|
3634
|
+
globalAny.RTCIceCandidate = wrtc.RTCIceCandidate;
|
|
3635
|
+
}
|
|
3636
|
+
async function createWebRtcProvider(ydoc, planId) {
|
|
3637
|
+
const iceServers = [
|
|
3638
|
+
{ urls: "stun:stun.l.google.com:19302" },
|
|
3639
|
+
{ urls: "stun:stun1.l.google.com:19302" }
|
|
3640
|
+
];
|
|
3641
|
+
if (process.env.TURN_URL && process.env.TURN_USERNAME && process.env.TURN_CREDENTIAL) {
|
|
3642
|
+
iceServers.push({
|
|
3643
|
+
urls: process.env.TURN_URL,
|
|
3644
|
+
username: process.env.TURN_USERNAME,
|
|
3645
|
+
credential: process.env.TURN_CREDENTIAL
|
|
3646
|
+
});
|
|
3647
|
+
logger.info({ turnUrl: process.env.TURN_URL }, "TURN server configured");
|
|
3648
|
+
}
|
|
3649
|
+
const roomName = `shipyard-${planId}`;
|
|
3650
|
+
const provider = new WebrtcProvider(roomName, ydoc, {
|
|
3651
|
+
signaling: [SIGNALING_SERVER],
|
|
3652
|
+
peerOpts: {
|
|
3653
|
+
// @ts-expect-error - wrtc type definitions don't match runtime structure
|
|
3654
|
+
wrtc: wrtc.default || wrtc,
|
|
3655
|
+
config: {
|
|
3656
|
+
iceServers
|
|
3657
|
+
}
|
|
3658
|
+
}
|
|
3659
|
+
});
|
|
3660
|
+
const username = await getGitHubUsername().catch(() => void 0);
|
|
3661
|
+
const fallbackId = `mcp-anon-${crypto.randomUUID().slice(0, 8)}`;
|
|
3662
|
+
const userId = username ? `mcp-${username}` : fallbackId;
|
|
3663
|
+
const displayName = username ? `Claude Code (${username})` : "Claude Code";
|
|
3664
|
+
const awarenessState = {
|
|
3665
|
+
user: {
|
|
3666
|
+
id: userId,
|
|
3667
|
+
name: displayName,
|
|
3668
|
+
color: "#0066cc"
|
|
3669
|
+
},
|
|
3670
|
+
platform: "claude-code",
|
|
3671
|
+
status: "approved",
|
|
3672
|
+
isOwner: true,
|
|
3673
|
+
webrtcPeerId: crypto.randomUUID(),
|
|
3674
|
+
context: getEnvironmentContext()
|
|
3675
|
+
};
|
|
3676
|
+
provider.awareness.setLocalStateField("planStatus", awarenessState);
|
|
3677
|
+
logger.info(
|
|
3678
|
+
{ planId, username: username ?? fallbackId, platform: "claude-code", hasContext: true },
|
|
3679
|
+
"MCP awareness state set"
|
|
3680
|
+
);
|
|
3681
|
+
sendApprovalStateToSignaling(provider, planId, username ?? fallbackId);
|
|
3682
|
+
setupProviderListeners(provider, planId);
|
|
3683
|
+
logger.info(
|
|
3684
|
+
{
|
|
3685
|
+
planId,
|
|
3686
|
+
roomName,
|
|
3687
|
+
signaling: SIGNALING_SERVER,
|
|
3688
|
+
hasTurn: iceServers.length > 2
|
|
3689
|
+
},
|
|
3690
|
+
"WebRTC provider created"
|
|
3691
|
+
);
|
|
3692
|
+
return provider;
|
|
3693
|
+
}
|
|
3694
|
+
function sendApprovalStateToSignaling(provider, planId, username) {
|
|
3695
|
+
const signalingConns = getSignalingConnections(provider);
|
|
3696
|
+
if (signalingConns.length === 0) {
|
|
3697
|
+
setTimeout(() => sendApprovalStateToSignaling(provider, planId, username), 1e3);
|
|
3698
|
+
return;
|
|
3699
|
+
}
|
|
3700
|
+
const identifyMessage = JSON.stringify({
|
|
3701
|
+
type: "subscribe",
|
|
3702
|
+
topics: [],
|
|
3703
|
+
userId: username
|
|
3704
|
+
});
|
|
3705
|
+
const approvalStateMessage = JSON.stringify({
|
|
3706
|
+
type: "approval_state",
|
|
3707
|
+
planId,
|
|
3708
|
+
ownerId: username,
|
|
3709
|
+
approvedUsers: [username],
|
|
3710
|
+
rejectedUsers: []
|
|
3711
|
+
});
|
|
3712
|
+
for (const conn of signalingConns) {
|
|
3713
|
+
const ws = conn.ws;
|
|
3714
|
+
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
3715
|
+
ws.send(identifyMessage);
|
|
3716
|
+
ws.send(approvalStateMessage);
|
|
3717
|
+
logger.info({ planId, username }, "Pushed identity and approval state to signaling server");
|
|
3718
|
+
}
|
|
3719
|
+
}
|
|
3720
|
+
}
|
|
3721
|
+
function setupProviderListeners(provider, planId) {
|
|
3722
|
+
provider.on("peers", (event) => {
|
|
3723
|
+
const peerCount = event.webrtcPeers.length;
|
|
3724
|
+
if (event.added.length > 0) {
|
|
3725
|
+
logger.info(
|
|
3726
|
+
{
|
|
3727
|
+
planId,
|
|
3728
|
+
added: event.added,
|
|
3729
|
+
totalPeers: peerCount
|
|
3730
|
+
},
|
|
3731
|
+
"WebRTC peer connected"
|
|
3732
|
+
);
|
|
3733
|
+
}
|
|
3734
|
+
if (event.removed.length > 0) {
|
|
3735
|
+
logger.info(
|
|
3736
|
+
{
|
|
3737
|
+
planId,
|
|
3738
|
+
removed: event.removed,
|
|
3739
|
+
totalPeers: peerCount
|
|
3740
|
+
},
|
|
3741
|
+
"WebRTC peer disconnected"
|
|
3742
|
+
);
|
|
3743
|
+
}
|
|
3744
|
+
});
|
|
3745
|
+
provider.on("synced", (event) => {
|
|
3746
|
+
logger.info(
|
|
3747
|
+
{
|
|
3748
|
+
planId,
|
|
3749
|
+
synced: event.synced
|
|
3750
|
+
},
|
|
3751
|
+
"WebRTC sync status changed"
|
|
3752
|
+
);
|
|
3753
|
+
});
|
|
3754
|
+
provider.on("status", (event) => {
|
|
3755
|
+
logger.info(
|
|
3756
|
+
{
|
|
3757
|
+
planId,
|
|
3758
|
+
connected: event.connected
|
|
3759
|
+
},
|
|
3760
|
+
"WebRTC signaling status changed"
|
|
3761
|
+
);
|
|
3762
|
+
});
|
|
3763
|
+
}
|
|
3764
|
+
|
|
3765
|
+
// src/doc-store.ts
|
|
3766
|
+
var currentMode = "uninitialized";
|
|
3767
|
+
var webrtcProviders = /* @__PURE__ */ new Map();
|
|
3768
|
+
function initAsHub() {
|
|
3769
|
+
if (currentMode !== "uninitialized") {
|
|
3770
|
+
logger.warn({ currentMode }, "Doc store already initialized");
|
|
3771
|
+
return;
|
|
3772
|
+
}
|
|
3773
|
+
currentMode = "hub";
|
|
3774
|
+
logger.info("Doc store initialized as hub (registry server mode)");
|
|
3775
|
+
}
|
|
3776
|
+
async function initAsClient(registryPort) {
|
|
3777
|
+
if (currentMode !== "uninitialized") {
|
|
3778
|
+
logger.warn({ currentMode }, "Doc store already initialized");
|
|
3779
|
+
return;
|
|
3780
|
+
}
|
|
3781
|
+
await initHubClient(registryPort);
|
|
3782
|
+
currentMode = "client";
|
|
3783
|
+
logger.info({ registryPort }, "Doc store initialized as client (hub-client mode)");
|
|
3784
|
+
}
|
|
3785
|
+
async function getOrCreateDoc3(docName) {
|
|
3786
|
+
let doc;
|
|
3787
|
+
switch (currentMode) {
|
|
3788
|
+
case "hub":
|
|
3789
|
+
doc = await getOrCreateDoc2(docName);
|
|
3790
|
+
break;
|
|
3791
|
+
case "client":
|
|
3792
|
+
doc = await getOrCreateDoc(docName);
|
|
3793
|
+
break;
|
|
3794
|
+
case "uninitialized":
|
|
3795
|
+
if (isHubClientInitialized()) {
|
|
3796
|
+
currentMode = "client";
|
|
3797
|
+
doc = await getOrCreateDoc(docName);
|
|
3798
|
+
} else {
|
|
3799
|
+
logger.warn("Doc store not initialized, defaulting to registry server mode");
|
|
3800
|
+
currentMode = "hub";
|
|
3801
|
+
doc = await getOrCreateDoc2(docName);
|
|
3802
|
+
}
|
|
3803
|
+
}
|
|
3804
|
+
if (!webrtcProviders.has(docName)) {
|
|
3805
|
+
try {
|
|
3806
|
+
const provider = await createWebRtcProvider(doc, docName);
|
|
3807
|
+
webrtcProviders.set(docName, provider);
|
|
3808
|
+
logger.info({ docName }, "WebRTC P2P sync enabled for plan");
|
|
3809
|
+
} catch (error) {
|
|
3810
|
+
logger.error({ error, docName }, "Failed to create WebRTC provider - P2P sync unavailable");
|
|
3811
|
+
}
|
|
3812
|
+
}
|
|
3813
|
+
return doc;
|
|
3814
|
+
}
|
|
3815
|
+
async function hasActiveConnections3(planId) {
|
|
3816
|
+
switch (currentMode) {
|
|
3817
|
+
case "hub":
|
|
3818
|
+
return hasActiveConnections2(planId);
|
|
3819
|
+
case "client":
|
|
3820
|
+
return await hasActiveConnections(planId);
|
|
3821
|
+
case "uninitialized":
|
|
3822
|
+
return false;
|
|
3823
|
+
}
|
|
3824
|
+
}
|
|
3825
|
+
|
|
3826
|
+
// src/hook-handlers.ts
|
|
3827
|
+
async function parseMarkdownToBlocks(markdown) {
|
|
3828
|
+
const editor = ServerBlockNoteEditor.create();
|
|
3829
|
+
return await editor.tryParseMarkdownToBlocks(markdown);
|
|
3830
|
+
}
|
|
3831
|
+
function extractTitleFromBlocks(blocks) {
|
|
3832
|
+
const UNTITLED = "Untitled Plan";
|
|
3833
|
+
const firstBlock = blocks[0];
|
|
3834
|
+
if (!firstBlock) return UNTITLED;
|
|
3835
|
+
const content = firstBlock.content;
|
|
3836
|
+
if (!content || !Array.isArray(content) || content.length === 0) {
|
|
3837
|
+
return UNTITLED;
|
|
3838
|
+
}
|
|
3839
|
+
const firstContent = content[0];
|
|
3840
|
+
if (!firstContent || typeof firstContent !== "object" || !("text" in firstContent)) {
|
|
3841
|
+
return UNTITLED;
|
|
3842
|
+
}
|
|
3843
|
+
const record = Object.fromEntries(Object.entries(firstContent));
|
|
3844
|
+
const text = record.text;
|
|
3845
|
+
if (typeof text !== "string") {
|
|
3846
|
+
return UNTITLED;
|
|
3847
|
+
}
|
|
3848
|
+
if (firstBlock.type === "heading") {
|
|
3849
|
+
return text;
|
|
3850
|
+
}
|
|
3851
|
+
return text.slice(0, 50);
|
|
3852
|
+
}
|
|
3853
|
+
async function createSessionHandler(input, ctx) {
|
|
3854
|
+
const existingSession = getSessionState(input.sessionId);
|
|
3855
|
+
if (existingSession) {
|
|
3856
|
+
const url2 = createPlanWebUrl(webConfig.SHIPYARD_WEB_URL, existingSession.planId);
|
|
3857
|
+
ctx.logger.info(
|
|
3858
|
+
{ planId: existingSession.planId, sessionId: input.sessionId },
|
|
3859
|
+
"Returning existing session (idempotent)"
|
|
3860
|
+
);
|
|
3861
|
+
return { planId: existingSession.planId, url: url2 };
|
|
3862
|
+
}
|
|
3863
|
+
const planId = nanoid4();
|
|
3864
|
+
const now = Date.now();
|
|
3865
|
+
ctx.logger.info(
|
|
3866
|
+
{ planId, sessionId: input.sessionId, agentType: input.agentType },
|
|
3867
|
+
"Creating plan from hook"
|
|
3868
|
+
);
|
|
3869
|
+
const PLAN_IN_PROGRESS = "Plan in progress...";
|
|
3870
|
+
const ownerId = await getGitHubUsername();
|
|
3871
|
+
ctx.logger.info({ ownerId }, "GitHub username for plan ownership");
|
|
3872
|
+
const repo = getRepositoryFullName() || void 0;
|
|
3873
|
+
if (repo) {
|
|
3874
|
+
ctx.logger.info({ repo }, "Auto-detected repository from current directory");
|
|
3875
|
+
}
|
|
3876
|
+
const ydoc = await ctx.getOrCreateDoc(planId);
|
|
3877
|
+
const origin = parseClaudeCodeOrigin(input.metadata) || {
|
|
3878
|
+
platform: "claude-code",
|
|
3879
|
+
sessionId: input.sessionId,
|
|
3880
|
+
transcriptPath: ""
|
|
3881
|
+
};
|
|
3882
|
+
initPlanMetadata(ydoc, {
|
|
3883
|
+
id: planId,
|
|
3884
|
+
title: PLAN_IN_PROGRESS,
|
|
3885
|
+
ownerId,
|
|
3886
|
+
repo,
|
|
3887
|
+
origin
|
|
3888
|
+
});
|
|
3889
|
+
setAgentPresence(ydoc, {
|
|
3890
|
+
agentType: input.agentType ?? "claude-code",
|
|
3891
|
+
sessionId: input.sessionId,
|
|
3892
|
+
connectedAt: now,
|
|
3893
|
+
lastSeenAt: now
|
|
3894
|
+
});
|
|
3895
|
+
if (origin && origin.platform === "claude-code") {
|
|
3896
|
+
const creator = typeof input.metadata?.ownerId === "string" ? input.metadata.ownerId : "unknown";
|
|
3897
|
+
const initialVersion = createInitialConversationVersion({
|
|
3898
|
+
versionId: nanoid4(),
|
|
3899
|
+
creator,
|
|
3900
|
+
platform: origin.platform,
|
|
3901
|
+
sessionId: origin.sessionId,
|
|
3902
|
+
messageCount: 0,
|
|
3903
|
+
createdAt: now
|
|
3904
|
+
});
|
|
3905
|
+
addConversationVersion(ydoc, initialVersion);
|
|
3906
|
+
ctx.logger.info(
|
|
3907
|
+
{ planId, versionId: initialVersion.versionId },
|
|
3908
|
+
"Added initial conversation version"
|
|
3909
|
+
);
|
|
3910
|
+
}
|
|
3911
|
+
const indexDoc = await ctx.getOrCreateDoc(PLAN_INDEX_DOC_NAME);
|
|
3912
|
+
setPlanIndexEntry(indexDoc, {
|
|
3913
|
+
id: planId,
|
|
3914
|
+
title: PLAN_IN_PROGRESS,
|
|
3915
|
+
status: "draft",
|
|
3916
|
+
createdAt: now,
|
|
3917
|
+
updatedAt: now,
|
|
3918
|
+
ownerId,
|
|
3919
|
+
deleted: false
|
|
3920
|
+
});
|
|
3921
|
+
const url = createPlanWebUrl(webConfig.SHIPYARD_WEB_URL, planId);
|
|
3922
|
+
ctx.logger.info({ url }, "Plan URL generated");
|
|
3923
|
+
setSessionState(input.sessionId, {
|
|
3924
|
+
lifecycle: "created",
|
|
3925
|
+
planId,
|
|
3926
|
+
createdAt: now,
|
|
3927
|
+
lastSyncedAt: now
|
|
3928
|
+
});
|
|
3929
|
+
ctx.logger.info({ sessionId: input.sessionId, planId }, "Session registered in registry");
|
|
3930
|
+
if (await hasActiveConnections3(PLAN_INDEX_DOC_NAME)) {
|
|
3931
|
+
indexDoc.getMap("navigation").set("target", planId);
|
|
3932
|
+
ctx.logger.info({ url, planId }, "Browser already connected, navigating via CRDT");
|
|
3933
|
+
} else {
|
|
3934
|
+
await open(url);
|
|
3935
|
+
ctx.logger.info({ url }, "Browser launched by server");
|
|
3936
|
+
}
|
|
3937
|
+
return { planId, url };
|
|
3938
|
+
}
|
|
3939
|
+
async function updateContentHandler(planId, input, ctx) {
|
|
3940
|
+
ctx.logger.info(
|
|
3941
|
+
{ planId, contentLength: input.content.length },
|
|
3942
|
+
"Updating plan content from hook"
|
|
3943
|
+
);
|
|
3944
|
+
const ydoc = await ctx.getOrCreateDoc(planId);
|
|
3945
|
+
const metadata = getPlanMetadata(ydoc);
|
|
3946
|
+
if (!metadata) {
|
|
3947
|
+
throw new TRPCError2({
|
|
3948
|
+
code: "NOT_FOUND",
|
|
3949
|
+
message: "Plan not found"
|
|
3950
|
+
});
|
|
3951
|
+
}
|
|
3952
|
+
const blocks = await parseMarkdownToBlocks(input.content);
|
|
3953
|
+
const title = extractTitleFromBlocks(blocks);
|
|
3954
|
+
const now = Date.now();
|
|
3955
|
+
const editor = ServerBlockNoteEditor.create();
|
|
3956
|
+
ydoc.transact(() => {
|
|
3957
|
+
const fragment = ydoc.getXmlFragment("document");
|
|
3958
|
+
while (fragment.length > 0) {
|
|
3959
|
+
fragment.delete(0, 1);
|
|
3960
|
+
}
|
|
3961
|
+
editor.blocksToYXmlFragment(blocks, fragment);
|
|
3962
|
+
const deliverablesArray = ydoc.getArray(YDOC_KEYS.DELIVERABLES);
|
|
3963
|
+
deliverablesArray.delete(0, deliverablesArray.length);
|
|
3964
|
+
const deliverables = extractDeliverables(blocks);
|
|
3965
|
+
for (const deliverable of deliverables) {
|
|
3966
|
+
addDeliverable(ydoc, deliverable);
|
|
3967
|
+
}
|
|
3968
|
+
if (deliverables.length > 0) {
|
|
3969
|
+
ctx.logger.info({ count: deliverables.length }, "Deliverables extracted from hook content");
|
|
3970
|
+
}
|
|
3971
|
+
});
|
|
3972
|
+
setPlanMetadata(ydoc, {
|
|
3973
|
+
title
|
|
3974
|
+
});
|
|
3975
|
+
const indexDoc = await ctx.getOrCreateDoc(PLAN_INDEX_DOC_NAME);
|
|
3976
|
+
if (metadata.ownerId) {
|
|
3977
|
+
setPlanIndexEntry(indexDoc, {
|
|
3978
|
+
id: planId,
|
|
3979
|
+
title,
|
|
3980
|
+
status: metadata.status,
|
|
3981
|
+
createdAt: metadata.createdAt ?? now,
|
|
3982
|
+
updatedAt: now,
|
|
3983
|
+
ownerId: metadata.ownerId,
|
|
3984
|
+
deleted: false
|
|
3985
|
+
});
|
|
3986
|
+
} else {
|
|
3987
|
+
ctx.logger.warn({ planId }, "Cannot update plan index: missing ownerId");
|
|
3988
|
+
}
|
|
3989
|
+
const sessionId = getSessionIdByPlanId(planId);
|
|
3990
|
+
if (sessionId) {
|
|
3991
|
+
const session = getSessionStateByPlanId(planId);
|
|
3992
|
+
if (session) {
|
|
3993
|
+
const contentHash = computeHash(input.content);
|
|
3994
|
+
switch (session.lifecycle) {
|
|
3995
|
+
case "created":
|
|
3996
|
+
case "approved_awaiting_token":
|
|
3997
|
+
setSessionState(sessionId, {
|
|
3998
|
+
...session,
|
|
3999
|
+
planFilePath: input.filePath
|
|
4000
|
+
});
|
|
4001
|
+
break;
|
|
4002
|
+
case "synced":
|
|
4003
|
+
case "approved":
|
|
4004
|
+
case "reviewed":
|
|
4005
|
+
setSessionState(sessionId, {
|
|
4006
|
+
...session,
|
|
4007
|
+
contentHash,
|
|
4008
|
+
planFilePath: input.filePath
|
|
4009
|
+
});
|
|
4010
|
+
break;
|
|
4011
|
+
default:
|
|
4012
|
+
assertNever(session);
|
|
4013
|
+
}
|
|
4014
|
+
ctx.logger.info(
|
|
4015
|
+
{ planId, sessionId, contentHash, lifecycle: session.lifecycle },
|
|
4016
|
+
"Updated session registry with content hash"
|
|
4017
|
+
);
|
|
4018
|
+
}
|
|
4019
|
+
}
|
|
4020
|
+
ctx.logger.info({ planId, title, blockCount: blocks.length }, "Plan content updated");
|
|
4021
|
+
return { success: true, updatedAt: now };
|
|
4022
|
+
}
|
|
4023
|
+
async function getReviewStatusHandler(planId, ctx) {
|
|
4024
|
+
const ydoc = await ctx.getOrCreateDoc(planId);
|
|
4025
|
+
const metadata = getPlanMetadata(ydoc);
|
|
4026
|
+
if (!metadata) {
|
|
4027
|
+
throw new TRPCError2({
|
|
4028
|
+
code: "NOT_FOUND",
|
|
4029
|
+
message: "Plan not found"
|
|
4030
|
+
});
|
|
4031
|
+
}
|
|
4032
|
+
switch (metadata.status) {
|
|
4033
|
+
case "draft":
|
|
4034
|
+
return { status: "draft" };
|
|
4035
|
+
case "pending_review":
|
|
4036
|
+
return {
|
|
4037
|
+
status: "pending_review",
|
|
4038
|
+
reviewRequestId: metadata.reviewRequestId
|
|
4039
|
+
};
|
|
4040
|
+
case "changes_requested": {
|
|
4041
|
+
const threadsMap = ydoc.getMap(YDOC_KEYS.THREADS);
|
|
4042
|
+
const threadsData = threadsMap.toJSON();
|
|
4043
|
+
const threads = parseThreads(threadsData);
|
|
4044
|
+
const feedback = threads.map((thread) => ({
|
|
4045
|
+
threadId: thread.id,
|
|
4046
|
+
blockId: thread.selectedText,
|
|
4047
|
+
comments: thread.comments.map((c) => ({
|
|
4048
|
+
author: c.userId ?? "Reviewer",
|
|
4049
|
+
content: typeof c.body === "string" ? c.body : JSON.stringify(c.body),
|
|
4050
|
+
createdAt: c.createdAt ?? Date.now()
|
|
4051
|
+
}))
|
|
4052
|
+
}));
|
|
4053
|
+
return {
|
|
4054
|
+
status: "changes_requested",
|
|
4055
|
+
reviewedAt: metadata.reviewedAt,
|
|
4056
|
+
reviewedBy: metadata.reviewedBy,
|
|
4057
|
+
reviewComment: metadata.reviewComment,
|
|
4058
|
+
feedback: feedback.length > 0 ? feedback : void 0
|
|
4059
|
+
};
|
|
4060
|
+
}
|
|
4061
|
+
case "in_progress":
|
|
4062
|
+
return {
|
|
4063
|
+
status: "in_progress",
|
|
4064
|
+
reviewedAt: metadata.reviewedAt,
|
|
4065
|
+
reviewedBy: metadata.reviewedBy
|
|
4066
|
+
};
|
|
4067
|
+
case "completed":
|
|
4068
|
+
return {
|
|
4069
|
+
status: "completed",
|
|
4070
|
+
completedAt: metadata.completedAt,
|
|
4071
|
+
completedBy: metadata.completedBy,
|
|
4072
|
+
snapshotUrl: metadata.snapshotUrl
|
|
4073
|
+
};
|
|
4074
|
+
default:
|
|
4075
|
+
assertNever(metadata);
|
|
4076
|
+
}
|
|
4077
|
+
}
|
|
4078
|
+
async function updatePresenceHandler(planId, input, ctx) {
|
|
4079
|
+
const ydoc = await ctx.getOrCreateDoc(planId);
|
|
4080
|
+
const now = Date.now();
|
|
4081
|
+
setAgentPresence(ydoc, {
|
|
4082
|
+
agentType: input.agentType,
|
|
4083
|
+
sessionId: input.sessionId,
|
|
4084
|
+
connectedAt: now,
|
|
4085
|
+
lastSeenAt: now
|
|
4086
|
+
});
|
|
4087
|
+
return { success: true };
|
|
4088
|
+
}
|
|
4089
|
+
async function setSessionTokenHandler(planId, sessionTokenHash, ctx) {
|
|
4090
|
+
ctx.logger.info({ planId }, "Setting session token from hook");
|
|
4091
|
+
const ydoc = await ctx.getOrCreateDoc(planId);
|
|
4092
|
+
const metadata = getPlanMetadata(ydoc);
|
|
4093
|
+
if (!metadata) {
|
|
4094
|
+
throw new TRPCError2({
|
|
4095
|
+
code: "NOT_FOUND",
|
|
4096
|
+
message: "Plan not found"
|
|
4097
|
+
});
|
|
4098
|
+
}
|
|
4099
|
+
setPlanMetadata(ydoc, {
|
|
4100
|
+
sessionTokenHash
|
|
4101
|
+
});
|
|
4102
|
+
const url = createPlanWebUrl(webConfig.SHIPYARD_WEB_URL, planId);
|
|
4103
|
+
const session = getSessionStateByPlanId(planId);
|
|
4104
|
+
const sessionId = getSessionIdByPlanId(planId);
|
|
4105
|
+
if (session && sessionId) {
|
|
4106
|
+
switch (session.lifecycle) {
|
|
4107
|
+
case "created":
|
|
4108
|
+
setSessionState(sessionId, {
|
|
4109
|
+
lifecycle: "synced",
|
|
4110
|
+
planId: session.planId,
|
|
4111
|
+
planFilePath: session.planFilePath,
|
|
4112
|
+
createdAt: session.createdAt,
|
|
4113
|
+
lastSyncedAt: session.lastSyncedAt,
|
|
4114
|
+
contentHash: "",
|
|
4115
|
+
sessionToken: sessionTokenHash,
|
|
4116
|
+
url
|
|
4117
|
+
});
|
|
4118
|
+
ctx.logger.info({ planId, sessionId }, "Transitioned session to synced state");
|
|
4119
|
+
break;
|
|
4120
|
+
case "approved_awaiting_token":
|
|
4121
|
+
setSessionState(sessionId, {
|
|
4122
|
+
lifecycle: "approved",
|
|
4123
|
+
planId: session.planId,
|
|
4124
|
+
planFilePath: session.planFilePath,
|
|
4125
|
+
createdAt: session.createdAt,
|
|
4126
|
+
lastSyncedAt: session.lastSyncedAt,
|
|
4127
|
+
contentHash: "",
|
|
4128
|
+
sessionToken: sessionTokenHash,
|
|
4129
|
+
url,
|
|
4130
|
+
approvedAt: session.approvedAt,
|
|
4131
|
+
deliverables: session.deliverables,
|
|
4132
|
+
reviewComment: session.reviewComment,
|
|
4133
|
+
reviewedBy: session.reviewedBy
|
|
4134
|
+
});
|
|
4135
|
+
ctx.logger.info(
|
|
4136
|
+
{ planId, sessionId },
|
|
4137
|
+
"Transitioned session from approved_awaiting_token to approved"
|
|
4138
|
+
);
|
|
4139
|
+
break;
|
|
4140
|
+
case "synced":
|
|
4141
|
+
case "approved":
|
|
4142
|
+
case "reviewed":
|
|
4143
|
+
setSessionState(sessionId, {
|
|
4144
|
+
...session,
|
|
4145
|
+
sessionToken: sessionTokenHash,
|
|
4146
|
+
url
|
|
4147
|
+
});
|
|
4148
|
+
ctx.logger.info({ planId, sessionId }, "Updated session token");
|
|
4149
|
+
break;
|
|
4150
|
+
default:
|
|
4151
|
+
assertNever(session);
|
|
4152
|
+
}
|
|
4153
|
+
}
|
|
4154
|
+
ctx.logger.info({ planId }, "Session token set successfully");
|
|
4155
|
+
return { url };
|
|
4156
|
+
}
|
|
4157
|
+
async function waitForApprovalHandler(planId, _reviewRequestIdParam, ctx) {
|
|
4158
|
+
let ydoc;
|
|
4159
|
+
try {
|
|
4160
|
+
ydoc = await ctx.getOrCreateDoc(planId);
|
|
4161
|
+
} catch (err) {
|
|
4162
|
+
ctx.logger.error({ err, planId }, "Failed to get or create doc for approval waiting");
|
|
4163
|
+
throw err;
|
|
4164
|
+
}
|
|
4165
|
+
const metadata = ydoc.getMap(YDOC_KEYS.METADATA);
|
|
4166
|
+
const planMetadata = getPlanMetadata(ydoc);
|
|
4167
|
+
const ownerId = planMetadata?.ownerId ?? "unknown";
|
|
4168
|
+
let reviewRequestId;
|
|
4169
|
+
if (planMetadata?.status === "pending_review" && planMetadata.reviewRequestId) {
|
|
4170
|
+
reviewRequestId = planMetadata.reviewRequestId;
|
|
4171
|
+
ctx.logger.info(
|
|
4172
|
+
{ planId, currentStatus: planMetadata.status, reviewRequestId },
|
|
4173
|
+
"Status already pending_review, reusing existing reviewRequestId for observer"
|
|
4174
|
+
);
|
|
4175
|
+
} else {
|
|
4176
|
+
reviewRequestId = nanoid4();
|
|
4177
|
+
const result = transitionPlanStatus(
|
|
4178
|
+
ydoc,
|
|
4179
|
+
{
|
|
4180
|
+
status: "pending_review",
|
|
4181
|
+
reviewRequestId
|
|
4182
|
+
},
|
|
4183
|
+
ownerId
|
|
4184
|
+
);
|
|
4185
|
+
if (!result.success) {
|
|
4186
|
+
ctx.logger.error({ planId, error: result.error }, "Failed to transition to pending_review");
|
|
4187
|
+
}
|
|
4188
|
+
}
|
|
4189
|
+
ctx.logger.info(
|
|
4190
|
+
{ planId, reviewRequestId },
|
|
4191
|
+
"[SERVER OBSERVER] Set reviewRequestId and status, starting observation"
|
|
4192
|
+
);
|
|
4193
|
+
const getReviewData = () => {
|
|
4194
|
+
const meta = getPlanMetadata(ydoc);
|
|
4195
|
+
if (meta?.status === "changes_requested" || meta?.status === "in_progress") {
|
|
4196
|
+
return {
|
|
4197
|
+
reviewComment: meta.reviewComment,
|
|
4198
|
+
reviewedBy: meta.reviewedBy
|
|
4199
|
+
};
|
|
4200
|
+
}
|
|
4201
|
+
return {
|
|
4202
|
+
reviewComment: void 0,
|
|
4203
|
+
reviewedBy: void 0
|
|
4204
|
+
};
|
|
4205
|
+
};
|
|
4206
|
+
const updateSessionRegistry = (status, extraData = {}) => {
|
|
4207
|
+
const sessionData = getSessionData();
|
|
4208
|
+
if (!sessionData) return;
|
|
4209
|
+
const { session, sessionId } = sessionData;
|
|
4210
|
+
validateSessionStateForTransition(session);
|
|
4211
|
+
const baseState = buildBaseState(session);
|
|
4212
|
+
const syncedFields = buildSyncedFields(session);
|
|
4213
|
+
const { reviewComment, reviewedBy } = getReviewData();
|
|
4214
|
+
if (status === "in_progress") {
|
|
4215
|
+
handleApprovedTransition(
|
|
4216
|
+
sessionId,
|
|
4217
|
+
baseState,
|
|
4218
|
+
syncedFields,
|
|
4219
|
+
extraData,
|
|
4220
|
+
reviewComment,
|
|
4221
|
+
reviewedBy
|
|
4222
|
+
);
|
|
4223
|
+
} else if (status === "changes_requested") {
|
|
4224
|
+
handleReviewedTransition(
|
|
4225
|
+
sessionId,
|
|
4226
|
+
baseState,
|
|
4227
|
+
syncedFields,
|
|
4228
|
+
session,
|
|
4229
|
+
extraData,
|
|
4230
|
+
reviewComment,
|
|
4231
|
+
reviewedBy
|
|
4232
|
+
);
|
|
4233
|
+
} else {
|
|
4234
|
+
throw new Error(
|
|
4235
|
+
`Invalid session state transition: missing required fields. status=${status}, hasApprovedAt=${!!extraData.approvedAt}, hasDeliverables=${!!extraData.deliverables}, hasReviewedBy=${!!reviewedBy}`
|
|
4236
|
+
);
|
|
4237
|
+
}
|
|
4238
|
+
logRegistryUpdate(status, extraData);
|
|
4239
|
+
};
|
|
4240
|
+
const getSessionData = () => {
|
|
4241
|
+
const session = getSessionStateByPlanId(planId);
|
|
4242
|
+
const sessionId = getSessionIdByPlanId(planId);
|
|
4243
|
+
if (!session || !sessionId) {
|
|
4244
|
+
ctx.logger.warn(
|
|
4245
|
+
{ planId },
|
|
4246
|
+
"Session not found in registry during approval - post-exit injection will not work"
|
|
4247
|
+
);
|
|
4248
|
+
return null;
|
|
4249
|
+
}
|
|
4250
|
+
return { session, sessionId };
|
|
4251
|
+
};
|
|
4252
|
+
const validateSessionStateForTransition = (_session) => {
|
|
4253
|
+
};
|
|
4254
|
+
const buildBaseState = (session) => ({
|
|
4255
|
+
planId: session.planId,
|
|
4256
|
+
planFilePath: session.planFilePath,
|
|
4257
|
+
createdAt: session.createdAt,
|
|
4258
|
+
lastSyncedAt: session.lastSyncedAt
|
|
4259
|
+
});
|
|
4260
|
+
const buildSyncedFields = (session) => {
|
|
4261
|
+
if (isSessionStateSynced(session) || isSessionStateApproved(session) || isSessionStateReviewed(session)) {
|
|
4262
|
+
return {
|
|
4263
|
+
contentHash: session.contentHash,
|
|
4264
|
+
sessionToken: session.sessionToken,
|
|
4265
|
+
url: session.url
|
|
4266
|
+
};
|
|
4267
|
+
}
|
|
4268
|
+
return null;
|
|
4269
|
+
};
|
|
4270
|
+
const handleApprovedTransition = (sessionId, baseState, syncedFields, extraData, reviewComment, reviewedBy) => {
|
|
4271
|
+
if (!extraData.approvedAt || !extraData.deliverables) {
|
|
4272
|
+
throw new Error(
|
|
4273
|
+
`Invalid session state transition: missing required fields for approval. hasApprovedAt=${!!extraData.approvedAt}, hasDeliverables=${!!extraData.deliverables}`
|
|
4274
|
+
);
|
|
4275
|
+
}
|
|
4276
|
+
const webUrl = webConfig.SHIPYARD_WEB_URL;
|
|
4277
|
+
if (syncedFields) {
|
|
4278
|
+
setSessionState(sessionId, {
|
|
4279
|
+
lifecycle: "approved",
|
|
4280
|
+
...baseState,
|
|
4281
|
+
...syncedFields,
|
|
4282
|
+
approvedAt: extraData.approvedAt,
|
|
4283
|
+
deliverables: extraData.deliverables,
|
|
4284
|
+
reviewComment,
|
|
4285
|
+
reviewedBy
|
|
4286
|
+
});
|
|
4287
|
+
} else {
|
|
4288
|
+
setSessionState(sessionId, {
|
|
4289
|
+
lifecycle: "approved_awaiting_token",
|
|
4290
|
+
...baseState,
|
|
4291
|
+
url: createPlanWebUrl(webUrl, baseState.planId),
|
|
4292
|
+
approvedAt: extraData.approvedAt,
|
|
4293
|
+
deliverables: extraData.deliverables,
|
|
4294
|
+
reviewComment,
|
|
4295
|
+
reviewedBy
|
|
4296
|
+
});
|
|
4297
|
+
}
|
|
4298
|
+
};
|
|
4299
|
+
const handleReviewedTransition = (sessionId, baseState, syncedFields, session, extraData, reviewComment, reviewedBy) => {
|
|
4300
|
+
if (!reviewedBy) {
|
|
4301
|
+
throw new Error(`Invalid session state transition: missing reviewedBy for changes_requested`);
|
|
4302
|
+
}
|
|
4303
|
+
const deliverables = extraData.deliverables || (isSessionStateApproved(session) || isSessionStateReviewed(session) || isSessionStateApprovedAwaitingToken(session) ? session.deliverables : []);
|
|
4304
|
+
const webUrl = webConfig.SHIPYARD_WEB_URL;
|
|
4305
|
+
setSessionState(sessionId, {
|
|
4306
|
+
lifecycle: "reviewed",
|
|
4307
|
+
...baseState,
|
|
4308
|
+
contentHash: syncedFields?.contentHash ?? "",
|
|
4309
|
+
sessionToken: syncedFields?.sessionToken ?? "",
|
|
4310
|
+
url: syncedFields?.url ?? createPlanWebUrl(webUrl, baseState.planId),
|
|
4311
|
+
deliverables,
|
|
4312
|
+
reviewComment: reviewComment || "",
|
|
4313
|
+
reviewedBy,
|
|
4314
|
+
reviewStatus: "changes_requested"
|
|
4315
|
+
});
|
|
4316
|
+
};
|
|
4317
|
+
const logRegistryUpdate = (status, extraData) => {
|
|
4318
|
+
const sessionId = getSessionIdByPlanId(planId);
|
|
4319
|
+
ctx.logger.info(
|
|
4320
|
+
{
|
|
4321
|
+
planId,
|
|
4322
|
+
sessionId,
|
|
4323
|
+
...extraData.deliverables && { deliverableCount: extraData.deliverables.length }
|
|
4324
|
+
},
|
|
4325
|
+
`Stored ${status === "in_progress" ? "approval" : "rejection"} data in session registry`
|
|
4326
|
+
);
|
|
4327
|
+
};
|
|
4328
|
+
const handleApproved = () => {
|
|
4329
|
+
const deliverables = getDeliverables(ydoc);
|
|
4330
|
+
const deliverableInfos = deliverables.map((d) => ({ id: d.id, text: d.text }));
|
|
4331
|
+
updateSessionRegistry("in_progress", {
|
|
4332
|
+
approvedAt: Date.now(),
|
|
4333
|
+
deliverables: deliverableInfos
|
|
4334
|
+
});
|
|
4335
|
+
const { reviewComment, reviewedBy } = getReviewData();
|
|
4336
|
+
ctx.logger.info(
|
|
4337
|
+
{ planId, reviewRequestId, reviewedBy },
|
|
4338
|
+
"[SERVER OBSERVER] Plan approved via Y.Doc - resolving promise"
|
|
4339
|
+
);
|
|
4340
|
+
return {
|
|
4341
|
+
approved: true,
|
|
4342
|
+
deliverables,
|
|
4343
|
+
reviewComment,
|
|
4344
|
+
reviewedBy: reviewedBy || "unknown",
|
|
4345
|
+
status: "in_progress"
|
|
4346
|
+
};
|
|
4347
|
+
};
|
|
4348
|
+
const handleChangesRequested = () => {
|
|
4349
|
+
updateSessionRegistry("changes_requested");
|
|
4350
|
+
const feedback = extractFeedbackFromYDoc(ydoc, ctx);
|
|
4351
|
+
const { reviewComment, reviewedBy } = getReviewData();
|
|
4352
|
+
ctx.logger.info(
|
|
4353
|
+
{ planId, reviewRequestId, feedback },
|
|
4354
|
+
"[SERVER OBSERVER] Changes requested via Y.Doc"
|
|
4355
|
+
);
|
|
4356
|
+
return {
|
|
4357
|
+
approved: false,
|
|
4358
|
+
feedback: feedback || "Changes requested",
|
|
4359
|
+
status: "changes_requested",
|
|
4360
|
+
reviewComment,
|
|
4361
|
+
reviewedBy
|
|
4362
|
+
};
|
|
4363
|
+
};
|
|
4364
|
+
return new Promise((resolve2, reject) => {
|
|
4365
|
+
const APPROVAL_TIMEOUT_MS = 30 * 60 * 1e3;
|
|
4366
|
+
let timeout = null;
|
|
4367
|
+
let checkStatus = null;
|
|
4368
|
+
const shouldProcessStatusChange = () => {
|
|
4369
|
+
const currentMeta = getPlanMetadata(ydoc);
|
|
4370
|
+
if (!currentMeta) return false;
|
|
4371
|
+
const currentReviewId = currentMeta.status === "pending_review" ? currentMeta.reviewRequestId : void 0;
|
|
4372
|
+
const status = currentMeta.status;
|
|
4373
|
+
if (currentReviewId !== reviewRequestId && currentMeta.status === "pending_review") {
|
|
4374
|
+
ctx.logger.warn(
|
|
4375
|
+
{ planId, expected: reviewRequestId, actual: currentReviewId, status },
|
|
4376
|
+
"[SERVER OBSERVER] Review ID mismatch, ignoring status change"
|
|
4377
|
+
);
|
|
4378
|
+
return false;
|
|
4379
|
+
}
|
|
4380
|
+
const isTerminalState = status === "in_progress" || status === "changes_requested";
|
|
4381
|
+
return isTerminalState;
|
|
4382
|
+
};
|
|
4383
|
+
const cleanupObserver = () => {
|
|
4384
|
+
if (timeout) clearTimeout(timeout);
|
|
4385
|
+
if (checkStatus) metadata.unobserve(checkStatus);
|
|
4386
|
+
};
|
|
4387
|
+
try {
|
|
4388
|
+
timeout = setTimeout(() => {
|
|
4389
|
+
if (checkStatus) {
|
|
4390
|
+
metadata.unobserve(checkStatus);
|
|
4391
|
+
}
|
|
4392
|
+
resolve2({
|
|
4393
|
+
approved: false,
|
|
4394
|
+
feedback: "Review timeout - no decision received in 30 minutes",
|
|
4395
|
+
status: "timeout"
|
|
4396
|
+
});
|
|
4397
|
+
}, APPROVAL_TIMEOUT_MS);
|
|
4398
|
+
checkStatus = () => {
|
|
4399
|
+
const currentMeta = getPlanMetadata(ydoc);
|
|
4400
|
+
const status = currentMeta?.status;
|
|
4401
|
+
const currentReviewId = currentMeta?.status === "pending_review" ? currentMeta.reviewRequestId : void 0;
|
|
4402
|
+
ctx.logger.debug(
|
|
4403
|
+
{ planId, status, currentReviewId, expectedReviewId: reviewRequestId },
|
|
4404
|
+
"[SERVER OBSERVER] Metadata changed, checking status"
|
|
4405
|
+
);
|
|
4406
|
+
if (!shouldProcessStatusChange()) return;
|
|
4407
|
+
cleanupObserver();
|
|
4408
|
+
resolve2(status === "in_progress" ? handleApproved() : handleChangesRequested());
|
|
4409
|
+
};
|
|
4410
|
+
ctx.logger.info(
|
|
4411
|
+
{ planId, reviewRequestId },
|
|
4412
|
+
"[SERVER OBSERVER] Registering metadata observer"
|
|
4413
|
+
);
|
|
4414
|
+
metadata.observe((event) => {
|
|
4415
|
+
ctx.logger.info(
|
|
4416
|
+
{
|
|
4417
|
+
planId,
|
|
4418
|
+
reviewRequestId,
|
|
4419
|
+
keysChanged: Array.from(event.keysChanged),
|
|
4420
|
+
target: event.target.constructor.name
|
|
4421
|
+
},
|
|
4422
|
+
"[SERVER OBSERVER] *** METADATA MAP CHANGED *** (Raw Y.Map observer)"
|
|
4423
|
+
);
|
|
4424
|
+
});
|
|
4425
|
+
metadata.observe(checkStatus);
|
|
4426
|
+
checkStatus();
|
|
4427
|
+
} catch (err) {
|
|
4428
|
+
if (timeout) clearTimeout(timeout);
|
|
4429
|
+
if (checkStatus) {
|
|
4430
|
+
try {
|
|
4431
|
+
metadata.unobserve(checkStatus);
|
|
4432
|
+
} catch (unobserveErr) {
|
|
4433
|
+
ctx.logger.warn({ err: unobserveErr }, "Failed to unobserve during error cleanup");
|
|
4434
|
+
}
|
|
4435
|
+
}
|
|
4436
|
+
ctx.logger.error({ err, planId }, "Failed to setup approval observer");
|
|
4437
|
+
reject(err);
|
|
4438
|
+
}
|
|
4439
|
+
});
|
|
4440
|
+
}
|
|
4441
|
+
function extractFeedbackFromYDoc(ydoc, ctx) {
|
|
4442
|
+
try {
|
|
4443
|
+
const planMeta = getPlanMetadata(ydoc);
|
|
4444
|
+
const reviewComment = planMeta?.status === "changes_requested" || planMeta?.status === "in_progress" ? planMeta.reviewComment : void 0;
|
|
4445
|
+
const reviewedBy = planMeta?.status === "changes_requested" || planMeta?.status === "in_progress" ? planMeta.reviewedBy : void 0;
|
|
4446
|
+
const threadsMap = ydoc.getMap(YDOC_KEYS.THREADS);
|
|
4447
|
+
const threadsData = threadsMap.toJSON();
|
|
4448
|
+
const threads = parseThreads(threadsData);
|
|
4449
|
+
if (!reviewComment && threads.length === 0) {
|
|
4450
|
+
return "Changes requested. Check the plan for reviewer comments.";
|
|
4451
|
+
}
|
|
4452
|
+
const contentFragment = ydoc.getXmlFragment(YDOC_KEYS.DOCUMENT_FRAGMENT);
|
|
4453
|
+
const fragmentJson = contentFragment.toJSON();
|
|
4454
|
+
let planText = "";
|
|
4455
|
+
if (Array.isArray(fragmentJson)) {
|
|
4456
|
+
planText = fragmentJson.map((block) => {
|
|
4457
|
+
if (!block || typeof block !== "object") return "";
|
|
4458
|
+
const blockRecord = Object.fromEntries(Object.entries(block));
|
|
4459
|
+
const content = blockRecord.content;
|
|
4460
|
+
if (!Array.isArray(content)) return "";
|
|
4461
|
+
return content.map((item) => {
|
|
4462
|
+
if (!item || typeof item !== "object") return "";
|
|
4463
|
+
const itemRecord = Object.fromEntries(Object.entries(item));
|
|
4464
|
+
const text = itemRecord.text;
|
|
4465
|
+
return typeof text === "string" ? text : "";
|
|
4466
|
+
}).join("");
|
|
4467
|
+
}).filter(Boolean).join("\n");
|
|
4468
|
+
}
|
|
4469
|
+
const resolveUser = createUserResolver(ydoc);
|
|
4470
|
+
const feedbackText = formatThreadsForLLM(threads, {
|
|
4471
|
+
includeResolved: false,
|
|
4472
|
+
selectedTextMaxLength: 100,
|
|
4473
|
+
resolveUser
|
|
4474
|
+
});
|
|
4475
|
+
let output = "Changes requested:\n\n";
|
|
4476
|
+
if (planText) {
|
|
4477
|
+
output += "## Current Plan\n\n";
|
|
4478
|
+
output += planText;
|
|
4479
|
+
output += "\n\n---\n\n";
|
|
4480
|
+
}
|
|
4481
|
+
if (reviewComment) {
|
|
4482
|
+
output += "## Reviewer Comment\n\n";
|
|
4483
|
+
output += `> **${reviewedBy ?? "Reviewer"}:** ${reviewComment}
|
|
4484
|
+
`;
|
|
4485
|
+
output += "\n---\n\n";
|
|
4486
|
+
}
|
|
4487
|
+
if (feedbackText) {
|
|
4488
|
+
output += "## Inline Feedback\n\n";
|
|
4489
|
+
output += feedbackText;
|
|
4490
|
+
}
|
|
4491
|
+
const deliverables = getDeliverables(ydoc);
|
|
4492
|
+
const deliverablesText = formatDeliverablesForLLM(deliverables);
|
|
4493
|
+
if (deliverablesText) {
|
|
4494
|
+
output += "\n\n---\n\n";
|
|
4495
|
+
output += deliverablesText;
|
|
4496
|
+
}
|
|
4497
|
+
return output;
|
|
4498
|
+
} catch (err) {
|
|
4499
|
+
ctx.logger.warn({ err }, "Failed to extract feedback from Y.Doc");
|
|
4500
|
+
return "Changes requested. Check the plan for reviewer comments.";
|
|
4501
|
+
}
|
|
4502
|
+
}
|
|
4503
|
+
async function getDeliverableContextHandler(planId, sessionToken, ctx) {
|
|
4504
|
+
const ydoc = await ctx.getOrCreateDoc(planId);
|
|
4505
|
+
const metadata = getPlanMetadata(ydoc);
|
|
4506
|
+
if (!metadata) {
|
|
4507
|
+
throw new TRPCError2({
|
|
4508
|
+
code: "NOT_FOUND",
|
|
4509
|
+
message: "Plan not found"
|
|
4510
|
+
});
|
|
4511
|
+
}
|
|
4512
|
+
const deliverables = getDeliverables(ydoc);
|
|
4513
|
+
const url = createPlanWebUrl(webConfig.SHIPYARD_WEB_URL, planId);
|
|
4514
|
+
let deliverablesSection = "";
|
|
4515
|
+
if (deliverables.length > 0) {
|
|
4516
|
+
deliverablesSection = `
|
|
4517
|
+
## Deliverables
|
|
4518
|
+
|
|
4519
|
+
Attach proof to each deliverable using add_artifact:
|
|
4520
|
+
|
|
4521
|
+
`;
|
|
4522
|
+
for (const d of deliverables) {
|
|
4523
|
+
deliverablesSection += `- ${d.text}
|
|
4524
|
+
deliverableId="${d.id}"
|
|
4525
|
+
`;
|
|
4526
|
+
}
|
|
4527
|
+
} else {
|
|
4528
|
+
deliverablesSection = `
|
|
4529
|
+
## Deliverables
|
|
4530
|
+
|
|
4531
|
+
No deliverables marked in this plan. You can still upload artifacts without linking them.`;
|
|
4532
|
+
}
|
|
4533
|
+
let feedbackSection = "";
|
|
4534
|
+
if (metadata.status === "changes_requested" && metadata.reviewComment?.trim()) {
|
|
4535
|
+
feedbackSection = `
|
|
4536
|
+
## Reviewer Feedback
|
|
4537
|
+
|
|
4538
|
+
${metadata.reviewedBy ? `**From:** ${metadata.reviewedBy}
|
|
4539
|
+
|
|
4540
|
+
` : ""}${metadata.reviewComment}
|
|
4541
|
+
|
|
4542
|
+
`;
|
|
4543
|
+
}
|
|
4544
|
+
const approvalMessage = metadata.status === "changes_requested" ? "[SHIPYARD] Changes requested on your plan \u26A0\uFE0F" : "[SHIPYARD] Plan approved! \u{1F389}";
|
|
4545
|
+
const context = `${approvalMessage}
|
|
4546
|
+
${deliverablesSection}${feedbackSection}
|
|
4547
|
+
## Session Info
|
|
4548
|
+
|
|
4549
|
+
planId="${planId}"
|
|
4550
|
+
sessionToken="${sessionToken}"
|
|
4551
|
+
url="${url}"
|
|
4552
|
+
|
|
4553
|
+
## How to Attach Proof
|
|
4554
|
+
|
|
4555
|
+
For each deliverable above, call:
|
|
4556
|
+
\`\`\`
|
|
4557
|
+
add_artifact(
|
|
4558
|
+
planId="${planId}",
|
|
4559
|
+
sessionToken="${sessionToken}",
|
|
4560
|
+
type="image",
|
|
4561
|
+
filePath="/path/to/file.png",
|
|
4562
|
+
deliverableId="<id from above>"
|
|
4563
|
+
)
|
|
4564
|
+
\`\`\`
|
|
4565
|
+
|
|
4566
|
+
When the LAST deliverable gets an artifact, the task auto-completes and returns a snapshot URL for your PR.`;
|
|
4567
|
+
return { context };
|
|
4568
|
+
}
|
|
4569
|
+
async function getSessionContextHandler(sessionId, ctx) {
|
|
4570
|
+
ctx.logger.info({ sessionId }, "Getting session context for post-exit injection");
|
|
4571
|
+
const sessionState = getSessionState(sessionId);
|
|
4572
|
+
if (!sessionState) {
|
|
4573
|
+
ctx.logger.warn({ sessionId }, "Session not found in registry");
|
|
4574
|
+
return { found: false };
|
|
4575
|
+
}
|
|
4576
|
+
if (isSessionStateApproved(sessionState)) {
|
|
4577
|
+
ctx.logger.info(
|
|
4578
|
+
{ sessionId, planId: sessionState.planId },
|
|
4579
|
+
"Session context retrieved (approved state, idempotent)"
|
|
4580
|
+
);
|
|
4581
|
+
return {
|
|
4582
|
+
found: true,
|
|
4583
|
+
planId: sessionState.planId,
|
|
4584
|
+
sessionToken: sessionState.sessionToken,
|
|
4585
|
+
url: sessionState.url,
|
|
4586
|
+
deliverables: sessionState.deliverables,
|
|
4587
|
+
reviewComment: sessionState.reviewComment,
|
|
4588
|
+
reviewedBy: sessionState.reviewedBy
|
|
4589
|
+
};
|
|
4590
|
+
}
|
|
4591
|
+
if (isSessionStateReviewed(sessionState)) {
|
|
4592
|
+
ctx.logger.info(
|
|
4593
|
+
{ sessionId, planId: sessionState.planId },
|
|
4594
|
+
"Session context retrieved (reviewed state, idempotent)"
|
|
4595
|
+
);
|
|
4596
|
+
return {
|
|
4597
|
+
found: true,
|
|
4598
|
+
planId: sessionState.planId,
|
|
4599
|
+
sessionToken: sessionState.sessionToken,
|
|
4600
|
+
url: sessionState.url,
|
|
4601
|
+
deliverables: sessionState.deliverables,
|
|
4602
|
+
reviewComment: sessionState.reviewComment,
|
|
4603
|
+
reviewedBy: sessionState.reviewedBy,
|
|
4604
|
+
reviewStatus: sessionState.reviewStatus
|
|
4605
|
+
};
|
|
4606
|
+
}
|
|
4607
|
+
ctx.logger.warn(
|
|
4608
|
+
{ sessionId, lifecycle: sessionState.lifecycle },
|
|
4609
|
+
"Session not ready for post-exit injection"
|
|
4610
|
+
);
|
|
4611
|
+
return { found: false };
|
|
4612
|
+
}
|
|
4613
|
+
function createHookHandlers() {
|
|
4614
|
+
return {
|
|
4615
|
+
createSession: (input, ctx) => createSessionHandler(input, ctx),
|
|
4616
|
+
updateContent: (planId, input, ctx) => updateContentHandler(planId, input, ctx),
|
|
4617
|
+
getReviewStatus: (planId, ctx) => getReviewStatusHandler(planId, ctx),
|
|
4618
|
+
updatePresence: (planId, input, ctx) => updatePresenceHandler(planId, input, ctx),
|
|
4619
|
+
setSessionToken: (planId, sessionTokenHash, ctx) => setSessionTokenHandler(planId, sessionTokenHash, ctx),
|
|
4620
|
+
waitForApproval: (planId, reviewRequestId, ctx) => waitForApprovalHandler(planId, reviewRequestId, ctx),
|
|
4621
|
+
getDeliverableContext: (planId, sessionToken, ctx) => getDeliverableContextHandler(planId, sessionToken, ctx),
|
|
4622
|
+
getSessionContext: (sessionId, ctx) => getSessionContextHandler(sessionId, ctx)
|
|
4623
|
+
};
|
|
4624
|
+
}
|
|
4625
|
+
|
|
4626
|
+
// src/subscriptions/manager.ts
|
|
4627
|
+
import { nanoid as nanoid5 } from "nanoid";
|
|
4628
|
+
var subscriptions = /* @__PURE__ */ new Map();
|
|
4629
|
+
var SUBSCRIPTION_TTL_MS = 5 * 60 * 1e3;
|
|
4630
|
+
function createSubscription(config) {
|
|
4631
|
+
const id = nanoid5();
|
|
4632
|
+
const now = Date.now();
|
|
4633
|
+
const subscription = {
|
|
4634
|
+
id,
|
|
4635
|
+
config,
|
|
4636
|
+
pendingChanges: [],
|
|
4637
|
+
windowStartedAt: null,
|
|
4638
|
+
lastFlushedAt: now,
|
|
4639
|
+
lastActivityAt: now,
|
|
4640
|
+
ready: false
|
|
4641
|
+
};
|
|
4642
|
+
let planSubs = subscriptions.get(config.planId);
|
|
4643
|
+
if (!planSubs) {
|
|
4644
|
+
planSubs = /* @__PURE__ */ new Map();
|
|
4645
|
+
subscriptions.set(config.planId, planSubs);
|
|
4646
|
+
}
|
|
4647
|
+
planSubs.set(id, subscription);
|
|
4648
|
+
logger.info(
|
|
4649
|
+
{ planId: config.planId, subscriptionId: id, subscribe: config.subscribe },
|
|
4650
|
+
"Subscription created"
|
|
4651
|
+
);
|
|
4652
|
+
return id;
|
|
4653
|
+
}
|
|
4654
|
+
function deleteSubscription(planId, subscriptionId) {
|
|
4655
|
+
const deleted = subscriptions.get(planId)?.delete(subscriptionId) ?? false;
|
|
4656
|
+
if (deleted) {
|
|
4657
|
+
logger.info({ planId, subscriptionId }, "Subscription deleted");
|
|
4658
|
+
if (subscriptions.get(planId)?.size === 0) {
|
|
4659
|
+
subscriptions.delete(planId);
|
|
4660
|
+
}
|
|
4661
|
+
}
|
|
4662
|
+
return deleted;
|
|
4663
|
+
}
|
|
4664
|
+
function notifyChange(planId, change) {
|
|
4665
|
+
const planSubs = subscriptions.get(planId);
|
|
4666
|
+
if (!planSubs) return;
|
|
4667
|
+
const now = Date.now();
|
|
4668
|
+
for (const sub of planSubs.values()) {
|
|
4669
|
+
if (!sub.config.subscribe.includes(change.type)) continue;
|
|
4670
|
+
sub.pendingChanges.push(change);
|
|
4671
|
+
sub.lastActivityAt = now;
|
|
4672
|
+
if (sub.windowStartedAt === null) {
|
|
4673
|
+
sub.windowStartedAt = now;
|
|
4674
|
+
}
|
|
4675
|
+
checkFlushConditions(sub);
|
|
4676
|
+
}
|
|
4677
|
+
logger.debug(
|
|
4678
|
+
{ planId, changeType: change.type, subscriberCount: planSubs.size },
|
|
4679
|
+
"Change notified"
|
|
4680
|
+
);
|
|
4681
|
+
}
|
|
4682
|
+
function getChanges(planId, subscriptionId) {
|
|
4683
|
+
const sub = subscriptions.get(planId)?.get(subscriptionId);
|
|
4684
|
+
if (!sub) return null;
|
|
4685
|
+
const now = Date.now();
|
|
4686
|
+
sub.lastActivityAt = now;
|
|
4687
|
+
checkFlushConditions(sub);
|
|
4688
|
+
if (!sub.ready) {
|
|
4689
|
+
return {
|
|
4690
|
+
ready: false,
|
|
4691
|
+
pending: sub.pendingChanges.length,
|
|
4692
|
+
windowExpiresIn: sub.windowStartedAt ? Math.max(0, sub.config.windowMs - (now - sub.windowStartedAt)) : sub.config.windowMs
|
|
4693
|
+
};
|
|
4694
|
+
}
|
|
4695
|
+
const changes = sub.pendingChanges;
|
|
4696
|
+
const summary = summarizeChanges(changes);
|
|
4697
|
+
sub.pendingChanges = [];
|
|
4698
|
+
sub.windowStartedAt = null;
|
|
4699
|
+
sub.lastFlushedAt = now;
|
|
4700
|
+
sub.ready = false;
|
|
4701
|
+
logger.debug({ planId, subscriptionId, changeCount: changes.length }, "Changes flushed");
|
|
4702
|
+
return {
|
|
4703
|
+
ready: true,
|
|
4704
|
+
changes: summary,
|
|
4705
|
+
details: changes
|
|
4706
|
+
};
|
|
4707
|
+
}
|
|
4708
|
+
function startCleanupInterval() {
|
|
4709
|
+
startPeriodicCleanup();
|
|
4710
|
+
setInterval(() => {
|
|
4711
|
+
const now = Date.now();
|
|
4712
|
+
let cleanedCount = 0;
|
|
4713
|
+
for (const [planId, planSubs] of subscriptions.entries()) {
|
|
4714
|
+
for (const [subId, sub] of planSubs.entries()) {
|
|
4715
|
+
if (now - sub.lastActivityAt > SUBSCRIPTION_TTL_MS) {
|
|
4716
|
+
planSubs.delete(subId);
|
|
4717
|
+
cleanedCount++;
|
|
4718
|
+
}
|
|
4719
|
+
}
|
|
4720
|
+
if (planSubs.size === 0) {
|
|
4721
|
+
subscriptions.delete(planId);
|
|
4722
|
+
}
|
|
4723
|
+
}
|
|
4724
|
+
if (cleanedCount > 0) {
|
|
4725
|
+
logger.info({ cleanedCount }, "Cleaned up stale subscriptions");
|
|
4726
|
+
}
|
|
4727
|
+
}, 6e4);
|
|
4728
|
+
}
|
|
4729
|
+
function checkFlushConditions(sub) {
|
|
4730
|
+
const now = Date.now();
|
|
4731
|
+
const { windowMs, maxWindowMs, threshold } = sub.config;
|
|
4732
|
+
if (sub.pendingChanges.length >= threshold) {
|
|
4733
|
+
sub.ready = true;
|
|
4734
|
+
return;
|
|
4735
|
+
}
|
|
4736
|
+
if (sub.windowStartedAt && now - sub.windowStartedAt >= windowMs) {
|
|
4737
|
+
sub.ready = true;
|
|
4738
|
+
return;
|
|
4739
|
+
}
|
|
4740
|
+
if (now - sub.lastFlushedAt >= maxWindowMs && sub.pendingChanges.length > 0) {
|
|
4741
|
+
sub.ready = true;
|
|
4742
|
+
}
|
|
4743
|
+
}
|
|
4744
|
+
function summarizeChanges(changes) {
|
|
4745
|
+
const parts = [];
|
|
4746
|
+
const statusChanges = changes.filter((c) => c.type === "status");
|
|
4747
|
+
if (statusChanges.length > 0) {
|
|
4748
|
+
const latest = statusChanges[statusChanges.length - 1];
|
|
4749
|
+
if (latest) {
|
|
4750
|
+
parts.push(`Status: ${latest.details?.newValue}`);
|
|
4751
|
+
}
|
|
4752
|
+
}
|
|
4753
|
+
const commentChanges = changes.filter((c) => c.type === "comments");
|
|
4754
|
+
if (commentChanges.length > 0) {
|
|
4755
|
+
const totalAdded = commentChanges.reduce((acc, c) => {
|
|
4756
|
+
const added = c.details?.added;
|
|
4757
|
+
return acc + (typeof added === "number" ? added : 1);
|
|
4758
|
+
}, 0);
|
|
4759
|
+
parts.push(`${totalAdded} new comment(s)`);
|
|
4760
|
+
}
|
|
4761
|
+
const resolvedChanges = changes.filter((c) => c.type === "resolved");
|
|
4762
|
+
if (resolvedChanges.length > 0) {
|
|
4763
|
+
const totalResolved = resolvedChanges.reduce((acc, c) => {
|
|
4764
|
+
const resolved = c.details?.resolved;
|
|
4765
|
+
return acc + (typeof resolved === "number" ? resolved : 1);
|
|
4766
|
+
}, 0);
|
|
4767
|
+
parts.push(`${totalResolved} resolved`);
|
|
4768
|
+
}
|
|
4769
|
+
const contentChanges = changes.filter((c) => c.type === "content");
|
|
4770
|
+
if (contentChanges.length > 0) {
|
|
4771
|
+
parts.push("Content updated");
|
|
4772
|
+
}
|
|
4773
|
+
const artifactChanges = changes.filter((c) => c.type === "artifacts");
|
|
4774
|
+
if (artifactChanges.length > 0) {
|
|
4775
|
+
const totalAdded = artifactChanges.reduce((acc, c) => {
|
|
4776
|
+
const added = c.details?.added;
|
|
4777
|
+
return acc + (typeof added === "number" ? added : 1);
|
|
4778
|
+
}, 0);
|
|
4779
|
+
parts.push(`${totalAdded} artifact(s) added`);
|
|
4780
|
+
}
|
|
4781
|
+
return parts.join(" | ") || "No changes";
|
|
4782
|
+
}
|
|
4783
|
+
|
|
4784
|
+
// src/subscriptions/observers.ts
|
|
4785
|
+
var previousState = /* @__PURE__ */ new Map();
|
|
4786
|
+
var lastContentEdit = /* @__PURE__ */ new Map();
|
|
4787
|
+
var CONTENT_EDIT_DEBOUNCE_MS = 5e3;
|
|
4788
|
+
function attachObservers(planId, doc) {
|
|
4789
|
+
const metadata = getPlanMetadata(doc);
|
|
4790
|
+
const threadsMap = doc.getMap(YDOC_KEYS.THREADS);
|
|
4791
|
+
const threads = parseThreads(threadsMap.toJSON());
|
|
4792
|
+
const deliverables = getDeliverables(doc);
|
|
4793
|
+
const allFulfilled = deliverables.length > 0 && deliverables.every((d) => d.linkedArtifactId);
|
|
4794
|
+
const initialCommentIds = /* @__PURE__ */ new Set();
|
|
4795
|
+
for (const thread of threads) {
|
|
4796
|
+
for (const comment of thread.comments) {
|
|
4797
|
+
initialCommentIds.add(comment.id);
|
|
4798
|
+
}
|
|
4799
|
+
}
|
|
4800
|
+
previousState.set(planId, {
|
|
4801
|
+
status: metadata?.status,
|
|
4802
|
+
commentCount: threads.reduce((acc, t2) => acc + t2.comments.length, 0),
|
|
4803
|
+
resolvedCount: threads.filter((t2) => t2.resolved).length,
|
|
4804
|
+
contentLength: doc.getXmlFragment("document").length,
|
|
4805
|
+
artifactCount: doc.getArray(YDOC_KEYS.ARTIFACTS).length,
|
|
4806
|
+
deliverablesFulfilled: allFulfilled,
|
|
4807
|
+
commentIds: initialCommentIds
|
|
4808
|
+
});
|
|
4809
|
+
logger.debug({ planId }, "Attached observers to plan");
|
|
4810
|
+
doc.getMap(YDOC_KEYS.METADATA).observe((event, transaction) => {
|
|
4811
|
+
if (event.keysChanged.has("status")) {
|
|
4812
|
+
let isValidStatus2 = function(s) {
|
|
4813
|
+
return typeof s === "string" && (s === "draft" || s === "pending_review" || s === "changes_requested" || s === "in_progress" || s === "completed");
|
|
4814
|
+
};
|
|
4815
|
+
var isValidStatus = isValidStatus2;
|
|
4816
|
+
const prev = previousState.get(planId);
|
|
4817
|
+
const rawStatus = doc.getMap(YDOC_KEYS.METADATA).get("status");
|
|
4818
|
+
const newStatus = isValidStatus2(rawStatus) ? rawStatus : void 0;
|
|
4819
|
+
if (prev?.status && prev.status !== newStatus && newStatus) {
|
|
4820
|
+
const actor = transaction.origin?.actor || "System";
|
|
4821
|
+
logPlanEvent(doc, "status_changed", actor, {
|
|
4822
|
+
fromStatus: prev.status,
|
|
4823
|
+
toStatus: newStatus
|
|
4824
|
+
});
|
|
4825
|
+
const change = {
|
|
4826
|
+
type: "status",
|
|
4827
|
+
timestamp: Date.now(),
|
|
4828
|
+
summary: `Status changed to ${newStatus}`,
|
|
4829
|
+
details: { oldValue: prev.status, newValue: newStatus }
|
|
4830
|
+
};
|
|
4831
|
+
notifyChange(planId, change);
|
|
4832
|
+
prev.status = newStatus;
|
|
4833
|
+
logger.debug({ planId, oldStatus: prev.status, newStatus }, "Status change detected");
|
|
4834
|
+
}
|
|
4835
|
+
}
|
|
4836
|
+
});
|
|
4837
|
+
doc.getMap(YDOC_KEYS.THREADS).observeDeep((_events, transaction) => {
|
|
4838
|
+
const prev = previousState.get(planId);
|
|
4839
|
+
if (!prev) return;
|
|
4840
|
+
const actor = transaction.origin?.actor || "System";
|
|
4841
|
+
const threadsMap2 = doc.getMap(YDOC_KEYS.THREADS);
|
|
4842
|
+
const threads2 = parseThreads(threadsMap2.toJSON());
|
|
4843
|
+
handleNewComments(doc, planId, threads2, prev, actor);
|
|
4844
|
+
handleResolvedComments(doc, planId, threads2, prev, actor);
|
|
4845
|
+
});
|
|
4846
|
+
doc.getXmlFragment("document").observeDeep((_events, transaction) => {
|
|
4847
|
+
const now = Date.now();
|
|
4848
|
+
const lastEdit = lastContentEdit.get(planId) || 0;
|
|
4849
|
+
if (now - lastEdit > CONTENT_EDIT_DEBOUNCE_MS) {
|
|
4850
|
+
const actor = transaction.origin?.actor || "System";
|
|
4851
|
+
logPlanEvent(doc, "content_edited", actor);
|
|
4852
|
+
lastContentEdit.set(planId, now);
|
|
4853
|
+
}
|
|
4854
|
+
notifyChange(planId, {
|
|
4855
|
+
type: "content",
|
|
4856
|
+
timestamp: Date.now(),
|
|
4857
|
+
summary: "Content updated"
|
|
4858
|
+
});
|
|
4859
|
+
logger.debug({ planId }, "Content change detected");
|
|
4860
|
+
});
|
|
4861
|
+
doc.getArray(YDOC_KEYS.ARTIFACTS).observe((_event, transaction) => {
|
|
4862
|
+
const prev = previousState.get(planId);
|
|
4863
|
+
if (!prev) return;
|
|
4864
|
+
const actor = transaction.origin?.actor || "System";
|
|
4865
|
+
const newCount = doc.getArray(YDOC_KEYS.ARTIFACTS).length;
|
|
4866
|
+
if (newCount > prev.artifactCount) {
|
|
4867
|
+
const diff = newCount - prev.artifactCount;
|
|
4868
|
+
const artifacts = doc.getArray(YDOC_KEYS.ARTIFACTS).toArray();
|
|
4869
|
+
const lastArtifact = artifacts[artifacts.length - 1];
|
|
4870
|
+
const artifactId = lastArtifact && typeof lastArtifact === "object" && "id" in lastArtifact ? String(lastArtifact.id) : "unknown";
|
|
4871
|
+
logPlanEvent(doc, "artifact_uploaded", actor, {
|
|
4872
|
+
artifactId
|
|
4873
|
+
});
|
|
4874
|
+
notifyChange(planId, {
|
|
4875
|
+
type: "artifacts",
|
|
4876
|
+
timestamp: Date.now(),
|
|
4877
|
+
summary: `${diff} artifact(s) added`,
|
|
4878
|
+
details: { added: diff }
|
|
4879
|
+
});
|
|
4880
|
+
prev.artifactCount = newCount;
|
|
4881
|
+
logger.debug({ planId, added: diff }, "Artifacts added detected");
|
|
4882
|
+
}
|
|
4883
|
+
});
|
|
4884
|
+
doc.getArray(YDOC_KEYS.DELIVERABLES).observeDeep((_events, transaction) => {
|
|
4885
|
+
const prev = previousState.get(planId);
|
|
4886
|
+
if (!prev) return;
|
|
4887
|
+
const deliverables2 = getDeliverables(doc);
|
|
4888
|
+
const allFulfilled2 = deliverables2.length > 0 && deliverables2.every((d) => d.linkedArtifactId);
|
|
4889
|
+
if (allFulfilled2 && !prev.deliverablesFulfilled) {
|
|
4890
|
+
const actor = transaction.origin?.actor || "System";
|
|
4891
|
+
logPlanEvent(
|
|
4892
|
+
doc,
|
|
4893
|
+
"deliverable_linked",
|
|
4894
|
+
actor,
|
|
4895
|
+
{
|
|
4896
|
+
allFulfilled: true
|
|
4897
|
+
},
|
|
4898
|
+
{
|
|
4899
|
+
inboxWorthy: true,
|
|
4900
|
+
inboxFor: "owner"
|
|
4901
|
+
}
|
|
4902
|
+
);
|
|
4903
|
+
prev.deliverablesFulfilled = true;
|
|
4904
|
+
logger.debug({ planId }, "All deliverables fulfilled - inbox-worthy event logged");
|
|
4905
|
+
}
|
|
4906
|
+
});
|
|
4907
|
+
}
|
|
4908
|
+
function detectNewComments(threads, prevCommentIds) {
|
|
4909
|
+
const newComments = [];
|
|
4910
|
+
for (const thread of threads) {
|
|
4911
|
+
for (const comment of thread.comments) {
|
|
4912
|
+
if (!prevCommentIds.has(comment.id)) {
|
|
4913
|
+
newComments.push(comment);
|
|
4914
|
+
}
|
|
4915
|
+
}
|
|
4916
|
+
}
|
|
4917
|
+
return newComments;
|
|
4918
|
+
}
|
|
4919
|
+
function logCommentWithMentions(doc, planId, comment, actor) {
|
|
4920
|
+
const mentions = extractMentions(comment.body);
|
|
4921
|
+
const hasMentions = mentions.length > 0;
|
|
4922
|
+
logPlanEvent(
|
|
4923
|
+
doc,
|
|
4924
|
+
"comment_added",
|
|
4925
|
+
actor,
|
|
4926
|
+
{ commentId: comment.id, mentions: hasMentions },
|
|
4927
|
+
{
|
|
4928
|
+
inboxWorthy: hasMentions,
|
|
4929
|
+
inboxFor: hasMentions ? mentions : void 0
|
|
4930
|
+
}
|
|
4931
|
+
);
|
|
4932
|
+
if (hasMentions) {
|
|
4933
|
+
logger.debug(
|
|
4934
|
+
{ planId, commentId: comment.id, mentions },
|
|
4935
|
+
"Comment with @mentions logged as inbox-worthy"
|
|
4936
|
+
);
|
|
4937
|
+
}
|
|
4938
|
+
}
|
|
4939
|
+
function handleNewComments(doc, planId, threads, prev, actor) {
|
|
4940
|
+
const newCommentCount = threads.reduce((acc, t2) => acc + t2.comments.length, 0);
|
|
4941
|
+
if (newCommentCount <= prev.commentCount) return;
|
|
4942
|
+
const diff = newCommentCount - prev.commentCount;
|
|
4943
|
+
const newComments = detectNewComments(threads, prev.commentIds);
|
|
4944
|
+
for (const comment of newComments) {
|
|
4945
|
+
prev.commentIds.add(comment.id);
|
|
4946
|
+
logCommentWithMentions(doc, planId, comment, actor);
|
|
4947
|
+
}
|
|
4948
|
+
notifyChange(planId, {
|
|
4949
|
+
type: "comments",
|
|
4950
|
+
timestamp: Date.now(),
|
|
4951
|
+
summary: `${diff} new comment(s)`,
|
|
4952
|
+
details: { added: diff }
|
|
4953
|
+
});
|
|
4954
|
+
prev.commentCount = newCommentCount;
|
|
4955
|
+
logger.debug({ planId, added: diff }, "New comments detected");
|
|
4956
|
+
}
|
|
4957
|
+
function handleResolvedComments(doc, planId, threads, prev, actor) {
|
|
4958
|
+
const newResolvedCount = threads.filter((t2) => t2.resolved).length;
|
|
4959
|
+
if (newResolvedCount <= prev.resolvedCount) return;
|
|
4960
|
+
const diff = newResolvedCount - prev.resolvedCount;
|
|
4961
|
+
logPlanEvent(doc, "comment_resolved", actor, { resolvedCount: diff });
|
|
4962
|
+
notifyChange(planId, {
|
|
4963
|
+
type: "resolved",
|
|
4964
|
+
timestamp: Date.now(),
|
|
4965
|
+
summary: `${diff} comment(s) resolved`,
|
|
4966
|
+
details: { resolved: diff }
|
|
4967
|
+
});
|
|
4968
|
+
prev.resolvedCount = newResolvedCount;
|
|
4969
|
+
logger.debug({ planId, resolved: diff }, "Comments resolved detected");
|
|
4970
|
+
}
|
|
4971
|
+
|
|
4972
|
+
// src/registry-server.ts
|
|
4973
|
+
function getParam(value) {
|
|
4974
|
+
if (Array.isArray(value)) return value[0];
|
|
4975
|
+
return value;
|
|
4976
|
+
}
|
|
4977
|
+
function getErrorStatus2(error) {
|
|
4978
|
+
if (!error || typeof error !== "object") return 500;
|
|
4979
|
+
const record = Object.fromEntries(Object.entries(error));
|
|
4980
|
+
const status = record.status;
|
|
4981
|
+
return typeof status === "number" ? status : 500;
|
|
4982
|
+
}
|
|
4983
|
+
var PERSISTENCE_DIR = join4(homedir3(), ".shipyard", "plans");
|
|
4984
|
+
var HUB_LOCK_FILE = join4(homedir3(), ".shipyard", "hub.lock");
|
|
4985
|
+
var SHIPYARD_DIR = join4(homedir3(), ".shipyard");
|
|
4986
|
+
var MAX_LOCK_RETRIES = 3;
|
|
4987
|
+
var messageSync = 0;
|
|
4988
|
+
var messageAwareness = 1;
|
|
4989
|
+
var docs2 = /* @__PURE__ */ new Map();
|
|
4990
|
+
var awarenessMap = /* @__PURE__ */ new Map();
|
|
4991
|
+
var conns = /* @__PURE__ */ new Map();
|
|
4992
|
+
var ldb = null;
|
|
4993
|
+
async function readLockHolderPid() {
|
|
4994
|
+
try {
|
|
4995
|
+
const content = await readFile(HUB_LOCK_FILE, "utf-8");
|
|
4996
|
+
const pidStr = content.split("\n")[0] ?? "";
|
|
4997
|
+
return Number.parseInt(pidStr, 10);
|
|
4998
|
+
} catch (readErr) {
|
|
4999
|
+
logger.error({ err: readErr }, "Failed to read hub lock file");
|
|
5000
|
+
return null;
|
|
5001
|
+
}
|
|
5002
|
+
}
|
|
5003
|
+
function isLockHolderAlive(pid) {
|
|
5004
|
+
try {
|
|
5005
|
+
process.kill(pid, 0);
|
|
5006
|
+
return true;
|
|
5007
|
+
} catch {
|
|
5008
|
+
return false;
|
|
5009
|
+
}
|
|
5010
|
+
}
|
|
5011
|
+
async function tryRemoveStaleLock(stalePid, retryCount) {
|
|
5012
|
+
logger.warn({ stalePid, retryCount }, "Removing stale hub lock");
|
|
5013
|
+
try {
|
|
5014
|
+
await unlink(HUB_LOCK_FILE);
|
|
5015
|
+
return true;
|
|
5016
|
+
} catch (unlinkErr) {
|
|
5017
|
+
logger.error({ err: unlinkErr, stalePid, retryCount }, "Failed to remove stale hub lock");
|
|
5018
|
+
return false;
|
|
5019
|
+
}
|
|
5020
|
+
}
|
|
5021
|
+
function registerLockCleanupHandler() {
|
|
5022
|
+
process.once("exit", () => {
|
|
5023
|
+
try {
|
|
5024
|
+
unlinkSync(HUB_LOCK_FILE);
|
|
5025
|
+
} catch {
|
|
5026
|
+
}
|
|
5027
|
+
});
|
|
5028
|
+
}
|
|
5029
|
+
async function handleExistingLock(retryCount) {
|
|
5030
|
+
const pid = await readLockHolderPid();
|
|
5031
|
+
if (pid === null) return false;
|
|
5032
|
+
if (isLockHolderAlive(pid)) {
|
|
5033
|
+
logger.debug({ holderPid: pid }, "Hub lock held by active process");
|
|
5034
|
+
return false;
|
|
5035
|
+
}
|
|
5036
|
+
if (retryCount >= MAX_LOCK_RETRIES) {
|
|
5037
|
+
logger.error(
|
|
5038
|
+
{ stalePid: pid, retryCount },
|
|
5039
|
+
"Max retries exceeded while removing stale hub lock"
|
|
5040
|
+
);
|
|
5041
|
+
return false;
|
|
5042
|
+
}
|
|
5043
|
+
await tryRemoveStaleLock(pid, retryCount);
|
|
5044
|
+
return tryAcquireHubLock(retryCount + 1);
|
|
5045
|
+
}
|
|
5046
|
+
async function tryAcquireHubLock(retryCount = 0) {
|
|
5047
|
+
try {
|
|
5048
|
+
mkdirSync(SHIPYARD_DIR, { recursive: true });
|
|
5049
|
+
await writeFile2(HUB_LOCK_FILE, `${process.pid}
|
|
5050
|
+
${Date.now()}`, { flag: "wx" });
|
|
5051
|
+
registerLockCleanupHandler();
|
|
5052
|
+
logger.info({ pid: process.pid }, "Acquired hub lock");
|
|
5053
|
+
return true;
|
|
5054
|
+
} catch (err) {
|
|
5055
|
+
if (hasErrorCode(err, "EEXIST")) {
|
|
5056
|
+
return handleExistingLock(retryCount);
|
|
5057
|
+
}
|
|
5058
|
+
logger.error({ err }, "Failed to acquire hub lock");
|
|
5059
|
+
return false;
|
|
5060
|
+
}
|
|
5061
|
+
}
|
|
5062
|
+
async function releaseHubLock() {
|
|
5063
|
+
try {
|
|
5064
|
+
await unlink(HUB_LOCK_FILE);
|
|
5065
|
+
logger.info("Released hub lock");
|
|
5066
|
+
} catch (err) {
|
|
5067
|
+
logger.debug({ err }, "Hub lock already released");
|
|
5068
|
+
}
|
|
5069
|
+
}
|
|
5070
|
+
function isLevelDbLockError(error) {
|
|
5071
|
+
return error.message?.includes("LOCK") || error.message?.includes("lock");
|
|
5072
|
+
}
|
|
5073
|
+
function isProcessAlive(pid) {
|
|
5074
|
+
try {
|
|
5075
|
+
process.kill(pid, 0);
|
|
5076
|
+
return true;
|
|
5077
|
+
} catch {
|
|
5078
|
+
return false;
|
|
5079
|
+
}
|
|
5080
|
+
}
|
|
5081
|
+
function tryRecoverStaleLock(originalError) {
|
|
5082
|
+
const lockFile = join4(PERSISTENCE_DIR, "LOCK");
|
|
5083
|
+
try {
|
|
5084
|
+
const hubLockContent = readFileSync2(HUB_LOCK_FILE, "utf-8");
|
|
5085
|
+
const pidStr = hubLockContent.split("\n")[0] ?? "";
|
|
5086
|
+
const pid = Number.parseInt(pidStr, 10);
|
|
5087
|
+
if (isProcessAlive(pid)) {
|
|
5088
|
+
logger.error({ holderPid: pid }, "LevelDB locked by active process, cannot recover");
|
|
5089
|
+
throw originalError;
|
|
5090
|
+
}
|
|
5091
|
+
logger.warn("Hub process dead, removing stale LevelDB lock");
|
|
5092
|
+
unlinkSync(lockFile);
|
|
5093
|
+
return true;
|
|
5094
|
+
} catch (hubLockErr) {
|
|
5095
|
+
if (hubLockErr === originalError) {
|
|
5096
|
+
throw hubLockErr;
|
|
5097
|
+
}
|
|
5098
|
+
logger.warn("No hub.lock found, assuming LevelDB lock is stale");
|
|
5099
|
+
unlinkSync(lockFile);
|
|
5100
|
+
return true;
|
|
5101
|
+
}
|
|
5102
|
+
}
|
|
5103
|
+
function initPersistence() {
|
|
5104
|
+
if (ldb) return;
|
|
5105
|
+
mkdirSync(PERSISTENCE_DIR, { recursive: true });
|
|
5106
|
+
try {
|
|
5107
|
+
ldb = new LeveldbPersistence(PERSISTENCE_DIR);
|
|
5108
|
+
logger.info({ dir: PERSISTENCE_DIR }, "LevelDB persistence initialized");
|
|
5109
|
+
return;
|
|
5110
|
+
} catch (err) {
|
|
5111
|
+
if (!(err instanceof Error)) {
|
|
5112
|
+
logger.error({ err }, "Failed to initialize LevelDB persistence with unknown error");
|
|
5113
|
+
throw new Error(String(err));
|
|
5114
|
+
}
|
|
5115
|
+
if (!isLevelDbLockError(err)) {
|
|
5116
|
+
logger.error({ err }, "Failed to initialize LevelDB persistence");
|
|
5117
|
+
throw err;
|
|
5118
|
+
}
|
|
5119
|
+
logger.warn({ err }, "LevelDB locked, checking for stale lock");
|
|
5120
|
+
tryRecoverStaleLock(err);
|
|
5121
|
+
ldb = new LeveldbPersistence(PERSISTENCE_DIR);
|
|
5122
|
+
logger.info("Recovered from stale LevelDB lock");
|
|
5123
|
+
}
|
|
5124
|
+
}
|
|
5125
|
+
async function getDoc(docName) {
|
|
5126
|
+
initPersistence();
|
|
5127
|
+
const persistence = ldb;
|
|
5128
|
+
if (!persistence) {
|
|
5129
|
+
throw new Error("LevelDB persistence failed to initialize");
|
|
5130
|
+
}
|
|
5131
|
+
let doc = docs2.get(docName);
|
|
5132
|
+
if (!doc) {
|
|
5133
|
+
doc = new Y4.Doc();
|
|
5134
|
+
const persistedDoc = await persistence.getYDoc(docName);
|
|
5135
|
+
const state = Y4.encodeStateAsUpdate(persistedDoc);
|
|
5136
|
+
Y4.applyUpdate(doc, state);
|
|
5137
|
+
doc.on("update", (update) => {
|
|
5138
|
+
persistence.storeUpdate(docName, update);
|
|
5139
|
+
});
|
|
5140
|
+
docs2.set(docName, doc);
|
|
5141
|
+
const awareness = new awarenessProtocol.Awareness(doc);
|
|
5142
|
+
awarenessMap.set(docName, awareness);
|
|
5143
|
+
attachObservers(docName, doc);
|
|
5144
|
+
attachCRDTValidation(docName, doc);
|
|
5145
|
+
}
|
|
5146
|
+
return doc;
|
|
5147
|
+
}
|
|
5148
|
+
async function getOrCreateDoc2(docName) {
|
|
5149
|
+
return getDoc(docName);
|
|
5150
|
+
}
|
|
5151
|
+
function hasActiveConnections2(planId) {
|
|
5152
|
+
const connections = conns.get(planId);
|
|
5153
|
+
return connections !== void 0 && connections.size > 0;
|
|
5154
|
+
}
|
|
5155
|
+
function send(ws, message) {
|
|
5156
|
+
if (ws.readyState === ws.OPEN) {
|
|
5157
|
+
ws.send(message);
|
|
5158
|
+
}
|
|
5159
|
+
}
|
|
5160
|
+
function broadcastUpdate(docName, update, origin) {
|
|
5161
|
+
const docConns = conns.get(docName);
|
|
5162
|
+
if (!docConns) return;
|
|
5163
|
+
const encoder = encoding.createEncoder();
|
|
5164
|
+
encoding.writeVarUint(encoder, messageSync);
|
|
5165
|
+
syncProtocol.writeUpdate(encoder, update);
|
|
5166
|
+
const message = encoding.toUint8Array(encoder);
|
|
5167
|
+
for (const conn of docConns) {
|
|
5168
|
+
if (conn !== origin) {
|
|
5169
|
+
send(conn, message);
|
|
5170
|
+
}
|
|
5171
|
+
}
|
|
5172
|
+
}
|
|
5173
|
+
function processMessage(message, doc, awareness, planId, ws) {
|
|
5174
|
+
try {
|
|
5175
|
+
const decoder = decoding.createDecoder(new Uint8Array(message));
|
|
5176
|
+
const messageType = decoding.readVarUint(decoder);
|
|
5177
|
+
switch (messageType) {
|
|
5178
|
+
case messageSync: {
|
|
5179
|
+
const encoder = encoding.createEncoder();
|
|
5180
|
+
encoding.writeVarUint(encoder, messageSync);
|
|
5181
|
+
syncProtocol.readSyncMessage(decoder, encoder, doc, ws);
|
|
5182
|
+
if (encoding.length(encoder) > 1) {
|
|
5183
|
+
send(ws, encoding.toUint8Array(encoder));
|
|
5184
|
+
}
|
|
5185
|
+
break;
|
|
5186
|
+
}
|
|
5187
|
+
case messageAwareness: {
|
|
5188
|
+
awarenessProtocol.applyAwarenessUpdate(awareness, decoding.readVarUint8Array(decoder), ws);
|
|
5189
|
+
break;
|
|
5190
|
+
}
|
|
5191
|
+
}
|
|
5192
|
+
} catch (err) {
|
|
5193
|
+
logger.error({ err, planId }, "Failed to process message");
|
|
5194
|
+
}
|
|
5195
|
+
}
|
|
5196
|
+
function handleWebSocketConnection(ws, req) {
|
|
5197
|
+
const planId = req.url?.slice(1) || "default";
|
|
5198
|
+
logger.info({ planId }, "WebSocket client connected to registry");
|
|
5199
|
+
const pendingMessages = [];
|
|
5200
|
+
let docReady = false;
|
|
5201
|
+
let doc;
|
|
5202
|
+
let awareness;
|
|
5203
|
+
ws.on("message", (message) => {
|
|
5204
|
+
if (!docReady) {
|
|
5205
|
+
pendingMessages.push(message);
|
|
5206
|
+
logger.debug(
|
|
5207
|
+
{ planId, bufferedCount: pendingMessages.length },
|
|
5208
|
+
"Buffering message (doc not ready)"
|
|
5209
|
+
);
|
|
5210
|
+
return;
|
|
5211
|
+
}
|
|
5212
|
+
processMessage(message, doc, awareness, planId, ws);
|
|
5213
|
+
});
|
|
5214
|
+
ws.on("error", (err) => {
|
|
5215
|
+
logger.error({ err, planId }, "WebSocket error");
|
|
5216
|
+
});
|
|
5217
|
+
(async () => {
|
|
5218
|
+
try {
|
|
5219
|
+
doc = await getDoc(planId);
|
|
5220
|
+
const awarenessResult = awarenessMap.get(planId);
|
|
5221
|
+
if (!awarenessResult) {
|
|
5222
|
+
throw new Error(`Awareness not found for planId: ${planId}`);
|
|
5223
|
+
}
|
|
5224
|
+
awareness = awarenessResult;
|
|
5225
|
+
logger.debug({ planId }, "Got doc and awareness");
|
|
5226
|
+
if (!conns.has(planId)) {
|
|
5227
|
+
conns.set(planId, /* @__PURE__ */ new Set());
|
|
5228
|
+
}
|
|
5229
|
+
const planConns = conns.get(planId);
|
|
5230
|
+
planConns?.add(ws);
|
|
5231
|
+
const updateHandler = (update, origin) => {
|
|
5232
|
+
broadcastUpdate(planId, update, origin);
|
|
5233
|
+
};
|
|
5234
|
+
doc.on("update", updateHandler);
|
|
5235
|
+
const awarenessHandler = ({ added, updated, removed }, _origin) => {
|
|
5236
|
+
const changedClients = added.concat(updated, removed);
|
|
5237
|
+
const encoder2 = encoding.createEncoder();
|
|
5238
|
+
encoding.writeVarUint(encoder2, messageAwareness);
|
|
5239
|
+
encoding.writeVarUint8Array(
|
|
5240
|
+
encoder2,
|
|
5241
|
+
awarenessProtocol.encodeAwarenessUpdate(awareness, changedClients)
|
|
5242
|
+
);
|
|
5243
|
+
const message = encoding.toUint8Array(encoder2);
|
|
5244
|
+
for (const conn of conns.get(planId) || []) {
|
|
5245
|
+
send(conn, message);
|
|
5246
|
+
}
|
|
5247
|
+
};
|
|
5248
|
+
awareness.on("update", awarenessHandler);
|
|
5249
|
+
docReady = true;
|
|
5250
|
+
if (pendingMessages.length > 0) {
|
|
5251
|
+
logger.debug({ planId, count: pendingMessages.length }, "Processing buffered messages");
|
|
5252
|
+
for (const msg of pendingMessages) {
|
|
5253
|
+
processMessage(msg, doc, awareness, planId, ws);
|
|
5254
|
+
}
|
|
5255
|
+
pendingMessages.length = 0;
|
|
5256
|
+
}
|
|
5257
|
+
const encoder = encoding.createEncoder();
|
|
5258
|
+
encoding.writeVarUint(encoder, messageSync);
|
|
5259
|
+
syncProtocol.writeSyncStep1(encoder, doc);
|
|
5260
|
+
send(ws, encoding.toUint8Array(encoder));
|
|
5261
|
+
const awarenessStates = awareness.getStates();
|
|
5262
|
+
if (awarenessStates.size > 0) {
|
|
5263
|
+
const awarenessEncoder = encoding.createEncoder();
|
|
5264
|
+
encoding.writeVarUint(awarenessEncoder, messageAwareness);
|
|
5265
|
+
encoding.writeVarUint8Array(
|
|
5266
|
+
awarenessEncoder,
|
|
5267
|
+
awarenessProtocol.encodeAwarenessUpdate(awareness, Array.from(awarenessStates.keys()))
|
|
5268
|
+
);
|
|
5269
|
+
send(ws, encoding.toUint8Array(awarenessEncoder));
|
|
5270
|
+
}
|
|
5271
|
+
ws.on("close", () => {
|
|
5272
|
+
logger.info({ planId }, "WebSocket client disconnected from registry");
|
|
5273
|
+
doc.off("update", updateHandler);
|
|
5274
|
+
awareness.off("update", awarenessHandler);
|
|
5275
|
+
conns.get(planId)?.delete(ws);
|
|
5276
|
+
awarenessProtocol.removeAwarenessStates(awareness, [doc.clientID], null);
|
|
5277
|
+
});
|
|
5278
|
+
} catch (err) {
|
|
5279
|
+
logger.error({ err, planId }, "Error handling WebSocket connection");
|
|
5280
|
+
ws.close();
|
|
5281
|
+
}
|
|
5282
|
+
})();
|
|
5283
|
+
}
|
|
5284
|
+
async function handleHealthCheck(_req, res) {
|
|
5285
|
+
res.json({ status: "ok" });
|
|
5286
|
+
}
|
|
5287
|
+
async function handleGetPRDiff(req, res) {
|
|
5288
|
+
const planId = getParam(req.params.id);
|
|
5289
|
+
const prNumber = getParam(req.params.prNumber);
|
|
5290
|
+
if (!planId || !prNumber) {
|
|
5291
|
+
res.status(400).json({ error: "Missing plan ID or PR number" });
|
|
5292
|
+
return;
|
|
5293
|
+
}
|
|
5294
|
+
try {
|
|
5295
|
+
const doc = await getOrCreateDoc2(planId);
|
|
5296
|
+
const metadata = getPlanMetadata(doc);
|
|
5297
|
+
if (!metadata || !metadata.repo) {
|
|
5298
|
+
res.status(404).json({ error: "Plan not found or repo not set" });
|
|
5299
|
+
return;
|
|
5300
|
+
}
|
|
5301
|
+
const octokit = getOctokit();
|
|
5302
|
+
if (!octokit) {
|
|
5303
|
+
res.status(500).json({ error: "GitHub authentication not configured" });
|
|
5304
|
+
return;
|
|
5305
|
+
}
|
|
5306
|
+
const { owner, repoName } = parseRepoString(metadata.repo);
|
|
5307
|
+
const prNum = Number.parseInt(prNumber, 10);
|
|
5308
|
+
const response = await octokit.request("GET /repos/{owner}/{repo}/pulls/{pull_number}", {
|
|
5309
|
+
owner,
|
|
5310
|
+
repo: repoName,
|
|
5311
|
+
pull_number: prNum,
|
|
5312
|
+
headers: {
|
|
5313
|
+
accept: "application/vnd.github.diff"
|
|
5314
|
+
}
|
|
5315
|
+
});
|
|
5316
|
+
res.type("text/plain").send(response.data);
|
|
5317
|
+
logger.debug({ planId, prNumber: prNum, repo: metadata.repo }, "Served PR diff");
|
|
5318
|
+
} catch (error) {
|
|
5319
|
+
logger.error({ error, planId, prNumber }, "Failed to fetch PR diff");
|
|
5320
|
+
const status = getErrorStatus2(error);
|
|
5321
|
+
res.status(status).json({ error: "Failed to fetch PR diff" });
|
|
5322
|
+
}
|
|
5323
|
+
}
|
|
5324
|
+
async function handleGetPRFiles(req, res) {
|
|
5325
|
+
const planId = getParam(req.params.id);
|
|
5326
|
+
const prNumber = getParam(req.params.prNumber);
|
|
5327
|
+
if (!planId || !prNumber) {
|
|
5328
|
+
res.status(400).json({ error: "Missing plan ID or PR number" });
|
|
5329
|
+
return;
|
|
5330
|
+
}
|
|
5331
|
+
try {
|
|
5332
|
+
const doc = await getOrCreateDoc2(planId);
|
|
5333
|
+
const metadata = getPlanMetadata(doc);
|
|
5334
|
+
if (!metadata || !metadata.repo) {
|
|
5335
|
+
res.status(404).json({ error: "Plan not found or repo not set" });
|
|
5336
|
+
return;
|
|
5337
|
+
}
|
|
5338
|
+
const octokit = getOctokit();
|
|
5339
|
+
if (!octokit) {
|
|
5340
|
+
res.status(500).json({ error: "GitHub authentication not configured" });
|
|
5341
|
+
return;
|
|
5342
|
+
}
|
|
5343
|
+
const { owner, repoName } = parseRepoString(metadata.repo);
|
|
5344
|
+
const prNum = Number.parseInt(prNumber, 10);
|
|
5345
|
+
const { data: files } = await octokit.pulls.listFiles({
|
|
5346
|
+
owner,
|
|
5347
|
+
repo: repoName,
|
|
5348
|
+
pull_number: prNum
|
|
5349
|
+
});
|
|
5350
|
+
const fileList = files.map((file) => ({
|
|
5351
|
+
filename: file.filename,
|
|
5352
|
+
status: file.status,
|
|
5353
|
+
additions: file.additions,
|
|
5354
|
+
deletions: file.deletions,
|
|
5355
|
+
changes: file.changes,
|
|
5356
|
+
patch: file.patch
|
|
5357
|
+
}));
|
|
5358
|
+
res.json({ files: fileList });
|
|
5359
|
+
logger.debug({ planId, prNumber: prNum, fileCount: fileList.length }, "Served PR files");
|
|
5360
|
+
} catch (error) {
|
|
5361
|
+
logger.error({ error, planId, prNumber }, "Failed to fetch PR files");
|
|
5362
|
+
const status = getErrorStatus2(error);
|
|
5363
|
+
res.status(status).json({ error: "Failed to fetch PR files" });
|
|
5364
|
+
}
|
|
5365
|
+
}
|
|
5366
|
+
async function handleGetTranscript(req, res) {
|
|
5367
|
+
const planId = getParam(req.params.id);
|
|
5368
|
+
if (!planId) {
|
|
5369
|
+
res.status(400).json({ error: "Missing plan ID" });
|
|
5370
|
+
return;
|
|
5371
|
+
}
|
|
5372
|
+
try {
|
|
5373
|
+
const doc = await getOrCreateDoc2(planId);
|
|
5374
|
+
const metadata = getPlanMetadata(doc);
|
|
5375
|
+
if (!metadata?.origin) {
|
|
5376
|
+
res.status(404).json({ error: "Plan has no origin metadata" });
|
|
5377
|
+
return;
|
|
5378
|
+
}
|
|
5379
|
+
if (metadata.origin.platform !== "claude-code") {
|
|
5380
|
+
res.status(400).json({ error: "Transcript only available for Claude Code plans" });
|
|
5381
|
+
return;
|
|
5382
|
+
}
|
|
5383
|
+
const originRecord = Object.fromEntries(Object.entries(metadata.origin));
|
|
5384
|
+
const transcriptPath = originRecord.transcriptPath;
|
|
5385
|
+
if (typeof transcriptPath !== "string" || !transcriptPath) {
|
|
5386
|
+
res.status(404).json({ error: "No transcript path in origin metadata" });
|
|
5387
|
+
return;
|
|
5388
|
+
}
|
|
5389
|
+
const content = await readFile(transcriptPath, "utf-8");
|
|
5390
|
+
res.type("text/plain").send(content);
|
|
5391
|
+
logger.debug({ planId, transcriptPath, size: content.length }, "Served transcript for handoff");
|
|
5392
|
+
} catch (error) {
|
|
5393
|
+
if (hasErrorCode(error, "ENOENT")) {
|
|
5394
|
+
res.status(404).json({ error: "Transcript file not found" });
|
|
5395
|
+
} else {
|
|
5396
|
+
logger.error({ error, planId }, "Failed to read transcript");
|
|
5397
|
+
res.status(500).json({ error: "Failed to read transcript" });
|
|
5398
|
+
}
|
|
5399
|
+
}
|
|
5400
|
+
}
|
|
5401
|
+
function createPlanStore() {
|
|
5402
|
+
return {
|
|
5403
|
+
createSubscription: (params) => {
|
|
5404
|
+
const subscribe = params.subscribe.filter(
|
|
5405
|
+
(s) => s === "status" || s === "comments" || s === "resolved" || s === "content" || s === "artifacts"
|
|
5406
|
+
);
|
|
5407
|
+
return createSubscription({
|
|
5408
|
+
planId: params.planId,
|
|
5409
|
+
subscribe,
|
|
5410
|
+
windowMs: params.windowMs,
|
|
5411
|
+
maxWindowMs: params.maxWindowMs,
|
|
5412
|
+
threshold: params.threshold
|
|
5413
|
+
});
|
|
5414
|
+
},
|
|
5415
|
+
getChanges: (planId, clientId) => getChanges(planId, clientId),
|
|
5416
|
+
deleteSubscription: (planId, clientId) => deleteSubscription(planId, clientId),
|
|
5417
|
+
hasActiveConnections: async (planId) => hasActiveConnections2(planId)
|
|
5418
|
+
};
|
|
5419
|
+
}
|
|
5420
|
+
function createContext() {
|
|
5421
|
+
return {
|
|
5422
|
+
getOrCreateDoc: getOrCreateDoc2,
|
|
5423
|
+
getPlanStore: createPlanStore,
|
|
5424
|
+
logger,
|
|
5425
|
+
hookHandlers: createHookHandlers(),
|
|
5426
|
+
conversationHandlers: createConversationHandlers(),
|
|
5427
|
+
getLocalChanges,
|
|
5428
|
+
getFileContent
|
|
5429
|
+
};
|
|
5430
|
+
}
|
|
5431
|
+
function createApp() {
|
|
5432
|
+
const app = express();
|
|
5433
|
+
const httpServer = http.createServer(app);
|
|
5434
|
+
app.use((_req, res, next) => {
|
|
5435
|
+
res.header("Access-Control-Allow-Origin", "*");
|
|
5436
|
+
res.header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
|
|
5437
|
+
res.header("Access-Control-Allow-Headers", "Content-Type");
|
|
5438
|
+
next();
|
|
5439
|
+
});
|
|
5440
|
+
app.options("{*splat}", (_req, res) => {
|
|
5441
|
+
res.sendStatus(204);
|
|
5442
|
+
});
|
|
5443
|
+
app.use(express.json({ limit: "10mb" }));
|
|
5444
|
+
app.use(
|
|
5445
|
+
"/trpc",
|
|
5446
|
+
createExpressMiddleware({
|
|
5447
|
+
router: appRouter,
|
|
5448
|
+
createContext
|
|
5449
|
+
})
|
|
5450
|
+
);
|
|
5451
|
+
app.get("/registry", handleHealthCheck);
|
|
5452
|
+
app.get("/api/plan/:planId/has-connections", (req, res) => {
|
|
5453
|
+
const planId = req.params.planId;
|
|
5454
|
+
if (!planId) {
|
|
5455
|
+
res.status(400).json({ error: "Missing plan ID" });
|
|
5456
|
+
return;
|
|
5457
|
+
}
|
|
5458
|
+
const hasConnections = hasActiveConnections2(planId);
|
|
5459
|
+
res.json({ hasConnections });
|
|
5460
|
+
});
|
|
5461
|
+
app.get("/api/plan/:id/transcript", handleGetTranscript);
|
|
5462
|
+
app.get("/api/plans/:id/pr-diff/:prNumber", handleGetPRDiff);
|
|
5463
|
+
app.get("/api/plans/:id/pr-files/:prNumber", handleGetPRFiles);
|
|
5464
|
+
app.get("/artifacts/:planId/:filename", async (req, res) => {
|
|
5465
|
+
const planId = getParam(req.params.planId);
|
|
5466
|
+
const filename = getParam(req.params.filename);
|
|
5467
|
+
if (!planId || !filename) {
|
|
5468
|
+
res.status(400).json({ error: "Missing planId or filename" });
|
|
5469
|
+
return;
|
|
5470
|
+
}
|
|
5471
|
+
const ARTIFACTS_DIR = join4(homedir3(), ".shipyard", "artifacts");
|
|
5472
|
+
const fullPath = resolve(ARTIFACTS_DIR, planId, filename);
|
|
5473
|
+
if (!fullPath.startsWith(ARTIFACTS_DIR + sep)) {
|
|
5474
|
+
res.status(400).json({ error: "Invalid artifact path" });
|
|
5475
|
+
return;
|
|
5476
|
+
}
|
|
5477
|
+
const buffer = await readFile(fullPath).catch(() => null);
|
|
5478
|
+
if (!buffer) {
|
|
5479
|
+
res.status(404).json({ error: "Artifact not found" });
|
|
5480
|
+
return;
|
|
5481
|
+
}
|
|
5482
|
+
const ext = filename.split(".").pop()?.toLowerCase();
|
|
5483
|
+
const mimeTypes = {
|
|
5484
|
+
png: "image/png",
|
|
5485
|
+
jpg: "image/jpeg",
|
|
5486
|
+
jpeg: "image/jpeg",
|
|
5487
|
+
mp4: "video/mp4",
|
|
5488
|
+
webm: "video/webm",
|
|
5489
|
+
json: "application/json",
|
|
5490
|
+
txt: "text/plain"
|
|
5491
|
+
};
|
|
5492
|
+
const contentType = mimeTypes[ext || ""] || "application/octet-stream";
|
|
5493
|
+
res.setHeader("Content-Type", contentType);
|
|
5494
|
+
res.setHeader("Content-Length", buffer.length);
|
|
5495
|
+
res.setHeader("Cache-Control", "public, max-age=31536000");
|
|
5496
|
+
res.send(buffer);
|
|
5497
|
+
});
|
|
5498
|
+
return { app, httpServer };
|
|
5499
|
+
}
|
|
5500
|
+
async function startRegistryServer() {
|
|
5501
|
+
const ports = registryConfig.REGISTRY_PORT;
|
|
5502
|
+
const { httpServer } = createApp();
|
|
5503
|
+
const wss = new WebSocketServer({ noServer: true });
|
|
5504
|
+
httpServer.on("upgrade", (request, socket, head) => {
|
|
5505
|
+
wss.handleUpgrade(request, socket, head, (ws) => {
|
|
5506
|
+
wss.emit("connection", ws, request);
|
|
5507
|
+
});
|
|
5508
|
+
});
|
|
5509
|
+
wss.on("connection", handleWebSocketConnection);
|
|
5510
|
+
process.once("SIGINT", async () => {
|
|
5511
|
+
logger.info("SIGINT received, shutting down gracefully");
|
|
5512
|
+
const { stopPeriodicCleanup } = await import("./session-registry-CBDXMXY3.js");
|
|
5513
|
+
stopPeriodicCleanup();
|
|
5514
|
+
await releaseHubLock();
|
|
5515
|
+
process.exit(0);
|
|
5516
|
+
});
|
|
5517
|
+
process.once("SIGTERM", async () => {
|
|
5518
|
+
logger.info("SIGTERM received, shutting down gracefully");
|
|
5519
|
+
const { stopPeriodicCleanup } = await import("./session-registry-CBDXMXY3.js");
|
|
5520
|
+
stopPeriodicCleanup();
|
|
5521
|
+
await releaseHubLock();
|
|
5522
|
+
process.exit(0);
|
|
5523
|
+
});
|
|
5524
|
+
for (const port of ports) {
|
|
5525
|
+
try {
|
|
5526
|
+
await new Promise((resolve2, reject) => {
|
|
5527
|
+
httpServer.listen(port, "127.0.0.1", () => {
|
|
5528
|
+
logger.info(
|
|
5529
|
+
{ port, persistence: PERSISTENCE_DIR },
|
|
5530
|
+
"Registry server started with WebSocket and tRPC support"
|
|
5531
|
+
);
|
|
5532
|
+
startCleanupInterval();
|
|
5533
|
+
resolve2();
|
|
5534
|
+
});
|
|
5535
|
+
httpServer.on("error", (err) => {
|
|
5536
|
+
if (err.code === "EADDRINUSE") {
|
|
5537
|
+
reject(err);
|
|
5538
|
+
} else {
|
|
5539
|
+
logger.error({ err, port }, "Registry server error");
|
|
5540
|
+
}
|
|
5541
|
+
});
|
|
5542
|
+
});
|
|
5543
|
+
return port;
|
|
5544
|
+
} catch (err) {
|
|
5545
|
+
logger.debug({ err, port }, "Port unavailable or server failed to start");
|
|
5546
|
+
}
|
|
5547
|
+
}
|
|
5548
|
+
logger.warn({ ports }, "All registry ports in use");
|
|
5549
|
+
return null;
|
|
5550
|
+
}
|
|
5551
|
+
async function isRegistryRunning() {
|
|
5552
|
+
const ports = registryConfig.REGISTRY_PORT;
|
|
5553
|
+
for (const port of ports) {
|
|
5554
|
+
try {
|
|
5555
|
+
const res = await fetch(`http://localhost:${port}/registry`, {
|
|
5556
|
+
signal: AbortSignal.timeout(1e3)
|
|
5557
|
+
});
|
|
5558
|
+
if (res.ok) {
|
|
5559
|
+
return port;
|
|
5560
|
+
}
|
|
5561
|
+
} catch {
|
|
5562
|
+
}
|
|
5563
|
+
}
|
|
5564
|
+
return null;
|
|
5565
|
+
}
|
|
5566
|
+
|
|
5567
|
+
export {
|
|
5568
|
+
PlanStatusValues,
|
|
5569
|
+
createLinkedPR,
|
|
3743
5570
|
InputRequestSchema,
|
|
3744
5571
|
createInputRequest,
|
|
3745
|
-
normalizeChoiceOptions,
|
|
3746
|
-
CHOICE_DROPDOWN_THRESHOLD,
|
|
3747
|
-
MAX_QUESTIONS_PER_REQUEST,
|
|
3748
|
-
QuestionSchema,
|
|
3749
|
-
MultiQuestionInputRequestSchema,
|
|
3750
|
-
AnyInputRequestSchema,
|
|
3751
5572
|
createMultiQuestionInputRequest,
|
|
3752
5573
|
YDOC_KEYS,
|
|
3753
|
-
isValidYDocKey,
|
|
3754
|
-
ThreadCommentSchema,
|
|
3755
|
-
ThreadSchema,
|
|
3756
|
-
isThread,
|
|
3757
5574
|
parseThreads,
|
|
3758
5575
|
extractTextFromCommentBody,
|
|
3759
|
-
extractMentions,
|
|
3760
|
-
VALID_STATUS_TRANSITIONS,
|
|
3761
5576
|
getPlanMetadata,
|
|
3762
|
-
getPlanMetadataWithValidation,
|
|
3763
5577
|
setPlanMetadata,
|
|
3764
5578
|
resetPlanToDraft,
|
|
3765
5579
|
transitionPlanStatus,
|
|
3766
5580
|
initPlanMetadata,
|
|
3767
|
-
getStepCompletions,
|
|
3768
|
-
toggleStepCompletion,
|
|
3769
|
-
isStepCompleted,
|
|
3770
5581
|
getArtifacts,
|
|
3771
5582
|
addArtifact,
|
|
3772
|
-
removeArtifact,
|
|
3773
|
-
getAgentPresences,
|
|
3774
|
-
setAgentPresence,
|
|
3775
|
-
clearAgentPresence,
|
|
3776
|
-
getAgentPresence,
|
|
3777
5583
|
getDeliverables,
|
|
3778
5584
|
addDeliverable,
|
|
3779
5585
|
linkArtifactToDeliverable,
|
|
3780
|
-
getPlanOwnerId,
|
|
3781
|
-
isApprovalRequired,
|
|
3782
|
-
getApprovedUsers,
|
|
3783
|
-
isUserApproved,
|
|
3784
|
-
approveUser,
|
|
3785
|
-
revokeUser,
|
|
3786
|
-
getRejectedUsers,
|
|
3787
|
-
isUserRejected,
|
|
3788
|
-
rejectUser,
|
|
3789
|
-
unrejectUser,
|
|
3790
5586
|
getLinkedPRs,
|
|
3791
5587
|
linkPR,
|
|
3792
|
-
unlinkPR,
|
|
3793
|
-
getLinkedPR,
|
|
3794
|
-
updateLinkedPRStatus,
|
|
3795
5588
|
getPRReviewComments,
|
|
3796
|
-
getPRReviewCommentsForPR,
|
|
3797
|
-
addPRReviewComment,
|
|
3798
|
-
resolvePRReviewComment,
|
|
3799
|
-
removePRReviewComment,
|
|
3800
5589
|
getLocalDiffComments,
|
|
3801
|
-
getLocalDiffCommentsForFile,
|
|
3802
|
-
addLocalDiffComment,
|
|
3803
|
-
resolveLocalDiffComment,
|
|
3804
|
-
removeLocalDiffComment,
|
|
3805
|
-
markPlanAsViewed,
|
|
3806
|
-
getViewedBy,
|
|
3807
|
-
isPlanUnread,
|
|
3808
|
-
getConversationVersions,
|
|
3809
|
-
addConversationVersion,
|
|
3810
|
-
markVersionHandedOff,
|
|
3811
5590
|
logPlanEvent,
|
|
3812
5591
|
getPlanEvents,
|
|
3813
5592
|
getSnapshots,
|
|
3814
5593
|
addSnapshot,
|
|
3815
5594
|
createPlanSnapshot,
|
|
3816
|
-
getLatestSnapshot,
|
|
3817
|
-
addPlanTag,
|
|
3818
|
-
removePlanTag,
|
|
3819
|
-
getAllTagsFromIndex,
|
|
3820
|
-
archivePlan,
|
|
3821
|
-
unarchivePlan,
|
|
3822
|
-
answerInputRequest,
|
|
3823
|
-
answerMultiQuestionInputRequest,
|
|
3824
|
-
cancelInputRequest,
|
|
3825
|
-
declineInputRequest,
|
|
3826
5595
|
atomicRegenerateTokenIfOwner,
|
|
3827
|
-
isUrlEncodedPlanV1,
|
|
3828
|
-
isUrlEncodedPlanV2,
|
|
3829
|
-
encodePlan,
|
|
3830
|
-
decodePlan,
|
|
3831
|
-
createPlanUrl,
|
|
3832
5596
|
createPlanUrlWithHistory,
|
|
3833
|
-
getPlanFromUrl,
|
|
3834
|
-
A2ATextPartSchema,
|
|
3835
|
-
A2ADataPartSchema,
|
|
3836
|
-
A2AFilePartSchema,
|
|
3837
|
-
A2APartSchema,
|
|
3838
|
-
A2AMessageSchema,
|
|
3839
|
-
ConversationExportMetaSchema,
|
|
3840
|
-
ClaudeCodeMessageSchema,
|
|
3841
|
-
parseClaudeCodeTranscriptString,
|
|
3842
|
-
claudeCodeToA2A,
|
|
3843
|
-
validateA2AMessages,
|
|
3844
|
-
summarizeA2AConversation,
|
|
3845
|
-
a2aToClaudeCode,
|
|
3846
|
-
formatAsClaudeCodeJSONL,
|
|
3847
5597
|
formatDeliverablesForLLM,
|
|
3848
5598
|
extractDeliverables,
|
|
3849
|
-
hashLineContent,
|
|
3850
|
-
computeCommentStaleness,
|
|
3851
|
-
isLineContentStale,
|
|
3852
|
-
withStalenessInfo,
|
|
3853
|
-
withStalenessInfoBatch,
|
|
3854
|
-
buildLineContentMap,
|
|
3855
|
-
formatStalenessMarker,
|
|
3856
5599
|
formatDiffCommentsForLLM,
|
|
3857
|
-
formatPRCommentsForLLM,
|
|
3858
|
-
getPRCommentsSummary,
|
|
3859
|
-
EnvironmentContextSchema,
|
|
3860
5600
|
GitHubPRResponseSchema,
|
|
3861
|
-
asPlanId,
|
|
3862
|
-
asAwarenessClientId,
|
|
3863
|
-
asWebRTCPeerId,
|
|
3864
|
-
asGitHubUsername,
|
|
3865
|
-
ROUTES,
|
|
3866
5601
|
createPlanWebUrl,
|
|
3867
|
-
InviteTokenSchema,
|
|
3868
|
-
InviteRedemptionSchema,
|
|
3869
|
-
parseInviteFromUrl,
|
|
3870
|
-
buildInviteUrl,
|
|
3871
|
-
getTokenTimeRemaining,
|
|
3872
|
-
GitFileStatusSchema,
|
|
3873
|
-
LocalFileChangeSchema,
|
|
3874
|
-
LocalChangesResponseSchema,
|
|
3875
|
-
LocalChangesUnavailableReasonSchema,
|
|
3876
|
-
LocalChangesUnavailableSchema,
|
|
3877
|
-
LocalChangesResultSchema,
|
|
3878
|
-
P2PMessageType,
|
|
3879
|
-
ConversationExportStartMetaSchema,
|
|
3880
|
-
ChunkMessageSchema,
|
|
3881
|
-
ConversationExportEndSchema,
|
|
3882
|
-
isConversationExportStart,
|
|
3883
|
-
isConversationChunk,
|
|
3884
|
-
isConversationExportEnd,
|
|
3885
|
-
isP2PConversationMessage,
|
|
3886
|
-
encodeExportStartMessage,
|
|
3887
|
-
decodeExportStartMessage,
|
|
3888
|
-
encodeChunkMessage,
|
|
3889
|
-
decodeChunkMessage,
|
|
3890
|
-
encodeExportEndMessage,
|
|
3891
|
-
decodeExportEndMessage,
|
|
3892
|
-
decodeP2PMessage,
|
|
3893
|
-
assertNeverP2PMessage,
|
|
3894
5602
|
PLAN_INDEX_DOC_NAME,
|
|
3895
|
-
PLAN_INDEX_VIEWED_BY_KEY,
|
|
3896
|
-
NON_PLAN_DB_NAMES,
|
|
3897
|
-
PlanIndexEntrySchema,
|
|
3898
|
-
getPlanIndex,
|
|
3899
|
-
getPlanIndexEntry,
|
|
3900
5603
|
setPlanIndexEntry,
|
|
3901
|
-
removePlanIndexEntry,
|
|
3902
5604
|
touchPlanIndexEntry,
|
|
3903
|
-
getViewedByFromIndex,
|
|
3904
|
-
updatePlanIndexViewedBy,
|
|
3905
|
-
clearPlanIndexViewedBy,
|
|
3906
|
-
getAllViewedByFromIndex,
|
|
3907
|
-
removeViewedByFromIndex,
|
|
3908
|
-
PLAN_INDEX_EVENT_VIEWED_BY_KEY,
|
|
3909
|
-
markEventAsViewed,
|
|
3910
|
-
clearEventViewedBy,
|
|
3911
|
-
isEventUnread,
|
|
3912
|
-
getAllEventViewedByForPlan,
|
|
3913
|
-
formatThreadsForLLM,
|
|
3914
5605
|
TOOL_NAMES,
|
|
3915
|
-
PlanIdSchema,
|
|
3916
|
-
PlanStatusResponseSchema,
|
|
3917
|
-
HasConnectionsResponseSchema,
|
|
3918
|
-
SubscriptionClientIdSchema,
|
|
3919
|
-
ChangeSchema,
|
|
3920
|
-
ChangesResponseSchema,
|
|
3921
|
-
DeleteSubscriptionResponseSchema,
|
|
3922
|
-
SetSessionTokenRequestSchema,
|
|
3923
|
-
SetSessionTokenResponseSchema,
|
|
3924
|
-
ImportConversationRequestSchema,
|
|
3925
|
-
ImportConversationResponseSchema,
|
|
3926
|
-
conversationRouter,
|
|
3927
|
-
hookRouter,
|
|
3928
|
-
planRouter,
|
|
3929
|
-
subscriptionRouter,
|
|
3930
|
-
appRouter,
|
|
3931
|
-
isErrnoException,
|
|
3932
5606
|
hasErrorCode,
|
|
3933
|
-
isBuffer,
|
|
3934
5607
|
createUserResolver,
|
|
3935
|
-
|
|
3936
|
-
|
|
3937
|
-
|
|
5608
|
+
registryConfig,
|
|
5609
|
+
getLocalChanges,
|
|
5610
|
+
parseRepoString,
|
|
5611
|
+
isArtifactsEnabled,
|
|
5612
|
+
GitHubAuthError,
|
|
5613
|
+
getOctokit,
|
|
5614
|
+
isGitHubConfigured,
|
|
5615
|
+
uploadArtifact,
|
|
5616
|
+
generateSessionToken,
|
|
5617
|
+
hashSessionToken,
|
|
5618
|
+
webConfig,
|
|
5619
|
+
getRepositoryFullName,
|
|
5620
|
+
getGitHubUsername,
|
|
5621
|
+
getVerifiedGitHubUsername,
|
|
5622
|
+
tryAcquireHubLock,
|
|
5623
|
+
releaseHubLock,
|
|
5624
|
+
startRegistryServer,
|
|
5625
|
+
isRegistryRunning,
|
|
5626
|
+
initAsHub,
|
|
5627
|
+
initAsClient,
|
|
5628
|
+
getOrCreateDoc3 as getOrCreateDoc,
|
|
5629
|
+
hasActiveConnections3 as hasActiveConnections
|
|
3938
5630
|
};
|