@cargo-ai/cli 1.0.77 → 1.0.80

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.
Files changed (73) hide show
  1. package/README.md +12 -0
  2. package/build/commands/ai/chat.d.ts.map +1 -1
  3. package/build/commands/ai/chat.js +6 -0
  4. package/build/commands/ai/message.d.ts.map +1 -1
  5. package/build/commands/ai/message.js +10 -0
  6. package/build/commands/ai/release.d.ts.map +1 -1
  7. package/build/commands/ai/release.js +29 -2
  8. package/build/commands/ask/argvFlags.d.ts +18 -0
  9. package/build/commands/ask/argvFlags.d.ts.map +1 -0
  10. package/build/commands/ask/argvFlags.js +46 -0
  11. package/build/commands/ask/artifacts.d.ts +2 -0
  12. package/build/commands/ask/artifacts.d.ts.map +1 -0
  13. package/build/commands/ask/artifacts.js +100 -0
  14. package/build/commands/ask/binding.d.ts +13 -0
  15. package/build/commands/ask/binding.d.ts.map +1 -0
  16. package/build/commands/ask/binding.js +19 -0
  17. package/build/commands/ask/claudeConfig.d.ts +23 -0
  18. package/build/commands/ask/claudeConfig.d.ts.map +1 -0
  19. package/build/commands/ask/claudeConfig.js +33 -0
  20. package/build/commands/ask/completer.d.ts +48 -0
  21. package/build/commands/ask/completer.d.ts.map +1 -0
  22. package/build/commands/ask/completer.js +217 -0
  23. package/build/commands/ask/history.d.ts +32 -0
  24. package/build/commands/ask/history.d.ts.map +1 -0
  25. package/build/commands/ask/history.js +122 -0
  26. package/build/commands/ask/index.d.ts +14 -0
  27. package/build/commands/ask/index.d.ts.map +1 -0
  28. package/build/commands/ask/index.js +111 -0
  29. package/build/commands/ask/localHarness.d.ts +225 -0
  30. package/build/commands/ask/localHarness.d.ts.map +1 -0
  31. package/build/commands/ask/localHarness.js +1074 -0
  32. package/build/commands/ask/messageStream.d.ts +33 -0
  33. package/build/commands/ask/messageStream.d.ts.map +1 -0
  34. package/build/commands/ask/messageStream.js +114 -0
  35. package/build/commands/ask/persist.d.ts +5 -0
  36. package/build/commands/ask/persist.d.ts.map +1 -0
  37. package/build/commands/ask/persist.js +31 -0
  38. package/build/commands/ask/pnpmShim.d.ts +20 -0
  39. package/build/commands/ask/pnpmShim.d.ts.map +1 -0
  40. package/build/commands/ask/pnpmShim.js +39 -0
  41. package/build/commands/ask/prompt.d.ts +20 -0
  42. package/build/commands/ask/prompt.d.ts.map +1 -0
  43. package/build/commands/ask/prompt.js +228 -0
  44. package/build/commands/ask/rewriteChatFlag.d.ts +9 -0
  45. package/build/commands/ask/rewriteChatFlag.d.ts.map +1 -0
  46. package/build/commands/ask/rewriteChatFlag.js +26 -0
  47. package/build/commands/ask/session.d.ts +94 -0
  48. package/build/commands/ask/session.d.ts.map +1 -0
  49. package/build/commands/ask/session.js +965 -0
  50. package/build/commands/ask/slash.d.ts +20 -0
  51. package/build/commands/ask/slash.d.ts.map +1 -0
  52. package/build/commands/ask/slash.js +73 -0
  53. package/build/commands/ask/state.d.ts +9 -0
  54. package/build/commands/ask/state.d.ts.map +1 -0
  55. package/build/commands/ask/state.js +18 -0
  56. package/build/commands/bookDemo.d.ts +14 -0
  57. package/build/commands/bookDemo.d.ts.map +1 -0
  58. package/build/commands/bookDemo.js +100 -0
  59. package/build/commands/runHandler.d.ts +9 -1
  60. package/build/commands/runHandler.d.ts.map +1 -1
  61. package/build/commands/runHandler.js +16 -1
  62. package/build/index.js +56 -18
  63. package/build/paletteLayout.d.ts +6 -1
  64. package/build/paletteLayout.d.ts.map +1 -1
  65. package/build/paletteLayout.js +35 -9
  66. package/build/utils/cdkState.d.ts +17 -4
  67. package/build/utils/cdkState.d.ts.map +1 -1
  68. package/build/utils/cdkState.js +26 -4
  69. package/build/utils/loadCompiledAgent.d.ts.map +1 -1
  70. package/build/utils/loadCompiledAgent.js +51 -11
  71. package/build/utils/prompt.d.ts.map +1 -1
  72. package/build/utils/prompt.js +4 -1
  73. package/package.json +8 -4
@@ -0,0 +1,217 @@
1
+ import { existsSync, readdirSync, statSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { basename, dirname, isAbsolute, join, relative, resolve, sep, } from "node:path";
4
+ /** Chat slash commands offered on Tab when the line is `/…`. */
5
+ export const ASK_SLASH_COMMANDS = [
6
+ "/help",
7
+ "/clear",
8
+ "/new",
9
+ "/agent",
10
+ "/cancel",
11
+ "/exit",
12
+ ];
13
+ /**
14
+ * Decide whether a `/…` token is a chat command, a filesystem path, or
15
+ * neither.
16
+ *
17
+ * The completer and the slash parser both need this answer, and they must
18
+ * agree: anything the completer offers has to be accepted when the user
19
+ * presses Enter on it. Written twice, the two drifted — `/tmp` completed as a
20
+ * path and was then answered with "is not a chat command".
21
+ *
22
+ * Shape settles most tokens, but a bare `/word` is ambiguous: `/tmp` and
23
+ * `/orchestration` are spelled the same. Only the filesystem separates them,
24
+ * so an unknown word is looked up — cheap, and reached only for a single
25
+ * bare word that no command matches.
26
+ */
27
+ export function classifySlashToken(token) {
28
+ // A separator beyond the leading one is unambiguously a path.
29
+ if (token.slice(1).includes("/") === true) {
30
+ return "path";
31
+ }
32
+ // Still a command while it prefixes one (`/he` → `/help`).
33
+ if (ASK_SLASH_COMMANDS.some((command) => command.startsWith(token)) === true) {
34
+ return "command";
35
+ }
36
+ // A bare word is only a path if one exists — `/deploy-prod` is a typo worth
37
+ // a card, not prose to spend a turn on.
38
+ return existsSync(token) === true ? "path" : "unknown";
39
+ }
40
+ /** Max rows shown in the live Ask suggestion menu. */
41
+ export const ASK_COMPLETION_MENU_LIMIT = 8;
42
+ /**
43
+ * Tab completer for the Ask prompt: slash commands when the line is `/…`,
44
+ * otherwise local path completion for a path-like last token.
45
+ */
46
+ export function createAskCompleter(cwd) {
47
+ return (line) => {
48
+ const slash = completeSlash(line);
49
+ if (slash !== undefined) {
50
+ return slash;
51
+ }
52
+ return completePath(line, cwd);
53
+ };
54
+ }
55
+ /**
56
+ * Matches for the live suggestion menu (slash commands, else path hits).
57
+ * Empty when the line is plain prose.
58
+ */
59
+ export function listCompletions(line, cwd) {
60
+ const slash = completeSlash(line);
61
+ if (slash !== undefined) {
62
+ return slash[0];
63
+ }
64
+ const [hits] = completePath(line, cwd);
65
+ return hits;
66
+ }
67
+ /**
68
+ * Replace the completed span with `completion`: whole line for slash
69
+ * commands, last token for paths.
70
+ */
71
+ export function applyCompletion(line, completion) {
72
+ if (completeSlash(line) !== undefined) {
73
+ return completion;
74
+ }
75
+ const token = lastToken(line);
76
+ if (token === undefined) {
77
+ return completion;
78
+ }
79
+ return `${line.slice(0, line.length - token.length)}${completion}`;
80
+ }
81
+ /**
82
+ * `/hel` → `/help`. Only when the whole line is a single `/token` (no spaces)
83
+ * and it matches a chat command prefix. `/tmp` falls through to path completion.
84
+ */
85
+ export function completeSlash(line) {
86
+ if (/^\/[A-Za-z]*$/.test(line) === false) {
87
+ return undefined;
88
+ }
89
+ const hits = ASK_SLASH_COMMANDS.filter((command) => command.startsWith(line));
90
+ if (hits.length === 0) {
91
+ return undefined;
92
+ }
93
+ return [[...hits], line];
94
+ }
95
+ /**
96
+ * Complete the last whitespace-separated token when it looks like a path.
97
+ * Exported for tests.
98
+ */
99
+ export function completePath(line, cwd) {
100
+ const token = lastToken(line);
101
+ if (token === undefined || isPathLike(token) === false) {
102
+ return [[], line];
103
+ }
104
+ const expanded = expandHome(token);
105
+ const absoluteBase = isAbsolute(expanded) ? expanded : resolve(cwd, expanded);
106
+ let dir;
107
+ let namePrefix;
108
+ // `.`, `..` and `~` name a directory to look inside, the same as a trailing
109
+ // separator. Splitting them with dirname/basename instead would complete
110
+ // siblings of the current, parent or home directory.
111
+ if (token.endsWith("/") ||
112
+ token.endsWith(sep) ||
113
+ token === "." ||
114
+ token === ".." ||
115
+ token === "~") {
116
+ dir = absoluteBase;
117
+ namePrefix = "";
118
+ }
119
+ else {
120
+ dir = dirname(absoluteBase);
121
+ namePrefix = basename(absoluteBase);
122
+ }
123
+ let entries;
124
+ try {
125
+ entries = readdirSync(dir);
126
+ }
127
+ catch {
128
+ return [[], token];
129
+ }
130
+ const hits = [];
131
+ for (const entry of entries) {
132
+ if (entry.startsWith(namePrefix) === false) {
133
+ continue;
134
+ }
135
+ // Hide dotfiles until the name being typed starts with a dot. Listing a
136
+ // directory (`.`, `~`, a trailing slash) has an empty prefix, so those
137
+ // stay hidden there too.
138
+ if (entry.startsWith(".") === true &&
139
+ namePrefix.startsWith(".") === false) {
140
+ continue;
141
+ }
142
+ const absoluteHit = join(dir, entry);
143
+ let display = collapseHome(toDisplayPath(absoluteHit, cwd, token));
144
+ try {
145
+ if (statSync(absoluteHit).isDirectory() === true) {
146
+ display = display.endsWith("/") ? display : `${display}/`;
147
+ }
148
+ }
149
+ catch {
150
+ // Skip unreadable entries.
151
+ continue;
152
+ }
153
+ hits.push(display);
154
+ }
155
+ hits.sort((a, b) => a.localeCompare(b));
156
+ return [hits, token];
157
+ }
158
+ function lastToken(line) {
159
+ const match = /(?:^|\s)(\S+)$/.exec(line);
160
+ if (match === null) {
161
+ return undefined;
162
+ }
163
+ return match[1];
164
+ }
165
+ function isPathLike(token) {
166
+ if (token.startsWith("./") === true ||
167
+ token.startsWith("../") === true ||
168
+ token.startsWith("~/") === true ||
169
+ token === "~" ||
170
+ token === "." ||
171
+ token === "..") {
172
+ return true;
173
+ }
174
+ if (token.startsWith("/") === true) {
175
+ return classifySlashToken(token) !== "command";
176
+ }
177
+ return token.includes("/");
178
+ }
179
+ function expandHome(token) {
180
+ if (token === "~") {
181
+ return homedir();
182
+ }
183
+ if (token.startsWith("~/") === true) {
184
+ return join(homedir(), token.slice(2));
185
+ }
186
+ return token;
187
+ }
188
+ function collapseHome(path) {
189
+ const home = homedir();
190
+ if (path === home) {
191
+ return "~";
192
+ }
193
+ if (path.startsWith(`${home}/`) === true) {
194
+ return `~/${path.slice(home.length + 1)}`;
195
+ }
196
+ return path;
197
+ }
198
+ /**
199
+ * Prefer a relative form when the typed token was relative; keep absolute /
200
+ * home forms when the user started that way.
201
+ */
202
+ function toDisplayPath(absolute, cwd, typedToken) {
203
+ if (typedToken.startsWith("~/") === true || typedToken === "~") {
204
+ return absolute;
205
+ }
206
+ if (isAbsolute(typedToken) === true) {
207
+ return absolute;
208
+ }
209
+ const rel = relative(cwd, absolute);
210
+ const display = rel === "" ? "." : rel;
211
+ if (typedToken.startsWith("./") === true &&
212
+ display.startsWith(".") === false &&
213
+ isAbsolute(display) === false) {
214
+ return `./${display}`;
215
+ }
216
+ return display;
217
+ }
@@ -0,0 +1,32 @@
1
+ import type { Api } from "../../api.js";
2
+ /**
3
+ * The earlier turns of a chat, rendered for a Claude Code session that has
4
+ * never seen them.
5
+ *
6
+ * Local Ask starts a fresh harness session on every CLI run — the harness can
7
+ * only resume Claude Code by rerunning it, and a rerun cancels whatever tool
8
+ * approval is in flight, which Claude Code then records as the user refusing
9
+ * the tool. Continuity therefore comes from the chat history the API already
10
+ * has, replayed into the first prompt.
11
+ *
12
+ * Best effort: a chat whose history cannot be read is answered without it
13
+ * rather than not at all.
14
+ */
15
+ export declare function loadChatHistoryPreamble(input: {
16
+ api: Api;
17
+ chatUuid: string;
18
+ }): Promise<string | undefined>;
19
+ type HistoryMessage = {
20
+ type: string;
21
+ status?: string;
22
+ parts: Array<{
23
+ type: string;
24
+ }>;
25
+ };
26
+ /**
27
+ * Exported for tests. Takes the API's messages oldest-first and returns the
28
+ * preamble, or `undefined` when there is nothing worth replaying.
29
+ */
30
+ export declare function renderHistoryPreamble(messages: readonly HistoryMessage[]): string | undefined;
31
+ export {};
32
+ //# sourceMappingURL=history.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"history.d.ts","sourceRoot":"","sources":["../../../src/commands/ask/history.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,cAAc,CAAC;AA0BxC;;;;;;;;;;;;GAYG;AACH,wBAAsB,uBAAuB,CAAC,KAAK,EAAE;IACnD,GAAG,EAAE,GAAG,CAAC;IACT,QAAQ,EAAE,MAAM,CAAC;CAClB,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAU9B;AAED,KAAK,cAAc,GAAG;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CAChC,CAAC;AAEF;;;GAGG;AACH,wBAAgB,qBAAqB,CACnC,QAAQ,EAAE,SAAS,cAAc,EAAE,GAClC,MAAM,GAAG,SAAS,CA6CpB"}
@@ -0,0 +1,122 @@
1
+ /**
2
+ * How many of the newest messages to replay. A chat far longer than this is
3
+ * past the point where replaying all of it helps: the recent turns carry the
4
+ * thread.
5
+ */
6
+ const HISTORY_MESSAGE_LIMIT = 40;
7
+ /** Chosen so that all but the longest chats are read in a single request. */
8
+ const HISTORY_PAGE_SIZE = 500;
9
+ /** Stop walking rather than paging a pathological chat forever. */
10
+ const HISTORY_MAX_PAGES = 20;
11
+ /**
12
+ * Budget for the replayed transcript. Claude Code prices the preamble as
13
+ * ordinary input on the first turn of the session, so this is a real cost and
14
+ * not just a context-window concern.
15
+ */
16
+ const HISTORY_MAX_CHARS = 20_000;
17
+ /**
18
+ * The earlier turns of a chat, rendered for a Claude Code session that has
19
+ * never seen them.
20
+ *
21
+ * Local Ask starts a fresh harness session on every CLI run — the harness can
22
+ * only resume Claude Code by rerunning it, and a rerun cancels whatever tool
23
+ * approval is in flight, which Claude Code then records as the user refusing
24
+ * the tool. Continuity therefore comes from the chat history the API already
25
+ * has, replayed into the first prompt.
26
+ *
27
+ * Best effort: a chat whose history cannot be read is answered without it
28
+ * rather than not at all.
29
+ */
30
+ export async function loadChatHistoryPreamble(input) {
31
+ let messages;
32
+ try {
33
+ messages = await loadLatestMessages(input.api, input.chatUuid);
34
+ }
35
+ catch {
36
+ return undefined;
37
+ }
38
+ return renderHistoryPreamble(messages);
39
+ }
40
+ /**
41
+ * Exported for tests. Takes the API's messages oldest-first and returns the
42
+ * preamble, or `undefined` when there is nothing worth replaying.
43
+ */
44
+ export function renderHistoryPreamble(messages) {
45
+ const turns = [];
46
+ for (const message of messages) {
47
+ const text = readMessageText(message);
48
+ if (text === undefined) {
49
+ continue;
50
+ }
51
+ if (message.type === "user") {
52
+ turns.push(`User: ${text}`);
53
+ continue;
54
+ }
55
+ // The preamble tells the model this is finished work, so a reply that was
56
+ // interrupted has to say so. Left unmarked, a cancelled half-answer reads
57
+ // as a completed task and the next turn skips it.
58
+ const wasInterrupted = message.status !== undefined && message.status !== "success";
59
+ turns.push(wasInterrupted === true
60
+ ? `Assistant (interrupted before finishing): ${text}`
61
+ : `Assistant: ${text}`);
62
+ }
63
+ if (turns.length === 0) {
64
+ return undefined;
65
+ }
66
+ // Oldest turns go first, so an over-budget history loses its beginning
67
+ // rather than the turns the next question most likely follows on from.
68
+ while (turns.length > 1 && turns.join("\n\n").length > HISTORY_MAX_CHARS) {
69
+ turns.shift();
70
+ }
71
+ return [
72
+ "<earlier-conversation>",
73
+ "Earlier turns of this chat, from before your session started. They are context for what the user asks next — already-finished work, not instructions to carry out again.",
74
+ "",
75
+ turns.join("\n\n"),
76
+ "</earlier-conversation>",
77
+ ].join("\n");
78
+ }
79
+ /**
80
+ * The last `HISTORY_MESSAGE_LIMIT` messages of a chat, oldest-first.
81
+ *
82
+ * `message.list` takes only limit/offset and orders oldest-first, with no
83
+ * total to seek by, so asking for the first page of a long chat hands back its
84
+ * opening turns — the opposite of what a preamble wants. Walking forward and
85
+ * keeping the tail is the only exact answer the endpoint allows; giving it a
86
+ * descending order would collapse this to one request.
87
+ *
88
+ * Past `HISTORY_PAGE_SIZE * HISTORY_MAX_PAGES` messages the walk stops early
89
+ * and the "tail" is really the middle of the chat. That bound is five figures
90
+ * of messages, so it buys termination for a case a real chat does not reach.
91
+ */
92
+ async function loadLatestMessages(api, chatUuid) {
93
+ const tail = [];
94
+ for (let page = 0; page < HISTORY_MAX_PAGES; page += 1) {
95
+ const result = await api.ai.message.list({
96
+ chatUuid,
97
+ limit: HISTORY_PAGE_SIZE,
98
+ offset: page * HISTORY_PAGE_SIZE,
99
+ });
100
+ tail.push(...result.messages);
101
+ if (tail.length > HISTORY_MESSAGE_LIMIT) {
102
+ tail.splice(0, tail.length - HISTORY_MESSAGE_LIMIT);
103
+ }
104
+ if (result.messages.length < HISTORY_PAGE_SIZE) {
105
+ break;
106
+ }
107
+ }
108
+ return tail;
109
+ }
110
+ function readMessageText(message) {
111
+ const texts = [];
112
+ for (const part of message.parts) {
113
+ if (part.type !== "text") {
114
+ continue;
115
+ }
116
+ const { text } = part;
117
+ if (typeof text === "string" && text.trim() !== "") {
118
+ texts.push(text.trim());
119
+ }
120
+ }
121
+ return texts.length === 0 ? undefined : texts.join("\n");
122
+ }
@@ -0,0 +1,14 @@
1
+ import type { Command } from "commander";
2
+ import type { Api } from "../../api.js";
3
+ export type AskOptions = {
4
+ continue?: boolean;
5
+ print?: boolean;
6
+ agentUuid?: string;
7
+ chatUuid?: string;
8
+ dir?: string;
9
+ };
10
+ export type AskCommandDeps = {
11
+ openPalette: () => Promise<void>;
12
+ };
13
+ export declare function registerAskCommand(program: Command, getApi: () => Api, deps: AskCommandDeps): void;
14
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/commands/ask/index.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEzC,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,cAAc,CAAC;AAOxC,MAAM,MAAM,UAAU,GAAG;IACvB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,GAAG,CAAC,EAAE,MAAM,CAAC;CACd,CAAC;AAEF,MAAM,MAAM,cAAc,GAAG;IAC3B,WAAW,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAClC,CAAC;AAEF,wBAAgB,kBAAkB,CAChC,OAAO,EAAE,OAAO,EAChB,MAAM,EAAE,MAAM,GAAG,EACjB,IAAI,EAAE,cAAc,GACnB,IAAI,CAwIN"}
@@ -0,0 +1,111 @@
1
+ import { resolve } from "node:path";
2
+ import { isInteractive } from "@cargo-ai/cdk/cli";
3
+ import { parseBooleanOption } from "../../utils/booleanOption.js";
4
+ import { ExitCodes, failWith } from "../runHandler.js";
5
+ import { loadAskState } from "./persist.js";
6
+ import { AskInterrupted } from "./prompt.js";
7
+ import { doesChatExist, runAskPrint, runAskSession } from "./session.js";
8
+ export function registerAskCommand(program, getApi, deps) {
9
+ program
10
+ .command("ask")
11
+ .description("Chat in the palette tree. Defaults to Claude Code on this machine; optional --agent-uuid streams as that agent.")
12
+ .argument("[message...]", "Question to send as the first message")
13
+ .option("--continue [true|false]", "Reopen the last chat from this machine (defaults to true when passed bare; starts a new chat when there is none)", parseBooleanOption)
14
+ .option("--print [true|false]", "One turn, JSON on stdout (defaults to true when passed bare)", parseBooleanOption)
15
+ .option("--agent-uuid <uuid>", "Talk to this workspace agent instead of the local Claude Code default")
16
+ .option("--chat-uuid <uuid>", "Existing chat to continue")
17
+ .option("--dir <path>", "Working directory for the local Claude Code workspace (default: cwd)")
18
+ .addHelpText("after", `
19
+ Examples:
20
+ $ cargo-ai ask
21
+ $ cargo-ai ask "who touched Acme last week"
22
+ $ cargo-ai ask --continue
23
+ $ cargo-ai ask --print "summarise this workspace"
24
+ $ cargo-ai --chat
25
+
26
+ Default Ask is Claude Code on this machine (--dir, default cwd), with no
27
+ agent. --continue reopens the last chat thread but does not reattach an
28
+ agent. Workspace agents are optional via --agent-uuid or /agent (remote
29
+ turns use POST /v1/ai/messages/stream, like the webapp chat).
30
+
31
+ Ask continues the palette tree and streams the reply as it arrives:
32
+ ◇ your message
33
+ 🧱 reply
34
+ ◆ to type the next line (slash and path suggestions as you type)
35
+ Empty Enter or /exit returns to the menu. Ctrl-C cancels the turn in flight
36
+ and comes back to the prompt. Slash commands (/help, /clear, /new, /agent,
37
+ /cancel, /exit) never reach the model. Cargo CLI commands stay in the
38
+ palette (/orchestration, …).
39
+
40
+ Local turns read files freely and ask before anything that writes or runs a
41
+ shell command; they can also query workspace data read-only (models, schema,
42
+ SQL).
43
+
44
+ --print is the non-interactive path: one turn, JSON on stdout. It denies
45
+ tools that need approval, since nobody is there to answer. Pipes, CI, and
46
+ coding agents must use it.`)
47
+ .action(async (messageParts, opts) => {
48
+ const message = messageParts.length > 0 ? messageParts.join(" ") : undefined;
49
+ const api = getApi();
50
+ const dir = opts.dir !== undefined ? resolve(opts.dir) : process.cwd();
51
+ let chatUuid = opts.chatUuid;
52
+ const agentUuid = opts.agentUuid;
53
+ // Resolved before the --print branch so a non-interactive turn can
54
+ // resume the last thread too. Resumes the chat thread only: the agent
55
+ // stays unset so Ask keeps the local Claude Code default unless the
56
+ // user passed --agent-uuid.
57
+ //
58
+ // The saved chat has to still be there. Deleting one is a soft delete,
59
+ // so it stops being readable while its uuid keeps the primary key —
60
+ // carrying it forward would fail the create on the first question, and
61
+ // the palette opens Ask with --continue every time. A chat that is gone
62
+ // means a fresh thread, not a resurrection of its uuid.
63
+ if (opts.continue === true && chatUuid === undefined) {
64
+ const state = loadAskState();
65
+ if (state !== undefined &&
66
+ (await doesChatExist(api, state.chatUuid)) === true) {
67
+ chatUuid = state.chatUuid;
68
+ }
69
+ }
70
+ if (opts.print === true) {
71
+ if (message === undefined) {
72
+ failWith("--print needs a message.", {
73
+ code: ExitCodes.InvalidUsage,
74
+ });
75
+ }
76
+ await runAskPrint({
77
+ api,
78
+ message,
79
+ agentUuid,
80
+ chatUuid,
81
+ dir,
82
+ });
83
+ return;
84
+ }
85
+ if (isInteractive() === false || process.stdout.isTTY !== true) {
86
+ failWith("The chat needs a terminal. Re-run with --print for one JSON turn.", { code: ExitCodes.InvalidUsage });
87
+ }
88
+ try {
89
+ await runAskSession({
90
+ api,
91
+ message,
92
+ continueChat: chatUuid !== undefined,
93
+ agentUuid,
94
+ chatUuid,
95
+ dir,
96
+ });
97
+ }
98
+ catch (error) {
99
+ // Ctrl-C at the prompt. The session has torn the harness down on its
100
+ // way out, so all that is left is the conventional SIGINT status —
101
+ // and not dropping the user back into the palette they just left.
102
+ if (error instanceof AskInterrupted) {
103
+ process.exit(130);
104
+ }
105
+ throw error;
106
+ }
107
+ if (process.env["CARGO_PALETTE"] !== "1") {
108
+ await deps.openPalette();
109
+ }
110
+ });
111
+ }