@p4code/cli 0.1.44 → 0.1.46
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 +778 -114
- package/dist/client/assets/{DiffPanel-DGA6lmJS.js → DiffPanel-CPaFbotP.js} +3 -3
- package/dist/client/assets/{FilePreviewPanel-CK8qJp9F.js → FilePreviewPanel-DFsoFXYz.js} +9 -9
- package/dist/client/assets/PreviewPanel-DeS97XYl.js +2 -0
- package/dist/client/assets/{PullRequestCodeTab-CDe0I2BM.js → PullRequestCodeTab-CgTgedYJ.js} +4 -4
- package/dist/client/assets/arrow-right-04_CsPkt.js +2 -0
- package/dist/client/assets/{fileCommentAnnotations-BhSDtT3N.js → fileCommentAnnotations-Dy17abwC.js} +2 -2
- package/dist/client/assets/index-C4Mhhj24.js +1053 -0
- package/dist/client/assets/index-Fl1HKL6P.css +1 -0
- package/dist/client/assets/{renderFileChildren-fmT_Ub8N.js → renderFileChildren-uPwkTxXP.js} +2 -2
- package/dist/client/assets/terminal-links-FRGOm6CP.js +46 -0
- package/dist/client/assets/{toggle-group-BEyPkq7x.js → toggle-group-DSjkAIxH.js} +2 -2
- package/dist/client/index.html +3 -3
- package/package.json +1 -1
- package/dist/client/assets/PreviewPanel-CI4w6I2v.js +0 -2
- package/dist/client/assets/arrow-right-DmbvicH4.js +0 -2
- package/dist/client/assets/index-BC9k5nqi.css +0 -1
- package/dist/client/assets/index-DqhCy3ru.js +0 -1044
- package/dist/client/assets/terminal-links-B9jLgUsu.js +0 -46
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.
|
|
240
|
+
var version = "0.1.46";
|
|
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:
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
37593
|
+
toolInput: asRecord$6(record?.input)
|
|
37366
37594
|
});
|
|
37367
37595
|
}
|
|
37368
37596
|
//#endregion
|
|
37369
37597
|
//#region src/orchestration/ActivityPayloadProjection.ts
|
|
37370
|
-
function asRecord$
|
|
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$
|
|
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$
|
|
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$
|
|
37651
|
+
const input = asRecord$5(item.input);
|
|
37424
37652
|
if (input && "command" in input) projectedItem.input = { command: input.command };
|
|
37425
|
-
const result = asRecord$
|
|
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$
|
|
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$
|
|
37464
|
-
const data = asRecord$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
37838
|
-
const record = asRecord$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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*
|
|
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*
|
|
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" }),
|
|
@@ -62578,6 +63283,18 @@ function normalizeClaudeStreamMessages(cause) {
|
|
|
62578
63283
|
const squashed = toMessage(Cause.squash(cause), "").trim();
|
|
62579
63284
|
return squashed.length > 0 ? [squashed] : [];
|
|
62580
63285
|
}
|
|
63286
|
+
const CLAUDE_MISSING_PATH_PATTERN = /path "([^"]+)" does not exist/i;
|
|
63287
|
+
/**
|
|
63288
|
+
* Raw cause text never reaches a runtime event: it is CLI output that can carry
|
|
63289
|
+
* credential material, which is why the event payload stays structural. But the
|
|
63290
|
+
* wrapper detail alone ("Claude runtime stream failed.") reads the same whether
|
|
63291
|
+
* the CLI crashed or the thread's directory was deleted, so recognized failure
|
|
63292
|
+
* shapes get a curated sentence built from matched fragments only.
|
|
63293
|
+
*/
|
|
63294
|
+
function describeProcessFailure(failure) {
|
|
63295
|
+
const missingPath = CLAUDE_MISSING_PATH_PATTERN.exec(toMessage(failure.cause, ""))?.[1];
|
|
63296
|
+
return missingPath === void 0 ? failure.detail : `${failure.detail} Directory does not exist: ${missingPath}`;
|
|
63297
|
+
}
|
|
62581
63298
|
function getEffectiveClaudeAgentEffort(effort, model) {
|
|
62582
63299
|
const normalized = normalizeClaudeCliEffort(effort, model);
|
|
62583
63300
|
return normalized ? normalized : null;
|
|
@@ -64683,7 +65400,8 @@ const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* (claudeSettin
|
|
|
64683
65400
|
if (context.turnState) yield* completeTurn(context, "interrupted", "Claude runtime interrupted.");
|
|
64684
65401
|
} else {
|
|
64685
65402
|
const failures = exit.cause.reasons.flatMap((reason) => Cause.isFailReason(reason) ? [reason.error] : []);
|
|
64686
|
-
const
|
|
65403
|
+
const firstFailure = failures[0];
|
|
65404
|
+
const message = firstFailure === void 0 ? "Claude runtime stream failed." : describeProcessFailure(firstFailure);
|
|
64687
65405
|
yield* emitRuntimeError(context, message, {
|
|
64688
65406
|
failureCount: failures.length,
|
|
64689
65407
|
failureTags: failures.map((failure) => failure._tag)
|
|
@@ -64776,6 +65494,13 @@ const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* (claudeSettin
|
|
|
64776
65494
|
operation: "startSession",
|
|
64777
65495
|
issue: `Expected provider '${PROVIDER$6}' but received '${input.provider}'.`
|
|
64778
65496
|
});
|
|
65497
|
+
if (input.cwd !== void 0 && input.cwd.length > 0) {
|
|
65498
|
+
if (!(yield* fileSystem.exists(input.cwd).pipe(Effect.orElseSucceed(() => false)))) return yield* new ProviderAdapterValidationError({
|
|
65499
|
+
provider: PROVIDER$6,
|
|
65500
|
+
operation: "startSession",
|
|
65501
|
+
issue: `Working directory does not exist: ${input.cwd}`
|
|
65502
|
+
});
|
|
65503
|
+
}
|
|
64779
65504
|
const existingContext = sessions.get(input.threadId);
|
|
64780
65505
|
if (existingContext) {
|
|
64781
65506
|
yield* Effect.logWarning("claude.session.replacing", {
|
|
@@ -90132,7 +90857,7 @@ const layerChildProcess = (handle, options = {}) => {
|
|
|
90132
90857
|
};
|
|
90133
90858
|
//#endregion
|
|
90134
90859
|
//#region ../../packages/shared/src/toolActivity.ts
|
|
90135
|
-
function asRecord
|
|
90860
|
+
function asRecord(value) {
|
|
90136
90861
|
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
90137
90862
|
}
|
|
90138
90863
|
function asTrimmedString(value) {
|
|
@@ -90162,10 +90887,10 @@ function extractCommandFromTitle$1(title) {
|
|
|
90162
90887
|
return /`([^`]+)`/u.exec(title)?.[1]?.trim() || void 0;
|
|
90163
90888
|
}
|
|
90164
90889
|
function extractToolCommand(data, title) {
|
|
90165
|
-
const item = asRecord
|
|
90166
|
-
const itemInput = asRecord
|
|
90167
|
-
const itemResult = asRecord
|
|
90168
|
-
const rawInput = asRecord
|
|
90890
|
+
const item = asRecord(data?.item);
|
|
90891
|
+
const itemInput = asRecord(item?.input);
|
|
90892
|
+
const itemResult = asRecord(item?.result);
|
|
90893
|
+
const rawInput = asRecord(data?.rawInput);
|
|
90169
90894
|
const direct = [
|
|
90170
90895
|
normalizeCommandValue$1(item?.command),
|
|
90171
90896
|
normalizeCommandValue$1(itemInput?.command),
|
|
@@ -90193,7 +90918,7 @@ function collectPaths(value, paths, seen, depth) {
|
|
|
90193
90918
|
}
|
|
90194
90919
|
return;
|
|
90195
90920
|
}
|
|
90196
|
-
const record = asRecord
|
|
90921
|
+
const record = asRecord(value);
|
|
90197
90922
|
if (!record) return;
|
|
90198
90923
|
for (const key of [
|
|
90199
90924
|
"path",
|
|
@@ -90252,7 +90977,7 @@ function deriveToolActivityPresentation(input) {
|
|
|
90252
90977
|
const title = asTrimmedString(input.title);
|
|
90253
90978
|
const detail = stripTrailingExitCode(asTrimmedString(input.detail));
|
|
90254
90979
|
const fallbackSummary = asTrimmedString(input.fallbackSummary) ?? "Tool";
|
|
90255
|
-
const data = asRecord
|
|
90980
|
+
const data = asRecord(input.data);
|
|
90256
90981
|
const command = extractToolCommand(data, title);
|
|
90257
90982
|
const primaryPath = extractPrimaryPath(data);
|
|
90258
90983
|
const action = classifyToolAction({
|
|
@@ -90276,7 +91001,7 @@ function deriveToolActivityPresentation(input) {
|
|
|
90276
91001
|
...primaryPath ? { detail: primaryPath } : {}
|
|
90277
91002
|
};
|
|
90278
91003
|
if (action === "search") {
|
|
90279
|
-
const query = asTrimmedString(asRecord
|
|
91004
|
+
const query = asTrimmedString(asRecord(data?.rawInput)?.query) ?? asTrimmedString(asRecord(data?.rawInput)?.pattern) ?? asTrimmedString(asRecord(data?.rawInput)?.searchTerm);
|
|
90280
91005
|
return {
|
|
90281
91006
|
summary: "Searched files",
|
|
90282
91007
|
...query ? { detail: query } : {}
|
|
@@ -98279,84 +99004,23 @@ const PreviewSnapshotToolkitHandlersLive = PreviewSnapshotToolkit.toLayer({ prev
|
|
|
98279
99004
|
PreviewToolkit.toLayer(handlers$3);
|
|
98280
99005
|
//#endregion
|
|
98281
99006
|
//#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
99007
|
var TicketResolver = class extends Context.Service()("@p4code/cli/mcp/TicketResolver") {};
|
|
98293
99008
|
const stringField = (record, key) => {
|
|
98294
99009
|
const value = record[key];
|
|
98295
99010
|
return typeof value === "string" && value.trim().length > 0 ? value.trim() : void 0;
|
|
98296
99011
|
};
|
|
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
99012
|
const make$4 = Effect.gen(function* () {
|
|
98323
|
-
const
|
|
98324
|
-
const oauth = yield* McpOAuth;
|
|
98325
|
-
const http = yield* HttpClient$1.HttpClient;
|
|
99013
|
+
const linear = yield* LinearClient;
|
|
98326
99014
|
return { resolve: Effect.fn("TicketResolver.resolve")(function* (reference) {
|
|
98327
99015
|
const identifier = parseTicketReference(reference);
|
|
98328
99016
|
if (identifier === null) return yield* new TicketResolveError({
|
|
98329
99017
|
reason: "not_found",
|
|
98330
99018
|
detail: `"${reference}" does not look like a ticket reference.`
|
|
98331
99019
|
});
|
|
98332
|
-
const
|
|
98333
|
-
|
|
98334
|
-
|
|
98335
|
-
|
|
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);
|
|
99020
|
+
const issue = readOne(yield* linear.call(LINEAR_GET_ISSUE_TOOL, { id: identifier }).pipe(Effect.mapError((cause) => new TicketResolveError({
|
|
99021
|
+
reason: cause.reason === "failed" ? "unavailable" : cause.reason,
|
|
99022
|
+
detail: cause.detail
|
|
99023
|
+
}))));
|
|
98360
99024
|
const url = issue === void 0 ? void 0 : stringField(issue, "url");
|
|
98361
99025
|
const title = issue === void 0 ? void 0 : stringField(issue, "title");
|
|
98362
99026
|
if (issue === void 0 || url === void 0 || title === void 0) return yield* new TicketResolveError({
|
|
@@ -102821,7 +103485,7 @@ const WorkspaceLayerLive = Layer.mergeAll(layer$51, WorkspaceEntriesLayerLive, W
|
|
|
102821
103485
|
const ProjectFaviconResolverLayerLive = layer$49.pipe(Layer.provide(layer$51), Layer.provide(layer$50));
|
|
102822
103486
|
const AuthLayerLive = layer$71.pipe(Layer.provideMerge(PersistenceLayerLive), Layer.provide(layer$75));
|
|
102823
103487
|
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.
|
|
103488
|
+
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
103489
|
const UsageLayerLive = layer$39.pipe(Layer.provide(layer$68.pipe(Layer.provide(layer$75))));
|
|
102826
103490
|
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
103491
|
/**
|