@akanjs/devkit 3.0.0-beta.9 → 3.0.0
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/DEV_RUNTIME_KNOBS.md +44 -26
- package/README.ko.md +21 -7
- package/README.md +17 -8
- package/abstractDoc.test.ts +6 -15
- package/abstractDoc.ts +6 -8
- package/agentsIndex.test.ts +4 -7
- package/agentsIndex.ts +6 -22
- package/akanApp/BackendImportGraph.test.ts +8 -22
- package/akanApp/BackendImportGraph.ts +1 -20
- package/akanApp/akanApp.host.test.ts +56 -3
- package/akanApp/akanApp.host.ts +218 -278
- package/akanApp/devBootLatch.test.ts +39 -0
- package/akanApp/devBootLatch.ts +39 -0
- package/akanApp/devHostPolicy.test.ts +81 -35
- package/akanApp/devHostPolicy.ts +41 -102
- package/akanConfig/akanBin.test.ts +62 -0
- package/akanConfig/akanBin.ts +78 -0
- package/akanConfig/akanConfig.test.ts +439 -141
- package/akanConfig/akanConfig.ts +359 -275
- package/akanConfig/index.ts +1 -0
- package/akanConfig/types.ts +22 -8
- package/akanContext.ts +110 -160
- package/akanMcpContract.ts +28 -73
- package/appSelectionMemory.ts +1 -4
- package/applicationBuildReporter.ts +37 -5
- package/applicationBuildRunner.test.ts +41 -5
- package/applicationBuildRunner.ts +71 -63
- package/applicationTestPreload.ts +3 -12
- package/artifact/implicitRootLayout.test.ts +32 -30
- package/artifact/implicitRootLayout.ts +58 -106
- package/artifact/routeSeedIndex.test.ts +7 -6
- package/artifact/routeSeedIndex.ts +11 -44
- package/biome.base.json +35 -2
- package/biomeBase.ts +2 -5
- package/cloud/cloudApi.test.ts +202 -0
- package/cloud/cloudApi.ts +77 -39
- package/cloud/configLock.test.ts +64 -0
- package/cloud/configLock.ts +96 -0
- package/cloud/constants.ts +26 -5
- package/cloud/globalConfig.ts +71 -32
- package/codeAgent/agent/AkanCodeServices.ts +97 -0
- package/codeAgent/agent/AkanContextFiles.ts +94 -0
- package/codeAgent/agent/CodeAgent.ts +551 -0
- package/codeAgent/agent/CodeAgentAsks.ts +66 -0
- package/codeAgent/agent/CodeAgentEventMapper.ts +225 -0
- package/codeAgent/agent/CodeAgentGate.ts +93 -0
- package/codeAgent/agent/CodeAgentProxy.ts +82 -0
- package/codeAgent/agent/CodeAgentRpcHost.ts +103 -0
- package/codeAgent/agent/CodeAgentRpcListener.ts +109 -0
- package/codeAgent/agent/CodeAgentStreamPrinter.ts +86 -0
- package/codeAgent/agent/CodeAgentSuspended.ts +85 -0
- package/codeAgent/agent/CodeAgentUi.ts +47 -0
- package/codeAgent/agent/CodeMailbox.ts +148 -0
- package/codeAgent/agent/CodeSessionFork.ts +56 -0
- package/codeAgent/agent/CodeSessionIndex.ts +78 -0
- package/codeAgent/agent/SubagentPool.ts +214 -0
- package/codeAgent/agent/akanCodeModel.ts +47 -0
- package/codeAgent/agent/akanCodePaths.ts +31 -0
- package/codeAgent/agent/akanContextFiles.test.ts +114 -0
- package/codeAgent/agent/akanSystemPrompt.ts +46 -0
- package/codeAgent/agent/codeAgent.test.ts +178 -0
- package/codeAgent/agent/codeAgentContract.test.ts +523 -0
- package/codeAgent/agent/codeAgentProxy.test.ts +74 -0
- package/codeAgent/agent/codeAgentRpcListener.test.ts +113 -0
- package/codeAgent/agent/codeAgentSuspended.test.ts +47 -0
- package/codeAgent/agent/codeMailbox.test.ts +76 -0
- package/codeAgent/agent/codeSessionFork.test.ts +58 -0
- package/codeAgent/agent/codeSessionIndex.test.ts +77 -0
- package/codeAgent/agent/consoleToStderr.ts +8 -0
- package/codeAgent/agent/jsonLines.ts +29 -0
- package/codeAgent/agent/stringArg.ts +5 -0
- package/codeAgent/feedback/DevLogFeedback.ts +82 -0
- package/codeAgent/feedback/PreviewChrome.ts +82 -0
- package/codeAgent/feedback/PreviewFeedback.ts +46 -0
- package/codeAgent/feedback/PreviewView.ts +95 -0
- package/codeAgent/feedback/TurnFeedback.ts +98 -0
- package/codeAgent/feedback/VerifyFeedback.ts +40 -0
- package/codeAgent/feedback/previewChrome.test.ts +73 -0
- package/codeAgent/index.ts +34 -0
- package/codeAgent/skills/akan-code/SKILL.md +130 -0
- package/codeAgent/skills/akan-debug/SKILL.md +42 -0
- package/codeAgent/skills/akan-deps/SKILL.md +50 -0
- package/codeAgent/skills/akan-err/SKILL.md +39 -0
- package/codeAgent/skills/akan-module/SKILL.md +45 -0
- package/codeAgent/skills/akan-scaffold/SKILL.md +76 -0
- package/codeAgent/skills/akan-scalar/SKILL.md +52 -0
- package/codeAgent/skills/akan-service/SKILL.md +34 -0
- package/codeAgent/skills/akan-store/SKILL.md +67 -0
- package/codeAgent/skills/akan-ui/SKILL.md +63 -0
- package/codeAgent/skills/akan-validate/SKILL.md +44 -0
- package/codeAgent/tools/AkanCli.ts +50 -0
- package/codeAgent/tools/AkanCodePlugins.ts +136 -0
- package/codeAgent/tools/AkanEditScope.ts +48 -0
- package/codeAgent/tools/AkanEnvKeys.ts +50 -0
- package/codeAgent/tools/AkanToolPack.ts +92 -0
- package/codeAgent/tools/AkanVerifier.ts +48 -0
- package/codeAgent/tools/AskToolPack.ts +99 -0
- package/codeAgent/tools/MailToolPack.ts +76 -0
- package/codeAgent/tools/McpClient.ts +187 -0
- package/codeAgent/tools/McpOAuth.ts +233 -0
- package/codeAgent/tools/McpOAuthLoopback.ts +64 -0
- package/codeAgent/tools/McpServerConfig.ts +129 -0
- package/codeAgent/tools/McpSignIn.ts +158 -0
- package/codeAgent/tools/McpTokenStore.ts +81 -0
- package/codeAgent/tools/McpToolPack.ts +199 -0
- package/codeAgent/tools/SessionToolPack.ts +91 -0
- package/codeAgent/tools/WebToolPack.ts +147 -0
- package/codeAgent/tools/akanCodePlugins.test.ts +677 -0
- package/codeAgent/tools/askToolPack.test.ts +116 -0
- package/codeAgent/tools/mcpOAuth.test.ts +612 -0
- package/codeAgent/tools/mcpServerConfig.test.ts +212 -0
- package/codegenLock.test.ts +4 -13
- package/codegenLock.ts +3 -17
- package/commandDecorators/argMeta.ts +1 -5
- package/commandDecorators/camelToKebabCase.ts +1 -0
- package/commandDecorators/command.ts +46 -102
- package/commandDecorators/commandBuilder.ts +30 -61
- package/commandDecorators/commandDecorators.test.ts +2 -10
- package/commandDecorators/dependencyBuilder.ts +10 -1
- package/commandDecorators/helpFormatter.ts +68 -145
- package/commandDecorators/index.ts +0 -2
- package/commandDecorators/targetMeta.ts +9 -7
- package/createTunnel.ts +7 -14
- package/csrE2e/csrE2eHarness.fixture.ts +505 -0
- package/csrE2e/csrFastRefresh.e2e.test.ts +233 -0
- package/csrE2e/csrPageStack.e2e.test.ts +252 -0
- package/csrE2e/csrPatchFirst.e2e.test.ts +87 -0
- package/csrE2e/csrRootLayout.e2e.test.ts +134 -0
- package/csrE2e/devHmrLatency.e2e.test.ts +391 -0
- package/csrE2e/registryVendor.e2e.test.ts +221 -0
- package/csrE2e/ssrRegistry.e2e.test.ts +644 -0
- package/dependencyScanner.test.ts +4 -16
- package/dependencyScanner.ts +31 -80
- package/devkitUtils.test.ts +48 -20
- package/executors.linkFallback.test.ts +103 -0
- package/executors.test.ts +176 -106
- package/executors.ts +212 -498
- package/fileEditor.ts +9 -37
- package/{getDirname.test.ts → fileSys.test.ts} +5 -3
- package/fileSys.ts +22 -9
- package/formSetterScanner.ts +3 -16
- package/frontendBuild/allRoutesBuilder.ts +7 -21
- package/frontendBuild/autoImportSync.test.ts +27 -38
- package/frontendBuild/autoImportSync.ts +31 -65
- package/frontendBuild/buildRouteClient.test.ts +123 -37
- package/frontendBuild/bundleDefine.test.ts +12 -0
- package/frontendBuild/bundleDefine.ts +16 -0
- package/frontendBuild/clientBuildTypes.ts +5 -36
- package/frontendBuild/clientEntriesBundler.ts +9 -40
- package/frontendBuild/clientEntryDiscovery.test.ts +112 -0
- package/frontendBuild/clientEntryDiscovery.ts +122 -110
- package/frontendBuild/csrArtifactBuilder.ts +9 -31
- package/frontendBuild/csrDevArtifactWriter.test.ts +151 -0
- package/frontendBuild/csrDevArtifactWriter.ts +307 -0
- package/frontendBuild/csrDevBundler.test.ts +66 -0
- package/frontendBuild/csrDevBundler.ts +95 -0
- package/frontendBuild/csrDevFactoryCheck.test.ts +33 -0
- package/frontendBuild/csrDevFactoryCheck.ts +39 -0
- package/frontendBuild/csrDevModuleCompiler.test.ts +293 -0
- package/frontendBuild/csrDevModuleCompiler.ts +422 -0
- package/frontendBuild/csrDevPatcher.test.ts +572 -0
- package/frontendBuild/csrDevPatcher.ts +548 -0
- package/frontendBuild/csrDevPaths.ts +145 -0
- package/frontendBuild/csrDevResolver.test.ts +239 -0
- package/frontendBuild/csrDevResolver.ts +363 -0
- package/frontendBuild/csrDevTypes.ts +61 -0
- package/frontendBuild/csrEntryFiles.ts +24 -0
- package/frontendBuild/cssCandidateCache.test.ts +0 -6
- package/frontendBuild/cssCandidateCache.ts +6 -41
- package/frontendBuild/cssCompiler.ts +31 -82
- package/frontendBuild/cssImportResolver.ts +6 -19
- package/frontendBuild/devChangePlanner.ts +7 -10
- package/frontendBuild/devGeneratedIndexSync.test.ts +12 -27
- package/frontendBuild/devGeneratedIndexSync.ts +1 -4
- package/frontendBuild/devRegistryBundler.ts +192 -0
- package/frontendBuild/fontOptimizer.test.ts +6 -14
- package/frontendBuild/fontOptimizer.ts +15 -72
- package/frontendBuild/fontPruner.test.ts +5 -10
- package/frontendBuild/fontPruner.ts +5 -20
- package/frontendBuild/frontendBuild.test.ts +57 -70
- package/frontendBuild/hmrChangeClassifier.ts +2 -2
- package/frontendBuild/hmrWatcher.test.ts +59 -32
- package/frontendBuild/hmrWatcher.ts +31 -74
- package/frontendBuild/index.ts +4 -0
- package/frontendBuild/pagesBundleBuilder.ts +28 -30
- package/frontendBuild/pagesEntrySourceGenerator.ts +56 -6
- package/frontendBuild/precompressArtifacts.ts +5 -18
- package/frontendBuild/registryMetadataFingerprint.test.ts +63 -0
- package/frontendBuild/registryMetadataFingerprint.ts +28 -0
- package/frontendBuild/routeClientBuilder.ts +100 -62
- package/frontendBuild/routesManifestArtifactSerializer.ts +5 -36
- package/frontendBuild/serverGraphFile.test.ts +77 -0
- package/frontendBuild/serverGraphFile.ts +93 -0
- package/frontendBuild/sourceMtimeIndex.test.ts +41 -97
- package/frontendBuild/sourceMtimeIndex.ts +19 -130
- package/frontendBuild/ssrBaseArtifactBuilder.ts +19 -13
- package/frontendBuild/ssrDevBundler.ts +85 -0
- package/frontendBuild/styleContract.ts +1 -8
- package/frontendBuild/themeValidator.ts +4 -26
- package/frontendBuild/watchRootResolver.test.ts +23 -42
- package/frontendBuild/watchRootResolver.ts +5 -27
- package/getRelatedCnsts.ts +12 -92
- package/incrementalBuilder/buildBatch.proc.ts +147 -63
- package/incrementalBuilder/buildBatchProtocol.ts +13 -38
- package/incrementalBuilder/buildBatchRunner.test.ts +65 -0
- package/incrementalBuilder/buildBatchRunner.ts +26 -38
- package/incrementalBuilder/builderChannel.test.ts +24 -49
- package/incrementalBuilder/builderChannel.ts +6 -37
- package/incrementalBuilder/builderRequestRouter.test.ts +0 -8
- package/incrementalBuilder/builderRequestRouter.ts +5 -24
- package/incrementalBuilder/builderWorkQueue.test.ts +94 -0
- package/incrementalBuilder/builderWorkQueue.ts +126 -0
- package/incrementalBuilder/devWatchBatch.test.ts +21 -24
- package/incrementalBuilder/incrementalBuilder.host.test.ts +53 -56
- package/incrementalBuilder/incrementalBuilder.host.ts +68 -81
- package/incrementalBuilder/incrementalBuilder.proc.test.ts +270 -0
- package/incrementalBuilder/incrementalBuilder.proc.ts +596 -208
- package/index.ts +5 -19
- package/integration/devResourceProbe.ts +50 -56
- package/integration/devStability.integration.test.ts +105 -199
- package/integration/devStabilityHarness.test.ts +13 -28
- package/integration/devStabilityHarness.ts +65 -330
- package/integration/ssrMemoryProbe.ts +41 -167
- package/libSource.test.ts +4 -15
- package/libSource.ts +19 -31
- package/lint/__fixtures__/no-document-cookie/bad.tsx +4 -0
- package/lint/__fixtures__/no-document-cookie/good.tsx +7 -0
- package/lint/__fixtures__/no-web-only-api-outside-webkit/bad.tsx +7 -0
- package/lint/__fixtures__/no-web-only-api-outside-webkit/good.tsx +11 -0
- package/lint/__fixtures__/no-web-storage/bad.tsx +6 -0
- package/lint/__fixtures__/no-web-storage/good.tsx +7 -0
- package/lint/gritRules.test.ts +8 -24
- package/lint/no-deep-internal-import.grit +2 -2
- package/lint/no-document-cookie.grit +29 -0
- package/lint/no-redeclare-predefined-endpoint.grit +2 -1
- package/lint/no-web-only-api-outside-webkit.grit +44 -0
- package/lint/no-web-storage.grit +30 -0
- package/linter.test.ts +1 -2
- package/linter.ts +26 -116
- package/mobile/desktopBin.test.ts +182 -0
- package/mobile/desktopBin.ts +144 -0
- package/mobile/desktopServerStage.test.ts +252 -0
- package/mobile/desktopServerStage.ts +178 -0
- package/mobile/index.ts +9 -0
- package/mobile/mobileTarget.test.ts +57 -8
- package/mobile/mobileTarget.ts +45 -26
- package/mobile/nativeApi.test.ts +30 -0
- package/mobile/nativeApi.ts +40 -0
- package/mobile/nativeApp.test.ts +184 -0
- package/mobile/nativeApp.ts +454 -0
- package/mobile/nativeAppLine.test.ts +69 -0
- package/mobile/nativeAppLine.ts +71 -0
- package/mobile/nativeConfig.test.ts +438 -0
- package/mobile/nativeConfig.ts +294 -0
- package/mobile/nativePluginFolders.test.ts +81 -0
- package/mobile/nativePluginFolders.ts +60 -0
- package/mobile/nativeWebDir.test.ts +80 -0
- package/mobile/nativeWebDir.ts +66 -0
- package/mobile/placeholderAppId.test.ts +13 -0
- package/mobile/placeholderAppId.ts +16 -0
- package/mobile/usageDescriptions.test.ts +27 -0
- package/mobile/usageDescriptions.ts +24 -0
- package/package.json +11 -11
- package/packageExports.test.ts +3 -18
- package/packageExportsMap.ts +3 -19
- package/piDependencyPins.test.ts +46 -0
- package/platformTest/BunTestReport.test.ts +60 -0
- package/platformTest/BunTestReport.ts +65 -0
- package/platformTest/IdleWatchedProcess.test.ts +56 -0
- package/platformTest/IdleWatchedProcess.ts +73 -0
- package/platformTest/LinuxDockerTarget.ts +105 -0
- package/platformTest/PlatformTestRun.test.ts +64 -0
- package/platformTest/PlatformTestRun.ts +141 -0
- package/platformTest/PlatformTestTarget.ts +142 -0
- package/platformTest/TestSnapshot.test.ts +72 -0
- package/platformTest/TestSnapshot.ts +103 -0
- package/platformTest/WindowsSshTarget.ts +151 -0
- package/prompter.ts +3 -13
- package/qualityScanner.test.ts +9 -26
- package/qualityScanner.ts +200 -286
- package/recipeScanner.test.ts +7 -12
- package/recipeScanner.ts +9 -38
- package/repoIdentity.ts +2 -9
- package/routeSourceValidator.test.ts +0 -1
- package/routeSourceValidator.ts +7 -42
- package/scanInfo.ts +91 -225
- package/selectModel.ts +1 -2
- package/semver.ts +3 -5
- package/slicePlanner.test.ts +6 -24
- package/slicePlanner.ts +11 -32
- package/spinner.test.ts +3 -8
- package/spinner.ts +4 -16
- package/sshTunnel.ts +1 -6
- package/ssrScanner.test.ts +1 -6
- package/ssrScanner.ts +103 -113
- package/stripAnsi.ts +3 -0
- package/subspace.test.ts +9 -25
- package/subspace.ts +48 -115
- package/subspaceConfig.ts +3 -11
- package/testHelpers.ts +194 -0
- package/transforms/asyncDefaultExportDetector.ts +3 -11
- package/transforms/barrelAnalyzer.ts +43 -135
- package/transforms/barrelImportsPlugin.ts +38 -175
- package/transforms/externalizeFrameworkPlugin.test.ts +39 -118
- package/transforms/externalizeFrameworkPlugin.ts +13 -87
- package/transforms/moduleSyntax.ts +32 -0
- package/transforms/rscUseClientTransform.ts +42 -20
- package/transforms/serverEnvPlugin.test.ts +103 -0
- package/transforms/serverEnvPlugin.ts +41 -0
- package/transforms/transforms.test.ts +166 -44
- package/transforms/tsconfigPackageResolver.test.ts +6 -24
- package/transforms/useClientBundlePlugin.ts +6 -23
- package/typeChecker.ts +7 -63
- package/types.ts +2 -0
- package/uploadRelease.ts +10 -25
- package/workflow/artifacts.ts +18 -0
- package/workflow/executor.test.ts +1 -12
- package/workflow/executor.ts +101 -110
- package/workflow/index.ts +0 -1
- package/workflow/moduleIndex.test.ts +4 -15
- package/workflow/moduleIndex.ts +9 -49
- package/workflow/plan.ts +18 -25
- package/workflow/render.ts +80 -96
- package/workflow/rolloutGate.test.ts +1 -1
- package/workflow/source.ts +52 -69
- package/workflow/sourceAst.ts +50 -0
- package/workspaceLayout.test.ts +18 -3
- package/workspaceLayout.ts +22 -23
- package/abstractCompactor.ts +0 -112
- package/aiEditor.test.ts +0 -60
- package/aiEditor.ts +0 -387
- package/applicationReleasePackager.ts +0 -209
- package/capacitorApp.test.ts +0 -372
- package/capacitorApp.ts +0 -1648
- package/commandDecorators/commandMeta.ts +0 -7
- package/commandDecorators/types.ts +0 -10
- package/getDirname.ts +0 -4
- package/guideline.ts +0 -19
- package/local/optimize-resource/ipcprobe/child.ts +0 -1
- package/local/optimize-resource/ipcprobe/parent.ts +0 -11
- package/workflow/primitive.ts +0 -59
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import type { ExtensionAPI, InlineExtension } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { Type } from "typebox";
|
|
3
|
+
import type { Workspace } from "../../commandDecorators";
|
|
4
|
+
import { AkanEditScope } from "./AkanEditScope";
|
|
5
|
+
import { AkanVerifier } from "./AkanVerifier";
|
|
6
|
+
|
|
7
|
+
export interface AkanToolPackOptions {
|
|
8
|
+
workspace: Workspace;
|
|
9
|
+
cwd: string;
|
|
10
|
+
/** `readonly` for a reviewer, `apply` for an agent that may run a workflow. */
|
|
11
|
+
mode: "readonly" | "plan" | "apply";
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// Wraps the akan MCP catalogue's in-process dispatcher (ContextRunner.callMcpTool); a second copy would drift from it.
|
|
15
|
+
export class AkanToolPack {
|
|
16
|
+
readonly #options: AkanToolPackOptions;
|
|
17
|
+
|
|
18
|
+
constructor(options: AkanToolPackOptions) {
|
|
19
|
+
this.#options = options;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
extension(): InlineExtension {
|
|
23
|
+
return { name: "akan-tools", factory: async (pi) => await this.#register(pi) };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// The session allowlist covers extension tools too; a name missing here tells the model the tool does not exist.
|
|
27
|
+
// devkit must not depend on the CLI, so ContextRunner is reached by relative path; publishing bundles both.
|
|
28
|
+
async names() {
|
|
29
|
+
const { ContextRunner } = await import("../../../cli/context/context.runner");
|
|
30
|
+
return [
|
|
31
|
+
...new ContextRunner().listMcpTools(this.#options.mode).map((tool) => tool.name),
|
|
32
|
+
AkanToolPack.verifyToolName,
|
|
33
|
+
];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
static readonly verifyToolName = "akan_verify";
|
|
37
|
+
|
|
38
|
+
async #register(pi: ExtensionAPI) {
|
|
39
|
+
const { ContextRunner } = await import("../../../cli/context/context.runner");
|
|
40
|
+
const runner = new ContextRunner();
|
|
41
|
+
const guidelineNames = await AkanToolPack.#guidelineNames();
|
|
42
|
+
for (const tool of runner.listMcpTools(this.#options.mode, { guidelineNames })) {
|
|
43
|
+
const description = tool.description ?? tool.name;
|
|
44
|
+
pi.registerTool({
|
|
45
|
+
name: tool.name,
|
|
46
|
+
label: tool.name,
|
|
47
|
+
description,
|
|
48
|
+
// Without a snippet the engine leaves a custom tool out of the system prompt's tool list entirely.
|
|
49
|
+
promptSnippet: `${tool.name}: ${description.split(".")[0] ?? description}`,
|
|
50
|
+
parameters: Type.Unsafe<Record<string, unknown>>(tool.inputSchema),
|
|
51
|
+
execute: async (_id, params) => {
|
|
52
|
+
const result = await runner.callMcpTool(this.#options.workspace, tool.name, params ?? {}, {
|
|
53
|
+
mode: this.#options.mode,
|
|
54
|
+
});
|
|
55
|
+
const text = typeof result === "string" ? result : JSON.stringify(result, null, 2);
|
|
56
|
+
return { content: [{ type: "text", text }], details: undefined };
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
this.#registerVerify(pi);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
#registerVerify(pi: ExtensionAPI) {
|
|
64
|
+
const verifier = new AkanVerifier(this.#options.cwd);
|
|
65
|
+
pi.registerTool({
|
|
66
|
+
name: AkanToolPack.verifyToolName,
|
|
67
|
+
label: "akan verify",
|
|
68
|
+
description:
|
|
69
|
+
"Run the akan validation chain (sync, lint, typecheck, and quality ssr for .tsx changes) over everything the working tree has changed, and report what failed. Call this after editing before you finish.",
|
|
70
|
+
promptSnippet: "akan_verify: run sync/lint/typecheck/quality over the working tree's changes",
|
|
71
|
+
parameters: Type.Object({}),
|
|
72
|
+
execute: async () => {
|
|
73
|
+
const scope = await AkanEditScope.since(this.#options.cwd, new Set());
|
|
74
|
+
const report = await verifier.verify(scope);
|
|
75
|
+
return {
|
|
76
|
+
content: [{ type: "text", text: AkanVerifier.summarize(report) }],
|
|
77
|
+
details: undefined,
|
|
78
|
+
isError: !report.ok,
|
|
79
|
+
};
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
static async #guidelineNames() {
|
|
85
|
+
try {
|
|
86
|
+
const { Prompter } = await import("../../prompter");
|
|
87
|
+
return await Prompter.listGuidelines();
|
|
88
|
+
} catch {
|
|
89
|
+
return [];
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { AkanCli, type AkanCliResult } from "./AkanCli";
|
|
2
|
+
import type { AkanEditScopeResult } from "./AkanEditScope";
|
|
3
|
+
|
|
4
|
+
export interface AkanVerifyReport {
|
|
5
|
+
ok: boolean;
|
|
6
|
+
steps: AkanCliResult[];
|
|
7
|
+
scope: AkanEditScopeResult;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export class AkanVerifier {
|
|
11
|
+
readonly #cli: AkanCli;
|
|
12
|
+
|
|
13
|
+
constructor(cwd: string) {
|
|
14
|
+
this.#cli = new AkanCli(cwd);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async verify(scope: AkanEditScopeResult): Promise<AkanVerifyReport> {
|
|
18
|
+
const steps: AkanCliResult[] = [];
|
|
19
|
+
const targets = [...scope.apps, ...scope.libs];
|
|
20
|
+
// Order matters: a stale barrel reads to typecheck as a missing symbol, and lint moves the positions it reports.
|
|
21
|
+
if (scope.needsSync) for (const target of targets) steps.push(await this.#cli.run(["sync", target]));
|
|
22
|
+
for (const target of targets) steps.push(await this.#cli.run(["lint", target]));
|
|
23
|
+
for (const app of scope.apps) steps.push(await this.#cli.run(["typecheck", app]));
|
|
24
|
+
if (scope.touchesTsx) steps.push(await this.#cli.run(["quality", "ssr"]));
|
|
25
|
+
return { ok: steps.every((step) => step.ok), steps, scope };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** What a model needs to act: which step failed and its tail — never the whole output, which rides every later turn. */
|
|
29
|
+
static summarize(report: AkanVerifyReport, { tailLines = 40 }: { tailLines?: number } = {}) {
|
|
30
|
+
if (!report.steps.length) return "No akan target was touched, so there was nothing to verify.";
|
|
31
|
+
if (report.ok) return `Verification passed: ${report.steps.map((step) => step.command).join(", ")}.`;
|
|
32
|
+
const failed = report.steps.filter((step) => !step.ok);
|
|
33
|
+
const passed = report.steps.filter((step) => step.ok).map((step) => step.command);
|
|
34
|
+
const blocks = failed.map((step) => `### ${step.command} (exit ${step.exitCode})\n${tail(step.output, tailLines)}`);
|
|
35
|
+
return [
|
|
36
|
+
`Verification failed: ${failed.map((step) => step.command).join(", ")}.`,
|
|
37
|
+
passed.length ? `Passed: ${passed.join(", ")}.` : "",
|
|
38
|
+
...blocks,
|
|
39
|
+
]
|
|
40
|
+
.filter(Boolean)
|
|
41
|
+
.join("\n\n");
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const tail = (text: string, lines: number) => {
|
|
46
|
+
const all = text.split("\n");
|
|
47
|
+
return all.length <= lines ? text : `…\n${all.slice(-lines).join("\n")}`;
|
|
48
|
+
};
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import type { ExtensionAPI, InlineExtension } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import type { CodeAgentProfile, CodeAgentQuestion } from "akanjs/common";
|
|
3
|
+
import { Type } from "typebox";
|
|
4
|
+
|
|
5
|
+
export interface AskToolPackOptions {
|
|
6
|
+
profile: CodeAgentProfile;
|
|
7
|
+
/** Puts one question on the wire and resolves with the labels chosen, or nothing when it was skipped. */
|
|
8
|
+
ask: (question: Omit<CodeAgentQuestion, "questionId">) => Promise<string | undefined>;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// One question at a time even when several are sent: the host draws one block, and an answer can change the next.
|
|
12
|
+
export class AskToolPack {
|
|
13
|
+
static readonly toolName = "ask_user";
|
|
14
|
+
/** Four is what fits on a screen without the transcript disappearing behind the questions. */
|
|
15
|
+
static readonly maxQuestions = 4;
|
|
16
|
+
|
|
17
|
+
readonly #options: AskToolPackOptions;
|
|
18
|
+
|
|
19
|
+
constructor(options: AskToolPackOptions) {
|
|
20
|
+
this.#options = options;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
names() {
|
|
24
|
+
return this.#options.profile.ui.canPrompt ? [AskToolPack.toolName] : [];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
extension(): InlineExtension | undefined {
|
|
28
|
+
if (!this.#options.profile.ui.canPrompt) return undefined;
|
|
29
|
+
return { name: "akan-ask", factory: (pi: ExtensionAPI) => this.#register(pi) };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
#register(pi: ExtensionAPI) {
|
|
33
|
+
pi.registerTool({
|
|
34
|
+
name: AskToolPack.toolName,
|
|
35
|
+
label: "Ask",
|
|
36
|
+
description:
|
|
37
|
+
"Ask the user to decide something you cannot decide from the code or the request. Use it when two readings of the task would lead to materially different work, not for choices with an obvious default — and never to ask permission for work you were already asked to do. Offer concrete options whenever you can name them; mark one recommended when you have a view.",
|
|
38
|
+
promptSnippet: "ask_user: put a decision the user owns back to them, with options",
|
|
39
|
+
parameters: Type.Object({
|
|
40
|
+
questions: Type.Array(
|
|
41
|
+
Type.Object({
|
|
42
|
+
question: Type.String({ description: "The whole question, as one sentence ending in a question mark." }),
|
|
43
|
+
header: Type.Optional(
|
|
44
|
+
Type.String({ description: "Two or three words naming what is being chosen, e.g. 'Auth method'." }),
|
|
45
|
+
),
|
|
46
|
+
options: Type.Optional(
|
|
47
|
+
Type.Array(
|
|
48
|
+
Type.Object({
|
|
49
|
+
label: Type.String({ description: "The choice, in one to five words." }),
|
|
50
|
+
detail: Type.Optional(
|
|
51
|
+
Type.String({ description: "What picking it means, or the trade-off it carries." }),
|
|
52
|
+
),
|
|
53
|
+
recommended: Type.Optional(
|
|
54
|
+
Type.Boolean({ description: "Mark at most one. It is offered first and labelled as such." }),
|
|
55
|
+
),
|
|
56
|
+
}),
|
|
57
|
+
{ description: "Two to four distinct choices. Leave it out to ask for an answer in prose." },
|
|
58
|
+
),
|
|
59
|
+
),
|
|
60
|
+
multiSelect: Type.Optional(
|
|
61
|
+
Type.Boolean({ description: "Several options may be chosen together, rather than exactly one." }),
|
|
62
|
+
),
|
|
63
|
+
freeText: Type.Optional(
|
|
64
|
+
Type.Boolean({
|
|
65
|
+
description: "Offer a further choice that lets the user type an answer of their own. Default true.",
|
|
66
|
+
}),
|
|
67
|
+
),
|
|
68
|
+
}),
|
|
69
|
+
{ description: `One to ${AskToolPack.maxQuestions} questions, asked in order.` },
|
|
70
|
+
),
|
|
71
|
+
}),
|
|
72
|
+
execute: async (_id, params) => {
|
|
73
|
+
const questions = (params.questions ?? []).slice(0, AskToolPack.maxQuestions);
|
|
74
|
+
if (!questions.length)
|
|
75
|
+
return { content: [{ type: "text", text: "No question was asked." }], details: undefined, isError: true };
|
|
76
|
+
const answers: string[] = [];
|
|
77
|
+
for (const entry of questions) {
|
|
78
|
+
const options = (entry.options ?? []).map((option, at) => ({
|
|
79
|
+
key: `${at}`,
|
|
80
|
+
label: option.label,
|
|
81
|
+
...(option.detail ? { detail: option.detail } : {}),
|
|
82
|
+
...(option.recommended ? { recommended: true } : {}),
|
|
83
|
+
}));
|
|
84
|
+
const answer = await this.#options.ask({
|
|
85
|
+
prompt: entry.header ? `${entry.header} — ${entry.question}` : entry.question,
|
|
86
|
+
kind: options.length ? "select" : "text",
|
|
87
|
+
...(options.length ? { options } : {}),
|
|
88
|
+
...(entry.multiSelect ? { multiSelect: true } : {}),
|
|
89
|
+
// On by default: the model's options are a guess, and only the person knows when none of them fits.
|
|
90
|
+
freeText: entry.freeText ?? true,
|
|
91
|
+
});
|
|
92
|
+
// Spelled out as a skip: a blank answer reads to a model as "the user wanted nothing".
|
|
93
|
+
answers.push(`${entry.question}\n→ ${answer ?? "(skipped — decide this yourself and say what you chose)"}`);
|
|
94
|
+
}
|
|
95
|
+
return { content: [{ type: "text", text: answers.join("\n\n") }], details: undefined };
|
|
96
|
+
},
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import type { ExtensionAPI, InlineExtension } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import type { CodeAgentProfile } from "akanjs/common";
|
|
3
|
+
import { Type } from "typebox";
|
|
4
|
+
import type { CodeMailbox, CodePeer } from "../agent/CodeMailbox";
|
|
5
|
+
|
|
6
|
+
export interface MailToolPackOptions {
|
|
7
|
+
profile: CodeAgentProfile;
|
|
8
|
+
/** Late, because the mailbox only exists once the session it announces has one. */
|
|
9
|
+
mailbox: () => CodeMailbox | undefined;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export class MailToolPack {
|
|
13
|
+
static readonly toolNames = ["list_peers", "send_peer_message"];
|
|
14
|
+
|
|
15
|
+
readonly #options: MailToolPackOptions;
|
|
16
|
+
|
|
17
|
+
constructor(options: MailToolPackOptions) {
|
|
18
|
+
this.#options = options;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
names() {
|
|
22
|
+
return this.#options.profile.session.store === "file" ? MailToolPack.toolNames : [];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
extension(): InlineExtension | undefined {
|
|
26
|
+
if (!this.names().length) return undefined;
|
|
27
|
+
return { name: "akan-mail", factory: (pi: ExtensionAPI) => this.#register(pi) };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
#register(pi: ExtensionAPI) {
|
|
31
|
+
pi.registerTool({
|
|
32
|
+
name: "list_peers",
|
|
33
|
+
label: "List peer sessions",
|
|
34
|
+
description:
|
|
35
|
+
"List the other akan code sessions running in this workspace right now, with their name and working directory. Call it before send_peer_message so you name a session that is actually there.",
|
|
36
|
+
promptSnippet: "list_peers: the other akan code sessions running here",
|
|
37
|
+
parameters: Type.Object({}),
|
|
38
|
+
execute: async () => ({
|
|
39
|
+
content: [{ type: "text" as const, text: MailToolPack.#render(this.#options.mailbox()?.peers() ?? []) }],
|
|
40
|
+
details: undefined,
|
|
41
|
+
}),
|
|
42
|
+
});
|
|
43
|
+
pi.registerTool({
|
|
44
|
+
name: "send_peer_message",
|
|
45
|
+
label: "Message a peer session",
|
|
46
|
+
description:
|
|
47
|
+
"Hand a message to another akan code session in this workspace. It arrives there as a prompt, so it costs that session a turn — use it to hand over a piece of work or to warn it about a change you made, not to chat. Say what you want done and what you already did.",
|
|
48
|
+
promptSnippet: "send_peer_message: hand work to another akan code session",
|
|
49
|
+
parameters: Type.Object({
|
|
50
|
+
to: Type.String({ description: "The peer's name, or the front of its id, from list_peers." }),
|
|
51
|
+
message: Type.String({ description: "What that session should know or do." }),
|
|
52
|
+
}),
|
|
53
|
+
execute: async (_id, params) => {
|
|
54
|
+
const mailbox = this.#options.mailbox();
|
|
55
|
+
const peer = mailbox?.find(params.to);
|
|
56
|
+
if (!mailbox || !peer)
|
|
57
|
+
return {
|
|
58
|
+
content: [{ type: "text" as const, text: `No live session matches "${params.to}". Call list_peers.` }],
|
|
59
|
+
details: undefined,
|
|
60
|
+
isError: true,
|
|
61
|
+
};
|
|
62
|
+
try {
|
|
63
|
+
mailbox.send(peer.id, params.message);
|
|
64
|
+
return { content: [{ type: "text" as const, text: `Sent to ${peer.name}.` }], details: undefined };
|
|
65
|
+
} catch (error: unknown) {
|
|
66
|
+
return { content: [{ type: "text" as const, text: String(error) }], details: undefined, isError: true };
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
static #render(peers: CodePeer[]) {
|
|
73
|
+
if (!peers.length) return "No other akan code session is running in this workspace.";
|
|
74
|
+
return peers.map((peer) => `${peer.name} (${peer.id.slice(0, 8)}) — ${peer.cwd}`).join("\n");
|
|
75
|
+
}
|
|
76
|
+
}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import type { CodeAgentMcpServerRef } from "akanjs/common";
|
|
2
|
+
|
|
3
|
+
export interface McpToolInfo {
|
|
4
|
+
name: string;
|
|
5
|
+
description?: string;
|
|
6
|
+
inputSchema: { type: "object"; properties: Record<string, unknown>; required?: string[] };
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
interface JsonRpcResponse {
|
|
10
|
+
id?: number;
|
|
11
|
+
result?: { tools?: McpToolInfo[]; nextCursor?: string; content?: unknown; isError?: boolean };
|
|
12
|
+
error?: { code: number; message: string };
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const protocolVersion = "2025-06-18";
|
|
16
|
+
|
|
17
|
+
// Its own class: a 401 is fixed by signing in, and calling it "unavailable" sends someone to debug a working server.
|
|
18
|
+
export class McpUnauthorized extends Error {
|
|
19
|
+
/** The `WWW-Authenticate` value, which names the resource metadata discovery starts from. */
|
|
20
|
+
readonly challenge: string;
|
|
21
|
+
|
|
22
|
+
constructor(name: string, challenge: string) {
|
|
23
|
+
super(`MCP server "${name}" requires authentication`);
|
|
24
|
+
this.name = "McpUnauthorized";
|
|
25
|
+
this.challenge = challenge;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export class McpClient {
|
|
30
|
+
/** A ceiling on a server that never stops paging; 50 pages is 5,000 tools at the usual page size. */
|
|
31
|
+
static readonly maxToolPages = 50;
|
|
32
|
+
|
|
33
|
+
readonly ref: CodeAgentMcpServerRef;
|
|
34
|
+
#proc: Bun.Subprocess<"pipe", "pipe", "ignore"> | undefined;
|
|
35
|
+
#nextId = 0;
|
|
36
|
+
#buffer = "";
|
|
37
|
+
readonly #pending = new Map<number, (response: JsonRpcResponse) => void>();
|
|
38
|
+
#sessionId: string | undefined;
|
|
39
|
+
readonly #token: string | undefined;
|
|
40
|
+
|
|
41
|
+
constructor(ref: CodeAgentMcpServerRef, token?: string) {
|
|
42
|
+
this.ref = ref;
|
|
43
|
+
this.#token = token;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async connect() {
|
|
47
|
+
if (this.ref.transport === "stdio") this.#spawn();
|
|
48
|
+
const initialize = await this.#call("initialize", {
|
|
49
|
+
protocolVersion,
|
|
50
|
+
capabilities: {},
|
|
51
|
+
clientInfo: { name: "akan-code", version: "1" },
|
|
52
|
+
});
|
|
53
|
+
if (initialize.error) throw new Error(initialize.error.message);
|
|
54
|
+
await this.#notify("notifications/initialized");
|
|
55
|
+
return this;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Every page: `tools/list` pages by `nextCursor` (an akan server at 100), and one page silently drops the tail. */
|
|
59
|
+
async listTools(): Promise<McpToolInfo[]> {
|
|
60
|
+
const tools: McpToolInfo[] = [];
|
|
61
|
+
let cursor: string | undefined;
|
|
62
|
+
for (let page = 0; page < McpClient.maxToolPages; page += 1) {
|
|
63
|
+
const response = await this.#call("tools/list", cursor ? { cursor } : {});
|
|
64
|
+
if (response.error) throw new Error(response.error.message);
|
|
65
|
+
tools.push(...(response.result?.tools ?? []));
|
|
66
|
+
const next = response.result?.nextCursor;
|
|
67
|
+
// Handing back the cursor it was given is a server that would page forever; so is one that cycles.
|
|
68
|
+
if (!next || next === cursor) return tools;
|
|
69
|
+
cursor = next;
|
|
70
|
+
}
|
|
71
|
+
return tools;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async callTool(name: string, args: Record<string, unknown>) {
|
|
75
|
+
const response = await this.#call("tools/call", { name, arguments: args });
|
|
76
|
+
if (response.error) return { text: response.error.message, isError: true };
|
|
77
|
+
return { text: McpClient.#renderContent(response.result?.content), isError: !!response.result?.isError };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
close() {
|
|
81
|
+
this.#proc?.kill();
|
|
82
|
+
this.#proc = undefined;
|
|
83
|
+
for (const resolve of this.#pending.values()) resolve({ error: { code: -1, message: "MCP server closed" } });
|
|
84
|
+
this.#pending.clear();
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
#spawn() {
|
|
88
|
+
if (!this.ref.command) throw new Error(`MCP server "${this.ref.name}" declares no command`);
|
|
89
|
+
this.#proc = Bun.spawn([this.ref.command, ...(this.ref.args ?? [])], {
|
|
90
|
+
stdin: "pipe",
|
|
91
|
+
stdout: "pipe",
|
|
92
|
+
stderr: "ignore",
|
|
93
|
+
env: { ...process.env, ...this.ref.env },
|
|
94
|
+
});
|
|
95
|
+
void this.#readStdout();
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async #readStdout() {
|
|
99
|
+
const decoder = new TextDecoder();
|
|
100
|
+
for await (const chunk of this.#proc?.stdout ?? []) {
|
|
101
|
+
this.#buffer += decoder.decode(chunk as Uint8Array, { stream: true });
|
|
102
|
+
let index = this.#buffer.indexOf("\n");
|
|
103
|
+
while (index >= 0) {
|
|
104
|
+
const line = this.#buffer.slice(0, index).trim();
|
|
105
|
+
this.#buffer = this.#buffer.slice(index + 1);
|
|
106
|
+
if (line) this.#receive(line);
|
|
107
|
+
index = this.#buffer.indexOf("\n");
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
#receive(line: string) {
|
|
113
|
+
try {
|
|
114
|
+
const message = JSON.parse(line) as JsonRpcResponse;
|
|
115
|
+
if (typeof message.id !== "number") return;
|
|
116
|
+
this.#pending.get(message.id)?.(message);
|
|
117
|
+
this.#pending.delete(message.id);
|
|
118
|
+
} catch {
|
|
119
|
+
// A server that writes anything but JSON-RPC on stdout is misbehaving; dropping the line keeps us alive.
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async #call(method: string, params: unknown): Promise<JsonRpcResponse> {
|
|
124
|
+
this.#nextId += 1;
|
|
125
|
+
const id = this.#nextId;
|
|
126
|
+
const request = { jsonrpc: "2.0", id, method, params };
|
|
127
|
+
if (this.ref.transport === "http") return await this.#post(request);
|
|
128
|
+
this.#proc?.stdin.write(`${JSON.stringify(request)}\n`);
|
|
129
|
+
return await new Promise<JsonRpcResponse>((resolve) => {
|
|
130
|
+
this.#pending.set(id, resolve);
|
|
131
|
+
setTimeout(() => {
|
|
132
|
+
if (!this.#pending.delete(id)) return;
|
|
133
|
+
resolve({ error: { code: -2, message: `MCP server "${this.ref.name}" timed out on ${method}` } });
|
|
134
|
+
}, 30_000);
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async #notify(method: string) {
|
|
139
|
+
const request = { jsonrpc: "2.0", method };
|
|
140
|
+
if (this.ref.transport === "http") await this.#post(request);
|
|
141
|
+
else this.#proc?.stdin.write(`${JSON.stringify(request)}\n`);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async #post(body: unknown): Promise<JsonRpcResponse> {
|
|
145
|
+
if (!this.ref.url) throw new Error(`MCP server "${this.ref.name}" declares no url`);
|
|
146
|
+
const response = await fetch(this.ref.url, {
|
|
147
|
+
method: "POST",
|
|
148
|
+
headers: {
|
|
149
|
+
"content-type": "application/json",
|
|
150
|
+
// Streamable HTTP lets a server answer either way; a client that omits the SSE type gets a 406.
|
|
151
|
+
accept: "application/json, text/event-stream",
|
|
152
|
+
"mcp-protocol-version": protocolVersion,
|
|
153
|
+
...this.ref.headers,
|
|
154
|
+
...(this.#token ? { authorization: `Bearer ${this.#token}` } : {}),
|
|
155
|
+
...(this.#sessionId ? { "mcp-session-id": this.#sessionId } : {}),
|
|
156
|
+
},
|
|
157
|
+
body: JSON.stringify(body),
|
|
158
|
+
signal: AbortSignal.timeout(30_000),
|
|
159
|
+
});
|
|
160
|
+
// Thrown, not returned as a JSON-RPC error: a 401's body says nothing, and the caller needs its header.
|
|
161
|
+
if (response.status === 401)
|
|
162
|
+
throw new McpUnauthorized(this.ref.name, response.headers.get("www-authenticate") ?? "");
|
|
163
|
+
this.#sessionId = response.headers.get("mcp-session-id") ?? this.#sessionId;
|
|
164
|
+
const text = await response.text();
|
|
165
|
+
if (!text.trim()) return {};
|
|
166
|
+
// An SSE answer to a single request carries one `data:` line holding the whole JSON-RPC response.
|
|
167
|
+
const payload = text.startsWith("event:") || text.startsWith("data:") ? McpClient.#firstSseData(text) : text;
|
|
168
|
+
return payload ? (JSON.parse(payload) as JsonRpcResponse) : {};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
static #firstSseData(text: string) {
|
|
172
|
+
return text
|
|
173
|
+
.split("\n")
|
|
174
|
+
.find((line) => line.startsWith("data:"))
|
|
175
|
+
?.slice(5)
|
|
176
|
+
.trim();
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
static #renderContent(content: unknown) {
|
|
180
|
+
if (!Array.isArray(content)) return typeof content === "string" ? content : JSON.stringify(content ?? null);
|
|
181
|
+
return content
|
|
182
|
+
.map((part: { type?: string; text?: string }) =>
|
|
183
|
+
part.type === "text" ? (part.text ?? "") : JSON.stringify(part),
|
|
184
|
+
)
|
|
185
|
+
.join("\n");
|
|
186
|
+
}
|
|
187
|
+
}
|