@cargo-ai/cli 1.0.85 → 1.0.87
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/build/commands/ai/mcpServer.d.ts.map +1 -1
- package/build/commands/ai/mcpServer.js +14 -2
- package/build/commands/ask/localHarness.d.ts +39 -0
- package/build/commands/ask/localHarness.d.ts.map +1 -1
- package/build/commands/ask/localHarness.js +166 -25
- package/build/commands/ask/session.d.ts.map +1 -1
- package/build/commands/ask/session.js +72 -11
- package/package.json +4 -4
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mcpServer.d.ts","sourceRoot":"","sources":["../../../src/commands/ai/mcpServer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEzC,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,cAAc,CAAC;AAGxC,wBAAgB,yBAAyB,CACvC,MAAM,EAAE,OAAO,EACf,MAAM,EAAE,MAAM,GAAG,GAChB,IAAI,
|
|
1
|
+
{"version":3,"file":"mcpServer.d.ts","sourceRoot":"","sources":["../../../src/commands/ai/mcpServer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEzC,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,cAAc,CAAC;AAGxC,wBAAgB,yBAAyB,CACvC,MAAM,EAAE,OAAO,EACf,MAAM,EAAE,MAAM,GAAG,GAChB,IAAI,CAgJN"}
|
|
@@ -25,19 +25,24 @@ export function registerMcpServerCommands(parent, getApi) {
|
|
|
25
25
|
.requiredOption("--name <name>", "MCP server name (string, required)")
|
|
26
26
|
.option("--actions <json>", "Actions to expose (JSON array of action objects, default: [])")
|
|
27
27
|
.option("--resources <json>", "Resources to expose (JSON array of resource objects, default: [])")
|
|
28
|
+
.option("--capabilities <json>", "Capabilities to expose (JSON array of capability objects, default: [])")
|
|
28
29
|
.addHelpText("after", `
|
|
29
30
|
Examples:
|
|
30
31
|
$ cargo-ai ai mcp-server create --name "My Server"
|
|
31
32
|
$ cargo-ai ai mcp-server create --name "My Server" \\
|
|
32
33
|
--actions '[{"slug":"my-action","kind":"tool","name":null,"description":null,"isBulkAllowed":false,"config":{}}]' \\
|
|
33
|
-
--resources '[{"kind":"model","slug":"my-resource","name":"My Resource","description":null,"integrationSlug":"hubspot","modelUuid":null,"filter":null,"selectedColumnSlugs":null,"limit":null,"isReadOnly":true}]'
|
|
34
|
+
--resources '[{"kind":"model","slug":"my-resource","name":"My Resource","description":null,"integrationSlug":"hubspot","modelUuid":null,"filter":null,"selectedColumnSlugs":null,"limit":null,"isReadOnly":true}]' \\
|
|
35
|
+
--capabilities '[{"slug":"webSearch","config":{}}]'
|
|
34
36
|
|
|
35
37
|
Action object shape:
|
|
36
38
|
{ "slug": string, "kind": "tool", "name": string | null, "description": string | null, "isBulkAllowed": boolean, "config": object }
|
|
37
39
|
|
|
38
40
|
Resource object shapes:
|
|
39
41
|
Model: { "kind": "model", "slug": string, "name": string, "description": string | null, "integrationSlug": string, "modelUuid": string | null, "filter": object | null, "selectedColumnSlugs": string[] | null, "limit": number | null, "isReadOnly": boolean }
|
|
40
|
-
File: { "kind": "file", "slug": string, "name": string, "description": string | null, "items": [{"kind":"file","fileUuid": string}] }
|
|
42
|
+
File: { "kind": "file", "slug": string, "name": string, "description": string | null, "items": [{"kind":"file","fileUuid": string}] }
|
|
43
|
+
|
|
44
|
+
Capability object shape:
|
|
45
|
+
{ "slug": "webSearch" | "context" | …, "config": object }`)
|
|
41
46
|
.action(async (opts) => {
|
|
42
47
|
const api = getApi();
|
|
43
48
|
const result = await handleApiCall(() => api.ai.mcpServer.create({
|
|
@@ -48,6 +53,9 @@ Resource object shapes:
|
|
|
48
53
|
resources: opts.resources !== undefined
|
|
49
54
|
? parseJson(opts.resources, "--resources")
|
|
50
55
|
: [],
|
|
56
|
+
capabilities: opts.capabilities !== undefined
|
|
57
|
+
? parseJson(opts.capabilities, "--capabilities")
|
|
58
|
+
: [],
|
|
51
59
|
}));
|
|
52
60
|
outputJson(result);
|
|
53
61
|
});
|
|
@@ -66,6 +74,7 @@ Resource object shapes:
|
|
|
66
74
|
.option("--name <name>", "MCP server name (string)")
|
|
67
75
|
.option("--actions <json>", "Actions to expose (JSON array of action objects, same format as 'mcp-server create')")
|
|
68
76
|
.option("--resources <json>", "Resources to expose (JSON array of resource objects, same format as 'mcp-server create')")
|
|
77
|
+
.option("--capabilities <json>", "Capabilities to expose (JSON array of capability objects, same format as 'mcp-server create')")
|
|
69
78
|
.action(async (opts) => {
|
|
70
79
|
const api = getApi();
|
|
71
80
|
const result = await handleApiCall(() => api.ai.mcpServer.update({
|
|
@@ -77,6 +86,9 @@ Resource object shapes:
|
|
|
77
86
|
resources: opts.resources !== undefined
|
|
78
87
|
? parseJson(opts.resources, "--resources")
|
|
79
88
|
: undefined,
|
|
89
|
+
capabilities: opts.capabilities !== undefined
|
|
90
|
+
? parseJson(opts.capabilities, "--capabilities")
|
|
91
|
+
: undefined,
|
|
80
92
|
}));
|
|
81
93
|
outputJson(result);
|
|
82
94
|
});
|
|
@@ -25,12 +25,26 @@ export type LocalToolApproval = {
|
|
|
25
25
|
/** A tool starting or finishing, so the caller can show what is happening. */
|
|
26
26
|
export type LocalToolActivity = {
|
|
27
27
|
kind: "start";
|
|
28
|
+
toolCallId: string;
|
|
28
29
|
toolName: string;
|
|
29
30
|
summary: string;
|
|
30
31
|
} | {
|
|
31
32
|
kind: "end";
|
|
33
|
+
toolCallId: string;
|
|
32
34
|
toolName: string;
|
|
33
35
|
isError: boolean;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Announced, then never run — a tool whose input stopped mid-stream, one
|
|
39
|
+
* that was refused, or a sibling whose approval request the harness lost.
|
|
40
|
+
* None of them report a result, so `end` never comes: a caller tracking
|
|
41
|
+
* what is live has to be told, or it shows that tool for the rest of the
|
|
42
|
+
* turn. `retractAbandonedCalls` decides which calls qualify and when.
|
|
43
|
+
*/
|
|
44
|
+
| {
|
|
45
|
+
kind: "dropped";
|
|
46
|
+
toolCallId: string;
|
|
47
|
+
toolName: string;
|
|
34
48
|
};
|
|
35
49
|
export type LocalStreamHandlers = {
|
|
36
50
|
onTextDelta?: (delta: string) => void;
|
|
@@ -49,6 +63,12 @@ export type LocalStreamHandlers = {
|
|
|
49
63
|
export type LocalStreamOptions = LocalStreamHandlers & {
|
|
50
64
|
/** Aborts the turn; the harness interrupts Claude Code gracefully. */
|
|
51
65
|
abortSignal?: AbortSignal;
|
|
66
|
+
/**
|
|
67
|
+
* Fired when the stall watchdog gives up, before the turn is rejected.
|
|
68
|
+
* Should abort `abortSignal` so Claude Code stops rather than being torn
|
|
69
|
+
* down out from under a still-open bridge.
|
|
70
|
+
*/
|
|
71
|
+
onStall?: () => void;
|
|
52
72
|
};
|
|
53
73
|
/**
|
|
54
74
|
* What one turn spent upstream, summed over the streams a suspended turn is
|
|
@@ -197,6 +217,11 @@ export declare function runPromptTurn(input: {
|
|
|
197
217
|
unanswered?: LocalToolApproval[];
|
|
198
218
|
/** Overridden by tests; `Infinity` waits forever. */
|
|
199
219
|
stallTimeoutMs?: number;
|
|
220
|
+
/**
|
|
221
|
+
* Called when the stall watchdog fires, before the turn is rejected — abort
|
|
222
|
+
* the harness stream so Claude Code does not keep running after we give up.
|
|
223
|
+
*/
|
|
224
|
+
onStall?: () => void;
|
|
200
225
|
}): Promise<string>;
|
|
201
226
|
/**
|
|
202
227
|
* The previous turn could not be wound down, so this session cannot take
|
|
@@ -209,6 +234,20 @@ export declare function runPromptTurn(input: {
|
|
|
209
234
|
export declare class HarnessTurnStuck extends Error {
|
|
210
235
|
constructor();
|
|
211
236
|
}
|
|
237
|
+
/**
|
|
238
|
+
* The turn produced nothing for `TURN_STALL_TIMEOUT_MS` while no tool was in
|
|
239
|
+
* flight. Common causes: a step that asked to run several confirmable tools at
|
|
240
|
+
* once (the harness only surfaces one approval), or the Claude Code bridge
|
|
241
|
+
* going silent. Recoverable — replace the session and ask again.
|
|
242
|
+
*/
|
|
243
|
+
export declare class HarnessTurnStalled extends Error {
|
|
244
|
+
constructor(timeoutMs: number);
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* The Claude Code bridge dropped mid-turn. Same recovery as a stall: the
|
|
248
|
+
* process behind the session is gone, so the next question needs a new one.
|
|
249
|
+
*/
|
|
250
|
+
export declare function isBridgeClosedError(error: unknown): boolean;
|
|
212
251
|
/**
|
|
213
252
|
* Wait for `work`, but give up after `timeoutMs` rather than blocking a
|
|
214
253
|
* teardown forever. Resolves either way; the caller has already decided that
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"localHarness.d.ts","sourceRoot":"","sources":["../../../src/commands/ask/localHarness.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,cAAc,CAAC;AAuGxC;;;GAGG;AACH,wBAAgB,wBAAwB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAM5D;AAED,0BAA0B;AAC1B,wBAAgB,kBAAkB,CAChC,GAAG,EAAE,MAAM,GACV,KAAK,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC,CAM/D;AAED;;;GAGG;AACH,MAAM,MAAM,iBAAiB,GAAG;IAC9B,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,oEAAoE;IACpE,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,OAAO,CAAC;CAChB,CAAC;AAEF,8EAA8E;AAC9E,MAAM,MAAM,iBAAiB,GACzB;
|
|
1
|
+
{"version":3,"file":"localHarness.d.ts","sourceRoot":"","sources":["../../../src/commands/ask/localHarness.ts"],"names":[],"mappings":"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"}
|
|
@@ -395,6 +395,11 @@ export async function createLocalHarness(input) {
|
|
|
395
395
|
handlers: options,
|
|
396
396
|
accumulator,
|
|
397
397
|
unanswered,
|
|
398
|
+
// Interrupt Claude Code through the turn's abort signal so the
|
|
399
|
+
// bridge stops cleanly. Without this the host walks away from the
|
|
400
|
+
// stream and teardown looks like "bridge closed before the turn
|
|
401
|
+
// finished" rather than an intentional stop.
|
|
402
|
+
onStall: options === undefined ? undefined : options.onStall,
|
|
398
403
|
});
|
|
399
404
|
}
|
|
400
405
|
catch (error) {
|
|
@@ -524,7 +529,7 @@ export async function runPromptTurn(input) {
|
|
|
524
529
|
const toolCalls = new Map();
|
|
525
530
|
const turnState = {
|
|
526
531
|
toolRanSinceText: false,
|
|
527
|
-
|
|
532
|
+
announced: [],
|
|
528
533
|
};
|
|
529
534
|
let stream = await input.driver.start();
|
|
530
535
|
let round = 0;
|
|
@@ -537,6 +542,7 @@ export async function runPromptTurn(input) {
|
|
|
537
542
|
usage: accumulator.usage,
|
|
538
543
|
turnState,
|
|
539
544
|
stallTimeoutMs,
|
|
545
|
+
onStall: input.onStall,
|
|
540
546
|
});
|
|
541
547
|
if (approvals.length === 0) {
|
|
542
548
|
break;
|
|
@@ -549,7 +555,6 @@ export async function runPromptTurn(input) {
|
|
|
549
555
|
throw new Error(`Local Claude Code asked for tool approval ${String(MAX_APPROVAL_ROUNDS)} times in one turn without finishing. Stopping the turn.`);
|
|
550
556
|
}
|
|
551
557
|
const continuations = [];
|
|
552
|
-
const approvedToolCallIds = [];
|
|
553
558
|
for (const approval of approvals) {
|
|
554
559
|
// Refused before the user is asked: not their call to make, and a
|
|
555
560
|
// prompt for it could not say what it was granting.
|
|
@@ -561,8 +566,13 @@ export async function runPromptTurn(input) {
|
|
|
561
566
|
: onApproval === undefined
|
|
562
567
|
? false
|
|
563
568
|
: await onApproval(approval);
|
|
564
|
-
|
|
565
|
-
|
|
569
|
+
// Recorded against the call so the retraction pass below knows it is
|
|
570
|
+
// settled: an approved one is running and must be left alone, a refused
|
|
571
|
+
// one is certainly not and needs no reprieve to find out. Absent when
|
|
572
|
+
// its result already arrived, which takes it off the list.
|
|
573
|
+
const announced = turnState.announced.find((call) => call.toolCallId === approval.toolCallId);
|
|
574
|
+
if (announced !== undefined) {
|
|
575
|
+
announced.status = approved === true ? "approved" : "refused";
|
|
566
576
|
}
|
|
567
577
|
// Answered, so the drain on a later turn must not deny it again.
|
|
568
578
|
if (input.unanswered !== undefined) {
|
|
@@ -588,15 +598,7 @@ export async function runPromptTurn(input) {
|
|
|
588
598
|
},
|
|
589
599
|
});
|
|
590
600
|
}
|
|
591
|
-
|
|
592
|
-
// surfaces one approval per stream, so a sibling the model announced in
|
|
593
|
-
// the same stream was never asked about and will never report a result —
|
|
594
|
-
// left in the set it would tell the stall watchdog a tool is in flight
|
|
595
|
-
// for the rest of the turn, turning a silent stream into a hang.
|
|
596
|
-
turnState.inFlightToolCallIds.clear();
|
|
597
|
-
for (const toolCallId of approvedToolCallIds) {
|
|
598
|
-
turnState.inFlightToolCallIds.add(toolCallId);
|
|
599
|
-
}
|
|
601
|
+
retractAbandonedCalls(turnState.announced, handlers.onActivity);
|
|
600
602
|
stream = await input.driver.continueWithApprovals(continuations);
|
|
601
603
|
}
|
|
602
604
|
const streamed = assistantChunks.join("");
|
|
@@ -612,6 +614,53 @@ export async function runPromptTurn(input) {
|
|
|
612
614
|
}
|
|
613
615
|
return fallback;
|
|
614
616
|
}
|
|
617
|
+
/**
|
|
618
|
+
* Retract the calls the caller was told about and will never hear the end of,
|
|
619
|
+
* at the boundary where the answers to this round's approvals are known.
|
|
620
|
+
*
|
|
621
|
+
* Left on screen instead, an abandoned call outlives the work: no result is
|
|
622
|
+
* coming, so no `end` event is either, and the caller keeps showing a tool
|
|
623
|
+
* that stopped existing.
|
|
624
|
+
*
|
|
625
|
+
* `"approved"` is kept, and `"running"` is the one case that cannot be judged
|
|
626
|
+
* yet — the reason this is not a plain sweep. It is either the sibling whose
|
|
627
|
+
* approval request the harness lost (it surfaces one per stream) or a tool
|
|
628
|
+
* Claude Code ran without asking, whose result has simply not arrived. Both
|
|
629
|
+
* look identical here, so it gets one more stream to prove itself: a live one
|
|
630
|
+
* reports its result and leaves this list on its own.
|
|
631
|
+
*
|
|
632
|
+
* Everything else is certain and retracted — `"streaming"` never finished
|
|
633
|
+
* arriving, `"refused"` was told no, and `"abandoning"` already had its
|
|
634
|
+
* stream and stayed silent. Refusals matter especially: a tool
|
|
635
|
+
* `isAlwaysDenied` refuses never reaches `onApproval`, so this is the caller's
|
|
636
|
+
* only notice.
|
|
637
|
+
*
|
|
638
|
+
* The reprieve is for the caller's benefit only — `"abandoning"` is not an
|
|
639
|
+
* executing status, so the stall watchdog is armed throughout it. Extending
|
|
640
|
+
* the reprieve to the watchdog would deadlock the very hang it exists to
|
|
641
|
+
* prevent: a dropped sibling would hold the timer off, the stream would stay
|
|
642
|
+
* silent, and the next boundary, the thing that resolves the ambiguity, would
|
|
643
|
+
* never arrive.
|
|
644
|
+
*/
|
|
645
|
+
function retractAbandonedCalls(announced, onActivity) {
|
|
646
|
+
for (const call of [...announced]) {
|
|
647
|
+
if (call.status === "approved") {
|
|
648
|
+
continue;
|
|
649
|
+
}
|
|
650
|
+
if (call.status === "running") {
|
|
651
|
+
call.status = "abandoning";
|
|
652
|
+
continue;
|
|
653
|
+
}
|
|
654
|
+
announced.splice(announced.indexOf(call), 1);
|
|
655
|
+
if (onActivity !== undefined) {
|
|
656
|
+
onActivity({
|
|
657
|
+
kind: "dropped",
|
|
658
|
+
toolCallId: call.toolCallId,
|
|
659
|
+
toolName: call.toolName,
|
|
660
|
+
});
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
}
|
|
615
664
|
/**
|
|
616
665
|
* How long a turn may produce nothing at all while no tool is running.
|
|
617
666
|
*
|
|
@@ -644,6 +693,27 @@ export class HarnessTurnStuck extends Error {
|
|
|
644
693
|
this.name = "HarnessTurnStuck";
|
|
645
694
|
}
|
|
646
695
|
}
|
|
696
|
+
/**
|
|
697
|
+
* The turn produced nothing for `TURN_STALL_TIMEOUT_MS` while no tool was in
|
|
698
|
+
* flight. Common causes: a step that asked to run several confirmable tools at
|
|
699
|
+
* once (the harness only surfaces one approval), or the Claude Code bridge
|
|
700
|
+
* going silent. Recoverable — replace the session and ask again.
|
|
701
|
+
*/
|
|
702
|
+
export class HarnessTurnStalled extends Error {
|
|
703
|
+
constructor(timeoutMs) {
|
|
704
|
+
const seconds = String(Math.round(timeoutMs / 1000));
|
|
705
|
+
super(`Local Claude Code went quiet for ${seconds}s with no tool running. Often this is a step that asked to run several confirmable tools at once (only the first can be answered), or the coding session itself stalled.`);
|
|
706
|
+
this.name = "HarnessTurnStalled";
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
/**
|
|
710
|
+
* The Claude Code bridge dropped mid-turn. Same recovery as a stall: the
|
|
711
|
+
* process behind the session is gone, so the next question needs a new one.
|
|
712
|
+
*/
|
|
713
|
+
export function isBridgeClosedError(error) {
|
|
714
|
+
return (error instanceof Error &&
|
|
715
|
+
error.message.includes("bridge closed before the turn finished") === true);
|
|
716
|
+
}
|
|
647
717
|
/**
|
|
648
718
|
* Fail a turn that has gone silent with no tool running, instead of hanging.
|
|
649
719
|
*
|
|
@@ -657,7 +727,7 @@ export class HarnessTurnStuck extends Error {
|
|
|
657
727
|
* The timer is armed only while no tool is in flight — a ten-minute test run
|
|
658
728
|
* is silent too, and must not be cut off.
|
|
659
729
|
*/
|
|
660
|
-
async function* withStallTimeout(stream, timeoutMs, isToolInFlight) {
|
|
730
|
+
async function* withStallTimeout(stream, timeoutMs, isToolInFlight, onStall) {
|
|
661
731
|
if (Number.isFinite(timeoutMs) === false) {
|
|
662
732
|
for await (const part of stream) {
|
|
663
733
|
yield part;
|
|
@@ -669,7 +739,7 @@ async function* withStallTimeout(stream, timeoutMs, isToolInFlight) {
|
|
|
669
739
|
const next = iterator.next();
|
|
670
740
|
const result = await (isToolInFlight() === true
|
|
671
741
|
? next
|
|
672
|
-
: rejectIfIdle(next, timeoutMs));
|
|
742
|
+
: rejectIfIdle(next, timeoutMs, onStall));
|
|
673
743
|
if (result.done === true) {
|
|
674
744
|
return;
|
|
675
745
|
}
|
|
@@ -699,14 +769,22 @@ export async function settleWithin(work, timeoutMs) {
|
|
|
699
769
|
}
|
|
700
770
|
}
|
|
701
771
|
}
|
|
702
|
-
async function rejectIfIdle(next, timeoutMs) {
|
|
772
|
+
async function rejectIfIdle(next, timeoutMs, onStall) {
|
|
703
773
|
let timer;
|
|
704
774
|
try {
|
|
705
775
|
return await Promise.race([
|
|
706
776
|
next,
|
|
707
777
|
new Promise((_resolve, reject) => {
|
|
708
778
|
timer = setTimeout(() => {
|
|
709
|
-
|
|
779
|
+
// Reject first so Promise.race settles on HarnessTurnStalled, not
|
|
780
|
+
// on an AbortError the interrupt below may race onto `next`.
|
|
781
|
+
reject(new HarnessTurnStalled(timeoutMs));
|
|
782
|
+
// Then stop the bridge so teardown is an intentional abort, not a
|
|
783
|
+
// mysterious "bridge closed before the turn finished" after we
|
|
784
|
+
// have already walked away from the stream.
|
|
785
|
+
if (onStall !== undefined) {
|
|
786
|
+
onStall();
|
|
787
|
+
}
|
|
710
788
|
}, timeoutMs);
|
|
711
789
|
}),
|
|
712
790
|
]);
|
|
@@ -794,7 +872,7 @@ async function consumeTurn(input) {
|
|
|
794
872
|
const approvals = [];
|
|
795
873
|
const { handlers } = input;
|
|
796
874
|
const pendingFiles = new Map();
|
|
797
|
-
const parts = withStallTimeout(input.stream.fullStream, input.stallTimeoutMs, () => input.turnState.
|
|
875
|
+
const parts = withStallTimeout(input.stream.fullStream, input.stallTimeoutMs, () => input.turnState.announced.some((call) => call.status === "running" || call.status === "approved"), input.onStall);
|
|
798
876
|
for await (const rawPart of parts) {
|
|
799
877
|
if (typeof rawPart !== "object" || rawPart === null) {
|
|
800
878
|
continue;
|
|
@@ -808,6 +886,43 @@ async function consumeTurn(input) {
|
|
|
808
886
|
? error
|
|
809
887
|
: "Local Claude Code turn failed.");
|
|
810
888
|
}
|
|
889
|
+
// Claude Code streams the tool name before the full call is assembled.
|
|
890
|
+
// Surface that early so the spinner is not stuck on "Asking…" while the
|
|
891
|
+
// input is still arriving — and so a later `tool-call` can fill in the
|
|
892
|
+
// summary without looking like a second tool.
|
|
893
|
+
if (part["type"] === "tool-input-start") {
|
|
894
|
+
const toolCallId = part["id"];
|
|
895
|
+
const toolName = part["toolName"];
|
|
896
|
+
if (typeof toolCallId === "string" && typeof toolName === "string") {
|
|
897
|
+
if (input.toolCalls.has(toolCallId) === false) {
|
|
898
|
+
input.toolCalls.set(toolCallId, {
|
|
899
|
+
toolName,
|
|
900
|
+
input: undefined,
|
|
901
|
+
});
|
|
902
|
+
input.turnState.toolRanSinceText = true;
|
|
903
|
+
// Announced, because the spinner is about to show this name and is
|
|
904
|
+
// owed a retraction if the call never happens — but `"streaming"`,
|
|
905
|
+
// not executing. The tool is not running yet, its input is still
|
|
906
|
+
// being generated, and the deltas that carry it are output the
|
|
907
|
+
// watchdog is entitled to expect; standing it down here would turn a
|
|
908
|
+
// bridge that dies mid-input into a hang.
|
|
909
|
+
input.turnState.announced.push({
|
|
910
|
+
toolCallId,
|
|
911
|
+
toolName,
|
|
912
|
+
status: "streaming",
|
|
913
|
+
});
|
|
914
|
+
if (handlers.onActivity !== undefined) {
|
|
915
|
+
handlers.onActivity({
|
|
916
|
+
kind: "start",
|
|
917
|
+
toolCallId,
|
|
918
|
+
toolName,
|
|
919
|
+
summary: "",
|
|
920
|
+
});
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
continue;
|
|
925
|
+
}
|
|
811
926
|
if (part["type"] === "tool-call") {
|
|
812
927
|
const toolCallId = part["toolCallId"];
|
|
813
928
|
const toolName = part["toolName"];
|
|
@@ -817,10 +932,26 @@ async function consumeTurn(input) {
|
|
|
817
932
|
input: part["input"],
|
|
818
933
|
});
|
|
819
934
|
input.turnState.toolRanSinceText = true;
|
|
820
|
-
|
|
935
|
+
// The full call is assembled, so something is executing it — that is
|
|
936
|
+
// what stands the stall watchdog down, and it deliberately waits until
|
|
937
|
+
// here rather than firing at `tool-input-start`.
|
|
938
|
+
const announced = input.turnState.announced.find((call) => call.toolCallId === toolCallId);
|
|
939
|
+
if (announced === undefined) {
|
|
940
|
+
input.turnState.announced.push({
|
|
941
|
+
toolCallId,
|
|
942
|
+
toolName,
|
|
943
|
+
status: "running",
|
|
944
|
+
});
|
|
945
|
+
}
|
|
946
|
+
else {
|
|
947
|
+
announced.status = "running";
|
|
948
|
+
}
|
|
949
|
+
// Re-emitted when `tool-input-start` already announced the name, so
|
|
950
|
+
// the spinner can pick up the input summary once it is known.
|
|
821
951
|
if (handlers.onActivity !== undefined) {
|
|
822
952
|
handlers.onActivity({
|
|
823
953
|
kind: "start",
|
|
954
|
+
toolCallId,
|
|
824
955
|
toolName,
|
|
825
956
|
summary: summarizeToolInput(part["input"]),
|
|
826
957
|
});
|
|
@@ -842,18 +973,28 @@ async function consumeTurn(input) {
|
|
|
842
973
|
? input.toolCalls.get(toolCallId)
|
|
843
974
|
: undefined;
|
|
844
975
|
const isError = part["type"] === "tool-error";
|
|
845
|
-
|
|
976
|
+
// A preliminary result is progress from a tool that is still running,
|
|
977
|
+
// so it must not clear the spinner or tell the stall watchdog the tool
|
|
978
|
+
// is done.
|
|
979
|
+
const preliminary = part["preliminary"] === true;
|
|
980
|
+
if (preliminary === false &&
|
|
981
|
+
typeof toolCallId === "string" &&
|
|
982
|
+
handlers.onActivity !== undefined) {
|
|
846
983
|
handlers.onActivity({
|
|
847
984
|
kind: "end",
|
|
985
|
+
toolCallId,
|
|
848
986
|
toolName: recorded === undefined ? "tool" : recorded.toolName,
|
|
849
987
|
isError,
|
|
850
988
|
});
|
|
851
989
|
}
|
|
852
|
-
if (typeof toolCallId === "string") {
|
|
853
|
-
//
|
|
854
|
-
// so
|
|
855
|
-
|
|
856
|
-
|
|
990
|
+
if (typeof toolCallId === "string" && preliminary === false) {
|
|
991
|
+
// Off the list on the way out. The `end` above is the caller's
|
|
992
|
+
// retraction, so a later approval round must not announce a second
|
|
993
|
+
// one — and leaving under its own steam is how a tool that ran unasked
|
|
994
|
+
// proves it was alive rather than abandoned.
|
|
995
|
+
const at = input.turnState.announced.findIndex((call) => call.toolCallId === toolCallId);
|
|
996
|
+
if (at !== -1) {
|
|
997
|
+
input.turnState.announced.splice(at, 1);
|
|
857
998
|
}
|
|
858
999
|
const touched = pendingFiles.get(toolCallId);
|
|
859
1000
|
pendingFiles.delete(toolCallId);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session.d.ts","sourceRoot":"","sources":["../../../src/commands/ask/session.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,eAAe,CAAC;AAIxC,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,cAAc,CAAC;
|
|
1
|
+
{"version":3,"file":"session.d.ts","sourceRoot":"","sources":["../../../src/commands/ask/session.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,eAAe,CAAC;AAIxC,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,cAAc,CAAC;AA4BxC,MAAM,MAAM,iBAAiB,GAAG;IAC9B,GAAG,EAAE,GAAG,CAAC;IACT,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,OAAO,CAAC;IACtB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,GAAG,EAAE,MAAM,CAAC;CACb,CAAC;AAEF,KAAK,WAAW,GAAG;IAAE,IAAI,EAAE,QAAQ,CAAA;CAAE,GAAG;IAAE,IAAI,EAAE,OAAO,CAAA;CAAE,CAAC;AAoB1D;;;;;;;;;;;GAWG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE;IAC1C,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,MAAM,GAAG,SAAS,CAAC;IACjC,KAAK,EAAE,MAAM,CAAC;CACf,GAAG,MAAM,CAKT;AAiBD;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,GAAG,SAAS,GAAG,WAAW,CAErE;AA8ED;;;;;;;;;GASG;AACH;;;;GAIG;AACH,KAAK,aAAa,GACd;IAAE,OAAO,EAAE,SAAS,CAAA;CAAE,GACtB;IAAE,OAAO,EAAE,MAAM,CAAA;CAAE,GACnB;IAAE,OAAO,EAAE,aAAa,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE,CAAC;AAE/C;;;;GAIG;AACH,wBAAsB,aAAa,CACjC,GAAG,EAAE,GAAG,EACR,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,MAAM,GACd,OAAO,CAAC,aAAa,CAAC,CAkBxB;AAyCD,wBAAsB,aAAa,CACjC,GAAG,EAAE,GAAG,EACR,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,OAAO,CAAC,CAqBlB;AA2SD;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAQ7D;AAkGD;;;;;;GAMG;AACH,wBAAsB,aAAa,CACjC,OAAO,EAAE,iBAAiB,GACzB,OAAO,CAAC,SAAS,GAAG,MAAM,CAAC,CAygB7B;AAED,wBAAsB,WAAW,CAAC,KAAK,EAAE;IACvC,GAAG,EAAE,GAAG,CAAC;IACT,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,GAAG,EAAE,MAAM,CAAC;CACb,GAAG,OAAO,CAAC,IAAI,CAAC,CA8HhB"}
|
|
@@ -6,7 +6,7 @@ import { getConfig } from "../../config.js";
|
|
|
6
6
|
import { openPromptInput } from "../../utils/prompt.js";
|
|
7
7
|
import { colors, ExitCodes, failWith, handleApiCall, outputJson, pollUntilFinished, startSpinner, } from "../runHandler.js";
|
|
8
8
|
import { loadChatHistoryPreamble } from "./history.js";
|
|
9
|
-
import { createLocalHarness, HarnessTurnStuck, } from "./localHarness.js";
|
|
9
|
+
import { createLocalHarness, HarnessTurnStalled, HarnessTurnStuck, isBridgeClosedError, } from "./localHarness.js";
|
|
10
10
|
import { streamMessageTextDeltas } from "./messageStream.js";
|
|
11
11
|
import { saveAskState } from "./persist.js";
|
|
12
12
|
import { readAskLine } from "./prompt.js";
|
|
@@ -374,6 +374,9 @@ function beginCancellableTurn(onCancel) {
|
|
|
374
374
|
return {
|
|
375
375
|
signal: controller.signal,
|
|
376
376
|
wasCancelled: () => state.cancelled,
|
|
377
|
+
abort: () => {
|
|
378
|
+
controller.abort();
|
|
379
|
+
},
|
|
377
380
|
release: () => {
|
|
378
381
|
process.removeListener("SIGINT", handleInterrupt);
|
|
379
382
|
},
|
|
@@ -665,6 +668,9 @@ export async function runAskSession(options) {
|
|
|
665
668
|
let saidSomething = false;
|
|
666
669
|
const touchedFiles = new Set();
|
|
667
670
|
const notices = [];
|
|
671
|
+
// Reads and Cargo MCP tools are batched; the first one to finish must
|
|
672
|
+
// not wipe the spinner back to "Asking…" while siblings still run.
|
|
673
|
+
const activeTools = [];
|
|
668
674
|
const stopSpinner = () => {
|
|
669
675
|
if (spinner !== undefined) {
|
|
670
676
|
spinner.stop();
|
|
@@ -678,6 +684,36 @@ export async function runAskSession(options) {
|
|
|
678
684
|
}
|
|
679
685
|
spinner.update(label);
|
|
680
686
|
};
|
|
687
|
+
// The most recent tool labels the spinner. Nothing running means the
|
|
688
|
+
// turn is thinking rather than working.
|
|
689
|
+
const showActiveToolSpinner = () => {
|
|
690
|
+
const current = activeTools[activeTools.length - 1];
|
|
691
|
+
if (current === undefined) {
|
|
692
|
+
showSpinner("Asking…");
|
|
693
|
+
return;
|
|
694
|
+
}
|
|
695
|
+
showSpinner(describeToolActivity(current.toolName, current.summary));
|
|
696
|
+
};
|
|
697
|
+
// A tool is announced twice — once when its name arrives, again with a
|
|
698
|
+
// summary once its input is known — so the second has to overwrite the
|
|
699
|
+
// first rather than stack a duplicate label.
|
|
700
|
+
const rememberTool = (tool) => {
|
|
701
|
+
const at = activeTools.findIndex((active) => active.toolCallId === tool.toolCallId);
|
|
702
|
+
if (at === -1) {
|
|
703
|
+
activeTools.push(tool);
|
|
704
|
+
}
|
|
705
|
+
else {
|
|
706
|
+
activeTools[at] = tool;
|
|
707
|
+
}
|
|
708
|
+
showActiveToolSpinner();
|
|
709
|
+
};
|
|
710
|
+
const forgetTool = (toolCallId) => {
|
|
711
|
+
const at = activeTools.findIndex((active) => active.toolCallId === toolCallId);
|
|
712
|
+
if (at !== -1) {
|
|
713
|
+
activeTools.splice(at, 1);
|
|
714
|
+
}
|
|
715
|
+
showActiveToolSpinner();
|
|
716
|
+
};
|
|
681
717
|
// Registered before the spinner so our SIGINT handler runs first and
|
|
682
718
|
// clack's spinner is already stopped when its own handler fires —
|
|
683
719
|
// otherwise Ctrl-C prints its cancel line under ours.
|
|
@@ -686,6 +722,9 @@ export async function runAskSession(options) {
|
|
|
686
722
|
try {
|
|
687
723
|
await local.streamPrompt(question, {
|
|
688
724
|
abortSignal: cancellation.signal,
|
|
725
|
+
onStall: () => {
|
|
726
|
+
cancellation.abort();
|
|
727
|
+
},
|
|
689
728
|
onTextDelta: (delta) => {
|
|
690
729
|
if (live === undefined) {
|
|
691
730
|
stopSpinner();
|
|
@@ -699,15 +738,23 @@ export async function runAskSession(options) {
|
|
|
699
738
|
// not an unexplained pause. Claude Code narrates before it acts,
|
|
700
739
|
// so a tool usually starts with live text on screen: close that
|
|
701
740
|
// block first, and the next delta opens a new one.
|
|
702
|
-
|
|
703
|
-
|
|
741
|
+
//
|
|
742
|
+
// "dropped" is a tool the harness announced and then abandoned,
|
|
743
|
+
// so it has no result coming. Same handling as a finish: forget
|
|
744
|
+
// it, or the label outlives the work.
|
|
745
|
+
if (activity.kind === "end" || activity.kind === "dropped") {
|
|
746
|
+
forgetTool(activity.toolCallId);
|
|
704
747
|
return;
|
|
705
748
|
}
|
|
706
749
|
if (live !== undefined) {
|
|
707
750
|
endAssistantTurn(live);
|
|
708
751
|
live = undefined;
|
|
709
752
|
}
|
|
710
|
-
|
|
753
|
+
rememberTool({
|
|
754
|
+
toolCallId: activity.toolCallId,
|
|
755
|
+
toolName: activity.toolName,
|
|
756
|
+
summary: activity.summary,
|
|
757
|
+
});
|
|
711
758
|
},
|
|
712
759
|
onFileTouched: (path) => {
|
|
713
760
|
touchedFiles.add(path);
|
|
@@ -728,8 +775,18 @@ export async function runAskSession(options) {
|
|
|
728
775
|
if (decision === "always") {
|
|
729
776
|
approveEveryTool = true;
|
|
730
777
|
}
|
|
731
|
-
|
|
732
|
-
|
|
778
|
+
if (decision === "no") {
|
|
779
|
+
forgetTool(approval.toolCallId);
|
|
780
|
+
return false;
|
|
781
|
+
}
|
|
782
|
+
// The call was announced before the prompt; keep its label up
|
|
783
|
+
// while it runs instead of falling back to a blank "Asking…".
|
|
784
|
+
rememberTool({
|
|
785
|
+
toolCallId: approval.toolCallId,
|
|
786
|
+
toolName: approval.toolName,
|
|
787
|
+
summary: approval.summary,
|
|
788
|
+
});
|
|
789
|
+
return true;
|
|
733
790
|
},
|
|
734
791
|
});
|
|
735
792
|
stopSpinner();
|
|
@@ -758,14 +815,18 @@ export async function runAskSession(options) {
|
|
|
758
815
|
continue;
|
|
759
816
|
}
|
|
760
817
|
showNotices(notices);
|
|
761
|
-
// The runtime could not be wound down,
|
|
762
|
-
//
|
|
763
|
-
//
|
|
818
|
+
// The runtime could not be wound down, went silent, or the bridge
|
|
819
|
+
// dropped — this session will refuse every question from here.
|
|
820
|
+
// Replace it rather than exiting Ask: the chat, its history and the
|
|
764
821
|
// working copy all survive; only the Claude Code process is new.
|
|
765
|
-
if (error instanceof HarnessTurnStuck
|
|
822
|
+
if (error instanceof HarnessTurnStuck ||
|
|
823
|
+
error instanceof HarnessTurnStalled ||
|
|
824
|
+
isBridgeClosedError(error) === true) {
|
|
766
825
|
await local.close();
|
|
767
826
|
local = undefined;
|
|
768
|
-
showCard(
|
|
827
|
+
showCard(error instanceof HarnessTurnStalled
|
|
828
|
+
? `${error.message} The coding session has been replaced — ask again, or try smaller steps.`
|
|
829
|
+
: "That coding session stopped responding, so it has been replaced. Ask again — the chat is unchanged.");
|
|
769
830
|
continue;
|
|
770
831
|
}
|
|
771
832
|
throw error;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cargo-ai/cli",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.87",
|
|
4
4
|
"private": false,
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"homepage": "https://getcargo.ai",
|
|
@@ -66,10 +66,10 @@
|
|
|
66
66
|
"dependencies": {
|
|
67
67
|
"@ai-sdk/harness": "^1.0.95",
|
|
68
68
|
"@ai-sdk/harness-claude-code": "^1.0.99",
|
|
69
|
-
"@cargo-ai/api": "^1.0.
|
|
69
|
+
"@cargo-ai/api": "^1.0.76",
|
|
70
70
|
"@cargo-ai/app-sdk": "^1.0.10",
|
|
71
|
-
"@cargo-ai/cdk": "^1.0.
|
|
72
|
-
"@cargo-ai/types": "^1.0.
|
|
71
|
+
"@cargo-ai/cdk": "^1.0.71",
|
|
72
|
+
"@cargo-ai/types": "^1.0.73",
|
|
73
73
|
"@cargo-ai/utils": "^1.0.28",
|
|
74
74
|
"@cargo-ai/worker-sdk": "^1.0.17",
|
|
75
75
|
"@modelcontextprotocol/sdk": "1.29.0",
|