@akanjs/devkit 3.0.0-beta.19 → 3.0.0-beta.20
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/abstractDoc.test.ts +6 -15
- package/abstractDoc.ts +1 -4
- 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 +1 -3
- package/akanApp/akanApp.host.ts +64 -250
- package/akanApp/devHostPolicy.test.ts +10 -34
- package/akanApp/devHostPolicy.ts +12 -98
- package/akanConfig/akanConfig.test.ts +30 -39
- package/akanConfig/akanConfig.ts +40 -136
- package/akanContext.ts +106 -159
- package/akanMcpContract.ts +28 -73
- package/appSelectionMemory.ts +1 -4
- package/applicationBuildReporter.ts +37 -5
- package/applicationBuildRunner.ts +25 -59
- package/applicationReleasePackager.ts +24 -44
- package/applicationTestPreload.ts +3 -12
- package/artifact/implicitRootLayout.test.ts +3 -19
- package/artifact/implicitRootLayout.ts +34 -91
- package/artifact/routeSeedIndex.ts +11 -44
- package/biomeBase.ts +2 -5
- package/capacitorApp.test.ts +4 -14
- package/capacitorApp.ts +86 -235
- package/cloud/cloudApi.test.ts +202 -0
- package/cloud/cloudApi.ts +33 -57
- package/cloud/configLock.test.ts +64 -0
- package/cloud/configLock.ts +96 -0
- package/cloud/constants.ts +3 -2
- package/cloud/globalConfig.ts +64 -31
- package/codeAgent/agent/AkanCodeServices.ts +15 -57
- package/codeAgent/agent/AkanContextFiles.ts +5 -28
- package/codeAgent/agent/CodeAgent.ts +33 -108
- package/codeAgent/agent/CodeAgentAsks.ts +7 -30
- package/codeAgent/agent/CodeAgentEventMapper.ts +9 -52
- package/codeAgent/agent/CodeAgentGate.ts +8 -34
- package/codeAgent/agent/CodeAgentProxy.ts +4 -13
- package/codeAgent/agent/CodeAgentRpcHost.ts +3 -16
- package/codeAgent/agent/CodeAgentRpcListener.ts +2 -10
- package/codeAgent/agent/CodeAgentStreamPrinter.ts +4 -15
- package/codeAgent/agent/CodeAgentSuspended.ts +1 -8
- package/codeAgent/agent/CodeAgentUi.ts +2 -7
- package/codeAgent/agent/CodeMailbox.ts +7 -34
- package/codeAgent/agent/CodeSessionFork.ts +4 -19
- package/codeAgent/agent/CodeSessionIndex.ts +5 -31
- package/codeAgent/agent/SubagentPool.ts +48 -58
- package/codeAgent/agent/akanCodeModel.ts +5 -39
- package/codeAgent/agent/akanCodePaths.ts +4 -24
- package/codeAgent/agent/akanContextFiles.test.ts +0 -2
- package/codeAgent/agent/akanSystemPrompt.ts +1 -9
- package/codeAgent/agent/codeAgent.test.ts +178 -0
- package/codeAgent/agent/codeAgentContract.test.ts +5 -28
- package/codeAgent/agent/codeMailbox.test.ts +0 -4
- package/codeAgent/agent/codeSessionFork.test.ts +0 -2
- package/codeAgent/agent/codeSessionIndex.test.ts +9 -4
- package/codeAgent/agent/consoleToStderr.ts +2 -12
- package/codeAgent/agent/jsonLines.ts +29 -0
- package/codeAgent/agent/stringArg.ts +5 -0
- package/codeAgent/feedback/DevLogFeedback.ts +3 -18
- package/codeAgent/feedback/PreviewChrome.ts +5 -29
- package/codeAgent/feedback/PreviewFeedback.ts +2 -9
- package/codeAgent/feedback/PreviewView.ts +3 -15
- package/codeAgent/feedback/TurnFeedback.ts +2 -17
- package/codeAgent/feedback/VerifyFeedback.ts +1 -7
- package/codeAgent/feedback/previewChrome.test.ts +1 -1
- package/codeAgent/index.ts +2 -10
- package/codeAgent/skills/akan-code/SKILL.md +12 -4
- package/codeAgent/tools/AkanCli.ts +4 -16
- package/codeAgent/tools/AkanCodePlugins.ts +21 -43
- package/codeAgent/tools/AkanEditScope.ts +2 -12
- package/codeAgent/tools/AkanEnvKeys.ts +3 -11
- package/codeAgent/tools/AkanToolPack.ts +6 -28
- package/codeAgent/tools/AkanVerifier.ts +1 -8
- package/codeAgent/tools/AskToolPack.ts +3 -15
- package/codeAgent/tools/MailToolPack.ts +0 -9
- package/codeAgent/tools/McpClient.ts +3 -24
- package/codeAgent/tools/McpOAuth.ts +6 -41
- package/codeAgent/tools/McpOAuthLoopback.ts +2 -15
- package/codeAgent/tools/McpServerConfig.ts +7 -26
- package/codeAgent/tools/McpSignIn.ts +46 -49
- package/codeAgent/tools/McpTokenStore.ts +6 -17
- package/codeAgent/tools/McpToolPack.ts +107 -35
- package/codeAgent/tools/SessionToolPack.ts +1 -11
- package/codeAgent/tools/WebToolPack.ts +1 -7
- package/codeAgent/tools/akanCodePlugins.test.ts +231 -63
- package/codeAgent/tools/askToolPack.test.ts +0 -3
- package/codeAgent/tools/mcpOAuth.test.ts +315 -27
- package/codeAgent/tools/mcpServerConfig.test.ts +21 -14
- 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 +44 -102
- package/commandDecorators/commandBuilder.ts +30 -61
- package/commandDecorators/commandDecorators.test.ts +1 -9
- package/commandDecorators/dependencyBuilder.ts +10 -1
- package/commandDecorators/helpFormatter.ts +66 -144
- package/commandDecorators/index.ts +0 -2
- package/commandDecorators/targetMeta.ts +9 -7
- package/createTunnel.ts +7 -14
- package/dependencyScanner.test.ts +4 -16
- package/dependencyScanner.ts +27 -78
- package/devkitUtils.test.ts +47 -19
- package/executors.test.ts +114 -60
- package/executors.ts +165 -484
- package/fileEditor.ts +9 -37
- package/{getDirname.test.ts → fileSys.test.ts} +1 -1
- package/fileSys.ts +6 -8
- package/formSetterScanner.ts +3 -16
- package/frontendBuild/allRoutesBuilder.ts +7 -21
- package/frontendBuild/autoImportSync.test.ts +14 -38
- package/frontendBuild/autoImportSync.ts +15 -67
- package/frontendBuild/buildRouteClient.test.ts +10 -36
- package/frontendBuild/bundleDefine.ts +12 -0
- package/frontendBuild/clientBuildTypes.ts +5 -36
- package/frontendBuild/clientEntriesBundler.ts +9 -40
- package/frontendBuild/clientEntryDiscovery.ts +6 -41
- package/frontendBuild/csrArtifactBuilder.ts +6 -25
- package/frontendBuild/cssCandidateCache.test.ts +0 -6
- package/frontendBuild/cssCandidateCache.ts +6 -41
- package/frontendBuild/cssCompiler.ts +29 -78
- package/frontendBuild/cssImportResolver.ts +5 -18
- package/frontendBuild/devChangePlanner.ts +7 -10
- package/frontendBuild/devGeneratedIndexSync.test.ts +12 -27
- package/frontendBuild/devGeneratedIndexSync.ts +1 -4
- package/frontendBuild/fontOptimizer.test.ts +5 -13
- package/frontendBuild/fontOptimizer.ts +15 -72
- package/frontendBuild/fontPruner.test.ts +5 -10
- package/frontendBuild/fontPruner.ts +5 -20
- package/frontendBuild/frontendBuild.test.ts +27 -52
- package/frontendBuild/hmrChangeClassifier.ts +2 -2
- package/frontendBuild/hmrWatcher.test.ts +7 -31
- package/frontendBuild/hmrWatcher.ts +14 -71
- package/frontendBuild/pagesBundleBuilder.ts +4 -29
- package/frontendBuild/precompressArtifacts.ts +5 -18
- package/frontendBuild/routeClientBuilder.ts +7 -47
- package/frontendBuild/routesManifestArtifactSerializer.ts +5 -36
- package/frontendBuild/sourceMtimeIndex.test.ts +37 -94
- package/frontendBuild/sourceMtimeIndex.ts +19 -130
- package/frontendBuild/ssrBaseArtifactBuilder.ts +2 -8
- 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 +22 -57
- package/incrementalBuilder/buildBatchProtocol.ts +8 -36
- package/incrementalBuilder/buildBatchRunner.test.ts +34 -0
- package/incrementalBuilder/buildBatchRunner.ts +10 -31
- 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/devWatchBatch.test.ts +14 -18
- package/incrementalBuilder/incrementalBuilder.host.test.ts +18 -55
- package/incrementalBuilder/incrementalBuilder.host.ts +18 -76
- package/incrementalBuilder/incrementalBuilder.proc.test.ts +81 -0
- package/incrementalBuilder/incrementalBuilder.proc.ts +48 -161
- package/index.ts +3 -15
- package/integration/devResourceProbe.ts +50 -56
- package/integration/devStability.integration.test.ts +47 -194
- package/integration/devStabilityHarness.test.ts +9 -29
- 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/gritRules.test.ts +6 -23
- package/linter.test.ts +1 -2
- package/linter.ts +22 -115
- package/mobile/mobileTarget.ts +14 -13
- package/package.json +2 -2
- package/packageExports.test.ts +3 -18
- package/packageExportsMap.ts +3 -19
- package/piDependencyPins.test.ts +3 -14
- package/platformTest/IdleWatchedProcess.test.ts +5 -13
- package/platformTest/IdleWatchedProcess.ts +1 -5
- package/platformTest/LinuxDockerTarget.ts +20 -35
- package/platformTest/PlatformTestRun.test.ts +5 -9
- package/platformTest/PlatformTestTarget.ts +24 -2
- package/platformTest/TestSnapshot.test.ts +6 -14
- package/platformTest/TestSnapshot.ts +1 -5
- package/platformTest/WindowsSshTarget.ts +6 -22
- package/prompter.ts +3 -6
- package/qualityScanner.test.ts +6 -24
- package/qualityScanner.ts +196 -284
- package/recipeScanner.test.ts +7 -12
- package/recipeScanner.ts +9 -38
- package/repoIdentity.ts +2 -9
- package/routeSourceValidator.ts +7 -30
- package/scanInfo.ts +82 -214
- 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/subspace.test.ts +6 -25
- package/subspace.ts +31 -109
- package/subspaceConfig.ts +3 -11
- package/testHelpers.ts +49 -34
- 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 +35 -20
- package/transforms/transforms.test.ts +153 -44
- package/transforms/tsconfigPackageResolver.test.ts +6 -24
- package/transforms/useClientBundlePlugin.ts +2 -22
- package/typeChecker.ts +7 -63
- package/uploadRelease.ts +10 -25
- package/workflow/artifacts.ts +18 -0
- package/workflow/executor.test.ts +1 -12
- package/workflow/executor.ts +85 -146
- 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/source.ts +33 -56
- package/workflow/sourceAst.ts +50 -0
- package/workspaceLayout.ts +4 -19
- package/commandDecorators/commandMeta.ts +0 -7
- package/commandDecorators/types.ts +0 -10
- package/getDirname.ts +0 -4
- package/local/optimize-resource/ipcprobe/child.ts +0 -1
- package/local/optimize-resource/ipcprobe/parent.ts +0 -11
- package/workflow/primitive.ts +0 -59
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
codeAgentLabelChars,
|
|
9
9
|
codeAgentOutputChars,
|
|
10
10
|
} from "akanjs/common";
|
|
11
|
+
import { stringArg } from "./stringArg";
|
|
11
12
|
|
|
12
13
|
const toolOps: { [key: string]: CodeAgentToolOp } = {
|
|
13
14
|
read: "read",
|
|
@@ -19,13 +20,7 @@ const toolOps: { [key: string]: CodeAgentToolOp } = {
|
|
|
19
20
|
bash: "execute",
|
|
20
21
|
};
|
|
21
22
|
|
|
22
|
-
|
|
23
|
-
* Narrows the engine's event stream into the akan wire.
|
|
24
|
-
*
|
|
25
|
-
* One turn here is one user prompt, not one LLM round trip: the engine's `turn_start` fires once per round trip
|
|
26
|
-
* inside a run, which is an engine detail no host should have to fold. `agent_start`/`agent_end` are the
|
|
27
|
-
* boundaries a person perceives, so they become our `turn_start`/`turn_end`.
|
|
28
|
-
*/
|
|
23
|
+
// A turn is one prompt: the engine's `turn_start` fires per LLM round trip, so `agent_start`/`agent_end` bound it.
|
|
29
24
|
export class CodeAgentEventMapper {
|
|
30
25
|
#turn = 0;
|
|
31
26
|
#turnId = "";
|
|
@@ -46,8 +41,7 @@ export class CodeAgentEventMapper {
|
|
|
46
41
|
this.#outcome = undefined;
|
|
47
42
|
return [{ type: "turn_start", turnId: this.#turnId }];
|
|
48
43
|
case "agent_end":
|
|
49
|
-
// `turn_end` is last by contract
|
|
50
|
-
// when the run was aborted, so a host that closes the turn on this frame has already seen it.
|
|
44
|
+
// `turn_end` is last by contract; the engine finalizes the message before the run ends, even on abort.
|
|
51
45
|
return [
|
|
52
46
|
...this.flushBlocked(),
|
|
53
47
|
{ type: "turn_end", turnId: this.#turnId, stopReason: this.#stopReason(event) },
|
|
@@ -103,14 +97,7 @@ export class CodeAgentEventMapper {
|
|
|
103
97
|
}
|
|
104
98
|
}
|
|
105
99
|
|
|
106
|
-
|
|
107
|
-
* Records that a call was refused before it ran.
|
|
108
|
-
*
|
|
109
|
-
* The refusal arrives in the `tool_call` hook, which fires **before** `tool_execution_start` — measured: a
|
|
110
|
-
* blocked call produces no start event at all, and then a `tool_execution_end` carrying `isError`. So the
|
|
111
|
-
* start frame is synthesised here and the engine's own end frame is relabelled `blocked`, rather than
|
|
112
|
-
* synthesising a terminal frame that would arrive twice.
|
|
113
|
-
*/
|
|
100
|
+
// The engine sends a blocked call no start event, only an `isError` end: synthesise the start, relabel the end.
|
|
114
101
|
markBlocked(toolCallId: string, toolName: string, args: unknown, reason: string): CodeAgentEventBody[] {
|
|
115
102
|
this.#blocked.set(toolCallId, reason);
|
|
116
103
|
if (this.#openTools.has(toolCallId)) return [];
|
|
@@ -155,25 +142,12 @@ export class CodeAgentEventMapper {
|
|
|
155
142
|
};
|
|
156
143
|
}
|
|
157
144
|
|
|
158
|
-
/**
|
|
159
|
-
* Records how this turn is ending, from the side that actually knows.
|
|
160
|
-
*
|
|
161
|
-
* Consumed once, by the next `agent_end`. See {@link CodeAgentEventMapper.#stopReason} for why the engine's
|
|
162
|
-
* own report cannot be trusted for this.
|
|
163
|
-
*/
|
|
145
|
+
/** Consumed once, by the next `agent_end`, and wins over the engine's own stop reason. */
|
|
164
146
|
noteOutcome(reason: CodeAgentStopReason) {
|
|
165
147
|
this.#outcome = reason;
|
|
166
148
|
}
|
|
167
149
|
|
|
168
|
-
|
|
169
|
-
* **The engine does not report an abort.** Measured: interrupting a streaming turn finalizes the partial
|
|
170
|
-
* assistant message with `stopReason: "stop"` — the provider stream was cut, and from the engine's side that
|
|
171
|
-
* is an ordinary stop. Trusting it reports `done` for every interrupt, and a host then has no way to tell a
|
|
172
|
-
* finished answer from a truncated one.
|
|
173
|
-
*
|
|
174
|
-
* So the core's own record wins: it is the party that called `abort()`, and the party that suspended the
|
|
175
|
-
* turn to ask a question. The engine's report is only the fallback.
|
|
176
|
-
*/
|
|
150
|
+
// The engine reports an interrupted stream as `stop`, so the core's own note wins and the engine's is a fallback.
|
|
177
151
|
#stopReason(event: Extract<AgentSessionEvent, { type: "agent_end" }>): CodeAgentStopReason {
|
|
178
152
|
const noted = this.#outcome;
|
|
179
153
|
this.#outcome = undefined;
|
|
@@ -182,15 +156,13 @@ export class CodeAgentEventMapper {
|
|
|
182
156
|
const last = event.messages.at(-1) as { stopReason?: string } | undefined;
|
|
183
157
|
if (last?.stopReason === "aborted") return "aborted";
|
|
184
158
|
if (last?.stopReason === "error") return "error";
|
|
185
|
-
//
|
|
186
|
-
// read as failures lets `length` — the model hitting its output cap — leave as `done`, and a half-written
|
|
187
|
-
// answer is then stored, and replayed to the next turn, as a finished one.
|
|
159
|
+
// `length` is the model hitting its output cap: a half-written answer must not be stored as `done`.
|
|
188
160
|
if (last?.stopReason === "length") return "truncated";
|
|
189
161
|
return "done";
|
|
190
162
|
}
|
|
191
163
|
|
|
192
164
|
#summary(toolCallId: string, name: string, args: unknown): CodeAgentToolSummary {
|
|
193
|
-
const path =
|
|
165
|
+
const path = stringArg(args, "path");
|
|
194
166
|
return {
|
|
195
167
|
toolCallId,
|
|
196
168
|
name,
|
|
@@ -215,16 +187,6 @@ export class CodeAgentEventMapper {
|
|
|
215
187
|
return [{ type: "message", turnId: this.#turnId, role: message.role, text }];
|
|
216
188
|
}
|
|
217
189
|
|
|
218
|
-
static #pathOf(args: unknown) {
|
|
219
|
-
if (!args || typeof args !== "object") return undefined;
|
|
220
|
-
const value = (args as { path?: unknown }).path;
|
|
221
|
-
return typeof value === "string" && value ? value : undefined;
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
/**
|
|
225
|
-
* A one-line rendering, never the values themselves: a `write` call's argument is the whole new file body and
|
|
226
|
-
* an `edit` call's is every replacement in it.
|
|
227
|
-
*/
|
|
228
190
|
static #renderArgs(args: unknown) {
|
|
229
191
|
if (!args || typeof args !== "object") return "";
|
|
230
192
|
const entries = Object.entries(args as Record<string, unknown>).filter(([, value]) => value !== undefined);
|
|
@@ -233,12 +195,7 @@ export class CodeAgentEventMapper {
|
|
|
233
195
|
return `(${codeAgentClip(rendered, 120)})`;
|
|
234
196
|
}
|
|
235
197
|
|
|
236
|
-
|
|
237
|
-
* A structured argument is described by its shape, not stringified.
|
|
238
|
-
*
|
|
239
|
-
* `String({})` is `"[object Object]"` and an array of them is that repeated — which is where an `edit` call's
|
|
240
|
-
* replacement list ends up, so the row says nothing about a call that changed three places in a file.
|
|
241
|
-
*/
|
|
198
|
+
// By shape, not `String()`, which renders an `edit` call's replacement list as `[object Object]`s.
|
|
242
199
|
static #renderArgValue(value: unknown) {
|
|
243
200
|
if (Array.isArray(value)) return `${value.length} item${value.length === 1 ? "" : "s"}`;
|
|
244
201
|
if (value && typeof value === "object") {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import type { CodeAgentProfile } from "akanjs/common";
|
|
3
|
+
import { stringArg } from "./stringArg";
|
|
3
4
|
|
|
4
5
|
const writeTools = new Set(["write", "edit"]);
|
|
5
6
|
const commandTools = new Set(["bash"]);
|
|
@@ -10,13 +11,7 @@ export interface GateVerdict {
|
|
|
10
11
|
approval?: string;
|
|
11
12
|
}
|
|
12
13
|
|
|
13
|
-
|
|
14
|
-
* Decides, per tool call, whether the profile lets it through, needs a human, or is refused outright.
|
|
15
|
-
*
|
|
16
|
-
* Paths are checked here rather than inside a replacement for the engine's file operations because a refusal
|
|
17
|
-
* has to reach the model as a reason it can act on. An operations port that throws produces a stack trace the
|
|
18
|
-
* model reads as a bug in the tool, and it retries.
|
|
19
|
-
*/
|
|
14
|
+
// Gated here, not in the engine's file-ops port: a throw there reads to the model as a tool bug, and it retries.
|
|
20
15
|
export class CodeAgentGate {
|
|
21
16
|
readonly #profile: CodeAgentProfile;
|
|
22
17
|
readonly #deny: Bun.Glob[];
|
|
@@ -31,12 +26,12 @@ export class CodeAgentGate {
|
|
|
31
26
|
}
|
|
32
27
|
|
|
33
28
|
verdict(toolName: string, args: unknown): GateVerdict {
|
|
34
|
-
const target =
|
|
29
|
+
const target = stringArg(args, "path");
|
|
35
30
|
if (target) {
|
|
36
31
|
const denied = this.#deniedReason(target);
|
|
37
32
|
if (denied) return { block: denied };
|
|
38
33
|
}
|
|
39
|
-
const command =
|
|
34
|
+
const command = stringArg(args, "command");
|
|
40
35
|
if (command) {
|
|
41
36
|
const denied = this.#deniedCommandReason(command);
|
|
42
37
|
if (denied) return { block: denied };
|
|
@@ -45,15 +40,8 @@ export class CodeAgentGate {
|
|
|
45
40
|
return { approval: CodeAgentGate.#summarize(toolName, args) };
|
|
46
41
|
}
|
|
47
42
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
*
|
|
51
|
-
* ⚠️ **This is a speed bump, not a boundary.** A shell can reach any path the process can — through a
|
|
52
|
-
* variable, a glob, a subshell, `base64`, anything — and gating it properly would mean interpreting the
|
|
53
|
-
* command. The real boundary is the container the `pod` profile runs in. What this does buy is that the
|
|
54
|
-
* obvious accident, `cat .env`, does not put a live key into a transcript that is then persisted and
|
|
55
|
-
* replayed on every later turn.
|
|
56
|
-
*/
|
|
43
|
+
// A speed bump, not a boundary: a shell reaches any path; the real boundary is the `pod` profile's container.
|
|
44
|
+
// It stops the obvious accident (`cat .env`) from persisting a live key into the transcript.
|
|
57
45
|
#deniedCommandReason(command: string) {
|
|
58
46
|
const hit = this.#deniedFragments.find((fragment) => command.includes(fragment));
|
|
59
47
|
if (!hit) return undefined;
|
|
@@ -87,9 +75,7 @@ export class CodeAgentGate {
|
|
|
87
75
|
}
|
|
88
76
|
}
|
|
89
77
|
|
|
90
|
-
// A
|
|
91
|
-
// line. A pattern with a wildcard left in the middle yields nothing matchable, so it is dropped rather than
|
|
92
|
-
// guessed at.
|
|
78
|
+
// A glob cannot match a shell line, so a deny pattern is reduced to a literal; a mid-pattern wildcard is dropped.
|
|
93
79
|
static #fragmentOf(pattern: string) {
|
|
94
80
|
const trimmed = pattern
|
|
95
81
|
.replace(/^\*\*\//, "")
|
|
@@ -98,22 +84,10 @@ export class CodeAgentGate {
|
|
|
98
84
|
return trimmed.includes("*") ? "" : trimmed;
|
|
99
85
|
}
|
|
100
86
|
|
|
101
|
-
static #commandOf(args: unknown) {
|
|
102
|
-
if (!args || typeof args !== "object") return undefined;
|
|
103
|
-
const value = (args as { command?: unknown }).command;
|
|
104
|
-
return typeof value === "string" && value ? value : undefined;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
static #pathOf(args: unknown) {
|
|
108
|
-
if (!args || typeof args !== "object") return undefined;
|
|
109
|
-
const value = (args as { path?: unknown }).path;
|
|
110
|
-
return typeof value === "string" && value ? value : undefined;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
87
|
static #summarize(toolName: string, args: unknown) {
|
|
114
88
|
const record = (args ?? {}) as Record<string, unknown>;
|
|
115
89
|
if (toolName === "bash") return `run: ${String(record.command ?? "").slice(0, 200)}`;
|
|
116
|
-
const target =
|
|
90
|
+
const target = stringArg(args, "path");
|
|
117
91
|
return target ? `${toolName} ${target}` : toolName;
|
|
118
92
|
}
|
|
119
93
|
}
|
|
@@ -17,12 +17,7 @@ export interface CodeAgentProxyOptions {
|
|
|
17
17
|
token?: string;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
/**
|
|
21
|
-
* Routes every model request through a host's LLM proxy, so a pod never holds a provider key: the host mints a
|
|
22
|
-
* short-lived token, writes it to a file, and rotates it there while the worker keeps running.
|
|
23
|
-
*
|
|
24
|
-
* `baseUrl` may carry `{provider}` for a proxy that fronts several providers under one host.
|
|
25
|
-
*/
|
|
20
|
+
/** `baseUrl` may carry `{provider}` for a proxy that fronts several providers under one host. */
|
|
26
21
|
export class CodeAgentProxy {
|
|
27
22
|
static readonly placeholderKey = "akan-code-proxy";
|
|
28
23
|
|
|
@@ -59,8 +54,7 @@ export class CodeAgentProxy {
|
|
|
59
54
|
return this.baseUrl.replaceAll("{provider}", provider);
|
|
60
55
|
}
|
|
61
56
|
|
|
62
|
-
//* Read per request, not cached: the host rotates
|
|
63
|
-
//* first one would start failing half an hour into a session it is still running.
|
|
57
|
+
//* Read per request, not cached: the host rotates the token while a session is still running.
|
|
64
58
|
async token() {
|
|
65
59
|
if (!this.#tokenFile) return this.#token ?? "";
|
|
66
60
|
const token = (await readFile(this.#tokenFile, "utf8")).trim();
|
|
@@ -68,11 +62,8 @@ export class CodeAgentProxy {
|
|
|
68
62
|
return token;
|
|
69
63
|
}
|
|
70
64
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
* why it is wrapped rather than the public stream methods. It is private in the engine's typings, so its
|
|
74
|
-
* absence after an engine upgrade fails here at boot instead of sending a request with no key.
|
|
75
|
-
*/
|
|
65
|
+
// `prepareRequest` is the one path every request takes. It is engine-private, so an upgrade that drops it must
|
|
66
|
+
// fail here at boot rather than send a request with no key.
|
|
76
67
|
async apply(runtime: ModelRuntime) {
|
|
77
68
|
const target = runtime as unknown as { prepareRequest?: PrepareRequest };
|
|
78
69
|
const original = target.prepareRequest?.bind(runtime);
|
|
@@ -1,16 +1,7 @@
|
|
|
1
1
|
import type { CodeAgentCommand, CodeAgentEvent, CodeAgentReply, CodeAgentRequest } from "akanjs/common";
|
|
2
2
|
import type { CodeAgent } from "./CodeAgent";
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
* Serves one code agent as newline-delimited akan wire frames, over stdio or whatever transport attaches.
|
|
6
|
-
*
|
|
7
|
-
* The protocol is **ours**, not the engine's: a host — the web relay, a pod runner, a test harness — reads the
|
|
8
|
-
* same events the in-process API emits, so an engine upgrade stops at the core instead of reaching every
|
|
9
|
-
* consumer.
|
|
10
|
-
*
|
|
11
|
-
* stdout carries frames and nothing else. That only holds because `consoleToStderr` was imported before the
|
|
12
|
-
* engine: under Bun the engine's own stdout guard misses `console.log`, which writes to fd 1 natively.
|
|
13
|
-
*/
|
|
4
|
+
// stdout carries only frames because `consoleToStderr` was imported before the engine (Bun's console bypasses it).
|
|
14
5
|
export class CodeAgentRpcHost {
|
|
15
6
|
readonly #agent: CodeAgent;
|
|
16
7
|
#write: ((line: string) => void) | null;
|
|
@@ -40,8 +31,7 @@ export class CodeAgentRpcHost {
|
|
|
40
31
|
this.#agent.announce();
|
|
41
32
|
}
|
|
42
33
|
|
|
43
|
-
//* Frames
|
|
44
|
-
//* that attaches catches up with `get_state { sinceSeq }` instead of receiving a burst it did not ask for.
|
|
34
|
+
//* Frames with no client attached are dropped; the next client catches up with `get_state { sinceSeq }`.
|
|
45
35
|
attach(write: ((line: string) => void) | null) {
|
|
46
36
|
this.#write = write;
|
|
47
37
|
this.#buffer = "";
|
|
@@ -74,10 +64,7 @@ export class CodeAgentRpcHost {
|
|
|
74
64
|
if (request.command?.type === "shutdown") this.#resolveShutdown();
|
|
75
65
|
}
|
|
76
66
|
|
|
77
|
-
|
|
78
|
-
* `prompt` answers as soon as the turn is accepted, not when it finishes — a client that had to wait for the
|
|
79
|
-
* reply could not send `abort`, which is the one command that matters while a turn runs.
|
|
80
|
-
*/
|
|
67
|
+
// `prompt` replies on acceptance, not completion, so the client can still send `abort` while the turn runs.
|
|
81
68
|
async #run(command: CodeAgentCommand): Promise<unknown> {
|
|
82
69
|
switch (command.type) {
|
|
83
70
|
case "prompt":
|
|
@@ -9,16 +9,8 @@ interface ClientState {
|
|
|
9
9
|
decoder: TextDecoder;
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
* outlives the process driving it. A control plane that restarts reconnects and resumes with
|
|
15
|
-
* `get_state { sinceSeq }`; the turn it was watching kept running in the meantime.
|
|
16
|
-
*
|
|
17
|
-
* One client at a time: a new connection replaces the old one, which is what a reconnecting control plane whose
|
|
18
|
-
* previous socket has not timed out yet needs. TCP binds 127.0.0.1 unless a host is named, because the wire has
|
|
19
|
-
* no authentication of its own — reach a pod through its unix socket and whatever the orchestrator already
|
|
20
|
-
* authenticates.
|
|
21
|
-
*/
|
|
12
|
+
// One client at a time: a reconnecting control plane replaces a socket that has not timed out yet.
|
|
13
|
+
// TCP binds 127.0.0.1 unless a host is named, because the wire has no authentication of its own.
|
|
22
14
|
export class CodeAgentRpcListener {
|
|
23
15
|
readonly #host: CodeAgentRpcHost;
|
|
24
16
|
readonly #address: CodeAgentRpcAddress;
|
|
@@ -8,12 +8,6 @@ export interface CodeAgentStreamPrinterOptions {
|
|
|
8
8
|
write?: (text: string) => void;
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
-
/**
|
|
12
|
-
* Prints the akan wire to a terminal, reading nothing but the contract.
|
|
13
|
-
*
|
|
14
|
-
* That restriction is the point: the TUI and the web host read the same events, so anything this printer cannot
|
|
15
|
-
* render is a hole in the contract rather than a missing feature of one host.
|
|
16
|
-
*/
|
|
17
11
|
export class CodeAgentStreamPrinter {
|
|
18
12
|
readonly #options: CodeAgentStreamPrinterOptions;
|
|
19
13
|
readonly #write: (text: string) => void;
|
|
@@ -34,28 +28,23 @@ export class CodeAgentStreamPrinter {
|
|
|
34
28
|
if (!this.#options.thinking) return;
|
|
35
29
|
this.#streaming = true;
|
|
36
30
|
return this.#write(chalk.dim(event.text));
|
|
37
|
-
case "message":
|
|
38
|
-
return;
|
|
39
31
|
case "tool_start":
|
|
40
32
|
return this.#line(chalk.cyan(`→ ${event.tool.title}`));
|
|
41
|
-
case "tool_progress":
|
|
42
|
-
return;
|
|
43
33
|
case "tool_end":
|
|
44
34
|
return this.#line(CodeAgentStreamPrinter.#toolEnd(event));
|
|
45
35
|
case "session":
|
|
46
36
|
return this.#line(chalk.dim(codeAgentEventLabel(event)));
|
|
47
37
|
case "turn_end":
|
|
48
|
-
//
|
|
49
|
-
// A truncated answer is the one that looks exactly like a finished one unless it is said out loud.
|
|
38
|
+
// Only `truncated` is said: a cut answer looks exactly like a finished one.
|
|
50
39
|
if (event.stopReason === "truncated") return this.#line(chalk.yellow(codeAgentEventLabel(event)));
|
|
51
40
|
return;
|
|
41
|
+
case "message":
|
|
42
|
+
case "tool_progress":
|
|
52
43
|
case "turn_start":
|
|
53
44
|
case "idle":
|
|
54
45
|
case "queue":
|
|
55
46
|
case "context":
|
|
56
|
-
|
|
57
|
-
// The rail is a live status for a screen that redraws. A stream has no row to update, and the pool
|
|
58
|
-
// already says on the transcript when a child starts and what it spent when it stops.
|
|
47
|
+
// A stream has no rail to redraw; the pool already reports a child's start and cost as notices.
|
|
59
48
|
case "subagent":
|
|
60
49
|
return;
|
|
61
50
|
case "question":
|
|
@@ -7,14 +7,7 @@ interface SuspendedState {
|
|
|
7
7
|
approvals: CodeAgentApprovalRequest[];
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
* What a suspending profile is waiting on after its turn ended — the question it asked and the approvals it
|
|
12
|
-
* parked. The turn is gone, so nothing in memory holds them; this does, and on a file-backed session it writes
|
|
13
|
-
* them beside the session so a worker that restarts and resumes still knows what it asked.
|
|
14
|
-
*
|
|
15
|
-
* Taking an ask removes it, which is what makes an answer idempotent: a second tab or a retried frame finds
|
|
16
|
-
* nothing to take and is refused instead of opening a second turn.
|
|
17
|
-
*/
|
|
10
|
+
// Taking an ask removes it, which makes an answer idempotent: a second tab or a retried frame finds nothing.
|
|
18
11
|
export class CodeAgentSuspended {
|
|
19
12
|
readonly #file: string | null;
|
|
20
13
|
#state: SuspendedState;
|
|
@@ -5,13 +5,8 @@ export interface CodeAgentUiHandlers {
|
|
|
5
5
|
notify(level: "info" | "warning" | "error", message: string): void;
|
|
6
6
|
}
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
*
|
|
11
|
-
* Only the four dialog calls and `notify` mean anything off a TUI; the rest of the port paints widgets, footers
|
|
12
|
-
* and editors that exist solely inside the engine's own interactive shell. They are no-ops here rather than
|
|
13
|
-
* throws — an extension that sets a status line should not take the turn down with it.
|
|
14
|
-
*/
|
|
8
|
+
// Only the dialogs and `notify` mean anything off the engine's TUI; the rest are no-ops, not throws, so an
|
|
9
|
+
// extension that sets a status line does not take the turn down with it.
|
|
15
10
|
export class CodeAgentUi {
|
|
16
11
|
readonly #handlers: CodeAgentUiHandlers;
|
|
17
12
|
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
writeFileSync,
|
|
10
10
|
} from "node:fs";
|
|
11
11
|
import path from "node:path";
|
|
12
|
+
import { parseJsonLine, readJsonLines } from "./jsonLines";
|
|
12
13
|
|
|
13
14
|
export interface CodePeer {
|
|
14
15
|
id: string;
|
|
@@ -26,18 +27,9 @@ export interface CodeMail {
|
|
|
26
27
|
text: string;
|
|
27
28
|
}
|
|
28
29
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
*
|
|
32
|
-
* A directory, not a socket or a daemon: the sessions already share a checkout and already write their
|
|
33
|
-
* transcripts there, and a mailbox on disk survives one end restarting, needs nothing listening to accept a
|
|
34
|
-
* message, and is inspectable with `cat` when a message does not arrive.
|
|
35
|
-
*
|
|
36
|
-
* Presence is a heartbeat file rather than a registration, because the interesting failure is a session that
|
|
37
|
-
* died without saying so — a registry would keep listing it forever, while a stale mtime says it is gone.
|
|
38
|
-
*/
|
|
30
|
+
// A directory, not a socket or daemon: it survives either end restarting and needs nothing listening.
|
|
31
|
+
// Presence is a heartbeat file, not a registration, so a session that died silently goes stale on its own.
|
|
39
32
|
export class CodeMailbox {
|
|
40
|
-
/** A session older than this said nothing for four heartbeats, so it is not there to answer. */
|
|
41
33
|
static readonly staleMs = 20_000;
|
|
42
34
|
static readonly beatMs = 5_000;
|
|
43
35
|
static readonly pollMs = 1_500;
|
|
@@ -66,9 +58,8 @@ export class CodeMailbox {
|
|
|
66
58
|
open(cwd: string, onMail: (mail: CodeMail) => void) {
|
|
67
59
|
mkdirSync(path.join(this.#dir, "live"), { recursive: true });
|
|
68
60
|
mkdirSync(path.join(this.#dir, "inbox"), { recursive: true });
|
|
69
|
-
//
|
|
70
|
-
|
|
71
|
-
this.#read = CodeMailbox.#lines(this.inbox).length;
|
|
61
|
+
// What is already in the inbox belongs to an earlier run; delivering it would replay a past conversation.
|
|
62
|
+
this.#read = readJsonLines(this.inbox).length;
|
|
72
63
|
this.beat(cwd);
|
|
73
64
|
this.#beat = setInterval(() => this.beat(cwd), CodeMailbox.beatMs);
|
|
74
65
|
this.#poll = setInterval(() => {
|
|
@@ -137,12 +128,12 @@ export class CodeMailbox {
|
|
|
137
128
|
}
|
|
138
129
|
|
|
139
130
|
#drain(): CodeMail[] {
|
|
140
|
-
const lines =
|
|
131
|
+
const lines = readJsonLines(this.inbox);
|
|
141
132
|
if (lines.length <= this.#read) return [];
|
|
142
133
|
const fresh = lines.slice(this.#read);
|
|
143
134
|
this.#read = lines.length;
|
|
144
135
|
return fresh
|
|
145
|
-
.map((line) =>
|
|
136
|
+
.map((line) => parseJsonLine<CodeMail>(line))
|
|
146
137
|
.filter((mail): mail is CodeMail => !!mail?.text && typeof mail.from === "string");
|
|
147
138
|
}
|
|
148
139
|
|
|
@@ -154,22 +145,4 @@ export class CodeMailbox {
|
|
|
154
145
|
return undefined;
|
|
155
146
|
}
|
|
156
147
|
}
|
|
157
|
-
|
|
158
|
-
static #lines(file: string) {
|
|
159
|
-
try {
|
|
160
|
-
return readFileSync(file, "utf8")
|
|
161
|
-
.split("\n")
|
|
162
|
-
.filter((line) => !!line.trim());
|
|
163
|
-
} catch {
|
|
164
|
-
return [];
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
static #parse(line: string) {
|
|
169
|
-
try {
|
|
170
|
-
return JSON.parse(line) as CodeMail;
|
|
171
|
-
} catch {
|
|
172
|
-
return undefined;
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
148
|
}
|
|
@@ -3,6 +3,7 @@ import { readFileSync, writeFileSync } from "node:fs";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { codeAgentSessionName } from "akanjs/common";
|
|
5
5
|
import { CodeSessionIndex } from "./CodeSessionIndex";
|
|
6
|
+
import { parseJsonLine } from "./jsonLines";
|
|
6
7
|
|
|
7
8
|
interface CodeSessionRecord {
|
|
8
9
|
[key: string]: unknown;
|
|
@@ -10,14 +11,7 @@ interface CodeSessionRecord {
|
|
|
10
11
|
id?: string;
|
|
11
12
|
}
|
|
12
13
|
|
|
13
|
-
|
|
14
|
-
* Copies a session's file so one conversation can continue two ways.
|
|
15
|
-
*
|
|
16
|
-
* A fork is a file copy because that is exactly what a session is: an append-only log the engine replays. The
|
|
17
|
-
* copy is rewritten in one place — the `session` record's id — and then the two files know nothing about each
|
|
18
|
-
* other, which is the property that matters. Sharing an id instead would give two live agents one inbox, one
|
|
19
|
-
* presence entry and one resume target, and the second one to write would win silently.
|
|
20
|
-
*/
|
|
14
|
+
// Only the `session` record's id is rewritten: a shared id would give two agents one inbox and one resume target.
|
|
21
15
|
export class CodeSessionFork {
|
|
22
16
|
static fork(dir: string, id: string, name?: string) {
|
|
23
17
|
const source = CodeSessionIndex.fileOf(dir, id);
|
|
@@ -29,15 +23,14 @@ export class CodeSessionFork {
|
|
|
29
23
|
const at = new Date();
|
|
30
24
|
const forked = Bun.randomUUIDv7();
|
|
31
25
|
const records = lines
|
|
32
|
-
.map((line) =>
|
|
26
|
+
.map((line) => parseJsonLine<CodeSessionRecord>(line))
|
|
33
27
|
.filter((record): record is CodeSessionRecord => !!record);
|
|
34
28
|
const first = records.find((record) => record.type === "session");
|
|
35
29
|
if (!first) throw new Error(`Session ${id} carries no session record`);
|
|
36
30
|
first.id = forked;
|
|
37
31
|
first.timestamp = at.toISOString();
|
|
38
32
|
records.push(CodeSessionFork.#named(records, name ?? CodeSessionFork.#nameOf(records), at));
|
|
39
|
-
// The
|
|
40
|
-
// what `CodeSessionIndex` matches on when a resume names one.
|
|
33
|
+
// The engine's own file-name shape, which `CodeSessionIndex.fileOf` matches a resume against.
|
|
41
34
|
const file = path.join(dir, `${at.toISOString().replace(/[:.]/g, "-")}_${forked}.jsonl`);
|
|
42
35
|
writeFileSync(file, `${records.map((record) => JSON.stringify(record)).join("\n")}\n`);
|
|
43
36
|
return { id: forked, file };
|
|
@@ -60,12 +53,4 @@ export class CodeSessionFork {
|
|
|
60
53
|
const current = typeof named?.name === "string" ? named.name : "";
|
|
61
54
|
return codeAgentSessionName(current ? `${current} fork` : "fork");
|
|
62
55
|
}
|
|
63
|
-
|
|
64
|
-
static #parse(line: string) {
|
|
65
|
-
try {
|
|
66
|
-
return JSON.parse(line) as CodeSessionRecord;
|
|
67
|
-
} catch {
|
|
68
|
-
return undefined;
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
56
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { existsSync, readdirSync,
|
|
1
|
+
import { existsSync, readdirSync, statSync } from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import { parseJsonLine, readJsonLines } from "./jsonLines";
|
|
3
4
|
|
|
4
5
|
export interface CodeSessionEntry {
|
|
5
6
|
id: string;
|
|
@@ -12,15 +13,8 @@ export interface CodeSessionEntry {
|
|
|
12
13
|
turns: number;
|
|
13
14
|
}
|
|
14
15
|
|
|
15
|
-
|
|
16
|
-
* The sessions on disk, read as files rather than through the engine.
|
|
17
|
-
*
|
|
18
|
-
* The engine's session manager addresses one session at a time — it can open a file and append to it, and has
|
|
19
|
-
* nothing that enumerates the directory. The format is self-describing JSONL, so the listing is a read of
|
|
20
|
-
* lines we already own the shape of, and it costs no engine instance to produce.
|
|
21
|
-
*/
|
|
16
|
+
// Read as files: the engine's session manager has nothing that enumerates the directory.
|
|
22
17
|
export class CodeSessionIndex {
|
|
23
|
-
/** Past this, a file is a session long enough that its opening lines are far from the interesting part. */
|
|
24
18
|
static readonly readLimit = 512 * 1024;
|
|
25
19
|
|
|
26
20
|
static list(dir: string, limit = 30): CodeSessionEntry[] {
|
|
@@ -52,11 +46,11 @@ export class CodeSessionIndex {
|
|
|
52
46
|
}
|
|
53
47
|
|
|
54
48
|
static #read(file: string, updatedAt: number): CodeSessionEntry | undefined {
|
|
55
|
-
const lines =
|
|
49
|
+
const lines = readJsonLines(file, CodeSessionIndex.readLimit);
|
|
56
50
|
if (!lines.length) return undefined;
|
|
57
51
|
const entry: CodeSessionEntry = { id: "", file, name: undefined, opening: "", updatedAt, turns: 0 };
|
|
58
52
|
for (const line of lines) {
|
|
59
|
-
const record =
|
|
53
|
+
const record = parseJsonLine<{ [key: string]: unknown; type?: string }>(line);
|
|
60
54
|
if (!record) continue;
|
|
61
55
|
if (record.type === "session" && typeof record.id === "string") entry.id = record.id;
|
|
62
56
|
// A rename appends a second entry rather than editing the first, so the last one on the file wins.
|
|
@@ -70,26 +64,6 @@ export class CodeSessionIndex {
|
|
|
70
64
|
return entry.id ? entry : undefined;
|
|
71
65
|
}
|
|
72
66
|
|
|
73
|
-
static #lines(file: string) {
|
|
74
|
-
try {
|
|
75
|
-
if (statSync(file).size > CodeSessionIndex.readLimit) return [];
|
|
76
|
-
return readFileSync(file, "utf8")
|
|
77
|
-
.split("\n")
|
|
78
|
-
.filter((line) => !!line.trim());
|
|
79
|
-
} catch {
|
|
80
|
-
return [];
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
static #parse(line: string): { [key: string]: unknown; type?: string } | undefined {
|
|
85
|
-
try {
|
|
86
|
-
return JSON.parse(line) as { type?: string };
|
|
87
|
-
} catch {
|
|
88
|
-
return undefined;
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
/** One line, always: the opening ask is a label in a list, and a pasted stack trace is not a label. */
|
|
93
67
|
static #text(content: unknown) {
|
|
94
68
|
const text =
|
|
95
69
|
typeof content === "string"
|