@cmmd-center/forge 0.13.3 → 0.13.4

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 (65) hide show
  1. package/dist/{BunHttpServer-VI75aqJw.mjs → BunHttpServer-CDh4Vioe.mjs} +6 -6
  2. package/dist/{BunHttpServer-BhE7Szci.cjs → BunHttpServer-DKvSpNd1.cjs} +6 -6
  3. package/dist/{BunPTY-oVISucl_.mjs → BunPTY-DkAIzLIx.mjs} +3 -3
  4. package/dist/{BunPTY-86ttlTi6.cjs → BunPTY-uyreNRGp.cjs} +3 -3
  5. package/dist/{BunServices-D0JTDujL.mjs → BunServices-D6PAheqS.mjs} +3 -3
  6. package/dist/{BunServices-BwE9KexF.cjs → BunServices-DDh7Whyx.cjs} +3 -3
  7. package/dist/{Cache-p0X77SKw.mjs → Cache-BVilO50s.mjs} +3 -3
  8. package/dist/{Cache-CWlxNDbC.cjs → Cache-DCF22Dfk.cjs} +3 -3
  9. package/dist/{Config-AqUR_lhX.mjs → Config-Cdfpphpo.mjs} +2 -2
  10. package/dist/{Config-CkEzdD9L.cjs → Config-CwKCCk14.cjs} +2 -2
  11. package/dist/{HttpServer-DNGCTTdk.mjs → HttpServer-ClsGgAq1.mjs} +3 -3
  12. package/dist/{HttpServer-DGoLR-kU.cjs → HttpServer-D1gSU7NX.cjs} +3 -3
  13. package/dist/{NodeHttpServer-Dw7ivXfX.mjs → NodeHttpServer-DT0TF8s4.mjs} +7 -7
  14. package/dist/{NodeHttpServer-BPjxt_DT.cjs → NodeHttpServer-Dfc7nSMq.cjs} +7 -7
  15. package/dist/{NodePTY-Cps0jsDA.cjs → NodePTY-S4tPEwZc.cjs} +4 -4
  16. package/dist/{NodePTY-CVH2yNYl.mjs → NodePTY-loxi8Vku.mjs} +4 -4
  17. package/dist/{NodeServices-CBNXOYLG.mjs → NodeServices-BahzLhbo.mjs} +3 -3
  18. package/dist/{NodeServices-Cw6SxEbl.cjs → NodeServices-Cb0rXFb2.cjs} +3 -3
  19. package/dist/{NodeServices-Ddr-tYRu.cjs → NodeServices-DE8jhdcU.cjs} +1 -1
  20. package/dist/{NodeSqliteClient-DGInbxQ3.mjs → NodeSqliteClient-DqFjB3u_.mjs} +6 -6
  21. package/dist/{NodeSqliteClient-CmxPzcBf.cjs → NodeSqliteClient-qjBWAOGq.cjs} +6 -6
  22. package/dist/{NodeTerminal-BEGtoMSS.mjs → NodeTerminal-BPw7bktz.mjs} +3 -3
  23. package/dist/{NodeTerminal-5iWbO0B2.cjs → NodeTerminal-rYbifme7.cjs} +3 -3
  24. package/dist/{PTY-BxYDiW3h.cjs → PTY-B4ZctbwQ.cjs} +2 -2
  25. package/dist/{PTY-rIPf52QR.mjs → PTY-BzdbZ0Ym.mjs} +2 -2
  26. package/dist/{Path-C6Zb2WHp.mjs → Path-C851t6sC.mjs} +2 -2
  27. package/dist/{Path-Be7zSwar.cjs → Path-CGCc_dDx.cjs} +2 -2
  28. package/dist/{Schema-dLi7Qssy.cjs → Schema-CDCJ3xZo.cjs} +7 -1
  29. package/dist/{Schema-CJyk7ah1.mjs → Schema-DOBWF8gQ.mjs} +2 -2
  30. package/dist/{SqlError-cyJT2IRY.mjs → SqlError-3aqibZrj.mjs} +3 -3
  31. package/dist/{SqlError-Bt0Bcd5C.cjs → SqlError-CzB6p9lL.cjs} +3 -3
  32. package/dist/{SqliteClient-B08FzZM4.cjs → SqliteClient-Cq1peWN1.cjs} +5 -5
  33. package/dist/{SqliteClient-CIsITMoQ.mjs → SqliteClient-DTWCGkrj.mjs} +5 -5
  34. package/dist/bin.cjs +1086 -110
  35. package/dist/bin.mjs +1109 -136
  36. package/dist/client/assets/{CanvasPanel-BrVen-8G.js → CanvasPanel-DkUmuXgO.js} +3 -3
  37. package/dist/client/assets/CompositeItem-DG5YBMcI.js +2 -0
  38. package/dist/client/assets/{DiffPanel-B_HjSs6Q.js → DiffPanel-DLCCD1ht.js} +3 -3
  39. package/dist/client/assets/DiffPanel.logic-dIDfHUQm.js +21 -0
  40. package/dist/client/assets/DiffPanelShell-Ba6NA1s7.js +2531 -0
  41. package/dist/client/assets/{DiffWorkerPoolProvider-CwUTD5yv.js → DiffWorkerPoolProvider-Bqy6DzWP.js} +52 -52
  42. package/dist/client/assets/PullRequestCodePanel-8SJWZPM5.js +5 -0
  43. package/dist/client/assets/canvasStateStore-BWZ8xdXr.js +2 -0
  44. package/dist/client/assets/{cmmdAuthRedirect-sqR8W6vY.js → cmmdAuthRedirect--srSJjFS.js} +2 -2
  45. package/dist/client/assets/diffTargetResolution-DA0SlQE0.js +2 -0
  46. package/dist/client/assets/index-C9Upewz-.js +151 -0
  47. package/dist/client/assets/index-DlyGyVH6.css +1 -0
  48. package/dist/client/assets/src-DwOYg6gv.js +3 -0
  49. package/dist/client/assets/toggle-Ozy-3fZ2.js +2 -0
  50. package/dist/client/assets/useTriggerFocusGuards-FzV_CNjl.js +2 -0
  51. package/dist/client/index.html +11 -11
  52. package/dist/forge-build.json +1 -1
  53. package/dist/{open-FIgTDosf.mjs → open-D8mGyWmt.mjs} +3 -3
  54. package/package.json +1 -1
  55. package/dist/client/assets/CompositeItem-BNZfMUvm.js +0 -2
  56. package/dist/client/assets/DiffPanel.logic-_zUBJ9_7.js +0 -21
  57. package/dist/client/assets/DiffPanelShell-DmjIQbsj.js +0 -2531
  58. package/dist/client/assets/PullRequestCodePanel-CAaS8yoZ.js +0 -5
  59. package/dist/client/assets/canvasStateStore-DN5mPB5B.js +0 -2
  60. package/dist/client/assets/diffTargetResolution-IthQ5nAw.js +0 -2
  61. package/dist/client/assets/index-CgwIqrjs.js +0 -151
  62. package/dist/client/assets/index-Dk7T0zVg.css +0 -1
  63. package/dist/client/assets/src-CFqnisEM.js +0 -3
  64. package/dist/client/assets/toggle-CLUqZXdO.js +0 -2
  65. package/dist/client/assets/useTriggerFocusGuards-CaUimC-O.js +0 -2
package/dist/bin.cjs CHANGED
@@ -2,18 +2,18 @@
2
2
 
3
3
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
4
4
  const require_chunk = require("./chunk-BhaIgcZK.cjs");
5
- const require_Schema$1 = require("./Schema-dLi7Qssy.cjs");
6
- const require_Path = require("./Path-Be7zSwar.cjs");
7
- const require_NodeTerminal = require("./NodeTerminal-5iWbO0B2.cjs");
8
- const require_NodeServices = require("./NodeServices-Cw6SxEbl.cjs");
9
- const require_HttpServer = require("./HttpServer-DGoLR-kU.cjs");
10
- const require_Config = require("./Config-CkEzdD9L.cjs");
11
- const require_Cache = require("./Cache-CWlxNDbC.cjs");
12
- const require_SqlError = require("./SqlError-Bt0Bcd5C.cjs");
5
+ const require_Schema$1 = require("./Schema-CDCJ3xZo.cjs");
6
+ const require_Path = require("./Path-CGCc_dDx.cjs");
7
+ const require_NodeTerminal = require("./NodeTerminal-rYbifme7.cjs");
8
+ const require_NodeServices = require("./NodeServices-Cb0rXFb2.cjs");
9
+ const require_HttpServer = require("./HttpServer-D1gSU7NX.cjs");
10
+ const require_Config = require("./Config-CwKCCk14.cjs");
11
+ const require_Cache = require("./Cache-DCF22Dfk.cjs");
12
+ const require_SqlError = require("./SqlError-CzB6p9lL.cjs");
13
13
  const require_Mime = require("./Mime-Bilnh8zE.cjs");
14
14
  const require_esm = require("./esm-BZhXwX5p.cjs");
15
15
  const require_runtimeControlAuthority = require("./runtimeControlAuthority-B3kNmYRT.cjs");
16
- const require_PTY = require("./PTY-BxYDiW3h.cjs");
16
+ const require_PTY = require("./PTY-B4ZctbwQ.cjs");
17
17
  let node_child_process = require("node:child_process");
18
18
  node_child_process = require_chunk.__toESM(node_child_process);
19
19
  let node_stream = require("node:stream");
@@ -16544,7 +16544,7 @@ function makeNetServiceShape(createServer = node_net.createServer) {
16544
16544
  }
16545
16545
  //#endregion
16546
16546
  //#region ../../packages/shared/src/releaseInfo.generated.ts
16547
- const FORGE_RELEASE_VERSION = "0.13.3";
16547
+ const FORGE_RELEASE_VERSION = "0.13.4";
16548
16548
  //#endregion
16549
16549
  //#region ../../packages/shared/src/hostProcess.ts
16550
16550
  /**
@@ -16668,6 +16668,70 @@ const CheckpointRef = makeEntityId("CheckpointRef");
16668
16668
  */
16669
16669
  const ThreadEnvMode = require_Schema$1.Literals(["local", "worktree"]);
16670
16670
  //#endregion
16671
+ //#region ../../packages/contracts/src/assets.ts
16672
+ /**
16673
+ * Addresses for the bytes a client cannot fetch by guessing.
16674
+ *
16675
+ * Forge already serves the bytes: attachments under `/attachments/*` and
16676
+ * project favicons at `GET /api/project-favicon?cwd=`. What it did not serve
16677
+ * was the question "what is the URL for this thing", which is the only way a
16678
+ * client that holds an attachment id or a workspace root can reach either.
16679
+ * The phone asks that question on the thread list for every project, so
16680
+ * until this existed the answer was an error the phone drew as a spinner.
16681
+ *
16682
+ * These schemas are the Forge side of `assets.*` in the vendored t3 contracts.
16683
+ * They must decode what that package encodes: same tags, same field names.
16684
+ */
16685
+ const ASSET_PATH_MAX_LENGTH = 1024;
16686
+ const ASSET_ID_MAX_LENGTH = 256;
16687
+ const ASSET_URL_MAX_LENGTH = 4096;
16688
+ const AssetResource = require_Schema$1.Union([
16689
+ require_Schema$1.TaggedStruct("workspace-file", {
16690
+ threadId: ThreadId,
16691
+ path: TrimmedNonEmptyString.check(require_Schema$1.isMaxLength(ASSET_PATH_MAX_LENGTH))
16692
+ }),
16693
+ require_Schema$1.TaggedStruct("attachment", { attachmentId: TrimmedNonEmptyString.check(require_Schema$1.isMaxLength(ASSET_ID_MAX_LENGTH)) }),
16694
+ require_Schema$1.TaggedStruct("project-favicon", {
16695
+ cwd: TrimmedNonEmptyString.check(require_Schema$1.isMaxLength(ASSET_PATH_MAX_LENGTH)),
16696
+ path: require_Schema$1.optional(TrimmedNonEmptyString.check(require_Schema$1.isMaxLength(ASSET_PATH_MAX_LENGTH)))
16697
+ })
16698
+ ]);
16699
+ const AssetCreateUrlInput = require_Schema$1.Struct({ resource: AssetResource });
16700
+ const AssetCreateUrlResult = require_Schema$1.Struct({
16701
+ relativeUrl: TrimmedNonEmptyString.check(require_Schema$1.isMaxLength(ASSET_URL_MAX_LENGTH)),
16702
+ expiresAt: require_Schema$1.Number,
16703
+ sourcePath: require_Schema$1.optional(TrimmedNonEmptyString.check(require_Schema$1.isMaxLength(ASSET_PATH_MAX_LENGTH)))
16704
+ });
16705
+ /**
16706
+ * The resource cannot be addressed by URL on this server.
16707
+ *
16708
+ * Tag and message match the vendored client's `AssetAccessError` union so the
16709
+ * phone decodes the failure rather than falling through to a generic transport
16710
+ * error. `workspace-file` is the case that lands here.
16711
+ */
16712
+ var AssetWorkspaceResolutionError = class extends require_Schema$1.TaggedErrorClass()("AssetWorkspaceResolutionError", {
16713
+ resource: AssetResource,
16714
+ cause: require_Schema$1.optional(require_Schema$1.Defect)
16715
+ }) {
16716
+ get message() {
16717
+ return "Failed to resolve workspace.";
16718
+ }
16719
+ };
16720
+ /** The signing key that makes an asset URL self-authenticating could not be read. */
16721
+ var AssetSigningKeyLoadError = class extends require_Schema$1.TaggedErrorClass()("AssetSigningKeyLoadError", {
16722
+ resource: AssetResource,
16723
+ cause: require_Schema$1.optional(require_Schema$1.Defect)
16724
+ }) {
16725
+ get message() {
16726
+ return "Failed to load the asset signing key.";
16727
+ }
16728
+ };
16729
+ const AssetAccessError = require_Schema$1.Union([AssetWorkspaceResolutionError, AssetSigningKeyLoadError]);
16730
+ const AssetsRpcSchemas = { createUrl: {
16731
+ input: AssetCreateUrlInput,
16732
+ output: AssetCreateUrlResult
16733
+ } };
16734
+ //#endregion
16671
16735
  //#region ../../node_modules/.bun/effect@4.0.0-beta.51/node_modules/effect/dist/unstable/rpc/RpcMiddleware.js
16672
16736
  /**
16673
16737
  * @since 4.0.0
@@ -18200,6 +18264,7 @@ const ExecutionEnvironmentCapabilities = require_Schema$1.Struct({
18200
18264
  threadPullRequestLinking: require_Schema$1.optionalKey(require_Schema$1.Boolean),
18201
18265
  cloudMode: require_Schema$1.Boolean.pipe(require_Schema$1.withDecodingDefault(require_Schema$1.succeed(false))),
18202
18266
  threadSettlement: require_Schema$1.optionalKey(require_Schema$1.Boolean),
18267
+ threadTitleRegeneration: require_Schema$1.optionalKey(require_Schema$1.Boolean),
18203
18268
  posthogPublicKey: require_Schema$1.NullOr(require_Schema$1.String).pipe(require_Schema$1.withDecodingDefault(require_Schema$1.succeed(null))),
18204
18269
  posthogHost: require_Schema$1.NullOr(require_Schema$1.String).pipe(require_Schema$1.withDecodingDefault(require_Schema$1.succeed(null))),
18205
18270
  runtime: require_Schema$1.optionalKey(ExecutionEnvironmentRuntimeCapabilities),
@@ -19228,6 +19293,51 @@ require_Schema$1.Struct({
19228
19293
  headRepositoryNameWithOwner: require_Schema$1.optional(require_Schema$1.NullOr(TrimmedNonEmptyString)),
19229
19294
  headRepositoryOwnerLogin: require_Schema$1.optional(require_Schema$1.NullOr(TrimmedNonEmptyString))
19230
19295
  });
19296
+ const SourceControlCloneProtocol = require_Schema$1.Literals([
19297
+ "auto",
19298
+ "ssh",
19299
+ "https"
19300
+ ]);
19301
+ const SourceControlRepositoryInfo = require_Schema$1.Struct({
19302
+ provider: SourceControlProviderKind$1,
19303
+ nameWithOwner: TrimmedNonEmptyString,
19304
+ url: TrimmedNonEmptyString,
19305
+ sshUrl: TrimmedNonEmptyString
19306
+ });
19307
+ const SourceControlRepositoryLookupInput = require_Schema$1.Struct({
19308
+ provider: SourceControlProviderKind$1,
19309
+ repository: TrimmedNonEmptyString,
19310
+ cwd: require_Schema$1.optional(TrimmedNonEmptyString)
19311
+ });
19312
+ /**
19313
+ * Either name the repository (`provider` + `repository`) or paste a clone URL
19314
+ * (`remoteUrl`). Both are optional in the schema because the phone sends one
19315
+ * or the other depending on how the user got here; the server rejects a call
19316
+ * that supplies neither, and rejects a `remoteUrl` it cannot resolve to a
19317
+ * repository it is allowed to fetch a token for.
19318
+ */
19319
+ const SourceControlCloneRepositoryInput = require_Schema$1.Struct({
19320
+ provider: require_Schema$1.optional(SourceControlProviderKind$1),
19321
+ repository: require_Schema$1.optional(TrimmedNonEmptyString),
19322
+ remoteUrl: require_Schema$1.optional(TrimmedNonEmptyString),
19323
+ destinationPath: TrimmedNonEmptyString,
19324
+ protocol: require_Schema$1.optional(SourceControlCloneProtocol)
19325
+ });
19326
+ const SourceControlCloneRepositoryResult = require_Schema$1.Struct({
19327
+ cwd: TrimmedNonEmptyString,
19328
+ remoteUrl: TrimmedNonEmptyString,
19329
+ repository: require_Schema$1.NullOr(SourceControlRepositoryInfo)
19330
+ });
19331
+ var SourceControlRepositoryError = class extends require_Schema$1.TaggedErrorClass()("SourceControlRepositoryError", {
19332
+ provider: SourceControlProviderKind$1,
19333
+ operation: require_Schema$1.String,
19334
+ detail: require_Schema$1.String,
19335
+ cause: require_Schema$1.optional(require_Schema$1.Defect)
19336
+ }) {
19337
+ get message() {
19338
+ return `Source control repository operation ${this.operation} failed for ${this.provider}: ${this.detail}`;
19339
+ }
19340
+ };
19231
19341
  //#endregion
19232
19342
  //#region ../../packages/contracts/src/orchestration.ts
19233
19343
  const ORCHESTRATION_WS_METHODS = {
@@ -19239,7 +19349,8 @@ const ORCHESTRATION_WS_METHODS = {
19239
19349
  replayEvents: "orchestration.replayEvents",
19240
19350
  getArchivedShellSnapshot: "orchestration.getArchivedShellSnapshot",
19241
19351
  subscribeShell: "orchestration.subscribeShell",
19242
- subscribeThread: "orchestration.subscribeThread"
19352
+ subscribeThread: "orchestration.subscribeThread",
19353
+ searchThreads: "orchestration.searchThreads"
19243
19354
  };
19244
19355
  const ProviderKind = require_Schema$1.Literals([
19245
19356
  "codex",
@@ -20623,6 +20734,32 @@ const OrchestrationGetFullThreadDiffInput = require_Schema$1.Struct({
20623
20734
  const OrchestrationGetFullThreadDiffResult = ThreadTurnDiff;
20624
20735
  const OrchestrationReplayEventsInput = require_Schema$1.Struct({ fromSequenceExclusive: NonNegativeInt });
20625
20736
  const OrchestrationReplayEventsResult = require_Schema$1.ArraySchema(OrchestrationEvent);
20737
+ /**
20738
+ * Which side of the conversation a search hit came from. `null`
20739
+ * `messageCreatedAt` on a match means it came from the thread's own metadata
20740
+ * (its title or its branch) rather than from a message.
20741
+ */
20742
+ const OrchestrationThreadSearchSource = require_Schema$1.Literals(["user", "assistant"]);
20743
+ /**
20744
+ * The server's SQLite client is synchronous and single-connection, so both the
20745
+ * scan input and the response size are bounded: a search must not be able to
20746
+ * monopolize the connection every keystroke.
20747
+ */
20748
+ const OrchestrationSearchThreadsInput = require_Schema$1.Struct({
20749
+ query: TrimmedString.check(require_Schema$1.isMinLength(2), require_Schema$1.isMaxLength(200)),
20750
+ limit: require_Schema$1.optionalKey(require_Schema$1.Int.check(require_Schema$1.isBetween({
20751
+ minimum: 1,
20752
+ maximum: 50
20753
+ })))
20754
+ });
20755
+ const OrchestrationThreadSearchMatch = require_Schema$1.Struct({
20756
+ threadId: ThreadId,
20757
+ projectId: ProjectId,
20758
+ source: OrchestrationThreadSearchSource,
20759
+ snippet: require_Schema$1.String.check(require_Schema$1.isMaxLength(240)),
20760
+ messageCreatedAt: require_Schema$1.NullOr(IsoDateTime)
20761
+ });
20762
+ const OrchestrationSearchThreadsResult = require_Schema$1.Struct({ matches: require_Schema$1.ArraySchema(OrchestrationThreadSearchMatch) });
20626
20763
  const ProgressPercent = NonNegativeInt.check(require_Schema$1.isLessThanOrEqualTo(100));
20627
20764
  const AgentProtocolProgress = require_Schema$1.Struct({
20628
20765
  kind: require_Schema$1.Literal("PROGRESS"),
@@ -20698,6 +20835,10 @@ const OrchestrationRpcSchemas = {
20698
20835
  getArchivedShellSnapshot: {
20699
20836
  input: require_Schema$1.Struct({}),
20700
20837
  output: OrchestrationShellSnapshot
20838
+ },
20839
+ searchThreads: {
20840
+ input: OrchestrationSearchThreadsInput,
20841
+ output: OrchestrationSearchThreadsResult
20701
20842
  }
20702
20843
  };
20703
20844
  var OrchestrationGetSnapshotError = class extends require_Schema$1.TaggedErrorClass()("OrchestrationGetSnapshotError", {
@@ -20720,6 +20861,10 @@ var OrchestrationReplayEventsError = class extends require_Schema$1.TaggedErrorC
20720
20861
  message: TrimmedNonEmptyString,
20721
20862
  cause: require_Schema$1.optional(require_Schema$1.Defect)
20722
20863
  }) {};
20864
+ var OrchestrationSearchThreadsError = class extends require_Schema$1.TaggedErrorClass()("OrchestrationSearchThreadsError", {
20865
+ message: TrimmedNonEmptyString,
20866
+ cause: require_Schema$1.optional(require_Schema$1.Defect)
20867
+ }) {};
20723
20868
  //#endregion
20724
20869
  //#region ../../packages/contracts/src/provider.ts
20725
20870
  const ProviderSessionStatus = require_Schema$1.Literals([
@@ -20782,6 +20927,31 @@ const ProviderRespondToUserInputInput = require_Schema$1.Struct({
20782
20927
  requestId: ApprovalRequestId,
20783
20928
  answers: ProviderUserInputAnswers
20784
20929
  });
20930
+ /**
20931
+ * Sending a thread to the people who maintain Forge.
20932
+ *
20933
+ * The vendored client's `/feedback` slash command calls this. Upstream it
20934
+ * uploads the transcript to the model provider; Forge routes it to the same
20935
+ * CMMD feedback intake the web app's feedback dialog already posts to, so one
20936
+ * report lands in one place no matter which surface filed it.
20937
+ *
20938
+ * `reason` is what the person typed after the command, and is the only part
20939
+ * of the report they wrote. The server fills in the rest.
20940
+ */
20941
+ const ProviderUploadFeedbackInput = require_Schema$1.Struct({
20942
+ threadId: ThreadId,
20943
+ reason: require_Schema$1.optional(TrimmedNonEmptyString.check(require_Schema$1.isMaxLength(4e3)))
20944
+ });
20945
+ const ProviderUploadFeedbackResult = require_Schema$1.Struct({ feedbackId: TrimmedNonEmptyString });
20946
+ var ProviderUploadFeedbackError = class extends require_Schema$1.TaggedErrorClass()("ProviderUploadFeedbackError", {
20947
+ threadId: ThreadId,
20948
+ reason: require_Schema$1.optional(TrimmedNonEmptyString.check(require_Schema$1.isMaxLength(500))),
20949
+ cause: require_Schema$1.optional(require_Schema$1.Defect)
20950
+ }) {
20951
+ get message() {
20952
+ return this.reason ?? "Feedback could not be sent. Please try again.";
20953
+ }
20954
+ };
20785
20955
  const ProviderEventKind = require_Schema$1.Literals([
20786
20956
  "session",
20787
20957
  "notification",
@@ -21261,6 +21431,46 @@ const TaskStartedPayload = require_Schema$1.Struct({
21261
21431
  description: require_Schema$1.optional(TrimmedNonEmptyStringSchema$2),
21262
21432
  taskType: require_Schema$1.optional(TrimmedNonEmptyStringSchema$2)
21263
21433
  });
21434
+ /**
21435
+ * Watch-loop task types: Monitor-tool tasks plus background shells. A shell
21436
+ * that outlives its turn is in practice a watch loop, not delegated work.
21437
+ */
21438
+ const MONITOR_TASK_TYPES = new Set([
21439
+ "monitor",
21440
+ "monitor_mcp",
21441
+ "local_bash",
21442
+ "shell"
21443
+ ]);
21444
+ /** Task types that are neither agents nor watch loops (plan-mode bookkeeping). */
21445
+ const INERT_TASK_TYPES = new Set(["plan", "dream"]);
21446
+ /**
21447
+ * Agent-vs-background classification for a task row.
21448
+ *
21449
+ * The mobile client reads `agentKind` off the persisted activity payload and
21450
+ * treats a row without it as background, so a server that never stamps the
21451
+ * field leaves the Agents roster permanently empty however much subagent work
21452
+ * is running (FOR-36646). This is the stamp.
21453
+ *
21454
+ * A denylist, not an allowlist, and deliberately so: the agent-flavoured type
21455
+ * names drift with the SDKs (subagent, local_agent, local_workflow, and
21456
+ * whatever lands next), and an allowlist drops real subagents the day a new
21457
+ * name appears. Only the watch loops and the plan-mode bookkeeping types are
21458
+ * named here; everything else is delegated work.
21459
+ *
21460
+ * A task launched from inside a subagent (`agentId` set) is agent-internal
21461
+ * background work unless it is itself agent-flavoured, because a nested agent
21462
+ * can outlive its parent and has to stay in the roster.
21463
+ *
21464
+ * This mirrors `classifyTaskAgentKind` in the contracts the phone speaks. The
21465
+ * two packages do not import each other, so the sets are duplicated on
21466
+ * purpose; they have to agree, and the tests on both sides pin the same cases.
21467
+ */
21468
+ function classifyTaskAgentKind(input) {
21469
+ const { taskType, agentId } = input;
21470
+ const nonAgentType = taskType !== void 0 && (MONITOR_TASK_TYPES.has(taskType) || INERT_TASK_TYPES.has(taskType));
21471
+ if (agentId !== void 0 && agentId.trim().length > 0) return taskType === void 0 || nonAgentType ? "background" : "agent";
21472
+ return nonAgentType ? "background" : "agent";
21473
+ }
21264
21474
  const TaskProgressPayload = require_Schema$1.Struct({
21265
21475
  taskId: RuntimeTaskId,
21266
21476
  description: TrimmedNonEmptyStringSchema$2,
@@ -25989,6 +26199,8 @@ const WS_METHODS = {
25989
26199
  projectsListEntries: "projects.listEntries",
25990
26200
  serverDiscoverSourceControl: "server.discoverSourceControl",
25991
26201
  serverReportClientActivity: "server.reportClientActivity",
26202
+ sourceControlLookupRepository: "sourceControl.lookupRepository",
26203
+ sourceControlCloneRepository: "sourceControl.cloneRepository",
25992
26204
  projectsWriteFile: "projects.writeFile",
25993
26205
  projectCheckoutDeletionPreview: "projectCheckoutDeletion.preview",
25994
26206
  projectCheckoutDeletionExecute: "projectCheckoutDeletion.execute",
@@ -26092,7 +26304,9 @@ const WS_METHODS = {
26092
26304
  subscribeServerConfig: "subscribeServerConfig",
26093
26305
  subscribeServerLifecycle: "subscribeServerLifecycle",
26094
26306
  subscribeAuthAccess: "subscribeAuthAccess",
26095
- subscribeWorkspaceReadiness: "subscribeWorkspaceReadiness"
26307
+ subscribeWorkspaceReadiness: "subscribeWorkspaceReadiness",
26308
+ assetsCreateUrl: "assets.createUrl",
26309
+ providerUploadFeedback: "provider.uploadFeedback"
26096
26310
  };
26097
26311
  const WsServerUpsertKeybindingRpc = make$30(WS_METHODS.serverUpsertKeybinding, {
26098
26312
  payload: ServerUpsertKeybindingInput,
@@ -26621,6 +26835,29 @@ const WsOrchestrationGetArchivedShellSnapshotRpc = make$30(ORCHESTRATION_WS_METH
26621
26835
  success: OrchestrationRpcSchemas.getArchivedShellSnapshot.output,
26622
26836
  error: OrchestrationGetSnapshotError
26623
26837
  });
26838
+ const WsOrchestrationSearchThreadsRpc = make$30(ORCHESTRATION_WS_METHODS.searchThreads, {
26839
+ payload: OrchestrationSearchThreadsInput,
26840
+ success: OrchestrationRpcSchemas.searchThreads.output,
26841
+ error: OrchestrationSearchThreadsError
26842
+ });
26843
+ /**
26844
+ * Resolve `owner/repo` to the repository the caller is allowed to clone.
26845
+ *
26846
+ * Both of these run against the same CMMD-brokered GitHub credential the web
26847
+ * add-project flow uses, so a lookup that answers is a lookup whose clone can
26848
+ * also be authorized, and a repository the caller cannot reach is a failure
26849
+ * here rather than a clone that dies half way.
26850
+ */
26851
+ const WsSourceControlLookupRepositoryRpc = make$30(WS_METHODS.sourceControlLookupRepository, {
26852
+ payload: SourceControlRepositoryLookupInput,
26853
+ success: SourceControlRepositoryInfo,
26854
+ error: SourceControlRepositoryError
26855
+ });
26856
+ const WsSourceControlCloneRepositoryRpc = make$30(WS_METHODS.sourceControlCloneRepository, {
26857
+ payload: SourceControlCloneRepositoryInput,
26858
+ success: SourceControlCloneRepositoryResult,
26859
+ error: SourceControlRepositoryError
26860
+ });
26624
26861
  const WsOrchestrationSubscribeShellRpc = make$30(ORCHESTRATION_WS_METHODS.subscribeShell, {
26625
26862
  payload: OrchestrationRpcSchemas.subscribeShell.input,
26626
26863
  success: OrchestrationRpcSchemas.subscribeShell.output,
@@ -26701,7 +26938,15 @@ const WsRpcGroup = make$29(WsServerGetConfigRpc, WsServerRefreshProvidersRpc, Ws
26701
26938
  payload: UsageSummaryInput,
26702
26939
  success: UsageSummary,
26703
26940
  error: UsageReadError
26704
- }), WsPullRequestsListRpc, WsPullRequestsListStatsRpc, WsPullRequestsDetailRpc, WsPullRequestsActivityRpc, WsPullRequestsThreadCommentsRpc, WsPullRequestsDiffFileContentsRpc, WsPullRequestsRunActionRpc, WsPullRequestsUpdateRpc, WsPullRequestsCommentRpc, WsPullRequestsUpdateCommentRpc, WsPullRequestsSubmitReviewRpc, WsPullRequestsReplyToThreadRpc, WsPullRequestsSetThreadResolutionRpc, WsPullRequestsSetReactionRpc, WsPullRequestsInvalidateRpc, WsPullRequestsReviewerCandidatesRpc, WsPullRequestsRequestReviewersRpc, WsProjectsSearchEntriesRpc, WsProjectsWriteFileRpc, WsProjectCheckoutDeletionPreviewRpc, WsProjectCheckoutDeletionExecuteRpc, WsCodeGraphSummaryRpc, WsCodeGraphNeighborhoodRpc, WsCodeGraphSearchRpc, WsWorkspaceReadFileRpc, WsWorkspaceBeginWriteFileRpc, WsWorkspaceWriteFileChunkRpc, WsWorkspaceCommitWriteFileRpc, WsWorkspaceCancelWriteFileRpc, WsShellOpenInEditorRpc, WsSubscribeGitStatusRpc, WsGitPullRpc, WsGitRefreshStatusRpc, WsGitRunStackedActionRpc, WsGitResolvePullRequestRpc, WsGitPreparePullRequestThreadRpc, WsGitListBranchesRpc, WsVcsListRefsRpc, WsVcsRefreshStatusRpc, WsSubscribeVcsStatusRpc, WsVcsPullRpc, WsVcsCreateWorktreeRpc, WsVcsRemoveWorktreeRpc, WsVcsCreateRefRpc, WsVcsSwitchRefRpc, WsVcsInitRpc, WsGitCreateWorktreeRpc, WsGitRemoveWorktreeRpc, WsGitCreateBranchRpc, WsGitCheckoutRpc, WsGitInitRpc, WsFilesystemBrowseRpc, WsProjectsReadFileRpc, WsReviewGetDiffPreviewRpc, WsCloudListRuntimesRpc, WsTerminalOpenRpc, WsTerminalAttachRpc, WsTerminalWriteRpc, WsTerminalResizeRpc, WsTerminalSignalRpc, WsTerminalClearRpc, WsTerminalRestartRpc, WsTerminalCloseRpc, WsBrowserOpenSessionRpc, WsBrowserListSessionsRpc, WsBrowserCloseSessionRpc, WsBrowserGetSessionRpc, WsBrowserCommandRpc, WsBrowserClearBrowsingDataRpc, WsBrowserAttachDisplayRpc, WsBrowserAddDisplayIceCandidateRpc, WsComputerInputRpc, WsDevTunnelRegisterRpc, WsDevTunnelUnregisterRpc, WsDevTunnelListRpc, WsThreadDevRuntimeGetRpc, WsThreadDevRuntimeAllocatePortRpc, WsThreadDevRuntimeReleaseRpc, WsWorkspaceEnvironmentPreflightRpc, WsWorkspaceEnsureScratchRpc, WsSubscribeTerminalEventsRpc, WsSubscribeTerminalMetadataRpc, WsProjectsListEntriesRpc, WsServerDiscoverSourceControlRpc, WsServerReportClientActivityRpc, WsSubscribeBrowserEventsRpc, WsSubscribeServerConfigRpc, WsSubscribeServerLifecycleRpc, WsSubscribeAuthAccessRpc, WsSubscribeWorkspaceReadinessRpc, WsOrchestrationDispatchCommandRpc, WsOrchestrationGetTurnDiffRpc, WsOrchestrationGetFullThreadDiffRpc, WsOrchestrationStreamTurnDiffRpc, WsOrchestrationStreamFullThreadDiffRpc, WsOrchestrationReplayEventsRpc, WsOrchestrationGetArchivedShellSnapshotRpc, WsOrchestrationSubscribeShellRpc, WsOrchestrationSubscribeThreadRpc);
26941
+ }), WsPullRequestsListRpc, WsPullRequestsListStatsRpc, WsPullRequestsDetailRpc, WsPullRequestsActivityRpc, WsPullRequestsThreadCommentsRpc, WsPullRequestsDiffFileContentsRpc, WsPullRequestsRunActionRpc, WsPullRequestsUpdateRpc, WsPullRequestsCommentRpc, WsPullRequestsUpdateCommentRpc, WsPullRequestsSubmitReviewRpc, WsPullRequestsReplyToThreadRpc, WsPullRequestsSetThreadResolutionRpc, WsPullRequestsSetReactionRpc, WsPullRequestsInvalidateRpc, WsPullRequestsReviewerCandidatesRpc, WsPullRequestsRequestReviewersRpc, WsProjectsSearchEntriesRpc, WsProjectsWriteFileRpc, WsProjectCheckoutDeletionPreviewRpc, WsProjectCheckoutDeletionExecuteRpc, WsCodeGraphSummaryRpc, WsCodeGraphNeighborhoodRpc, WsCodeGraphSearchRpc, WsWorkspaceReadFileRpc, WsWorkspaceBeginWriteFileRpc, WsWorkspaceWriteFileChunkRpc, WsWorkspaceCommitWriteFileRpc, WsWorkspaceCancelWriteFileRpc, WsShellOpenInEditorRpc, WsSubscribeGitStatusRpc, WsGitPullRpc, WsGitRefreshStatusRpc, WsGitRunStackedActionRpc, WsGitResolvePullRequestRpc, WsGitPreparePullRequestThreadRpc, WsGitListBranchesRpc, WsVcsListRefsRpc, WsVcsRefreshStatusRpc, WsSubscribeVcsStatusRpc, WsVcsPullRpc, WsVcsCreateWorktreeRpc, WsVcsRemoveWorktreeRpc, WsVcsCreateRefRpc, WsVcsSwitchRefRpc, WsVcsInitRpc, WsGitCreateWorktreeRpc, WsGitRemoveWorktreeRpc, WsGitCreateBranchRpc, WsGitCheckoutRpc, WsGitInitRpc, WsFilesystemBrowseRpc, WsProjectsReadFileRpc, WsReviewGetDiffPreviewRpc, WsCloudListRuntimesRpc, WsTerminalOpenRpc, WsTerminalAttachRpc, WsTerminalWriteRpc, WsTerminalResizeRpc, WsTerminalSignalRpc, WsTerminalClearRpc, WsTerminalRestartRpc, WsTerminalCloseRpc, WsBrowserOpenSessionRpc, WsBrowserListSessionsRpc, WsBrowserCloseSessionRpc, WsBrowserGetSessionRpc, WsBrowserCommandRpc, WsBrowserClearBrowsingDataRpc, WsBrowserAttachDisplayRpc, WsBrowserAddDisplayIceCandidateRpc, WsComputerInputRpc, WsDevTunnelRegisterRpc, WsDevTunnelUnregisterRpc, WsDevTunnelListRpc, WsThreadDevRuntimeGetRpc, WsThreadDevRuntimeAllocatePortRpc, WsThreadDevRuntimeReleaseRpc, WsWorkspaceEnvironmentPreflightRpc, WsWorkspaceEnsureScratchRpc, WsSubscribeTerminalEventsRpc, WsSubscribeTerminalMetadataRpc, WsProjectsListEntriesRpc, WsServerDiscoverSourceControlRpc, WsServerReportClientActivityRpc, WsSubscribeBrowserEventsRpc, WsSubscribeServerConfigRpc, WsSubscribeServerLifecycleRpc, WsSubscribeAuthAccessRpc, WsSubscribeWorkspaceReadinessRpc, WsOrchestrationDispatchCommandRpc, WsOrchestrationGetTurnDiffRpc, WsOrchestrationGetFullThreadDiffRpc, WsOrchestrationStreamTurnDiffRpc, WsOrchestrationStreamFullThreadDiffRpc, WsOrchestrationReplayEventsRpc, WsOrchestrationGetArchivedShellSnapshotRpc, WsOrchestrationSubscribeShellRpc, WsOrchestrationSubscribeThreadRpc, make$30(WS_METHODS.assetsCreateUrl, {
26942
+ payload: AssetsRpcSchemas.createUrl.input,
26943
+ success: AssetsRpcSchemas.createUrl.output,
26944
+ error: AssetAccessError
26945
+ }), make$30(WS_METHODS.providerUploadFeedback, {
26946
+ payload: ProviderUploadFeedbackInput,
26947
+ success: ProviderUploadFeedbackResult,
26948
+ error: ProviderUploadFeedbackError
26949
+ }), WsOrchestrationSearchThreadsRpc, WsSourceControlLookupRepositoryRpc, WsSourceControlCloneRepositoryRpc);
26705
26950
  const RelayWsRpcGroup = WsRpcGroup.middleware(RelayAuthorizationMiddleware).middleware(RelayDeliveryMiddleware);
26706
26951
  /**
26707
26952
  * The direct-WebSocket mount of the same group, with per-method scope
@@ -31841,6 +32086,31 @@ const RELAY_METHOD_POLICY_BASES = [
31841
32086
  method: ORCHESTRATION_WS_METHODS.subscribeThread,
31842
32087
  requiredScope: "thread.read",
31843
32088
  deliveryClass: "subscription"
32089
+ },
32090
+ {
32091
+ method: WS_METHODS.assetsCreateUrl,
32092
+ requiredScope: "thread.read",
32093
+ deliveryClass: "read"
32094
+ },
32095
+ {
32096
+ method: ORCHESTRATION_WS_METHODS.searchThreads,
32097
+ requiredScope: "thread.read",
32098
+ deliveryClass: "read"
32099
+ },
32100
+ {
32101
+ method: WS_METHODS.providerUploadFeedback,
32102
+ requiredScope: "thread.read",
32103
+ deliveryClass: "create-command"
32104
+ },
32105
+ {
32106
+ method: WS_METHODS.sourceControlLookupRepository,
32107
+ requiredScope: "git.read",
32108
+ deliveryClass: "read"
32109
+ },
32110
+ {
32111
+ method: WS_METHODS.sourceControlCloneRepository,
32112
+ requiredScope: "project.write",
32113
+ deliveryClass: "idempotent-command"
31844
32114
  }
31845
32115
  ];
31846
32116
  const FRAME_JOURNALED_METHODS = new Set([
@@ -34008,7 +34278,7 @@ function isRejectedCredential(cause) {
34008
34278
  //#region src/connect/hiddenInput.ts
34009
34279
  /** Ctrl-C arrives as this byte on a raw TTY rather than as a signal. */
34010
34280
  const END_OF_TEXT = "";
34011
- function firstLine(value) {
34281
+ function firstLine$1(value) {
34012
34282
  const index = value.indexOf("\n");
34013
34283
  const line = index === -1 ? value : value.slice(0, index);
34014
34284
  return line.endsWith("\r") ? line.slice(0, -1) : line;
@@ -34033,7 +34303,7 @@ function readHiddenLine(input) {
34033
34303
  settled = true;
34034
34304
  detach();
34035
34305
  input.output.write("\n");
34036
- resolve(firstLine(value));
34306
+ resolve(firstLine$1(value));
34037
34307
  };
34038
34308
  const onData = (chunk) => {
34039
34309
  buffer += chunk.toString();
@@ -35465,7 +35735,7 @@ function stringify(value, fallback = "[unserializable]") {
35465
35735
  return typeof fallback === "function" ? fallback(value) : fallback;
35466
35736
  }
35467
35737
  }
35468
- function truncate$1(str, max = 0) {
35738
+ function truncate$2(str, max = 0) {
35469
35739
  if (typeof str !== "string" || max === 0) return str;
35470
35740
  return str.length <= max ? str : `${str.slice(0, max)}...`;
35471
35741
  }
@@ -36072,7 +36342,7 @@ var Scope = class Scope {
36072
36342
  const mergedBreadcrumb = {
36073
36343
  timestamp: dateTimestampInSeconds(),
36074
36344
  ...breadcrumb,
36075
- message: breadcrumb.message ? truncate$1(breadcrumb.message, 2048) : breadcrumb.message
36345
+ message: breadcrumb.message ? truncate$2(breadcrumb.message, 2048) : breadcrumb.message
36076
36346
  };
36077
36347
  this._breadcrumbs.push(mergedBreadcrumb);
36078
36348
  if (this._breadcrumbs.length > maxCrumbs) {
@@ -38730,9 +39000,9 @@ function applyClientOptions(event, options) {
38730
39000
  if (!event.release && release) event.release = release;
38731
39001
  if (!event.dist && dist) event.dist = dist;
38732
39002
  const request = event.request;
38733
- if (request?.url && maxValueLength) request.url = truncate$1(request.url, maxValueLength);
39003
+ if (request?.url && maxValueLength) request.url = truncate$2(request.url, maxValueLength);
38734
39004
  if (maxValueLength) event.exception?.values?.forEach((exception) => {
38735
- if (exception.value) exception.value = truncate$1(exception.value, maxValueLength);
39005
+ if (exception.value) exception.value = truncate$2(exception.value, maxValueLength);
38736
39006
  });
38737
39007
  }
38738
39008
  function applyDebugIds(event, stackParser) {
@@ -62349,7 +62619,7 @@ const cacheResponseHook = (span, redisCommand, cmdArgs, response) => {
62349
62619
  [SEMANTIC_ATTRIBUTE_CACHE_KEY]: safeKey
62350
62620
  });
62351
62621
  const spanDescription = safeKey.join(", ");
62352
- span.updateName(_redisOptions.maxCacheKeyLength ? truncate$1(spanDescription, _redisOptions.maxCacheKeyLength) : spanDescription);
62622
+ span.updateName(_redisOptions.maxCacheKeyLength ? truncate$2(spanDescription, _redisOptions.maxCacheKeyLength) : spanDescription);
62353
62623
  };
62354
62624
  //#endregion
62355
62625
  //#region ../../node_modules/.bun/@sentry+node@10.70.0+cfd4f484ffd29da6/node_modules/@sentry/node/build/esm/integrations/tracing/redis/vendored/semconv.js
@@ -65864,7 +66134,7 @@ function normalizeNumberish(value) {
65864
66134
  }
65865
66135
  //#endregion
65866
66136
  //#region package.json
65867
- var version$1 = "0.13.3";
66137
+ var version$1 = "0.13.4";
65868
66138
  //#endregion
65869
66139
  //#region src/sentry.ts
65870
66140
  const SERVER_APP_NAME = "forge-server";
@@ -69400,6 +69670,103 @@ var BrowserTraceCollector = class extends require_Schema$1.Service()("forge/obse
69400
69670
  */
69401
69671
  var ProjectFaviconResolver = class extends require_Schema$1.Service()("forge/project/Services/ProjectFaviconResolver") {};
69402
69672
  //#endregion
69673
+ //#region src/auth/Services/ServerSecretStore.ts
69674
+ var SecretStoreError = class extends require_Schema$1.TaggedError("SecretStoreError") {};
69675
+ var ServerSecretStore = class extends require_Schema$1.Service()("forge/auth/Services/ServerSecretStore") {};
69676
+ //#endregion
69677
+ //#region src/auth/assetUrlToken.ts
69678
+ /**
69679
+ * A credential an image tag can carry.
69680
+ *
69681
+ * `/attachments/*` and `GET /api/project-favicon` both require an authenticated
69682
+ * request, and the clients that render those bytes hand a bare URL to an image
69683
+ * loader that sends no cookie and no Authorization header. Web gets away with
69684
+ * it because the browser attaches the session cookie itself. The phone holds a
69685
+ * bearer token it has no way to attach, so every asset it fetched would 401.
69686
+ *
69687
+ * So the URL carries its own proof. The token is signed with the same server
69688
+ * signing key as the session and WebSocket credentials, and it is deliberately
69689
+ * weaker than either of those: it names one resource, and presenting it grants
69690
+ * that resource and nothing else. A leaked favicon URL is a leaked favicon.
69691
+ */
69692
+ const ASSET_TOKEN_KIND = "asset";
69693
+ const ASSET_URL_TOKEN_QUERY_PARAM = "assetToken";
69694
+ /**
69695
+ * An hour, against a client that re-asks every thirty minutes.
69696
+ *
69697
+ * The vendored client refreshes asset URLs on a thirty-minute interval, so a
69698
+ * shorter life would expire URLs the client still believes are good, and the
69699
+ * failure would show up as an image that worked and then stopped.
69700
+ */
69701
+ const DEFAULT_ASSET_URL_TOKEN_TTL = require_Schema$1.hours(1);
69702
+ const SIGNING_SECRET_NAME$1 = "server-signing-key";
69703
+ const AssetTokenClaims = require_Schema$1.Struct({
69704
+ v: require_Schema$1.Literal(1),
69705
+ kind: require_Schema$1.Literal(ASSET_TOKEN_KIND),
69706
+ sid: require_Schema$1.String,
69707
+ res: require_Schema$1.String,
69708
+ iat: require_Schema$1.Number,
69709
+ exp: require_Schema$1.Number
69710
+ });
69711
+ const decodeAssetTokenClaims = require_Schema$1.decodeUnknownEffect(require_Schema$1.fromJsonString(AssetTokenClaims));
69712
+ /**
69713
+ * The canonical name of one addressable asset.
69714
+ *
69715
+ * Both the mint and the check compute this from their own side: the RPC from
69716
+ * the resource it was asked about, the route from the request it received. They
69717
+ * have to agree exactly, which is the point. A token minted for one attachment
69718
+ * cannot be replayed against another, or against a favicon.
69719
+ *
69720
+ * The key is built from the address, not the logical id, because the address is
69721
+ * what both sides actually hold. The attachments route reads its id straight
69722
+ * out of the URL path without percent-decoding, so keying on the decoded id
69723
+ * would let the two sides disagree about an id that encodes to something else.
69724
+ */
69725
+ function assetResourceKey(resource) {
69726
+ return resource.kind === "attachment" ? `attachment:${resource.urlPathSegment}` : `project-favicon:${resource.cwd}`;
69727
+ }
69728
+ const resolveSigningSecret = require_Schema$1.gen(function* () {
69729
+ const serverConfig = yield* ServerConfig;
69730
+ const secretStore = yield* ServerSecretStore;
69731
+ return resolveConfiguredSigningKey(serverConfig.sessionSigningKey) ?? (yield* secretStore.getOrCreateRandom(SIGNING_SECRET_NAME$1, 32));
69732
+ });
69733
+ const issueAssetUrlToken = (input) => require_Schema$1.gen(function* () {
69734
+ const secret = yield* resolveSigningSecret;
69735
+ const issuedAt = yield* require_Schema$1.now;
69736
+ const expiresAt = require_Schema$1.add(issuedAt, { milliseconds: require_Schema$1.toMillis(input.ttl ?? DEFAULT_ASSET_URL_TOKEN_TTL) });
69737
+ const claims = {
69738
+ v: 1,
69739
+ kind: ASSET_TOKEN_KIND,
69740
+ sid: input.sessionId,
69741
+ res: input.resourceKey,
69742
+ iat: issuedAt.epochMilliseconds,
69743
+ exp: expiresAt.epochMilliseconds
69744
+ };
69745
+ const encodedPayload = base64UrlEncode(JSON.stringify(claims));
69746
+ return {
69747
+ token: `${encodedPayload}.${signPayload(encodedPayload, secret)}`,
69748
+ expiresAt: expiresAt.epochMilliseconds
69749
+ };
69750
+ });
69751
+ /**
69752
+ * Whether this token opens this resource, right now.
69753
+ *
69754
+ * Returns a boolean rather than failing, because every negative answer means
69755
+ * the same thing to the caller: fall through to ordinary request
69756
+ * authentication. A malformed token from a stale client should behave like no
69757
+ * token at all, not like a different error.
69758
+ */
69759
+ const verifyAssetUrlToken = (input) => require_Schema$1.gen(function* () {
69760
+ const [encodedPayload, signature] = input.token.split(".");
69761
+ if (!encodedPayload || !signature) return false;
69762
+ if (!timingSafeEqualBase64Url(signature, signPayload(encodedPayload, yield* resolveSigningSecret))) return false;
69763
+ const claims = yield* decodeAssetTokenClaims(base64UrlDecodeUtf8(encodedPayload)).pipe(require_Schema$1.orElseSucceed(() => null));
69764
+ if (!claims) return false;
69765
+ if (claims.res !== input.resourceKey) return false;
69766
+ const now = yield* require_Schema$1.currentTimeMillis;
69767
+ return claims.exp > now;
69768
+ }).pipe(require_Schema$1.orElseSucceed(() => false));
69769
+ //#endregion
69403
69770
  //#region src/environment/Services/ServerEnvironment.ts
69404
69771
  var ServerEnvironment = class extends require_Schema$1.Service()("forge/environment/Services/ServerEnvironment") {};
69405
69772
  //#endregion
@@ -69674,6 +70041,28 @@ const requireAuthenticatedRequest$1 = require_Schema$1.gen(function* () {
69674
70041
  const request = yield* require_HttpServer.HttpServerRequest;
69675
70042
  yield* (yield* ServerAuth).authenticateHttpRequest(request);
69676
70043
  });
70044
+ /**
70045
+ * Authenticate a request for asset bytes.
70046
+ *
70047
+ * Ordinary request authentication first, so the web app's cookie keeps working
70048
+ * exactly as it did and nothing about these routes changes for it. An asset
70049
+ * token is the fallback, for the clients that render these bytes through an
70050
+ * image loader with no way to attach a credential. See `auth/assetUrlToken.ts`
70051
+ * for why the token is scoped to one resource.
70052
+ */
70053
+ const requireAssetAccess = (resourceKey) => require_Schema$1.gen(function* () {
70054
+ const request = yield* require_HttpServer.HttpServerRequest;
70055
+ const serverAuth = yield* ServerAuth;
70056
+ const url = require_HttpServer.toURL(request);
70057
+ const token = require_Schema$1.isSome(url) ? url.value.searchParams.get(ASSET_URL_TOKEN_QUERY_PARAM) : null;
70058
+ if (token && token.trim().length > 0) {
70059
+ if (yield* verifyAssetUrlToken({
70060
+ token,
70061
+ resourceKey
70062
+ })) return;
70063
+ }
70064
+ yield* serverAuth.authenticateHttpRequest(request);
70065
+ });
69677
70066
  const serverEnvironmentRouteLayer = add$1("GET", "/.well-known/forge/environment", require_Schema$1.gen(function* () {
69678
70067
  const descriptor = yield* require_Schema$1.service(ServerEnvironment).pipe(require_Schema$1.flatMap((serverEnvironment) => serverEnvironment.getDescriptor));
69679
70068
  return require_HttpServer.jsonUnsafe(descriptor, { status: 200 });
@@ -69743,13 +70132,16 @@ const otlpTracesProxyRouteLayer = add$1("POST", OTLP_TRACES_PROXY_PATH, require_
69743
70132
  captureServerError(cause, { route: "/api/observability/v1/traces" });
69744
70133
  }).pipe(require_Schema$1.andThen(() => require_Schema$1.logError("otlp-traces unhandled cause", { cause }))))));
69745
70134
  const attachmentsRouteLayer = add$1("GET", `${ATTACHMENTS_ROUTE_PREFIX}/*`, require_Schema$1.gen(function* () {
69746
- yield* requireAuthenticatedRequest$1;
69747
70135
  const request = yield* require_HttpServer.HttpServerRequest;
69748
70136
  const url = require_HttpServer.toURL(request);
69749
70137
  if (require_Schema$1.isNone(url)) return require_HttpServer.text("Bad Request", { status: 400 });
69750
70138
  const config = yield* ServerConfig;
69751
70139
  const normalizedRelativePath = normalizeAttachmentRelativePath(url.value.pathname.slice(12));
69752
70140
  if (!normalizedRelativePath) return require_HttpServer.text("Invalid attachment path", { status: 400 });
70141
+ yield* requireAssetAccess(assetResourceKey({
70142
+ kind: "attachment",
70143
+ urlPathSegment: normalizedRelativePath
70144
+ }));
69753
70145
  const isIdLookup = !normalizedRelativePath.includes("/") && !normalizedRelativePath.includes(".");
69754
70146
  const filePath = isIdLookup ? resolveAttachmentPathById({
69755
70147
  attachmentsDir: config.attachmentsDir,
@@ -69768,11 +70160,14 @@ const attachmentsRouteLayer = add$1("GET", `${ATTACHMENTS_ROUTE_PREFIX}/*`, requ
69768
70160
  }).pipe(require_Schema$1.catchTag("AuthError", respondToAuthError)));
69769
70161
  const projectFaviconRouteLayer = add$1("GET", "/api/project-favicon", require_Schema$1.gen(function* () {
69770
70162
  const request = yield* require_HttpServer.HttpServerRequest;
69771
- yield* (yield* ServerAuth).authenticateHttpRequest(request);
69772
70163
  const url = require_HttpServer.toURL(request);
69773
70164
  if (require_Schema$1.isNone(url)) return require_HttpServer.text("Bad Request", { status: 400 });
69774
70165
  const projectCwd = url.value.searchParams.get("cwd");
69775
70166
  if (!projectCwd) return require_HttpServer.text("Missing cwd parameter", { status: 400 });
70167
+ yield* requireAssetAccess(assetResourceKey({
70168
+ kind: "project-favicon",
70169
+ cwd: projectCwd
70170
+ }));
69776
70171
  const faviconFilePath = yield* (yield* ProjectFaviconResolver).resolvePath(projectCwd);
69777
70172
  if (!faviconFilePath) return require_HttpServer.empty({
69778
70173
  status: 204,
@@ -77504,6 +77899,195 @@ require_Schema$1.succeed$1(TraceDiagnostics, TraceDiagnostics.of({ read: (option
77504
77899
  }
77505
77900
  })) }));
77506
77901
  //#endregion
77902
+ //#region src/assets/createAssetUrl.ts
77903
+ const PROJECT_FAVICON_ROUTE = "/api/project-favicon";
77904
+ /**
77905
+ * Turn a resource into an address a client can fetch.
77906
+ *
77907
+ * Nothing new is served here. `/attachments/*` and `GET /api/project-favicon`
77908
+ * already return these bytes; what did not exist was the step in between, and
77909
+ * without it a client holding an attachment id or a workspace root has no way
77910
+ * to name the URL. The vendored mobile client asks this for every project on
77911
+ * its thread list, so the gap was not an edge case, it was the most visited
77912
+ * screen.
77913
+ *
77914
+ * The URL is relative so it resolves against whichever base the client is
77915
+ * already talking to, and it carries a token because the clients that render
77916
+ * these bytes hand the URL to an image loader that attaches no credential of
77917
+ * its own. See `auth/assetUrlToken.ts`.
77918
+ */
77919
+ const createAssetUrl = (sessionId, resource) => require_Schema$1.gen(function* () {
77920
+ if (resource._tag === "workspace-file") return yield* new AssetWorkspaceResolutionError({
77921
+ resource,
77922
+ cause: "Workspace files are read over projects.readFile, not by URL."
77923
+ });
77924
+ const address = resolveAddress(resource);
77925
+ const issued = yield* issueAssetUrlToken({
77926
+ sessionId,
77927
+ resourceKey: address.resourceKey
77928
+ }).pipe(require_Schema$1.mapError((cause) => new AssetSigningKeyLoadError({
77929
+ resource,
77930
+ cause
77931
+ })));
77932
+ return {
77933
+ relativeUrl: `${address.path}?${address.query}assetToken=${encodeURIComponent(issued.token)}`,
77934
+ expiresAt: issued.expiresAt,
77935
+ ...resource._tag === "project-favicon" && resource.path ? { sourcePath: resource.path } : {}
77936
+ };
77937
+ });
77938
+ function resolveAddress(resource) {
77939
+ if (resource._tag === "attachment") {
77940
+ const urlPathSegment = encodeURIComponent(resource.attachmentId);
77941
+ return {
77942
+ resourceKey: assetResourceKey({
77943
+ kind: "attachment",
77944
+ urlPathSegment
77945
+ }),
77946
+ path: `${ATTACHMENTS_ROUTE_PREFIX}/${urlPathSegment}`,
77947
+ query: ""
77948
+ };
77949
+ }
77950
+ return {
77951
+ resourceKey: assetResourceKey({
77952
+ kind: "project-favicon",
77953
+ cwd: resource.cwd
77954
+ }),
77955
+ path: PROJECT_FAVICON_ROUTE,
77956
+ query: `cwd=${encodeURIComponent(resource.cwd)}&`
77957
+ };
77958
+ }
77959
+ //#endregion
77960
+ //#region src/cmmd/submitFeedback.ts
77961
+ /** The intake the web app's feedback dialog already posts to, reached directly. */
77962
+ const CMMD_FEEDBACK_PATH = "/api/forge/context/feedback";
77963
+ /**
77964
+ * The intake answers with the task it created.
77965
+ *
77966
+ * Web reaches it through the proxy registry, which unwraps `data` before the
77967
+ * caller sees it; this calls the gateway directly and does not, so both shapes
77968
+ * are read here rather than pinning the one that happens to arrive today.
77969
+ */
77970
+ function readFeedbackId(response) {
77971
+ if (!response || typeof response !== "object") return null;
77972
+ const record = response;
77973
+ const envelope = record["data"];
77974
+ const id = (envelope && typeof envelope === "object" ? envelope : record)["id"];
77975
+ if (typeof id === "number") return String(id);
77976
+ if (typeof id === "string" && id.trim().length > 0) return id.trim();
77977
+ return null;
77978
+ }
77979
+ /**
77980
+ * File feedback against the caller's own CMMD account.
77981
+ *
77982
+ * The session's CMMD JWT is what scopes it. A server with no CMMD connection
77983
+ * has nowhere to send feedback and says so in a sentence, because the caller
77984
+ * draws this text straight into the thread the person is looking at.
77985
+ */
77986
+ function submitCmmdFeedbackForSession(input) {
77987
+ return require_Schema$1.gen(function* () {
77988
+ const jwt = yield* resolveCmmdJwtForSession({
77989
+ sessionId: input.sessionId,
77990
+ ...input.subject === void 0 ? {} : { subject: input.subject },
77991
+ logPrefix: "cmmd-feedback"
77992
+ }).pipe(require_Schema$1.orElseSucceed(() => null));
77993
+ if (!jwt) return {
77994
+ ok: false,
77995
+ reason: "This server is not connected to CMMD, so there is nowhere to send feedback."
77996
+ };
77997
+ const body = {
77998
+ type: "bug",
77999
+ title: input.title,
78000
+ source: "forge",
78001
+ priority: "medium",
78002
+ ...input.description ? { description: input.description } : {},
78003
+ ...input.senderName ? { senderName: input.senderName } : {}
78004
+ };
78005
+ const response = yield* require_Schema$1.tryPromise(() => cmmdGateway.request({
78006
+ method: "POST",
78007
+ jwt,
78008
+ path: CMMD_FEEDBACK_PATH,
78009
+ body
78010
+ })).pipe(require_Schema$1.orElseSucceed(() => null));
78011
+ if (response === null) return {
78012
+ ok: false,
78013
+ reason: "The feedback service did not accept the report. Please try again."
78014
+ };
78015
+ const feedbackId = readFeedbackId(response);
78016
+ if (feedbackId === null) return {
78017
+ ok: false,
78018
+ reason: "The feedback service answered without recording a report."
78019
+ };
78020
+ return {
78021
+ ok: true,
78022
+ feedbackId
78023
+ };
78024
+ });
78025
+ }
78026
+ //#endregion
78027
+ //#region src/provider/threadFeedback.ts
78028
+ const FEEDBACK_TITLE_MAX_CHARS = 120;
78029
+ function firstLine(text) {
78030
+ return text.split("\n", 1)[0]?.trim() ?? "";
78031
+ }
78032
+ function truncate$1(text, max) {
78033
+ return text.length <= max ? text : `${text.slice(0, max - 1).trimEnd()}…`;
78034
+ }
78035
+ /**
78036
+ * A title someone triaging the intake can act on.
78037
+ *
78038
+ * What the person typed after `/feedback` first, because it is the only part
78039
+ * they wrote. The thread's title is the fallback, and a bare thread id is the
78040
+ * last resort, since a queue of "Forge feedback" rows tells a triager nothing.
78041
+ */
78042
+ function feedbackTitle(input) {
78043
+ const reason = input.reason ? firstLine(input.reason) : "";
78044
+ if (reason.length > 0) return truncate$1(reason, FEEDBACK_TITLE_MAX_CHARS);
78045
+ const threadTitle = input.threadTitle?.trim() ?? "";
78046
+ if (threadTitle.length > 0) return truncate$1(`Forge feedback: ${threadTitle}`, FEEDBACK_TITLE_MAX_CHARS);
78047
+ return truncate$1(`Forge feedback: thread ${input.threadId}`, FEEDBACK_TITLE_MAX_CHARS);
78048
+ }
78049
+ /** The body, which always names the thread so the report can be traced back to it. */
78050
+ function feedbackDescription(input) {
78051
+ const lines = [`Filed from Forge thread \`${input.threadId}\`.`];
78052
+ if (input.threadTitle?.trim()) lines.push(`Thread title: ${input.threadTitle.trim()}`);
78053
+ if (input.reason?.trim()) lines.push("", input.reason.trim());
78054
+ return lines.join("\n");
78055
+ }
78056
+ /**
78057
+ * `/feedback`, served.
78058
+ *
78059
+ * The vendored client declared this method and called it; Forge answered
78060
+ * nothing, so the slash command printed the transport's own "unknown request
78061
+ * tag" string into the thread. Routing it to the CMMD intake the web feedback
78062
+ * dialog already uses means one report format and one destination, whichever
78063
+ * surface it came from.
78064
+ *
78065
+ * Every failure carries a sentence rather than a cause, because the client
78066
+ * draws `message` straight into the thread as an assistant reply.
78067
+ */
78068
+ const uploadThreadFeedback = (orchestrationEngine, sessionId, subject, input) => require_Schema$1.gen(function* () {
78069
+ const threadTitle = (yield* orchestrationEngine.getReadModel().pipe(require_Schema$1.orElseSucceed(() => null)))?.threads.find((thread) => thread.id === input.threadId)?.title;
78070
+ const outcome = yield* submitCmmdFeedbackForSession({
78071
+ sessionId,
78072
+ ...subject === void 0 ? {} : { subject },
78073
+ title: feedbackTitle({
78074
+ ...input,
78075
+ threadId: input.threadId,
78076
+ ...threadTitle ? { threadTitle } : {}
78077
+ }),
78078
+ description: feedbackDescription({
78079
+ ...input,
78080
+ threadId: input.threadId,
78081
+ ...threadTitle ? { threadTitle } : {}
78082
+ })
78083
+ });
78084
+ if (!outcome.ok) return yield* new ProviderUploadFeedbackError({
78085
+ threadId: input.threadId,
78086
+ reason: outcome.reason
78087
+ });
78088
+ return { feedbackId: outcome.feedbackId };
78089
+ });
78090
+ //#endregion
77507
78091
  //#region src/cloudRuntime/listCloudRuntimes.ts
77508
78092
  /**
77509
78093
  * List the caller's Forge Cloud runtimes, through CMMD, as the caller.
@@ -87559,95 +88143,278 @@ function resolveGitHubCloneDir(baseDir, repoFullName, targetDir) {
87559
88143
  const repoName = repoFullName.split("/")[1] ?? repoFullName;
87560
88144
  return targetDir ?? (0, node_path.join)(baseDir, "Projects", repoName);
87561
88145
  }
87562
- const githubCloneRouteLayer = add$1("POST", "/.well-known/forge/github/clone", require_Schema$1.gen(function* () {
87563
- const request = yield* require_HttpServer.HttpServerRequest;
87564
- const session = yield* (yield* ServerAuth).authenticateHttpRequest(request);
87565
- const config = yield* ServerConfig;
87566
- if (session.method === "proxy-asserted") {
87567
- const forwardedJwt = normalizeForwardedCmmdJwt(request.headers);
87568
- if (forwardedJwt !== void 0) yield* setCommandJwtForSession(session.sessionId, forwardedJwt).pipe(require_Schema$1.ignore({ log: true }));
87569
- }
87570
- const runtimeGrantAuth = resolveRuntimeGitHubGrantAuth(config, request.headers);
87571
- const jwt = runtimeGrantAuth ? null : yield* resolveJwt(session.sessionId, session.subject);
87572
- if (!runtimeGrantAuth && !jwt) return json$6({ error: "CMMD not connected" }, 401);
87573
- const rawBody = yield* request.json;
87574
- const parsed = require_Schema$1.decodeUnknownExit(CloneBodySchema)(rawBody);
87575
- if (parsed._tag === "Failure") return json$6({ error: "repo_full_name is required" }, 400);
87576
- const { repo_full_name, target_dir } = parsed.value;
87577
- let token = yield* fetchInstallationTokenForAuth(runtimeGrantAuth ?? {
87578
- kind: "cmmd-jwt",
87579
- jwt
87580
- }, repo_full_name);
87581
- if (!runtimeGrantAuth && !token.ok && shouldRetryWithRefreshedCmmdJwt(token.status)) {
87582
- const refreshedJwt = yield* resolveJwt(session.sessionId, session.subject, true);
87583
- if (refreshedJwt && refreshedJwt !== jwt) token = yield* fetchInstallationTokenForAuth({
88146
+ /**
88147
+ * The public clone URLs for a repository.
88148
+ *
88149
+ * Never derived from an authenticated remote: `buildAuthenticatedRemoteUrl`
88150
+ * embeds an installation token in the URL, and a token that reaches a client is
88151
+ * a token that reaches wherever the client writes it down.
88152
+ */
88153
+ function describeGitHubRepository(nameWithOwner) {
88154
+ return {
88155
+ nameWithOwner,
88156
+ url: `https://github.com/${nameWithOwner}`,
88157
+ sshUrl: `git@github.com:${nameWithOwner}.git`
88158
+ };
88159
+ }
88160
+ /**
88161
+ * Resolve `owner/repo` against the repositories CMMD says this caller may use.
88162
+ *
88163
+ * This asks the same brokered listing the web repository picker is populated
88164
+ * from, rather than github.com directly, so "found" here means the same thing
88165
+ * it means everywhere else in Forge: CMMD will vend this caller a token for it.
88166
+ * A repository that exists but that the caller cannot reach is a 404, which is
88167
+ * the answer they can act on.
88168
+ */
88169
+ function lookupGitHubRepositoryForSession(input) {
88170
+ return require_Schema$1.gen(function* () {
88171
+ const requested = input.repository.trim().replace(/^\/+|\/+$/g, "");
88172
+ if (!/^[^/\s]+\/[^/\s]+$/.test(requested)) return {
88173
+ status: 400,
88174
+ repository: null,
88175
+ error: "Enter a repository as owner/name."
88176
+ };
88177
+ const jwt = input.runtimeGrantAuth ? null : yield* resolveJwt(input.sessionId, input.subject);
88178
+ if (!input.runtimeGrantAuth && !jwt) return {
88179
+ status: 401,
88180
+ repository: null,
88181
+ error: "CMMD not connected"
88182
+ };
88183
+ const auth = input.runtimeGrantAuth ?? {
87584
88184
  kind: "cmmd-jwt",
87585
- jwt: refreshedJwt
87586
- }, repo_full_name);
87587
- }
87588
- if (!token.ok) return json$6(gitHubTokenFailureBody(token.status, token.error), token.status);
87589
- const gitToken = token.token;
87590
- const cloneDir = resolveGitHubCloneDir(config.baseDir, repo_full_name, target_dir);
87591
- yield* require_Schema$1.promise(() => (0, node_fs_promises.mkdir)((0, node_path.join)(cloneDir, ".."), { recursive: true }).catch(() => void 0));
87592
- if ((0, node_fs.existsSync)(cloneDir)) {
87593
- const workTree = yield* require_Schema$1.promise(() => runProcess("git", [
88185
+ jwt
88186
+ };
88187
+ const searchTerm = requested.split("/")[1] ?? requested;
88188
+ let result = yield* require_Schema$1.promise(() => fetchGitHubReposForAuth(auth, {
88189
+ search: searchTerm,
88190
+ organizationId: input.organizationId ?? ""
88191
+ }));
88192
+ if (!input.runtimeGrantAuth && shouldRetryWithRefreshedCmmdJwt(result.status)) {
88193
+ const refreshedJwt = yield* resolveJwt(input.sessionId, input.subject, true);
88194
+ if (refreshedJwt && refreshedJwt !== jwt) result = yield* require_Schema$1.promise(() => fetchGitHubReposForAuth({
88195
+ kind: "cmmd-jwt",
88196
+ jwt: refreshedJwt
88197
+ }, {
88198
+ search: searchTerm,
88199
+ organizationId: input.organizationId ?? ""
88200
+ }));
88201
+ }
88202
+ if (result.status !== 200) {
88203
+ const body = result.body;
88204
+ return {
88205
+ status: result.status,
88206
+ repository: null,
88207
+ error: typeof body.error === "string" ? body.error : "GitHub repositories are unavailable."
88208
+ };
88209
+ }
88210
+ const match = selectGitHubRepositoryMatch(result.body, requested);
88211
+ if (match === null) return {
88212
+ status: 404,
88213
+ repository: null,
88214
+ error: `Repository ${requested} was not found in the GitHub accounts connected to Forge.`
88215
+ };
88216
+ return {
88217
+ status: 200,
88218
+ repository: describeGitHubRepository(match),
88219
+ error: null
88220
+ };
88221
+ });
88222
+ }
88223
+ /**
88224
+ * Find the one repository in a brokered listing whose full name is the one
88225
+ * asked for.
88226
+ *
88227
+ * Exact and case-insensitive. The listing is a *search*, so it comes back with
88228
+ * near misses in it: asking for `owner/forge` and being handed `someone-else/
88229
+ * forge` because it sorted first is how a person ends up cloning a stranger's
88230
+ * repository, and a prefix match would do exactly that.
88231
+ */
88232
+ function selectGitHubRepositoryMatch(body, requested) {
88233
+ const repos = body?.repos ?? [];
88234
+ const wanted = requested.trim().toLowerCase();
88235
+ for (const repo of repos) if (typeof repo.full_name === "string" && repo.full_name.toLowerCase() === wanted) return repo.full_name;
88236
+ return null;
88237
+ }
88238
+ /**
88239
+ * The repository a clone request is really for, or `null` if there is not one.
88240
+ *
88241
+ * A caller may name `owner/repo`, or paste a clone URL and let the server work
88242
+ * it out. Anything that does not resolve to a GitHub repository returns `null`
88243
+ * and must be refused rather than cloned: Forge clones through a CMMD-brokered
88244
+ * installation token for a named repository, and "just run `git clone` on this
88245
+ * URL instead" would be a second, unbrokered way to write to disk.
88246
+ */
88247
+ function resolveGitHubCloneTarget(input) {
88248
+ const named = input.repository?.trim();
88249
+ if (named && /^[^/\s]+\/[^/\s]+$/.test(named)) return named;
88250
+ if (named) return null;
88251
+ const remoteUrl = input.remoteUrl?.trim();
88252
+ return remoteUrl ? parseRepoFullNameFromRemoteUrl(remoteUrl) : null;
88253
+ }
88254
+ /**
88255
+ * Clone `repoFullName` for an authenticated session.
88256
+ *
88257
+ * Authorization is the caller's own CMMD connection: the session's JWT (or a
88258
+ * runtime grant, when this VM is running under one) is exchanged with CMMD for
88259
+ * an installation token scoped to that one repository. A session with no CMMD
88260
+ * connection gets 401 and no clone; a repository CMMD will not issue a token
88261
+ * for gets CMMD's own refusal, not a `git clone` attempt.
88262
+ */
88263
+ function runGitHubCloneForSession(input) {
88264
+ return require_Schema$1.gen(function* () {
88265
+ const { config, repoFullName, targetDir } = input;
88266
+ const token = yield* acquireInstallationTokenForSession(input);
88267
+ if (!token.ok) return token.outcome;
88268
+ const cloneDir = resolveGitHubCloneDir(config.baseDir, repoFullName, targetDir);
88269
+ yield* require_Schema$1.promise(() => (0, node_fs_promises.mkdir)((0, node_path.join)(cloneDir, ".."), { recursive: true }).catch(() => void 0));
88270
+ const target = {
88271
+ cloneDir,
88272
+ repoFullName,
88273
+ token: token.token,
88274
+ config
88275
+ };
88276
+ return (0, node_fs.existsSync)(cloneDir) ? yield* adoptExistingGitHubClone(target) : yield* cloneGitHubRepositoryInto(target);
88277
+ });
88278
+ }
88279
+ /**
88280
+ * The installation token for one repository, on the caller's own authority.
88281
+ *
88282
+ * A runtime grant is used as-is. A session resolves its CMMD JWT and retries
88283
+ * once with a refreshed one when CMMD answers 401 or 403, because the cached
88284
+ * JWT may simply have expired. The failure branch is already the HTTP outcome,
88285
+ * so a caller returns it rather than translating it.
88286
+ */
88287
+ function acquireInstallationTokenForSession(input) {
88288
+ return require_Schema$1.gen(function* () {
88289
+ const { runtimeGrantAuth, repoFullName } = input;
88290
+ const jwt = runtimeGrantAuth ? null : yield* resolveJwt(input.sessionId, input.subject);
88291
+ if (!runtimeGrantAuth && !jwt) return {
88292
+ ok: false,
88293
+ outcome: {
88294
+ status: 401,
88295
+ body: { error: "CMMD not connected" }
88296
+ }
88297
+ };
88298
+ let token = yield* fetchInstallationTokenForAuth(runtimeGrantAuth ?? {
88299
+ kind: "cmmd-jwt",
88300
+ jwt
88301
+ }, repoFullName);
88302
+ if (!runtimeGrantAuth && !token.ok && shouldRetryWithRefreshedCmmdJwt(token.status)) {
88303
+ const refreshedJwt = yield* resolveJwt(input.sessionId, input.subject, true);
88304
+ if (refreshedJwt && refreshedJwt !== jwt) token = yield* fetchInstallationTokenForAuth({
88305
+ kind: "cmmd-jwt",
88306
+ jwt: refreshedJwt
88307
+ }, repoFullName);
88308
+ }
88309
+ if (!token.ok) return {
88310
+ ok: false,
88311
+ outcome: {
88312
+ status: token.status,
88313
+ body: gitHubTokenFailureBody(token.status, token.error)
88314
+ }
88315
+ };
88316
+ return {
88317
+ ok: true,
88318
+ token: token.token
88319
+ };
88320
+ });
88321
+ }
88322
+ /**
88323
+ * A directory already sits where the clone would go. Adopt it when it is a
88324
+ * checkout of the same repository, so a second "add project" is a no-op with
88325
+ * fresh credentials rather than a failure; refuse otherwise, naming the
88326
+ * folder, because overwriting it would destroy something the person made.
88327
+ */
88328
+ function adoptExistingGitHubClone(target) {
88329
+ return require_Schema$1.gen(function* () {
88330
+ const { cloneDir, repoFullName } = target;
88331
+ const git = (args) => require_Schema$1.promise(() => runProcess("git", [
87594
88332
  "-C",
87595
88333
  cloneDir,
87596
- "rev-parse",
87597
- "--is-inside-work-tree"
88334
+ ...args
87598
88335
  ], {
87599
88336
  timeoutMs: 1e4,
87600
88337
  allowNonZeroExit: true,
87601
88338
  env: process.env
87602
88339
  }));
87603
- const remote = yield* require_Schema$1.promise(() => runProcess("git", [
87604
- "-C",
87605
- cloneDir,
88340
+ const workTree = yield* git(["rev-parse", "--is-inside-work-tree"]);
88341
+ const remote = yield* git([
87606
88342
  "remote",
87607
88343
  "get-url",
87608
88344
  "origin"
87609
- ], {
87610
- timeoutMs: 1e4,
87611
- allowNonZeroExit: true,
87612
- env: process.env
87613
- }));
87614
- if (workTree.code === 0 && workTree.stdout.trim() === "true" && remote.code === 0 && isGitHubRemoteForRepo(remote.stdout.trim(), repo_full_name)) {
87615
- yield* applyGitCredentials({
87616
- cloneDir,
87617
- repoFullName: repo_full_name,
87618
- token: gitToken,
87619
- mode: config.forgeGitCredentialCache ? "cache" : "embed"
87620
- });
87621
- return json$6({
88345
+ ]);
88346
+ if (!(workTree.code === 0 && workTree.stdout.trim() === "true" && remote.code === 0 && isGitHubRemoteForRepo(remote.stdout.trim(), repoFullName))) return {
88347
+ status: 409,
88348
+ body: {
88349
+ error: "A project folder with that name already exists, but it is not the selected GitHub repository. Rename the import, remove the VM folder, or choose a different repository.",
88350
+ local_path: cloneDir
88351
+ }
88352
+ };
88353
+ yield* applyGitCredentialsFor(target);
88354
+ return {
88355
+ status: 200,
88356
+ body: {
87622
88357
  local_path: cloneDir,
87623
88358
  existing: true
87624
- });
87625
- }
87626
- return json$6({
87627
- error: "A project folder with that name already exists, but it is not the selected GitHub repository. Rename the import, remove the VM folder, or choose a different repository.",
87628
- local_path: cloneDir
87629
- }, 409);
87630
- }
87631
- const result = yield* require_Schema$1.promise(() => runProcess("git", buildAuthenticatedGitCloneArgs(repo_full_name, cloneDir), {
87632
- timeoutMs: 12e4,
87633
- allowNonZeroExit: true,
87634
- env: {
87635
- ...process.env,
87636
- CMMD_FORGE_GIT_TOKEN: gitToken,
87637
- GIT_TERMINAL_PROMPT: "0"
88359
+ }
88360
+ };
88361
+ });
88362
+ }
88363
+ function cloneGitHubRepositoryInto(target) {
88364
+ return require_Schema$1.gen(function* () {
88365
+ const { cloneDir, repoFullName, token } = target;
88366
+ const result = yield* require_Schema$1.promise(() => runProcess("git", buildAuthenticatedGitCloneArgs(repoFullName, cloneDir), {
88367
+ timeoutMs: 12e4,
88368
+ allowNonZeroExit: true,
88369
+ env: {
88370
+ ...process.env,
88371
+ CMMD_FORGE_GIT_TOKEN: token,
88372
+ GIT_TERMINAL_PROMPT: "0"
88373
+ }
88374
+ }));
88375
+ if (result.code !== 0) {
88376
+ const stderr = sanitizeGitCredentialText(result.stderr).trim().slice(0, 500);
88377
+ return {
88378
+ status: 500,
88379
+ body: { error: `git clone failed${stderr.length > 0 ? `: ${stderr}` : ""}` }
88380
+ };
87638
88381
  }
87639
- }));
87640
- if (result.code !== 0) {
87641
- const stderr = sanitizeGitCredentialText(result.stderr).trim().slice(0, 500);
87642
- return json$6({ error: `git clone failed${stderr.length > 0 ? `: ${stderr}` : ""}` }, 500);
88382
+ yield* applyGitCredentialsFor(target);
88383
+ return {
88384
+ status: 200,
88385
+ body: { local_path: cloneDir }
88386
+ };
88387
+ });
88388
+ }
88389
+ function applyGitCredentialsFor(target) {
88390
+ return applyGitCredentials({
88391
+ cloneDir: target.cloneDir,
88392
+ repoFullName: target.repoFullName,
88393
+ token: target.token,
88394
+ mode: target.config.forgeGitCredentialCache ? "cache" : "embed"
88395
+ });
88396
+ }
88397
+ const githubCloneRouteLayer = add$1("POST", "/.well-known/forge/github/clone", require_Schema$1.gen(function* () {
88398
+ const request = yield* require_HttpServer.HttpServerRequest;
88399
+ const session = yield* (yield* ServerAuth).authenticateHttpRequest(request);
88400
+ const config = yield* ServerConfig;
88401
+ if (session.method === "proxy-asserted") {
88402
+ const forwardedJwt = normalizeForwardedCmmdJwt(request.headers);
88403
+ if (forwardedJwt !== void 0) yield* setCommandJwtForSession(session.sessionId, forwardedJwt).pipe(require_Schema$1.ignore({ log: true }));
87643
88404
  }
87644
- yield* applyGitCredentials({
87645
- cloneDir,
88405
+ const rawBody = yield* request.json;
88406
+ const parsed = require_Schema$1.decodeUnknownExit(CloneBodySchema)(rawBody);
88407
+ if (parsed._tag === "Failure") return json$6({ error: "repo_full_name is required" }, 400);
88408
+ const { repo_full_name, target_dir } = parsed.value;
88409
+ const outcome = yield* runGitHubCloneForSession({
88410
+ config,
88411
+ sessionId: session.sessionId,
88412
+ subject: session.subject,
88413
+ runtimeGrantAuth: resolveRuntimeGitHubGrantAuth(config, request.headers),
87646
88414
  repoFullName: repo_full_name,
87647
- token: gitToken,
87648
- mode: config.forgeGitCredentialCache ? "cache" : "embed"
88415
+ targetDir: target_dir
87649
88416
  });
87650
- return json$6({ local_path: cloneDir });
88417
+ return json$6(outcome.body, outcome.status);
87651
88418
  }).pipe(require_Schema$1.catchTag("AuthError", respondToAuthError)));
87652
88419
  const githubCloneJobStartRouteLayer = add$1("POST", "/.well-known/forge/github/clone-jobs", require_Schema$1.gen(function* () {
87653
88420
  const request = yield* require_HttpServer.HttpServerRequest;
@@ -104469,7 +105236,9 @@ const REJECT_UNCERTAIN_METHODS = new Set([
104469
105236
  WS_METHODS.pullRequestsComment,
104470
105237
  WS_METHODS.pullRequestsSubmitReview,
104471
105238
  WS_METHODS.pullRequestsReplyToThread,
104472
- WS_METHODS.projectCheckoutDeletionExecute
105239
+ WS_METHODS.projectCheckoutDeletionExecute,
105240
+ WS_METHODS.providerUploadFeedback,
105241
+ WS_METHODS.sourceControlCloneRepository
104473
105242
  ]);
104474
105243
  function classifyInterruptedRelayCommand(input) {
104475
105244
  if ((input.method === WS_METHODS.gitCreateBranch || input.method === WS_METHODS.vcsCreateRef) && input.deliveryClass === "create-command") return { kind: "reconcile-git-create-branch" };
@@ -105401,7 +106170,77 @@ const makeWsRpcLayer = (currentSessionId, currentUserId) => WsRpcGroup.toLayer(r
105401
106170
  payload: initialSnapshot
105402
106171
  }), liveEvents);
105403
106172
  }), { "rpc.aggregate": "auth" }),
105404
- [WS_METHODS.subscribeWorkspaceReadiness]: (_input) => currentUserId === void 0 ? require_Path.empty : workspaceReadinessState.subscribe(currentUserId)
106173
+ [WS_METHODS.subscribeWorkspaceReadiness]: (_input) => currentUserId === void 0 ? require_Path.empty : workspaceReadinessState.subscribe(currentUserId),
106174
+ [WS_METHODS.assetsCreateUrl]: (input) => observeRpcEffect(WS_METHODS.assetsCreateUrl, createAssetUrl(currentSessionId, input.resource), { "rpc.aggregate": "assets" }),
106175
+ [WS_METHODS.providerUploadFeedback]: (input) => observeRpcEffect(WS_METHODS.providerUploadFeedback, uploadThreadFeedback(orchestrationEngine, currentSessionId, currentUserId, input), { "rpc.aggregate": "provider" }),
106176
+ [ORCHESTRATION_WS_METHODS.searchThreads]: (input) => observeRpcEffect(ORCHESTRATION_WS_METHODS.searchThreads, require_Schema$1.gen(function* () {
106177
+ yield* startup.awaitCommandReady;
106178
+ return yield* projectionSnapshotQuery.searchThreads(input);
106179
+ }).pipe(require_Schema$1.mapError((cause) => new OrchestrationSearchThreadsError({
106180
+ message: "Failed to search threads",
106181
+ cause
106182
+ }))), { "rpc.aggregate": "orchestration" }),
106183
+ [WS_METHODS.sourceControlLookupRepository]: (input) => observeRpcEffect(WS_METHODS.sourceControlLookupRepository, require_Schema$1.gen(function* () {
106184
+ if (input.provider !== "github") return yield* new SourceControlRepositoryError({
106185
+ provider: input.provider,
106186
+ operation: "lookupRepository",
106187
+ detail: `Forge can only look up GitHub repositories. ${input.provider} is not connected.`
106188
+ });
106189
+ if (currentUserId === void 0) return yield* new SourceControlRepositoryError({
106190
+ provider: "github",
106191
+ operation: "lookupRepository",
106192
+ detail: "Sign in to Forge before looking up a repository."
106193
+ });
106194
+ const outcome = yield* lookupGitHubRepositoryForSession({
106195
+ sessionId: currentSessionId,
106196
+ subject: currentUserId,
106197
+ runtimeGrantAuth: null,
106198
+ repository: input.repository
106199
+ });
106200
+ if (outcome.repository === null) return yield* new SourceControlRepositoryError({
106201
+ provider: "github",
106202
+ operation: "lookupRepository",
106203
+ detail: outcome.error ?? "Repository lookup failed."
106204
+ });
106205
+ return {
106206
+ provider: "github",
106207
+ ...outcome.repository
106208
+ };
106209
+ }), { "rpc.aggregate": "sourceControl" }),
106210
+ [WS_METHODS.sourceControlCloneRepository]: (input) => observeRpcEffect(WS_METHODS.sourceControlCloneRepository, require_Schema$1.gen(function* () {
106211
+ const provider = input.provider ?? "github";
106212
+ const repoFullName = resolveGitHubCloneTarget(input);
106213
+ const refuse = (detail) => new SourceControlRepositoryError({
106214
+ provider,
106215
+ operation: "cloneRepository",
106216
+ detail
106217
+ });
106218
+ if (provider !== "github") return yield* refuse(`Forge can only clone GitHub repositories. ${provider} is not connected.`);
106219
+ if (currentUserId === void 0) return yield* refuse("Sign in to Forge before cloning a repository.");
106220
+ if (repoFullName === null) return yield* refuse("Name the repository as owner/name, or paste a github.com clone URL.");
106221
+ const outcome = yield* runGitHubCloneForSession({
106222
+ config,
106223
+ sessionId: currentSessionId,
106224
+ subject: currentUserId,
106225
+ runtimeGrantAuth: null,
106226
+ repoFullName,
106227
+ targetDir: input.destinationPath
106228
+ });
106229
+ const localPath = outcome.body.local_path;
106230
+ if (outcome.status !== 200 || typeof localPath !== "string") {
106231
+ const detail = outcome.body.error;
106232
+ return yield* refuse(typeof detail === "string" ? detail : "Cloning the repository failed.");
106233
+ }
106234
+ const repository = describeGitHubRepository(repoFullName);
106235
+ return {
106236
+ cwd: localPath,
106237
+ remoteUrl: repository.url,
106238
+ repository: {
106239
+ provider: "github",
106240
+ ...repository
106241
+ }
106242
+ };
106243
+ }), { "rpc.aggregate": "sourceControl" })
105405
106244
  });
105406
106245
  }));
105407
106246
  const websocketRpcRouteLayer = require_Schema$1.unwrap(require_Schema$1.succeed(require_Schema$1.mergeAll(add$1("GET", "/ws", require_Schema$1.gen(function* () {
@@ -116489,8 +117328,8 @@ require_Schema$1.effectDiscard(runMigrations());
116489
117328
  //#endregion
116490
117329
  //#region src/persistence/Layers/Sqlite.ts
116491
117330
  const defaultSqliteClientLoaders = {
116492
- bun: () => Promise.resolve().then(() => require("./SqliteClient-B08FzZM4.cjs")),
116493
- node: () => Promise.resolve().then(() => require("./NodeSqliteClient-CmxPzcBf.cjs"))
117331
+ bun: () => Promise.resolve().then(() => require("./SqliteClient-Cq1peWN1.cjs")),
117332
+ node: () => Promise.resolve().then(() => require("./NodeSqliteClient-qjBWAOGq.cjs"))
116494
117333
  };
116495
117334
  const makeRuntimeSqliteLayer = require_Schema$1.fn("makeRuntimeSqliteLayer")(function* (config) {
116496
117335
  const loader = defaultSqliteClientLoaders[process.versions.bun !== void 0 ? "bun" : "node"];
@@ -287450,6 +288289,7 @@ function taskStartedToActivities(event) {
287450
288289
  summary: taskStartedSummary(event.payload.taskType),
287451
288290
  payload: {
287452
288291
  taskId: event.payload.taskId,
288292
+ agentKind: classifyTaskAgentKind({ taskType: event.payload.taskType }),
287453
288293
  ...event.payload.taskType ? { taskType: event.payload.taskType } : {},
287454
288294
  ...event.payload.description ? { detail: truncateDetail(event.payload.description) } : {}
287455
288295
  }
@@ -295309,7 +296149,7 @@ function resolveBuildCommitFromEnv(env) {
295309
296149
  * environment descriptor down instead of reporting an honest "unknown".
295310
296150
  */
295311
296151
  function readBakedBuildCommit() {
295312
- return "1881e36c16315be6322e1a109dd4a331b484b963";
296152
+ return "c220fa73834b4651c7fc65f272c171028edc3e81";
295313
296153
  }
295314
296154
  async function resolveServerBuildCommit(input) {
295315
296155
  if (isFullCommitSha(input.baked)) return input.baked;
@@ -295426,6 +296266,7 @@ function executionEnvironmentCapabilities(serverConfig) {
295426
296266
  pullRequests: isExecutionPlane,
295427
296267
  threadPullRequestLinking: isExecutionPlane,
295428
296268
  threadSettlement: isExecutionPlane,
296269
+ threadTitleRegeneration: isExecutionPlane,
295429
296270
  cloudMode: serverConfig.forgeCmmdRuntimeMode === true || isCloudShell,
295430
296271
  runtime: {
295431
296272
  topology: runtimeTopology(serverConfig),
@@ -295711,10 +296552,6 @@ const pulseSignalStateUpsertRouteLayer = add$1("POST", "/api/pulse/signals/state
295711
296552
  });
295712
296553
  }).pipe(require_Schema$1.catchTag("AuthError", respondToAuthError)));
295713
296554
  //#endregion
295714
- //#region src/auth/Services/ServerSecretStore.ts
295715
- var SecretStoreError = class extends require_Schema$1.TaggedError("SecretStoreError") {};
295716
- var ServerSecretStore = class extends require_Schema$1.Service()("forge/auth/Services/ServerSecretStore") {};
295717
- //#endregion
295718
296555
  //#region src/auth/Layers/ServerSecretStore.ts
295719
296556
  const OWNED_FILE_RECEIPT_PREFIX = "forge-owned-secret-receipt:v1:";
295720
296557
  const LEGACY_FILE_SECRET_NAMESPACE_PREFIX = "forge-secret-namespace:v1:";
@@ -302087,6 +302924,55 @@ const ProjectionCountsRowSchema = require_Schema$1.Struct({
302087
302924
  const WorkspaceRootLookupInput = require_Schema$1.Struct({ workspaceRoot: require_Schema$1.String });
302088
302925
  const ProjectIdLookupInput = require_Schema$1.Struct({ projectId: ProjectId });
302089
302926
  const ThreadIdLookupInput = require_Schema$1.Struct({ threadId: ThreadId });
302927
+ const ThreadSearchQueryInput = require_Schema$1.Struct({
302928
+ pattern: require_Schema$1.String,
302929
+ limit: require_Schema$1.Number
302930
+ });
302931
+ const ThreadSearchMessageRowSchema = require_Schema$1.Struct({
302932
+ threadId: ThreadId,
302933
+ projectId: ProjectId,
302934
+ role: require_Schema$1.String,
302935
+ text: require_Schema$1.String,
302936
+ createdAt: IsoDateTime
302937
+ });
302938
+ const ThreadSearchMetadataRowSchema = require_Schema$1.Struct({
302939
+ threadId: ThreadId,
302940
+ projectId: ProjectId,
302941
+ title: require_Schema$1.String,
302942
+ branch: require_Schema$1.NullOr(require_Schema$1.String)
302943
+ });
302944
+ /** The contract caps a snippet at this length, so the server must too. */
302945
+ const THREAD_SEARCH_SNIPPET_LIMIT = 240;
302946
+ /** How many matches a search returns when the caller does not say. */
302947
+ const THREAD_SEARCH_DEFAULT_LIMIT = 20;
302948
+ /**
302949
+ * `LIKE` treats `%` and `_` as wildcards, so a person searching for a literal
302950
+ * underscore (every other identifier in a codebase) would otherwise match any
302951
+ * character. Escaped with a backslash, declared to SQL as `ESCAPE '\'`, and
302952
+ * lowercased on both sides because Postgres `LIKE` is case-sensitive where
302953
+ * SQLite's is not, and Forge runs on both.
302954
+ */
302955
+ function toThreadSearchPattern(query) {
302956
+ return `%${query.toLowerCase().replace(/[\\%_]/g, (character) => `\\${character}`)}%`;
302957
+ }
302958
+ /**
302959
+ * A window of the text around the first hit, not the head of it.
302960
+ *
302961
+ * A match 4000 characters into an assistant message is invisible in a snippet
302962
+ * that always starts at character zero, which is the difference between a
302963
+ * result a person can recognize and a result they have to open to understand.
302964
+ */
302965
+ function buildThreadSearchSnippet(text, lowerQuery) {
302966
+ const collapsed = text.replace(/\s+/g, " ").trim();
302967
+ if (collapsed.length <= THREAD_SEARCH_SNIPPET_LIMIT) return collapsed;
302968
+ const matchIndex = collapsed.toLowerCase().indexOf(lowerQuery);
302969
+ if (matchIndex < 0) return `${collapsed.slice(0, THREAD_SEARCH_SNIPPET_LIMIT - 1)}…`;
302970
+ const leading = Math.floor((THREAD_SEARCH_SNIPPET_LIMIT - lowerQuery.length) / 2);
302971
+ const start = Math.max(0, matchIndex - Math.max(leading, 0));
302972
+ const windowed = collapsed.slice(start, start + THREAD_SEARCH_SNIPPET_LIMIT);
302973
+ const prefixed = start > 0 ? `…${windowed}` : windowed;
302974
+ return prefixed.length > THREAD_SEARCH_SNIPPET_LIMIT ? prefixed.slice(0, THREAD_SEARCH_SNIPPET_LIMIT) : prefixed;
302975
+ }
302090
302976
  const ProjectionProjectLookupRowSchema = ProjectionProjectDbRowSchema;
302091
302977
  const ProjectionThreadIdLookupRowSchema = require_Schema$1.Struct({ threadId: ThreadId });
302092
302978
  const ProjectionThreadCheckpointContextThreadRowSchema = require_Schema$1.Struct({
@@ -302332,6 +303218,61 @@ const makeProjectionSnapshotQuery = require_Schema$1.gen(function* () {
302332
303218
  AND deleted_at IS NULL
302333
303219
  AND archived_at IS NULL
302334
303220
  ORDER BY created_at ASC, thread_id ASC
303221
+ `
303222
+ });
303223
+ /**
303224
+ * Message bodies containing the query.
303225
+ *
303226
+ * `role IN ('user', 'assistant')` is not a filter for tidiness: the match
303227
+ * shape on the wire names one of those two as the side the hit came from,
303228
+ * and a system message has no side to report. Ordered newest first because a
303229
+ * capped search should keep what the person is most likely still thinking
303230
+ * about.
303231
+ */
303232
+ const searchThreadMessageRows = findAll({
303233
+ Request: ThreadSearchQueryInput,
303234
+ Result: ThreadSearchMessageRowSchema,
303235
+ execute: ({ pattern, limit }) => sql`
303236
+ SELECT
303237
+ m.thread_id AS "threadId",
303238
+ t.project_id AS "projectId",
303239
+ m.role AS "role",
303240
+ m.text AS "text",
303241
+ m.created_at AS "createdAt"
303242
+ FROM projection_thread_messages AS m
303243
+ JOIN projection_threads AS t ON t.thread_id = m.thread_id
303244
+ WHERE t.deleted_at IS NULL
303245
+ AND m.role IN ('user', 'assistant')
303246
+ AND LOWER(m.text) LIKE ${pattern} ESCAPE '\\'
303247
+ ORDER BY m.created_at DESC, m.message_id ASC
303248
+ LIMIT ${limit}
303249
+ `
303250
+ });
303251
+ /**
303252
+ * Threads whose own metadata contains the query.
303253
+ *
303254
+ * Title and branch, which is what the web command palette matches on. Title
303255
+ * is duplicated work for a client that already filters its in-memory list,
303256
+ * and harmless because matches are deduplicated by thread; branch is not,
303257
+ * because no client holds every branch.
303258
+ */
303259
+ const searchThreadMetadataRows = findAll({
303260
+ Request: ThreadSearchQueryInput,
303261
+ Result: ThreadSearchMetadataRowSchema,
303262
+ execute: ({ pattern, limit }) => sql`
303263
+ SELECT
303264
+ thread_id AS "threadId",
303265
+ project_id AS "projectId",
303266
+ title,
303267
+ branch
303268
+ FROM projection_threads
303269
+ WHERE deleted_at IS NULL
303270
+ AND (
303271
+ LOWER(title) LIKE ${pattern} ESCAPE '\\'
303272
+ OR LOWER(branch) LIKE ${pattern} ESCAPE '\\'
303273
+ )
303274
+ ORDER BY updated_at DESC, thread_id ASC
303275
+ LIMIT ${limit}
302335
303276
  `
302336
303277
  });
302337
303278
  /**
@@ -303078,6 +304019,40 @@ const makeProjectionSnapshotQuery = require_Schema$1.gen(function* () {
303078
304019
  };
303079
304020
  const getShellSnapshot = () => readShellSnapshot("getShellSnapshot", listThreadRows);
303080
304021
  const getArchivedShellSnapshot = () => readShellSnapshot("getArchivedShellSnapshot", listArchivedThreadRows);
304022
+ const searchThreads = (input) => {
304023
+ const limit = input.limit ?? THREAD_SEARCH_DEFAULT_LIMIT;
304024
+ const lowerQuery = input.query.toLowerCase();
304025
+ const request = {
304026
+ pattern: toThreadSearchPattern(input.query),
304027
+ limit
304028
+ };
304029
+ return require_Schema$1.all([searchThreadMessageRows(request).pipe(require_Schema$1.mapError(toPersistenceSqlOrDecodeError("ProjectionSnapshotQuery.searchThreads:messages:query", "ProjectionSnapshotQuery.searchThreads:messages:decodeRows"))), searchThreadMetadataRows(request).pipe(require_Schema$1.mapError(toPersistenceSqlOrDecodeError("ProjectionSnapshotQuery.searchThreads:metadata:query", "ProjectionSnapshotQuery.searchThreads:metadata:decodeRows")))]).pipe(require_Schema$1.map(([messageRows, metadataRows]) => {
304030
+ const byThread = /* @__PURE__ */ new Map();
304031
+ for (const row of messageRows) {
304032
+ if (byThread.has(row.threadId)) continue;
304033
+ byThread.set(row.threadId, {
304034
+ threadId: row.threadId,
304035
+ projectId: row.projectId,
304036
+ source: row.role === "assistant" ? "assistant" : "user",
304037
+ snippet: buildThreadSearchSnippet(row.text, lowerQuery),
304038
+ messageCreatedAt: row.createdAt
304039
+ });
304040
+ }
304041
+ for (const row of metadataRows) {
304042
+ if (byThread.has(row.threadId)) continue;
304043
+ const branchMatched = row.branch?.toLowerCase().includes(lowerQuery) === true;
304044
+ const titleMatched = row.title.toLowerCase().includes(lowerQuery);
304045
+ byThread.set(row.threadId, {
304046
+ threadId: row.threadId,
304047
+ projectId: row.projectId,
304048
+ source: "user",
304049
+ snippet: buildThreadSearchSnippet(titleMatched || !branchMatched ? row.title : row.branch ?? row.title, lowerQuery),
304050
+ messageCreatedAt: null
304051
+ });
304052
+ }
304053
+ return { matches: [...byThread.values()].slice(0, limit) };
304054
+ }));
304055
+ };
303081
304056
  const listActiveWorkerShells = () => sql.withTransaction(require_Schema$1.all([
303082
304057
  listActiveWorkerThreadRows(void 0).pipe(require_Schema$1.mapError(toPersistenceSqlOrDecodeError("ProjectionSnapshotQuery.listActiveWorkerShells:listThreads:query", "ProjectionSnapshotQuery.listActiveWorkerShells:listThreads:decodeRows"))),
303083
304058
  listActiveWorkerSessionRows(void 0).pipe(require_Schema$1.mapError(toPersistenceSqlOrDecodeError("ProjectionSnapshotQuery.listActiveWorkerShells:listThreadSessions:query", "ProjectionSnapshotQuery.listActiveWorkerShells:listThreadSessions:decodeRows"))),
@@ -303246,6 +304221,7 @@ const makeProjectionSnapshotQuery = require_Schema$1.gen(function* () {
303246
304221
  getSnapshot,
303247
304222
  getShellSnapshot,
303248
304223
  getArchivedShellSnapshot,
304224
+ searchThreads,
303249
304225
  listActiveWorkerShells,
303250
304226
  listStaleRunningSessionShells,
303251
304227
  getCounts,
@@ -318510,13 +319486,13 @@ const gitUrlCloneRouteLayer = add$1("POST", "/api/cloud/git/clone-url", require_
318510
319486
  //#endregion
318511
319487
  //#region src/server.ts
318512
319488
  const PtyAdapterLive = require_Schema$1.unwrap(require_Schema$1.gen(function* () {
318513
- if (typeof Bun !== "undefined") return (yield* require_Schema$1.promise(() => Promise.resolve().then(() => require("./BunPTY-86ttlTi6.cjs")))).layer;
318514
- else return (yield* require_Schema$1.promise(() => Promise.resolve().then(() => require("./NodePTY-Cps0jsDA.cjs")))).layer;
319489
+ if (typeof Bun !== "undefined") return (yield* require_Schema$1.promise(() => Promise.resolve().then(() => require("./BunPTY-uyreNRGp.cjs")))).layer;
319490
+ else return (yield* require_Schema$1.promise(() => Promise.resolve().then(() => require("./NodePTY-S4tPEwZc.cjs")))).layer;
318515
319491
  }));
318516
319492
  const HttpServerLive = require_Schema$1.unwrap(require_Schema$1.gen(function* () {
318517
319493
  const config = yield* ServerConfig;
318518
- if (typeof Bun !== "undefined") return (yield* require_Schema$1.promise(() => Promise.resolve().then(() => require("./BunHttpServer-BhE7Szci.cjs")))).layer(bunHttpServerOptions(config));
318519
- const [NodeHttpServer, NodeHttp] = yield* require_Schema$1.all([require_Schema$1.promise(() => Promise.resolve().then(() => require("./NodeHttpServer-BPjxt_DT.cjs"))), require_Schema$1.promise(() => import("node:http"))]);
319494
+ if (typeof Bun !== "undefined") return (yield* require_Schema$1.promise(() => Promise.resolve().then(() => require("./BunHttpServer-DKvSpNd1.cjs")))).layer(bunHttpServerOptions(config));
319495
+ const [NodeHttpServer, NodeHttp] = yield* require_Schema$1.all([require_Schema$1.promise(() => Promise.resolve().then(() => require("./NodeHttpServer-Dfc7nSMq.cjs"))), require_Schema$1.promise(() => import("node:http"))]);
318520
319496
  return NodeHttpServer.layer(NodeHttp.createServer, {
318521
319497
  host: config.host,
318522
319498
  port: config.port
@@ -318524,10 +319500,10 @@ const HttpServerLive = require_Schema$1.unwrap(require_Schema$1.gen(function* ()
318524
319500
  }));
318525
319501
  const PlatformServicesLive = require_Schema$1.unwrap(require_Schema$1.gen(function* () {
318526
319502
  if (typeof Bun !== "undefined") {
318527
- const { layer } = yield* require_Schema$1.promise(() => Promise.resolve().then(() => require("./BunServices-BwE9KexF.cjs")));
319503
+ const { layer } = yield* require_Schema$1.promise(() => Promise.resolve().then(() => require("./BunServices-DDh7Whyx.cjs")));
318528
319504
  return layer;
318529
319505
  } else {
318530
- const { layer } = yield* require_Schema$1.promise(() => Promise.resolve().then(() => require("./NodeServices-Ddr-tYRu.cjs")));
319506
+ const { layer } = yield* require_Schema$1.promise(() => Promise.resolve().then(() => require("./NodeServices-DE8jhdcU.cjs")));
318531
319507
  return layer;
318532
319508
  }
318533
319509
  }));