@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.
- package/README.md +12 -0
- package/build/commands/ai/chat.d.ts.map +1 -1
- package/build/commands/ai/chat.js +6 -0
- package/build/commands/ai/message.d.ts.map +1 -1
- package/build/commands/ai/message.js +10 -0
- package/build/commands/ai/release.d.ts.map +1 -1
- package/build/commands/ai/release.js +29 -2
- package/build/commands/ask/argvFlags.d.ts +18 -0
- package/build/commands/ask/argvFlags.d.ts.map +1 -0
- package/build/commands/ask/argvFlags.js +46 -0
- package/build/commands/ask/artifacts.d.ts +2 -0
- package/build/commands/ask/artifacts.d.ts.map +1 -0
- package/build/commands/ask/artifacts.js +100 -0
- package/build/commands/ask/binding.d.ts +13 -0
- package/build/commands/ask/binding.d.ts.map +1 -0
- package/build/commands/ask/binding.js +19 -0
- package/build/commands/ask/claudeConfig.d.ts +23 -0
- package/build/commands/ask/claudeConfig.d.ts.map +1 -0
- package/build/commands/ask/claudeConfig.js +33 -0
- package/build/commands/ask/completer.d.ts +48 -0
- package/build/commands/ask/completer.d.ts.map +1 -0
- package/build/commands/ask/completer.js +217 -0
- package/build/commands/ask/history.d.ts +32 -0
- package/build/commands/ask/history.d.ts.map +1 -0
- package/build/commands/ask/history.js +122 -0
- package/build/commands/ask/index.d.ts +14 -0
- package/build/commands/ask/index.d.ts.map +1 -0
- package/build/commands/ask/index.js +111 -0
- package/build/commands/ask/localHarness.d.ts +225 -0
- package/build/commands/ask/localHarness.d.ts.map +1 -0
- package/build/commands/ask/localHarness.js +1074 -0
- package/build/commands/ask/messageStream.d.ts +33 -0
- package/build/commands/ask/messageStream.d.ts.map +1 -0
- package/build/commands/ask/messageStream.js +114 -0
- package/build/commands/ask/persist.d.ts +5 -0
- package/build/commands/ask/persist.d.ts.map +1 -0
- package/build/commands/ask/persist.js +31 -0
- package/build/commands/ask/pnpmShim.d.ts +20 -0
- package/build/commands/ask/pnpmShim.d.ts.map +1 -0
- package/build/commands/ask/pnpmShim.js +39 -0
- package/build/commands/ask/prompt.d.ts +20 -0
- package/build/commands/ask/prompt.d.ts.map +1 -0
- package/build/commands/ask/prompt.js +228 -0
- package/build/commands/ask/rewriteChatFlag.d.ts +9 -0
- package/build/commands/ask/rewriteChatFlag.d.ts.map +1 -0
- package/build/commands/ask/rewriteChatFlag.js +26 -0
- package/build/commands/ask/session.d.ts +94 -0
- package/build/commands/ask/session.d.ts.map +1 -0
- package/build/commands/ask/session.js +965 -0
- package/build/commands/ask/slash.d.ts +20 -0
- package/build/commands/ask/slash.d.ts.map +1 -0
- package/build/commands/ask/slash.js +73 -0
- package/build/commands/ask/state.d.ts +9 -0
- package/build/commands/ask/state.d.ts.map +1 -0
- package/build/commands/ask/state.js +18 -0
- package/build/commands/bookDemo.d.ts +14 -0
- package/build/commands/bookDemo.d.ts.map +1 -0
- package/build/commands/bookDemo.js +100 -0
- package/build/commands/runHandler.d.ts +9 -1
- package/build/commands/runHandler.d.ts.map +1 -1
- package/build/commands/runHandler.js +16 -1
- package/build/index.js +56 -18
- package/build/paletteLayout.d.ts +6 -1
- package/build/paletteLayout.d.ts.map +1 -1
- package/build/paletteLayout.js +35 -9
- package/build/utils/cdkState.d.ts +17 -4
- package/build/utils/cdkState.d.ts.map +1 -1
- package/build/utils/cdkState.js +26 -4
- package/build/utils/loadCompiledAgent.d.ts.map +1 -1
- package/build/utils/loadCompiledAgent.js +51 -11
- package/build/utils/prompt.d.ts.map +1 -1
- package/build/utils/prompt.js +4 -1
- 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
|
+
}
|