@schoolai/shipyard 3.2.2 → 3.2.3-rc.20260422.1

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.
Files changed (31) hide show
  1. package/dist/{auth-5RJ4YKPC.js → auth-LS3NBD42.js} +3 -3
  2. package/dist/{chunk-KWRAP2UK.js → chunk-2FZJ7TW3.js} +5 -5
  3. package/dist/{chunk-NZFMJMJN.js → chunk-67VJDX7G.js} +3 -3
  4. package/dist/{chunk-N4ZTO3K3.js → chunk-AWHTFJIM.js} +2 -2
  5. package/dist/{chunk-N4ZTO3K3.js.map → chunk-AWHTFJIM.js.map} +1 -1
  6. package/dist/{chunk-HF5GUPSU.js → chunk-CCW5QAUH.js} +2 -2
  7. package/dist/{chunk-DB5R6SKT.js → chunk-DNIC3FOH.js} +2 -2
  8. package/dist/{chunk-5RH5JP3R.js → chunk-GLH3V7NG.js} +2 -2
  9. package/dist/{chunk-KUV4J6NI.js → chunk-M5M6VC5F.js} +1 -2
  10. package/dist/{chunk-KUV4J6NI.js.map → chunk-M5M6VC5F.js.map} +1 -1
  11. package/dist/index.js +8 -8
  12. package/dist/login-MW5MFGXA.js +19 -0
  13. package/dist/{logout-432UPTCL.js → logout-M7F7HXUU.js} +5 -5
  14. package/dist/{mcp-servers-YT5BADYE.js → mcp-servers-MUVTAMDT.js} +4 -4
  15. package/dist/{roi-MVOOANFC.js → roi-ZCVNBSTO.js} +3 -3
  16. package/dist/{serve-O4AHOTL4.js → serve-KNOYKFPQ.js} +232 -100
  17. package/dist/{serve-O4AHOTL4.js.map → serve-KNOYKFPQ.js.map} +1 -1
  18. package/dist/{start-G2VIRDQI.js → start-FONQGWYX.js} +8 -8
  19. package/package.json +3 -3
  20. package/dist/login-ZPYQLQ52.js +0 -19
  21. /package/dist/{auth-5RJ4YKPC.js.map → auth-LS3NBD42.js.map} +0 -0
  22. /package/dist/{chunk-KWRAP2UK.js.map → chunk-2FZJ7TW3.js.map} +0 -0
  23. /package/dist/{chunk-NZFMJMJN.js.map → chunk-67VJDX7G.js.map} +0 -0
  24. /package/dist/{chunk-HF5GUPSU.js.map → chunk-CCW5QAUH.js.map} +0 -0
  25. /package/dist/{chunk-DB5R6SKT.js.map → chunk-DNIC3FOH.js.map} +0 -0
  26. /package/dist/{chunk-5RH5JP3R.js.map → chunk-GLH3V7NG.js.map} +0 -0
  27. /package/dist/{login-ZPYQLQ52.js.map → login-MW5MFGXA.js.map} +0 -0
  28. /package/dist/{logout-432UPTCL.js.map → logout-M7F7HXUU.js.map} +0 -0
  29. /package/dist/{mcp-servers-YT5BADYE.js.map → mcp-servers-MUVTAMDT.js.map} +0 -0
  30. /package/dist/{roi-MVOOANFC.js.map → roi-ZCVNBSTO.js.map} +0 -0
  31. /package/dist/{start-G2VIRDQI.js.map → start-FONQGWYX.js.map} +0 -0
@@ -47,11 +47,11 @@ import {
47
47
  VaultKeyPutRequestSchema,
48
48
  VaultKeyPutResponseSchema,
49
49
  classifyClaudeCodeCompatibility
50
- } from "./chunk-N4ZTO3K3.js";
50
+ } from "./chunk-AWHTFJIM.js";
51
51
  import "./chunk-EHQITHQX.js";
52
52
  import {
53
53
  loadAuthToken
54
- } from "./chunk-5RH5JP3R.js";
54
+ } from "./chunk-GLH3V7NG.js";
55
55
  import {
56
56
  DiscoveryStateSchema,
57
57
  detectMCPServers,
@@ -62,19 +62,19 @@ import {
62
62
  redactEnv,
63
63
  resolveEnabledMcpServers,
64
64
  resolveStdioEnv
65
- } from "./chunk-NZFMJMJN.js";
65
+ } from "./chunk-67VJDX7G.js";
66
66
  import {
67
67
  createChildLogger,
68
68
  flushLogger,
69
69
  logger
70
- } from "./chunk-DB5R6SKT.js";
70
+ } from "./chunk-DNIC3FOH.js";
71
71
  import {
72
72
  external_exports,
73
73
  getShipyardHome,
74
74
  isVanillaAgentMode,
75
75
  toJSONSchema,
76
76
  validateEnv
77
- } from "./chunk-KUV4J6NI.js";
77
+ } from "./chunk-M5M6VC5F.js";
78
78
  import {
79
79
  detectSkills
80
80
  } from "./chunk-DPMRSLYJ.js";
@@ -83,8 +83,8 @@ import {
83
83
  } from "./chunk-2H7UOFLK.js";
84
84
 
85
85
  // src/services/serve.ts
86
- import { mkdir as mkdir25, realpath as realpath2 } from "fs/promises";
87
- import { join as join56 } from "path";
86
+ import { mkdir as mkdir25, realpath as realpath3 } from "fs/promises";
87
+ import { join as join57 } from "path";
88
88
  import { query as query2 } from "@anthropic-ai/claude-agent-sdk";
89
89
 
90
90
  // ../../node_modules/.pnpm/@loro-extended+change@6.0.0-beta.0_loro-crdt@1.11.1/node_modules/@loro-extended/change/dist/index.js
@@ -27270,7 +27270,8 @@ var PlanDetectionSchema = external_exports.object({
27270
27270
  filePath: external_exports.string(),
27271
27271
  planDocId: external_exports.string(),
27272
27272
  allowedPrompts: external_exports.array(AllowedPromptSchema).optional(),
27273
- originalMarkdown: external_exports.string().optional()
27273
+ originalMarkdown: external_exports.string().optional(),
27274
+ approved: external_exports.boolean().optional()
27274
27275
  });
27275
27276
  var TaskRecordSchema = external_exports.object({
27276
27277
  taskId: external_exports.string(),
@@ -28220,7 +28221,8 @@ var DaemonToBrowserControlMessageSchema = external_exports.discriminatedUnion("t
28220
28221
  tool: external_exports.string(),
28221
28222
  prompt: external_exports.string()
28222
28223
  })).optional(),
28223
- isRevision: external_exports.boolean().optional()
28224
+ isRevision: external_exports.boolean().optional(),
28225
+ approved: external_exports.boolean().optional()
28224
28226
  }),
28225
28227
  external_exports.object({
28226
28228
  type: external_exports.literal("plan_continuation_timeout"),
@@ -28633,7 +28635,12 @@ var HunkRangeSchema = external_exports.object({
28633
28635
  modifiedEnd: external_exports.number()
28634
28636
  });
28635
28637
  var BrowserToFileIOMessageSchema = external_exports.discriminatedUnion("type", [
28636
- external_exports.object({ type: external_exports.literal("read_file"), requestId: external_exports.string(), path: external_exports.string() }),
28638
+ external_exports.object({
28639
+ type: external_exports.literal("read_file"),
28640
+ requestId: external_exports.string(),
28641
+ path: external_exports.string(),
28642
+ external: external_exports.boolean().optional()
28643
+ }),
28637
28644
  external_exports.object({
28638
28645
  type: external_exports.literal("write_file"),
28639
28646
  requestId: external_exports.string(),
@@ -28641,7 +28648,12 @@ var BrowserToFileIOMessageSchema = external_exports.discriminatedUnion("type", [
28641
28648
  content: external_exports.string()
28642
28649
  }),
28643
28650
  external_exports.object({ type: external_exports.literal("readdir"), requestId: external_exports.string(), path: external_exports.string() }),
28644
- external_exports.object({ type: external_exports.literal("stat"), requestId: external_exports.string(), path: external_exports.string() }),
28651
+ external_exports.object({
28652
+ type: external_exports.literal("stat"),
28653
+ requestId: external_exports.string(),
28654
+ path: external_exports.string(),
28655
+ external: external_exports.boolean().optional()
28656
+ }),
28645
28657
  external_exports.object({ type: external_exports.literal("git_status"), requestId: external_exports.string() }),
28646
28658
  external_exports.object({
28647
28659
  type: external_exports.literal("git_diff_file"),
@@ -31690,7 +31702,7 @@ function nanoid(size2 = 21) {
31690
31702
  }
31691
31703
 
31692
31704
  // src/services/bootstrap/signaling.ts
31693
- var DAEMON_NPM_VERSION = true ? "3.2.2" : "unknown";
31705
+ var DAEMON_NPM_VERSION = true ? "3.2.3" : "unknown";
31694
31706
  function createDaemonSignaling(config2) {
31695
31707
  const agentId = config2.agentId ?? nanoid();
31696
31708
  function send(msg) {
@@ -34005,7 +34017,6 @@ async function deleteAdvertisement(shipyardHome) {
34005
34017
 
34006
34018
  // src/services/local-direct/local-direct-wiring.ts
34007
34019
  async function setupLocalDirect(deps) {
34008
- if (!deps.env.SHIPYARD_LOCAL_DIRECT) return null;
34009
34020
  const token = generateLocalDirectToken();
34010
34021
  const server = await createLocalDirectServer({
34011
34022
  token,
@@ -66609,22 +66620,22 @@ function isTextSelectionAcrossCells({ $from, $to }) {
66609
66620
  function normalizeSelection(state, tr2, allowTableNodeSelection) {
66610
66621
  const sel = (tr2 || state).selection;
66611
66622
  const doc3 = (tr2 || state).doc;
66612
- let normalize7;
66623
+ let normalize8;
66613
66624
  let role;
66614
66625
  if (sel instanceof NodeSelection && (role = sel.node.type.spec.tableRole)) {
66615
- if (role == "cell" || role == "header_cell") normalize7 = CellSelection.create(doc3, sel.from);
66626
+ if (role == "cell" || role == "header_cell") normalize8 = CellSelection.create(doc3, sel.from);
66616
66627
  else if (role == "row") {
66617
66628
  const $cell = doc3.resolve(sel.from + 1);
66618
- normalize7 = CellSelection.rowSelection($cell, $cell);
66629
+ normalize8 = CellSelection.rowSelection($cell, $cell);
66619
66630
  } else if (!allowTableNodeSelection) {
66620
66631
  const map3 = TableMap.get(sel.node);
66621
66632
  const start = sel.from + 1;
66622
66633
  const lastCell = start + map3.map[map3.width * map3.height - 1];
66623
- normalize7 = CellSelection.create(doc3, start + 1, lastCell);
66634
+ normalize8 = CellSelection.create(doc3, start + 1, lastCell);
66624
66635
  }
66625
- } else if (sel instanceof TextSelection && isCellBoundarySelection(sel)) normalize7 = TextSelection.create(doc3, sel.from);
66626
- else if (sel instanceof TextSelection && isTextSelectionAcrossCells(sel)) normalize7 = TextSelection.create(doc3, sel.$from.start(), sel.$from.end());
66627
- if (normalize7) (tr2 || (tr2 = state.tr)).setSelection(normalize7);
66636
+ } else if (sel instanceof TextSelection && isCellBoundarySelection(sel)) normalize8 = TextSelection.create(doc3, sel.from);
66637
+ else if (sel instanceof TextSelection && isTextSelectionAcrossCells(sel)) normalize8 = TextSelection.create(doc3, sel.$from.start(), sel.$from.end());
66638
+ if (normalize8) (tr2 || (tr2 = state.tr)).setSelection(normalize8);
66628
66639
  return tr2;
66629
66640
  }
66630
66641
  var fixTablesKey = new PluginKey("fix-tables");
@@ -84572,16 +84583,18 @@ async function buildRateLimitStore(dataDir, opts) {
84572
84583
  }
84573
84584
  };
84574
84585
  }
84586
+ function deriveWindowFromEvent(incoming, eventTime) {
84587
+ if (!isWindowKey(incoming.rateLimitType)) return void 0;
84588
+ const utilization = typeof incoming.utilization === "number" ? incoming.utilization : incoming.status === "rejected" ? 1 : void 0;
84589
+ if (utilization === void 0) return void 0;
84590
+ return { utilization, resetsAt: incoming.resetsAt, updatedAt: eventTime };
84591
+ }
84575
84592
  function mergeEvent(prev, incoming, eventTime) {
84576
84593
  const prevByWindow = prev?.byWindow ?? {};
84577
84594
  const byWindow = { ...prevByWindow };
84578
- if (isWindowKey(incoming.rateLimitType) && typeof incoming.utilization === "number") {
84579
- const window2 = {
84580
- utilization: incoming.utilization,
84581
- resetsAt: incoming.resetsAt,
84582
- updatedAt: eventTime
84583
- };
84584
- byWindow[incoming.rateLimitType] = window2;
84595
+ const derived = deriveWindowFromEvent(incoming, eventTime);
84596
+ if (derived && isWindowKey(incoming.rateLimitType)) {
84597
+ byWindow[incoming.rateLimitType] = derived;
84585
84598
  }
84586
84599
  const nowInOverage = incoming.isUsingOverage === true;
84587
84600
  const wasInOverage = prev?.overageStartedAt !== void 0;
@@ -84635,12 +84648,15 @@ function migrateV1toV2(v1) {
84635
84648
  for (const [accountKey, record] of Object.entries(v1.records)) {
84636
84649
  const { info, updatedAt } = record;
84637
84650
  const byWindow = {};
84638
- if (isWindowKey(info.rateLimitType) && typeof info.utilization === "number") {
84639
- byWindow[info.rateLimitType] = {
84640
- utilization: info.utilization,
84641
- resetsAt: info.resetsAt,
84642
- updatedAt
84643
- };
84651
+ if (isWindowKey(info.rateLimitType)) {
84652
+ const utilization = typeof info.utilization === "number" ? info.utilization : info.status === "rejected" ? 1 : void 0;
84653
+ if (utilization !== void 0) {
84654
+ byWindow[info.rateLimitType] = {
84655
+ utilization,
84656
+ resetsAt: info.resetsAt,
84657
+ updatedAt
84658
+ };
84659
+ }
84644
84660
  }
84645
84661
  const nowInOverage = info.isUsingOverage === true;
84646
84662
  records[accountKey] = {
@@ -88292,14 +88308,17 @@ var PlanHandler = class {
88292
88308
  });
88293
88309
  if (this.#approvalReceived) {
88294
88310
  this.#approvalReceived = false;
88295
- this.#persistDetection(null).catch((err) => {
88296
- this.#deps.log({
88297
- event: "plan_persist_clear_failed",
88298
- taskId: this.#deps.taskId,
88299
- toolUseId,
88300
- error: err instanceof Error ? err.message : String(err)
88311
+ const previous = this.#lastPlanDetection;
88312
+ if (previous) {
88313
+ this.#persistDetection({ ...previous, approved: true }).catch((err) => {
88314
+ this.#deps.log({
88315
+ event: "plan_persist_approve_failed",
88316
+ taskId: this.#deps.taskId,
88317
+ toolUseId,
88318
+ error: err instanceof Error ? err.message : String(err)
88319
+ });
88301
88320
  });
88302
- });
88321
+ }
88303
88322
  this.#deps.log({
88304
88323
  event: "exit_plan_mode_approved",
88305
88324
  taskId: this.#deps.taskId,
@@ -88402,7 +88421,7 @@ var PlanHandler = class {
88402
88421
  */
88403
88422
  replayPlanDetection(send) {
88404
88423
  if (!this.#lastPlanDetection) return;
88405
- const { toolUseId, filePath, planDocId, allowedPrompts } = this.#lastPlanDetection;
88424
+ const { toolUseId, filePath, planDocId, allowedPrompts, approved } = this.#lastPlanDetection;
88406
88425
  const markdown = this.#deps.planRepo.getContent(this.#deps.taskId);
88407
88426
  send({
88408
88427
  type: "plan_detected",
@@ -88411,7 +88430,8 @@ var PlanHandler = class {
88411
88430
  filePath,
88412
88431
  planDocId,
88413
88432
  markdown,
88414
- allowedPrompts
88433
+ allowedPrompts,
88434
+ approved
88415
88435
  });
88416
88436
  }
88417
88437
  /**
@@ -91117,11 +91137,24 @@ var StructuredTaskTracker = class {
91117
91137
  #ccTaskWatcherDispose = null;
91118
91138
  #suppressWriteThrough = false;
91119
91139
  #ccTaskFileWriter;
91120
- #restoreInProgress = false;
91140
+ /**
91141
+ * Gate the first flush until an overlay has either been applied or the
91142
+ * caller has explicitly signaled that none is coming. Both flags below
91143
+ * must be false for the flush to proceed during restore.
91144
+ */
91145
+ #restoreNeedsOverlay = false;
91146
+ /**
91147
+ * Gate the first flush until the file watcher's initial disk reconcile
91148
+ * has fired. Only set during restore for tasks with a session id.
91149
+ */
91150
+ #restoreNeedsDisk = false;
91121
91151
  constructor(deps) {
91122
91152
  this.#deps = deps;
91123
91153
  this.#ccTaskFileWriter = createCCTaskFileWriter(null, deps.log);
91124
- this.#restoreInProgress = deps.restoreInProgress ?? false;
91154
+ if (deps.restoreInProgress) {
91155
+ this.#restoreNeedsOverlay = true;
91156
+ this.#restoreNeedsDisk = deps.restoreExpectDisk ?? false;
91157
+ }
91125
91158
  }
91126
91159
  get currentOverlay() {
91127
91160
  return this.#currentOverlay;
@@ -91165,24 +91198,37 @@ var StructuredTaskTracker = class {
91165
91198
  }
91166
91199
  /**
91167
91200
  * Apply an overlay on top of CC tasks. Stores the overlay and re-flushes.
91168
- * Exits restore mode applyOverlay is the terminal piece of restoration
91169
- * (disk reconcile is idempotent with the seeded overlay, so we flush once
91170
- * here with full inputs instead of producing a partial pre-overlay flush).
91201
+ * Clears the overlay-pending gate. The flush still waits if a disk reconcile
91202
+ * is expected (restoreExpectDisk=true at construction): without that second
91203
+ * gate, the applyOverlay-first restart ordering would emit an overlay-only
91204
+ * map to the store on every boot, bumping TaskRecord.lastActivityAt for
91205
+ * every restored task that has an overlay.
91171
91206
  */
91172
91207
  applyOverlay(overlay) {
91173
91208
  this.#currentOverlay = overlay;
91174
- this.#restoreInProgress = false;
91209
+ this.#restoreNeedsOverlay = false;
91210
+ this.#flushStructuredTasks();
91211
+ }
91212
+ /**
91213
+ * Signal that no overlay will be applied during this restore. Clears the
91214
+ * overlay-pending gate but NOT the disk gate — if the restored task has a
91215
+ * session id, the file watcher's first reconcile remains the terminal
91216
+ * signal before the store receives a flush.
91217
+ */
91218
+ markNoOverlay() {
91219
+ if (!this.#restoreNeedsOverlay) return;
91220
+ this.#restoreNeedsOverlay = false;
91175
91221
  this.#flushStructuredTasks();
91176
91222
  }
91177
91223
  /**
91178
- * Signal that restoration has finished for tasks that have no persisted
91179
- * overlay to apply. Unblocks #flushStructuredTasks and runs one flush with
91180
- * the current state (disk-only, no overlay). Must be called by the
91181
- * restoration caller whenever applyOverlay will not be invoked.
91224
+ * Clear BOTH restore gates. Used in error paths (hydration promise rejected)
91225
+ * and by restorers that know neither an overlay nor a disk reconcile will
91226
+ * arrive. If either signal fires after this, the natural flush handles it.
91182
91227
  */
91183
91228
  markRestoreComplete() {
91184
- if (!this.#restoreInProgress) return;
91185
- this.#restoreInProgress = false;
91229
+ if (!this.#restoreNeedsOverlay && !this.#restoreNeedsDisk) return;
91230
+ this.#restoreNeedsOverlay = false;
91231
+ this.#restoreNeedsDisk = false;
91186
91232
  this.#flushStructuredTasks();
91187
91233
  }
91188
91234
  processStructuredTaskEvents(content) {
@@ -91292,7 +91338,7 @@ var StructuredTaskTracker = class {
91292
91338
  * 7. Push to updateStructuredTasks
91293
91339
  */
91294
91340
  #flushStructuredTasks() {
91295
- if (this.#restoreInProgress) return;
91341
+ if (this.#restoreNeedsOverlay || this.#restoreNeedsDisk) return;
91296
91342
  const overlay = this.#currentOverlay ?? DEFAULT_TASK_OVERLAY;
91297
91343
  const merged = applyOverlayToMap(this.#structuredTasks, overlay);
91298
91344
  const todoProgress = computeTodoProgress(merged);
@@ -91314,6 +91360,7 @@ var StructuredTaskTracker = class {
91314
91360
  * - Re-flush with overlay applied
91315
91361
  */
91316
91362
  #reconcileFromDisk(ccTasks) {
91363
+ this.#restoreNeedsDisk = false;
91317
91364
  const currentDiskIds = /* @__PURE__ */ new Set();
91318
91365
  for (const file of ccTasks) {
91319
91366
  currentDiskIds.add(file.id);
@@ -92497,7 +92544,12 @@ var Task = class {
92497
92544
  taskId: deps.taskId,
92498
92545
  log: deps.log,
92499
92546
  updateStructuredTasks: deps.updateStructuredTasks,
92500
- restoreInProgress: deps.restoreInProgress ?? false
92547
+ restoreInProgress: deps.restoreInProgress ?? false,
92548
+ /**
92549
+ * Only wait for a disk reconcile if a CC session exists — else the file
92550
+ * watcher never attaches and the gate would never clear.
92551
+ */
92552
+ restoreExpectDisk: Boolean(deps.restoreInProgress && deps.existingSessionId)
92501
92553
  });
92502
92554
  this.#pushManager = new ResourcePushManager({
92503
92555
  taskId: deps.taskId,
@@ -94192,6 +94244,10 @@ Use this context to maintain continuity. You have already done this work \u2014
94192
94244
  markStructuredTaskRestoreComplete() {
94193
94245
  this.#structuredTaskTracker.markRestoreComplete();
94194
94246
  }
94247
+ /** See StructuredTaskTracker.markNoOverlay. */
94248
+ markStructuredTaskNoOverlay() {
94249
+ this.#structuredTaskTracker.markNoOverlay();
94250
+ }
94195
94251
  /** ---------------------------------------------------------------- */
94196
94252
  /** Resource resolution */
94197
94253
  /** ---------------------------------------------------------------- */
@@ -95132,11 +95188,14 @@ var TaskManager = class {
95132
95188
  const cwd = opts.cwd;
95133
95189
  const mode = opts.mode ?? "task";
95134
95190
  let orchestratorRef = null;
95191
+ const hasSession = Boolean(opts.existingSessionId);
95135
95192
  const restoreHydration = async () => {
95136
95193
  const record = await this.#deps.taskStateStore.getTask(taskId);
95137
95194
  if (!orchestratorRef || !this.#tasks.has(taskId)) return;
95138
95195
  if (record?.taskOverlay) {
95139
95196
  orchestratorRef.applyOverlay(record.taskOverlay);
95197
+ } else if (hasSession) {
95198
+ orchestratorRef.markStructuredTaskNoOverlay();
95140
95199
  } else {
95141
95200
  orchestratorRef.markStructuredTaskRestoreComplete();
95142
95201
  }
@@ -99787,7 +99846,7 @@ function friendlyExecError(err) {
99787
99846
  async function refreshPluginCapabilities(daemon, tokenStore) {
99788
99847
  const [updatedMarketplace, updatedMcp, updatedSkills] = await Promise.all([
99789
99848
  detectMarketplacePlugins(),
99790
- import("./mcp-servers-YT5BADYE.js").then(
99849
+ import("./mcp-servers-MUVTAMDT.js").then(
99791
99850
  (m2) => m2.detectMCPServers(daemon.capabilities.environments, tokenStore)
99792
99851
  ),
99793
99852
  import("./skills-NCKYNLUS.js").then(
@@ -102046,10 +102105,53 @@ function createCollabRoomManager(deps) {
102046
102105
 
102047
102106
  // src/services/file-io-handler.ts
102048
102107
  import { execFile as execFile10, spawn as spawn6 } from "child_process";
102049
- import { readdir as readdir14, readFile as readFile34, realpath, stat as stat10, unlink as unlink10, writeFile as writeFile28 } from "fs/promises";
102050
- import { normalize as normalize6, relative as relative3, resolve as resolve2 } from "path";
102108
+ import { readdir as readdir14, readFile as readFile34, realpath as realpath2, stat as stat10, unlink as unlink10, writeFile as writeFile28 } from "fs/promises";
102109
+ import { normalize as normalize7, relative as relative3, resolve as resolve2 } from "path";
102051
102110
  import { promisify as promisify7 } from "util";
102052
102111
 
102112
+ // src/shared/file-io-path-safety.ts
102113
+ import { realpath } from "fs/promises";
102114
+ import { basename as basename5, dirname as dirname19, join as join52, normalize as normalize6 } from "path";
102115
+ async function safeAbsolutePath(userPath, isHidden2, allowedHiddenNames) {
102116
+ const normalized = prepareAbsolutePath(userPath, isHidden2, allowedHiddenNames);
102117
+ if (normalized === null) return null;
102118
+ const canonical = await canonicalizeOrRecombine(normalized);
102119
+ if (canonical === null) return null;
102120
+ if (!checkSegmentsAllowed(canonical, isHidden2, allowedHiddenNames)) return null;
102121
+ return canonical;
102122
+ }
102123
+ function prepareAbsolutePath(userPath, isHidden2, allowedHiddenNames) {
102124
+ if (!userPath.startsWith("/")) return null;
102125
+ const normalized = normalize6(userPath);
102126
+ if (normalized.startsWith("..") || normalized.includes("/..") || normalized.includes("\\..")) {
102127
+ return null;
102128
+ }
102129
+ if (!checkSegmentsAllowed(normalized, isHidden2, allowedHiddenNames)) return null;
102130
+ return normalized;
102131
+ }
102132
+ async function canonicalizeOrRecombine(path2) {
102133
+ try {
102134
+ return await realpath(path2);
102135
+ } catch (err) {
102136
+ if (!isEnoent2(err)) return null;
102137
+ const parentCanonical = await realpath(dirname19(path2)).catch(() => null);
102138
+ if (parentCanonical === null) return null;
102139
+ return join52(parentCanonical, basename5(path2));
102140
+ }
102141
+ }
102142
+ function isEnoent2(err) {
102143
+ return typeof err === "object" && err !== null && "code" in err && err.code === "ENOENT";
102144
+ }
102145
+ function checkSegmentsAllowed(path2, isHidden2, allowedHiddenNames) {
102146
+ const segments = path2.split(/[/\\]/).filter((s2) => s2.length > 0 && s2 !== ".");
102147
+ for (const seg of segments) {
102148
+ if (!isHidden2(seg)) continue;
102149
+ if (allowedHiddenNames?.has(seg)) continue;
102150
+ return false;
102151
+ }
102152
+ return true;
102153
+ }
102154
+
102053
102155
  // src/services/channels/handlers/snapshot-diff-handler.ts
102054
102156
  function planSnapshotDiff(input) {
102055
102157
  const { fromRef, toRef, fromRefExists, toRefExists, autoFetch, parentIsMerged } = input;
@@ -102280,7 +102382,7 @@ function handleFileIOChannel(initialCwd, send, log, deps) {
102280
102382
  return safePathWithOverrides(userPath, /* @__PURE__ */ new Set(["node_modules"]));
102281
102383
  }
102282
102384
  function safePathWithOverrides(userPath, allowedHiddenNames) {
102283
- const normalized = normalize6(userPath);
102385
+ const normalized = normalize7(userPath);
102284
102386
  if (normalized.startsWith("..") || normalized.includes("/..") || normalized.includes("\\..")) {
102285
102387
  return null;
102286
102388
  }
@@ -102295,6 +102397,9 @@ function handleFileIOChannel(initialCwd, send, log, deps) {
102295
102397
  if (rel.startsWith("..") || rel.startsWith("/")) return null;
102296
102398
  return abs;
102297
102399
  }
102400
+ function safeAbsolutePath2(userPath, allowedHiddenNames) {
102401
+ return safeAbsolutePath(userPath, isHidden, allowedHiddenNames);
102402
+ }
102298
102403
  function onMessage(data) {
102299
102404
  let parsed;
102300
102405
  try {
@@ -102347,26 +102452,38 @@ function handleFileIOChannel(initialCwd, send, log, deps) {
102347
102452
  assertNever3(msg);
102348
102453
  }
102349
102454
  }
102350
- function dispatchFileOp(msg) {
102455
+ async function dispatchFileOp(msg) {
102351
102456
  const isReadOnlyOp = msg.type === "read_file" || msg.type === "stat";
102352
- const abs = isReadOnlyOp ? safePathForRead(msg.path) : safePath(msg.path);
102457
+ const isExternal = isReadOnlyOp && msg.external === true;
102458
+ if (isExternal && !deps.allowExternalReads) {
102459
+ log({ event: "file_io_external_denied", path: msg.path });
102460
+ respondError(msg.requestId, `external_not_allowed:${msg.path}`);
102461
+ return;
102462
+ }
102463
+ let abs;
102464
+ if (isExternal) {
102465
+ abs = await safeAbsolutePath2(msg.path, /* @__PURE__ */ new Set(["node_modules"]));
102466
+ } else {
102467
+ abs = isReadOnlyOp ? safePathForRead(msg.path) : safePath(msg.path);
102468
+ }
102353
102469
  if (!abs) {
102354
- log({ event: "file_io_path_rejected", path: msg.path });
102355
- respondError(msg.requestId, `hidden_path:${msg.path}`);
102470
+ log({ event: "file_io_path_rejected", path: msg.path, external: isExternal });
102471
+ const code2 = isExternal ? "external_blocked" : "hidden_path";
102472
+ respondError(msg.requestId, `${code2}:${msg.path}`);
102356
102473
  return;
102357
102474
  }
102358
102475
  switch (msg.type) {
102359
102476
  case "read_file":
102360
- handleReadFile(msg.requestId, abs);
102477
+ await handleReadFile(msg.requestId, abs);
102361
102478
  break;
102362
102479
  case "write_file":
102363
- handleWriteFile(msg.requestId, abs, msg.content);
102480
+ await handleWriteFile(msg.requestId, abs, msg.content);
102364
102481
  break;
102365
102482
  case "readdir":
102366
- handleReaddir(msg.requestId, abs);
102483
+ await handleReaddir(msg.requestId, abs);
102367
102484
  break;
102368
102485
  case "stat":
102369
- handleStat(msg.requestId, abs);
102486
+ await handleStat(msg.requestId, abs);
102370
102487
  break;
102371
102488
  default:
102372
102489
  assertNever3(msg);
@@ -102389,7 +102506,7 @@ function handleFileIOChannel(initialCwd, send, log, deps) {
102389
102506
  case "write_file":
102390
102507
  case "readdir":
102391
102508
  case "stat":
102392
- dispatchFileOp(msg);
102509
+ void dispatchFileOp(msg);
102393
102510
  break;
102394
102511
  case "set_cwd":
102395
102512
  handleSetCwd(msg.requestId, msg.path);
@@ -102505,7 +102622,7 @@ function handleFileIOChannel(initialCwd, send, log, deps) {
102505
102622
  async function handleSetCwd(requestId, newPath) {
102506
102623
  try {
102507
102624
  const resolved = resolve2(newPath);
102508
- const canonical = await realpath(resolved);
102625
+ const canonical = await realpath2(resolved);
102509
102626
  if (!deps.isAllowedCwd(canonical)) {
102510
102627
  respondError(requestId, "cwd not allowed");
102511
102628
  log({ event: "file_io_cwd_rejected", cwd: canonical });
@@ -102544,7 +102661,7 @@ function handleFileIOChannel(initialCwd, send, log, deps) {
102544
102661
  const { stdout } = await execFileAsync3(
102545
102662
  "git",
102546
102663
  ["ls-files", "--cached", "--others", "--exclude-standard", "-z"],
102547
- { cwd, maxBuffer: 10 * 1024 * 1024 }
102664
+ { cwd, maxBuffer: 100 * 1024 * 1024 }
102548
102665
  );
102549
102666
  const files = stdout.split("\0").filter((f2) => f2.length > 0);
102550
102667
  respond({ type: "git_ls_files_result", requestId, files });
@@ -103332,13 +103449,13 @@ function wireThreadErrorFallback(daemon, dc, taskId, threadId, channelId, log) {
103332
103449
  // src/shared/pty-manager.ts
103333
103450
  import { accessSync, chmodSync, constants as constants2 } from "fs";
103334
103451
  import { createRequire as createRequire4 } from "module";
103335
- import { dirname as dirname19, resolve as resolve3 } from "path";
103452
+ import { dirname as dirname20, resolve as resolve3 } from "path";
103336
103453
  import * as pty from "node-pty";
103337
103454
  function ensureSpawnHelperExecutable() {
103338
103455
  if (globalThis.process.platform === "win32") return;
103339
103456
  try {
103340
103457
  const req = createRequire4(import.meta.url);
103341
- const nodePtyDir = dirname19(req.resolve("node-pty/package.json"));
103458
+ const nodePtyDir = dirname20(req.resolve("node-pty/package.json"));
103342
103459
  const spawnHelper = resolve3(
103343
103460
  nodePtyDir,
103344
103461
  "prebuilds",
@@ -103933,7 +104050,16 @@ function buildCollabRoomManager(deps) {
103933
104050
  const taskCwd = daemon.taskManager.getTaskCwd(collabTaskId);
103934
104051
  if (!taskCwd) return false;
103935
104052
  return isUnderAllowedRoot2(abs, [taskCwd]);
103936
- }
104053
+ },
104054
+ /**
104055
+ * Collab peers must NOT get external-path reads — a peer
104056
+ * could otherwise send `{external: true, path: '/etc/hosts'}`
104057
+ * or walk to another task's cwd via the abs-path flag, which
104058
+ * would defeat the entire task-isolation allowlist above.
104059
+ * The daemon rejects external:true with
104060
+ * `external_not_allowed:...` when this flag is false.
104061
+ */
104062
+ allowExternalReads: false
103937
104063
  }
103938
104064
  });
103939
104065
  log.info("Collab file I/O channel wired");
@@ -104065,13 +104191,13 @@ function buildCollabRoomManager(deps) {
104065
104191
  import { execSync } from "child_process";
104066
104192
  import { existsSync as existsSync8 } from "fs";
104067
104193
  import { createRequire as createRequire5 } from "module";
104068
- import { dirname as dirname20, join as join53 } from "path";
104194
+ import { dirname as dirname21, join as join54 } from "path";
104069
104195
 
104070
104196
  // src/services/bootstrap/self-update.ts
104071
104197
  import { execFile as execFile11, spawn as spawn8 } from "child_process";
104072
104198
  import { createHash as createHash6 } from "crypto";
104073
104199
  import { chmod as chmod3, mkdir as mkdir22, readFile as readFile35, rename as rename20, unlink as unlink11, writeFile as writeFile29 } from "fs/promises";
104074
- import { join as join52 } from "path";
104200
+ import { join as join53 } from "path";
104075
104201
 
104076
104202
  // src/services/bootstrap/self-update-installer-scripts.ts
104077
104203
  function buildPosixInstallerScript(params) {
@@ -104473,7 +104599,7 @@ async function downloadTarball(url, destPath, fetchFn) {
104473
104599
  throw new Error(`download failed: HTTP ${response.status} ${response.statusText}`);
104474
104600
  }
104475
104601
  const bytes = new Uint8Array(await response.arrayBuffer());
104476
- await mkdir22(join52(destPath, ".."), { recursive: true });
104602
+ await mkdir22(join53(destPath, ".."), { recursive: true });
104477
104603
  try {
104478
104604
  await writeFile29(tmpPath, bytes);
104479
104605
  await rename20(tmpPath, destPath);
@@ -104503,7 +104629,7 @@ async function verifyChecksum(path2, expectedHash) {
104503
104629
  }
104504
104630
  }
104505
104631
  async function stageInstallerScript(scriptPath, params) {
104506
- await mkdir22(join52(scriptPath, ".."), { recursive: true });
104632
+ await mkdir22(join53(scriptPath, ".."), { recursive: true });
104507
104633
  const body = process.platform === "win32" ? buildWindowsInstallerScript(params) : buildPosixInstallerScript(params);
104508
104634
  await writeFile29(scriptPath, body);
104509
104635
  await chmod3(scriptPath, 493);
@@ -104553,16 +104679,16 @@ function buildInstallerParams(state, deps) {
104553
104679
  targetVersion: state.resolved.version,
104554
104680
  previousVersion: deps.currentVersion,
104555
104681
  parentPid: deps.pid,
104556
- statusFilePath: join52(deps.shipyardHome, "update-status.json"),
104557
- pidFilePath: join52(deps.shipyardHome, "daemon.pid"),
104558
- logPath: join52(deps.shipyardHome, "updates", `install-${deps.pid}.log`),
104559
- snapshotPath: join52(deps.shipyardHome, "updates", `rollback-${deps.currentVersion}`),
104682
+ statusFilePath: join53(deps.shipyardHome, "update-status.json"),
104683
+ pidFilePath: join53(deps.shipyardHome, "daemon.pid"),
104684
+ logPath: join53(deps.shipyardHome, "updates", `install-${deps.pid}.log`),
104685
+ snapshotPath: join53(deps.shipyardHome, "updates", `rollback-${deps.currentVersion}`),
104560
104686
  npmBin: "npm"
104561
104687
  };
104562
104688
  }
104563
104689
  function tarballPathFor(shipyardHome, version, shasum) {
104564
104690
  const shaPrefix = shasum.slice(0, 12);
104565
- return join52(shipyardHome, "updates", `${version}-${shaPrefix}.tgz`);
104691
+ return join53(shipyardHome, "updates", `${version}-${shaPrefix}.tgz`);
104566
104692
  }
104567
104693
  async function runResolveStep(step, state, deps) {
104568
104694
  const resolver = deps.resolveVersion ?? defaultResolveVersion;
@@ -104728,7 +104854,7 @@ function resolveClaudeCodePath(log) {
104728
104854
  try {
104729
104855
  const req = createRequire5(import.meta.url);
104730
104856
  const sdkMain = req.resolve("@anthropic-ai/claude-agent-sdk");
104731
- const p2 = join53(dirname20(sdkMain), "cli.js");
104857
+ const p2 = join54(dirname21(sdkMain), "cli.js");
104732
104858
  if (existsSync8(p2)) return ok("sdk_bundled", p2);
104733
104859
  } catch {
104734
104860
  }
@@ -104738,7 +104864,7 @@ function resolveClaudeCodePath(log) {
104738
104864
  } catch {
104739
104865
  }
104740
104866
  for (const c of [
104741
- join53(process.env.HOME ?? "", ".local", "bin", "claude"),
104867
+ join54(process.env.HOME ?? "", ".local", "bin", "claude"),
104742
104868
  "/usr/local/bin/claude"
104743
104869
  ])
104744
104870
  if (existsSync8(c)) return ok("well_known", c);
@@ -105069,7 +105195,14 @@ function buildSharedChannelCallbacks(deps) {
105069
105195
  diffTurn: (taskId, turnIndex) => daemon.taskManager.diffTurn(taskId, turnIndex),
105070
105196
  diffTurnFile: (taskId, turnIndex, path2) => daemon.taskManager.diffTurnFile(taskId, turnIndex, path2),
105071
105197
  revertTurn: (taskId, turnIndex, mode, filePath) => daemon.taskManager.revertTurn(taskId, turnIndex, mode, filePath),
105072
- isAllowedCwd: (abs) => isUnderAllowedRoot2(abs, daemon.taskManager.getAllowedCwds())
105198
+ isAllowedCwd: (abs) => isUnderAllowedRoot2(abs, daemon.taskManager.getAllowedCwds()),
105199
+ /**
105200
+ * Personal-mode: owner is operating on their own host, so reading
105201
+ * arbitrary files outside the workspace (subject to HIDDEN_PATTERNS)
105202
+ * is the feature that lets them open ~/.claude/plans/*.md and
105203
+ * similar. Collab wiring must pass `false` here.
105204
+ */
105205
+ allowExternalReads: true
105073
105206
  },
105074
105207
  onCwdChange: (newCwd) => {
105075
105208
  portDetectorRef.current?.setCwd(newCwd);
@@ -105237,7 +105370,7 @@ function buildLocalDirectChannelCallbacks(deps) {
105237
105370
  // src/services/storage/daemon-settings-store.ts
105238
105371
  import { createHash as createHash7 } from "crypto";
105239
105372
  import { mkdir as mkdir23, readFile as readFile36, rename as rename21, writeFile as writeFile30 } from "fs/promises";
105240
- import { join as join54 } from "path";
105373
+ import { join as join55 } from "path";
105241
105374
  var ProjectSettingsSchema = external_exports.object({
105242
105375
  disabledMcpServers: external_exports.array(external_exports.string()).optional()
105243
105376
  });
@@ -105245,9 +105378,9 @@ function hashProjectPath(projectPath) {
105245
105378
  return createHash7("sha256").update(projectPath).digest("hex").slice(0, 16);
105246
105379
  }
105247
105380
  function buildDaemonSettingsStore(dataDir) {
105248
- const settingsDir = join54(dataDir, "settings");
105381
+ const settingsDir = join55(dataDir, "settings");
105249
105382
  function settingsPath(projectPath) {
105250
- return join54(settingsDir, `${hashProjectPath(projectPath)}.json`);
105383
+ return join55(settingsDir, `${hashProjectPath(projectPath)}.json`);
105251
105384
  }
105252
105385
  async function ensureDir() {
105253
105386
  await mkdir23(settingsDir, { recursive: true });
@@ -105274,12 +105407,12 @@ function buildDaemonSettingsStore(dataDir) {
105274
105407
 
105275
105408
  // src/services/storage/plugin-config-store.ts
105276
105409
  import { mkdir as mkdir24, readFile as readFile37, rename as rename22, writeFile as writeFile31 } from "fs/promises";
105277
- import { join as join55 } from "path";
105410
+ import { join as join56 } from "path";
105278
105411
  function buildPluginConfigStore(pluginsDir) {
105279
105412
  const cache2 = /* @__PURE__ */ new Map();
105280
105413
  const writeQueues = /* @__PURE__ */ new Map();
105281
105414
  function configPath(pluginId) {
105282
- return join55(pluginsDir, pluginId, "config.json");
105415
+ return join56(pluginsDir, pluginId, "config.json");
105283
105416
  }
105284
105417
  async function ensureLoaded(pluginId) {
105285
105418
  const cached2 = cache2.get(pluginId);
@@ -105310,7 +105443,7 @@ function buildPluginConfigStore(pluginsDir) {
105310
105443
  const next = prev.then(async () => {
105311
105444
  const filePath = configPath(pluginId);
105312
105445
  const tmpPath = `${filePath}.tmp`;
105313
- await mkdir24(join55(pluginsDir, pluginId), { recursive: true });
105446
+ await mkdir24(join56(pluginsDir, pluginId), { recursive: true });
105314
105447
  await writeFile31(tmpPath, JSON.stringify(config2, null, 2), "utf-8");
105315
105448
  await rename22(tmpPath, filePath);
105316
105449
  });
@@ -105331,13 +105464,13 @@ function buildPluginConfigStore(pluginsDir) {
105331
105464
  var LEGACY_EPOCH = 5;
105332
105465
  async function serve(options = {}) {
105333
105466
  const shipyardHome = options.shipyardHome ?? getShipyardHome();
105334
- const dataDir = join56(shipyardHome, options.isDev ? "data-dev" : "data");
105467
+ const dataDir = join57(shipyardHome, options.isDev ? "data-dev" : "data");
105335
105468
  const log = createChildLogger({ mode: "serve" });
105336
- const workspaceRoot = await realpath2(findProjectRoot(process.cwd())).catch(
105469
+ const workspaceRoot = await realpath3(findProjectRoot(process.cwd())).catch(
105337
105470
  () => findProjectRoot(process.cwd())
105338
105471
  );
105339
105472
  registerBuiltinPlugins();
105340
- const pluginConfigStore = buildPluginConfigStore(join56(dataDir, "plugins"));
105473
+ const pluginConfigStore = buildPluginConfigStore(join57(dataDir, "plugins"));
105341
105474
  await mkdir25(dataDir, { recursive: true });
105342
105475
  log.info({ shipyardHome, dataDir, workspaceRoot }, "Starting daemon");
105343
105476
  function logAdapter(entry) {
@@ -105363,8 +105496,8 @@ async function serve(options = {}) {
105363
105496
  await lifecycle.acquirePidFile(shipyardHome);
105364
105497
  const pidTracker = buildPidTracker(shipyardHome);
105365
105498
  await pidTracker.sweepOrphans(logAdapter);
105366
- await pruneOldEpochData(join56(dataDir, "loro"), LEGACY_EPOCH, logAdapter);
105367
- const storage = new FileStorageAdapter(join56(dataDir, "loro"));
105499
+ await pruneOldEpochData(join57(dataDir, "loro"), LEGACY_EPOCH, logAdapter);
105500
+ const storage = new FileStorageAdapter(join57(dataDir, "loro"));
105368
105501
  const personalWebrtcAdapter = new WebRtcDataChannelAdapter();
105369
105502
  const peerRoleRegistry = createPeerRoleRegistry();
105370
105503
  const presencePoolRef = { pool: {} };
@@ -105425,7 +105558,7 @@ async function serve(options = {}) {
105425
105558
  previewProxy
105426
105559
  });
105427
105560
  daemon.healthMetrics.start();
105428
- const pluginsDir = join56(shipyardHome, "plugins");
105561
+ const pluginsDir = join57(shipyardHome, "plugins");
105429
105562
  await mkdir25(pluginsDir, { recursive: true });
105430
105563
  let loadedPlugins = [];
105431
105564
  const loadedPluginsRef = {
@@ -105558,7 +105691,6 @@ async function serve(options = {}) {
105558
105691
  const peerManager = peerSetupDeps ? buildPeerManager(peerSetupDeps) : null;
105559
105692
  if (peerSetupDeps && !localDirectRef.current) {
105560
105693
  const handle = await setupLocalDirect({
105561
- env,
105562
105694
  shipyardHome,
105563
105695
  daemonId: daemonPeerId,
105564
105696
  userId: auth3.userId,
@@ -105701,4 +105833,4 @@ export {
105701
105833
  _testing,
105702
105834
  serve
105703
105835
  };
105704
- //# sourceMappingURL=serve-O4AHOTL4.js.map
105836
+ //# sourceMappingURL=serve-KNOYKFPQ.js.map