@p4code/cli 0.1.43 → 0.1.45

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/bin.mjs CHANGED
@@ -237,7 +237,7 @@ const make$87 = () => {
237
237
  const layer$79 = Layer.sync(NetService, make$87);
238
238
  //#endregion
239
239
  //#region package.json
240
- var version = "0.1.43";
240
+ var version = "0.1.45";
241
241
  //#endregion
242
242
  //#region src/config.ts
243
243
  /**
@@ -1606,6 +1606,7 @@ const ORCHESTRATION_WS_METHODS = {
1606
1606
  dispatchCommand: "orchestration.dispatchCommand",
1607
1607
  getTurnDiff: "orchestration.getTurnDiff",
1608
1608
  getFullThreadDiff: "orchestration.getFullThreadDiff",
1609
+ searchThreads: "orchestration.searchThreads",
1609
1610
  getArchivedShellSnapshot: "orchestration.getArchivedShellSnapshot",
1610
1611
  subscribeShell: "orchestration.subscribeShell",
1611
1612
  subscribeThread: "orchestration.subscribeThread"
@@ -2771,6 +2772,23 @@ const OrchestrationGetFullThreadDiffInput = Schema$1.Struct({
2771
2772
  toTurnCount: NonNegativeInt,
2772
2773
  ignoreWhitespace: Schema$1.optionalKey(Schema$1.Boolean)
2773
2774
  });
2775
+ const OrchestrationGetFullThreadDiffResult = ThreadTurnDiff;
2776
+ const OrchestrationThreadSearchSource = Schema$1.Literals(["user", "assistant"]);
2777
+ const OrchestrationSearchThreadsInput = Schema$1.Struct({
2778
+ query: TrimmedString.check(Schema$1.isMinLength(2), Schema$1.isMaxLength(200)),
2779
+ limit: Schema$1.optionalKey(Schema$1.Int.check(Schema$1.isBetween({
2780
+ minimum: 1,
2781
+ maximum: 50
2782
+ })))
2783
+ });
2784
+ const OrchestrationThreadSearchMatch = Schema$1.Struct({
2785
+ threadId: ThreadId,
2786
+ projectId: ProjectId,
2787
+ source: OrchestrationThreadSearchSource,
2788
+ snippet: Schema$1.String.check(Schema$1.isMaxLength(240)),
2789
+ messageCreatedAt: Schema$1.NullOr(IsoDateTime)
2790
+ });
2791
+ const OrchestrationSearchThreadsResult = Schema$1.Struct({ matches: Schema$1.Array(OrchestrationThreadSearchMatch) });
2774
2792
  const OrchestrationRpcSchemas = {
2775
2793
  dispatchCommand: {
2776
2794
  input: ClientOrchestrationCommand,
@@ -2782,7 +2800,11 @@ const OrchestrationRpcSchemas = {
2782
2800
  },
2783
2801
  getFullThreadDiff: {
2784
2802
  input: OrchestrationGetFullThreadDiffInput,
2785
- output: ThreadTurnDiff
2803
+ output: OrchestrationGetFullThreadDiffResult
2804
+ },
2805
+ searchThreads: {
2806
+ input: OrchestrationSearchThreadsInput,
2807
+ output: OrchestrationSearchThreadsResult
2786
2808
  },
2787
2809
  getArchivedShellSnapshot: {
2788
2810
  input: Schema$1.Struct({}),
@@ -2813,6 +2835,10 @@ var OrchestrationGetFullThreadDiffError = class extends Schema$1.TaggedErrorClas
2813
2835
  message: TrimmedNonEmptyString,
2814
2836
  cause: Schema$1.optional(Schema$1.Defect())
2815
2837
  }) {};
2838
+ var OrchestrationSearchThreadsError = class extends Schema$1.TaggedErrorClass()("OrchestrationSearchThreadsError", {
2839
+ message: TrimmedNonEmptyString,
2840
+ cause: Schema$1.optional(Schema$1.Defect())
2841
+ }) {};
2816
2842
  //#endregion
2817
2843
  //#region ../../packages/contracts/src/vcs.ts
2818
2844
  const VcsDriverKind = Schema$1.Literals([
@@ -8981,6 +9007,67 @@ var TicketResolveError = class extends Schema$1.TaggedErrorClass()("TicketResolv
8981
9007
  }
8982
9008
  };
8983
9009
  //#endregion
9010
+ //#region ../../packages/contracts/src/tracker.ts
9011
+ /**
9012
+ * Connecting an external issue tracker, by whichever transport is available.
9013
+ *
9014
+ * A tracker can be reached two ways: through a p4code-held API key, or through
9015
+ * the MCP server the user registered for it. The API key wins when both exist,
9016
+ * because it is the transport p4code can verify at the moment it is entered;
9017
+ * the MCP registration remains as the fallback so a machine that never entered
9018
+ * a key keeps working exactly as it did.
9019
+ *
9020
+ * The schemas are provider-keyed rather than Linear-shaped so the next tracker
9021
+ * adds a literal to `TrackerProviderName` instead of a parallel set of calls.
9022
+ *
9023
+ * @module contracts/tracker
9024
+ */
9025
+ /** Every tracker p4code can talk to. Today that is Linear. */
9026
+ const TrackerProviderName = Schema$1.Literals(["linear"]);
9027
+ /**
9028
+ * Which transport a tracker call would use right now.
9029
+ *
9030
+ * `none` is a real answer rather than an error: the settings panel renders it
9031
+ * as "not connected", and the board uses it to disable the tracker source with
9032
+ * a reason instead of offering one that fails every call.
9033
+ */
9034
+ const TrackerConnectionMode = Schema$1.Literals([
9035
+ "api",
9036
+ "mcp",
9037
+ "none"
9038
+ ]);
9039
+ const TrackerStatus = Schema$1.Struct({
9040
+ provider: TrackerProviderName,
9041
+ mode: TrackerConnectionMode,
9042
+ /**
9043
+ * The workspace the API key belongs to, learned when the key was verified.
9044
+ * Null in `mcp` and `none` modes, where p4code holds no credential to ask
9045
+ * with.
9046
+ */
9047
+ workspace: Schema$1.NullOr(Schema$1.String)
9048
+ });
9049
+ const TrackerProviderInput = Schema$1.Struct({ provider: TrackerProviderName });
9050
+ const TrackerApiKeyInput = Schema$1.Struct({
9051
+ provider: TrackerProviderName,
9052
+ apiKey: TrimmedNonEmptyString
9053
+ });
9054
+ /**
9055
+ * Why an API key could not be stored or removed.
9056
+ *
9057
+ * `invalid_key` means the tracker itself rejected the key when it was
9058
+ * verified, so nothing was stored; the fix is a different key. `unavailable`
9059
+ * means the tracker or the secret store could not be reached, so the same key
9060
+ * may well work on retry. The two deserve different sentences in the panel.
9061
+ */
9062
+ var TrackerApiKeyError = class extends Schema$1.TaggedErrorClass()("TrackerApiKeyError", {
9063
+ reason: Schema$1.Literals(["invalid_key", "unavailable"]),
9064
+ detail: Schema$1.String
9065
+ }) {
9066
+ get message() {
9067
+ return this.detail;
9068
+ }
9069
+ };
9070
+ //#endregion
8984
9071
  //#region ../../packages/contracts/src/threadControl.ts
8985
9072
  /**
8986
9073
  * Thread control over MCP - the toolkit an agent uses to act on its own thread
@@ -10441,6 +10528,9 @@ const WS_METHODS = {
10441
10528
  mcpSetSecret: "mcp.setSecret",
10442
10529
  mcpOAuthBegin: "mcp.oauthBegin",
10443
10530
  mcpOAuthDisconnect: "mcp.oauthDisconnect",
10531
+ trackerStatus: "tracker.status",
10532
+ trackerSetApiKey: "tracker.setApiKey",
10533
+ trackerClearApiKey: "tracker.clearApiKey",
10444
10534
  sourceControlLookupRepository: "sourceControl.lookupRepository",
10445
10535
  sourceControlCloneRepository: "sourceControl.cloneRepository",
10446
10536
  sourceControlPublishRepository: "sourceControl.publishRepository",
@@ -10746,6 +10836,31 @@ const WsMcpOAuthBeginRpc = Rpc.make(WS_METHODS.mcpOAuthBegin, {
10746
10836
  success: McpOAuthBeginResult,
10747
10837
  error: Schema$1.Union([McpRegistryError, EnvironmentAuthorizationError])
10748
10838
  });
10839
+ /**
10840
+ * The tracker's API-key trio, beside the MCP calls rather than folded into
10841
+ * them: the key belongs to a tracker provider, not to an MCP server, and it
10842
+ * must be settable on a machine that has no such server registered at all —
10843
+ * that is the case the key exists for.
10844
+ *
10845
+ * `setApiKey` verifies the key against the tracker before storing it, and its
10846
+ * success carries the workspace the tracker reported, so the panel can say
10847
+ * "connected to omnicasa" rather than "saved".
10848
+ */
10849
+ const WsTrackerStatusRpc = Rpc.make(WS_METHODS.trackerStatus, {
10850
+ payload: TrackerProviderInput,
10851
+ success: TrackerStatus,
10852
+ error: Schema$1.Union([EnvironmentAuthorizationError])
10853
+ });
10854
+ const WsTrackerSetApiKeyRpc = Rpc.make(WS_METHODS.trackerSetApiKey, {
10855
+ payload: TrackerApiKeyInput,
10856
+ success: TrackerStatus,
10857
+ error: Schema$1.Union([TrackerApiKeyError, EnvironmentAuthorizationError])
10858
+ });
10859
+ const WsTrackerClearApiKeyRpc = Rpc.make(WS_METHODS.trackerClearApiKey, {
10860
+ payload: TrackerProviderInput,
10861
+ success: TrackerStatus,
10862
+ error: Schema$1.Union([TrackerApiKeyError, EnvironmentAuthorizationError])
10863
+ });
10749
10864
  const WsMcpOAuthDisconnectRpc = Rpc.make(WS_METHODS.mcpOAuthDisconnect, {
10750
10865
  payload: McpServerNameInput,
10751
10866
  success: McpServerList,
@@ -11178,6 +11293,11 @@ const WsOrchestrationGetFullThreadDiffRpc = Rpc.make(ORCHESTRATION_WS_METHODS.ge
11178
11293
  success: OrchestrationRpcSchemas.getFullThreadDiff.output,
11179
11294
  error: Schema$1.Union([OrchestrationGetFullThreadDiffError, EnvironmentAuthorizationError])
11180
11295
  });
11296
+ const WsOrchestrationSearchThreadsRpc = Rpc.make(ORCHESTRATION_WS_METHODS.searchThreads, {
11297
+ payload: OrchestrationSearchThreadsInput,
11298
+ success: OrchestrationRpcSchemas.searchThreads.output,
11299
+ error: Schema$1.Union([OrchestrationSearchThreadsError, EnvironmentAuthorizationError])
11300
+ });
11181
11301
  const WsOrchestrationGetArchivedShellSnapshotRpc = Rpc.make(ORCHESTRATION_WS_METHODS.getArchivedShellSnapshot, {
11182
11302
  payload: OrchestrationRpcSchemas.getArchivedShellSnapshot.input,
11183
11303
  success: OrchestrationRpcSchemas.getArchivedShellSnapshot.output,
@@ -11229,7 +11349,7 @@ const WsSubscribeAuthAccessRpc = Rpc.make(WS_METHODS.subscribeAuthAccess, {
11229
11349
  error: Schema$1.Union([AuthAccessStreamError, EnvironmentAuthorizationError]),
11230
11350
  stream: true
11231
11351
  });
11232
- const WsRpcGroup = RpcGroup.make(WsServerProbeRpc, WsServerGetConfigRpc, WsServerRefreshProvidersRpc, WsServerUpdateProviderRpc, WsServerUpdateServerRpc, WsServerUpsertKeybindingRpc, WsServerRemoveKeybindingRpc, WsServerGetSettingsRpc, WsServerUpdateSettingsRpc, WsServerDiscoverSourceControlRpc, WsServerGetProviderUsageRpc, WsServerGetUsageSummaryRpc, WsServerGetTraceDiagnosticsRpc, WsServerGetProcessDiagnosticsRpc, WsServerGetProcessResourceHistoryRpc, WsServerSignalProcessRpc, WsPullRequestsListRpc, WsPullRequestsListStatsRpc, WsPullRequestsDetailRpc, WsPullRequestsActivityRpc, WsPullRequestsDiffFileContentsRpc, WsPullRequestsRunActionRpc, WsPullRequestsUpdateRpc, WsPullRequestsCommentRpc, WsPullRequestsUpdateCommentRpc, WsPullRequestsSubmitReviewRpc, WsPullRequestsReplyToThreadRpc, WsPullRequestsSetThreadResolutionRpc, WsPullRequestsSetReactionRpc, WsPullRequestsInvalidateRpc, WsPullRequestsReviewerCandidatesRpc, WsPullRequestsRequestReviewersRpc, WsSourceControlLookupRepositoryRpc, WsSourceControlCloneRepositoryRpc, WsSourceControlPublishRepositoryRpc, WsProjectsListEntriesRpc, WsProjectsReadFileRpc, WsProjectsSearchEntriesRpc, WsProjectsWriteFileRpc, WsShellOpenInEditorRpc, WsFilesystemBrowseRpc, WsAssetsCreateUrlRpc, WsSubscribeVcsStatusRpc, WsVcsPullRpc, WsVcsRefreshStatusRpc, WsGitRunStackedActionRpc, WsGitResolvePullRequestRpc, WsGitPreparePullRequestThreadRpc, WsVcsListRefsRpc, WsVcsCreateWorktreeRpc, WsVcsRemoveWorktreeRpc, WsVcsCreateRefRpc, WsVcsSwitchRefRpc, WsVcsInitRpc, WsReviewGetDiffPreviewRpc, WsTerminalOpenRpc, WsTerminalAttachRpc, WsTerminalWriteRpc, WsTerminalResizeRpc, WsTerminalClearRpc, WsTerminalRestartRpc, WsTerminalCloseRpc, WsSubscribeTerminalEventsRpc, WsSubscribeTerminalMetadataRpc, WsPreviewOpenRpc, WsPreviewNavigateRpc, WsPreviewResizeRpc, WsPreviewRefreshRpc, WsPreviewCloseRpc, WsPreviewListRpc, WsPreviewReportStatusRpc, WsPreviewAutomationConnectRpc, WsPreviewAutomationRespondRpc, WsPreviewAutomationFocusHostRpc, WsSubscribePreviewEventsRpc, WsSubscribeDiscoveredLocalServersRpc, WsSubscribeServerConfigRpc, WsSubscribeServerLifecycleRpc, WsSubscribeAuthAccessRpc, WsTasksListRpc, WsTasksGetRpc, WsTasksCreateRpc, WsTasksUpdateRpc, WsTasksDeleteRpc, WsTasksStartThreadRpc, WsSubscribeTasksRpc, WsHubGetSyncStatusRpc, WsHubConnectRpc, WsHubDisconnectRpc, WsHubSetSyncModeRpc, WsHubSetShareModeRpc, WsHubMintTokenRpc, WsSkillsSyncRpc, WsSkillsPublishRpc, WsSkillsPublishAllRpc, WsSkillsUnpublishRpc, WsAssetsReadRpc, WsAssetsSaveRpc, WsAssetsDeleteRpc, WsAssetsCreateLocalRpc, WsAssetsRemoveLocalRpc, WsSkillRegistrySearchRpc, WsSkillRegistryFetchRpc, WsMcpListRpc, WsMcpSaveRpc, WsMcpRemoveRpc, WsMcpSetSecretRpc, WsMcpOAuthBeginRpc, WsMcpOAuthDisconnectRpc, WsOrchestrationDispatchCommandRpc, WsOrchestrationGetTurnDiffRpc, WsOrchestrationGetFullThreadDiffRpc, WsOrchestrationGetArchivedShellSnapshotRpc, WsOrchestrationSubscribeShellRpc, WsOrchestrationSubscribeThreadRpc);
11352
+ const WsRpcGroup = RpcGroup.make(WsServerProbeRpc, WsServerGetConfigRpc, WsServerRefreshProvidersRpc, WsServerUpdateProviderRpc, WsServerUpdateServerRpc, WsServerUpsertKeybindingRpc, WsServerRemoveKeybindingRpc, WsServerGetSettingsRpc, WsServerUpdateSettingsRpc, WsServerDiscoverSourceControlRpc, WsServerGetProviderUsageRpc, WsServerGetUsageSummaryRpc, WsServerGetTraceDiagnosticsRpc, WsServerGetProcessDiagnosticsRpc, WsServerGetProcessResourceHistoryRpc, WsServerSignalProcessRpc, WsPullRequestsListRpc, WsPullRequestsListStatsRpc, WsPullRequestsDetailRpc, WsPullRequestsActivityRpc, WsPullRequestsDiffFileContentsRpc, WsPullRequestsRunActionRpc, WsPullRequestsUpdateRpc, WsPullRequestsCommentRpc, WsPullRequestsUpdateCommentRpc, WsPullRequestsSubmitReviewRpc, WsPullRequestsReplyToThreadRpc, WsPullRequestsSetThreadResolutionRpc, WsPullRequestsSetReactionRpc, WsPullRequestsInvalidateRpc, WsPullRequestsReviewerCandidatesRpc, WsPullRequestsRequestReviewersRpc, WsSourceControlLookupRepositoryRpc, WsSourceControlCloneRepositoryRpc, WsSourceControlPublishRepositoryRpc, WsProjectsListEntriesRpc, WsProjectsReadFileRpc, WsProjectsSearchEntriesRpc, WsProjectsWriteFileRpc, WsShellOpenInEditorRpc, WsFilesystemBrowseRpc, WsAssetsCreateUrlRpc, WsSubscribeVcsStatusRpc, WsVcsPullRpc, WsVcsRefreshStatusRpc, WsGitRunStackedActionRpc, WsGitResolvePullRequestRpc, WsGitPreparePullRequestThreadRpc, WsVcsListRefsRpc, WsVcsCreateWorktreeRpc, WsVcsRemoveWorktreeRpc, WsVcsCreateRefRpc, WsVcsSwitchRefRpc, WsVcsInitRpc, WsReviewGetDiffPreviewRpc, WsTerminalOpenRpc, WsTerminalAttachRpc, WsTerminalWriteRpc, WsTerminalResizeRpc, WsTerminalClearRpc, WsTerminalRestartRpc, WsTerminalCloseRpc, WsSubscribeTerminalEventsRpc, WsSubscribeTerminalMetadataRpc, WsPreviewOpenRpc, WsPreviewNavigateRpc, WsPreviewResizeRpc, WsPreviewRefreshRpc, WsPreviewCloseRpc, WsPreviewListRpc, WsPreviewReportStatusRpc, WsPreviewAutomationConnectRpc, WsPreviewAutomationRespondRpc, WsPreviewAutomationFocusHostRpc, WsSubscribePreviewEventsRpc, WsSubscribeDiscoveredLocalServersRpc, WsSubscribeServerConfigRpc, WsSubscribeServerLifecycleRpc, WsSubscribeAuthAccessRpc, WsTasksListRpc, WsTasksGetRpc, WsTasksCreateRpc, WsTasksUpdateRpc, WsTasksDeleteRpc, WsTasksStartThreadRpc, WsSubscribeTasksRpc, WsHubGetSyncStatusRpc, WsHubConnectRpc, WsHubDisconnectRpc, WsHubSetSyncModeRpc, WsHubSetShareModeRpc, WsHubMintTokenRpc, WsSkillsSyncRpc, WsSkillsPublishRpc, WsSkillsPublishAllRpc, WsSkillsUnpublishRpc, WsAssetsReadRpc, WsAssetsSaveRpc, WsAssetsDeleteRpc, WsAssetsCreateLocalRpc, WsAssetsRemoveLocalRpc, WsSkillRegistrySearchRpc, WsSkillRegistryFetchRpc, WsMcpListRpc, WsMcpSaveRpc, WsMcpRemoveRpc, WsMcpSetSecretRpc, WsMcpOAuthBeginRpc, WsMcpOAuthDisconnectRpc, WsTrackerStatusRpc, WsTrackerSetApiKeyRpc, WsTrackerClearApiKeyRpc, WsOrchestrationDispatchCommandRpc, WsOrchestrationGetTurnDiffRpc, WsOrchestrationGetFullThreadDiffRpc, WsOrchestrationSearchThreadsRpc, WsOrchestrationGetArchivedShellSnapshotRpc, WsOrchestrationSubscribeShellRpc, WsOrchestrationSubscribeThreadRpc);
11233
11353
  //#endregion
11234
11354
  //#region ../../packages/shared/src/oauthScope.ts
11235
11355
  const OAUTH_SCOPE_TOKEN = /^[\u0021\u0023-\u005b\u005d-\u007e]+$/u;
@@ -26505,6 +26625,17 @@ const ProjectionCountsRowSchema = Schema$1.Struct({
26505
26625
  projectCount: Schema$1.Number,
26506
26626
  threadCount: Schema$1.Number
26507
26627
  });
26628
+ const ProjectionThreadSearchRequest = Schema$1.Struct({
26629
+ pattern: Schema$1.String,
26630
+ limit: Schema$1.Int
26631
+ });
26632
+ const ProjectionThreadSearchRow = Schema$1.Struct({
26633
+ threadId: ThreadId,
26634
+ projectId: ProjectId,
26635
+ source: OrchestrationThreadSearchSource,
26636
+ matchText: Schema$1.String,
26637
+ messageCreatedAt: Schema$1.NullOr(IsoDateTime)
26638
+ });
26508
26639
  const WorkspaceRootLookupInput = Schema$1.Struct({ workspaceRoot: Schema$1.String });
26509
26640
  const ProjectIdLookupInput = Schema$1.Struct({ projectId: ProjectId });
26510
26641
  const ThreadIdLookupInput = Schema$1.Struct({ threadId: ThreadId });
@@ -26541,6 +26672,23 @@ function maxIso(left, right) {
26541
26672
  if (left === null) return right;
26542
26673
  return left > right ? left : right;
26543
26674
  }
26675
+ function escapeLikePattern(value) {
26676
+ return value.replaceAll("!", "!!").replaceAll("%", "!%").replaceAll("_", "!_");
26677
+ }
26678
+ function foldAsciiCase(value) {
26679
+ return value.replace(/[A-Z]/g, (character) => character.toLowerCase());
26680
+ }
26681
+ function buildSearchSnippet(text, query) {
26682
+ const normalizedText = text.replace(/\s+/g, " ").trim();
26683
+ if (normalizedText.length <= 240) return normalizedText;
26684
+ const normalizedQuery = foldAsciiCase(query.replace(/\s+/g, " ").trim());
26685
+ const matchIndex = foldAsciiCase(normalizedText).indexOf(normalizedQuery);
26686
+ const bodyLength = 236;
26687
+ const idealStart = Math.max(0, matchIndex - 72);
26688
+ const start = Math.min(idealStart, normalizedText.length - bodyLength);
26689
+ const end = Math.min(normalizedText.length, start + bodyLength);
26690
+ return `${start > 0 ? "…" : ""}${normalizedText.slice(start, end)}${end < normalizedText.length ? "…" : ""}`;
26691
+ }
26544
26692
  function computeSnapshotSequence(stateRows) {
26545
26693
  if (stateRows.length === 0) return 0;
26546
26694
  const sequenceByProjector = new Map(stateRows.map((row) => [row.projector, row.lastAppliedSequence]));
@@ -26965,6 +27113,72 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () {
26965
27113
  SELECT
26966
27114
  (SELECT COUNT(*) FROM projection_projects) AS "projectCount",
26967
27115
  (SELECT COUNT(*) FROM projection_threads) AS "threadCount"
27116
+ `
27117
+ });
27118
+ const searchActiveThreadRows = SqlSchema.findAll({
27119
+ Request: ProjectionThreadSearchRequest,
27120
+ Result: ProjectionThreadSearchRow,
27121
+ execute: ({ pattern, limit }) => sql`
27122
+ WITH ranked AS (
27123
+ SELECT
27124
+ threads.thread_id AS thread_id,
27125
+ threads.project_id AS project_id,
27126
+ CASE messages.role
27127
+ WHEN 'user' THEN 'user'
27128
+ ELSE 'assistant'
27129
+ END AS source,
27130
+ messages.text AS match_text,
27131
+ messages.created_at AS message_created_at,
27132
+ CASE messages.role
27133
+ WHEN 'user' THEN 0
27134
+ ELSE 1
27135
+ END AS match_rank,
27136
+ threads.updated_at AS thread_updated_at,
27137
+ ROW_NUMBER() OVER (
27138
+ PARTITION BY threads.thread_id
27139
+ ORDER BY
27140
+ CASE messages.role
27141
+ WHEN 'user' THEN 0
27142
+ ELSE 1
27143
+ END ASC,
27144
+ messages.created_at DESC,
27145
+ messages.message_id ASC
27146
+ ) AS thread_match_rank
27147
+ FROM projection_thread_messages AS messages
27148
+ INNER JOIN projection_threads AS threads
27149
+ ON threads.thread_id = messages.thread_id
27150
+ INNER JOIN projection_projects AS projects
27151
+ ON projects.project_id = threads.project_id
27152
+ WHERE threads.deleted_at IS NULL
27153
+ AND threads.archived_at IS NULL
27154
+ AND projects.deleted_at IS NULL
27155
+ AND messages.is_streaming = 0
27156
+ AND (
27157
+ messages.role = 'user'
27158
+ OR (
27159
+ messages.role = 'assistant'
27160
+ AND messages.message_id IN (
27161
+ SELECT turns.assistant_message_id
27162
+ FROM projection_turns AS turns
27163
+ WHERE turns.assistant_message_id IS NOT NULL
27164
+ )
27165
+ )
27166
+ )
27167
+ AND messages.text LIKE ${pattern} ESCAPE '!'
27168
+ )
27169
+ SELECT
27170
+ thread_id AS "threadId",
27171
+ project_id AS "projectId",
27172
+ source,
27173
+ match_text AS "matchText",
27174
+ message_created_at AS "messageCreatedAt"
27175
+ FROM ranked
27176
+ WHERE thread_match_rank = 1
27177
+ ORDER BY
27178
+ match_rank ASC,
27179
+ thread_updated_at DESC,
27180
+ thread_id ASC
27181
+ LIMIT ${limit}
26968
27182
  `
26969
27183
  });
26970
27184
  const getActiveProjectRowByWorkspaceRoot = SqlSchema.findOneOption({
@@ -27664,6 +27878,19 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () {
27664
27878
  toCheckpointRef: row.value.toCheckpointRef
27665
27879
  });
27666
27880
  });
27881
+ const searchThreads = (input) => Effect.gen(function* () {
27882
+ const escapedQuery = escapeLikePattern(input.query);
27883
+ return { matches: (yield* searchActiveThreadRows({
27884
+ pattern: `%${escapedQuery}%`,
27885
+ limit: input.limit ?? 50
27886
+ }).pipe(Effect.mapError(toPersistenceSqlOrDecodeError$1("ProjectionSnapshotQuery.searchThreads:query", "ProjectionSnapshotQuery.searchThreads:decodeRows")))).map((row) => ({
27887
+ threadId: row.threadId,
27888
+ projectId: row.projectId,
27889
+ source: row.source,
27890
+ snippet: buildSearchSnippet(row.matchText, input.query),
27891
+ messageCreatedAt: row.messageCreatedAt
27892
+ })) };
27893
+ });
27667
27894
  const getThreadShellById = (threadId) => Effect.gen(function* () {
27668
27895
  const [threadRow, latestTurnRow, sessionRow] = yield* Effect.all([
27669
27896
  getActiveThreadRowById({ threadId }).pipe(Effect.mapError(toPersistenceSqlOrDecodeError$1("ProjectionSnapshotQuery.getThreadShellById:getThread:query", "ProjectionSnapshotQuery.getThreadShellById:getThread:decodeRow"))),
@@ -27784,6 +28011,7 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () {
27784
28011
  getSnapshot,
27785
28012
  getShellSnapshot,
27786
28013
  getArchivedShellSnapshot,
28014
+ searchThreads,
27787
28015
  getSnapshotSequence,
27788
28016
  getCounts,
27789
28017
  getActiveProjectByWorkspaceRoot,
@@ -34223,7 +34451,7 @@ const NESTED_PAYLOAD_KEYS = [
34223
34451
  "operations"
34224
34452
  ];
34225
34453
  const MAX_COLLECT_DEPTH = 4;
34226
- function asRecord$6(value) {
34454
+ function asRecord$7(value) {
34227
34455
  return typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
34228
34456
  }
34229
34457
  function pushChangedFilePath(target, value) {
@@ -34238,7 +34466,7 @@ function collectChangedFilePaths(value, target, depth) {
34238
34466
  for (const entry of value) collectChangedFilePaths(entry, target, depth + 1);
34239
34467
  return;
34240
34468
  }
34241
- const record = asRecord$6(value);
34469
+ const record = asRecord$7(value);
34242
34470
  if (!record) return;
34243
34471
  for (const field of CHANGED_FILE_FIELDS) pushChangedFilePath(target, record[field]);
34244
34472
  for (const nestedKey of NESTED_PAYLOAD_KEYS) if (nestedKey in record) collectChangedFilePaths(record[nestedKey], target, depth + 1);
@@ -34249,7 +34477,7 @@ function collectChangedFilePaths(value, target, depth) {
34249
34477
  */
34250
34478
  function collectActivityChangedFilePaths(payload) {
34251
34479
  const target = /* @__PURE__ */ new Set();
34252
- collectChangedFilePaths(asRecord$6(asRecord$6(payload)?.data), target, 0);
34480
+ collectChangedFilePaths(asRecord$7(asRecord$7(payload)?.data), target, 0);
34253
34481
  return target;
34254
34482
  }
34255
34483
  /**
@@ -37352,22 +37580,22 @@ function classifyToolCategory(input) {
37352
37580
  if (normalized.includes("image")) return "image_view";
37353
37581
  return "tool";
37354
37582
  }
37355
- function asRecord$5(value) {
37583
+ function asRecord$6(value) {
37356
37584
  return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
37357
37585
  }
37358
37586
  /** Classify from a runtime item payload's `data` (`{ toolName, input }`). */
37359
37587
  function classifyToolCategoryFromToolData(data) {
37360
- const record = asRecord$5(data);
37588
+ const record = asRecord$6(data);
37361
37589
  const toolName = record?.toolName;
37362
37590
  if (typeof toolName !== "string" || toolName.trim().length === 0) return;
37363
37591
  return classifyToolCategory({
37364
37592
  toolName,
37365
- toolInput: asRecord$5(record?.input)
37593
+ toolInput: asRecord$6(record?.input)
37366
37594
  });
37367
37595
  }
37368
37596
  //#endregion
37369
37597
  //#region src/orchestration/ActivityPayloadProjection.ts
37370
- function asRecord$4(value) {
37598
+ function asRecord$5(value) {
37371
37599
  return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
37372
37600
  }
37373
37601
  function asTrimmedString$1(value) {
@@ -37390,7 +37618,7 @@ function collectChangedFiles(value, target, seen, depth) {
37390
37618
  }
37391
37619
  return;
37392
37620
  }
37393
- const record = asRecord$4(value);
37621
+ const record = asRecord$5(value);
37394
37622
  if (!record) return;
37395
37623
  pushChangedFile(target, seen, record.path);
37396
37624
  pushChangedFile(target, seen, record.filePath);
@@ -37416,13 +37644,13 @@ function collectChangedFiles(value, target, seen, depth) {
37416
37644
  }
37417
37645
  }
37418
37646
  function projectCommandData(data) {
37419
- const item = asRecord$4(data.item);
37647
+ const item = asRecord$5(data.item);
37420
37648
  if (!item) return;
37421
37649
  const projectedItem = {};
37422
37650
  if ("command" in item) projectedItem.command = item.command;
37423
- const input = asRecord$4(item.input);
37651
+ const input = asRecord$5(item.input);
37424
37652
  if (input && "command" in input) projectedItem.input = { command: input.command };
37425
- const result = asRecord$4(item.result);
37653
+ const result = asRecord$5(item.result);
37426
37654
  if (result && "command" in result) projectedItem.result = { command: result.command };
37427
37655
  return Object.keys(projectedItem).length > 0 ? projectedItem : void 0;
37428
37656
  }
@@ -37438,7 +37666,7 @@ function summarizeToolTextOutput(value) {
37438
37666
  return null;
37439
37667
  }
37440
37668
  function projectRawOutput(value) {
37441
- const rawOutput = asRecord$4(value);
37669
+ const rawOutput = asRecord$5(value);
37442
37670
  if (!rawOutput) return;
37443
37671
  if (typeof rawOutput.totalFiles === "number" && Number.isFinite(rawOutput.totalFiles)) return {
37444
37672
  totalFiles: rawOutput.totalFiles,
@@ -37460,14 +37688,14 @@ function projectRawOutput(value) {
37460
37688
  * the full payload in persistence and the event store.
37461
37689
  */
37462
37690
  function projectActivityPayload(activity) {
37463
- const payload = asRecord$4(activity.payload);
37464
- const data = asRecord$4(payload?.data);
37691
+ const payload = asRecord$5(activity.payload);
37692
+ const data = asRecord$5(payload?.data);
37465
37693
  if (!payload || !data || payload.itemType === "mcp_tool_call") return activity;
37466
37694
  const projectedData = {};
37467
37695
  const item = projectCommandData(data);
37468
37696
  if (item) projectedData.item = item;
37469
37697
  if ("command" in data) projectedData.command = data.command;
37470
- const input = asRecord$4(data.input);
37698
+ const input = asRecord$5(data.input);
37471
37699
  if (input && "command" in input) projectedData.input = { command: input.command };
37472
37700
  const changedFiles = [];
37473
37701
  collectChangedFiles(data, changedFiles, /* @__PURE__ */ new Set(), 0);
@@ -37494,7 +37722,7 @@ function projectActivityPayload(activity) {
37494
37722
  */
37495
37723
  function isResolvableContextWindowActivity(activity) {
37496
37724
  if (activity.kind !== "context-window.updated") return false;
37497
- const usedTokens = asRecord$4(activity.payload)?.usedTokens;
37725
+ const usedTokens = asRecord$5(activity.payload)?.usedTokens;
37498
37726
  return typeof usedTokens === "number" && Number.isFinite(usedTokens) && usedTokens >= 0;
37499
37727
  }
37500
37728
  /**
@@ -37510,7 +37738,7 @@ function isResolvableContextWindowActivity(activity) {
37510
37738
  * client.
37511
37739
  */
37512
37740
  function withoutContextWindowBreakdown$1(activity) {
37513
- const payload = asRecord$4(activity.payload);
37741
+ const payload = asRecord$5(activity.payload);
37514
37742
  if (!payload || payload.breakdown === void 0) return activity;
37515
37743
  const { breakdown: _breakdown, ...rest } = payload;
37516
37744
  return {
@@ -37525,7 +37753,7 @@ function dropStaleContextWindowActivities(activities) {
37525
37753
  const retainedIndexes = new Set(latestIndexByTurn.values());
37526
37754
  let breakdownIndex = null;
37527
37755
  for (const index of retainedIndexes) {
37528
- if (asRecord$4(activities[index].payload)?.breakdown === void 0) continue;
37756
+ if (asRecord$5(activities[index].payload)?.breakdown === void 0) continue;
37529
37757
  if (breakdownIndex === null || index > breakdownIndex) breakdownIndex = index;
37530
37758
  }
37531
37759
  return activities.flatMap((activity, index) => {
@@ -37654,7 +37882,7 @@ const normalizeDispatchCommand = (command) => Effect.gen(function* () {
37654
37882
  * already negotiated.
37655
37883
  */
37656
37884
  const MCP_PROTOCOL_VERSION = "2025-06-18";
37657
- const REQUEST_TIMEOUT_MS$1 = 15e3;
37885
+ const REQUEST_TIMEOUT_MS$2 = 15e3;
37658
37886
  var McpToolCallError = class extends Schema$1.TaggedErrorClass()("McpToolCallError", {
37659
37887
  detail: Schema$1.String,
37660
37888
  status: Schema$1.NullOr(Schema$1.Number)
@@ -37718,7 +37946,7 @@ const callMcpTool = Effect.fn("McpToolClient.callMcpTool")(function* (input) {
37718
37946
  ...input.headers,
37719
37947
  ...extraHeaders,
37720
37948
  accept: "application/json, text/event-stream"
37721
- }), body)).pipe(Effect.timeout(REQUEST_TIMEOUT_MS$1), Effect.catchCause((cause) => Effect.fail(new McpToolCallError({
37949
+ }), body)).pipe(Effect.timeout(REQUEST_TIMEOUT_MS$2), Effect.catchCause((cause) => Effect.fail(new McpToolCallError({
37722
37950
  detail: `Could not reach ${input.url}: ${String(cause)}`,
37723
37951
  status: null
37724
37952
  }))));
@@ -37791,6 +38019,14 @@ const callMcpTool = Effect.fn("McpToolClient.callMcpTool")(function* (input) {
37791
38019
  text
37792
38020
  };
37793
38021
  });
38022
+ /**
38023
+ * Linear's own tool for reading one issue.
38024
+ *
38025
+ * Named here rather than at each call site because two of them are now in
38026
+ * different modules - the board's store and the mirror refresh - and a tool
38027
+ * name that drifts between them fails at runtime with a shrug from the server.
38028
+ */
38029
+ const LINEAR_GET_ISSUE_TOOL = "get_issue";
37794
38030
  /** Why a Linear call could not be made, in terms a person can act on. */
37795
38031
  var LinearUnavailable = class extends Schema$1.TaggedErrorClass()("LinearUnavailable", {
37796
38032
  reason: Schema$1.Literals([
@@ -37804,8 +38040,8 @@ var LinearUnavailable = class extends Schema$1.TaggedErrorClass()("LinearUnavail
37804
38040
  return this.detail;
37805
38041
  }
37806
38042
  };
37807
- const asRecord$3 = (value) => typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
37808
- const asArray = (value) => Array.isArray(value) ? value : void 0;
38043
+ const asRecord$4 = (value) => typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
38044
+ const asArray$1 = (value) => Array.isArray(value) ? value : void 0;
37809
38045
  /**
37810
38046
  * Find the payload inside whatever the tool returned.
37811
38047
  *
@@ -37833,23 +38069,23 @@ const readPayload$1 = (result) => {
37833
38069
  * pagination scalars beside it.
37834
38070
  */
37835
38071
  const readRows = (payload) => {
37836
- const direct = asArray(payload);
37837
- if (direct !== void 0) return direct.map(asRecord$3).filter((row) => row !== void 0);
37838
- const record = asRecord$3(payload);
38072
+ const direct = asArray$1(payload);
38073
+ if (direct !== void 0) return direct.map(asRecord$4).filter((row) => row !== void 0);
38074
+ const record = asRecord$4(payload);
37839
38075
  if (record === void 0) return [];
37840
38076
  for (const value of Object.values(record)) {
37841
- const rows = asArray(value);
37842
- if (rows !== void 0) return rows.map(asRecord$3).filter((row) => row !== void 0);
38077
+ const rows = asArray$1(value);
38078
+ if (rows !== void 0) return rows.map(asRecord$4).filter((row) => row !== void 0);
37843
38079
  }
37844
38080
  return [];
37845
38081
  };
37846
38082
  /** The single object out of a result, unwrapping one level of nesting. */
37847
38083
  const readOne = (payload) => {
37848
- const record = asRecord$3(payload);
38084
+ const record = asRecord$4(payload);
37849
38085
  if (record === void 0) return;
37850
38086
  if (record["id"] !== void 0 || record["identifier"] !== void 0) return record;
37851
38087
  for (const value of Object.values(record)) {
37852
- const nested = asRecord$3(value);
38088
+ const nested = asRecord$4(value);
37853
38089
  if (nested?.["id"] !== void 0) return nested;
37854
38090
  }
37855
38091
  };
@@ -37908,6 +38144,463 @@ const makeLinearMcpClient = Effect.gen(function* () {
37908
38144
  });
37909
38145
  const LinearMcpClientLive = Layer.effect(LinearMcpClient, makeLinearMcpClient);
37910
38146
  //#endregion
38147
+ //#region src/tracker/TrackerSecrets.ts
38148
+ const secretName = (provider) => `tracker.${provider}.apiKey`;
38149
+ var TrackerSecrets = class extends Context.Service()("@p4code/cli/tracker/TrackerSecrets") {};
38150
+ const decodeCredential = (bytes) => {
38151
+ try {
38152
+ const parsed = JSON.parse(new TextDecoder().decode(bytes));
38153
+ if (typeof parsed !== "object" || parsed === null) return void 0;
38154
+ const record = parsed;
38155
+ const apiKey = record["apiKey"];
38156
+ if (typeof apiKey !== "string" || apiKey.length === 0) return void 0;
38157
+ const workspace = record["workspace"];
38158
+ return {
38159
+ apiKey,
38160
+ workspace: typeof workspace === "string" ? workspace : null
38161
+ };
38162
+ } catch {
38163
+ return;
38164
+ }
38165
+ };
38166
+ const makeTrackerSecrets = Effect.gen(function* () {
38167
+ const store = yield* ServerSecretStore;
38168
+ const read = (provider) => store.get(secretName(provider)).pipe(Effect.map(Option.match({
38169
+ onNone: () => Option.none(),
38170
+ onSome: (value) => Option.fromNullishOr(decodeCredential(value))
38171
+ })), Effect.orElseSucceed(() => Option.none()));
38172
+ const write = (provider, credential) => store.set(secretName(provider), new TextEncoder().encode(JSON.stringify(credential)));
38173
+ const clear = (provider) => store.remove(secretName(provider));
38174
+ return {
38175
+ read,
38176
+ write,
38177
+ clear
38178
+ };
38179
+ });
38180
+ const TrackerSecretsLive = Layer.effect(TrackerSecrets, makeTrackerSecrets);
38181
+ //#endregion
38182
+ //#region src/tracker/graphqlRequest.ts
38183
+ /**
38184
+ * One GraphQL POST, for any tracker that speaks it.
38185
+ *
38186
+ * Shared by design rather than folded into the Linear client: the next
38187
+ * tracker with a GraphQL API gets its transport from here and writes only its
38188
+ * queries. Deliberately minimal for the same reason `McpToolClient` is - no
38189
+ * retries, no batching, no persisted queries - one request, one parsed body.
38190
+ *
38191
+ * Transport failures and HTTP statuses fail the effect; GraphQL-level errors
38192
+ * do not. A body with `errors` beside partial `data` is a real answer some
38193
+ * calls expect - Linear reports a missing issue exactly that way - and only
38194
+ * the caller knows which errors mean "not found" and which mean "broken".
38195
+ *
38196
+ * @module tracker/graphqlRequest
38197
+ */
38198
+ const REQUEST_TIMEOUT_MS$1 = 15e3;
38199
+ var GraphqlRequestError = class extends Schema$1.TaggedErrorClass()("GraphqlRequestError", {
38200
+ detail: Schema$1.String,
38201
+ status: Schema$1.NullOr(Schema$1.Number)
38202
+ }) {
38203
+ get message() {
38204
+ return this.detail;
38205
+ }
38206
+ };
38207
+ const asRecord$3 = (value) => typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
38208
+ const errorMessages = (value) => {
38209
+ if (!Array.isArray(value)) return [];
38210
+ return value.map((entry) => asRecord$3(entry)?.["message"]).filter((message) => typeof message === "string");
38211
+ };
38212
+ const parseJsonRecord = (text) => {
38213
+ try {
38214
+ return asRecord$3(JSON.parse(text));
38215
+ } catch {
38216
+ return;
38217
+ }
38218
+ };
38219
+ const graphqlRequest = Effect.fn("tracker/graphqlRequest")(function* (input) {
38220
+ const response = yield* (yield* HttpClient$1.HttpClient).execute(HttpClientRequest$1.bodyJsonUnsafe(HttpClientRequest$1.setHeaders(HttpClientRequest$1.post(input.url), input.headers), {
38221
+ query: input.query,
38222
+ variables: input.variables ?? {}
38223
+ })).pipe(Effect.timeout(REQUEST_TIMEOUT_MS$1), Effect.catchCause((cause) => Effect.fail(new GraphqlRequestError({
38224
+ detail: `Could not reach ${input.url}: ${String(cause)}`,
38225
+ status: null
38226
+ }))));
38227
+ const body = yield* response.text.pipe(Effect.catchCause(() => Effect.succeed("")));
38228
+ if (response.status >= 400) return yield* new GraphqlRequestError({
38229
+ detail: response.status === 401 || response.status === 403 ? "The server rejected the credential." : `The server answered ${response.status}.`,
38230
+ status: response.status
38231
+ });
38232
+ const record = parseJsonRecord(body);
38233
+ if (record === void 0) return yield* new GraphqlRequestError({
38234
+ detail: "The server's answer was not a GraphQL response.",
38235
+ status: response.status
38236
+ });
38237
+ return {
38238
+ data: asRecord$3(record["data"]),
38239
+ errors: errorMessages(record["errors"])
38240
+ };
38241
+ });
38242
+ //#endregion
38243
+ //#region src/tracker/LinearApiClient.ts
38244
+ /**
38245
+ * Linear over its own GraphQL API, wearing the MCP tool vocabulary.
38246
+ *
38247
+ * This speaks the same three "tools" the Linear MCP server offers -
38248
+ * `list_issues`, `get_issue`, `save_issue` - and answers in the same row
38249
+ * shapes, so `LinearTasks` and the resolver cannot tell which transport
38250
+ * served them. That is the point: the MCP server remains the fallback, and a
38251
+ * consumer that could tell the two apart would need two code paths.
38252
+ *
38253
+ * The one real difference is hidden here: the MCP tools accept names where
38254
+ * the API wants ids - a team's key, a state's type, a person's display name -
38255
+ * so a write first resolves names to ids with lookup queries. A name that
38256
+ * resolves to nothing fails with a sentence naming it, which is the same
38257
+ * answer the MCP server gives, not a silent skip.
38258
+ *
38259
+ * @module tracker/LinearApiClient
38260
+ */
38261
+ const LINEAR_GRAPHQL_URL = "https://api.linear.app/graphql";
38262
+ /**
38263
+ * Every field a board card or a resolved ticket reads, fetched on every
38264
+ * query. The MCP server's `list_issues` takes a field selector; here the
38265
+ * selection is fixed and complete, because the cost of a spare field is
38266
+ * nothing next to the cost of a card that quietly lost its assignee.
38267
+ */
38268
+ const ISSUE_SELECTION = `
38269
+ id
38270
+ identifier
38271
+ title
38272
+ description
38273
+ url
38274
+ priority
38275
+ createdAt
38276
+ updatedAt
38277
+ state { name type }
38278
+ assignee { displayName name email }
38279
+ labels { nodes { name } }
38280
+ parent { id }
38281
+ `;
38282
+ /** Linear's state types, which `save_issue` accepts in place of a state name. */
38283
+ const STATE_TYPES = /* @__PURE__ */ new Set([
38284
+ "triage",
38285
+ "backlog",
38286
+ "unstarted",
38287
+ "started",
38288
+ "completed",
38289
+ "canceled"
38290
+ ]);
38291
+ const asRecord$2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
38292
+ const asArray = (value) => Array.isArray(value) ? value : [];
38293
+ const asString = (value) => typeof value === "string" && value.trim().length > 0 ? value.trim() : void 0;
38294
+ /**
38295
+ * One issue node, flattened to the row shape the MCP tools answer with, which
38296
+ * is the shape `taskFromLinearIssue` reads: state pulled apart into
38297
+ * `status`/`statusType`, the parent reduced to its id. `assignee` and
38298
+ * `labels` stay structured - the mapping already accepts both forms.
38299
+ */
38300
+ const toIssueRow = (node) => {
38301
+ const state = asRecord$2(node["state"]);
38302
+ const parent = asRecord$2(node["parent"]);
38303
+ const labels = asRecord$2(node["labels"]);
38304
+ return {
38305
+ id: node["id"],
38306
+ identifier: node["identifier"],
38307
+ title: node["title"],
38308
+ description: node["description"],
38309
+ url: node["url"],
38310
+ priority: node["priority"],
38311
+ createdAt: node["createdAt"],
38312
+ updatedAt: node["updatedAt"],
38313
+ status: state?.["name"],
38314
+ statusType: state?.["type"],
38315
+ assignee: node["assignee"],
38316
+ labels: labels === void 0 ? [] : asArray(labels["nodes"]),
38317
+ parentId: parent?.["id"]
38318
+ };
38319
+ };
38320
+ /** A case-insensitive equality filter over the ways a person is named. */
38321
+ const personFilter = (person) => ({ or: [
38322
+ { displayName: { eqIgnoreCase: person } },
38323
+ { name: { eqIgnoreCase: person } },
38324
+ { email: { eqIgnoreCase: person } }
38325
+ ] });
38326
+ const makeLinearApiTransport = Effect.gen(function* () {
38327
+ const secrets = yield* TrackerSecrets;
38328
+ const http = yield* HttpClient$1.HttpClient;
38329
+ const request = (apiKey, query, variables) => graphqlRequest({
38330
+ url: LINEAR_GRAPHQL_URL,
38331
+ headers: { authorization: apiKey },
38332
+ query,
38333
+ ...variables === void 0 ? {} : { variables }
38334
+ }).pipe(Effect.provideService(HttpClient$1.HttpClient, http), Effect.mapError((error) => error.status === 401 || error.status === 403 ? new LinearUnavailable({
38335
+ reason: "not_authorized",
38336
+ detail: "Linear rejected the stored API key. Update it in Settings."
38337
+ }) : new LinearUnavailable({
38338
+ reason: "failed",
38339
+ detail: error.detail
38340
+ })));
38341
+ /** The one entity out of `data`, or a failure carrying Linear's own words. */
38342
+ const readEntity = (response, key) => {
38343
+ const entity = asRecord$2(response.data?.[key]);
38344
+ if (entity !== void 0) return Effect.succeed(entity);
38345
+ if (response.errors.length === 0) return Effect.succeed(void 0);
38346
+ if (response.errors.some((message) => /not found|does not exist/iu.test(message))) return Effect.succeed(void 0);
38347
+ return Effect.fail(new LinearUnavailable({
38348
+ reason: "failed",
38349
+ detail: response.errors.join("; ")
38350
+ }));
38351
+ };
38352
+ const nodesOf = (response, key) => asArray(asRecord$2(response.data?.[key])?.["nodes"]).map(asRecord$2).filter((node) => node !== void 0);
38353
+ const resolveTeamId = (apiKey, team) => Effect.gen(function* () {
38354
+ const response = yield* request(apiKey, `query($filter: TeamFilter) { teams(filter: $filter, first: 2) { nodes { id } } }`, { filter: { or: [{ key: { eqIgnoreCase: team } }, { name: { eqIgnoreCase: team } }] } });
38355
+ const id = asString(nodesOf(response, "teams")[0]?.["id"]);
38356
+ if (id === void 0) return yield* new LinearUnavailable({
38357
+ reason: "failed",
38358
+ detail: `Linear has no team whose key or name is "${team}".`
38359
+ });
38360
+ return id;
38361
+ });
38362
+ const teamIdOfIssue = (apiKey, issueId) => Effect.gen(function* () {
38363
+ const response = yield* request(apiKey, `query($id: String!) { issue(id: $id) { team { id } } }`, { id: issueId });
38364
+ const issue = yield* readEntity(response, "issue");
38365
+ const id = asString(asRecord$2(issue?.["team"])?.["id"]);
38366
+ if (id === void 0) return yield* new LinearUnavailable({
38367
+ reason: "failed",
38368
+ detail: `Linear has no issue "${issueId}" to read a team from.`
38369
+ });
38370
+ return id;
38371
+ });
38372
+ /**
38373
+ * The state a write should land in, given a type, a name or an id - the
38374
+ * three things `save_issue` accepts.
38375
+ *
38376
+ * When a type names several states, the first by the team's own ordering
38377
+ * wins, except that `started` skips review-named states: a team's "In
38378
+ * Review" is a `started` state by type, and landing `in_progress` writes in
38379
+ * the review column is precisely the mixup the by-name special case in
38380
+ * `linearTaskMapping` exists to avoid.
38381
+ */
38382
+ const resolveStateId = (apiKey, teamId, state) => Effect.gen(function* () {
38383
+ const response = yield* request(apiKey, `query($filter: WorkflowStateFilter) {
38384
+ workflowStates(filter: $filter, first: 100) { nodes { id name type position } }
38385
+ }`, { filter: { team: { id: { eq: teamId } } } });
38386
+ const states = [...nodesOf(response, "workflowStates")].sort((a, b) => (Number(a["position"]) || 0) - (Number(b["position"]) || 0));
38387
+ const wanted = state.toLowerCase();
38388
+ const match = STATE_TYPES.has(wanted) ? states.find((candidate) => candidate["type"] === wanted && !(wanted === "started" && /review/iu.test(asString(candidate["name"]) ?? ""))) ?? states.find((candidate) => candidate["type"] === wanted) : states.find((candidate) => (asString(candidate["name"]) ?? "").toLowerCase() === wanted) ?? states.find((candidate) => candidate["id"] === state);
38389
+ const id = asString(match?.["id"]);
38390
+ if (id === void 0) return yield* new LinearUnavailable({
38391
+ reason: "failed",
38392
+ detail: `The team has no Linear state matching "${state}".`
38393
+ });
38394
+ return id;
38395
+ });
38396
+ const resolveAssigneeId = (apiKey, person) => Effect.gen(function* () {
38397
+ const response = yield* request(apiKey, `query($filter: UserFilter) { users(filter: $filter, first: 2) { nodes { id } } }`, { filter: personFilter(person) });
38398
+ const id = asString(nodesOf(response, "users")[0]?.["id"]);
38399
+ if (id === void 0) return yield* new LinearUnavailable({
38400
+ reason: "failed",
38401
+ detail: `Linear has no user matching "${person}".`
38402
+ });
38403
+ return id;
38404
+ });
38405
+ const resolveLabelIds = (apiKey, labels) => Effect.gen(function* () {
38406
+ if (labels.length === 0) return [];
38407
+ const response = yield* request(apiKey, `query($filter: IssueLabelFilter) { issueLabels(filter: $filter, first: 250) { nodes { id name } } }`, { filter: { or: labels.map((label) => ({ name: { eqIgnoreCase: label } })) } });
38408
+ const byName = new Map(nodesOf(response, "issueLabels").map((node) => [(asString(node["name"]) ?? "").toLowerCase(), asString(node["id"])]));
38409
+ const missing = labels.filter((label) => byName.get(label.toLowerCase()) === void 0);
38410
+ if (missing.length > 0) return yield* new LinearUnavailable({
38411
+ reason: "failed",
38412
+ detail: `Linear has no label named ${missing.map((label) => `"${label}"`).join(", ")}.`
38413
+ });
38414
+ return labels.map((label) => byName.get(label.toLowerCase())).filter((id) => id !== void 0);
38415
+ });
38416
+ const listIssues = (apiKey, args) => Effect.gen(function* () {
38417
+ const team = asString(args["team"]);
38418
+ const state = asString(args["state"]);
38419
+ const assignee = asString(args["assignee"]);
38420
+ const filter = {
38421
+ ...team === void 0 ? {} : { team: { or: [{ key: { eqIgnoreCase: team } }, { name: { eqIgnoreCase: team } }] } },
38422
+ ...state === void 0 ? {} : STATE_TYPES.has(state.toLowerCase()) ? { state: { type: { eq: state.toLowerCase() } } } : { state: { name: { eqIgnoreCase: state } } },
38423
+ ...assignee === void 0 ? {} : { assignee: personFilter(assignee) }
38424
+ };
38425
+ const response = yield* request(apiKey, `query($first: Int!, $includeArchived: Boolean!, $filter: IssueFilter) {
38426
+ issues(first: $first, includeArchived: $includeArchived, filter: $filter, orderBy: updatedAt) {
38427
+ nodes { ${ISSUE_SELECTION} }
38428
+ }
38429
+ }`, {
38430
+ first: typeof args["limit"] === "number" ? args["limit"] : 250,
38431
+ includeArchived: args["includeArchived"] !== false,
38432
+ filter
38433
+ });
38434
+ if (response.data === void 0 && response.errors.length > 0) return yield* new LinearUnavailable({
38435
+ reason: "failed",
38436
+ detail: response.errors.join("; ")
38437
+ });
38438
+ return { issues: nodesOf(response, "issues").map(toIssueRow) };
38439
+ });
38440
+ const getIssue = (apiKey, args) => Effect.gen(function* () {
38441
+ const id = asString(args["id"]);
38442
+ if (id === void 0) return yield* new LinearUnavailable({
38443
+ reason: "failed",
38444
+ detail: "get_issue needs an id."
38445
+ });
38446
+ const response = yield* request(apiKey, `query($id: String!) { issue(id: $id) { ${ISSUE_SELECTION} } }`, { id });
38447
+ const issue = yield* readEntity(response, "issue");
38448
+ return issue === void 0 ? {} : toIssueRow(issue);
38449
+ });
38450
+ const saveIssue = (apiKey, args) => Effect.gen(function* () {
38451
+ const issueId = asString(args["id"]);
38452
+ const team = asString(args["team"]);
38453
+ const state = asString(args["state"]);
38454
+ const input = {
38455
+ ...asString(args["title"]) === void 0 ? {} : { title: args["title"] },
38456
+ ...typeof args["description"] === "string" ? { description: args["description"] } : {},
38457
+ ...typeof args["priority"] === "number" ? { priority: args["priority"] } : {},
38458
+ ..."parentId" in args ? { parentId: args["parentId"] } : {}
38459
+ };
38460
+ if (issueId === void 0 && team === void 0) return yield* new LinearUnavailable({
38461
+ reason: "failed",
38462
+ detail: "save_issue needs an id to update or a team to create in."
38463
+ });
38464
+ if (state !== void 0) {
38465
+ const teamId = issueId === void 0 ? yield* resolveTeamId(apiKey, team ?? "") : yield* teamIdOfIssue(apiKey, issueId);
38466
+ input["stateId"] = yield* resolveStateId(apiKey, teamId, state);
38467
+ }
38468
+ if (issueId === void 0 && team !== void 0) input["teamId"] = yield* resolveTeamId(apiKey, team);
38469
+ if ("assignee" in args) {
38470
+ const person = asString(args["assignee"]);
38471
+ input["assigneeId"] = person === void 0 ? null : yield* resolveAssigneeId(apiKey, person);
38472
+ }
38473
+ if (Array.isArray(args["labels"])) input["labelIds"] = yield* resolveLabelIds(apiKey, args["labels"].filter((label) => typeof label === "string"));
38474
+ const response = issueId === void 0 ? yield* request(apiKey, `mutation($input: IssueCreateInput!) {
38475
+ issueCreate(input: $input) { issue { ${ISSUE_SELECTION} } }
38476
+ }`, { input }) : yield* request(apiKey, `mutation($id: String!, $input: IssueUpdateInput!) {
38477
+ issueUpdate(id: $id, input: $input) { issue { ${ISSUE_SELECTION} } }
38478
+ }`, {
38479
+ id: issueId,
38480
+ input
38481
+ });
38482
+ const payload = yield* readEntity(response, issueId === void 0 ? "issueCreate" : "issueUpdate");
38483
+ const issue = asRecord$2(payload?.["issue"]);
38484
+ if (issue === void 0) return yield* new LinearUnavailable({
38485
+ reason: "failed",
38486
+ detail: response.errors.length > 0 ? response.errors.join("; ") : "Linear accepted the write but returned no issue."
38487
+ });
38488
+ return toIssueRow(issue);
38489
+ });
38490
+ const isConfigured = secrets.read("linear").pipe(Effect.map((credential) => Option.isSome(credential)));
38491
+ const call = (toolName, args) => Effect.gen(function* () {
38492
+ const credential = yield* secrets.read("linear");
38493
+ if (Option.isNone(credential)) return yield* new LinearUnavailable({
38494
+ reason: "not_configured",
38495
+ detail: "No Linear API key is stored on this machine. Add one in Settings."
38496
+ });
38497
+ const apiKey = credential.value.apiKey;
38498
+ switch (toolName) {
38499
+ case "list_issues": return yield* listIssues(apiKey, args);
38500
+ case "get_issue": return yield* getIssue(apiKey, args);
38501
+ case "save_issue": return yield* saveIssue(apiKey, args);
38502
+ default: return yield* new LinearUnavailable({
38503
+ reason: "failed",
38504
+ detail: `The Linear API transport has no translation for "${toolName}".`
38505
+ });
38506
+ }
38507
+ });
38508
+ const verify = (apiKey) => Effect.gen(function* () {
38509
+ const response = yield* request(apiKey, `query { viewer { id } organization { name urlKey } }`).pipe(Effect.mapError((error) => error.reason === "not_authorized" ? new LinearUnavailable({
38510
+ reason: "not_authorized",
38511
+ detail: "Linear rejected this API key."
38512
+ }) : error));
38513
+ if (asString(asRecord$2(response.data?.["viewer"])?.["id"]) === void 0) return yield* new LinearUnavailable({
38514
+ reason: "not_authorized",
38515
+ detail: response.errors.length > 0 ? response.errors.join("; ") : "Linear rejected this API key."
38516
+ });
38517
+ const organization = asRecord$2(response.data?.["organization"]);
38518
+ return { workspace: asString(organization?.["name"]) ?? asString(organization?.["urlKey"]) ?? null };
38519
+ });
38520
+ return {
38521
+ isConfigured,
38522
+ call,
38523
+ verify
38524
+ };
38525
+ });
38526
+ //#endregion
38527
+ //#region src/tracker/LinearClient.ts
38528
+ /**
38529
+ * Linear, by whichever transport this machine has.
38530
+ *
38531
+ * Two transports exist: the GraphQL API behind a p4code-held key, and the MCP
38532
+ * server the user registered. The key wins whenever one is stored - it is the
38533
+ * transport p4code verified the moment it was entered - and the MCP path
38534
+ * remains the fallback, so a machine that never entered a key behaves exactly
38535
+ * as it always has.
38536
+ *
38537
+ * Chosen per call rather than at startup: a key can be entered or cleared in
38538
+ * Settings while the server runs, and a client captured at construction would
38539
+ * keep answering from a credential that no longer exists.
38540
+ *
38541
+ * Every consumer - the board, the resolver, the settings panel - talks to
38542
+ * this and never to a transport directly, which is what keeps "connected to
38543
+ * Linear" meaning one thing.
38544
+ *
38545
+ * @module tracker/LinearClient
38546
+ */
38547
+ var LinearClient = class extends Context.Service()("@p4code/cli/tracker/LinearClient") {};
38548
+ const makeLinearClient = Effect.gen(function* () {
38549
+ const secrets = yield* TrackerSecrets;
38550
+ const api = yield* makeLinearApiTransport;
38551
+ const mcp = yield* LinearMcpClient;
38552
+ const storedCredential = secrets.read("linear");
38553
+ const isConfigured = Effect.gen(function* () {
38554
+ if (Option.isSome(yield* storedCredential)) return true;
38555
+ return yield* mcp.isConfigured;
38556
+ });
38557
+ const call = (toolName, args) => Effect.gen(function* () {
38558
+ const credential = yield* storedCredential;
38559
+ return Option.isSome(credential) ? yield* api.call(toolName, args) : yield* mcp.call(toolName, args);
38560
+ });
38561
+ const status = Effect.gen(function* () {
38562
+ const credential = yield* storedCredential;
38563
+ if (Option.isSome(credential)) return {
38564
+ provider: "linear",
38565
+ mode: "api",
38566
+ workspace: credential.value.workspace
38567
+ };
38568
+ return {
38569
+ provider: "linear",
38570
+ mode: (yield* mcp.isConfigured) ? "mcp" : "none",
38571
+ workspace: null
38572
+ };
38573
+ });
38574
+ const setApiKey = (apiKey) => Effect.gen(function* () {
38575
+ const verified = yield* api.verify(apiKey).pipe(Effect.mapError((cause) => new TrackerApiKeyError({
38576
+ reason: cause.reason === "not_authorized" ? "invalid_key" : "unavailable",
38577
+ detail: cause.detail
38578
+ })));
38579
+ yield* secrets.write("linear", {
38580
+ apiKey,
38581
+ workspace: verified.workspace
38582
+ }).pipe(Effect.mapError((cause) => new TrackerApiKeyError({
38583
+ reason: "unavailable",
38584
+ detail: `The key was verified but could not be stored: ${cause.message}`
38585
+ })));
38586
+ return yield* status;
38587
+ });
38588
+ return {
38589
+ isConfigured,
38590
+ call,
38591
+ status,
38592
+ setApiKey,
38593
+ clearApiKey: Effect.gen(function* () {
38594
+ yield* secrets.clear("linear").pipe(Effect.mapError((cause) => new TrackerApiKeyError({
38595
+ reason: "unavailable",
38596
+ detail: `The stored key could not be removed: ${cause.message}`
38597
+ })));
38598
+ return yield* status;
38599
+ })
38600
+ };
38601
+ });
38602
+ const LinearClientLive = Layer.effect(LinearClient, makeLinearClient);
38603
+ //#endregion
37911
38604
  //#region ../../packages/shared/src/ticketReference.ts
37912
38605
  /**
37913
38606
  * Recognizing a tracker reference in text somebody pasted.
@@ -38022,7 +38715,7 @@ const STATUS_FROM_LINEAR_TYPE = {
38022
38715
  completed: "done",
38023
38716
  canceled: "cancelled"
38024
38717
  };
38025
- const asRecord$2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
38718
+ const asRecord$1 = (value) => typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
38026
38719
  const text = (value) => typeof value === "string" && value.trim().length > 0 ? value.trim() : void 0;
38027
38720
  /**
38028
38721
  * A person's name out of whatever Linear put in the field: an object when the
@@ -38031,14 +38724,14 @@ const text = (value) => typeof value === "string" && value.trim().length > 0 ? v
38031
38724
  const personName = (value) => {
38032
38725
  const direct = text(value);
38033
38726
  if (direct !== void 0) return direct;
38034
- const record = asRecord$2(value);
38727
+ const record = asRecord$1(value);
38035
38728
  if (record === void 0) return null;
38036
38729
  return text(record["displayName"]) ?? text(record["name"]) ?? text(record["email"]) ?? null;
38037
38730
  };
38038
38731
  /** Label names, from either `["Bug"]` or `[{ name: "Bug" }]`. */
38039
38732
  const labelNames = (value) => {
38040
38733
  if (!Array.isArray(value)) return [];
38041
- return value.map((entry) => text(entry) ?? text(asRecord$2(entry)?.["name"])).filter((name) => name !== void 0);
38734
+ return value.map((entry) => text(entry) ?? text(asRecord$1(entry)?.["name"])).filter((name) => name !== void 0);
38042
38735
  };
38043
38736
  /**
38044
38737
  * The p4code status an issue is in.
@@ -38083,7 +38776,7 @@ const statusToLinearState = (status) => {
38083
38776
  * Medium, and quietly rewrite the local row with it.
38084
38777
  */
38085
38778
  const priorityFromLinear = (value) => {
38086
- const numeric = typeof value === "number" ? value : asRecord$2(value)?.["value"];
38779
+ const numeric = typeof value === "number" ? value : asRecord$1(value)?.["value"];
38087
38780
  return typeof numeric === "number" ? PRIORITY_FROM_LINEAR[numeric] ?? "none" : "none";
38088
38781
  };
38089
38782
  const priorityToLinear = (priority) => PRIORITY_TO_LINEAR[priority];
@@ -38175,7 +38868,7 @@ const linearFailed = (operation, cause) => new PersistenceSqlError({
38175
38868
  });
38176
38869
  const makeLinearTaskRepository = Effect.gen(function* () {
38177
38870
  const sql = yield* SqlClient.SqlClient;
38178
- const linear = yield* LinearMcpClient;
38871
+ const linear = yield* LinearClient;
38179
38872
  const settings = yield* ServerSettingsService;
38180
38873
  const changes = yield* PubSub.unbounded();
38181
38874
  const publish = (event) => PubSub.publish(changes, event).pipe(Effect.asVoid);
@@ -38912,7 +39605,7 @@ const makeRegistry = Effect.gen(function* () {
38912
39605
  const linear = yield* makeLinearTaskRepository;
38913
39606
  return {
38914
39607
  forSource: (source) => source === "linear" ? linear : board,
38915
- linearAvailable: (yield* LinearMcpClient).isConfigured
39608
+ linearAvailable: (yield* LinearClient).isConfigured
38916
39609
  };
38917
39610
  });
38918
39611
  const TaskRepositoryRegistryLive = Layer.effect(TaskRepositoryRegistry, makeRegistry);
@@ -57931,6 +58624,7 @@ const RPC_REQUIRED_SCOPE = /* @__PURE__ */ new Map([
57931
58624
  [ORCHESTRATION_WS_METHODS.dispatchCommand, AuthOrchestrationOperateScope],
57932
58625
  [ORCHESTRATION_WS_METHODS.getTurnDiff, AuthOrchestrationReadScope],
57933
58626
  [ORCHESTRATION_WS_METHODS.getFullThreadDiff, AuthOrchestrationReadScope],
58627
+ [ORCHESTRATION_WS_METHODS.searchThreads, AuthOrchestrationReadScope],
57934
58628
  [ORCHESTRATION_WS_METHODS.subscribeShell, AuthOrchestrationReadScope],
57935
58629
  [ORCHESTRATION_WS_METHODS.getArchivedShellSnapshot, AuthOrchestrationReadScope],
57936
58630
  [ORCHESTRATION_WS_METHODS.subscribeThread, AuthOrchestrationReadScope],
@@ -57971,6 +58665,9 @@ const RPC_REQUIRED_SCOPE = /* @__PURE__ */ new Map([
57971
58665
  [WS_METHODS.mcpSetSecret, AuthOrchestrationOperateScope],
57972
58666
  [WS_METHODS.mcpOAuthBegin, AuthOrchestrationOperateScope],
57973
58667
  [WS_METHODS.mcpOAuthDisconnect, AuthOrchestrationOperateScope],
58668
+ [WS_METHODS.trackerStatus, AuthOrchestrationReadScope],
58669
+ [WS_METHODS.trackerSetApiKey, AuthOrchestrationOperateScope],
58670
+ [WS_METHODS.trackerClearApiKey, AuthOrchestrationOperateScope],
57974
58671
  [WS_METHODS.serverGetSettings, AuthOrchestrationReadScope],
57975
58672
  [WS_METHODS.serverUpdateSettings, AuthOrchestrationOperateScope],
57976
58673
  [WS_METHODS.serverDiscoverSourceControl, AuthOrchestrationReadScope],
@@ -58117,6 +58814,7 @@ const makeWsRpcLayer = (currentSession, previewAutomationBroker) => WsRpcGroup.t
58117
58814
  const assetSync = yield* AssetSync;
58118
58815
  const mcpRegistry = yield* McpRegistry;
58119
58816
  const mcpOAuth = yield* McpOAuth;
58817
+ const linearClient = yield* LinearClient;
58120
58818
  const claudeMcpFiles = yield* ClaudeMcpFiles;
58121
58819
  const skillRegistry = yield* SkillRegistry;
58122
58820
  const listMcpServersEverywhere = Effect.gen(function* () {
@@ -58493,6 +59191,10 @@ const makeWsRpcLayer = (currentSession, previewAutomationBroker) => WsRpcGroup.t
58493
59191
  message: "Failed to load full thread diff",
58494
59192
  cause
58495
59193
  }))), { "rpc.aggregate": "orchestration" }),
59194
+ [ORCHESTRATION_WS_METHODS.searchThreads]: (input) => observeRpcEffect$1(ORCHESTRATION_WS_METHODS.searchThreads, projectionSnapshotQuery.searchThreads(input).pipe(Effect.mapError((cause) => new OrchestrationSearchThreadsError({
59195
+ message: "Failed to search threads",
59196
+ cause
59197
+ }))), { "rpc.aggregate": "orchestration" }),
58496
59198
  [ORCHESTRATION_WS_METHODS.subscribeShell]: (input) => observeRpcStreamEffect$1(ORCHESTRATION_WS_METHODS.subscribeShell, Effect.gen(function* () {
58497
59199
  const liveBuffer = yield* Queue.unbounded();
58498
59200
  yield* Effect.forkScoped(orchestrationEngine.streamDomainEvents.pipe(Stream.runForEach((event) => Queue.offer(liveBuffer, {
@@ -58770,6 +59472,9 @@ const makeWsRpcLayer = (currentSession, previewAutomationBroker) => WsRpcGroup.t
58770
59472
  return yield* mcpOAuth.begin(found.registration);
58771
59473
  }), { "rpc.aggregate": "mcp" }),
58772
59474
  [WS_METHODS.mcpOAuthDisconnect]: ({ name }) => observeRpcEffect$1(WS_METHODS.mcpOAuthDisconnect, mcpOAuth.disconnect(name).pipe(Effect.andThen(listMcpServersEverywhere)), { "rpc.aggregate": "mcp" }),
59475
+ [WS_METHODS.trackerStatus]: (_input) => observeRpcEffect$1(WS_METHODS.trackerStatus, linearClient.status, { "rpc.aggregate": "tracker" }),
59476
+ [WS_METHODS.trackerSetApiKey]: ({ apiKey }) => observeRpcEffect$1(WS_METHODS.trackerSetApiKey, linearClient.setApiKey(apiKey), { "rpc.aggregate": "tracker" }),
59477
+ [WS_METHODS.trackerClearApiKey]: (_input) => observeRpcEffect$1(WS_METHODS.trackerClearApiKey, linearClient.clearApiKey, { "rpc.aggregate": "tracker" }),
58773
59478
  [WS_METHODS.serverGetSettings]: (_input) => observeRpcEffect$1(WS_METHODS.serverGetSettings, serverSettings.getSettings.pipe(Effect.map(redactServerSettingsForClient)), { "rpc.aggregate": "server" }),
58774
59479
  [WS_METHODS.serverUpdateSettings]: ({ patch }) => observeRpcEffect$1(WS_METHODS.serverUpdateSettings, serverSettings.updateSettings(patch).pipe(Effect.map(redactServerSettingsForClient)), { "rpc.aggregate": "server" }),
58775
59480
  [WS_METHODS.serverDiscoverSourceControl]: (_input) => observeRpcEffect$1(WS_METHODS.serverDiscoverSourceControl, sourceControlDiscovery.discover, { "rpc.aggregate": "server" }),
@@ -90132,7 +90837,7 @@ const layerChildProcess = (handle, options = {}) => {
90132
90837
  };
90133
90838
  //#endregion
90134
90839
  //#region ../../packages/shared/src/toolActivity.ts
90135
- function asRecord$1(value) {
90840
+ function asRecord(value) {
90136
90841
  return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
90137
90842
  }
90138
90843
  function asTrimmedString(value) {
@@ -90162,10 +90867,10 @@ function extractCommandFromTitle$1(title) {
90162
90867
  return /`([^`]+)`/u.exec(title)?.[1]?.trim() || void 0;
90163
90868
  }
90164
90869
  function extractToolCommand(data, title) {
90165
- const item = asRecord$1(data?.item);
90166
- const itemInput = asRecord$1(item?.input);
90167
- const itemResult = asRecord$1(item?.result);
90168
- const rawInput = asRecord$1(data?.rawInput);
90870
+ const item = asRecord(data?.item);
90871
+ const itemInput = asRecord(item?.input);
90872
+ const itemResult = asRecord(item?.result);
90873
+ const rawInput = asRecord(data?.rawInput);
90169
90874
  const direct = [
90170
90875
  normalizeCommandValue$1(item?.command),
90171
90876
  normalizeCommandValue$1(itemInput?.command),
@@ -90193,7 +90898,7 @@ function collectPaths(value, paths, seen, depth) {
90193
90898
  }
90194
90899
  return;
90195
90900
  }
90196
- const record = asRecord$1(value);
90901
+ const record = asRecord(value);
90197
90902
  if (!record) return;
90198
90903
  for (const key of [
90199
90904
  "path",
@@ -90252,7 +90957,7 @@ function deriveToolActivityPresentation(input) {
90252
90957
  const title = asTrimmedString(input.title);
90253
90958
  const detail = stripTrailingExitCode(asTrimmedString(input.detail));
90254
90959
  const fallbackSummary = asTrimmedString(input.fallbackSummary) ?? "Tool";
90255
- const data = asRecord$1(input.data);
90960
+ const data = asRecord(input.data);
90256
90961
  const command = extractToolCommand(data, title);
90257
90962
  const primaryPath = extractPrimaryPath(data);
90258
90963
  const action = classifyToolAction({
@@ -90276,7 +90981,7 @@ function deriveToolActivityPresentation(input) {
90276
90981
  ...primaryPath ? { detail: primaryPath } : {}
90277
90982
  };
90278
90983
  if (action === "search") {
90279
- const query = asTrimmedString(asRecord$1(data?.rawInput)?.query) ?? asTrimmedString(asRecord$1(data?.rawInput)?.pattern) ?? asTrimmedString(asRecord$1(data?.rawInput)?.searchTerm);
90984
+ const query = asTrimmedString(asRecord(data?.rawInput)?.query) ?? asTrimmedString(asRecord(data?.rawInput)?.pattern) ?? asTrimmedString(asRecord(data?.rawInput)?.searchTerm);
90280
90985
  return {
90281
90986
  summary: "Searched files",
90282
90987
  ...query ? { detail: query } : {}
@@ -98279,84 +98984,23 @@ const PreviewSnapshotToolkitHandlersLive = PreviewSnapshotToolkit.toLayer({ prev
98279
98984
  PreviewToolkit.toLayer(handlers$3);
98280
98985
  //#endregion
98281
98986
  //#region src/mcp/TicketResolver.ts
98282
- /**
98283
- * The registered server name a Linear ticket is resolved through.
98284
- *
98285
- * Matched by name rather than by URL: the name is what the user typed in
98286
- * Settings and what every other surface calls it, and a workspace may reach
98287
- * Linear through a proxy URL that no pattern here would recognize.
98288
- */
98289
- const LINEAR_SERVER_NAME = "linear";
98290
- /** Linear's own tool for reading one issue. */
98291
- const LINEAR_GET_ISSUE_TOOL = "get_issue";
98292
98987
  var TicketResolver = class extends Context.Service()("@p4code/cli/mcp/TicketResolver") {};
98293
98988
  const stringField = (record, key) => {
98294
98989
  const value = record[key];
98295
98990
  return typeof value === "string" && value.trim().length > 0 ? value.trim() : void 0;
98296
98991
  };
98297
- const asRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
98298
- /**
98299
- * Find the issue inside whatever the tool returned.
98300
- *
98301
- * MCP leaves the shape of a tool result to the tool, so this accepts the three
98302
- * arrangements in the wild - the structured payload itself, that payload with
98303
- * the issue nested one level down, or a text block that happens to be JSON -
98304
- * rather than assuming the one Linear happens to send today.
98305
- */
98306
- const readIssue = (result) => {
98307
- const structured = asRecord(result.structuredContent);
98308
- if (structured !== void 0) {
98309
- const nested = asRecord(structured["issue"]);
98310
- if (nested !== void 0) return nested;
98311
- if (stringField(structured, "url") !== void 0) return structured;
98312
- }
98313
- try {
98314
- const parsed = JSON.parse(result.text);
98315
- const record = asRecord(parsed);
98316
- if (record === void 0) return structured;
98317
- return asRecord(record["issue"]) ?? record;
98318
- } catch {
98319
- return structured;
98320
- }
98321
- };
98322
98992
  const make$4 = Effect.gen(function* () {
98323
- const registry = yield* McpRegistry;
98324
- const oauth = yield* McpOAuth;
98325
- const http = yield* HttpClient$1.HttpClient;
98993
+ const linear = yield* LinearClient;
98326
98994
  return { resolve: Effect.fn("TicketResolver.resolve")(function* (reference) {
98327
98995
  const identifier = parseTicketReference(reference);
98328
98996
  if (identifier === null) return yield* new TicketResolveError({
98329
98997
  reason: "not_found",
98330
98998
  detail: `"${reference}" does not look like a ticket reference.`
98331
98999
  });
98332
- const server = (yield* registry.list).find((candidate) => candidate.registration.name.toLowerCase() === LINEAR_SERVER_NAME && candidate.registration.enabled);
98333
- if (server === void 0 || server.registration.transport === "stdio") return yield* new TicketResolveError({
98334
- reason: "not_configured",
98335
- detail: "No Linear MCP server is registered on this machine. Add it in Settings to link tickets."
98336
- });
98337
- const registration = server.registration;
98338
- const token = yield* oauth.accessTokenFor(registration);
98339
- const headers = {
98340
- ...registration.headers,
98341
- ...Option.isSome(token) ? { authorization: `Bearer ${token.value}` } : {}
98342
- };
98343
- if (Option.isNone(token) && Object.keys(registration.headers ?? {}).length === 0) return yield* new TicketResolveError({
98344
- reason: "not_authorized",
98345
- detail: "Linear is registered but not signed in on this machine. Sign in from Settings."
98346
- });
98347
- const result = yield* callMcpTool({
98348
- url: registration.url,
98349
- headers,
98350
- toolName: LINEAR_GET_ISSUE_TOOL,
98351
- arguments: { id: identifier }
98352
- }).pipe(Effect.provideService(HttpClient$1.HttpClient, http), Effect.mapError((error) => error.status === 401 || error.status === 403 ? new TicketResolveError({
98353
- reason: "not_authorized",
98354
- detail: "Linear rejected p4code's sign-in. Sign in again from Settings."
98355
- }) : new TicketResolveError({
98356
- reason: "unavailable",
98357
- detail: error.detail
98358
- })));
98359
- const issue = readIssue(result);
99000
+ const issue = readOne(yield* linear.call(LINEAR_GET_ISSUE_TOOL, { id: identifier }).pipe(Effect.mapError((cause) => new TicketResolveError({
99001
+ reason: cause.reason === "failed" ? "unavailable" : cause.reason,
99002
+ detail: cause.detail
99003
+ }))));
98360
99004
  const url = issue === void 0 ? void 0 : stringField(issue, "url");
98361
99005
  const title = issue === void 0 ? void 0 : stringField(issue, "title");
98362
99006
  if (issue === void 0 || url === void 0 || title === void 0) return yield* new TicketResolveError({
@@ -102821,7 +103465,7 @@ const WorkspaceLayerLive = Layer.mergeAll(layer$51, WorkspaceEntriesLayerLive, W
102821
103465
  const ProjectFaviconResolverLayerLive = layer$49.pipe(Layer.provide(layer$51), Layer.provide(layer$50));
102822
103466
  const AuthLayerLive = layer$71.pipe(Layer.provideMerge(PersistenceLayerLive), Layer.provide(layer$75));
102823
103467
  const ProviderRuntimeLayerLive = ProviderSessionReaperLive.pipe(Layer.provideMerge(ProviderLayerLive), Layer.provideMerge(OrchestrationLayerLive));
102824
- const RuntimeCoreDependenciesLive = ReactorLayerLive.pipe(Layer.provideMerge(CheckpointingLayerLive), Layer.provideMerge(SourceControlProviderRegistryLayerLive), Layer.provideMerge(GitLayerLive), Layer.provideMerge(VcsLayerLive), Layer.provideMerge(ProviderRuntimeLayerLive), Layer.provideMerge(Layer.mergeAll(TerminalLayerLive, PreviewLayerLive, RoutedTaskRepositoryLive)), Layer.provideMerge(Layer.mergeAll(PersistenceLayerLive, layer$64)), Layer.provideMerge(layer$59), Layer.provideMerge(ProviderRegistryLive), Layer.provideMerge(ProviderInstanceRegistryHydrationLive), Layer.provideMerge(ProviderEventLoggersLive), Layer.provideMerge(OpenCodeRuntimeLive), Layer.provideMerge(layer$68.pipe(Layer.provide(layer$75))), Layer.provideMerge(WorkspaceLayerLive), Layer.provideMerge(ProjectFaviconResolverLayerLive), Layer.provideMerge(layer$60), Layer.provideMerge(layer$53), Layer.provideMerge(AuthLayerLive), Layer.provideMerge(Layer.mergeAll(layer$1, LinearMcpClientLive).pipe(Layer.provideMerge(Layer.mergeAll(layer$65, layer$38).pipe(Layer.provideMerge(layer$66), Layer.provideMerge(layer$67))))), Layer.provideMerge(layer$75));
103468
+ const RuntimeCoreDependenciesLive = ReactorLayerLive.pipe(Layer.provideMerge(CheckpointingLayerLive), Layer.provideMerge(SourceControlProviderRegistryLayerLive), Layer.provideMerge(GitLayerLive), Layer.provideMerge(VcsLayerLive), Layer.provideMerge(ProviderRuntimeLayerLive), Layer.provideMerge(Layer.mergeAll(TerminalLayerLive, PreviewLayerLive, RoutedTaskRepositoryLive)), Layer.provideMerge(Layer.mergeAll(PersistenceLayerLive, layer$64)), Layer.provideMerge(layer$59), Layer.provideMerge(ProviderRegistryLive), Layer.provideMerge(ProviderInstanceRegistryHydrationLive), Layer.provideMerge(ProviderEventLoggersLive), Layer.provideMerge(OpenCodeRuntimeLive), Layer.provideMerge(layer$68.pipe(Layer.provide(layer$75))), Layer.provideMerge(WorkspaceLayerLive), Layer.provideMerge(ProjectFaviconResolverLayerLive), Layer.provideMerge(layer$60), Layer.provideMerge(layer$53), Layer.provideMerge(AuthLayerLive), Layer.provideMerge(layer$1.pipe(Layer.provideMerge(LinearClientLive), Layer.provideMerge(Layer.mergeAll(LinearMcpClientLive, TrackerSecretsLive)), Layer.provideMerge(Layer.mergeAll(layer$65, layer$38).pipe(Layer.provideMerge(layer$66), Layer.provideMerge(layer$67))))), Layer.provideMerge(layer$75));
102825
103469
  const UsageLayerLive = layer$39.pipe(Layer.provide(layer$68.pipe(Layer.provide(layer$75))));
102826
103470
  const RuntimeDependenciesLive = RuntimeCoreDependenciesLive.pipe(Layer.provideMerge(UsageLayerLive), Layer.provideMerge(layer$15), Layer.provideMerge(layer$14), Layer.provideMerge(layer$13), Layer.provideMerge(layer$56), Layer.provideMerge(layer$58), Layer.provideMerge(layer$57), Layer.provide(layer$79));
102827
103471
  /**