@cmmd-center/forge 0.13.2 → 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 +1216 -205
  35. package/dist/bin.mjs +1239 -231
  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-Bh9UFe_U.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-CYkhfTpq.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-A3r3u_Vr.js +0 -21
  57. package/dist/client/assets/DiffPanelShell-B30xoHOs.js +0 -2531
  58. package/dist/client/assets/PullRequestCodePanel-T32DALQE.js +0 -5
  59. package/dist/client/assets/Virtualizer-DbDh3RHG.js +0 -2
  60. package/dist/client/assets/canvasStateStore-DN5mPB5B.js +0 -2
  61. package/dist/client/assets/index-Dk7T0zVg.css +0 -1
  62. package/dist/client/assets/index-FpNJGy15.js +0 -151
  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.mjs CHANGED
@@ -1,18 +1,18 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  import { a as __toCommonJS, i as __require, n as __esmMin, o as __toESM, r as __exportAll, t as __commonJSMin } from "./chunk-C5ckVeoz.mjs";
4
- import { $ as encodeEffect, $a as make$39, $c as fromUndefinedOr, $i as spaced, $l as constUndefined, $n as filterMapOrFail, $r as runPromise, $s as min, $t as stringifyJson, Aa as Scope$1, Ac as fail$1, Ai as void_$1, An as cached$3, Ar as match$1, As as uninterruptibleMask, At as withConstructorDefault, Ba as CurrentLogSpans, Bc as empty$1, Bi as counter, Bl as hasProperty, Bn as catch_, Bo as addFinalizer, Br as orDie, Bs as make$42, Bt as head, C as Record, Ca as mergeAll, Ci as tx$1, Cl as isEffect$1, Cn as andThen, Co as hasDies, Cr as logInfo, Cs as sleep$3, Ct as optionalKey, D as TaggedStruct, Da as succeedContext, Di as uninterruptibleMask$1, Dl as NodeInspectSymbol, Dn as as$3, Do as interruptors, Ds as suspend, Dt as toCodecStringTree, E as TaggedErrorClass, Ea as succeed$2, Ei as uninterruptible, En as annotateSpans, Eo as interrupt$2, Er as map$3, Et as toCodecJson, F as UndefinedOr, Fa as forkUnsafe, Fi as withParentSpan, Fn as catchFilter, Fr as onError$1, Fs as MinimumTraceLevel, G as decodeExit, Ga as done, Gc as make$35, Gi as withAttributes, Gl as isObject$1, Gn as die$1, Gr as provideService, Gs as format$1, Gt as Transformation, H as declare, Hi as snapshotUnsafe, Hl as isNotNullish, Hn as contextWith, Ho as as$2, Hr as promise, Ht as size$2, I as Union, Ia as make$37, Ic as succeed$1, Ii as withSpan, In as catchIf, Io as ConsoleRef, Ir as onExit, Is as NativeSpan, It as assign, J as decodeUnknownEffect, Ja as failCause$3, Ji as exponential, Jn as exit, Jr as repeat, Jt as transformOrFail, K as decodeSync, Kc as makeUnsafe$3, Ki as addDelay, Kl as isTagged, Ko as consolePretty$1, Kr as race, Ks as fromInput, L as Unknown, La as makeUnsafe$4, Lc as Reference$1, Li as withTracerEnabled, Ln as catchReason, Lo as CurrentLoggers$1, Ls as ParentSpan, Lt as append$1, M as URLFromString, Ma as addFinalizerExit, Mi as whileLoop, Mn as callback, Mo as pretty, Ms as void_$2, Na as close, Nc as isFailure$1, Ni as withFiber$1, Nl as equals, Nn as catchCause, No as prettyErrors, Nr as matchEffect, O as TemplateLiteral, Oa as sync$2, Ol as toJson, On as asSome, Or as mapError, Os as sync, Ot as toJsonSchemaDocument, P as Undefined, Pa as closeUnsafe, Pc as isSuccess$1, Pn as catchDefect, Po as squash, Pr as never$1, Q as encode$4, Qc as fromNullishOr, Qi as recurs, Ql as constTrue, Qn as filter$1, Qr as runForkWith, Qs as millis, R as UnknownFromJsonString$1, Ra as provide$2, Rc as Service$1, Ri as yieldNow, Rn as catchTag, Rr as onInterrupt, Rs as Tracer, Rt as drop$1, S as Option, Sa as merge$3, Si as try_, Sn as all, Sr as logFatal, Ss as scopeAddFinalizerExit, St as optional$4, T as Struct, Ta as provideMerge, Ti as undefined_, Tn as annotateLogs, To as hasInterruptsOnly, Tr as makeSpanScoped, Tt as tag, Ua as TracerTimingEnabled, Ui as timer, Ul as isNotUndefined, Un as currentSpan, Ur as provide, Ut as toArray, V as brand, Va as MinimumLogLevel, Vc as get$3, Vi as gauge, Vl as isNotNull, Vn as context, Vo as andThen$1, Vr as orElseSucceed, Vt as isEmpty$2, W as decodeEffect, Wa as _await, Wc as getUnsafe$1, Wi as update$3, Wl as isNullish, Wr as provideContext, Ws as days, X as decodeUnknownOption, Xc as flatMap$2, Xl as compose, Xn as failCause$1, Xr as retry, Xt as onSome, Y as decodeUnknownExit, Yc as firstSomeOf, Yi as forever$2, Yl as cast, Yn as fail, Yr as result, Z as decodeUnknownSync, Za as isDeferred, Zc as flatten$1, Zi as passthrough$1, Zr as runFork, Zs as max, Zt as parseJson$1, _ as NonEmptyArray, _a as effectContext, _i as tapErrorTag, _n as toUtc, _t as isSchemaError, a as DateTimeUtc, aa as Random$1, ac as zero, al as map$5, an as value, ao as interrupt$3, ar as fnUntraced, at as isBetween, au as pipe$1, b as NullOr, bc as reverse, bi as timeoutOrElse, bn as acquireUseRelease, br as logDebug, bs as reportCauseUnsafe, bt as makeFilter, c as Defect, ca as Clock, cc as ensure, ci as sleep$4, cl as some, cn as add$3, co as match$2, cr as forkChild, cs as flatMap$1, ct as isGreaterThanOrEqualTo, d as Exit, dc as fromIterable, di as succeedSome, dn as isGreaterThanOrEqualTo$1, dr as forkScoped, ds as forkDetach, dt as isMaxLength, ea as catchDone, ec as minutes, el as getOrElse, er as filterOrFail, et as encodeSync, eu as constVoid, fa as TaggedError, fi as suspend$1, fn as make$38, fr as gen, ft as isMaxProperties, g as Never, ga as effect, gi as tapError, gl as YieldableProto, gn as toEpochMillis, gr as interruptible, gt as isSchema, h as Literals, ha as buildWithMemoMap, hi as tapCause, hl as PipeInspectableProto, hn as toDate, hr as interrupt$1, ht as isPattern, i as Class, ic as toSeconds, ii as scoped, il as isSome, in as make$40, io as failCause$2, ir as fn$1, it as is$1, iu as identity, ja as addFinalizer$1, jn as cachedWithTTL, jo as map$6, jr as matchCauseEffect, jt as withDecodingDefault, k as Trim, ka as unwrap, ki as useSpan, kl as format, kn as asVoid, ks as tracerLogger$1, kt as toType, l as Duration, la as currentTimeMillis, lc as filter, li as succeed, ll as Number$2, ln as distance, lr as forkDetach$1, ls as fnUntraced$1, lt as isInt, m as Literal, ma as build, mc as isReadonlyArrayNonEmpty, mi as tap, mn as now, mo as StackTrace, mr as ignoreCause, ms as loggerMake, mt as isNonEmpty, n as ArraySchema, nc as times, nl as getOrUndefined, no as die$2, nt as encodeUnknownExit, nu as dual, o as DateTimeUtcFromString, oa as clamp, oc as allocate, oi as service, ol as match, oo as isFailure, or as forEach, ot as isFinite$1, ou as pipeArguments, p as Int$1, pa as CurrentMemoMap, pc as isArrayNonEmpty, pi as sync$1, pn as makeUnsafe$5, po as NoSuchElementError, pr as ignore, pt as isMinLength, q as decodeTo, qa as fail$3, qc as merge$2, qi as both, qn as ensuring, qr as raceFirst, qs as fromInputUnsafe, r as Boolean$1, ra as isDoneCause, rc as toMillis, ri as scope$1, rl as isNone, rn as makeFormatterDefault, ro as fail$2, rr as flatten$2, rt as fromJsonString, sa as parse$6, sc as empty$3, si as serviceOption, sl as none$4, sn as encodeBase64Url$2, so as isSuccess, sr as forever$1, st as isGreaterThan, t as Any, tc as seconds, ti as runPromiseWith, tl as getOrNull, tn as InvalidValue$1, to as succeed$3, tr as flatMap, ts as fiberInterrupt, tt as encodeUnknownEffect, tu as constant, u as ErrorClass, ua as Class$1, uc as flatMapNullishOr, ui as succeedNone, ul as make$41, un as formatIso, uo as void_$3, ur as forkIn, us as forever, ut as isLessThanOrEqualTo, v as NonEmptyString, va as effectDiscard, vc as map$4, vi as timeout$1, vn as Transaction$1, vo as done$1, vr as isEffect, w as String$1, wa as provide$1, wi as txRetry, wl as withFiber, wn as annotateCurrentSpan, wo as hasInterrupts, wr as logWarning$1, wt as suspend$2, x as Number$1, xa as launch$1, xc as sort, xi as tryPromise, xn as addFinalizer$2, xr as logError, xs as scope, xt as mutable, y as Null, ya as empty$4, yi as timeoutOption, yn as acquireRelease, yo as fail$4, yr as log$4, yt as make$36, z as Void, za as CurrentLogAnnotations, zc as add$2, zi as zipWith, zl as compose$1, zn as catchTags, zr as option, zs as externalSpan, zt as empty$2 } from "./Schema-CJyk7ah1.mjs";
5
- import { $ as toAsyncIterable, A as make$45, At as make$44, Bt as publish, C as fromEffect, Ct as make$43, E as fromQueue, Et as end, F as merge$4, Ft as shutdown$1, G as runFold, H as run$2, Ht as subscribe$1, It as sliding, Jt as getCurrent, K as runForEach, Kt as makeUnsafe$7, Lt as take$1, M as mapAccum, Mt as offerAll, N as mapEffect$1, Nt as offerUnsafe, O as fromSubscription, Ot as failCause$4, Q as tick, R as onExit$1, Rt as takeAll, S as fromChannel, T as fromPubSub, Tt as bounded, U as runCollect, Ut as unbounded$1, V as provideContext$1, Vt as shutdown$2, W as runDrain, Y as succeed$4, Yt as interrupt$4, Z as takeWhile, Zt as runIn, _t as mapInput, at as make$46, b as flatMap$3, bt as fromTransform, c as callback$1, d as debounce$1, f as decodeText$1, g as ensuring$1, h as encodeText, it as get$4, j as map$7, jt as offer$1, k as isStream, l as catchCause$1, m as empty$5, mt as forEach$1, ot as get$5, pt as drain, q as runForEachArray, qt as await_, r as FileSystem, rt as unwrap$1, st as make$47, t as Path$2, u as concat$2, v as failCause$5, w as fromIterable$1, wt as makeUnsafe$6, x as fromAsyncIterable, y as filter$2, yt as fromSubscriptionArray, zt as unbounded$2 } from "./Path-C6Zb2WHp.mjs";
6
- import { _ as ChildProcessSpawner, a as Stdio, g as make$48, o as make$49, r as isQuitError } from "./NodeTerminal-BEGtoMSS.mjs";
7
- import { n as layer$7 } from "./NodeServices-CBNXOYLG.mjs";
8
- import { $ as file, A as SocketErrorReason, At as merge$5, Bt as set$3, Ct as HttpClientErrorSchema, Dt as empty$7, Ft as getAndSet, G as TracerPropagationEnabled, Ht as updateAndGet, I as HttpServerError, It as getUnsafe$2, J as transformResponse, K as filterStatusOk$1, Lt as make$50, Ot as fromInput$1, Pt as get$6, Q as empty$6, Rt as makeUnsafe$8, St as setBody, Tt as jsonUnsafe$1, U as layer$8, Vt as update$4, W as HttpClient, Z as symbol$1, _ as schemaBodyUrlParams, _t as acceptJson, a as withLogAddress, at as setCookie, bt as get$7, ct as stream, d as logger$1, dt as filterStatusOk, et as isHttpServerResponse, f as withLoggerDisabled, ft as matchStatus, g as schemaBodyJson, gt as schemaBodyJson$1, i as serve$1, it as schemaJson, kt as fromRecordUnsafe, lt as text$2, m as ParsedSearchParams, nt as raw, ot as setCookies, p as HttpServerRequest, q as retryTransient, rt as redirect, st as setHeader, t as HttpServer, tt as jsonUnsafe, u as cors, ut as uint8Array, v as schemaHeaders, vt as bearerToken, wt as isHttpClientError, xt as post, y as toURL, yt as bodyJson, z as RouteNotFound, zt as modify$3 } from "./HttpServer-DNGCTTdk.mjs";
9
- import { _ as withDefault$3, a as all$1, c as logLevel, d as option$1, f as port, g as url$2, i as TrueValues, l as map$8, m as string$7, n as FalseValues, o as boolean$5, p as schema$2, r as Record$1, s as int$2, t as Boolean$2, u as number$2 } from "./Config-AqUR_lhX.mjs";
10
- import { a as make$51, i as keys, n as getOption, o as makeWith$1, r as invalidate, s as set$4, t as get$8 } from "./Cache-p0X77SKw.mjs";
11
- import { S as layer$9, _ as defaultEscape, a as DeadlockError, b as makeCompiler$1, c as SqlError, d as UnknownError, g as custom, h as make$52, i as ConstraintError, l as SqlSyntaxError, m as SqlClient, n as AuthorizationError, o as LockTimeoutError, r as ConnectionError, s as SerializationError, t as AuthenticationError, u as StatementTimeoutError, v as defaultTransforms, y as fragment } from "./SqlError-cyJT2IRY.mjs";
4
+ import { $ as encodeEffect, $a as make$39, $c as fromNullishOr, $i as spaced, $l as constTrue, $n as filterMapOrFail, $r as runPromise, $s as millis, $t as stringifyJson, Aa as Scope$1, Ai as void_$1, Al as format, An as cached$3, Ar as match$1, As as uninterruptibleMask, At as withConstructorDefault, Ba as CurrentLogSpans, Bc as add$2, Bi as counter, Bl as compose$1, Bn as catch_, Bo as addFinalizer, Br as orDie, Bs as make$42, Bt as head, C as Record, Ca as mergeAll, Ci as tx$1, Cn as andThen, Co as hasDies, Cr as logInfo, Cs as sleep$3, Ct as optionalKey, D as TaggedStruct, Da as succeedContext, Di as uninterruptibleMask$1, Dn as as$3, Do as interruptors, Ds as suspend, Dt as toCodecStringTree, E as TaggedErrorClass, Ea as succeed$2, Ei as uninterruptible, En as annotateSpans, Eo as interrupt$2, Er as map$3, Et as toCodecJson, F as UndefinedOr, Fa as forkUnsafe, Fc as isSuccess$1, Fi as withParentSpan, Fn as catchFilter, Fr as onError$1, Fs as MinimumTraceLevel, G as decodeExit, Ga as done, Gc as getUnsafe$1, Gi as withAttributes, Gl as isNullish, Gn as die$1, Gr as provideService, Gs as format$1, Gt as Transformation, H as declare, Hc as get$3, Hi as snapshotUnsafe, Hl as isNotNull, Hn as contextWith, Ho as as$2, Hr as promise, Ht as size$2, I as Union, Ia as make$37, Ii as withSpan, In as catchIf, Io as ConsoleRef, Ir as onExit, Is as NativeSpan, It as assign, J as decodeUnknownEffect, Ja as failCause$3, Jc as merge$2, Ji as exponential, Jn as exit, Jr as repeat, Js as hours, Jt as transformOrFail, K as decodeSync, Kc as make$35, Ki as addDelay, Kl as isObject$1, Ko as consolePretty$1, Kr as race, Ks as fromInput, L as Unknown, La as makeUnsafe$4, Lc as succeed$1, Li as withTracerEnabled, Ln as catchReason, Lo as CurrentLoggers$1, Ls as ParentSpan, Lt as append$1, M as URLFromString, Ma as addFinalizerExit, Mi as whileLoop, Mn as callback, Mo as pretty, Ms as void_$2, Na as close, Ni as withFiber$1, Nn as catchCause, No as prettyErrors, Nr as matchEffect, O as TemplateLiteral, Oa as sync$2, Ol as NodeInspectSymbol, On as asSome, Or as mapError, Os as sync, Ot as toJsonSchemaDocument, P as Undefined, Pa as closeUnsafe, Pc as isFailure$1, Pl as equals, Pn as catchDefect, Po as squash, Pr as never$1, Q as encode$4, Qc as flatten$1, Qi as recurs, Qn as filter$1, Qr as runForkWith, Qs as max, R as UnknownFromJsonString$1, Ra as provide$2, Rc as Reference$1, Ri as yieldNow, Rn as catchTag, Rr as onInterrupt, Rs as Tracer, Rt as drop$1, S as Option, Sa as merge$3, Sc as sort, Si as try_, Sn as all, Sr as logFatal, Ss as scopeAddFinalizerExit, St as optional$4, T as Struct, Ta as provideMerge, Ti as undefined_, Tl as withFiber, Tn as annotateLogs, To as hasInterruptsOnly, Tr as makeSpanScoped, Tt as tag, Ua as TracerTimingEnabled, Ui as timer, Ul as isNotNullish, Un as currentSpan, Ur as provide, Ut as toArray, V as brand, Va as MinimumLogLevel, Vc as empty$1, Vi as gauge, Vl as hasProperty, Vn as context, Vo as andThen$1, Vr as orElseSucceed, Vt as isEmpty$2, W as decodeEffect, Wa as _await, Wi as update$3, Wl as isNotUndefined, Wr as provideContext, Ws as days, X as decodeUnknownOption, Xc as firstSomeOf, Xl as cast, Xn as failCause$1, Xr as retry, Xt as onSome, Y as decodeUnknownExit, Yi as forever$2, Yn as fail, Yr as result, Z as decodeUnknownSync, Za as isDeferred, Zc as flatMap$2, Zi as passthrough$1, Zl as compose, Zr as runFork, Zt as parseJson$1, _ as NonEmptyArray, _a as effectContext, _i as tapErrorTag, _l as YieldableProto, _n as toUtc, _t as isSchemaError, a as DateTimeUtc, aa as Random$1, ac as toSeconds, al as isSome, an as value, ao as interrupt$3, ar as fnUntraced, at as isBetween, au as identity, b as NullOr, bi as timeoutOrElse, bn as acquireUseRelease, br as logDebug, bs as reportCauseUnsafe, bt as makeFilter, c as Defect, ca as Clock, cc as empty$3, ci as sleep$4, cl as none$4, cn as add$3, co as match$2, cr as forkChild, cs as flatMap$1, ct as isGreaterThanOrEqualTo, d as Exit, dc as flatMapNullishOr, di as succeedSome, dl as make$41, dn as isGreaterThanOrEqualTo$1, dr as forkScoped, ds as forkDetach, dt as isMaxLength, ea as catchDone, ec as min, el as fromUndefinedOr, er as filterOrFail, et as encodeSync, eu as constUndefined, fa as TaggedError, fc as fromIterable, fi as suspend$1, fn as make$38, fr as gen, ft as isMaxProperties, g as Never, ga as effect, gi as tapError, gl as PipeInspectableProto, gn as toEpochMillis, gr as interruptible, gt as isSchema, h as Literals, ha as buildWithMemoMap, hc as isReadonlyArrayNonEmpty, hi as tapCause, hn as toDate, hr as interrupt$1, ht as isPattern, i as Class, ic as toMillis, ii as scoped, il as isNone, in as make$40, io as failCause$2, ir as fn$1, it as is$1, ja as addFinalizer$1, jc as fail$1, jn as cachedWithTTL, jo as map$6, jr as matchCauseEffect, jt as withDecodingDefault, k as Trim, ka as unwrap, ki as useSpan, kl as toJson, kn as asVoid, ks as tracerLogger$1, kt as toType, l as Duration, la as currentTimeMillis, lc as ensure, li as succeed, ll as some, ln as distance, lr as forkDetach$1, ls as fnUntraced$1, lt as isInt, m as Literal, ma as build, mc as isArrayNonEmpty, mi as tap, mn as now, mo as StackTrace, mr as ignoreCause, ms as loggerMake, mt as isNonEmpty, n as ArraySchema, nc as seconds, nl as getOrNull, no as die$2, nt as encodeUnknownExit, nu as constant, o as DateTimeUtcFromString, oa as clamp, oc as zero, oi as service, ol as map$5, oo as isFailure, or as forEach, ot as isFinite$1, ou as pipe$1, p as Int$1, pa as CurrentMemoMap, pi as sync$1, pn as makeUnsafe$5, po as NoSuchElementError, pr as ignore, pt as isMinLength, q as decodeTo, qa as fail$3, qc as makeUnsafe$3, qi as both, ql as isTagged, qn as ensuring, qr as raceFirst, qs as fromInputUnsafe, r as Boolean$1, ra as isDoneCause, rc as times, ri as scope$1, rl as getOrUndefined, rn as makeFormatterDefault, ro as fail$2, rr as flatten$2, rt as fromJsonString, ru as dual, sa as parse$6, sc as allocate, si as serviceOption, sl as match, sn as encodeBase64Url$2, so as isSuccess, sr as forever$1, st as isGreaterThan, su as pipeArguments, t as Any, tc as minutes, ti as runPromiseWith, tl as getOrElse, tn as InvalidValue$1, to as succeed$3, tr as flatMap, ts as fiberInterrupt, tt as encodeUnknownEffect, tu as constVoid, u as ErrorClass, ua as Class$1, uc as filter, ui as succeedNone, ul as Number$2, un as formatIso, uo as void_$3, ur as forkIn, us as forever, ut as isLessThanOrEqualTo, v as NonEmptyString, va as effectDiscard, vi as timeout$1, vn as Transaction$1, vo as done$1, vr as isEffect, w as String$1, wa as provide$1, wi as txRetry, wl as isEffect$1, wn as annotateCurrentSpan, wo as hasInterrupts, wr as logWarning$1, wt as suspend$2, x as Number$1, xa as launch$1, xc as reverse, xi as tryPromise, xn as addFinalizer$2, xr as logError, xs as scope, xt as mutable, y as Null, ya as empty$4, yc as map$4, yi as timeoutOption, yn as acquireRelease, yo as fail$4, yr as log$4, yt as make$36, z as Void, za as CurrentLogAnnotations, zc as Service$1, zi as zipWith, zn as catchTags, zr as option, zs as externalSpan, zt as empty$2 } from "./Schema-DOBWF8gQ.mjs";
5
+ import { $ as toAsyncIterable, A as make$45, At as make$44, Bt as publish, C as fromEffect, Ct as make$43, E as fromQueue, Et as end, F as merge$4, Ft as shutdown$1, G as runFold, H as run$2, Ht as subscribe$1, It as sliding, Jt as getCurrent, K as runForEach, Kt as makeUnsafe$7, Lt as take$1, M as mapAccum, Mt as offerAll, N as mapEffect$1, Nt as offerUnsafe, O as fromSubscription, Ot as failCause$4, Q as tick, R as onExit$1, Rt as takeAll, S as fromChannel, T as fromPubSub, Tt as bounded, U as runCollect, Ut as unbounded$1, V as provideContext$1, Vt as shutdown$2, W as runDrain, Y as succeed$4, Yt as interrupt$4, Z as takeWhile, Zt as runIn, _t as mapInput, at as make$46, b as flatMap$3, bt as fromTransform, c as callback$1, d as debounce$1, f as decodeText$1, g as ensuring$1, h as encodeText, it as get$4, j as map$7, jt as offer$1, k as isStream, l as catchCause$1, m as empty$5, mt as forEach$1, ot as get$5, pt as drain, q as runForEachArray, qt as await_, r as FileSystem, rt as unwrap$1, st as make$47, t as Path$2, u as concat$2, v as failCause$5, w as fromIterable$1, wt as makeUnsafe$6, x as fromAsyncIterable, y as filter$2, yt as fromSubscriptionArray, zt as unbounded$2 } from "./Path-C851t6sC.mjs";
6
+ import { _ as ChildProcessSpawner, a as Stdio, g as make$48, o as make$49, r as isQuitError } from "./NodeTerminal-BPw7bktz.mjs";
7
+ import { n as layer$7 } from "./NodeServices-BahzLhbo.mjs";
8
+ import { $ as file, A as SocketErrorReason, At as merge$5, Bt as set$3, Ct as HttpClientErrorSchema, Dt as empty$7, Ft as getAndSet, G as TracerPropagationEnabled, Ht as updateAndGet, I as HttpServerError, It as getUnsafe$2, J as transformResponse, K as filterStatusOk$1, Lt as make$50, Ot as fromInput$1, Pt as get$6, Q as empty$6, Rt as makeUnsafe$8, St as setBody, Tt as jsonUnsafe$1, U as layer$8, Vt as update$4, W as HttpClient, Z as symbol$1, _ as schemaBodyUrlParams, _t as acceptJson, a as withLogAddress, at as setCookie, bt as get$7, ct as stream, d as logger$1, dt as filterStatusOk, et as isHttpServerResponse, f as withLoggerDisabled, ft as matchStatus, g as schemaBodyJson, gt as schemaBodyJson$1, i as serve$1, it as schemaJson, kt as fromRecordUnsafe, lt as text$2, m as ParsedSearchParams, nt as raw, ot as setCookies, p as HttpServerRequest, q as retryTransient, rt as redirect, st as setHeader, t as HttpServer, tt as jsonUnsafe, u as cors, ut as uint8Array, v as schemaHeaders, vt as bearerToken, wt as isHttpClientError, xt as post, y as toURL, yt as bodyJson, z as RouteNotFound, zt as modify$3 } from "./HttpServer-ClsGgAq1.mjs";
9
+ import { _ as withDefault$3, a as all$1, c as logLevel, d as option$1, f as port, g as url$2, i as TrueValues, l as map$8, m as string$7, n as FalseValues, o as boolean$5, p as schema$2, r as Record$1, s as int$2, t as Boolean$2, u as number$2 } from "./Config-Cdfpphpo.mjs";
10
+ import { a as make$51, i as keys, n as getOption, o as makeWith$1, r as invalidate, s as set$4, t as get$8 } from "./Cache-BVilO50s.mjs";
11
+ import { S as layer$9, _ as defaultEscape, a as DeadlockError, b as makeCompiler$1, c as SqlError, d as UnknownError, g as custom, h as make$52, i as ConstraintError, l as SqlSyntaxError, m as SqlClient, n as AuthorizationError, o as LockTimeoutError, r as ConnectionError, s as SerializationError, t as AuthenticationError, u as StatementTimeoutError, v as defaultTransforms, y as fragment } from "./SqlError-3aqibZrj.mjs";
12
12
  import { r as wrapper_default, t as Mime_default } from "./Mime-Dq9IoNiV.mjs";
13
13
  import { a as diag, c as SpanKind$1, d as TraceFlags, f as ROOT_CONTEXT, i as propagation, l as isSpanContextValid, m as DiagLogLevel, n as init_esm$1, o as context$1, p as createContextKey, r as trace, s as SpanStatusCode, t as esm_exports$1 } from "./esm-wLIF-u3N.mjs";
14
14
  import { a as verifyCmmdAuthRuntimeTicket, c as verifyCmmdRuntimeTicket, d as setRuntimeGrantToken, f as createReadOnceFileTokenAccessor, i as sendRuntimeControlHeartbeat, l as verifyCmmdRuntimeTicketRefreshProof, n as bootstrapRuntimeControlAuthorityFromNarrowGrant, o as verifyCmmdClaudeReadinessTicket, r as readRuntimeControlAuthority, s as verifyCmmdClaudeUsageTicket, t as bootstrapRuntimeControlAuthority, u as runtimeGrantToken } from "./runtimeControlAuthority-D_zKr_eI.mjs";
15
- import { n as PtySpawnError, t as PtyAdapter } from "./PTY-rIPf52QR.mjs";
15
+ import { n as PtySpawnError, t as PtyAdapter } from "./PTY-BzdbZ0Ym.mjs";
16
16
  import { createRequire } from "node:module";
17
17
  import * as HE from "node:child_process";
18
18
  import { execFile, execFileSync, spawn, spawnSync } from "node:child_process";
@@ -22,7 +22,7 @@ import { createCipheriv, createDecipheriv, createHash, createHmac, createPublicK
22
22
  import * as FS$1 from "node:fs";
23
23
  import fs, { accessSync, chmodSync, constants, createReadStream, existsSync, mkdirSync, mkdtempSync, promises, readFile, readFileSync, readdir, readdirSync, realpathSync, rmSync, statSync, writeFileSync } from "node:fs";
24
24
  import * as NodeOS from "node:os";
25
- import os, { homedir, networkInterfaces, tmpdir } from "node:os";
25
+ import $U, { homedir, networkInterfaces, tmpdir } from "node:os";
26
26
  import * as Path$1 from "node:path";
27
27
  import path, { basename, delimiter, dirname, extname, isAbsolute, join, posix, relative, resolve, sep } from "node:path";
28
28
  import { pathToFileURL } from "node:url";
@@ -16520,7 +16520,7 @@ function makeNetServiceShape(createServer = Net.createServer) {
16520
16520
  }
16521
16521
  //#endregion
16522
16522
  //#region ../../packages/shared/src/releaseInfo.generated.ts
16523
- const FORGE_RELEASE_VERSION = "0.13.2";
16523
+ const FORGE_RELEASE_VERSION = "0.13.4";
16524
16524
  //#endregion
16525
16525
  //#region ../../packages/shared/src/hostProcess.ts
16526
16526
  /**
@@ -16644,6 +16644,70 @@ const CheckpointRef = makeEntityId("CheckpointRef");
16644
16644
  */
16645
16645
  const ThreadEnvMode = Literals(["local", "worktree"]);
16646
16646
  //#endregion
16647
+ //#region ../../packages/contracts/src/assets.ts
16648
+ /**
16649
+ * Addresses for the bytes a client cannot fetch by guessing.
16650
+ *
16651
+ * Forge already serves the bytes: attachments under `/attachments/*` and
16652
+ * project favicons at `GET /api/project-favicon?cwd=`. What it did not serve
16653
+ * was the question "what is the URL for this thing", which is the only way a
16654
+ * client that holds an attachment id or a workspace root can reach either.
16655
+ * The phone asks that question on the thread list for every project, so
16656
+ * until this existed the answer was an error the phone drew as a spinner.
16657
+ *
16658
+ * These schemas are the Forge side of `assets.*` in the vendored t3 contracts.
16659
+ * They must decode what that package encodes: same tags, same field names.
16660
+ */
16661
+ const ASSET_PATH_MAX_LENGTH = 1024;
16662
+ const ASSET_ID_MAX_LENGTH = 256;
16663
+ const ASSET_URL_MAX_LENGTH = 4096;
16664
+ const AssetResource = Union([
16665
+ TaggedStruct("workspace-file", {
16666
+ threadId: ThreadId,
16667
+ path: TrimmedNonEmptyString.check(isMaxLength(ASSET_PATH_MAX_LENGTH))
16668
+ }),
16669
+ TaggedStruct("attachment", { attachmentId: TrimmedNonEmptyString.check(isMaxLength(ASSET_ID_MAX_LENGTH)) }),
16670
+ TaggedStruct("project-favicon", {
16671
+ cwd: TrimmedNonEmptyString.check(isMaxLength(ASSET_PATH_MAX_LENGTH)),
16672
+ path: optional$4(TrimmedNonEmptyString.check(isMaxLength(ASSET_PATH_MAX_LENGTH)))
16673
+ })
16674
+ ]);
16675
+ const AssetCreateUrlInput = Struct({ resource: AssetResource });
16676
+ const AssetCreateUrlResult = Struct({
16677
+ relativeUrl: TrimmedNonEmptyString.check(isMaxLength(ASSET_URL_MAX_LENGTH)),
16678
+ expiresAt: Number$1,
16679
+ sourcePath: optional$4(TrimmedNonEmptyString.check(isMaxLength(ASSET_PATH_MAX_LENGTH)))
16680
+ });
16681
+ /**
16682
+ * The resource cannot be addressed by URL on this server.
16683
+ *
16684
+ * Tag and message match the vendored client's `AssetAccessError` union so the
16685
+ * phone decodes the failure rather than falling through to a generic transport
16686
+ * error. `workspace-file` is the case that lands here.
16687
+ */
16688
+ var AssetWorkspaceResolutionError = class extends TaggedErrorClass()("AssetWorkspaceResolutionError", {
16689
+ resource: AssetResource,
16690
+ cause: optional$4(Defect)
16691
+ }) {
16692
+ get message() {
16693
+ return "Failed to resolve workspace.";
16694
+ }
16695
+ };
16696
+ /** The signing key that makes an asset URL self-authenticating could not be read. */
16697
+ var AssetSigningKeyLoadError = class extends TaggedErrorClass()("AssetSigningKeyLoadError", {
16698
+ resource: AssetResource,
16699
+ cause: optional$4(Defect)
16700
+ }) {
16701
+ get message() {
16702
+ return "Failed to load the asset signing key.";
16703
+ }
16704
+ };
16705
+ const AssetAccessError = Union([AssetWorkspaceResolutionError, AssetSigningKeyLoadError]);
16706
+ const AssetsRpcSchemas = { createUrl: {
16707
+ input: AssetCreateUrlInput,
16708
+ output: AssetCreateUrlResult
16709
+ } };
16710
+ //#endregion
16647
16711
  //#region ../../node_modules/.bun/effect@4.0.0-beta.51/node_modules/effect/dist/unstable/rpc/RpcMiddleware.js
16648
16712
  /**
16649
16713
  * @since 4.0.0
@@ -18144,6 +18208,7 @@ const ExecutionEnvironmentCapabilities = Struct({
18144
18208
  threadPullRequestLinking: optionalKey(Boolean$1),
18145
18209
  cloudMode: Boolean$1.pipe(withDecodingDefault(succeed(false))),
18146
18210
  threadSettlement: optionalKey(Boolean$1),
18211
+ threadTitleRegeneration: optionalKey(Boolean$1),
18147
18212
  posthogPublicKey: NullOr(String$1).pipe(withDecodingDefault(succeed(null))),
18148
18213
  posthogHost: NullOr(String$1).pipe(withDecodingDefault(succeed(null))),
18149
18214
  runtime: optionalKey(ExecutionEnvironmentRuntimeCapabilities),
@@ -19141,6 +19206,51 @@ Struct({
19141
19206
  headRepositoryNameWithOwner: optional$4(NullOr(TrimmedNonEmptyString)),
19142
19207
  headRepositoryOwnerLogin: optional$4(NullOr(TrimmedNonEmptyString))
19143
19208
  });
19209
+ const SourceControlCloneProtocol = Literals([
19210
+ "auto",
19211
+ "ssh",
19212
+ "https"
19213
+ ]);
19214
+ const SourceControlRepositoryInfo = Struct({
19215
+ provider: SourceControlProviderKind$1,
19216
+ nameWithOwner: TrimmedNonEmptyString,
19217
+ url: TrimmedNonEmptyString,
19218
+ sshUrl: TrimmedNonEmptyString
19219
+ });
19220
+ const SourceControlRepositoryLookupInput = Struct({
19221
+ provider: SourceControlProviderKind$1,
19222
+ repository: TrimmedNonEmptyString,
19223
+ cwd: optional$4(TrimmedNonEmptyString)
19224
+ });
19225
+ /**
19226
+ * Either name the repository (`provider` + `repository`) or paste a clone URL
19227
+ * (`remoteUrl`). Both are optional in the schema because the phone sends one
19228
+ * or the other depending on how the user got here; the server rejects a call
19229
+ * that supplies neither, and rejects a `remoteUrl` it cannot resolve to a
19230
+ * repository it is allowed to fetch a token for.
19231
+ */
19232
+ const SourceControlCloneRepositoryInput = Struct({
19233
+ provider: optional$4(SourceControlProviderKind$1),
19234
+ repository: optional$4(TrimmedNonEmptyString),
19235
+ remoteUrl: optional$4(TrimmedNonEmptyString),
19236
+ destinationPath: TrimmedNonEmptyString,
19237
+ protocol: optional$4(SourceControlCloneProtocol)
19238
+ });
19239
+ const SourceControlCloneRepositoryResult = Struct({
19240
+ cwd: TrimmedNonEmptyString,
19241
+ remoteUrl: TrimmedNonEmptyString,
19242
+ repository: NullOr(SourceControlRepositoryInfo)
19243
+ });
19244
+ var SourceControlRepositoryError = class extends TaggedErrorClass()("SourceControlRepositoryError", {
19245
+ provider: SourceControlProviderKind$1,
19246
+ operation: String$1,
19247
+ detail: String$1,
19248
+ cause: optional$4(Defect)
19249
+ }) {
19250
+ get message() {
19251
+ return `Source control repository operation ${this.operation} failed for ${this.provider}: ${this.detail}`;
19252
+ }
19253
+ };
19144
19254
  //#endregion
19145
19255
  //#region ../../packages/contracts/src/orchestration.ts
19146
19256
  const ORCHESTRATION_WS_METHODS = {
@@ -19152,7 +19262,8 @@ const ORCHESTRATION_WS_METHODS = {
19152
19262
  replayEvents: "orchestration.replayEvents",
19153
19263
  getArchivedShellSnapshot: "orchestration.getArchivedShellSnapshot",
19154
19264
  subscribeShell: "orchestration.subscribeShell",
19155
- subscribeThread: "orchestration.subscribeThread"
19265
+ subscribeThread: "orchestration.subscribeThread",
19266
+ searchThreads: "orchestration.searchThreads"
19156
19267
  };
19157
19268
  const ProviderKind = Literals([
19158
19269
  "codex",
@@ -20508,6 +20619,31 @@ const OrchestrationGetFullThreadDiffInput = Struct({
20508
20619
  const OrchestrationGetFullThreadDiffResult = ThreadTurnDiff;
20509
20620
  const OrchestrationReplayEventsInput = Struct({ fromSequenceExclusive: NonNegativeInt });
20510
20621
  const OrchestrationReplayEventsResult = ArraySchema(OrchestrationEvent);
20622
+ /**
20623
+ * Which side of the conversation a search hit came from. `null`
20624
+ * `messageCreatedAt` on a match means it came from the thread's own metadata
20625
+ * (its title or its branch) rather than from a message.
20626
+ */
20627
+ const OrchestrationThreadSearchSource = Literals(["user", "assistant"]);
20628
+ /**
20629
+ * The server's SQLite client is synchronous and single-connection, so both the
20630
+ * scan input and the response size are bounded: a search must not be able to
20631
+ * monopolize the connection every keystroke.
20632
+ */
20633
+ const OrchestrationSearchThreadsInput = Struct({
20634
+ query: TrimmedString.check(isMinLength(2), isMaxLength(200)),
20635
+ limit: optionalKey(Int$1.check(isBetween({
20636
+ minimum: 1,
20637
+ maximum: 50
20638
+ })))
20639
+ });
20640
+ const OrchestrationSearchThreadsResult = Struct({ matches: ArraySchema(Struct({
20641
+ threadId: ThreadId,
20642
+ projectId: ProjectId,
20643
+ source: OrchestrationThreadSearchSource,
20644
+ snippet: String$1.check(isMaxLength(240)),
20645
+ messageCreatedAt: NullOr(IsoDateTime)
20646
+ })) });
20511
20647
  const ProgressPercent = NonNegativeInt.check(isLessThanOrEqualTo(100));
20512
20648
  const AgentProtocolMessage = Union([
20513
20649
  Struct({
@@ -20578,6 +20714,10 @@ const OrchestrationRpcSchemas = {
20578
20714
  getArchivedShellSnapshot: {
20579
20715
  input: Struct({}),
20580
20716
  output: OrchestrationShellSnapshot
20717
+ },
20718
+ searchThreads: {
20719
+ input: OrchestrationSearchThreadsInput,
20720
+ output: OrchestrationSearchThreadsResult
20581
20721
  }
20582
20722
  };
20583
20723
  var OrchestrationGetSnapshotError = class extends TaggedErrorClass()("OrchestrationGetSnapshotError", {
@@ -20600,6 +20740,10 @@ var OrchestrationReplayEventsError = class extends TaggedErrorClass()("Orchestra
20600
20740
  message: TrimmedNonEmptyString,
20601
20741
  cause: optional$4(Defect)
20602
20742
  }) {};
20743
+ var OrchestrationSearchThreadsError = class extends TaggedErrorClass()("OrchestrationSearchThreadsError", {
20744
+ message: TrimmedNonEmptyString,
20745
+ cause: optional$4(Defect)
20746
+ }) {};
20603
20747
  Struct({
20604
20748
  provider: ProviderKind,
20605
20749
  status: Literals([
@@ -20659,6 +20803,31 @@ const ProviderRespondToUserInputInput = Struct({
20659
20803
  requestId: ApprovalRequestId,
20660
20804
  answers: ProviderUserInputAnswers
20661
20805
  });
20806
+ /**
20807
+ * Sending a thread to the people who maintain Forge.
20808
+ *
20809
+ * The vendored client's `/feedback` slash command calls this. Upstream it
20810
+ * uploads the transcript to the model provider; Forge routes it to the same
20811
+ * CMMD feedback intake the web app's feedback dialog already posts to, so one
20812
+ * report lands in one place no matter which surface filed it.
20813
+ *
20814
+ * `reason` is what the person typed after the command, and is the only part
20815
+ * of the report they wrote. The server fills in the rest.
20816
+ */
20817
+ const ProviderUploadFeedbackInput = Struct({
20818
+ threadId: ThreadId,
20819
+ reason: optional$4(TrimmedNonEmptyString.check(isMaxLength(4e3)))
20820
+ });
20821
+ const ProviderUploadFeedbackResult = Struct({ feedbackId: TrimmedNonEmptyString });
20822
+ var ProviderUploadFeedbackError = class extends TaggedErrorClass()("ProviderUploadFeedbackError", {
20823
+ threadId: ThreadId,
20824
+ reason: optional$4(TrimmedNonEmptyString.check(isMaxLength(500))),
20825
+ cause: optional$4(Defect)
20826
+ }) {
20827
+ get message() {
20828
+ return this.reason ?? "Feedback could not be sent. Please try again.";
20829
+ }
20830
+ };
20662
20831
  Struct({
20663
20832
  id: EventId,
20664
20833
  kind: Literals([
@@ -21133,6 +21302,46 @@ const TaskStartedPayload = Struct({
21133
21302
  description: optional$4(TrimmedNonEmptyStringSchema$2),
21134
21303
  taskType: optional$4(TrimmedNonEmptyStringSchema$2)
21135
21304
  });
21305
+ /**
21306
+ * Watch-loop task types: Monitor-tool tasks plus background shells. A shell
21307
+ * that outlives its turn is in practice a watch loop, not delegated work.
21308
+ */
21309
+ const MONITOR_TASK_TYPES = new Set([
21310
+ "monitor",
21311
+ "monitor_mcp",
21312
+ "local_bash",
21313
+ "shell"
21314
+ ]);
21315
+ /** Task types that are neither agents nor watch loops (plan-mode bookkeeping). */
21316
+ const INERT_TASK_TYPES = new Set(["plan", "dream"]);
21317
+ /**
21318
+ * Agent-vs-background classification for a task row.
21319
+ *
21320
+ * The mobile client reads `agentKind` off the persisted activity payload and
21321
+ * treats a row without it as background, so a server that never stamps the
21322
+ * field leaves the Agents roster permanently empty however much subagent work
21323
+ * is running (FOR-36646). This is the stamp.
21324
+ *
21325
+ * A denylist, not an allowlist, and deliberately so: the agent-flavoured type
21326
+ * names drift with the SDKs (subagent, local_agent, local_workflow, and
21327
+ * whatever lands next), and an allowlist drops real subagents the day a new
21328
+ * name appears. Only the watch loops and the plan-mode bookkeeping types are
21329
+ * named here; everything else is delegated work.
21330
+ *
21331
+ * A task launched from inside a subagent (`agentId` set) is agent-internal
21332
+ * background work unless it is itself agent-flavoured, because a nested agent
21333
+ * can outlive its parent and has to stay in the roster.
21334
+ *
21335
+ * This mirrors `classifyTaskAgentKind` in the contracts the phone speaks. The
21336
+ * two packages do not import each other, so the sets are duplicated on
21337
+ * purpose; they have to agree, and the tests on both sides pin the same cases.
21338
+ */
21339
+ function classifyTaskAgentKind(input) {
21340
+ const { taskType, agentId } = input;
21341
+ const nonAgentType = taskType !== void 0 && (MONITOR_TASK_TYPES.has(taskType) || INERT_TASK_TYPES.has(taskType));
21342
+ if (agentId !== void 0 && agentId.trim().length > 0) return taskType === void 0 || nonAgentType ? "background" : "agent";
21343
+ return nonAgentType ? "background" : "agent";
21344
+ }
21136
21345
  const TaskProgressPayload = Struct({
21137
21346
  taskId: RuntimeTaskId,
21138
21347
  description: TrimmedNonEmptyStringSchema$2,
@@ -25715,6 +25924,8 @@ const WS_METHODS = {
25715
25924
  projectsListEntries: "projects.listEntries",
25716
25925
  serverDiscoverSourceControl: "server.discoverSourceControl",
25717
25926
  serverReportClientActivity: "server.reportClientActivity",
25927
+ sourceControlLookupRepository: "sourceControl.lookupRepository",
25928
+ sourceControlCloneRepository: "sourceControl.cloneRepository",
25718
25929
  projectsWriteFile: "projects.writeFile",
25719
25930
  projectCheckoutDeletionPreview: "projectCheckoutDeletion.preview",
25720
25931
  projectCheckoutDeletionExecute: "projectCheckoutDeletion.execute",
@@ -25818,7 +26029,9 @@ const WS_METHODS = {
25818
26029
  subscribeServerConfig: "subscribeServerConfig",
25819
26030
  subscribeServerLifecycle: "subscribeServerLifecycle",
25820
26031
  subscribeAuthAccess: "subscribeAuthAccess",
25821
- subscribeWorkspaceReadiness: "subscribeWorkspaceReadiness"
26032
+ subscribeWorkspaceReadiness: "subscribeWorkspaceReadiness",
26033
+ assetsCreateUrl: "assets.createUrl",
26034
+ providerUploadFeedback: "provider.uploadFeedback"
25822
26035
  };
25823
26036
  const WsServerUpsertKeybindingRpc = make$30(WS_METHODS.serverUpsertKeybinding, {
25824
26037
  payload: ServerUpsertKeybindingInput,
@@ -26347,6 +26560,29 @@ const WsOrchestrationGetArchivedShellSnapshotRpc = make$30(ORCHESTRATION_WS_METH
26347
26560
  success: OrchestrationRpcSchemas.getArchivedShellSnapshot.output,
26348
26561
  error: OrchestrationGetSnapshotError
26349
26562
  });
26563
+ const WsOrchestrationSearchThreadsRpc = make$30(ORCHESTRATION_WS_METHODS.searchThreads, {
26564
+ payload: OrchestrationSearchThreadsInput,
26565
+ success: OrchestrationRpcSchemas.searchThreads.output,
26566
+ error: OrchestrationSearchThreadsError
26567
+ });
26568
+ /**
26569
+ * Resolve `owner/repo` to the repository the caller is allowed to clone.
26570
+ *
26571
+ * Both of these run against the same CMMD-brokered GitHub credential the web
26572
+ * add-project flow uses, so a lookup that answers is a lookup whose clone can
26573
+ * also be authorized, and a repository the caller cannot reach is a failure
26574
+ * here rather than a clone that dies half way.
26575
+ */
26576
+ const WsSourceControlLookupRepositoryRpc = make$30(WS_METHODS.sourceControlLookupRepository, {
26577
+ payload: SourceControlRepositoryLookupInput,
26578
+ success: SourceControlRepositoryInfo,
26579
+ error: SourceControlRepositoryError
26580
+ });
26581
+ const WsSourceControlCloneRepositoryRpc = make$30(WS_METHODS.sourceControlCloneRepository, {
26582
+ payload: SourceControlCloneRepositoryInput,
26583
+ success: SourceControlCloneRepositoryResult,
26584
+ error: SourceControlRepositoryError
26585
+ });
26350
26586
  const WsOrchestrationSubscribeShellRpc = make$30(ORCHESTRATION_WS_METHODS.subscribeShell, {
26351
26587
  payload: OrchestrationRpcSchemas.subscribeShell.input,
26352
26588
  success: OrchestrationRpcSchemas.subscribeShell.output,
@@ -26427,7 +26663,15 @@ const WsRpcGroup = make$29(WsServerGetConfigRpc, WsServerRefreshProvidersRpc, Ws
26427
26663
  payload: UsageSummaryInput,
26428
26664
  success: UsageSummary,
26429
26665
  error: UsageReadError
26430
- }), 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);
26666
+ }), 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, {
26667
+ payload: AssetsRpcSchemas.createUrl.input,
26668
+ success: AssetsRpcSchemas.createUrl.output,
26669
+ error: AssetAccessError
26670
+ }), make$30(WS_METHODS.providerUploadFeedback, {
26671
+ payload: ProviderUploadFeedbackInput,
26672
+ success: ProviderUploadFeedbackResult,
26673
+ error: ProviderUploadFeedbackError
26674
+ }), WsOrchestrationSearchThreadsRpc, WsSourceControlLookupRepositoryRpc, WsSourceControlCloneRepositoryRpc);
26431
26675
  const RelayWsRpcGroup = WsRpcGroup.middleware(RelayAuthorizationMiddleware).middleware(RelayDeliveryMiddleware);
26432
26676
  /**
26433
26677
  * The direct-WebSocket mount of the same group, with per-method scope
@@ -31559,6 +31803,31 @@ const RELAY_METHOD_POLICY_BASES = [
31559
31803
  method: ORCHESTRATION_WS_METHODS.subscribeThread,
31560
31804
  requiredScope: "thread.read",
31561
31805
  deliveryClass: "subscription"
31806
+ },
31807
+ {
31808
+ method: WS_METHODS.assetsCreateUrl,
31809
+ requiredScope: "thread.read",
31810
+ deliveryClass: "read"
31811
+ },
31812
+ {
31813
+ method: ORCHESTRATION_WS_METHODS.searchThreads,
31814
+ requiredScope: "thread.read",
31815
+ deliveryClass: "read"
31816
+ },
31817
+ {
31818
+ method: WS_METHODS.providerUploadFeedback,
31819
+ requiredScope: "thread.read",
31820
+ deliveryClass: "create-command"
31821
+ },
31822
+ {
31823
+ method: WS_METHODS.sourceControlLookupRepository,
31824
+ requiredScope: "git.read",
31825
+ deliveryClass: "read"
31826
+ },
31827
+ {
31828
+ method: WS_METHODS.sourceControlCloneRepository,
31829
+ requiredScope: "project.write",
31830
+ deliveryClass: "idempotent-command"
31562
31831
  }
31563
31832
  ];
31564
31833
  const FRAME_JOURNALED_METHODS = new Set([
@@ -33726,7 +33995,7 @@ function isRejectedCredential(cause) {
33726
33995
  //#region src/connect/hiddenInput.ts
33727
33996
  /** Ctrl-C arrives as this byte on a raw TTY rather than as a signal. */
33728
33997
  const END_OF_TEXT = "";
33729
- function firstLine(value) {
33998
+ function firstLine$1(value) {
33730
33999
  const index = value.indexOf("\n");
33731
34000
  const line = index === -1 ? value : value.slice(0, index);
33732
34001
  return line.endsWith("\r") ? line.slice(0, -1) : line;
@@ -33751,7 +34020,7 @@ function readHiddenLine(input) {
33751
34020
  settled = true;
33752
34021
  detach();
33753
34022
  input.output.write("\n");
33754
- resolve(firstLine(value));
34023
+ resolve(firstLine$1(value));
33755
34024
  };
33756
34025
  const onData = (chunk) => {
33757
34026
  buffer += chunk.toString();
@@ -35183,7 +35452,7 @@ function stringify(value, fallback = "[unserializable]") {
35183
35452
  return typeof fallback === "function" ? fallback(value) : fallback;
35184
35453
  }
35185
35454
  }
35186
- function truncate$1(str, max = 0) {
35455
+ function truncate$2(str, max = 0) {
35187
35456
  if (typeof str !== "string" || max === 0) return str;
35188
35457
  return str.length <= max ? str : `${str.slice(0, max)}...`;
35189
35458
  }
@@ -35790,7 +36059,7 @@ var Scope = class Scope {
35790
36059
  const mergedBreadcrumb = {
35791
36060
  timestamp: dateTimestampInSeconds(),
35792
36061
  ...breadcrumb,
35793
- message: breadcrumb.message ? truncate$1(breadcrumb.message, 2048) : breadcrumb.message
36062
+ message: breadcrumb.message ? truncate$2(breadcrumb.message, 2048) : breadcrumb.message
35794
36063
  };
35795
36064
  this._breadcrumbs.push(mergedBreadcrumb);
35796
36065
  if (this._breadcrumbs.length > maxCrumbs) {
@@ -38448,9 +38717,9 @@ function applyClientOptions(event, options) {
38448
38717
  if (!event.release && release) event.release = release;
38449
38718
  if (!event.dist && dist) event.dist = dist;
38450
38719
  const request = event.request;
38451
- if (request?.url && maxValueLength) request.url = truncate$1(request.url, maxValueLength);
38720
+ if (request?.url && maxValueLength) request.url = truncate$2(request.url, maxValueLength);
38452
38721
  if (maxValueLength) event.exception?.values?.forEach((exception) => {
38453
- if (exception.value) exception.value = truncate$1(exception.value, maxValueLength);
38722
+ if (exception.value) exception.value = truncate$2(exception.value, maxValueLength);
38454
38723
  });
38455
38724
  }
38456
38725
  function applyDebugIds(event, stackParser) {
@@ -50194,7 +50463,7 @@ function _supportsColor(haveStream, { streamIsTTY, sniffFlags = true } = {}) {
50194
50463
  const min = forceColor || 0;
50195
50464
  if (env.TERM === "dumb") return min;
50196
50465
  if (process$1.platform === "win32") {
50197
- const osRelease = os.release().split(".");
50466
+ const osRelease = $U.release().split(".");
50198
50467
  if (Number(osRelease[0]) >= 10 && Number(osRelease[2]) >= 10586) return Number(osRelease[2]) >= 14931 ? 3 : 2;
50199
50468
  return 1;
50200
50469
  }
@@ -52637,7 +52906,7 @@ var require_ProcessDetector = /* @__PURE__ */ __commonJSMin(((exports) => {
52637
52906
  exports.processDetector = void 0;
52638
52907
  const api_1 = (init_esm$1(), __toCommonJS(esm_exports$1));
52639
52908
  const semconv_1 = require_semconv$1();
52640
- const os$4 = __require("os");
52909
+ const os$3 = __require("os");
52641
52910
  /**
52642
52911
  * ProcessDetector will be used to detect the resources related current process running
52643
52912
  * and being instrumented from the NodeJS Process module.
@@ -52659,7 +52928,7 @@ var require_ProcessDetector = /* @__PURE__ */ __commonJSMin(((exports) => {
52659
52928
  };
52660
52929
  if (process.argv.length > 1) attributes[semconv_1.ATTR_PROCESS_COMMAND] = process.argv[1];
52661
52930
  try {
52662
- const userInfo = os$4.userInfo();
52931
+ const userInfo = os$3.userInfo();
52663
52932
  attributes[semconv_1.ATTR_PROCESS_OWNER] = userInfo.username;
52664
52933
  } catch (e) {
52665
52934
  api_1.diag.debug(`error obtaining process owner: ${e}`);
@@ -62067,7 +62336,7 @@ const cacheResponseHook = (span, redisCommand, cmdArgs, response) => {
62067
62336
  [SEMANTIC_ATTRIBUTE_CACHE_KEY]: safeKey
62068
62337
  });
62069
62338
  const spanDescription = safeKey.join(", ");
62070
- span.updateName(_redisOptions.maxCacheKeyLength ? truncate$1(spanDescription, _redisOptions.maxCacheKeyLength) : spanDescription);
62339
+ span.updateName(_redisOptions.maxCacheKeyLength ? truncate$2(spanDescription, _redisOptions.maxCacheKeyLength) : spanDescription);
62071
62340
  };
62072
62341
  //#endregion
62073
62342
  //#region ../../node_modules/.bun/@sentry+node@10.70.0+cfd4f484ffd29da6/node_modules/@sentry/node/build/esm/integrations/tracing/redis/vendored/semconv.js
@@ -65582,7 +65851,7 @@ function normalizeNumberish(value) {
65582
65851
  }
65583
65852
  //#endregion
65584
65853
  //#region package.json
65585
- var version$1 = "0.13.2";
65854
+ var version$1 = "0.13.4";
65586
65855
  //#endregion
65587
65856
  //#region src/sentry.ts
65588
65857
  const SERVER_APP_NAME = "forge-server";
@@ -69101,6 +69370,102 @@ var BrowserTraceCollector = class extends Service$1()("forge/observability/Servi
69101
69370
  */
69102
69371
  var ProjectFaviconResolver = class extends Service$1()("forge/project/Services/ProjectFaviconResolver") {};
69103
69372
  //#endregion
69373
+ //#region src/auth/Services/ServerSecretStore.ts
69374
+ var SecretStoreError = class extends TaggedError("SecretStoreError") {};
69375
+ var ServerSecretStore = class extends Service$1()("forge/auth/Services/ServerSecretStore") {};
69376
+ //#endregion
69377
+ //#region src/auth/assetUrlToken.ts
69378
+ /**
69379
+ * A credential an image tag can carry.
69380
+ *
69381
+ * `/attachments/*` and `GET /api/project-favicon` both require an authenticated
69382
+ * request, and the clients that render those bytes hand a bare URL to an image
69383
+ * loader that sends no cookie and no Authorization header. Web gets away with
69384
+ * it because the browser attaches the session cookie itself. The phone holds a
69385
+ * bearer token it has no way to attach, so every asset it fetched would 401.
69386
+ *
69387
+ * So the URL carries its own proof. The token is signed with the same server
69388
+ * signing key as the session and WebSocket credentials, and it is deliberately
69389
+ * weaker than either of those: it names one resource, and presenting it grants
69390
+ * that resource and nothing else. A leaked favicon URL is a leaked favicon.
69391
+ */
69392
+ const ASSET_TOKEN_KIND = "asset";
69393
+ const ASSET_URL_TOKEN_QUERY_PARAM = "assetToken";
69394
+ /**
69395
+ * An hour, against a client that re-asks every thirty minutes.
69396
+ *
69397
+ * The vendored client refreshes asset URLs on a thirty-minute interval, so a
69398
+ * shorter life would expire URLs the client still believes are good, and the
69399
+ * failure would show up as an image that worked and then stopped.
69400
+ */
69401
+ const DEFAULT_ASSET_URL_TOKEN_TTL = hours(1);
69402
+ const SIGNING_SECRET_NAME$1 = "server-signing-key";
69403
+ const decodeAssetTokenClaims = decodeUnknownEffect(fromJsonString(Struct({
69404
+ v: Literal(1),
69405
+ kind: Literal(ASSET_TOKEN_KIND),
69406
+ sid: String$1,
69407
+ res: String$1,
69408
+ iat: Number$1,
69409
+ exp: Number$1
69410
+ })));
69411
+ /**
69412
+ * The canonical name of one addressable asset.
69413
+ *
69414
+ * Both the mint and the check compute this from their own side: the RPC from
69415
+ * the resource it was asked about, the route from the request it received. They
69416
+ * have to agree exactly, which is the point. A token minted for one attachment
69417
+ * cannot be replayed against another, or against a favicon.
69418
+ *
69419
+ * The key is built from the address, not the logical id, because the address is
69420
+ * what both sides actually hold. The attachments route reads its id straight
69421
+ * out of the URL path without percent-decoding, so keying on the decoded id
69422
+ * would let the two sides disagree about an id that encodes to something else.
69423
+ */
69424
+ function assetResourceKey(resource) {
69425
+ return resource.kind === "attachment" ? `attachment:${resource.urlPathSegment}` : `project-favicon:${resource.cwd}`;
69426
+ }
69427
+ const resolveSigningSecret = gen(function* () {
69428
+ const serverConfig = yield* ServerConfig;
69429
+ const secretStore = yield* ServerSecretStore;
69430
+ return resolveConfiguredSigningKey(serverConfig.sessionSigningKey) ?? (yield* secretStore.getOrCreateRandom(SIGNING_SECRET_NAME$1, 32));
69431
+ });
69432
+ const issueAssetUrlToken = (input) => gen(function* () {
69433
+ const secret = yield* resolveSigningSecret;
69434
+ const issuedAt = yield* now;
69435
+ const expiresAt = add$3(issuedAt, { milliseconds: toMillis(input.ttl ?? DEFAULT_ASSET_URL_TOKEN_TTL) });
69436
+ const claims = {
69437
+ v: 1,
69438
+ kind: ASSET_TOKEN_KIND,
69439
+ sid: input.sessionId,
69440
+ res: input.resourceKey,
69441
+ iat: issuedAt.epochMilliseconds,
69442
+ exp: expiresAt.epochMilliseconds
69443
+ };
69444
+ const encodedPayload = base64UrlEncode(JSON.stringify(claims));
69445
+ return {
69446
+ token: `${encodedPayload}.${signPayload(encodedPayload, secret)}`,
69447
+ expiresAt: expiresAt.epochMilliseconds
69448
+ };
69449
+ });
69450
+ /**
69451
+ * Whether this token opens this resource, right now.
69452
+ *
69453
+ * Returns a boolean rather than failing, because every negative answer means
69454
+ * the same thing to the caller: fall through to ordinary request
69455
+ * authentication. A malformed token from a stale client should behave like no
69456
+ * token at all, not like a different error.
69457
+ */
69458
+ const verifyAssetUrlToken = (input) => gen(function* () {
69459
+ const [encodedPayload, signature] = input.token.split(".");
69460
+ if (!encodedPayload || !signature) return false;
69461
+ if (!timingSafeEqualBase64Url(signature, signPayload(encodedPayload, yield* resolveSigningSecret))) return false;
69462
+ const claims = yield* decodeAssetTokenClaims(base64UrlDecodeUtf8(encodedPayload)).pipe(orElseSucceed(() => null));
69463
+ if (!claims) return false;
69464
+ if (claims.res !== input.resourceKey) return false;
69465
+ const now = yield* currentTimeMillis;
69466
+ return claims.exp > now;
69467
+ }).pipe(orElseSucceed(() => false));
69468
+ //#endregion
69104
69469
  //#region src/environment/Services/ServerEnvironment.ts
69105
69470
  var ServerEnvironment = class extends Service$1()("forge/environment/Services/ServerEnvironment") {};
69106
69471
  //#endregion
@@ -69375,6 +69740,28 @@ const requireAuthenticatedRequest$1 = gen(function* () {
69375
69740
  const request = yield* HttpServerRequest;
69376
69741
  yield* (yield* ServerAuth).authenticateHttpRequest(request);
69377
69742
  });
69743
+ /**
69744
+ * Authenticate a request for asset bytes.
69745
+ *
69746
+ * Ordinary request authentication first, so the web app's cookie keeps working
69747
+ * exactly as it did and nothing about these routes changes for it. An asset
69748
+ * token is the fallback, for the clients that render these bytes through an
69749
+ * image loader with no way to attach a credential. See `auth/assetUrlToken.ts`
69750
+ * for why the token is scoped to one resource.
69751
+ */
69752
+ const requireAssetAccess = (resourceKey) => gen(function* () {
69753
+ const request = yield* HttpServerRequest;
69754
+ const serverAuth = yield* ServerAuth;
69755
+ const url = toURL(request);
69756
+ const token = isSome(url) ? url.value.searchParams.get(ASSET_URL_TOKEN_QUERY_PARAM) : null;
69757
+ if (token && token.trim().length > 0) {
69758
+ if (yield* verifyAssetUrlToken({
69759
+ token,
69760
+ resourceKey
69761
+ })) return;
69762
+ }
69763
+ yield* serverAuth.authenticateHttpRequest(request);
69764
+ });
69378
69765
  const serverEnvironmentRouteLayer = add$1("GET", "/.well-known/forge/environment", gen(function* () {
69379
69766
  return jsonUnsafe(yield* service(ServerEnvironment).pipe(flatMap((serverEnvironment) => serverEnvironment.getDescriptor)), { status: 200 });
69380
69767
  }));
@@ -69443,12 +69830,15 @@ const otlpTracesProxyRouteLayer = add$1("POST", OTLP_TRACES_PROXY_PATH, gen(func
69443
69830
  captureServerError(cause, { route: "/api/observability/v1/traces" });
69444
69831
  }).pipe(andThen(() => logError("otlp-traces unhandled cause", { cause }))))));
69445
69832
  const attachmentsRouteLayer = add$1("GET", `${ATTACHMENTS_ROUTE_PREFIX}/*`, gen(function* () {
69446
- yield* requireAuthenticatedRequest$1;
69447
69833
  const url = toURL(yield* HttpServerRequest);
69448
69834
  if (isNone(url)) return text$2("Bad Request", { status: 400 });
69449
69835
  const config = yield* ServerConfig;
69450
69836
  const normalizedRelativePath = normalizeAttachmentRelativePath(url.value.pathname.slice(12));
69451
69837
  if (!normalizedRelativePath) return text$2("Invalid attachment path", { status: 400 });
69838
+ yield* requireAssetAccess(assetResourceKey({
69839
+ kind: "attachment",
69840
+ urlPathSegment: normalizedRelativePath
69841
+ }));
69452
69842
  const isIdLookup = !normalizedRelativePath.includes("/") && !normalizedRelativePath.includes(".");
69453
69843
  const filePath = isIdLookup ? resolveAttachmentPathById({
69454
69844
  attachmentsDir: config.attachmentsDir,
@@ -69466,12 +69856,14 @@ const attachmentsRouteLayer = add$1("GET", `${ATTACHMENTS_ROUTE_PREFIX}/*`, gen(
69466
69856
  }).pipe(catch_(() => succeed(text$2("Internal Server Error", { status: 500 }))));
69467
69857
  }).pipe(catchTag("AuthError", respondToAuthError)));
69468
69858
  const projectFaviconRouteLayer = add$1("GET", "/api/project-favicon", gen(function* () {
69469
- const request = yield* HttpServerRequest;
69470
- yield* (yield* ServerAuth).authenticateHttpRequest(request);
69471
- const url = toURL(request);
69859
+ const url = toURL(yield* HttpServerRequest);
69472
69860
  if (isNone(url)) return text$2("Bad Request", { status: 400 });
69473
69861
  const projectCwd = url.value.searchParams.get("cwd");
69474
69862
  if (!projectCwd) return text$2("Missing cwd parameter", { status: 400 });
69863
+ yield* requireAssetAccess(assetResourceKey({
69864
+ kind: "project-favicon",
69865
+ cwd: projectCwd
69866
+ }));
69475
69867
  const faviconFilePath = yield* (yield* ProjectFaviconResolver).resolvePath(projectCwd);
69476
69868
  if (!faviconFilePath) return empty$6({
69477
69869
  status: 204,
@@ -74334,7 +74726,7 @@ var require_node_gyp_build = /* @__PURE__ */ __commonJSMin(((exports, module) =>
74334
74726
  var fs$6 = __require("fs");
74335
74727
  var path$5 = __require("path");
74336
74728
  var url$1 = __require("url");
74337
- var os$3 = __require("os");
74729
+ var os$2 = __require("os");
74338
74730
  var runtimeRequire = typeof __webpack_require__ === "function" ? __non_webpack_require__ : __require;
74339
74731
  var vars = process.config && process.config.variables || {};
74340
74732
  var prebuildsOnly = !!process.env.PREBUILDS_ONLY;
@@ -74342,8 +74734,8 @@ var require_node_gyp_build = /* @__PURE__ */ __commonJSMin(((exports, module) =>
74342
74734
  var abi = versions.modules;
74343
74735
  if (versions.deno || process.isBun) abi = "unsupported";
74344
74736
  var runtime = isElectron() ? "electron" : isNwjs() ? "node-webkit" : "node";
74345
- var arch = process.env.npm_config_arch || os$3.arch();
74346
- var platform = process.env.npm_config_platform || os$3.platform();
74737
+ var arch = process.env.npm_config_arch || os$2.arch();
74738
+ var platform = process.env.npm_config_platform || os$2.platform();
74347
74739
  var libc = process.env.LIBC || (isMusl(platform) ? "musl" : "glibc");
74348
74740
  var armv = process.env.ARM_VERSION || (arch === "arm64" ? "8" : vars.arm_version) || "";
74349
74741
  var uv = (versions.uv || "").split(".")[0];
@@ -77193,6 +77585,195 @@ succeed$2(TraceDiagnostics, TraceDiagnostics.of({ read: (options) => succeed(mak
77193
77585
  }
77194
77586
  })) }));
77195
77587
  //#endregion
77588
+ //#region src/assets/createAssetUrl.ts
77589
+ const PROJECT_FAVICON_ROUTE = "/api/project-favicon";
77590
+ /**
77591
+ * Turn a resource into an address a client can fetch.
77592
+ *
77593
+ * Nothing new is served here. `/attachments/*` and `GET /api/project-favicon`
77594
+ * already return these bytes; what did not exist was the step in between, and
77595
+ * without it a client holding an attachment id or a workspace root has no way
77596
+ * to name the URL. The vendored mobile client asks this for every project on
77597
+ * its thread list, so the gap was not an edge case, it was the most visited
77598
+ * screen.
77599
+ *
77600
+ * The URL is relative so it resolves against whichever base the client is
77601
+ * already talking to, and it carries a token because the clients that render
77602
+ * these bytes hand the URL to an image loader that attaches no credential of
77603
+ * its own. See `auth/assetUrlToken.ts`.
77604
+ */
77605
+ const createAssetUrl = (sessionId, resource) => gen(function* () {
77606
+ if (resource._tag === "workspace-file") return yield* new AssetWorkspaceResolutionError({
77607
+ resource,
77608
+ cause: "Workspace files are read over projects.readFile, not by URL."
77609
+ });
77610
+ const address = resolveAddress(resource);
77611
+ const issued = yield* issueAssetUrlToken({
77612
+ sessionId,
77613
+ resourceKey: address.resourceKey
77614
+ }).pipe(mapError((cause) => new AssetSigningKeyLoadError({
77615
+ resource,
77616
+ cause
77617
+ })));
77618
+ return {
77619
+ relativeUrl: `${address.path}?${address.query}assetToken=${encodeURIComponent(issued.token)}`,
77620
+ expiresAt: issued.expiresAt,
77621
+ ...resource._tag === "project-favicon" && resource.path ? { sourcePath: resource.path } : {}
77622
+ };
77623
+ });
77624
+ function resolveAddress(resource) {
77625
+ if (resource._tag === "attachment") {
77626
+ const urlPathSegment = encodeURIComponent(resource.attachmentId);
77627
+ return {
77628
+ resourceKey: assetResourceKey({
77629
+ kind: "attachment",
77630
+ urlPathSegment
77631
+ }),
77632
+ path: `${ATTACHMENTS_ROUTE_PREFIX}/${urlPathSegment}`,
77633
+ query: ""
77634
+ };
77635
+ }
77636
+ return {
77637
+ resourceKey: assetResourceKey({
77638
+ kind: "project-favicon",
77639
+ cwd: resource.cwd
77640
+ }),
77641
+ path: PROJECT_FAVICON_ROUTE,
77642
+ query: `cwd=${encodeURIComponent(resource.cwd)}&`
77643
+ };
77644
+ }
77645
+ //#endregion
77646
+ //#region src/cmmd/submitFeedback.ts
77647
+ /** The intake the web app's feedback dialog already posts to, reached directly. */
77648
+ const CMMD_FEEDBACK_PATH = "/api/forge/context/feedback";
77649
+ /**
77650
+ * The intake answers with the task it created.
77651
+ *
77652
+ * Web reaches it through the proxy registry, which unwraps `data` before the
77653
+ * caller sees it; this calls the gateway directly and does not, so both shapes
77654
+ * are read here rather than pinning the one that happens to arrive today.
77655
+ */
77656
+ function readFeedbackId(response) {
77657
+ if (!response || typeof response !== "object") return null;
77658
+ const record = response;
77659
+ const envelope = record["data"];
77660
+ const id = (envelope && typeof envelope === "object" ? envelope : record)["id"];
77661
+ if (typeof id === "number") return String(id);
77662
+ if (typeof id === "string" && id.trim().length > 0) return id.trim();
77663
+ return null;
77664
+ }
77665
+ /**
77666
+ * File feedback against the caller's own CMMD account.
77667
+ *
77668
+ * The session's CMMD JWT is what scopes it. A server with no CMMD connection
77669
+ * has nowhere to send feedback and says so in a sentence, because the caller
77670
+ * draws this text straight into the thread the person is looking at.
77671
+ */
77672
+ function submitCmmdFeedbackForSession(input) {
77673
+ return gen(function* () {
77674
+ const jwt = yield* resolveCmmdJwtForSession({
77675
+ sessionId: input.sessionId,
77676
+ ...input.subject === void 0 ? {} : { subject: input.subject },
77677
+ logPrefix: "cmmd-feedback"
77678
+ }).pipe(orElseSucceed(() => null));
77679
+ if (!jwt) return {
77680
+ ok: false,
77681
+ reason: "This server is not connected to CMMD, so there is nowhere to send feedback."
77682
+ };
77683
+ const body = {
77684
+ type: "bug",
77685
+ title: input.title,
77686
+ source: "forge",
77687
+ priority: "medium",
77688
+ ...input.description ? { description: input.description } : {},
77689
+ ...input.senderName ? { senderName: input.senderName } : {}
77690
+ };
77691
+ const response = yield* tryPromise(() => cmmdGateway.request({
77692
+ method: "POST",
77693
+ jwt,
77694
+ path: CMMD_FEEDBACK_PATH,
77695
+ body
77696
+ })).pipe(orElseSucceed(() => null));
77697
+ if (response === null) return {
77698
+ ok: false,
77699
+ reason: "The feedback service did not accept the report. Please try again."
77700
+ };
77701
+ const feedbackId = readFeedbackId(response);
77702
+ if (feedbackId === null) return {
77703
+ ok: false,
77704
+ reason: "The feedback service answered without recording a report."
77705
+ };
77706
+ return {
77707
+ ok: true,
77708
+ feedbackId
77709
+ };
77710
+ });
77711
+ }
77712
+ //#endregion
77713
+ //#region src/provider/threadFeedback.ts
77714
+ const FEEDBACK_TITLE_MAX_CHARS = 120;
77715
+ function firstLine(text) {
77716
+ return text.split("\n", 1)[0]?.trim() ?? "";
77717
+ }
77718
+ function truncate$1(text, max) {
77719
+ return text.length <= max ? text : `${text.slice(0, max - 1).trimEnd()}…`;
77720
+ }
77721
+ /**
77722
+ * A title someone triaging the intake can act on.
77723
+ *
77724
+ * What the person typed after `/feedback` first, because it is the only part
77725
+ * they wrote. The thread's title is the fallback, and a bare thread id is the
77726
+ * last resort, since a queue of "Forge feedback" rows tells a triager nothing.
77727
+ */
77728
+ function feedbackTitle(input) {
77729
+ const reason = input.reason ? firstLine(input.reason) : "";
77730
+ if (reason.length > 0) return truncate$1(reason, FEEDBACK_TITLE_MAX_CHARS);
77731
+ const threadTitle = input.threadTitle?.trim() ?? "";
77732
+ if (threadTitle.length > 0) return truncate$1(`Forge feedback: ${threadTitle}`, FEEDBACK_TITLE_MAX_CHARS);
77733
+ return truncate$1(`Forge feedback: thread ${input.threadId}`, FEEDBACK_TITLE_MAX_CHARS);
77734
+ }
77735
+ /** The body, which always names the thread so the report can be traced back to it. */
77736
+ function feedbackDescription(input) {
77737
+ const lines = [`Filed from Forge thread \`${input.threadId}\`.`];
77738
+ if (input.threadTitle?.trim()) lines.push(`Thread title: ${input.threadTitle.trim()}`);
77739
+ if (input.reason?.trim()) lines.push("", input.reason.trim());
77740
+ return lines.join("\n");
77741
+ }
77742
+ /**
77743
+ * `/feedback`, served.
77744
+ *
77745
+ * The vendored client declared this method and called it; Forge answered
77746
+ * nothing, so the slash command printed the transport's own "unknown request
77747
+ * tag" string into the thread. Routing it to the CMMD intake the web feedback
77748
+ * dialog already uses means one report format and one destination, whichever
77749
+ * surface it came from.
77750
+ *
77751
+ * Every failure carries a sentence rather than a cause, because the client
77752
+ * draws `message` straight into the thread as an assistant reply.
77753
+ */
77754
+ const uploadThreadFeedback = (orchestrationEngine, sessionId, subject, input) => gen(function* () {
77755
+ const threadTitle = (yield* orchestrationEngine.getReadModel().pipe(orElseSucceed(() => null)))?.threads.find((thread) => thread.id === input.threadId)?.title;
77756
+ const outcome = yield* submitCmmdFeedbackForSession({
77757
+ sessionId,
77758
+ ...subject === void 0 ? {} : { subject },
77759
+ title: feedbackTitle({
77760
+ ...input,
77761
+ threadId: input.threadId,
77762
+ ...threadTitle ? { threadTitle } : {}
77763
+ }),
77764
+ description: feedbackDescription({
77765
+ ...input,
77766
+ threadId: input.threadId,
77767
+ ...threadTitle ? { threadTitle } : {}
77768
+ })
77769
+ });
77770
+ if (!outcome.ok) return yield* new ProviderUploadFeedbackError({
77771
+ threadId: input.threadId,
77772
+ reason: outcome.reason
77773
+ });
77774
+ return { feedbackId: outcome.feedbackId };
77775
+ });
77776
+ //#endregion
77196
77777
  //#region src/cloudRuntime/listCloudRuntimes.ts
77197
77778
  /**
77198
77779
  * List the caller's Forge Cloud runtimes, through CMMD, as the caller.
@@ -87222,95 +87803,278 @@ function resolveGitHubCloneDir(baseDir, repoFullName, targetDir) {
87222
87803
  const repoName = repoFullName.split("/")[1] ?? repoFullName;
87223
87804
  return targetDir ?? join(baseDir, "Projects", repoName);
87224
87805
  }
87225
- const githubCloneRouteLayer = add$1("POST", "/.well-known/forge/github/clone", gen(function* () {
87226
- const request = yield* HttpServerRequest;
87227
- const session = yield* (yield* ServerAuth).authenticateHttpRequest(request);
87228
- const config = yield* ServerConfig;
87229
- if (session.method === "proxy-asserted") {
87230
- const forwardedJwt = normalizeForwardedCmmdJwt(request.headers);
87231
- if (forwardedJwt !== void 0) yield* setCommandJwtForSession(session.sessionId, forwardedJwt).pipe(ignore({ log: true }));
87232
- }
87233
- const runtimeGrantAuth = resolveRuntimeGitHubGrantAuth(config, request.headers);
87234
- const jwt = runtimeGrantAuth ? null : yield* resolveJwt(session.sessionId, session.subject);
87235
- if (!runtimeGrantAuth && !jwt) return json$6({ error: "CMMD not connected" }, 401);
87236
- const rawBody = yield* request.json;
87237
- const parsed = decodeUnknownExit(CloneBodySchema)(rawBody);
87238
- if (parsed._tag === "Failure") return json$6({ error: "repo_full_name is required" }, 400);
87239
- const { repo_full_name, target_dir } = parsed.value;
87240
- let token = yield* fetchInstallationTokenForAuth(runtimeGrantAuth ?? {
87241
- kind: "cmmd-jwt",
87242
- jwt
87243
- }, repo_full_name);
87244
- if (!runtimeGrantAuth && !token.ok && shouldRetryWithRefreshedCmmdJwt(token.status)) {
87245
- const refreshedJwt = yield* resolveJwt(session.sessionId, session.subject, true);
87246
- if (refreshedJwt && refreshedJwt !== jwt) token = yield* fetchInstallationTokenForAuth({
87806
+ /**
87807
+ * The public clone URLs for a repository.
87808
+ *
87809
+ * Never derived from an authenticated remote: `buildAuthenticatedRemoteUrl`
87810
+ * embeds an installation token in the URL, and a token that reaches a client is
87811
+ * a token that reaches wherever the client writes it down.
87812
+ */
87813
+ function describeGitHubRepository(nameWithOwner) {
87814
+ return {
87815
+ nameWithOwner,
87816
+ url: `https://github.com/${nameWithOwner}`,
87817
+ sshUrl: `git@github.com:${nameWithOwner}.git`
87818
+ };
87819
+ }
87820
+ /**
87821
+ * Resolve `owner/repo` against the repositories CMMD says this caller may use.
87822
+ *
87823
+ * This asks the same brokered listing the web repository picker is populated
87824
+ * from, rather than github.com directly, so "found" here means the same thing
87825
+ * it means everywhere else in Forge: CMMD will vend this caller a token for it.
87826
+ * A repository that exists but that the caller cannot reach is a 404, which is
87827
+ * the answer they can act on.
87828
+ */
87829
+ function lookupGitHubRepositoryForSession(input) {
87830
+ return gen(function* () {
87831
+ const requested = input.repository.trim().replace(/^\/+|\/+$/g, "");
87832
+ if (!/^[^/\s]+\/[^/\s]+$/.test(requested)) return {
87833
+ status: 400,
87834
+ repository: null,
87835
+ error: "Enter a repository as owner/name."
87836
+ };
87837
+ const jwt = input.runtimeGrantAuth ? null : yield* resolveJwt(input.sessionId, input.subject);
87838
+ if (!input.runtimeGrantAuth && !jwt) return {
87839
+ status: 401,
87840
+ repository: null,
87841
+ error: "CMMD not connected"
87842
+ };
87843
+ const auth = input.runtimeGrantAuth ?? {
87247
87844
  kind: "cmmd-jwt",
87248
- jwt: refreshedJwt
87249
- }, repo_full_name);
87250
- }
87251
- if (!token.ok) return json$6(gitHubTokenFailureBody(token.status, token.error), token.status);
87252
- const gitToken = token.token;
87253
- const cloneDir = resolveGitHubCloneDir(config.baseDir, repo_full_name, target_dir);
87254
- yield* promise(() => mkdir(join(cloneDir, ".."), { recursive: true }).catch(() => void 0));
87255
- if (existsSync(cloneDir)) {
87256
- const workTree = yield* promise(() => runProcess("git", [
87845
+ jwt
87846
+ };
87847
+ const searchTerm = requested.split("/")[1] ?? requested;
87848
+ let result = yield* promise(() => fetchGitHubReposForAuth(auth, {
87849
+ search: searchTerm,
87850
+ organizationId: input.organizationId ?? ""
87851
+ }));
87852
+ if (!input.runtimeGrantAuth && shouldRetryWithRefreshedCmmdJwt(result.status)) {
87853
+ const refreshedJwt = yield* resolveJwt(input.sessionId, input.subject, true);
87854
+ if (refreshedJwt && refreshedJwt !== jwt) result = yield* promise(() => fetchGitHubReposForAuth({
87855
+ kind: "cmmd-jwt",
87856
+ jwt: refreshedJwt
87857
+ }, {
87858
+ search: searchTerm,
87859
+ organizationId: input.organizationId ?? ""
87860
+ }));
87861
+ }
87862
+ if (result.status !== 200) {
87863
+ const body = result.body;
87864
+ return {
87865
+ status: result.status,
87866
+ repository: null,
87867
+ error: typeof body.error === "string" ? body.error : "GitHub repositories are unavailable."
87868
+ };
87869
+ }
87870
+ const match = selectGitHubRepositoryMatch(result.body, requested);
87871
+ if (match === null) return {
87872
+ status: 404,
87873
+ repository: null,
87874
+ error: `Repository ${requested} was not found in the GitHub accounts connected to Forge.`
87875
+ };
87876
+ return {
87877
+ status: 200,
87878
+ repository: describeGitHubRepository(match),
87879
+ error: null
87880
+ };
87881
+ });
87882
+ }
87883
+ /**
87884
+ * Find the one repository in a brokered listing whose full name is the one
87885
+ * asked for.
87886
+ *
87887
+ * Exact and case-insensitive. The listing is a *search*, so it comes back with
87888
+ * near misses in it: asking for `owner/forge` and being handed `someone-else/
87889
+ * forge` because it sorted first is how a person ends up cloning a stranger's
87890
+ * repository, and a prefix match would do exactly that.
87891
+ */
87892
+ function selectGitHubRepositoryMatch(body, requested) {
87893
+ const repos = body?.repos ?? [];
87894
+ const wanted = requested.trim().toLowerCase();
87895
+ for (const repo of repos) if (typeof repo.full_name === "string" && repo.full_name.toLowerCase() === wanted) return repo.full_name;
87896
+ return null;
87897
+ }
87898
+ /**
87899
+ * The repository a clone request is really for, or `null` if there is not one.
87900
+ *
87901
+ * A caller may name `owner/repo`, or paste a clone URL and let the server work
87902
+ * it out. Anything that does not resolve to a GitHub repository returns `null`
87903
+ * and must be refused rather than cloned: Forge clones through a CMMD-brokered
87904
+ * installation token for a named repository, and "just run `git clone` on this
87905
+ * URL instead" would be a second, unbrokered way to write to disk.
87906
+ */
87907
+ function resolveGitHubCloneTarget(input) {
87908
+ const named = input.repository?.trim();
87909
+ if (named && /^[^/\s]+\/[^/\s]+$/.test(named)) return named;
87910
+ if (named) return null;
87911
+ const remoteUrl = input.remoteUrl?.trim();
87912
+ return remoteUrl ? parseRepoFullNameFromRemoteUrl(remoteUrl) : null;
87913
+ }
87914
+ /**
87915
+ * Clone `repoFullName` for an authenticated session.
87916
+ *
87917
+ * Authorization is the caller's own CMMD connection: the session's JWT (or a
87918
+ * runtime grant, when this VM is running under one) is exchanged with CMMD for
87919
+ * an installation token scoped to that one repository. A session with no CMMD
87920
+ * connection gets 401 and no clone; a repository CMMD will not issue a token
87921
+ * for gets CMMD's own refusal, not a `git clone` attempt.
87922
+ */
87923
+ function runGitHubCloneForSession(input) {
87924
+ return gen(function* () {
87925
+ const { config, repoFullName, targetDir } = input;
87926
+ const token = yield* acquireInstallationTokenForSession(input);
87927
+ if (!token.ok) return token.outcome;
87928
+ const cloneDir = resolveGitHubCloneDir(config.baseDir, repoFullName, targetDir);
87929
+ yield* promise(() => mkdir(join(cloneDir, ".."), { recursive: true }).catch(() => void 0));
87930
+ const target = {
87931
+ cloneDir,
87932
+ repoFullName,
87933
+ token: token.token,
87934
+ config
87935
+ };
87936
+ return existsSync(cloneDir) ? yield* adoptExistingGitHubClone(target) : yield* cloneGitHubRepositoryInto(target);
87937
+ });
87938
+ }
87939
+ /**
87940
+ * The installation token for one repository, on the caller's own authority.
87941
+ *
87942
+ * A runtime grant is used as-is. A session resolves its CMMD JWT and retries
87943
+ * once with a refreshed one when CMMD answers 401 or 403, because the cached
87944
+ * JWT may simply have expired. The failure branch is already the HTTP outcome,
87945
+ * so a caller returns it rather than translating it.
87946
+ */
87947
+ function acquireInstallationTokenForSession(input) {
87948
+ return gen(function* () {
87949
+ const { runtimeGrantAuth, repoFullName } = input;
87950
+ const jwt = runtimeGrantAuth ? null : yield* resolveJwt(input.sessionId, input.subject);
87951
+ if (!runtimeGrantAuth && !jwt) return {
87952
+ ok: false,
87953
+ outcome: {
87954
+ status: 401,
87955
+ body: { error: "CMMD not connected" }
87956
+ }
87957
+ };
87958
+ let token = yield* fetchInstallationTokenForAuth(runtimeGrantAuth ?? {
87959
+ kind: "cmmd-jwt",
87960
+ jwt
87961
+ }, repoFullName);
87962
+ if (!runtimeGrantAuth && !token.ok && shouldRetryWithRefreshedCmmdJwt(token.status)) {
87963
+ const refreshedJwt = yield* resolveJwt(input.sessionId, input.subject, true);
87964
+ if (refreshedJwt && refreshedJwt !== jwt) token = yield* fetchInstallationTokenForAuth({
87965
+ kind: "cmmd-jwt",
87966
+ jwt: refreshedJwt
87967
+ }, repoFullName);
87968
+ }
87969
+ if (!token.ok) return {
87970
+ ok: false,
87971
+ outcome: {
87972
+ status: token.status,
87973
+ body: gitHubTokenFailureBody(token.status, token.error)
87974
+ }
87975
+ };
87976
+ return {
87977
+ ok: true,
87978
+ token: token.token
87979
+ };
87980
+ });
87981
+ }
87982
+ /**
87983
+ * A directory already sits where the clone would go. Adopt it when it is a
87984
+ * checkout of the same repository, so a second "add project" is a no-op with
87985
+ * fresh credentials rather than a failure; refuse otherwise, naming the
87986
+ * folder, because overwriting it would destroy something the person made.
87987
+ */
87988
+ function adoptExistingGitHubClone(target) {
87989
+ return gen(function* () {
87990
+ const { cloneDir, repoFullName } = target;
87991
+ const git = (args) => promise(() => runProcess("git", [
87257
87992
  "-C",
87258
87993
  cloneDir,
87259
- "rev-parse",
87260
- "--is-inside-work-tree"
87994
+ ...args
87261
87995
  ], {
87262
87996
  timeoutMs: 1e4,
87263
87997
  allowNonZeroExit: true,
87264
87998
  env: process.env
87265
87999
  }));
87266
- const remote = yield* promise(() => runProcess("git", [
87267
- "-C",
87268
- cloneDir,
88000
+ const workTree = yield* git(["rev-parse", "--is-inside-work-tree"]);
88001
+ const remote = yield* git([
87269
88002
  "remote",
87270
88003
  "get-url",
87271
88004
  "origin"
87272
- ], {
87273
- timeoutMs: 1e4,
87274
- allowNonZeroExit: true,
87275
- env: process.env
87276
- }));
87277
- if (workTree.code === 0 && workTree.stdout.trim() === "true" && remote.code === 0 && isGitHubRemoteForRepo(remote.stdout.trim(), repo_full_name)) {
87278
- yield* applyGitCredentials({
87279
- cloneDir,
87280
- repoFullName: repo_full_name,
87281
- token: gitToken,
87282
- mode: config.forgeGitCredentialCache ? "cache" : "embed"
87283
- });
87284
- return json$6({
88005
+ ]);
88006
+ if (!(workTree.code === 0 && workTree.stdout.trim() === "true" && remote.code === 0 && isGitHubRemoteForRepo(remote.stdout.trim(), repoFullName))) return {
88007
+ status: 409,
88008
+ body: {
88009
+ 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.",
88010
+ local_path: cloneDir
88011
+ }
88012
+ };
88013
+ yield* applyGitCredentialsFor(target);
88014
+ return {
88015
+ status: 200,
88016
+ body: {
87285
88017
  local_path: cloneDir,
87286
88018
  existing: true
87287
- });
87288
- }
87289
- return json$6({
87290
- 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.",
87291
- local_path: cloneDir
87292
- }, 409);
87293
- }
87294
- const result = yield* promise(() => runProcess("git", buildAuthenticatedGitCloneArgs(repo_full_name, cloneDir), {
87295
- timeoutMs: 12e4,
87296
- allowNonZeroExit: true,
87297
- env: {
87298
- ...process.env,
87299
- CMMD_FORGE_GIT_TOKEN: gitToken,
87300
- GIT_TERMINAL_PROMPT: "0"
88019
+ }
88020
+ };
88021
+ });
88022
+ }
88023
+ function cloneGitHubRepositoryInto(target) {
88024
+ return gen(function* () {
88025
+ const { cloneDir, repoFullName, token } = target;
88026
+ const result = yield* promise(() => runProcess("git", buildAuthenticatedGitCloneArgs(repoFullName, cloneDir), {
88027
+ timeoutMs: 12e4,
88028
+ allowNonZeroExit: true,
88029
+ env: {
88030
+ ...process.env,
88031
+ CMMD_FORGE_GIT_TOKEN: token,
88032
+ GIT_TERMINAL_PROMPT: "0"
88033
+ }
88034
+ }));
88035
+ if (result.code !== 0) {
88036
+ const stderr = sanitizeGitCredentialText(result.stderr).trim().slice(0, 500);
88037
+ return {
88038
+ status: 500,
88039
+ body: { error: `git clone failed${stderr.length > 0 ? `: ${stderr}` : ""}` }
88040
+ };
87301
88041
  }
87302
- }));
87303
- if (result.code !== 0) {
87304
- const stderr = sanitizeGitCredentialText(result.stderr).trim().slice(0, 500);
87305
- return json$6({ error: `git clone failed${stderr.length > 0 ? `: ${stderr}` : ""}` }, 500);
88042
+ yield* applyGitCredentialsFor(target);
88043
+ return {
88044
+ status: 200,
88045
+ body: { local_path: cloneDir }
88046
+ };
88047
+ });
88048
+ }
88049
+ function applyGitCredentialsFor(target) {
88050
+ return applyGitCredentials({
88051
+ cloneDir: target.cloneDir,
88052
+ repoFullName: target.repoFullName,
88053
+ token: target.token,
88054
+ mode: target.config.forgeGitCredentialCache ? "cache" : "embed"
88055
+ });
88056
+ }
88057
+ const githubCloneRouteLayer = add$1("POST", "/.well-known/forge/github/clone", gen(function* () {
88058
+ const request = yield* HttpServerRequest;
88059
+ const session = yield* (yield* ServerAuth).authenticateHttpRequest(request);
88060
+ const config = yield* ServerConfig;
88061
+ if (session.method === "proxy-asserted") {
88062
+ const forwardedJwt = normalizeForwardedCmmdJwt(request.headers);
88063
+ if (forwardedJwt !== void 0) yield* setCommandJwtForSession(session.sessionId, forwardedJwt).pipe(ignore({ log: true }));
87306
88064
  }
87307
- yield* applyGitCredentials({
87308
- cloneDir,
88065
+ const rawBody = yield* request.json;
88066
+ const parsed = decodeUnknownExit(CloneBodySchema)(rawBody);
88067
+ if (parsed._tag === "Failure") return json$6({ error: "repo_full_name is required" }, 400);
88068
+ const { repo_full_name, target_dir } = parsed.value;
88069
+ const outcome = yield* runGitHubCloneForSession({
88070
+ config,
88071
+ sessionId: session.sessionId,
88072
+ subject: session.subject,
88073
+ runtimeGrantAuth: resolveRuntimeGitHubGrantAuth(config, request.headers),
87309
88074
  repoFullName: repo_full_name,
87310
- token: gitToken,
87311
- mode: config.forgeGitCredentialCache ? "cache" : "embed"
88075
+ targetDir: target_dir
87312
88076
  });
87313
- return json$6({ local_path: cloneDir });
88077
+ return json$6(outcome.body, outcome.status);
87314
88078
  }).pipe(catchTag("AuthError", respondToAuthError)));
87315
88079
  const githubCloneJobStartRouteLayer = add$1("POST", "/.well-known/forge/github/clone-jobs", gen(function* () {
87316
88080
  const request = yield* HttpServerRequest;
@@ -92073,7 +92837,7 @@ const launchDetached = (launch) => gen(function* () {
92073
92837
  });
92074
92838
  const OpenLive = effect(Open, gen(function* () {
92075
92839
  const open = yield* tryPromise({
92076
- try: () => import("./open-FIgTDosf.mjs"),
92840
+ try: () => import("./open-D8mGyWmt.mjs"),
92077
92841
  catch: (cause) => new OpenError$1({
92078
92842
  message: "failed to load browser opener",
92079
92843
  cause
@@ -95829,33 +96593,34 @@ function serializeNativeToolActivity(activity) {
95829
96593
  }
95830
96594
  };
95831
96595
  }
95832
- /** The failure activities `appendProviderFailureActivity` emits, all `provider.*.failed`. */
95833
- function isProviderFailureActivity(activity) {
95834
- return activity.kind.startsWith("provider.") && activity.kind.endsWith(".failed");
96596
+ function isReplicatedFailureActivity(activity) {
96597
+ return activity.kind === "runtime.error" || activity.kind.startsWith("provider.") && activity.kind.endsWith(".failed");
95835
96598
  }
95836
96599
  function failureDetail(activity) {
95837
96600
  const payload = activity.payload;
95838
- if (!payload || typeof payload !== "object" || !("detail" in payload)) return null;
95839
- const detail = payload.detail;
95840
- return typeof detail === "string" && detail.trim() ? detail.trim() : null;
96601
+ if (!payload || typeof payload !== "object") return null;
96602
+ const record = payload;
96603
+ const detail = typeof record.detail === "string" ? record.detail.trim() : "";
96604
+ if (detail) return detail;
96605
+ return (typeof record.message === "string" ? record.message.trim() : "") || null;
95841
96606
  }
95842
96607
  /**
95843
96608
  * Replicates why a turn failed.
95844
96609
  *
95845
- * Without this a failure never crossed the replication boundary — only `tool.`
95846
- * activities were serialized — so durable history could not show one and every
96610
+ * Without this a failure never crossed the replication boundary. Only `tool.`
96611
+ * activities were serialized, so durable history could not show one and every
95847
96612
  * failure reached the user as an endless "Working" spinner. Measured in
95848
96613
  * production on 2026-08-30: zero failure events had EVER been recorded against
95849
96614
  * 652 tool_call rows, which is how a workspace-path block, a broker-token
95850
96615
  * refusal and a dispatch into a deleted directory all looked identical.
95851
96616
  *
95852
- * `error_summary` is used rather than a new kind because both the Forge
95853
- * protocol and CMMD's ingest schema already accept it; nothing here needed a
95854
- * contract change. The detail carries the actionable sentence, so it is joined
95855
- * onto the summary — the event has no separate detail field.
96617
+ * `error_summary` is used because both the Forge protocol and CMMD's ingest
96618
+ * schema already accept it. Provider startup failures store `detail`, while
96619
+ * active runtime failures store `message`. The event has no separate detail
96620
+ * field, so either value is joined onto the summary.
95856
96621
  */
95857
- function serializeProviderFailureActivity(activity) {
95858
- if (!isProviderFailureActivity(activity)) return null;
96622
+ function serializeFailureActivity(activity) {
96623
+ if (!isReplicatedFailureActivity(activity)) return null;
95859
96624
  const summary = text(activity.summary, 4e3);
95860
96625
  if (summary === null) return null;
95861
96626
  const detail = failureDetail(activity);
@@ -96197,7 +96962,7 @@ function isTurnUsageTokens(value) {
96197
96962
  function serializeRuntimeThreadEvents(thread) {
96198
96963
  const messages = thread.messages.flatMap(serializeThreadMessage);
96199
96964
  const failureActivities = thread.activities.flatMap((activity) => {
96200
- const event = serializeProviderFailureActivity(activity);
96965
+ const event = serializeFailureActivity(activity);
96201
96966
  return event === null ? [] : [event];
96202
96967
  });
96203
96968
  const replicaActivities = thread.activities.flatMap((activity) => {
@@ -100459,6 +101224,31 @@ function claudeWriteToken(env = process.env) {
100459
101224
  return accessor.read(env);
100460
101225
  }
100461
101226
  //#endregion
101227
+ //#region ../../packages/shared/src/claudeReadinessReason.ts
101228
+ /**
101229
+ * The reason literals Forge acts on, confirmed against CMMD-36441's readiness
101230
+ * service. If the route renames one, the rename lands here as a fall-through
101231
+ * to null, never as the wrong kind.
101232
+ *
101233
+ * Reading the reason is what makes `stale` legible. CMMD-36441 answers
101234
+ * `credential_reconnect_required` on a `stale` answer that still reports
101235
+ * `connected: true`, and the older `credential_generation_not_applied` on a
101236
+ * `stale` answer that simply has not landed yet. Those want opposite
101237
+ * handling, and both used to get "Reconnect Claude in Settings" because the
101238
+ * guard read only the delivery status.
101239
+ */
101240
+ const CODE_BY_UPSTREAM_REASON$1 = {
101241
+ credential_expired_refreshing: "CREDENTIAL_EXPIRED_REFRESHING",
101242
+ credential_reconnect_required: "CREDENTIAL_RECONNECT_REQUIRED",
101243
+ credential_not_connected: "CREDENTIAL_RECONNECT_REQUIRED",
101244
+ credential_generation_not_applied: "CREDENTIAL_GENERATION_NOT_APPLIED",
101245
+ runtime_unavailable: "RUNTIME_UNAVAILABLE"
101246
+ };
101247
+ function claudeReadinessReasonCode(reason) {
101248
+ if (typeof reason !== "string" || !Object.hasOwn(CODE_BY_UPSTREAM_REASON$1, reason)) return null;
101249
+ return CODE_BY_UPSTREAM_REASON$1[reason] ?? null;
101250
+ }
101251
+ //#endregion
100462
101252
  //#region src/egress/providerDeliveryReceipt.ts
100463
101253
  const PROVIDER_ACCOUNT_IDENTITY$2 = /^pai_v1_[A-Za-z0-9_-]{43}$/;
100464
101254
  const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
@@ -100633,82 +101423,6 @@ function providerDeliveryBindingsEqual(left, right) {
100633
101423
  return classifyProviderDeliveryBindingChange(left, right) === "unchanged";
100634
101424
  }
100635
101425
  //#endregion
100636
- //#region src/provider/claudeReadinessReason.ts
100637
- /**
100638
- * The reason literals Forge has copy for, confirmed against CMMD-36441's
100639
- * readiness service. If the route renames one, the rename lands here as a
100640
- * fall-through to the generic sentence, never as a wrong sentence.
100641
- *
100642
- * Reading the reason is what makes `stale` legible. CMMD-36441 answers
100643
- * `credential_reconnect_required` on a `stale` answer that still reports
100644
- * `connected: true`, and the older `credential_generation_not_applied` on a
100645
- * `stale` answer that simply has not landed yet. Those want opposite
100646
- * sentences, and both used to get "Reconnect Claude in Settings" because the
100647
- * guard read only the delivery status.
100648
- */
100649
- const CODE_BY_UPSTREAM_REASON$1 = {
100650
- credential_expired_refreshing: "CREDENTIAL_EXPIRED_REFRESHING",
100651
- credential_reconnect_required: "CREDENTIAL_RECONNECT_REQUIRED",
100652
- credential_not_connected: "CREDENTIAL_RECONNECT_REQUIRED",
100653
- credential_generation_not_applied: "CREDENTIAL_GENERATION_NOT_APPLIED",
100654
- runtime_unavailable: "RUNTIME_UNAVAILABLE"
100655
- };
100656
- const CLAUDE_NATIVE_CREDENTIAL_UNUSABLE_COPY = "The delivered Claude credential is unavailable or unusable in this runtime.";
100657
- /**
100658
- * An ISO 8601 instant, with or without fractional seconds, UTC or offset.
100659
- * Anchored to nothing on purpose: CMMD wraps the time in a sentence, and the
100660
- * time is the only part of that sentence Forge repeats.
100661
- */
100662
- const ISO_INSTANT_PATTERN = /\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})/;
100663
- function claudeReadinessReasonCode(reason) {
100664
- if (typeof reason !== "string" || !Object.hasOwn(CODE_BY_UPSTREAM_REASON$1, reason)) return null;
100665
- return CODE_BY_UPSTREAM_REASON$1[reason] ?? null;
100666
- }
100667
- function expiredAtFromReadinessMessage(message) {
100668
- if (typeof message !== "string") return null;
100669
- return ISO_INSTANT_PATTERN.exec(message)?.[0] ?? null;
100670
- }
100671
- function claudeCredentialExpiredTurnCopy(expiredAt) {
100672
- return `The delivered Claude credential expired at ${expiredAt}. CMMD is delivering a refreshed one, so retry this turn in a moment.`;
100673
- }
100674
- function claudeCredentialExpiredCardCopy(expiredAt) {
100675
- return `The delivered Claude credential expired at ${expiredAt}. CMMD is delivering a refreshed one to this runtime.`;
100676
- }
100677
- /**
100678
- * The last allowlisted readiness reason this process saw.
100679
- *
100680
- * A server process serves exactly one runtime, so one slot is the whole store.
100681
- * It holds a reason code and a timestamp, never credential bytes. The provider
100682
- * catalog reads a local credential file and cannot tell an expired snapshot
100683
- * from a blank one, so without this the card can only offer the generic
100684
- * sentence while the guard already knows better.
100685
- */
100686
- let latestVerdict = null;
100687
- /**
100688
- * Remember, or clear, what the readiness answer said.
100689
- *
100690
- * An answer carrying no allowlisted reason clears the slot, which is what makes
100691
- * the card go back to the generic sentence the moment CMMD applies a
100692
- * credential: an applied answer carries no reason.
100693
- */
100694
- function recordClaudeReadinessVerdict(readiness) {
100695
- const code = claudeReadinessReasonCode(readiness.reason);
100696
- latestVerdict = code === null ? null : {
100697
- code,
100698
- expiredAt: expiredAtFromReadinessMessage(readiness.lastErrorMessage)
100699
- };
100700
- }
100701
- /**
100702
- * What the provider status card says about a delivered credential it cannot
100703
- * use. Names the expiry only when a readiness answer supplied the time; a
100704
- * guessed time would be worse than the generic sentence.
100705
- */
100706
- function claudeNativeCredentialUnusableMessage() {
100707
- const verdict = latestVerdict;
100708
- if (verdict?.code === "CREDENTIAL_EXPIRED_REFRESHING" && verdict.expiredAt !== null) return claudeCredentialExpiredCardCopy(verdict.expiredAt);
100709
- return CLAUDE_NATIVE_CREDENTIAL_UNUSABLE_COPY;
100710
- }
100711
- //#endregion
100712
101426
  //#region src/provider/providerReadiness.ts
100713
101427
  const GENERATION_HASH_PATTERN$1 = /^[a-f0-9]{64}$/;
100714
101428
  const STILL_APPLYING_STATUSES = new Set([
@@ -100753,10 +101467,18 @@ async function queryProviderReadiness(input) {
100753
101467
  }
100754
101468
  return classifyClaudeSnapshot(snapshot, sprite);
100755
101469
  }
100756
- function isAppliedClaudeSnapshot(snapshot) {
100757
- return snapshot.connected === true && snapshot.deliveryStatus === "applied" && snapshot.nativeExecutable === true && typeof snapshot.generationId === "string" && snapshot.generationId.trim().length > 0;
101470
+ /**
101471
+ * Whether CMMD allows a turn to start on this runtime. The delivery status is
101472
+ * not part of it: a refresh that failed over a credential the guest verified
101473
+ * and that has not expired reads as failed with nativeExecutable true, and the
101474
+ * turn is the probe (FOR-3085). The generation fence and the receipt below
101475
+ * still bind the answer.
101476
+ */
101477
+ function claudeSnapshotAllowsTurn(snapshot) {
101478
+ return snapshot.connected === true && snapshot.nativeExecutable === true && typeof snapshot.generationId === "string" && snapshot.generationId.trim().length > 0;
100758
101479
  }
100759
- function classifyUnappliedClaudeSnapshot(snapshot) {
101480
+ /** Why a turn may not start, for an answer that does not allow one. */
101481
+ function classifyClaudeTurnRefusal(snapshot) {
100760
101482
  const code = claudeReadinessReasonCode(snapshot.reason);
100761
101483
  if (code === "RUNTIME_UNAVAILABLE") return {
100762
101484
  kind: "not_ready",
@@ -100784,7 +101506,7 @@ function classifyUnappliedClaudeSnapshot(snapshot) {
100784
101506
  };
100785
101507
  }
100786
101508
  function classifyClaudeSnapshot(snapshot, sprite) {
100787
- if (!isAppliedClaudeSnapshot(snapshot)) return classifyUnappliedClaudeSnapshot(snapshot);
101509
+ if (!claudeSnapshotAllowsTurn(snapshot)) return classifyClaudeTurnRefusal(snapshot);
100788
101510
  const generationId = snapshot.generationId.trim();
100789
101511
  if (createHash("sha256").update(generationId).digest("hex") !== sprite.localGenerationHash) return {
100790
101512
  kind: "not_ready",
@@ -104126,7 +104848,9 @@ const REJECT_UNCERTAIN_METHODS = new Set([
104126
104848
  WS_METHODS.pullRequestsComment,
104127
104849
  WS_METHODS.pullRequestsSubmitReview,
104128
104850
  WS_METHODS.pullRequestsReplyToThread,
104129
- WS_METHODS.projectCheckoutDeletionExecute
104851
+ WS_METHODS.projectCheckoutDeletionExecute,
104852
+ WS_METHODS.providerUploadFeedback,
104853
+ WS_METHODS.sourceControlCloneRepository
104130
104854
  ]);
104131
104855
  function classifyInterruptedRelayCommand(input) {
104132
104856
  if ((input.method === WS_METHODS.gitCreateBranch || input.method === WS_METHODS.vcsCreateRef) && input.deliveryClass === "create-command") return { kind: "reconcile-git-create-branch" };
@@ -105056,7 +105780,77 @@ const makeWsRpcLayer = (currentSessionId, currentUserId) => WsRpcGroup.toLayer(g
105056
105780
  payload: initialSnapshot
105057
105781
  }), liveEvents);
105058
105782
  }), { "rpc.aggregate": "auth" }),
105059
- [WS_METHODS.subscribeWorkspaceReadiness]: (_input) => currentUserId === void 0 ? empty$5 : workspaceReadinessState.subscribe(currentUserId)
105783
+ [WS_METHODS.subscribeWorkspaceReadiness]: (_input) => currentUserId === void 0 ? empty$5 : workspaceReadinessState.subscribe(currentUserId),
105784
+ [WS_METHODS.assetsCreateUrl]: (input) => observeRpcEffect(WS_METHODS.assetsCreateUrl, createAssetUrl(currentSessionId, input.resource), { "rpc.aggregate": "assets" }),
105785
+ [WS_METHODS.providerUploadFeedback]: (input) => observeRpcEffect(WS_METHODS.providerUploadFeedback, uploadThreadFeedback(orchestrationEngine, currentSessionId, currentUserId, input), { "rpc.aggregate": "provider" }),
105786
+ [ORCHESTRATION_WS_METHODS.searchThreads]: (input) => observeRpcEffect(ORCHESTRATION_WS_METHODS.searchThreads, gen(function* () {
105787
+ yield* startup.awaitCommandReady;
105788
+ return yield* projectionSnapshotQuery.searchThreads(input);
105789
+ }).pipe(mapError((cause) => new OrchestrationSearchThreadsError({
105790
+ message: "Failed to search threads",
105791
+ cause
105792
+ }))), { "rpc.aggregate": "orchestration" }),
105793
+ [WS_METHODS.sourceControlLookupRepository]: (input) => observeRpcEffect(WS_METHODS.sourceControlLookupRepository, gen(function* () {
105794
+ if (input.provider !== "github") return yield* new SourceControlRepositoryError({
105795
+ provider: input.provider,
105796
+ operation: "lookupRepository",
105797
+ detail: `Forge can only look up GitHub repositories. ${input.provider} is not connected.`
105798
+ });
105799
+ if (currentUserId === void 0) return yield* new SourceControlRepositoryError({
105800
+ provider: "github",
105801
+ operation: "lookupRepository",
105802
+ detail: "Sign in to Forge before looking up a repository."
105803
+ });
105804
+ const outcome = yield* lookupGitHubRepositoryForSession({
105805
+ sessionId: currentSessionId,
105806
+ subject: currentUserId,
105807
+ runtimeGrantAuth: null,
105808
+ repository: input.repository
105809
+ });
105810
+ if (outcome.repository === null) return yield* new SourceControlRepositoryError({
105811
+ provider: "github",
105812
+ operation: "lookupRepository",
105813
+ detail: outcome.error ?? "Repository lookup failed."
105814
+ });
105815
+ return {
105816
+ provider: "github",
105817
+ ...outcome.repository
105818
+ };
105819
+ }), { "rpc.aggregate": "sourceControl" }),
105820
+ [WS_METHODS.sourceControlCloneRepository]: (input) => observeRpcEffect(WS_METHODS.sourceControlCloneRepository, gen(function* () {
105821
+ const provider = input.provider ?? "github";
105822
+ const repoFullName = resolveGitHubCloneTarget(input);
105823
+ const refuse = (detail) => new SourceControlRepositoryError({
105824
+ provider,
105825
+ operation: "cloneRepository",
105826
+ detail
105827
+ });
105828
+ if (provider !== "github") return yield* refuse(`Forge can only clone GitHub repositories. ${provider} is not connected.`);
105829
+ if (currentUserId === void 0) return yield* refuse("Sign in to Forge before cloning a repository.");
105830
+ if (repoFullName === null) return yield* refuse("Name the repository as owner/name, or paste a github.com clone URL.");
105831
+ const outcome = yield* runGitHubCloneForSession({
105832
+ config,
105833
+ sessionId: currentSessionId,
105834
+ subject: currentUserId,
105835
+ runtimeGrantAuth: null,
105836
+ repoFullName,
105837
+ targetDir: input.destinationPath
105838
+ });
105839
+ const localPath = outcome.body.local_path;
105840
+ if (outcome.status !== 200 || typeof localPath !== "string") {
105841
+ const detail = outcome.body.error;
105842
+ return yield* refuse(typeof detail === "string" ? detail : "Cloning the repository failed.");
105843
+ }
105844
+ const repository = describeGitHubRepository(repoFullName);
105845
+ return {
105846
+ cwd: localPath,
105847
+ remoteUrl: repository.url,
105848
+ repository: {
105849
+ provider: "github",
105850
+ ...repository
105851
+ }
105852
+ };
105853
+ }), { "rpc.aggregate": "sourceControl" })
105060
105854
  });
105061
105855
  }));
105062
105856
  const websocketRpcRouteLayer = unwrap(succeed(mergeAll(add$1("GET", "/ws", gen(function* () {
@@ -116133,8 +116927,8 @@ effectDiscard(runMigrations());
116133
116927
  //#endregion
116134
116928
  //#region src/persistence/Layers/Sqlite.ts
116135
116929
  const defaultSqliteClientLoaders = {
116136
- bun: () => import("./SqliteClient-CIsITMoQ.mjs"),
116137
- node: () => import("./NodeSqliteClient-DGInbxQ3.mjs")
116930
+ bun: () => import("./SqliteClient-DTWCGkrj.mjs"),
116931
+ node: () => import("./NodeSqliteClient-DqFjB3u_.mjs")
116138
116932
  };
116139
116933
  const makeRuntimeSqliteLayer = fn$1("makeRuntimeSqliteLayer")(function* (config) {
116140
116934
  const loader = defaultSqliteClientLoaders[process.versions.bun !== void 0 ? "bun" : "node"];
@@ -188985,7 +189779,7 @@ var Vce = {
188985
189779
  function Ve() {
188986
189780
  return Vce;
188987
189781
  }
188988
- var Us = os.homedir(), lk = os.tmpdir(), { env: Vc } = process$1, Yce = (e) => {
189782
+ var Us = $U.homedir(), lk = $U.tmpdir(), { env: Vc } = process$1, Yce = (e) => {
188989
189783
  let t = path.join(Us, "Library");
188990
189784
  return {
188991
189785
  data: path.join(t, "Application Support", e),
@@ -199185,12 +199979,12 @@ function qZ(e, t) {
199185
199979
  }
199186
199980
  return n;
199187
199981
  }
199188
- var os$2 = "https://code.claude.com/docs/en", kKe = [
199982
+ var os$1 = "https://code.claude.com/docs/en", kKe = [
199189
199983
  {
199190
199984
  matches: (e) => e.path === "permissions.defaultMode" && e.code === "invalid_value",
199191
199985
  tip: {
199192
199986
  suggestion: "Valid modes: \"acceptEdits\" (ask before file changes), \"plan\" (analysis only), \"bypassPermissions\" (auto-accept all), or \"default\" (standard behavior)",
199193
- docLink: `${os$2}/iam#permission-modes`
199987
+ docLink: `${os$1}/iam#permission-modes`
199194
199988
  }
199195
199989
  },
199196
199990
  {
@@ -199205,7 +199999,7 @@ var os$2 = "https://code.claude.com/docs/en", kKe = [
199205
199999
  matches: (e) => e.path.startsWith("env.") && e.code === "invalid_type",
199206
200000
  tip: {
199207
200001
  suggestion: "Environment variables must be strings. Wrap numbers and booleans in quotes. Example: \"DEBUG\": \"true\", \"PORT\": \"3000\"",
199208
- docLink: `${os$2}/settings#environment-variables`
200002
+ docLink: `${os$1}/settings#environment-variables`
199209
200003
  }
199210
200004
  },
199211
200005
  {
@@ -199216,14 +200010,14 @@ var os$2 = "https://code.claude.com/docs/en", kKe = [
199216
200010
  matches: (e) => e.path.startsWith("hooks.") && e.code === "invalid_key",
199217
200011
  tip: {
199218
200012
  suggestion: "Not a recognized hook event. Common events: PreToolUse, PostToolUse, UserPromptSubmit, SessionStart, SessionEnd, Stop. Check spelling and capitalization.",
199219
- docLink: `${os$2}/hooks`
200013
+ docLink: `${os$1}/hooks`
199220
200014
  }
199221
200015
  },
199222
200016
  {
199223
200017
  matches: (e) => /\.hooks\.\d+\.command$/.test(e.path) && e.code === "invalid_type" && e.received === "undefined",
199224
200018
  tip: {
199225
200019
  suggestion: "Command hooks require `command`. For exec form (no shell), set `command` to the executable and `args` to its arguments: {\"type\": \"command\", \"command\": \"echo\", \"args\": [\"hi\"]}. For shell form, set `command` to the full shell string: {\"type\": \"command\", \"command\": \"echo hi\"}.",
199226
- docLink: `${os$2}/hooks#exec-form-and-shell-form`
200020
+ docLink: `${os$1}/hooks#exec-form-and-shell-form`
199227
200021
  }
199228
200022
  },
199229
200023
  {
@@ -199238,7 +200032,7 @@ var os$2 = "https://code.claude.com/docs/en", kKe = [
199238
200032
  matches: (e) => e.code === "unrecognized_keys",
199239
200033
  tip: {
199240
200034
  suggestion: "Check for typos or refer to the documentation for valid fields",
199241
- docLink: `${os$2}/settings`
200035
+ docLink: `${os$1}/settings`
199242
200036
  }
199243
200037
  },
199244
200038
  {
@@ -199253,13 +200047,13 @@ var os$2 = "https://code.claude.com/docs/en", kKe = [
199253
200047
  matches: (e) => e.path === "permissions.additionalDirectories" && e.code === "invalid_type",
199254
200048
  tip: {
199255
200049
  suggestion: "Must be an array of directory paths. Example: [\"~/projects\", \"/tmp/workspace\"]. You can also use --add-dir flag or /add-dir command",
199256
- docLink: `${os$2}/iam#working-directories`
200050
+ docLink: `${os$1}/iam#working-directories`
199257
200051
  }
199258
200052
  }
199259
200053
  ], AKe = {
199260
- permissions: `${os$2}/iam#configuring-permissions`,
199261
- env: `${os$2}/settings#environment-variables`,
199262
- hooks: `${os$2}/hooks`
200054
+ permissions: `${os$1}/iam#configuring-permissions`,
200055
+ env: `${os$1}/settings#environment-variables`,
200056
+ hooks: `${os$1}/hooks`
199263
200057
  };
199264
200058
  function GZ(e) {
199265
200059
  let t = kKe.find((r) => r.matches(e));
@@ -211320,6 +212114,83 @@ const makeProviderAuthProvenance = (input) => {
211320
212114
  };
211321
212115
  };
211322
212116
  //#endregion
212117
+ //#region src/provider/claudeReadinessReason.ts
212118
+ /**
212119
+ * What Forge says about a delivered Claude credential that expired.
212120
+ *
212121
+ * CMMD's credential authority rotates the refresh token on every refresh, and a
212122
+ * runtime holds a one-time snapshot, so the snapshot's refresh token dies at
212123
+ * the next rotation. Nothing re-delivers on its own, and on 2026-09-04 23:07Z
212124
+ * the turn met a dead credential: the CLI said "OAuth session expired and could
212125
+ * not be refreshed", the thread said Claude was unavailable on this
212126
+ * Environment, and the provider card said the credential was "unavailable or
212127
+ * unusable". Three sentences, none of them the fact, which is that the
212128
+ * credential expired at a known time and a replacement is on its way.
212129
+ *
212130
+ * CMMD-36441 now answers the pre-turn readiness read with a typed reason and
212131
+ * queues a same-generation re-delivery. This module is the one place that
212132
+ * turns those reasons into words.
212133
+ *
212134
+ * Which kind of answer a reason names comes from
212135
+ * `@cmmd-forge/shared/claudeReadinessReason`, which the browser readiness poll
212136
+ * reads too, so the two cannot disagree about a reason (FOR-3085). A reason
212137
+ * that maps to nothing falls through to the generic copy, and every sentence a
212138
+ * person reads is written here rather than forwarded from upstream. Only an
212139
+ * ISO timestamp is ever lifted out of CMMD's message, so no credential detail
212140
+ * in an upstream string can reach a browser.
212141
+ */
212142
+ const CLAUDE_NATIVE_CREDENTIAL_UNUSABLE_COPY = "The delivered Claude credential is unavailable or unusable in this runtime.";
212143
+ /**
212144
+ * An ISO 8601 instant, with or without fractional seconds, UTC or offset.
212145
+ * Anchored to nothing on purpose: CMMD wraps the time in a sentence, and the
212146
+ * time is the only part of that sentence Forge repeats.
212147
+ */
212148
+ const ISO_INSTANT_PATTERN = /\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})/;
212149
+ function expiredAtFromReadinessMessage(message) {
212150
+ if (typeof message !== "string") return null;
212151
+ return ISO_INSTANT_PATTERN.exec(message)?.[0] ?? null;
212152
+ }
212153
+ function claudeCredentialExpiredTurnCopy(expiredAt) {
212154
+ return `The delivered Claude credential expired at ${expiredAt}. CMMD is delivering a refreshed one, so retry this turn in a moment.`;
212155
+ }
212156
+ function claudeCredentialExpiredCardCopy(expiredAt) {
212157
+ return `The delivered Claude credential expired at ${expiredAt}. CMMD is delivering a refreshed one to this runtime.`;
212158
+ }
212159
+ /**
212160
+ * The last allowlisted readiness reason this process saw.
212161
+ *
212162
+ * A server process serves exactly one runtime, so one slot is the whole store.
212163
+ * It holds a reason code and a timestamp, never credential bytes. The provider
212164
+ * catalog reads a local credential file and cannot tell an expired snapshot
212165
+ * from a blank one, so without this the card can only offer the generic
212166
+ * sentence while the guard already knows better.
212167
+ */
212168
+ let latestVerdict = null;
212169
+ /**
212170
+ * Remember, or clear, what the readiness answer said.
212171
+ *
212172
+ * An answer carrying no allowlisted reason clears the slot, which is what makes
212173
+ * the card go back to the generic sentence the moment CMMD applies a
212174
+ * credential: an applied answer carries no reason.
212175
+ */
212176
+ function recordClaudeReadinessVerdict(readiness) {
212177
+ const code = claudeReadinessReasonCode(readiness.reason);
212178
+ latestVerdict = code === null ? null : {
212179
+ code,
212180
+ expiredAt: expiredAtFromReadinessMessage(readiness.lastErrorMessage)
212181
+ };
212182
+ }
212183
+ /**
212184
+ * What the provider status card says about a delivered credential it cannot
212185
+ * use. Names the expiry only when a readiness answer supplied the time; a
212186
+ * guessed time would be worse than the generic sentence.
212187
+ */
212188
+ function claudeNativeCredentialUnusableMessage() {
212189
+ const verdict = latestVerdict;
212190
+ if (verdict?.code === "CREDENTIAL_EXPIRED_REFRESHING" && verdict.expiredAt !== null) return claudeCredentialExpiredCardCopy(verdict.expiredAt);
212191
+ return CLAUDE_NATIVE_CREDENTIAL_UNUSABLE_COPY;
212192
+ }
212193
+ //#endregion
211323
212194
  //#region src/provider/claudeCredentialStore.ts
211324
212195
  /**
211325
212196
  * Which Credential Store the Claude CLI uses on this machine, read the way the
@@ -265879,7 +266750,7 @@ var import_multicast_dns = /* @__PURE__ */ __toESM((/* @__PURE__ */ __commonJSMi
265879
266750
  var dgram = __require("dgram");
265880
266751
  var thunky = require_thunky();
265881
266752
  var events = __require("events");
265882
- var os$1 = __require("os");
266753
+ var os = __require("os");
265883
266754
  var noop = function() {};
265884
266755
  module.exports = function(opts) {
265885
266756
  if (!opts) opts = {};
@@ -266010,14 +266881,14 @@ var import_multicast_dns = /* @__PURE__ */ __toESM((/* @__PURE__ */ __commonJSMi
266010
266881
  return that;
266011
266882
  };
266012
266883
  function defaultInterface() {
266013
- var networks = os$1.networkInterfaces();
266884
+ var networks = os.networkInterfaces();
266014
266885
  var names = Object.keys(networks);
266015
266886
  for (var i = 0; i < names.length; i++) {
266016
266887
  var net = networks[names[i]];
266017
266888
  for (var j = 0; j < net.length; j++) {
266018
266889
  var iface = net[j];
266019
266890
  if (isIPv4(iface.family) && !iface.internal) {
266020
- if (os$1.platform() === "darwin" && names[i] === "en0") return iface.address;
266891
+ if (os.platform() === "darwin" && names[i] === "en0") return iface.address;
266021
266892
  return "0.0.0.0";
266022
266893
  }
266023
266894
  }
@@ -266025,7 +266896,7 @@ var import_multicast_dns = /* @__PURE__ */ __toESM((/* @__PURE__ */ __commonJSMi
266025
266896
  return "127.0.0.1";
266026
266897
  }
266027
266898
  function allInterfaces() {
266028
- var networks = os$1.networkInterfaces();
266899
+ var networks = os.networkInterfaces();
266029
266900
  var names = Object.keys(networks);
266030
266901
  var res = [];
266031
266902
  for (var i = 0; i < names.length; i++) {
@@ -286917,6 +287788,7 @@ function taskStartedToActivities(event) {
286917
287788
  summary: taskStartedSummary(event.payload.taskType),
286918
287789
  payload: {
286919
287790
  taskId: event.payload.taskId,
287791
+ agentKind: classifyTaskAgentKind({ taskType: event.payload.taskType }),
286920
287792
  ...event.payload.taskType ? { taskType: event.payload.taskType } : {},
286921
287793
  ...event.payload.description ? { detail: truncateDetail(event.payload.description) } : {}
286922
287794
  }
@@ -294724,7 +295596,7 @@ function resolveBuildCommitFromEnv(env) {
294724
295596
  * environment descriptor down instead of reporting an honest "unknown".
294725
295597
  */
294726
295598
  function readBakedBuildCommit() {
294727
- return "69affcc8397652715f8bd06fe2f226f4b607e1db";
295599
+ return "c220fa73834b4651c7fc65f272c171028edc3e81";
294728
295600
  }
294729
295601
  async function resolveServerBuildCommit(input) {
294730
295602
  if (isFullCommitSha(input.baked)) return input.baked;
@@ -294841,6 +295713,7 @@ function executionEnvironmentCapabilities(serverConfig) {
294841
295713
  pullRequests: isExecutionPlane,
294842
295714
  threadPullRequestLinking: isExecutionPlane,
294843
295715
  threadSettlement: isExecutionPlane,
295716
+ threadTitleRegeneration: isExecutionPlane,
294844
295717
  cloudMode: serverConfig.forgeCmmdRuntimeMode === true || isCloudShell,
294845
295718
  runtime: {
294846
295719
  topology: runtimeTopology(serverConfig),
@@ -295119,10 +295992,6 @@ const pulseSignalStateUpsertRouteLayer = add$1("POST", "/api/pulse/signals/state
295119
295992
  });
295120
295993
  }).pipe(catchTag("AuthError", respondToAuthError)));
295121
295994
  //#endregion
295122
- //#region src/auth/Services/ServerSecretStore.ts
295123
- var SecretStoreError = class extends TaggedError("SecretStoreError") {};
295124
- var ServerSecretStore = class extends Service$1()("forge/auth/Services/ServerSecretStore") {};
295125
- //#endregion
295126
295995
  //#region src/auth/Layers/ServerSecretStore.ts
295127
295996
  const OWNED_FILE_RECEIPT_PREFIX = "forge-owned-secret-receipt:v1:";
295128
295997
  const LEGACY_FILE_SECRET_NAMESPACE_PREFIX = "forge-secret-namespace:v1:";
@@ -301461,6 +302330,55 @@ const ProjectionCountsRowSchema = Struct({
301461
302330
  const WorkspaceRootLookupInput = Struct({ workspaceRoot: String$1 });
301462
302331
  const ProjectIdLookupInput = Struct({ projectId: ProjectId });
301463
302332
  const ThreadIdLookupInput = Struct({ threadId: ThreadId });
302333
+ const ThreadSearchQueryInput = Struct({
302334
+ pattern: String$1,
302335
+ limit: Number$1
302336
+ });
302337
+ const ThreadSearchMessageRowSchema = Struct({
302338
+ threadId: ThreadId,
302339
+ projectId: ProjectId,
302340
+ role: String$1,
302341
+ text: String$1,
302342
+ createdAt: IsoDateTime
302343
+ });
302344
+ const ThreadSearchMetadataRowSchema = Struct({
302345
+ threadId: ThreadId,
302346
+ projectId: ProjectId,
302347
+ title: String$1,
302348
+ branch: NullOr(String$1)
302349
+ });
302350
+ /** The contract caps a snippet at this length, so the server must too. */
302351
+ const THREAD_SEARCH_SNIPPET_LIMIT = 240;
302352
+ /** How many matches a search returns when the caller does not say. */
302353
+ const THREAD_SEARCH_DEFAULT_LIMIT = 20;
302354
+ /**
302355
+ * `LIKE` treats `%` and `_` as wildcards, so a person searching for a literal
302356
+ * underscore (every other identifier in a codebase) would otherwise match any
302357
+ * character. Escaped with a backslash, declared to SQL as `ESCAPE '\'`, and
302358
+ * lowercased on both sides because Postgres `LIKE` is case-sensitive where
302359
+ * SQLite's is not, and Forge runs on both.
302360
+ */
302361
+ function toThreadSearchPattern(query) {
302362
+ return `%${query.toLowerCase().replace(/[\\%_]/g, (character) => `\\${character}`)}%`;
302363
+ }
302364
+ /**
302365
+ * A window of the text around the first hit, not the head of it.
302366
+ *
302367
+ * A match 4000 characters into an assistant message is invisible in a snippet
302368
+ * that always starts at character zero, which is the difference between a
302369
+ * result a person can recognize and a result they have to open to understand.
302370
+ */
302371
+ function buildThreadSearchSnippet(text, lowerQuery) {
302372
+ const collapsed = text.replace(/\s+/g, " ").trim();
302373
+ if (collapsed.length <= THREAD_SEARCH_SNIPPET_LIMIT) return collapsed;
302374
+ const matchIndex = collapsed.toLowerCase().indexOf(lowerQuery);
302375
+ if (matchIndex < 0) return `${collapsed.slice(0, THREAD_SEARCH_SNIPPET_LIMIT - 1)}…`;
302376
+ const leading = Math.floor((THREAD_SEARCH_SNIPPET_LIMIT - lowerQuery.length) / 2);
302377
+ const start = Math.max(0, matchIndex - Math.max(leading, 0));
302378
+ const windowed = collapsed.slice(start, start + THREAD_SEARCH_SNIPPET_LIMIT);
302379
+ const prefixed = start > 0 ? `…${windowed}` : windowed;
302380
+ return prefixed.length > THREAD_SEARCH_SNIPPET_LIMIT ? prefixed.slice(0, THREAD_SEARCH_SNIPPET_LIMIT) : prefixed;
302381
+ }
301464
302382
  const ProjectionProjectLookupRowSchema = ProjectionProjectDbRowSchema;
301465
302383
  const ProjectionThreadIdLookupRowSchema = Struct({ threadId: ThreadId });
301466
302384
  const ProjectionThreadCheckpointContextThreadRowSchema = Struct({
@@ -301706,6 +302624,61 @@ const OrchestrationInfrastructureLayerLive = mergeAll(effect(ProjectionSnapshotQ
301706
302624
  AND deleted_at IS NULL
301707
302625
  AND archived_at IS NULL
301708
302626
  ORDER BY created_at ASC, thread_id ASC
302627
+ `
302628
+ });
302629
+ /**
302630
+ * Message bodies containing the query.
302631
+ *
302632
+ * `role IN ('user', 'assistant')` is not a filter for tidiness: the match
302633
+ * shape on the wire names one of those two as the side the hit came from,
302634
+ * and a system message has no side to report. Ordered newest first because a
302635
+ * capped search should keep what the person is most likely still thinking
302636
+ * about.
302637
+ */
302638
+ const searchThreadMessageRows = findAll({
302639
+ Request: ThreadSearchQueryInput,
302640
+ Result: ThreadSearchMessageRowSchema,
302641
+ execute: ({ pattern, limit }) => sql`
302642
+ SELECT
302643
+ m.thread_id AS "threadId",
302644
+ t.project_id AS "projectId",
302645
+ m.role AS "role",
302646
+ m.text AS "text",
302647
+ m.created_at AS "createdAt"
302648
+ FROM projection_thread_messages AS m
302649
+ JOIN projection_threads AS t ON t.thread_id = m.thread_id
302650
+ WHERE t.deleted_at IS NULL
302651
+ AND m.role IN ('user', 'assistant')
302652
+ AND LOWER(m.text) LIKE ${pattern} ESCAPE '\\'
302653
+ ORDER BY m.created_at DESC, m.message_id ASC
302654
+ LIMIT ${limit}
302655
+ `
302656
+ });
302657
+ /**
302658
+ * Threads whose own metadata contains the query.
302659
+ *
302660
+ * Title and branch, which is what the web command palette matches on. Title
302661
+ * is duplicated work for a client that already filters its in-memory list,
302662
+ * and harmless because matches are deduplicated by thread; branch is not,
302663
+ * because no client holds every branch.
302664
+ */
302665
+ const searchThreadMetadataRows = findAll({
302666
+ Request: ThreadSearchQueryInput,
302667
+ Result: ThreadSearchMetadataRowSchema,
302668
+ execute: ({ pattern, limit }) => sql`
302669
+ SELECT
302670
+ thread_id AS "threadId",
302671
+ project_id AS "projectId",
302672
+ title,
302673
+ branch
302674
+ FROM projection_threads
302675
+ WHERE deleted_at IS NULL
302676
+ AND (
302677
+ LOWER(title) LIKE ${pattern} ESCAPE '\\'
302678
+ OR LOWER(branch) LIKE ${pattern} ESCAPE '\\'
302679
+ )
302680
+ ORDER BY updated_at DESC, thread_id ASC
302681
+ LIMIT ${limit}
301709
302682
  `
301710
302683
  });
301711
302684
  /**
@@ -302452,6 +303425,40 @@ const OrchestrationInfrastructureLayerLive = mergeAll(effect(ProjectionSnapshotQ
302452
303425
  };
302453
303426
  const getShellSnapshot = () => readShellSnapshot("getShellSnapshot", listThreadRows);
302454
303427
  const getArchivedShellSnapshot = () => readShellSnapshot("getArchivedShellSnapshot", listArchivedThreadRows);
303428
+ const searchThreads = (input) => {
303429
+ const limit = input.limit ?? THREAD_SEARCH_DEFAULT_LIMIT;
303430
+ const lowerQuery = input.query.toLowerCase();
303431
+ const request = {
303432
+ pattern: toThreadSearchPattern(input.query),
303433
+ limit
303434
+ };
303435
+ return all([searchThreadMessageRows(request).pipe(mapError(toPersistenceSqlOrDecodeError("ProjectionSnapshotQuery.searchThreads:messages:query", "ProjectionSnapshotQuery.searchThreads:messages:decodeRows"))), searchThreadMetadataRows(request).pipe(mapError(toPersistenceSqlOrDecodeError("ProjectionSnapshotQuery.searchThreads:metadata:query", "ProjectionSnapshotQuery.searchThreads:metadata:decodeRows")))]).pipe(map$3(([messageRows, metadataRows]) => {
303436
+ const byThread = /* @__PURE__ */ new Map();
303437
+ for (const row of messageRows) {
303438
+ if (byThread.has(row.threadId)) continue;
303439
+ byThread.set(row.threadId, {
303440
+ threadId: row.threadId,
303441
+ projectId: row.projectId,
303442
+ source: row.role === "assistant" ? "assistant" : "user",
303443
+ snippet: buildThreadSearchSnippet(row.text, lowerQuery),
303444
+ messageCreatedAt: row.createdAt
303445
+ });
303446
+ }
303447
+ for (const row of metadataRows) {
303448
+ if (byThread.has(row.threadId)) continue;
303449
+ const branchMatched = row.branch?.toLowerCase().includes(lowerQuery) === true;
303450
+ const titleMatched = row.title.toLowerCase().includes(lowerQuery);
303451
+ byThread.set(row.threadId, {
303452
+ threadId: row.threadId,
303453
+ projectId: row.projectId,
303454
+ source: "user",
303455
+ snippet: buildThreadSearchSnippet(titleMatched || !branchMatched ? row.title : row.branch ?? row.title, lowerQuery),
303456
+ messageCreatedAt: null
303457
+ });
303458
+ }
303459
+ return { matches: [...byThread.values()].slice(0, limit) };
303460
+ }));
303461
+ };
302455
303462
  const listActiveWorkerShells = () => sql.withTransaction(all([
302456
303463
  listActiveWorkerThreadRows(void 0).pipe(mapError(toPersistenceSqlOrDecodeError("ProjectionSnapshotQuery.listActiveWorkerShells:listThreads:query", "ProjectionSnapshotQuery.listActiveWorkerShells:listThreads:decodeRows"))),
302457
303464
  listActiveWorkerSessionRows(void 0).pipe(mapError(toPersistenceSqlOrDecodeError("ProjectionSnapshotQuery.listActiveWorkerShells:listThreadSessions:query", "ProjectionSnapshotQuery.listActiveWorkerShells:listThreadSessions:decodeRows"))),
@@ -302619,6 +303626,7 @@ const OrchestrationInfrastructureLayerLive = mergeAll(effect(ProjectionSnapshotQ
302619
303626
  getSnapshot,
302620
303627
  getShellSnapshot,
302621
303628
  getArchivedShellSnapshot,
303629
+ searchThreads,
302622
303630
  listActiveWorkerShells,
302623
303631
  listStaleRunningSessionShells,
302624
303632
  getCounts,
@@ -317801,13 +318809,13 @@ const gitUrlCloneRouteLayer = add$1("POST", "/api/cloud/git/clone-url", gen(func
317801
318809
  //#endregion
317802
318810
  //#region src/server.ts
317803
318811
  const PtyAdapterLive = unwrap(gen(function* () {
317804
- if (typeof Bun !== "undefined") return (yield* promise(() => import("./BunPTY-oVISucl_.mjs"))).layer;
317805
- else return (yield* promise(() => import("./NodePTY-CVH2yNYl.mjs"))).layer;
318812
+ if (typeof Bun !== "undefined") return (yield* promise(() => import("./BunPTY-DkAIzLIx.mjs"))).layer;
318813
+ else return (yield* promise(() => import("./NodePTY-loxi8Vku.mjs"))).layer;
317806
318814
  }));
317807
318815
  const HttpServerLive = unwrap(gen(function* () {
317808
318816
  const config = yield* ServerConfig;
317809
- if (typeof Bun !== "undefined") return (yield* promise(() => import("./BunHttpServer-VI75aqJw.mjs"))).layer(bunHttpServerOptions(config));
317810
- const [NodeHttpServer, NodeHttp] = yield* all([promise(() => import("./NodeHttpServer-Dw7ivXfX.mjs")), promise(() => import("node:http"))]);
318817
+ if (typeof Bun !== "undefined") return (yield* promise(() => import("./BunHttpServer-CDh4Vioe.mjs"))).layer(bunHttpServerOptions(config));
318818
+ const [NodeHttpServer, NodeHttp] = yield* all([promise(() => import("./NodeHttpServer-DT0TF8s4.mjs")), promise(() => import("node:http"))]);
317811
318819
  return NodeHttpServer.layer(NodeHttp.createServer, {
317812
318820
  host: config.host,
317813
318821
  port: config.port
@@ -317815,10 +318823,10 @@ const HttpServerLive = unwrap(gen(function* () {
317815
318823
  }));
317816
318824
  const PlatformServicesLive = unwrap(gen(function* () {
317817
318825
  if (typeof Bun !== "undefined") {
317818
- const { layer } = yield* promise(() => import("./BunServices-D0JTDujL.mjs"));
318826
+ const { layer } = yield* promise(() => import("./BunServices-D6PAheqS.mjs"));
317819
318827
  return layer;
317820
318828
  } else {
317821
- const { layer } = yield* promise(() => import("./NodeServices-CBNXOYLG.mjs").then((n) => n.t));
318829
+ const { layer } = yield* promise(() => import("./NodeServices-BahzLhbo.mjs").then((n) => n.t));
317822
318830
  return layer;
317823
318831
  }
317824
318832
  }));
@@ -318813,7 +319821,7 @@ const connectLoginCommand = make$34("login", {
318813
319821
  })).json();
318814
319822
  },
318815
319823
  openBrowser: async (url) => {
318816
- const open = (await import("./open-FIgTDosf.mjs")).default;
319824
+ const open = (await import("./open-D8mGyWmt.mjs")).default;
318817
319825
  await open(url);
318818
319826
  return true;
318819
319827
  },