@cargo-ai/cli 1.0.95 → 1.0.96
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/ask/harness/approvals.d.ts.map +1 -1
- package/build/commands/ask/harness/approvals.js +0 -1
- package/build/commands/ask/harness/askUserQuestion.d.ts +69 -0
- package/build/commands/ask/harness/askUserQuestion.d.ts.map +1 -0
- package/build/commands/ask/harness/askUserQuestion.js +218 -0
- package/build/commands/ask/harness/claudeConfig.d.ts.map +1 -1
- package/build/commands/ask/harness/claudeConfig.js +28 -13
- package/build/commands/ask/harness/create.d.ts.map +1 -1
- package/build/commands/ask/harness/create.js +35 -13
- package/build/commands/ask/harness/index.d.ts +1 -0
- package/build/commands/ask/harness/index.d.ts.map +1 -1
- package/build/commands/ask/harness/index.js +1 -0
- package/build/commands/ask/harness/instructions.d.ts.map +1 -1
- package/build/commands/ask/harness/instructions.js +2 -0
- package/build/commands/ask/harness/turn.d.ts +7 -0
- package/build/commands/ask/harness/turn.d.ts.map +1 -1
- package/build/commands/ask/harness/turn.js +62 -12
- package/build/commands/ask/session/run.d.ts.map +1 -1
- package/build/commands/ask/session/run.js +15 -1
- package/build/commands/ask/session/tree.d.ts +10 -1
- package/build/commands/ask/session/tree.d.ts.map +1 -1
- package/build/commands/ask/session/tree.js +38 -0
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"approvals.d.ts","sourceRoot":"","sources":["../../../../src/commands/ask/harness/approvals.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"approvals.d.ts","sourceRoot":"","sources":["../../../../src/commands/ask/harness/approvals.ts"],"names":[],"mappings":"AA0CA,wBAAgB,cAAc,CAC5B,QAAQ,EAAE,MAAM,EAChB,OAAO,CAAC,EAAE;IAAE,UAAU,CAAC,EAAE,OAAO,CAAA;CAAE,GACjC,OAAO,CA2BT;AAqBD,eAAO,MAAM,kBAAkB,2HAC2F,CAAC;AAE3H,0BAA0B;AAC1B,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAExD"}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claude Code's `AskUserQuestion` — a structured multiple-choice prompt.
|
|
3
|
+
*
|
|
4
|
+
* The native picker cannot run here: `allow-reads` classifies it as readonly,
|
|
5
|
+
* so Claude Code starts it without asking us, then waits on a UI this session
|
|
6
|
+
* does not have. The native tool is withheld for that reason, via session
|
|
7
|
+
* `disallowedTools` — not a settings.json deny of the same name, which would
|
|
8
|
+
* also disable this host replacement. This session implements the tool as a
|
|
9
|
+
* host tool: the questions are shown in Ask's own picker, and the answers are
|
|
10
|
+
* the tool result.
|
|
11
|
+
*
|
|
12
|
+
* The input schema has to name `questions`. The Claude Code bridge turns host
|
|
13
|
+
* tool schemas into a Zod object shape; an empty shape strips every argument,
|
|
14
|
+
* and the picker sees `{}`.
|
|
15
|
+
*/
|
|
16
|
+
import { jsonSchema } from "ai";
|
|
17
|
+
export declare const ASK_USER_QUESTION_TOOL_NAME = "AskUserQuestion";
|
|
18
|
+
export declare const ASK_USER_QUESTION_DENIED_REASON = "Nobody is at the terminal to answer. Ask in your reply text instead.";
|
|
19
|
+
export declare const ASK_USER_QUESTION_MISSING_INPUT_REASON = "AskUserQuestion requires `questions`: an array of 1\u20134 items, each with `question`, `header`, `options` (each `{ label, description }`), and `multiSelect`. Call it again with those fields filled in.";
|
|
20
|
+
type AskUserQuestionHostTool = {
|
|
21
|
+
description: string;
|
|
22
|
+
inputSchema: ReturnType<typeof jsonSchema>;
|
|
23
|
+
execute: (data: unknown) => Promise<unknown>;
|
|
24
|
+
};
|
|
25
|
+
export type AskUserQuestionAsk = (input: unknown) => Promise<Record<string, string> | undefined>;
|
|
26
|
+
type BuiltinToolFiltering = {
|
|
27
|
+
mode: "allow" | "deny";
|
|
28
|
+
toolNames: string[];
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* Withhold Claude Code's native `AskUserQuestion` without dropping a host tool
|
|
32
|
+
* of the same name.
|
|
33
|
+
*
|
|
34
|
+
* HarnessAgent `inactiveTools` cannot do this: it also filters the user tool
|
|
35
|
+
* out of `toolSpecs`, so the replacement is never registered. A settings.json
|
|
36
|
+
* deny of `AskUserQuestion` matches the host tool too. Merging a deny into the
|
|
37
|
+
* session's builtin filtering becomes `disallowedTools` for the native tool
|
|
38
|
+
* only; host `execute` still runs.
|
|
39
|
+
*/
|
|
40
|
+
export declare function denyNativeAskUserQuestion(existing: BuiltinToolFiltering | undefined): BuiltinToolFiltering;
|
|
41
|
+
/**
|
|
42
|
+
* Host-side `AskUserQuestion`. Same name as the builtin so the model keeps
|
|
43
|
+
* calling it; HarnessAgent runs `execute` on the host instead of Claude Code.
|
|
44
|
+
*/
|
|
45
|
+
export declare function buildAskUserQuestionHostTool(getAsk: () => AskUserQuestionAsk | undefined): Record<string, AskUserQuestionHostTool>;
|
|
46
|
+
export type AskUserQuestionOption = {
|
|
47
|
+
label: string;
|
|
48
|
+
description: string | undefined;
|
|
49
|
+
};
|
|
50
|
+
export type AskUserQuestionItem = {
|
|
51
|
+
question: string;
|
|
52
|
+
header: string | undefined;
|
|
53
|
+
options: AskUserQuestionOption[];
|
|
54
|
+
multiSelect: boolean;
|
|
55
|
+
};
|
|
56
|
+
/**
|
|
57
|
+
* Pull the questions out of a tool input, whether the bridge sent a JSON
|
|
58
|
+
* string or the parsed object.
|
|
59
|
+
*/
|
|
60
|
+
export declare function readAskUserQuestions(toolInput: unknown): AskUserQuestionItem[] | undefined;
|
|
61
|
+
/** First question, for the spinner — not the whole JSON payload. */
|
|
62
|
+
export declare function summarizeAskUserQuestion(toolInput: unknown): string | undefined;
|
|
63
|
+
/**
|
|
64
|
+
* Told to the model as the denial reason so it can continue with the answers
|
|
65
|
+
* rather than asking again.
|
|
66
|
+
*/
|
|
67
|
+
export declare function formatAskUserQuestionAnswers(answers: Record<string, string>): string;
|
|
68
|
+
export {};
|
|
69
|
+
//# sourceMappingURL=askUserQuestion.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"askUserQuestion.d.ts","sourceRoot":"","sources":["../../../../src/commands/ask/harness/askUserQuestion.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EAAE,UAAU,EAAoB,MAAM,IAAI,CAAC;AAElD,eAAO,MAAM,2BAA2B,oBAAoB,CAAC;AAE7D,eAAO,MAAM,+BAA+B,yEAC4B,CAAC;AAEzE,eAAO,MAAM,sCAAsC,+MACsJ,CAAC;AA4C1M,KAAK,uBAAuB,GAAG;IAC7B,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,UAAU,CAAC,OAAO,UAAU,CAAC,CAAC;IAC3C,OAAO,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;CAC9C,CAAC;AAEF,MAAM,MAAM,kBAAkB,GAAG,CAC/B,KAAK,EAAE,OAAO,KACX,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,SAAS,CAAC,CAAC;AAEjD,KAAK,oBAAoB,GAAG;IAC1B,IAAI,EAAE,OAAO,GAAG,MAAM,CAAC;IACvB,SAAS,EAAE,MAAM,EAAE,CAAC;CACrB,CAAC;AAEF;;;;;;;;;GASG;AACH,wBAAgB,yBAAyB,CACvC,QAAQ,EAAE,oBAAoB,GAAG,SAAS,GACzC,oBAAoB,CAsBtB;AAED;;;GAGG;AACH,wBAAgB,4BAA4B,CAC1C,MAAM,EAAE,MAAM,kBAAkB,GAAG,SAAS,GAC3C,MAAM,CAAC,MAAM,EAAE,uBAAuB,CAAC,CA2BzC;AAED,MAAM,MAAM,qBAAqB,GAAG;IAClC,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,GAAG,SAAS,CAAC;CACjC,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAAG;IAChC,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC;IAC3B,OAAO,EAAE,qBAAqB,EAAE,CAAC;IACjC,WAAW,EAAE,OAAO,CAAC;CACtB,CAAC;AAEF;;;GAGG;AACH,wBAAgB,oBAAoB,CAClC,SAAS,EAAE,OAAO,GACjB,mBAAmB,EAAE,GAAG,SAAS,CA0BnC;AAED,oEAAoE;AACpE,wBAAgB,wBAAwB,CACtC,SAAS,EAAE,OAAO,GACjB,MAAM,GAAG,SAAS,CAkBpB;AAED;;;GAGG;AACH,wBAAgB,4BAA4B,CAC1C,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAC9B,MAAM,CAER"}
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claude Code's `AskUserQuestion` — a structured multiple-choice prompt.
|
|
3
|
+
*
|
|
4
|
+
* The native picker cannot run here: `allow-reads` classifies it as readonly,
|
|
5
|
+
* so Claude Code starts it without asking us, then waits on a UI this session
|
|
6
|
+
* does not have. The native tool is withheld for that reason, via session
|
|
7
|
+
* `disallowedTools` — not a settings.json deny of the same name, which would
|
|
8
|
+
* also disable this host replacement. This session implements the tool as a
|
|
9
|
+
* host tool: the questions are shown in Ask's own picker, and the answers are
|
|
10
|
+
* the tool result.
|
|
11
|
+
*
|
|
12
|
+
* The input schema has to name `questions`. The Claude Code bridge turns host
|
|
13
|
+
* tool schemas into a Zod object shape; an empty shape strips every argument,
|
|
14
|
+
* and the picker sees `{}`.
|
|
15
|
+
*/
|
|
16
|
+
import { jsonSchema } from "ai";
|
|
17
|
+
export const ASK_USER_QUESTION_TOOL_NAME = "AskUserQuestion";
|
|
18
|
+
export const ASK_USER_QUESTION_DENIED_REASON = "Nobody is at the terminal to answer. Ask in your reply text instead.";
|
|
19
|
+
export const ASK_USER_QUESTION_MISSING_INPUT_REASON = "AskUserQuestion requires `questions`: an array of 1–4 items, each with `question`, `header`, `options` (each `{ label, description }`), and `multiSelect`. Call it again with those fields filled in.";
|
|
20
|
+
const ASK_USER_QUESTION_JSON_SCHEMA = {
|
|
21
|
+
type: "object",
|
|
22
|
+
properties: {
|
|
23
|
+
questions: {
|
|
24
|
+
type: "array",
|
|
25
|
+
description: "1–4 multiple-choice questions to present to the user in this terminal.",
|
|
26
|
+
items: {
|
|
27
|
+
type: "object",
|
|
28
|
+
properties: {
|
|
29
|
+
question: {
|
|
30
|
+
type: "string",
|
|
31
|
+
description: "The question to ask.",
|
|
32
|
+
},
|
|
33
|
+
header: {
|
|
34
|
+
type: "string",
|
|
35
|
+
description: "Short label shown above the question.",
|
|
36
|
+
},
|
|
37
|
+
options: {
|
|
38
|
+
type: "array",
|
|
39
|
+
items: {
|
|
40
|
+
type: "object",
|
|
41
|
+
properties: {
|
|
42
|
+
label: { type: "string" },
|
|
43
|
+
description: { type: "string" },
|
|
44
|
+
preview: { type: "string" },
|
|
45
|
+
},
|
|
46
|
+
required: ["label", "description"],
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
multiSelect: {
|
|
50
|
+
type: "boolean",
|
|
51
|
+
description: "Whether the user may pick more than one option.",
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
required: ["question", "header", "options", "multiSelect"],
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
required: ["questions"],
|
|
59
|
+
};
|
|
60
|
+
/**
|
|
61
|
+
* Withhold Claude Code's native `AskUserQuestion` without dropping a host tool
|
|
62
|
+
* of the same name.
|
|
63
|
+
*
|
|
64
|
+
* HarnessAgent `inactiveTools` cannot do this: it also filters the user tool
|
|
65
|
+
* out of `toolSpecs`, so the replacement is never registered. A settings.json
|
|
66
|
+
* deny of `AskUserQuestion` matches the host tool too. Merging a deny into the
|
|
67
|
+
* session's builtin filtering becomes `disallowedTools` for the native tool
|
|
68
|
+
* only; host `execute` still runs.
|
|
69
|
+
*/
|
|
70
|
+
export function denyNativeAskUserQuestion(existing) {
|
|
71
|
+
if (existing === undefined) {
|
|
72
|
+
return { mode: "deny", toolNames: [ASK_USER_QUESTION_TOOL_NAME] };
|
|
73
|
+
}
|
|
74
|
+
if (existing.mode === "deny") {
|
|
75
|
+
if (existing.toolNames.includes(ASK_USER_QUESTION_TOOL_NAME) === true) {
|
|
76
|
+
return existing;
|
|
77
|
+
}
|
|
78
|
+
return {
|
|
79
|
+
mode: "deny",
|
|
80
|
+
toolNames: [...existing.toolNames, ASK_USER_QUESTION_TOOL_NAME],
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
return {
|
|
84
|
+
mode: "allow",
|
|
85
|
+
toolNames: existing.toolNames.filter((name) => name !== ASK_USER_QUESTION_TOOL_NAME),
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Host-side `AskUserQuestion`. Same name as the builtin so the model keeps
|
|
90
|
+
* calling it; HarnessAgent runs `execute` on the host instead of Claude Code.
|
|
91
|
+
*/
|
|
92
|
+
export function buildAskUserQuestionHostTool(getAsk) {
|
|
93
|
+
return {
|
|
94
|
+
[ASK_USER_QUESTION_TOOL_NAME]: {
|
|
95
|
+
description: "Ask the user multiple-choice questions via a structured UI. This session presents the options in the terminal and returns their answers.",
|
|
96
|
+
inputSchema: jsonSchema(ASK_USER_QUESTION_JSON_SCHEMA),
|
|
97
|
+
execute: async (data) => {
|
|
98
|
+
const ask = getAsk();
|
|
99
|
+
if (ask === undefined) {
|
|
100
|
+
return { error: ASK_USER_QUESTION_DENIED_REASON };
|
|
101
|
+
}
|
|
102
|
+
if (readAskUserQuestions(data) === undefined) {
|
|
103
|
+
return { error: ASK_USER_QUESTION_MISSING_INPUT_REASON };
|
|
104
|
+
}
|
|
105
|
+
const answers = await ask(data);
|
|
106
|
+
if (answers === undefined || Object.keys(answers).length === 0) {
|
|
107
|
+
return { error: ASK_USER_QUESTION_DENIED_REASON };
|
|
108
|
+
}
|
|
109
|
+
return { answers };
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Pull the questions out of a tool input, whether the bridge sent a JSON
|
|
116
|
+
* string or the parsed object.
|
|
117
|
+
*/
|
|
118
|
+
export function readAskUserQuestions(toolInput) {
|
|
119
|
+
const record = asRecord(toolInput);
|
|
120
|
+
if (record === undefined) {
|
|
121
|
+
return undefined;
|
|
122
|
+
}
|
|
123
|
+
const rawQuestions = record["questions"];
|
|
124
|
+
if (Array.isArray(rawQuestions) === false || rawQuestions.length === 0) {
|
|
125
|
+
return undefined;
|
|
126
|
+
}
|
|
127
|
+
const questions = [];
|
|
128
|
+
for (const rawQuestion of rawQuestions) {
|
|
129
|
+
const parsed = readQuestion(rawQuestion);
|
|
130
|
+
if (parsed === undefined) {
|
|
131
|
+
return undefined;
|
|
132
|
+
}
|
|
133
|
+
questions.push(parsed);
|
|
134
|
+
}
|
|
135
|
+
return questions;
|
|
136
|
+
}
|
|
137
|
+
/** First question, for the spinner — not the whole JSON payload. */
|
|
138
|
+
export function summarizeAskUserQuestion(toolInput) {
|
|
139
|
+
const questions = readAskUserQuestions(toolInput);
|
|
140
|
+
if (questions === undefined) {
|
|
141
|
+
return undefined;
|
|
142
|
+
}
|
|
143
|
+
const first = questions[0];
|
|
144
|
+
if (first === undefined) {
|
|
145
|
+
return undefined;
|
|
146
|
+
}
|
|
147
|
+
if (questions.length === 1) {
|
|
148
|
+
return first.question;
|
|
149
|
+
}
|
|
150
|
+
return `${first.question} (+${String(questions.length - 1)} more)`;
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Told to the model as the denial reason so it can continue with the answers
|
|
154
|
+
* rather than asking again.
|
|
155
|
+
*/
|
|
156
|
+
export function formatAskUserQuestionAnswers(answers) {
|
|
157
|
+
return `The user answered: ${JSON.stringify(answers)}. Continue with these answers. Do not call AskUserQuestion again for these questions.`;
|
|
158
|
+
}
|
|
159
|
+
function readQuestion(raw) {
|
|
160
|
+
const record = asRecord(raw);
|
|
161
|
+
if (record === undefined) {
|
|
162
|
+
return undefined;
|
|
163
|
+
}
|
|
164
|
+
const question = record["question"];
|
|
165
|
+
const rawOptions = record["options"];
|
|
166
|
+
if (typeof question !== "string" || question.length === 0) {
|
|
167
|
+
return undefined;
|
|
168
|
+
}
|
|
169
|
+
if (Array.isArray(rawOptions) === false || rawOptions.length === 0) {
|
|
170
|
+
return undefined;
|
|
171
|
+
}
|
|
172
|
+
const options = [];
|
|
173
|
+
for (const rawOption of rawOptions) {
|
|
174
|
+
const parsed = readOption(rawOption);
|
|
175
|
+
if (parsed === undefined) {
|
|
176
|
+
return undefined;
|
|
177
|
+
}
|
|
178
|
+
options.push(parsed);
|
|
179
|
+
}
|
|
180
|
+
const header = record["header"];
|
|
181
|
+
return {
|
|
182
|
+
question,
|
|
183
|
+
header: typeof header === "string" ? header : undefined,
|
|
184
|
+
options,
|
|
185
|
+
multiSelect: record["multiSelect"] === true,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
function readOption(raw) {
|
|
189
|
+
const record = asRecord(raw);
|
|
190
|
+
if (record === undefined) {
|
|
191
|
+
return undefined;
|
|
192
|
+
}
|
|
193
|
+
const label = record["label"];
|
|
194
|
+
const description = record["description"];
|
|
195
|
+
if (typeof label !== "string" || label.length === 0) {
|
|
196
|
+
return undefined;
|
|
197
|
+
}
|
|
198
|
+
return {
|
|
199
|
+
label,
|
|
200
|
+
description: typeof description === "string" ? description : undefined,
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
function asRecord(value) {
|
|
204
|
+
if (typeof value === "string") {
|
|
205
|
+
try {
|
|
206
|
+
const parsed = JSON.parse(value);
|
|
207
|
+
return typeof parsed === "object" && parsed !== null
|
|
208
|
+
? parsed
|
|
209
|
+
: undefined;
|
|
210
|
+
}
|
|
211
|
+
catch {
|
|
212
|
+
return undefined;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
return typeof value === "object" && value !== null
|
|
216
|
+
? value
|
|
217
|
+
: undefined;
|
|
218
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"claudeConfig.d.ts","sourceRoot":"","sources":["../../../../src/commands/ask/harness/claudeConfig.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"claudeConfig.d.ts","sourceRoot":"","sources":["../../../../src/commands/ask/harness/claudeConfig.ts"],"names":[],"mappings":"AAyCA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,kBAAkB,IAAI,MAAM,CAa3C"}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { getConfigDir } from "@cargo-ai/cdk/cli";
|
|
4
|
+
import { ASK_USER_QUESTION_TOOL_NAME } from "./askUserQuestion.js";
|
|
4
5
|
/**
|
|
5
6
|
* Tools that spawn a subagent, refused at source.
|
|
6
7
|
*
|
|
@@ -22,6 +23,16 @@ import { getConfigDir } from "@cargo-ai/cdk/cli";
|
|
|
22
23
|
* Code versions and a rule for a tool that does not exist costs nothing.
|
|
23
24
|
*/
|
|
24
25
|
const SUBAGENT_TOOL_NAMES = ["Agent", "Task"];
|
|
26
|
+
/**
|
|
27
|
+
* Native `AskUserQuestion` used to live on this list. A deny-by-name rule
|
|
28
|
+
* matches the host replacement of the same name, so model calls came back
|
|
29
|
+
* disabled instead of reaching host `execute`. Native withholding is now
|
|
30
|
+
* session `disallowedTools` — see `denyNativeAskUserQuestion`. Names here
|
|
31
|
+
* are stripped from an existing deny array so a previous Ask run cannot
|
|
32
|
+
* keep the collision around.
|
|
33
|
+
*/
|
|
34
|
+
const SETTINGS_DENIED_TOOL_NAMES = SUBAGENT_TOOL_NAMES;
|
|
35
|
+
const SETTINGS_UNDENIED_TOOL_NAMES = [ASK_USER_QUESTION_TOOL_NAME];
|
|
25
36
|
/**
|
|
26
37
|
* The Claude Agent SDK loads every filesystem settings source it can find when
|
|
27
38
|
* `settingSources` is omitted, and the harness bridge omits it. A local turn
|
|
@@ -52,24 +63,23 @@ export function askClaudeConfigDir() {
|
|
|
52
63
|
}
|
|
53
64
|
const dir = join(getConfigDir(), "ask-claude-config");
|
|
54
65
|
mkdirSync(dir, { recursive: true });
|
|
55
|
-
|
|
66
|
+
denySettingsTools(dir);
|
|
56
67
|
return dir;
|
|
57
68
|
}
|
|
58
69
|
/**
|
|
59
|
-
* Add the
|
|
60
|
-
*
|
|
70
|
+
* Add the deny rules to the settings file in Ask's own config directory,
|
|
71
|
+
* keeping whatever else is in there.
|
|
61
72
|
*
|
|
62
73
|
* Merged rather than written whole, because Claude Code keeps its own state
|
|
63
74
|
* under this directory.
|
|
64
75
|
*
|
|
65
76
|
* A failure here is reported rather than swallowed. There is no second line of
|
|
66
|
-
* defence to fall back on
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
* must not fail the question they just asked.
|
|
77
|
+
* defence to fall back on for tools `allow-reads` never asks about — so a
|
|
78
|
+
* settings file that could not be written means those tools run unattended,
|
|
79
|
+
* and the user is the only one who can tell whether that matters. Reported,
|
|
80
|
+
* not thrown: it must not fail the question they just asked.
|
|
71
81
|
*/
|
|
72
|
-
function
|
|
82
|
+
function denySettingsTools(dir) {
|
|
73
83
|
const path = join(dir, "settings.json");
|
|
74
84
|
try {
|
|
75
85
|
const existing = existsSync(path)
|
|
@@ -84,16 +94,21 @@ function denySubagentTools(dir) {
|
|
|
84
94
|
: {};
|
|
85
95
|
const denyValue = permissions["deny"];
|
|
86
96
|
const deny = Array.isArray(denyValue) ? denyValue : [];
|
|
87
|
-
const
|
|
88
|
-
|
|
97
|
+
const kept = deny.filter((name) => {
|
|
98
|
+
return typeof name === "string"
|
|
99
|
+
? SETTINGS_UNDENIED_TOOL_NAMES.includes(name) === false
|
|
100
|
+
: true;
|
|
101
|
+
});
|
|
102
|
+
const missing = SETTINGS_DENIED_TOOL_NAMES.filter((name) => kept.includes(name) === false);
|
|
103
|
+
if (missing.length === 0 && kept.length === deny.length) {
|
|
89
104
|
return;
|
|
90
105
|
}
|
|
91
106
|
writeFileSync(path, `${JSON.stringify({
|
|
92
107
|
...settings,
|
|
93
|
-
permissions: { ...permissions, deny: [...
|
|
108
|
+
permissions: { ...permissions, deny: [...kept, ...missing] },
|
|
94
109
|
}, null, 2)}\n`);
|
|
95
110
|
}
|
|
96
111
|
catch (error) {
|
|
97
|
-
process.stderr.write(`Could not write ${path}, so
|
|
112
|
+
process.stderr.write(`Could not write ${path}, so Agent and Task are not withheld from this session: ${error instanceof Error ? error.message : "unknown error"}\n`);
|
|
98
113
|
}
|
|
99
114
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"create.d.ts","sourceRoot":"","sources":["../../../../src/commands/ask/harness/create.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,eAAe,CAAC;AAGxC,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,iBAAiB,CAAC;
|
|
1
|
+
{"version":3,"file":"create.d.ts","sourceRoot":"","sources":["../../../../src/commands/ask/harness/create.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,eAAe,CAAC;AAGxC,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,iBAAiB,CAAC;AAuB3C,OAAO,EAGL,KAAK,kBAAkB,EAKxB,MAAM,WAAW,CAAC;AAGnB,MAAM,MAAM,YAAY,GAAG;IACzB,YAAY,EAAE,CACZ,QAAQ,EAAE,MAAM,EAChB,OAAO,CAAC,EAAE,kBAAkB,KACzB,OAAO,CAAC,MAAM,CAAC,CAAC;IACrB;;;;OAIG;IACH,iBAAiB,EAAE,MAAM,MAAM,GAAG,SAAS,CAAC;IAC5C,sEAAsE;IACtE,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC5B,CAAC;AAcF,wBAAsB,kBAAkB,CAAC,KAAK,EAAE;IAC9C,GAAG,EAAE,GAAG,CAAC;IACT;;;;OAIG;IACH,UAAU,EAAE,MAAM,CAAC;IACnB,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;;OAIG;IACH,OAAO,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;IACnB;;;;OAIG;IACH,aAAa,EAAE,MAAM,CAAC;IACtB,eAAe,EAAE,MAAM,CAAC;IACxB,iBAAiB,EAAE,MAAM,CAAC;IAC1B;;;;OAIG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;;OAIG;IACH,uBAAuB,EAAE,OAAO,CAAC;CAClC,GAAG,OAAO,CAAC,YAAY,CAAC,CAsUxB"}
|
|
@@ -2,6 +2,7 @@ import { HarnessAgent } from "@ai-sdk/harness/agent";
|
|
|
2
2
|
import { createClaudeCode } from "@ai-sdk/harness-claude-code";
|
|
3
3
|
import { createLocalSandbox } from "ai-sdk-sandbox-local";
|
|
4
4
|
import { pruneHarnessArtifacts } from "./artifacts.js";
|
|
5
|
+
import { buildAskUserQuestionHostTool, denyNativeAskUserQuestion, } from "./askUserQuestion.js";
|
|
5
6
|
import { askClaudeConfigDir } from "./claudeConfig.js";
|
|
6
7
|
import { ingestTurn } from "./ingest.js";
|
|
7
8
|
import { localHarnessInstructions, localHarnessSkills, } from "./instructions.js";
|
|
@@ -39,9 +40,15 @@ export async function createLocalHarness(input) {
|
|
|
39
40
|
setupPhase("stale harness artifacts pruned");
|
|
40
41
|
const anthropicBaseUrl = `${input.apiBaseUrl.replace(/\/$/, "")}/sandbox/anthropic`;
|
|
41
42
|
const release = input.release;
|
|
42
|
-
const
|
|
43
|
+
const releaseTools = input.allowWorkspaceMutations === false || release === undefined
|
|
43
44
|
? {}
|
|
44
45
|
: buildReleaseHostTools(input.api, release.actions);
|
|
46
|
+
// Bound per prompt: the picker lives on the turn, the tool on the session.
|
|
47
|
+
const askUserQuestionAsk = { current: undefined };
|
|
48
|
+
const hostTools = {
|
|
49
|
+
...releaseTools,
|
|
50
|
+
...buildAskUserQuestionHostTool(() => askUserQuestionAsk.current),
|
|
51
|
+
};
|
|
45
52
|
const mcpServers = {
|
|
46
53
|
[CARGO_MCP_SERVER_NAME]: cargoMcpServer(input.apiBaseUrl),
|
|
47
54
|
};
|
|
@@ -51,19 +58,29 @@ export async function createLocalHarness(input) {
|
|
|
51
58
|
mcpServers[name] = server;
|
|
52
59
|
}
|
|
53
60
|
}
|
|
61
|
+
// Pin auth the same way the backend harness stream does. Credentials go
|
|
62
|
+
// through `auth`, not `env`: passing them via `env` alone lets
|
|
63
|
+
// createClaudeCode auto-detect AI Gateway from the host process and ignore
|
|
64
|
+
// the Cargo proxy.
|
|
65
|
+
const claudeCode = createClaudeCode({
|
|
66
|
+
auth: {
|
|
67
|
+
ANTHROPIC_BASE_URL: anthropicBaseUrl,
|
|
68
|
+
ANTHROPIC_API_KEY: input.proxyToken,
|
|
69
|
+
},
|
|
70
|
+
env: { CLAUDE_CONFIG_DIR: askClaudeConfigDir() },
|
|
71
|
+
mcpServers,
|
|
72
|
+
});
|
|
54
73
|
const harnessAgent = new HarnessAgent({
|
|
55
|
-
//
|
|
56
|
-
//
|
|
57
|
-
//
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
},
|
|
64
|
-
|
|
65
|
-
mcpServers,
|
|
66
|
-
}),
|
|
74
|
+
// Native AskUserQuestion is withheld on the session, not via
|
|
75
|
+
// `inactiveTools` (that would drop the host tool of the same name) and
|
|
76
|
+
// not via settings.json deny (that name matches the replacement too).
|
|
77
|
+
harness: {
|
|
78
|
+
...claudeCode,
|
|
79
|
+
doStart: async (startOpts) => claudeCode.doStart({
|
|
80
|
+
...startOpts,
|
|
81
|
+
builtinToolFiltering: denyNativeAskUserQuestion(startOpts.builtinToolFiltering),
|
|
82
|
+
}),
|
|
83
|
+
},
|
|
67
84
|
id: "cargo-ai-ask",
|
|
68
85
|
sandbox: withGetPortEndpoint(createLocalSandbox({ cwd: workspace.sandboxCwd })),
|
|
69
86
|
sandboxConfig: workspace.workDir === undefined ? {} : { workDir: workspace.workDir },
|
|
@@ -166,6 +183,8 @@ export async function createLocalHarness(input) {
|
|
|
166
183
|
}
|
|
167
184
|
const accumulator = createTurnAccumulator();
|
|
168
185
|
let answer;
|
|
186
|
+
askUserQuestionAsk.current =
|
|
187
|
+
options === undefined ? undefined : options.onUserQuestion;
|
|
169
188
|
try {
|
|
170
189
|
answer = await runPromptTurn({
|
|
171
190
|
driver: {
|
|
@@ -230,6 +249,9 @@ export async function createLocalHarness(input) {
|
|
|
230
249
|
});
|
|
231
250
|
throw error;
|
|
232
251
|
}
|
|
252
|
+
finally {
|
|
253
|
+
askUserQuestionAsk.current = undefined;
|
|
254
|
+
}
|
|
233
255
|
if (didReachAnthropic(accumulator.usage) === true) {
|
|
234
256
|
proxyLastUsedAtMs = Date.now();
|
|
235
257
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { isAlwaysDenied, isAutoApproved } from "./approvals.js";
|
|
2
|
+
export { ASK_USER_QUESTION_DENIED_REASON, ASK_USER_QUESTION_MISSING_INPUT_REASON, ASK_USER_QUESTION_TOOL_NAME, buildAskUserQuestionHostTool, denyNativeAskUserQuestion, formatAskUserQuestionAnswers, readAskUserQuestions, summarizeAskUserQuestion, } from "./askUserQuestion.js";
|
|
2
3
|
export { createLocalHarness, type LocalHarness } from "./create.js";
|
|
3
4
|
export { localHarnessInstructions, localHarnessSkills, } from "./instructions.js";
|
|
4
5
|
export { resolveWorkspaceLayout } from "./layout.js";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/commands/ask/harness/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAChE,OAAO,EAAE,kBAAkB,EAAE,KAAK,YAAY,EAAE,MAAM,aAAa,CAAC;AACpE,OAAO,EACL,wBAAwB,EACxB,kBAAkB,GACnB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAAE,sBAAsB,EAAE,MAAM,aAAa,CAAC;AACrD,OAAO,EAAE,qBAAqB,EAAE,MAAM,UAAU,CAAC;AACjD,OAAO,EACL,kBAAkB,EAClB,gBAAgB,EAChB,mBAAmB,EACnB,KAAK,mBAAmB,EACxB,KAAK,kBAAkB,EACvB,KAAK,iBAAiB,EACtB,KAAK,iBAAiB,EACtB,aAAa,EACb,YAAY,EACZ,KAAK,UAAU,EACf,KAAK,UAAU,GAChB,MAAM,WAAW,CAAC;AACnB,OAAO,EACL,qBAAqB,EACrB,KAAK,oBAAoB,EACzB,KAAK,cAAc,EACnB,qBAAqB,GACtB,MAAM,YAAY,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/commands/ask/harness/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAChE,OAAO,EACL,+BAA+B,EAC/B,sCAAsC,EACtC,2BAA2B,EAC3B,4BAA4B,EAC5B,yBAAyB,EACzB,4BAA4B,EAC5B,oBAAoB,EACpB,wBAAwB,GACzB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAAE,kBAAkB,EAAE,KAAK,YAAY,EAAE,MAAM,aAAa,CAAC;AACpE,OAAO,EACL,wBAAwB,EACxB,kBAAkB,GACnB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAAE,sBAAsB,EAAE,MAAM,aAAa,CAAC;AACrD,OAAO,EAAE,qBAAqB,EAAE,MAAM,UAAU,CAAC;AACjD,OAAO,EACL,kBAAkB,EAClB,gBAAgB,EAChB,mBAAmB,EACnB,KAAK,mBAAmB,EACxB,KAAK,kBAAkB,EACvB,KAAK,iBAAiB,EACtB,KAAK,iBAAiB,EACtB,aAAa,EACb,YAAY,EACZ,KAAK,UAAU,EACf,KAAK,UAAU,GAChB,MAAM,WAAW,CAAC;AACnB,OAAO,EACL,qBAAqB,EACrB,KAAK,oBAAoB,EACzB,KAAK,cAAc,EACnB,qBAAqB,GACtB,MAAM,YAAY,CAAC"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { isAlwaysDenied, isAutoApproved } from "./approvals.js";
|
|
2
|
+
export { ASK_USER_QUESTION_DENIED_REASON, ASK_USER_QUESTION_MISSING_INPUT_REASON, ASK_USER_QUESTION_TOOL_NAME, buildAskUserQuestionHostTool, denyNativeAskUserQuestion, formatAskUserQuestionAnswers, readAskUserQuestions, summarizeAskUserQuestion, } from "./askUserQuestion.js";
|
|
2
3
|
export { createLocalHarness } from "./create.js";
|
|
3
4
|
export { localHarnessInstructions, localHarnessSkills, } from "./instructions.js";
|
|
4
5
|
export { resolveWorkspaceLayout } from "./layout.js";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"instructions.d.ts","sourceRoot":"","sources":["../../../../src/commands/ask/harness/instructions.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,eAAe,CAAC;
|
|
1
|
+
{"version":3,"file":"instructions.d.ts","sourceRoot":"","sources":["../../../../src/commands/ask/harness/instructions.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,eAAe,CAAC;AAwBxC;;;GAGG;AACH,wBAAgB,wBAAwB,CACtC,GAAG,EAAE,MAAM,EACX,OAAO,CAAC,EAAE,EAAE,CAAC,OAAO,GACnB,MAAM,CAkBR;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"}
|
|
@@ -4,6 +4,8 @@ const LOCAL_WORKING_COPY_INSTRUCTIONS = `You are working in the user's local wor
|
|
|
4
4
|
|
|
5
5
|
The user confirms each Bash, Write, Edit and NotebookEdit call, and can only answer one at a time. Ask for a single one of those per step and wait for its result before asking for the next; two in one step leaves the second waiting on a confirmation it will never be offered. Reads (Read, Glob, Grep) and the Cargo \`mcp__cargo__*\` tools need no confirmation — batch those freely.
|
|
6
6
|
|
|
7
|
+
When you need a multiple-choice answer, call AskUserQuestion. This session presents the options in the terminal and returns the user's answers as the tool result. Do not wait for a Claude Code question picker.
|
|
8
|
+
|
|
7
9
|
Prefer minimal, focused diffs; match the existing code style; run the project's own checks when they exist. End each task with a short summary of what changed and how it was validated.`;
|
|
8
10
|
const CDK_RAILS_INSTRUCTIONS = `This directory is a Cargo CDK project.
|
|
9
11
|
- Never edit cargo.state.json, cargo.state.lock, or .cargo-ai/ by hand — they are generated. Repoint this project's deploy state with \`cargo-cdk state bind\`.
|
|
@@ -43,6 +43,13 @@ export type LocalStreamHandlers = {
|
|
|
43
43
|
* do rather than silently running commands on the user's machine.
|
|
44
44
|
*/
|
|
45
45
|
onApproval?: (approval: LocalToolApproval) => Promise<boolean>;
|
|
46
|
+
/**
|
|
47
|
+
* Present `AskUserQuestion` in this session's own picker. Used by the host
|
|
48
|
+
* tool that replaces Claude Code's native picker, and by the approval path
|
|
49
|
+
* if a call is still offered. Omitted (`--print`) means the model is told
|
|
50
|
+
* to ask in its reply instead.
|
|
51
|
+
*/
|
|
52
|
+
onUserQuestion?: (input: unknown) => Promise<Record<string, string> | undefined>;
|
|
46
53
|
onActivity?: (activity: LocalToolActivity) => void;
|
|
47
54
|
/** Path of a file the turn wrote or edited, as the tool named it. */
|
|
48
55
|
onFileTouched?: (path: string) => void;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"turn.d.ts","sourceRoot":"","sources":["../../../../src/commands/ask/harness/turn.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"turn.d.ts","sourceRoot":"","sources":["../../../../src/commands/ask/harness/turn.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { DENIED_TOOL_REASON, isAlwaysDenied, isAutoApproved, } from "./approvals.js";
|
|
2
|
+
import { ASK_USER_QUESTION_DENIED_REASON, ASK_USER_QUESTION_TOOL_NAME, formatAskUserQuestionAnswers, summarizeAskUserQuestion, } from "./askUserQuestion.js";
|
|
2
3
|
import { addTurnUsage, createTurnAccumulator, } from "./usage.js";
|
|
3
4
|
/**
|
|
4
5
|
* How many times one turn may suspend for approvals. Each round trip is a
|
|
@@ -101,15 +102,20 @@ export async function runPromptTurn(input) {
|
|
|
101
102
|
// Refused before the user is asked: not their call to make, and a
|
|
102
103
|
// prompt for it could not say what it was granting.
|
|
103
104
|
const denied = isAlwaysDenied(approval.toolName);
|
|
104
|
-
const
|
|
105
|
+
const question = approval.toolName === ASK_USER_QUESTION_TOOL_NAME
|
|
106
|
+
? await settleAskUserQuestion(approval.input, handlers.onUserQuestion)
|
|
107
|
+
: undefined;
|
|
108
|
+
const approved = question !== undefined
|
|
105
109
|
? false
|
|
106
|
-
:
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
?
|
|
112
|
-
:
|
|
110
|
+
: denied === true
|
|
111
|
+
? false
|
|
112
|
+
: isAutoApproved(approval.toolName, {
|
|
113
|
+
unattended: onApproval === undefined,
|
|
114
|
+
}) === true
|
|
115
|
+
? true
|
|
116
|
+
: onApproval === undefined
|
|
117
|
+
? false
|
|
118
|
+
: await onApproval(approval);
|
|
113
119
|
// Recorded against the call so the retraction pass below knows it is
|
|
114
120
|
// settled: an approved one is running and must be left alone, a refused
|
|
115
121
|
// one is certainly not and needs no reprieve to find out. Absent when
|
|
@@ -132,7 +138,11 @@ export async function runPromptTurn(input) {
|
|
|
132
138
|
approved,
|
|
133
139
|
// Told why, so it does something else rather than asking again
|
|
134
140
|
// until the approval-round cap ends the turn.
|
|
135
|
-
reason:
|
|
141
|
+
reason: question !== undefined
|
|
142
|
+
? question.reason
|
|
143
|
+
: denied === true
|
|
144
|
+
? DENIED_TOOL_REASON
|
|
145
|
+
: undefined,
|
|
136
146
|
},
|
|
137
147
|
toolCall: {
|
|
138
148
|
type: "tool-call",
|
|
@@ -400,7 +410,7 @@ async function consumeTurn(input) {
|
|
|
400
410
|
}
|
|
401
411
|
continue;
|
|
402
412
|
}
|
|
403
|
-
if (part
|
|
413
|
+
if (isToolCallPart(part) === true) {
|
|
404
414
|
const toolCallId = part["toolCallId"];
|
|
405
415
|
const toolName = part["toolName"];
|
|
406
416
|
if (typeof toolCallId === "string" && typeof toolName === "string") {
|
|
@@ -444,12 +454,12 @@ async function consumeTurn(input) {
|
|
|
444
454
|
}
|
|
445
455
|
continue;
|
|
446
456
|
}
|
|
447
|
-
if (part
|
|
457
|
+
if (isToolEndPart(part) === true) {
|
|
448
458
|
const toolCallId = part["toolCallId"];
|
|
449
459
|
const recorded = typeof toolCallId === "string"
|
|
450
460
|
? input.toolCalls.get(toolCallId)
|
|
451
461
|
: undefined;
|
|
452
|
-
const isError = part
|
|
462
|
+
const isError = isToolErrorPart(part);
|
|
453
463
|
// A preliminary result is progress from a tool that is still running,
|
|
454
464
|
// so it must not clear the spinner or tell the stall watchdog the tool
|
|
455
465
|
// is done.
|
|
@@ -616,11 +626,51 @@ function summarizeToolInput(toolInput) {
|
|
|
616
626
|
return truncate(value);
|
|
617
627
|
}
|
|
618
628
|
}
|
|
629
|
+
const question = summarizeAskUserQuestion(toolInput);
|
|
630
|
+
if (question !== undefined) {
|
|
631
|
+
return truncate(question);
|
|
632
|
+
}
|
|
619
633
|
return truncate(JSON.stringify(record));
|
|
620
634
|
}
|
|
635
|
+
async function settleAskUserQuestion(toolInput, onUserQuestion) {
|
|
636
|
+
if (onUserQuestion === undefined) {
|
|
637
|
+
return { reason: ASK_USER_QUESTION_DENIED_REASON };
|
|
638
|
+
}
|
|
639
|
+
const answers = await onUserQuestion(toolInput);
|
|
640
|
+
if (answers === undefined || Object.keys(answers).length === 0) {
|
|
641
|
+
return { reason: ASK_USER_QUESTION_DENIED_REASON };
|
|
642
|
+
}
|
|
643
|
+
return { reason: formatAskUserQuestionAnswers(answers) };
|
|
644
|
+
}
|
|
621
645
|
function truncate(value) {
|
|
622
646
|
const flat = value.replace(/\s+/g, " ").trim();
|
|
623
647
|
return flat.length > TOOL_SUMMARY_MAX_LENGTH
|
|
624
648
|
? `${flat.slice(0, TOOL_SUMMARY_MAX_LENGTH - 1)}…`
|
|
625
649
|
: flat;
|
|
626
650
|
}
|
|
651
|
+
/**
|
|
652
|
+
* `fullStream` still uses `tool-call`. The UI message stream — and some
|
|
653
|
+
* harness adapters after AI SDK 7 — spell the assembled call
|
|
654
|
+
* `tool-input-available` instead. Treat them as the same moment: the tool
|
|
655
|
+
* is now executing.
|
|
656
|
+
*/
|
|
657
|
+
function isToolCallPart(part) {
|
|
658
|
+
return (part["type"] === "tool-call" || part["type"] === "tool-input-available");
|
|
659
|
+
}
|
|
660
|
+
/**
|
|
661
|
+
* Same split for the result. Missing the UI names left the tool "running"
|
|
662
|
+
* after it had already finished, which is the spinner-stuck-on-every-tool
|
|
663
|
+
* hang: the stall watchdog switched to its long deadline and waited there.
|
|
664
|
+
*/
|
|
665
|
+
function isToolEndPart(part) {
|
|
666
|
+
return (part["type"] === "tool-result" ||
|
|
667
|
+
part["type"] === "tool-error" ||
|
|
668
|
+
part["type"] === "tool-output-available" ||
|
|
669
|
+
part["type"] === "tool-output-error" ||
|
|
670
|
+
part["type"] === "tool-output-denied");
|
|
671
|
+
}
|
|
672
|
+
function isToolErrorPart(part) {
|
|
673
|
+
return (part["type"] === "tool-error" ||
|
|
674
|
+
part["type"] === "tool-output-error" ||
|
|
675
|
+
part["type"] === "tool-output-denied");
|
|
676
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"run.d.ts","sourceRoot":"","sources":["../../../../src/commands/ask/session/run.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,iBAAiB,CAAC;
|
|
1
|
+
{"version":3,"file":"run.d.ts","sourceRoot":"","sources":["../../../../src/commands/ask/session/run.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,iBAAiB,CAAC;AAoD3C,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;AAkHF;;;;;;;;;GASG;AACH,wBAAsB,aAAa,CACjC,GAAG,EAAE,GAAG,EACR,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,OAAO,CAAC,CAqBlB;AAyND;;;;;;GAMG;AACH,wBAAsB,aAAa,CACjC,OAAO,EAAE,iBAAiB,GACzB,OAAO,CAAC,SAAS,GAAG,MAAM,CAAC,CAsmB7B;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,CAqGhB"}
|
|
@@ -13,7 +13,7 @@ import { saveAskState } from "../persist/persist.js";
|
|
|
13
13
|
import { ASK_STATE_VERSION } from "../persist/state.js";
|
|
14
14
|
import { claimChatTurn, proxyTokenDeadlineMs, releaseChatTurn, reportApiFailure, } from "./claim.js";
|
|
15
15
|
import { pickThinkingWord } from "./thinking.js";
|
|
16
|
-
import { appendAssistantDelta, askToolApproval, beginAssistantTurn, describeToolActivity, endAssistantTurn, setAssistantFooter, showAssistantTurn, showCard, showNotices, showToolRun, showTouchedFiles, showUserTurn, } from "./tree.js";
|
|
16
|
+
import { appendAssistantDelta, askToolApproval, askUserQuestions, beginAssistantTurn, describeToolActivity, endAssistantTurn, setAssistantFooter, showAssistantTurn, showCard, showNotices, showToolRun, showTouchedFiles, showUserTurn, } from "./tree.js";
|
|
17
17
|
function requireInteractive() {
|
|
18
18
|
if (isInteractive() === false || process.stdout.isTTY !== true) {
|
|
19
19
|
failWith("Ask needs a terminal. Re-run with --print for one JSON turn.", {
|
|
@@ -663,6 +663,20 @@ export async function runAskSession(options) {
|
|
|
663
663
|
});
|
|
664
664
|
return true;
|
|
665
665
|
},
|
|
666
|
+
onUserQuestion: async (input) => {
|
|
667
|
+
if (live !== undefined) {
|
|
668
|
+
endAssistantTurn(live);
|
|
669
|
+
live = undefined;
|
|
670
|
+
}
|
|
671
|
+
stopSpinner();
|
|
672
|
+
await typing.pause();
|
|
673
|
+
try {
|
|
674
|
+
return await askUserQuestions(input);
|
|
675
|
+
}
|
|
676
|
+
finally {
|
|
677
|
+
typing.resume();
|
|
678
|
+
}
|
|
679
|
+
},
|
|
666
680
|
});
|
|
667
681
|
stopSpinner();
|
|
668
682
|
if (live !== undefined) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type Spinner } from "../../runHandler.js";
|
|
2
|
-
import type { LocalToolApproval } from "../harness/
|
|
2
|
+
import type { LocalToolApproval } from "../harness/turn.js";
|
|
3
3
|
/**
|
|
4
4
|
* Greedy wrap on spaces. A word longer than the width keeps its own line
|
|
5
5
|
* rather than being cut mid-token.
|
|
@@ -87,4 +87,13 @@ export declare function showNotices(notices: string[]): void;
|
|
|
87
87
|
* │ ● Yes …
|
|
88
88
|
*/
|
|
89
89
|
export declare function askToolApproval(approval: LocalToolApproval): Promise<"yes" | "always" | "no">;
|
|
90
|
+
/**
|
|
91
|
+
* Claude Code's question picker, in this session's tree. Each question is one
|
|
92
|
+
* `selectOne` — the same prompt as a tool approval, so the rail stays intact.
|
|
93
|
+
*
|
|
94
|
+
* Multi-select is flattened to one pick: this terminal can only answer one
|
|
95
|
+
* row at a time, and a comma-joined bag of labels is worse than the one the
|
|
96
|
+
* user actually chose.
|
|
97
|
+
*/
|
|
98
|
+
export declare function askUserQuestions(toolInput: unknown): Promise<Record<string, string> | undefined>;
|
|
90
99
|
//# sourceMappingURL=tree.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tree.d.ts","sourceRoot":"","sources":["../../../../src/commands/ask/session/tree.ts"],"names":[],"mappings":"AAIA,OAAO,EAAuB,KAAK,OAAO,EAAE,MAAM,qBAAqB,CAAC;
|
|
1
|
+
{"version":3,"file":"tree.d.ts","sourceRoot":"","sources":["../../../../src/commands/ask/session/tree.ts"],"names":[],"mappings":"AAIA,OAAO,EAAuB,KAAK,OAAO,EAAE,MAAM,qBAAqB,CAAC;AAKxE,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AA6B5D;;;;;GAKG;AACH,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CAe9D;AA0DD,wBAAgB,QAAQ,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAM1D;AAED;;;;GAIG;AACH,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAK3C;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAE/C;AAED,MAAM,MAAM,iBAAiB,GAAG;IAC9B,MAAM,EAAE,MAAM,CAAC;IACf,YAAY,EAAE,MAAM,CAAC;IACrB;;;;;;;OAOG;IACH,MAAM,EAAE,MAAM,CAAC;CAChB,CAAC;AA+DF,4DAA4D;AAC5D,wBAAgB,kBAAkB,IAAI,iBAAiB,CAItD;AAED,wBAAgB,oBAAoB,CAClC,IAAI,EAAE,iBAAiB,EACvB,KAAK,EAAE,MAAM,GACZ,IAAI,CAeN;AAkBD,uEAAuE;AACvE,wBAAgB,kBAAkB,CAChC,IAAI,EAAE,iBAAiB,EACvB,MAAM,EAAE,MAAM,GACb,IAAI,CAON;AAED,8DAA8D;AAC9D,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,iBAAiB,GAAG,IAAI,CAK9D;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAIpD;AAiBD,wBAAgB,oBAAoB,CAClC,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,MAAM,GACd,MAAM,CAYR;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE;IACjC,OAAO,EAAE,OAAO,GAAG,SAAS,CAAC;IAC7B,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,OAAO,CAAC;CAClB,GAAG,IAAI,CAYP;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAQ7D;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAC9B,KAAK,EAAE,WAAW,CAAC,MAAM,CAAC,EAC1B,GAAG,EAAE,MAAM,GACV,IAAI,CAWN;AAED;;;GAGG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,IAAI,CAQnD;AAED;;;;;;GAMG;AACH,wBAAsB,eAAe,CACnC,QAAQ,EAAE,iBAAiB,GAC1B,OAAO,CAAC,KAAK,GAAG,QAAQ,GAAG,IAAI,CAAC,CAkBlC;AAID;;;;;;;GAOG;AACH,wBAAsB,gBAAgB,CACpC,SAAS,EAAE,OAAO,GACjB,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,SAAS,CAAC,CA6B7C"}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { isAbsolute, relative } from "node:path";
|
|
2
2
|
import { selectOne } from "@cargo-ai/cdk/cli";
|
|
3
3
|
import { clipToWidth, colors } from "../../runHandler.js";
|
|
4
|
+
import { ASK_USER_QUESTION_TOOL_NAME, readAskUserQuestions, } from "../harness/askUserQuestion.js";
|
|
4
5
|
/**
|
|
5
6
|
* What an assistant turn is marked with, alongside `◇` for a step and `│` for
|
|
6
7
|
* the rail.
|
|
@@ -217,6 +218,9 @@ export function showAssistantTurn(body) {
|
|
|
217
218
|
}
|
|
218
219
|
/** Common tool names arrive lowercase (`bash`); natives keep their case. */
|
|
219
220
|
function toolLabel(toolName) {
|
|
221
|
+
if (toolName === ASK_USER_QUESTION_TOOL_NAME) {
|
|
222
|
+
return "Ask user";
|
|
223
|
+
}
|
|
220
224
|
if (toolName.length === 0) {
|
|
221
225
|
return "tool";
|
|
222
226
|
}
|
|
@@ -316,3 +320,37 @@ export async function askToolApproval(approval) {
|
|
|
316
320
|
});
|
|
317
321
|
return decision === "always" ? "always" : decision === "no" ? "no" : "yes";
|
|
318
322
|
}
|
|
323
|
+
const SKIP_QUESTION = "__skip__";
|
|
324
|
+
/**
|
|
325
|
+
* Claude Code's question picker, in this session's tree. Each question is one
|
|
326
|
+
* `selectOne` — the same prompt as a tool approval, so the rail stays intact.
|
|
327
|
+
*
|
|
328
|
+
* Multi-select is flattened to one pick: this terminal can only answer one
|
|
329
|
+
* row at a time, and a comma-joined bag of labels is worse than the one the
|
|
330
|
+
* user actually chose.
|
|
331
|
+
*/
|
|
332
|
+
export async function askUserQuestions(toolInput) {
|
|
333
|
+
const questions = readAskUserQuestions(toolInput);
|
|
334
|
+
if (questions === undefined) {
|
|
335
|
+
return undefined;
|
|
336
|
+
}
|
|
337
|
+
const answers = {};
|
|
338
|
+
for (const question of questions) {
|
|
339
|
+
const header = question.header === undefined ? "" : `${question.header}: `;
|
|
340
|
+
const first = question.options[0];
|
|
341
|
+
const chosen = await selectOne({
|
|
342
|
+
message: `${header}${question.question}`,
|
|
343
|
+
recommended: first === undefined ? undefined : first.label,
|
|
344
|
+
cancelValue: SKIP_QUESTION,
|
|
345
|
+
choices: question.options.map((option) => ({
|
|
346
|
+
value: option.label,
|
|
347
|
+
label: option.label,
|
|
348
|
+
hint: option.description,
|
|
349
|
+
})),
|
|
350
|
+
});
|
|
351
|
+
if (chosen !== SKIP_QUESTION) {
|
|
352
|
+
answers[question.question] = chosen;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
return Object.keys(answers).length === 0 ? undefined : answers;
|
|
356
|
+
}
|