@getpaseo/server 0.1.99 → 0.1.101
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/server/executable-resolution/windows.js +3 -0
- package/dist/server/server/agent/agent-manager.d.ts +10 -0
- package/dist/server/server/agent/agent-manager.js +65 -27
- package/dist/server/server/agent/agent-sdk-types.d.ts +8 -0
- package/dist/server/server/agent/mcp-server.d.ts +2 -45
- package/dist/server/server/agent/mcp-server.js +45 -1985
- package/dist/server/server/agent/prompt-attachments.js +6 -2
- package/dist/server/server/agent/provider-registry.js +1 -0
- package/dist/server/server/agent/provider-snapshot-manager.d.ts +4 -0
- package/dist/server/server/agent/provider-snapshot-manager.js +58 -13
- package/dist/server/server/agent/providers/acp-agent.d.ts +39 -2
- package/dist/server/server/agent/providers/acp-agent.js +281 -20
- package/dist/server/server/agent/providers/claude/agent.js +96 -62
- package/dist/server/server/agent/providers/codex-app-server-agent.js +6 -57
- package/dist/server/server/agent/providers/copilot-acp-agent.d.ts +2 -1
- package/dist/server/server/agent/providers/copilot-acp-agent.js +10 -0
- package/dist/server/server/agent/providers/diagnostic-utils.d.ts +1 -0
- package/dist/server/server/agent/providers/diagnostic-utils.js +1 -1
- package/dist/server/server/agent/providers/generic-acp-agent.d.ts +3 -0
- package/dist/server/server/agent/providers/generic-acp-agent.js +41 -23
- package/dist/server/server/agent/providers/mock-load-test-agent.js +4 -2
- package/dist/server/server/agent/providers/opencode/server-manager.d.ts +14 -11
- package/dist/server/server/agent/providers/opencode/server-manager.js +149 -91
- package/dist/server/server/agent/providers/opencode/test-server-manager.d.ts +6 -5
- package/dist/server/server/agent/providers/opencode/test-server-manager.js +13 -3
- package/dist/server/server/agent/providers/opencode/test-utils/{test-opencode-runtime.d.ts → test-opencode-harness.d.ts} +11 -11
- package/dist/server/server/agent/providers/opencode/test-utils/{test-opencode-runtime.js → test-opencode-harness.js} +23 -10
- package/dist/server/server/agent/providers/opencode-agent.d.ts +9 -3
- package/dist/server/server/agent/providers/opencode-agent.js +26 -38
- package/dist/server/server/agent/providers/pi/agent.d.ts +4 -2
- package/dist/server/server/agent/providers/pi/agent.js +8 -3
- package/dist/server/server/agent/providers/pi/cli-runtime.d.ts +3 -0
- package/dist/server/server/agent/providers/pi/cli-runtime.js +6 -3
- package/dist/server/server/agent/providers/pi/rpc-types.d.ts +2 -1
- package/dist/server/server/agent/providers/provider-image-output.d.ts +5 -0
- package/dist/server/server/agent/providers/provider-image-output.js +55 -0
- package/dist/server/server/agent/tools/paseo-tools.d.ts +48 -0
- package/dist/server/server/agent/tools/paseo-tools.js +2121 -0
- package/dist/server/server/agent/tools/types.d.ts +36 -0
- package/dist/server/server/agent/tools/types.js +2 -0
- package/dist/server/server/bootstrap.js +71 -62
- package/dist/server/server/persisted-config.d.ts +5 -0
- package/dist/server/server/persisted-config.js +10 -2
- package/dist/server/server/session/agent-updates/agent-updates-service.d.ts +59 -0
- package/dist/server/server/session/agent-updates/agent-updates-service.js +220 -0
- package/dist/server/server/session/checkout/checkout-session.d.ts +13 -15
- package/dist/server/server/session/checkout/checkout-session.js +18 -16
- package/dist/server/server/session/checkout/git-metadata-generator.d.ts +53 -0
- package/dist/server/server/session/checkout/git-metadata-generator.js +159 -0
- package/dist/server/server/session/daemon/daemon-session.d.ts +14 -0
- package/dist/server/server/session/daemon/daemon-session.js +38 -0
- package/dist/server/server/session/daemon/diagnostics.d.ts +41 -0
- package/dist/server/server/session/daemon/diagnostics.js +421 -0
- package/dist/server/server/session/git-mutation/git-mutation-service.d.ts +34 -0
- package/dist/server/server/session/git-mutation/git-mutation-service.js +71 -0
- package/dist/server/server/session/workspace-git-observer/workspace-git-observer-service.d.ts +36 -0
- package/dist/server/server/session/workspace-git-observer/workspace-git-observer-service.js +134 -0
- package/dist/server/server/session/workspace-provisioning/workspace-provisioning-service.d.ts +34 -0
- package/dist/server/server/session/workspace-provisioning/workspace-provisioning-service.js +190 -0
- package/dist/server/server/session/workspace-scripts/workspace-scripts-service.d.ts +41 -0
- package/dist/server/server/session/workspace-scripts/workspace-scripts-service.js +100 -0
- package/dist/server/server/session.d.ts +7 -51
- package/dist/server/server/session.js +113 -938
- package/dist/server/server/speech/providers/openai/config.d.ts +1 -2
- package/dist/server/server/speech/providers/openai/config.js +13 -9
- package/dist/server/server/speech/providers/openai/runtime.js +2 -16
- package/dist/server/server/speech/providers/openai/stt.d.ts +1 -0
- package/dist/server/server/speech/providers/openai/stt.js +4 -2
- package/dist/server/server/speech/providers/openai/tts.d.ts +1 -0
- package/dist/server/server/speech/providers/openai/tts.js +1 -0
- package/dist/server/server/websocket/runtime-metrics.d.ts +20 -0
- package/dist/server/server/websocket-server.d.ts +1 -2
- package/dist/server/server/websocket-server.js +26 -21
- package/dist/server/server/worktree-bootstrap.d.ts +1 -1
- package/dist/server/server/worktree-branch-name-generator.js +3 -1
- package/dist/server/utils/checkout-git.js +51 -26
- package/dist/src/executable-resolution/windows.js +3 -0
- package/dist/src/server/persisted-config.js +10 -2
- package/package.json +5 -5
- package/dist/server/server/agent/providers/opencode/runtime.d.ts +0 -28
- package/dist/server/server/agent/providers/opencode/runtime.js +0 -5
- package/dist/server/server/speech/providers/openai/realtime-transcription-session.d.ts +0 -42
- package/dist/server/server/speech/providers/openai/realtime-transcription-session.js +0 -168
|
@@ -17,6 +17,7 @@ import { realClaudeRewindSdk, revertClaudeConversation, revertClaudeFiles } from
|
|
|
17
17
|
import { normalizeProviderReplayTimestamp } from "../../provider-history-timestamps.js";
|
|
18
18
|
import { claudeProjectDirSync } from "./project-dir.js";
|
|
19
19
|
import { SETTING_APPLIES_NEXT_TURN_NOTICE } from "../../provider-notices.js";
|
|
20
|
+
import { isProviderImageMarkdown, materializeProviderImage, renderProviderImageOutputAsAssistantMarkdown, } from "../provider-image-output.js";
|
|
20
21
|
import { getAgentStreamEventTurnId, } from "../../agent-sdk-types.js";
|
|
21
22
|
import { importSessionFromPersistence } from "../../provider-session-import.js";
|
|
22
23
|
import { checkProviderLaunchAvailable, createProviderEnv, createProviderEnvSpec, resolveProviderLaunch, } from "../../provider-launch-config.js";
|
|
@@ -370,6 +371,39 @@ function coerceToolResultContentToString(content) {
|
|
|
370
371
|
}
|
|
371
372
|
return deterministicStringify(content);
|
|
372
373
|
}
|
|
374
|
+
function toBase64ImageOutput(block) {
|
|
375
|
+
const record = toObjectRecord(block);
|
|
376
|
+
if (!record || record.type !== "image") {
|
|
377
|
+
return null;
|
|
378
|
+
}
|
|
379
|
+
const source = toObjectRecord(record.source);
|
|
380
|
+
if (!source || source.type !== "base64" || typeof source.data !== "string") {
|
|
381
|
+
return null;
|
|
382
|
+
}
|
|
383
|
+
return {
|
|
384
|
+
data: source.data,
|
|
385
|
+
mimeType: typeof source.media_type === "string" ? source.media_type : null,
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
// Claude returns images inside tool_result content as base64 Anthropic blocks. Left in place they
|
|
389
|
+
// reach coerceToolResultContentToString, which JSON.stringifies the whole array — dumping base64
|
|
390
|
+
// into the tool output. We pull those blocks out to render them as image markdown and leave a
|
|
391
|
+
// "[image]" placeholder so image-only results still produce non-empty output.
|
|
392
|
+
function splitClaudeToolResultImages(content) {
|
|
393
|
+
if (!Array.isArray(content)) {
|
|
394
|
+
return { images: [], text: content };
|
|
395
|
+
}
|
|
396
|
+
const images = [];
|
|
397
|
+
const text = content.map((block) => {
|
|
398
|
+
const image = toBase64ImageOutput(block);
|
|
399
|
+
if (image) {
|
|
400
|
+
images.push(image);
|
|
401
|
+
return { type: "text", text: "[image]" };
|
|
402
|
+
}
|
|
403
|
+
return block;
|
|
404
|
+
});
|
|
405
|
+
return { images, text };
|
|
406
|
+
}
|
|
373
407
|
function normalizeClaudeTranscriptText(value) {
|
|
374
408
|
if (typeof value !== "string") {
|
|
375
409
|
return null;
|
|
@@ -1252,23 +1286,6 @@ function readLegacyResultUsageTokens(usage) {
|
|
|
1252
1286
|
const usageRecord = toObjectRecord(usage);
|
|
1253
1287
|
return usageRecord ? readUsageTokenTotal(usageRecord) : undefined;
|
|
1254
1288
|
}
|
|
1255
|
-
function readCurrentContextUsage(value) {
|
|
1256
|
-
const record = toObjectRecord(value);
|
|
1257
|
-
if (!record) {
|
|
1258
|
-
return undefined;
|
|
1259
|
-
}
|
|
1260
|
-
const totalTokens = record.totalTokens;
|
|
1261
|
-
if (typeof totalTokens !== "number" || !Number.isFinite(totalTokens) || totalTokens < 0) {
|
|
1262
|
-
return undefined;
|
|
1263
|
-
}
|
|
1264
|
-
const maxTokens = record.maxTokens;
|
|
1265
|
-
return {
|
|
1266
|
-
totalTokens,
|
|
1267
|
-
...(typeof maxTokens === "number" && Number.isFinite(maxTokens) && maxTokens > 0
|
|
1268
|
-
? { maxTokens }
|
|
1269
|
-
: {}),
|
|
1270
|
-
};
|
|
1271
|
-
}
|
|
1272
1289
|
function isClaudeSubagentToolName(name) {
|
|
1273
1290
|
return name === "Task" || name === "Agent";
|
|
1274
1291
|
}
|
|
@@ -1280,6 +1297,7 @@ class ClaudeContextUsageState {
|
|
|
1280
1297
|
beginTurn() {
|
|
1281
1298
|
this.streamRequestInputTokens = undefined;
|
|
1282
1299
|
this.streamRequestOutputTokens = undefined;
|
|
1300
|
+
this.compactedContextWindowUsedTokens = undefined;
|
|
1283
1301
|
}
|
|
1284
1302
|
setInitialContextWindowMaxTokens(contextWindowMaxTokens) {
|
|
1285
1303
|
this.contextWindowMaxTokens = contextWindowMaxTokens;
|
|
@@ -1291,11 +1309,6 @@ class ClaudeContextUsageState {
|
|
|
1291
1309
|
}
|
|
1292
1310
|
return this.contextWindowMaxTokens;
|
|
1293
1311
|
}
|
|
1294
|
-
recordCurrentContextUsage(usage) {
|
|
1295
|
-
if (usage?.maxTokens !== undefined) {
|
|
1296
|
-
this.contextWindowMaxTokens = usage.maxTokens;
|
|
1297
|
-
}
|
|
1298
|
-
}
|
|
1299
1312
|
buildStreamUsageEvent(event) {
|
|
1300
1313
|
const streamEvent = toObjectRecord(event);
|
|
1301
1314
|
if (!streamEvent) {
|
|
@@ -1326,7 +1339,7 @@ class ClaudeContextUsageState {
|
|
|
1326
1339
|
}
|
|
1327
1340
|
return this.createUsageUpdatedEvent(usedTokens);
|
|
1328
1341
|
}
|
|
1329
|
-
buildResultUsage(message, modelUsage
|
|
1342
|
+
buildResultUsage(message, modelUsage) {
|
|
1330
1343
|
try {
|
|
1331
1344
|
if (!message.usage) {
|
|
1332
1345
|
return undefined;
|
|
@@ -1338,7 +1351,6 @@ class ClaudeContextUsageState {
|
|
|
1338
1351
|
totalCostUsd: message.total_cost_usd,
|
|
1339
1352
|
};
|
|
1340
1353
|
const modelContextWindowMaxTokens = this.recordModelUsage(modelUsage ?? message.modelUsage);
|
|
1341
|
-
this.recordCurrentContextUsage(currentContextUsage);
|
|
1342
1354
|
if (this.contextWindowMaxTokens !== undefined) {
|
|
1343
1355
|
usage.contextWindowMaxTokens = this.contextWindowMaxTokens;
|
|
1344
1356
|
}
|
|
@@ -1347,13 +1359,14 @@ class ClaudeContextUsageState {
|
|
|
1347
1359
|
}
|
|
1348
1360
|
const activeResultUsageTokens = readActiveUsageTokens(message.usage) ??
|
|
1349
1361
|
(this.completedResultTurns === 0 ? readLegacyResultUsageTokens(message.usage) : undefined);
|
|
1350
|
-
const usedTokens =
|
|
1362
|
+
const usedTokens = this.streamUsedTokens() ?? activeResultUsageTokens ?? this.compactedContextWindowUsedTokens;
|
|
1351
1363
|
if (usedTokens !== undefined) {
|
|
1352
1364
|
usage.contextWindowUsedTokens = usedTokens;
|
|
1353
1365
|
}
|
|
1354
1366
|
return usage;
|
|
1355
1367
|
}
|
|
1356
1368
|
finally {
|
|
1369
|
+
this.compactedContextWindowUsedTokens = undefined;
|
|
1357
1370
|
this.completedResultTurns += 1;
|
|
1358
1371
|
}
|
|
1359
1372
|
}
|
|
@@ -1362,7 +1375,8 @@ class ClaudeContextUsageState {
|
|
|
1362
1375
|
typeof this.streamRequestOutputTokens !== "number") {
|
|
1363
1376
|
return undefined;
|
|
1364
1377
|
}
|
|
1365
|
-
|
|
1378
|
+
const usedTokens = this.streamRequestInputTokens + this.streamRequestOutputTokens;
|
|
1379
|
+
return usedTokens > 0 ? usedTokens : undefined;
|
|
1366
1380
|
}
|
|
1367
1381
|
createUsageUpdatedEvent(contextWindowUsedTokens) {
|
|
1368
1382
|
const usage = {
|
|
@@ -1377,6 +1391,23 @@ class ClaudeContextUsageState {
|
|
|
1377
1391
|
usage,
|
|
1378
1392
|
};
|
|
1379
1393
|
}
|
|
1394
|
+
buildCompactionUsageEvent(postTokens) {
|
|
1395
|
+
this.streamRequestInputTokens = undefined;
|
|
1396
|
+
this.streamRequestOutputTokens = undefined;
|
|
1397
|
+
this.compactedContextWindowUsedTokens = postTokens;
|
|
1398
|
+
const usage = {};
|
|
1399
|
+
if (this.contextWindowMaxTokens !== undefined) {
|
|
1400
|
+
usage.contextWindowMaxTokens = this.contextWindowMaxTokens;
|
|
1401
|
+
}
|
|
1402
|
+
if (postTokens !== undefined) {
|
|
1403
|
+
usage.contextWindowUsedTokens = postTokens;
|
|
1404
|
+
}
|
|
1405
|
+
return {
|
|
1406
|
+
type: "usage_updated",
|
|
1407
|
+
provider: "claude",
|
|
1408
|
+
usage,
|
|
1409
|
+
};
|
|
1410
|
+
}
|
|
1380
1411
|
}
|
|
1381
1412
|
class ClaudeAgentSession {
|
|
1382
1413
|
constructor(config, options) {
|
|
@@ -2659,7 +2690,7 @@ class ClaudeAgentSession {
|
|
|
2659
2690
|
if (await this.handleMissingResumedConversation(message, activeQuery)) {
|
|
2660
2691
|
return true;
|
|
2661
2692
|
}
|
|
2662
|
-
await this.routeSdkMessageFromPump(message
|
|
2693
|
+
await this.routeSdkMessageFromPump(message);
|
|
2663
2694
|
return false;
|
|
2664
2695
|
};
|
|
2665
2696
|
const drainActiveQuery = async () => {
|
|
@@ -2727,7 +2758,7 @@ class ClaudeAgentSession {
|
|
|
2727
2758
|
message.type === "tool_progress" ||
|
|
2728
2759
|
(message.type === "system" && message.subtype === "task_notification"));
|
|
2729
2760
|
}
|
|
2730
|
-
async routeSdkMessageFromPump(message
|
|
2761
|
+
async routeSdkMessageFromPump(message) {
|
|
2731
2762
|
if (this.shouldSuppressStaleResult(message)) {
|
|
2732
2763
|
return;
|
|
2733
2764
|
}
|
|
@@ -2750,7 +2781,7 @@ class ClaudeAgentSession {
|
|
|
2750
2781
|
identifiers,
|
|
2751
2782
|
rawEvent: message,
|
|
2752
2783
|
}, "provider.claude.parsed_event");
|
|
2753
|
-
const events = await this.buildPumpedMessageEvents(message,
|
|
2784
|
+
const events = await this.buildPumpedMessageEvents(message, identifiers.messageId, turnId);
|
|
2754
2785
|
if (events.length === 0) {
|
|
2755
2786
|
return;
|
|
2756
2787
|
}
|
|
@@ -2773,14 +2804,10 @@ class ClaudeAgentSession {
|
|
|
2773
2804
|
}
|
|
2774
2805
|
this.dispatchEvents(events);
|
|
2775
2806
|
}
|
|
2776
|
-
async buildPumpedMessageEvents(message,
|
|
2777
|
-
const currentContextUsage = message.type === "result" && message.subtype === "success"
|
|
2778
|
-
? await this.queryCurrentContextUsage(activeQuery)
|
|
2779
|
-
: undefined;
|
|
2807
|
+
async buildPumpedMessageEvents(message, messageIdHint, turnId) {
|
|
2780
2808
|
const messageEvents = this.translateMessageToEvents(message, {
|
|
2781
2809
|
suppressAssistantText: true,
|
|
2782
2810
|
suppressReasoning: true,
|
|
2783
|
-
currentContextUsage,
|
|
2784
2811
|
});
|
|
2785
2812
|
const assistantTimelineEvents = this.timelineAssembler
|
|
2786
2813
|
.consume({
|
|
@@ -2795,16 +2822,6 @@ class ClaudeAgentSession {
|
|
|
2795
2822
|
}));
|
|
2796
2823
|
return [...messageEvents, ...assistantTimelineEvents];
|
|
2797
2824
|
}
|
|
2798
|
-
async queryCurrentContextUsage(activeQuery) {
|
|
2799
|
-
try {
|
|
2800
|
-
const usage = await withTimeout(activeQuery.getContextUsage(), 3000, "timeout");
|
|
2801
|
-
return readCurrentContextUsage(usage);
|
|
2802
|
-
}
|
|
2803
|
-
catch (error) {
|
|
2804
|
-
this.logger.debug({ err: error }, "Claude context usage query failed");
|
|
2805
|
-
return undefined;
|
|
2806
|
-
}
|
|
2807
|
-
}
|
|
2808
2825
|
async handleMissingResumedConversation(message, activeQuery) {
|
|
2809
2826
|
const staleResumeError = this.readMissingResumedConversationError(message);
|
|
2810
2827
|
if (!staleResumeError) {
|
|
@@ -2895,9 +2912,7 @@ class ClaudeAgentSession {
|
|
|
2895
2912
|
this.appendStreamEventEvents(message, events, options);
|
|
2896
2913
|
break;
|
|
2897
2914
|
case "result":
|
|
2898
|
-
this.appendResultEvents(message, events
|
|
2899
|
-
currentContextUsage: options?.currentContextUsage,
|
|
2900
|
-
});
|
|
2915
|
+
this.appendResultEvents(message, events);
|
|
2901
2916
|
break;
|
|
2902
2917
|
default:
|
|
2903
2918
|
break;
|
|
@@ -2963,6 +2978,7 @@ class ClaudeAgentSession {
|
|
|
2963
2978
|
},
|
|
2964
2979
|
provider: "claude",
|
|
2965
2980
|
});
|
|
2981
|
+
events.push(this.contextUsage.buildCompactionUsageEvent(compactMetadata?.postTokens));
|
|
2966
2982
|
return;
|
|
2967
2983
|
}
|
|
2968
2984
|
if (message.subtype === "task_notification") {
|
|
@@ -3067,8 +3083,8 @@ class ClaudeAgentSession {
|
|
|
3067
3083
|
events.push({ type: "timeline", item, provider: "claude" });
|
|
3068
3084
|
}
|
|
3069
3085
|
}
|
|
3070
|
-
appendResultEvents(message, events
|
|
3071
|
-
const usage = this.convertUsage(message, message.modelUsage
|
|
3086
|
+
appendResultEvents(message, events) {
|
|
3087
|
+
const usage = this.convertUsage(message, message.modelUsage);
|
|
3072
3088
|
if (message.subtype === "success") {
|
|
3073
3089
|
// Built-in slash commands (e.g. /voice, /usage, "Unknown command: …")
|
|
3074
3090
|
// run client-side in the Claude CLI with no model turn — output_tokens
|
|
@@ -3212,8 +3228,8 @@ class ClaudeAgentSession {
|
|
|
3212
3228
|
}
|
|
3213
3229
|
return null;
|
|
3214
3230
|
}
|
|
3215
|
-
convertUsage(message, modelUsage
|
|
3216
|
-
return this.contextUsage.buildResultUsage(message, modelUsage
|
|
3231
|
+
convertUsage(message, modelUsage) {
|
|
3232
|
+
return this.contextUsage.buildResultUsage(message, modelUsage);
|
|
3217
3233
|
}
|
|
3218
3234
|
enqueueTimeline(item) {
|
|
3219
3235
|
this.pushEvent({ type: "timeline", item, provider: "claude" });
|
|
@@ -3490,15 +3506,17 @@ class ClaudeAgentSession {
|
|
|
3490
3506
|
const callId = typeof block.tool_use_id === "string" && block.tool_use_id.length > 0
|
|
3491
3507
|
? block.tool_use_id
|
|
3492
3508
|
: (entry?.id ?? null);
|
|
3493
|
-
//
|
|
3494
|
-
|
|
3509
|
+
// Pull image blocks out of the result so base64 never reaches the tool output, and render each
|
|
3510
|
+
// one as an assistant_message markdown image after the tool_call (matching how Codex emits).
|
|
3511
|
+
const { images, text } = splitClaudeToolResultImages(block.content);
|
|
3512
|
+
const output = this.buildToolOutput(text, block, entry);
|
|
3495
3513
|
if (block.is_error) {
|
|
3496
3514
|
this.pushToolCall(mapClaudeFailedToolCall({
|
|
3497
3515
|
name: toolName,
|
|
3498
3516
|
callId,
|
|
3499
3517
|
input: entry?.input ?? null,
|
|
3500
3518
|
output: output ?? null,
|
|
3501
|
-
error: block,
|
|
3519
|
+
error: { ...block, content: text },
|
|
3502
3520
|
}), items);
|
|
3503
3521
|
}
|
|
3504
3522
|
else {
|
|
@@ -3509,12 +3527,20 @@ class ClaudeAgentSession {
|
|
|
3509
3527
|
output: output ?? null,
|
|
3510
3528
|
}), items);
|
|
3511
3529
|
}
|
|
3530
|
+
for (const image of images) {
|
|
3531
|
+
const imageItem = renderProviderImageOutputAsAssistantMarkdown(image, {
|
|
3532
|
+
materialize: materializeProviderImage,
|
|
3533
|
+
});
|
|
3534
|
+
if (imageItem) {
|
|
3535
|
+
items.push(imageItem);
|
|
3536
|
+
}
|
|
3537
|
+
}
|
|
3512
3538
|
if (typeof block.tool_use_id === "string") {
|
|
3513
3539
|
this.toolUseCache.delete(block.tool_use_id);
|
|
3514
3540
|
this.sidechainTracker.delete(block.tool_use_id);
|
|
3515
3541
|
}
|
|
3516
3542
|
}
|
|
3517
|
-
buildToolOutput(block, entry) {
|
|
3543
|
+
buildToolOutput(content, block, entry) {
|
|
3518
3544
|
if (block.is_error) {
|
|
3519
3545
|
return undefined;
|
|
3520
3546
|
}
|
|
@@ -3522,23 +3548,23 @@ class ClaudeAgentSession {
|
|
|
3522
3548
|
const blockToolName = typeof block.tool_name === "string" ? block.tool_name : undefined;
|
|
3523
3549
|
const server = entry?.server ?? blockServer ?? "tool";
|
|
3524
3550
|
const tool = entry?.name ?? blockToolName ?? "tool";
|
|
3525
|
-
const
|
|
3551
|
+
const coercedContent = coerceToolResultContentToString(content);
|
|
3526
3552
|
const input = entry?.input;
|
|
3527
3553
|
// Build structured result based on tool type
|
|
3528
|
-
const structured = this.buildStructuredToolResult(server, tool,
|
|
3554
|
+
const structured = this.buildStructuredToolResult(server, tool, coercedContent, input);
|
|
3529
3555
|
if (structured) {
|
|
3530
3556
|
return structured;
|
|
3531
3557
|
}
|
|
3532
3558
|
// Fallback format - try to parse JSON first
|
|
3533
3559
|
const result = {};
|
|
3534
|
-
if (
|
|
3560
|
+
if (coercedContent.length > 0) {
|
|
3535
3561
|
try {
|
|
3536
3562
|
// If content is a JSON string, parse it
|
|
3537
|
-
result.output = JSON.parse(
|
|
3563
|
+
result.output = JSON.parse(coercedContent);
|
|
3538
3564
|
}
|
|
3539
3565
|
catch {
|
|
3540
3566
|
// If not JSON, return unchanged (no extra wrapping)
|
|
3541
|
-
result.output =
|
|
3567
|
+
result.output = coercedContent;
|
|
3542
3568
|
}
|
|
3543
3569
|
}
|
|
3544
3570
|
// Preserve file changes tracked during tool execution
|
|
@@ -3899,7 +3925,9 @@ function readCompactionMetadata(source) {
|
|
|
3899
3925
|
const trigger = typeof metadata.trigger === "string" ? metadata.trigger : undefined;
|
|
3900
3926
|
const preTokensRaw = metadata.preTokens ?? metadata.pre_tokens;
|
|
3901
3927
|
const preTokens = typeof preTokensRaw === "number" ? preTokensRaw : undefined;
|
|
3902
|
-
|
|
3928
|
+
const postTokensRaw = metadata.postTokens ?? metadata.post_tokens;
|
|
3929
|
+
const postTokens = typeof postTokensRaw === "number" ? postTokensRaw : undefined;
|
|
3930
|
+
return { trigger, preTokens, postTokens };
|
|
3903
3931
|
}
|
|
3904
3932
|
return null;
|
|
3905
3933
|
}
|
|
@@ -3961,6 +3989,9 @@ function convertClaudeHistoryEntryPreamble(entry) {
|
|
|
3961
3989
|
}
|
|
3962
3990
|
return { proceed: { content } };
|
|
3963
3991
|
}
|
|
3992
|
+
function isProviderImageMessage(item) {
|
|
3993
|
+
return item.type === "assistant_message" && isProviderImageMarkdown(item.text);
|
|
3994
|
+
}
|
|
3964
3995
|
export function convertClaudeHistoryEntry(entry, mapBlocks) {
|
|
3965
3996
|
const preamble = convertClaudeHistoryEntryPreamble(entry);
|
|
3966
3997
|
if ("shortCircuit" in preamble) {
|
|
@@ -3996,7 +4027,10 @@ export function convertClaudeHistoryEntry(entry, mapBlocks) {
|
|
|
3996
4027
|
if (hasToolBlock && normalizedBlocks) {
|
|
3997
4028
|
const mapped = mapBlocks(normalizedBlocks);
|
|
3998
4029
|
if (entry.type === "user") {
|
|
3999
|
-
|
|
4030
|
+
// tool_result handling (handleToolResult) emits image markdown as an assistant_message
|
|
4031
|
+
// alongside the tool_call. User-entry text blocks also map to assistant_message in this path
|
|
4032
|
+
// and must stay suppressed, so keep tool_calls plus only the image assistant_messages.
|
|
4033
|
+
const toolItems = mapped.filter((item) => item.type === "tool_call" || isProviderImageMessage(item));
|
|
4000
4034
|
return timeline.length ? [...timeline, ...toolItems] : toolItems;
|
|
4001
4035
|
}
|
|
4002
4036
|
return mapped;
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { getAgentStreamEventTurnId, } from "../agent-sdk-types.js";
|
|
2
2
|
import { importSessionFromPersistence } from "../provider-session-import.js";
|
|
3
3
|
import { randomUUID } from "node:crypto";
|
|
4
|
-
import * as fsSync from "node:fs";
|
|
5
4
|
import fs from "node:fs/promises";
|
|
6
5
|
import os from "node:os";
|
|
7
6
|
import path from "node:path";
|
|
@@ -18,7 +17,7 @@ import { extractCodexTerminalSessionId, nonEmptyString } from "./tool-call-mappe
|
|
|
18
17
|
import { buildCodexFeatures, codexModelSupportsFastMode } from "./codex-feature-definitions.js";
|
|
19
18
|
import { CodexAppServerClient, parseCodexThreadForkResponse, parseCodexThreadRollbackResponse, } from "./codex/app-server-transport.js";
|
|
20
19
|
import { revertCodexConversation } from "./codex/rewind.js";
|
|
21
|
-
import { renderProviderImageOutputAsAssistantMarkdown, } from "./provider-image-output.js";
|
|
20
|
+
import { materializeProviderImage, renderProviderImageOutputAsAssistantMarkdown, } from "./provider-image-output.js";
|
|
22
21
|
import { normalizeProviderReplayTimestamp } from "../provider-history-timestamps.js";
|
|
23
22
|
import { formatProviderDiagnostic, formatProviderDiagnosticError, buildBinaryDiagnosticRows, buildCommandResolutionDiagnosticRows, resolveBinaryVersion, } from "./diagnostic-utils.js";
|
|
24
23
|
import { runProviderTurn } from "./provider-runner.js";
|
|
@@ -38,7 +37,6 @@ function isCodexAlreadyUnarchivedError(error, threadId) {
|
|
|
38
37
|
const TURN_START_TIMEOUT_MS = 90 * 1000;
|
|
39
38
|
const INTERRUPT_TIMEOUT_MS = 2000;
|
|
40
39
|
const CODEX_PROVIDER = "codex";
|
|
41
|
-
const CODEX_IMAGE_ATTACHMENT_DIR = "paseo-attachments";
|
|
42
40
|
// Codex treats most app-server client names as the model-request originator.
|
|
43
41
|
// This reserved Codex name is non-originating, so requests keep Codex's default
|
|
44
42
|
// CLI identity instead of showing up as Paseo in provider usage logs.
|
|
@@ -1220,21 +1218,6 @@ function codexImageOutputFromResult(result) {
|
|
|
1220
1218
|
mimeType: firstStringField(resultRecord, ["mimeType", "mime_type"]),
|
|
1221
1219
|
};
|
|
1222
1220
|
}
|
|
1223
|
-
function writeImageAttachmentSync(mimeType, data) {
|
|
1224
|
-
const attachmentsDir = path.join(os.tmpdir(), CODEX_IMAGE_ATTACHMENT_DIR);
|
|
1225
|
-
fsSync.mkdirSync(attachmentsDir, { recursive: true });
|
|
1226
|
-
const normalized = normalizeImageData(mimeType, data);
|
|
1227
|
-
const extension = getImageExtension(normalized.mimeType);
|
|
1228
|
-
const filename = `${randomUUID()}.${extension}`;
|
|
1229
|
-
const filePath = path.join(attachmentsDir, filename);
|
|
1230
|
-
fsSync.writeFileSync(filePath, Buffer.from(normalized.data, "base64"));
|
|
1231
|
-
return filePath;
|
|
1232
|
-
}
|
|
1233
|
-
function materializeCodexImageOutput(image) {
|
|
1234
|
-
return {
|
|
1235
|
-
path: writeImageAttachmentSync(image.mimeType ?? "image/png", image.data),
|
|
1236
|
-
};
|
|
1237
|
-
}
|
|
1238
1221
|
function mapCodexThreadImageItem(normalizedType, normalizedItem) {
|
|
1239
1222
|
if (normalizedType === "imageView") {
|
|
1240
1223
|
return renderProviderImageOutputAsAssistantMarkdown({
|
|
@@ -1248,7 +1231,7 @@ function mapCodexThreadImageItem(normalizedType, normalizedItem) {
|
|
|
1248
1231
|
url: result?.url ?? null,
|
|
1249
1232
|
data: result?.data ?? null,
|
|
1250
1233
|
mimeType: result?.mimeType ?? null,
|
|
1251
|
-
}, { materialize:
|
|
1234
|
+
}, { materialize: materializeProviderImage });
|
|
1252
1235
|
}
|
|
1253
1236
|
export function threadItemToTimeline(item, options) {
|
|
1254
1237
|
const itemRecord = toObjectRecord(item);
|
|
@@ -1357,33 +1340,6 @@ function toSandboxPolicy(type, networkAccess) {
|
|
|
1357
1340
|
return { type: "workspaceWrite", networkAccess: networkAccess ?? false };
|
|
1358
1341
|
}
|
|
1359
1342
|
}
|
|
1360
|
-
function getImageExtension(mimeType) {
|
|
1361
|
-
switch (mimeType) {
|
|
1362
|
-
case "image/jpeg":
|
|
1363
|
-
return "jpg";
|
|
1364
|
-
case "image/png":
|
|
1365
|
-
return "png";
|
|
1366
|
-
case "image/webp":
|
|
1367
|
-
return "webp";
|
|
1368
|
-
case "image/gif":
|
|
1369
|
-
return "gif";
|
|
1370
|
-
case "image/bmp":
|
|
1371
|
-
return "bmp";
|
|
1372
|
-
case "image/tiff":
|
|
1373
|
-
return "tiff";
|
|
1374
|
-
default:
|
|
1375
|
-
return "bin";
|
|
1376
|
-
}
|
|
1377
|
-
}
|
|
1378
|
-
function normalizeImageData(mimeType, data) {
|
|
1379
|
-
if (data.startsWith("data:")) {
|
|
1380
|
-
const match = data.match(/^data:([^;]+);base64,(.*)$/);
|
|
1381
|
-
if (match) {
|
|
1382
|
-
return { mimeType: match[1], data: match[2] };
|
|
1383
|
-
}
|
|
1384
|
-
}
|
|
1385
|
-
return { mimeType, data };
|
|
1386
|
-
}
|
|
1387
1343
|
const ThreadStartedNotificationSchema = z
|
|
1388
1344
|
.object({
|
|
1389
1345
|
thread: z.object({ id: z.string() }).passthrough(),
|
|
@@ -2008,16 +1964,6 @@ const CodexNotificationSchema = z.union([
|
|
|
2008
1964
|
.object({ method: z.string(), params: z.unknown() })
|
|
2009
1965
|
.transform(({ method, params }) => ({ kind: "unknown_method", method, params })),
|
|
2010
1966
|
]);
|
|
2011
|
-
async function writeImageAttachment(mimeType, data) {
|
|
2012
|
-
const attachmentsDir = path.join(os.tmpdir(), CODEX_IMAGE_ATTACHMENT_DIR);
|
|
2013
|
-
await fs.mkdir(attachmentsDir, { recursive: true });
|
|
2014
|
-
const normalized = normalizeImageData(mimeType, data);
|
|
2015
|
-
const extension = getImageExtension(normalized.mimeType);
|
|
2016
|
-
const filename = `${randomUUID()}.${extension}`;
|
|
2017
|
-
const filePath = path.join(attachmentsDir, filename);
|
|
2018
|
-
await fs.writeFile(filePath, Buffer.from(normalized.data, "base64"));
|
|
2019
|
-
return filePath;
|
|
2020
|
-
}
|
|
2021
1967
|
async function readCodexConfiguredDefaults(client, logger) {
|
|
2022
1968
|
let savedConfigDefaults = {};
|
|
2023
1969
|
try {
|
|
@@ -2071,7 +2017,10 @@ export async function codexAppServerTurnInputFromPrompt(prompt, logger) {
|
|
|
2071
2017
|
}
|
|
2072
2018
|
if (block.type === "image") {
|
|
2073
2019
|
try {
|
|
2074
|
-
const filePath =
|
|
2020
|
+
const filePath = materializeProviderImage({
|
|
2021
|
+
data: block.data,
|
|
2022
|
+
mimeType: block.mimeType,
|
|
2023
|
+
}).path;
|
|
2075
2024
|
output.push({ type: "localImage", path: filePath });
|
|
2076
2025
|
}
|
|
2077
2026
|
catch (error) {
|
|
@@ -2,8 +2,9 @@ import type { Logger } from "pino";
|
|
|
2
2
|
import type { SessionConfigOption } from "@agentclientprotocol/sdk";
|
|
3
3
|
import type { AgentMode } from "../agent-sdk-types.js";
|
|
4
4
|
import { type ProviderRuntimeSettings } from "../provider-launch-config.js";
|
|
5
|
-
import { ACPAgentClient, type ACPBeforeModeWriteResult, type ACPProviderModeWriteResult, type ACPProviderModeWriterContext, type SessionStateResponse } from "./acp-agent.js";
|
|
5
|
+
import { ACPAgentClient, type ACPConfigFeatureOption, type ACPBeforeModeWriteResult, type ACPProviderModeWriteResult, type ACPProviderModeWriterContext, type SessionStateResponse } from "./acp-agent.js";
|
|
6
6
|
export declare const COPILOT_ALLOW_ALL_MODE_ID = "allow-all";
|
|
7
|
+
export declare const COPILOT_AGENT_FEATURE_OPTION: ACPConfigFeatureOption;
|
|
7
8
|
export declare const COPILOT_MODES: AgentMode[];
|
|
8
9
|
interface CopilotACPAgentClientOptions {
|
|
9
10
|
logger: Logger;
|
|
@@ -20,6 +20,15 @@ export const COPILOT_ALLOW_ALL_MODE_ID = "allow-all";
|
|
|
20
20
|
const COPILOT_ALLOW_ALL_CONFIG_ID = "allow_all";
|
|
21
21
|
const COPILOT_ALLOW_ALL_ON = "on";
|
|
22
22
|
const COPILOT_ALLOW_ALL_OFF = "off";
|
|
23
|
+
export const COPILOT_AGENT_FEATURE_OPTION = {
|
|
24
|
+
id: "agent",
|
|
25
|
+
configId: "agent",
|
|
26
|
+
category: "_agent",
|
|
27
|
+
label: "Agent",
|
|
28
|
+
description: "Use a Copilot custom agent profile",
|
|
29
|
+
tooltip: "Select Copilot agent",
|
|
30
|
+
emptyOptionLabel: "Default",
|
|
31
|
+
};
|
|
23
32
|
export const COPILOT_MODES = [
|
|
24
33
|
{
|
|
25
34
|
id: COPILOT_AGENT_MODE_ID,
|
|
@@ -47,6 +56,7 @@ export class CopilotACPAgentClient extends ACPAgentClient {
|
|
|
47
56
|
defaultModes: COPILOT_MODES,
|
|
48
57
|
sessionResponseTransformer: transformCopilotSessionResponse,
|
|
49
58
|
configOptionsTransformer: transformCopilotConfigOptions,
|
|
59
|
+
configFeatureOptions: [COPILOT_AGENT_FEATURE_OPTION],
|
|
50
60
|
modeIdTransformer: transformCopilotModeId,
|
|
51
61
|
providerModeWriter: writeCopilotProviderMode,
|
|
52
62
|
beforeModeWriter: beforeCopilotModeWriter,
|
|
@@ -10,6 +10,7 @@ export declare function formatDiagnosticStatus(available: boolean, error?: {
|
|
|
10
10
|
source: string;
|
|
11
11
|
cause: unknown;
|
|
12
12
|
}): string;
|
|
13
|
+
export declare function truncateForDiagnostic(value: string): string;
|
|
13
14
|
export declare function toDiagnosticErrorMessage(error: unknown): string;
|
|
14
15
|
export declare function resolveBinaryVersion(binaryPath: string): Promise<string>;
|
|
15
16
|
export interface BinaryDiagnosticVersionCommand {
|
|
@@ -24,7 +24,7 @@ export function formatDiagnosticStatus(available, error) {
|
|
|
24
24
|
return formatAvailabilityStatus(available);
|
|
25
25
|
}
|
|
26
26
|
const DIAGNOSTIC_OUTPUT_CAP = 4096;
|
|
27
|
-
function truncateForDiagnostic(value) {
|
|
27
|
+
export function truncateForDiagnostic(value) {
|
|
28
28
|
const trimmed = value.trim();
|
|
29
29
|
if (trimmed.length <= DIAGNOSTIC_OUTPUT_CAP) {
|
|
30
30
|
return trimmed;
|
|
@@ -13,11 +13,13 @@ interface GenericACPAgentClientOptions {
|
|
|
13
13
|
providerParams?: unknown;
|
|
14
14
|
waitForInitialCommands?: boolean;
|
|
15
15
|
initialCommandsWaitTimeoutMs?: number;
|
|
16
|
+
diagnosticPhaseTimeoutMs?: number;
|
|
16
17
|
}
|
|
17
18
|
export declare class GenericACPAgentClient extends ACPAgentClient {
|
|
18
19
|
private readonly command;
|
|
19
20
|
private readonly providerId?;
|
|
20
21
|
private readonly label?;
|
|
22
|
+
private readonly diagnosticPhaseTimeoutMs?;
|
|
21
23
|
constructor(options: GenericACPAgentClientOptions);
|
|
22
24
|
protected resolveLaunchCommand(): Promise<{
|
|
23
25
|
command: string;
|
|
@@ -28,6 +30,7 @@ export declare class GenericACPAgentClient extends ACPAgentClient {
|
|
|
28
30
|
diagnostic: string;
|
|
29
31
|
}>;
|
|
30
32
|
private resolveConfiguredLaunch;
|
|
33
|
+
private getACPProbeRowsForDiagnostic;
|
|
31
34
|
}
|
|
32
35
|
export interface CommandInvocation {
|
|
33
36
|
command: string;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { checkProviderLaunchAvailable, resolveProviderLaunch } from "../provider-launch-config.js";
|
|
3
3
|
import { ACPAgentClient, DEFAULT_ACP_CAPABILITIES } from "./acp-agent.js";
|
|
4
|
-
import {
|
|
4
|
+
import { buildBinaryDiagnosticRows, formatProviderDiagnostic, toDiagnosticErrorMessage, } from "./diagnostic-utils.js";
|
|
5
5
|
export const GenericACPProviderParamsSchema = z
|
|
6
6
|
.object({
|
|
7
7
|
supportsMcpServers: z.boolean().optional(),
|
|
@@ -23,6 +23,7 @@ export class GenericACPAgentClient extends ACPAgentClient {
|
|
|
23
23
|
this.command = options.command;
|
|
24
24
|
this.providerId = options.providerId;
|
|
25
25
|
this.label = options.label;
|
|
26
|
+
this.diagnosticPhaseTimeoutMs = options.diagnosticPhaseTimeoutMs;
|
|
26
27
|
}
|
|
27
28
|
async resolveLaunchCommand() {
|
|
28
29
|
return {
|
|
@@ -37,34 +38,36 @@ export class GenericACPAgentClient extends ACPAgentClient {
|
|
|
37
38
|
}
|
|
38
39
|
async getDiagnostic() {
|
|
39
40
|
const providerName = formatProviderName(this.label, this.providerId);
|
|
41
|
+
const entries = [
|
|
42
|
+
{ label: "Provider ID", value: this.providerId ?? "unknown" },
|
|
43
|
+
{ label: "Configured command", value: this.command.join(" ") },
|
|
44
|
+
];
|
|
45
|
+
const versionProbe = buildVersionProbeCommand(this.command);
|
|
40
46
|
try {
|
|
41
47
|
const launch = await this.resolveConfiguredLaunch();
|
|
42
48
|
const availability = await checkProviderLaunchAvailable(launch);
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
command: versionProbe.command,
|
|
52
|
-
args: versionProbe.args,
|
|
53
|
-
env: this.runtimeSettings?.env,
|
|
54
|
-
},
|
|
55
|
-
})),
|
|
56
|
-
{
|
|
57
|
-
label: "Version command",
|
|
58
|
-
value: formatCommand(versionProbe.command, versionProbe.args),
|
|
59
|
-
},
|
|
60
|
-
]),
|
|
61
|
-
};
|
|
49
|
+
entries.push(...(await buildBinaryDiagnosticRows(launch, availability, {
|
|
50
|
+
binaryLabel: "Launcher binary",
|
|
51
|
+
versionCommand: {
|
|
52
|
+
command: versionProbe.command,
|
|
53
|
+
args: versionProbe.args,
|
|
54
|
+
env: this.runtimeSettings?.env,
|
|
55
|
+
},
|
|
56
|
+
})));
|
|
62
57
|
}
|
|
63
58
|
catch (error) {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
59
|
+
entries.push({
|
|
60
|
+
label: "Launcher binary",
|
|
61
|
+
value: `error: ${toDiagnosticErrorMessage(error)}`,
|
|
62
|
+
});
|
|
67
63
|
}
|
|
64
|
+
entries.push({
|
|
65
|
+
label: "Version command",
|
|
66
|
+
value: formatCommand(versionProbe.command, versionProbe.args),
|
|
67
|
+
}, ...(await this.getACPProbeRowsForDiagnostic()));
|
|
68
|
+
return {
|
|
69
|
+
diagnostic: formatProviderDiagnostic(providerName, entries),
|
|
70
|
+
};
|
|
68
71
|
}
|
|
69
72
|
async resolveConfiguredLaunch() {
|
|
70
73
|
return resolveProviderLaunch({
|
|
@@ -72,6 +75,21 @@ export class GenericACPAgentClient extends ACPAgentClient {
|
|
|
72
75
|
defaultBinary: this.command[0],
|
|
73
76
|
});
|
|
74
77
|
}
|
|
78
|
+
async getACPProbeRowsForDiagnostic() {
|
|
79
|
+
try {
|
|
80
|
+
return await this.buildACPProbeDiagnosticRows({
|
|
81
|
+
phaseTimeoutMs: this.diagnosticPhaseTimeoutMs,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
catch (error) {
|
|
85
|
+
return [
|
|
86
|
+
{
|
|
87
|
+
label: "ACP probe",
|
|
88
|
+
value: `error: ${toDiagnosticErrorMessage(error)}`,
|
|
89
|
+
},
|
|
90
|
+
];
|
|
91
|
+
}
|
|
92
|
+
}
|
|
75
93
|
}
|
|
76
94
|
function buildGenericACPCapabilities(options) {
|
|
77
95
|
const params = parseGenericACPProviderParams(options.providerParams);
|
|
@@ -167,7 +167,7 @@ function parseAgentStreamStressPrompt(prompt) {
|
|
|
167
167
|
}
|
|
168
168
|
function parseStructuredBranchNamePrompt(prompt) {
|
|
169
169
|
const text = promptToText(prompt);
|
|
170
|
-
const hasBranchNamePrompt = text.includes("Generate a git branch name for a coding agent") &&
|
|
170
|
+
const hasBranchNamePrompt = text.includes("Generate a title and a git branch name for a coding agent") &&
|
|
171
171
|
(text.includes("Return JSON only with fields 'title' and 'branch'.") ||
|
|
172
172
|
text.includes('"title"') ||
|
|
173
173
|
text.includes('"branch"'));
|
|
@@ -177,7 +177,9 @@ function parseStructuredBranchNamePrompt(prompt) {
|
|
|
177
177
|
text.includes('"branch"'))) {
|
|
178
178
|
return null;
|
|
179
179
|
}
|
|
180
|
-
const seed = text.
|
|
180
|
+
const seed = text.match(/<user-prompt>\n([\s\S]*?)\n<\/user-prompt>/)?.[1]?.trim() ??
|
|
181
|
+
text.match(/<attachments>\n([\s\S]*?)\n<\/attachments>/)?.[1]?.trim() ??
|
|
182
|
+
"";
|
|
181
183
|
const firstLine = seed
|
|
182
184
|
.split("\n")
|
|
183
185
|
.find((line) => line.trim().length > 0)
|