@pentoshi/clai 0.11.2 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent/loop-guard.d.ts +6 -0
- package/dist/agent/loop-guard.js +28 -3
- package/dist/agent/loop-guard.js.map +1 -1
- package/dist/agent/runner.d.ts +7 -0
- package/dist/agent/runner.js +101 -9
- package/dist/agent/runner.js.map +1 -1
- package/dist/commands/update.js +1 -1
- package/dist/prompts/index.d.ts +1 -1
- package/dist/prompts/index.js +63 -7
- package/dist/prompts/index.js.map +1 -1
- package/dist/repl.d.ts +2 -0
- package/dist/repl.js +126 -6
- package/dist/repl.js.map +1 -1
- package/dist/safety/classifier.js +26 -0
- package/dist/safety/classifier.js.map +1 -1
- package/dist/store/config.js +1 -1
- package/dist/store/config.js.map +1 -1
- package/dist/tools/fs.d.ts +15 -0
- package/dist/tools/fs.js +72 -2
- package/dist/tools/fs.js.map +1 -1
- package/dist/tools/registry.js +9 -1
- package/dist/tools/registry.js.map +1 -1
- package/dist/ui/banner.js +2 -2
- package/dist/ui/banner.js.map +1 -1
- package/dist/ui/mentions.d.ts +66 -0
- package/dist/ui/mentions.js +366 -0
- package/dist/ui/mentions.js.map +1 -0
- package/package.json +3 -5
package/dist/prompts/index.js
CHANGED
|
@@ -10,14 +10,15 @@ For every user request, respond with:
|
|
|
10
10
|
4. Security caveats, OPSEC notes, and safer alternatives where applicable
|
|
11
11
|
|
|
12
12
|
When advising on pentesting, follow standard methodology (recon → enumeration → exploitation → post-exploitation). Always note which phase the user is in and suggest logical next steps.`;
|
|
13
|
-
const agentPrompt = `You are clai, a terminal AI agent
|
|
13
|
+
const agentPrompt = `You are clai, a terminal AI agent. You are a capable software engineer AND a cybersecurity/pentesting/sysadmin specialist. You can write code, scaffold and modify projects, edit files, run commands, and do recon/enumeration/exploitation work — like a coding agent (Claude Code / opencode) fused with a security toolkit.
|
|
14
14
|
OS: {{os}} | Shell: {{shell}} | CWD: {{cwd}}
|
|
15
15
|
Current date/time: {{datetime}}
|
|
16
16
|
|
|
17
17
|
TOOLS (use EXACT arg names — wrong names = failure):
|
|
18
18
|
- shell.exec: {"command":"<cmd>"} — run any shell command. Optional: {"command":"...","cwd":"/path","timeoutMs":300000}
|
|
19
19
|
- fs.read: {"path":"<file>"} — read a file
|
|
20
|
-
- fs.write: {"path":"<file>","content":"<data>"} — write a file
|
|
20
|
+
- fs.write: {"path":"<file>","content":"<data>"} — write a single file
|
|
21
|
+
- fs.writeMany: {"files":[{"path":"<file>","content":"<data>"}, ...]} — write MANY files in ONE call (up to 50). USE THIS to scaffold a project (e.g. a React/Express app) instead of one fs.write per file — it saves steps and is the preferred way to create multiple files at once. Parent dirs are auto-created.
|
|
21
22
|
- fs.list: {"path":"<dir>"} — list directory
|
|
22
23
|
- fs.search: {"pattern":"<regex>","path":"<dir>"} — search file CONTENTS (NOT filenames)
|
|
23
24
|
- pkg.install: {"tool":"<name>"} — install package (only if user asks or command not found)
|
|
@@ -133,12 +134,67 @@ RESILIENT ERROR HANDLING:
|
|
|
133
134
|
- Chain: fail → diagnose → fix/adapt → retry. Never stop at the first error.
|
|
134
135
|
|
|
135
136
|
TASK PLANNING:
|
|
136
|
-
-
|
|
137
|
-
|
|
138
|
-
-
|
|
139
|
-
-
|
|
137
|
+
- BEFORE acting on any non-trivial task, decide: is this one quick step, or multiple steps?
|
|
138
|
+
· Simple (single command, quick lookup, one file) → just execute immediately, no plan.
|
|
139
|
+
· Multi-step (scaffold a project, refactor across files, full recon, build a feature) → FIRST
|
|
140
|
+
write a short numbered plan (3-7 steps) in plain text, THEN execute the steps one by one.
|
|
141
|
+
- State the plan to the user before the first tool call so they can follow along. Example:
|
|
142
|
+
Plan:
|
|
143
|
+
1. Inspect the current directory to understand what's here
|
|
144
|
+
2. Read package.json / key files for context
|
|
145
|
+
3. Scaffold the missing files
|
|
146
|
+
4. Verify it builds/runs
|
|
147
|
+
Then proceed with step 1. Keep the plan concise — do not over-plan trivial work.
|
|
148
|
+
- As you finish steps, briefly note progress ("done 1-2, starting 3"). Adapt the plan if a step fails.
|
|
149
|
+
- You OWN the plan — nothing is predetermined. This applies to BOTH coding and security tasks
|
|
150
|
+
(e.g. a layered recon → enumeration → reporting flow is a plan too).
|
|
140
151
|
|
|
141
|
-
|
|
152
|
+
WORKING ON CODE & PROJECTS (act like a coding agent):
|
|
153
|
+
- "create X here" / "build X" / "add Y to this project" means work in the CURRENT directory ({{cwd}}).
|
|
154
|
+
- UNDERSTAND BEFORE YOU WRITE. Do not dump a generic template. First gather just enough context:
|
|
155
|
+
· fs.list the current directory (and key subdirs) to see what already exists.
|
|
156
|
+
· fs.read the files that matter (package.json, config, entry points, the file being changed).
|
|
157
|
+
· Use tool.batch to read several files at once instead of many sequential reads.
|
|
158
|
+
· Detect the existing stack/tooling (e.g. Vite vs CRA, the framework, the package manager) and
|
|
159
|
+
MATCH it. Never replace a project's tooling with a different one unless asked.
|
|
160
|
+
- Keep context lean: read what you need, not the whole tree. Skip node_modules, dist, .git, lockfiles.
|
|
161
|
+
- For a brand-new project, pick sensible modern defaults and say which you chose (e.g. "scaffolding
|
|
162
|
+
with Vite + React" ) — then create a MINIMAL working skeleton, not an overstuffed boilerplate.
|
|
163
|
+
- fs.write creates parent directories automatically — you can write "src/App.jsx" directly without a
|
|
164
|
+
separate mkdir. Do NOT call mkdir before fs.write.
|
|
165
|
+
- SCAFFOLD WITH fs.writeMany: when a task needs several files (a React app, an Express server, a CLI),
|
|
166
|
+
create them ALL in ONE fs.writeMany call instead of many fs.write calls. This is faster and avoids
|
|
167
|
+
running out of steps mid-build.
|
|
168
|
+
- NEVER rewrite a file you already wrote with identical content. After a file is saved, move to the
|
|
169
|
+
NEXT file or step. Re-writing the same file wastes steps and the build guard will block it.
|
|
170
|
+
- DO NOT claim work you did not do. Only say "dependencies installed" after pkg.install / npm install
|
|
171
|
+
actually ran and succeeded; only say "the dev server is running" after shell.start actually started
|
|
172
|
+
it. If you have not run those steps, tell the user the exact commands to run instead.
|
|
173
|
+
- After writing files, verify when practical: list the tree you created, and if there's a build/test
|
|
174
|
+
command, run it (or tell the user the exact command to run, e.g. \`npm install && npm run dev\`).
|
|
175
|
+
- Prefer fs.edit for changing existing files; use fs.write for new files or full rewrites.
|
|
176
|
+
- For multi-file scaffolds: 1) give a one-line structure overview, 2) create the minimal files, 3) summarize.
|
|
177
|
+
|
|
178
|
+
MODERN TOOLING & DEPENDENCIES (avoid deprecated/legacy setups):
|
|
179
|
+
- PREFER OFFICIAL SCAFFOLDERS over hand-writing build configs. They pull current, non-deprecated
|
|
180
|
+
dependencies and need far fewer files:
|
|
181
|
+
· React / Vue / Svelte / vanilla frontend → \`npm create vite@latest <name> -- --template react\`
|
|
182
|
+
(or react-ts, vue, svelte, etc). Do NOT hand-roll webpack + babel-loader — that drags in
|
|
183
|
+
deprecated transitive deps (inflight, rimraf@3, glob@7, old uuid) and dozens of extra packages.
|
|
184
|
+
· Next.js → \`npx create-next-app@latest\`. Vue → \`npm create vue@latest\`. Astro → \`npm create astro@latest\`.
|
|
185
|
+
· Node/Express API → a small package.json with \`"type":"module"\`, Express 5, and ES module imports.
|
|
186
|
+
- Use \`@latest\` (or a recent known-good major) when invoking scaffolders so the user gets current
|
|
187
|
+
versions, not whatever is cached.
|
|
188
|
+
- When you DO write package.json by hand, pin to current major versions and avoid abandoned packages
|
|
189
|
+
(e.g. use the built-in \`node:crypto\` randomUUID instead of the \`uuid\` package; \`rimraf\`/\`glob\` are
|
|
190
|
+
rarely needed in app code). Use ESM (\`import\`) and \`"type":"module"\` for new Node projects.
|
|
191
|
+
- Use current, non-deprecated APIs in generated code: \`createRoot\` (not \`ReactDOM.render\`), the native
|
|
192
|
+
\`fetch\` (not \`request\`/\`node-fetch\` on modern Node), \`node:\` prefixed core imports, \`Buffer.subarray\`
|
|
193
|
+
(not \`Buffer.slice\`), and \`String.prototype.replaceAll\`/\`slice\` (not \`substr\`).
|
|
194
|
+
- If a scaffolder CLI is the right move, run it with shell.exec (or shell.start for its dev server),
|
|
195
|
+
then adapt the generated files — don't fight the tool by recreating its output by hand.
|
|
196
|
+
- After install, if you see deprecation warnings for transitive deps you control, prefer a newer
|
|
197
|
+
direct dependency that doesn't pull them in rather than ignoring them.
|
|
142
198
|
- "scan my network" / "find devices" / "what's on my LAN" → net.context FIRST (gets interfaces+CIDR), then net.pingSweep with discovered CIDR.
|
|
143
199
|
- Do NOT guess 192.168.1.0/24 or any range. Always discover it via net.context.
|
|
144
200
|
- Do NOT use shell.exec for ping sweeps. Use net.pingSweep which has intelligent fallback.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/prompts/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAE/C,MAAM,SAAS,GAAG;;;;;;;;;;0LAUwK,CAAC;AAE3L,MAAM,WAAW,GAAG
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/prompts/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAE/C,MAAM,SAAS,GAAG;;;;;;;;;;0LAUwK,CAAC;AAE3L,MAAM,WAAW,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;+MA8N2L,CAAC;AAEhN,SAAS,MAAM,CAAC,QAAgB,EAAE,MAA8B;IAC9D,OAAO,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,MAAM,CAClC,CAAC,OAAO,EAAE,CAAC,GAAG,EAAE,KAAK,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,UAAU,CAAC,KAAK,GAAG,IAAI,EAAE,KAAK,CAAC,EAClE,QAAQ,CACT,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,sBAAsB,CAAC,GAAG,GAAG,IAAI,IAAI,EAAE;IACrD,MAAM,KAAK,GAAG,GAAG,CAAC,cAAc,CAAC,SAAS,EAAE;QAC1C,OAAO,EAAE,MAAM;QACf,IAAI,EAAE,SAAS;QACf,KAAK,EAAE,MAAM;QACb,GAAG,EAAE,SAAS;QACd,IAAI,EAAE,SAAS;QACf,MAAM,EAAE,SAAS;QACjB,MAAM,EAAE,SAAS;QACjB,YAAY,EAAE,OAAO;KACtB,CAAC,CAAC;IACH,OAAO,GAAG,KAAK,UAAU,GAAG,CAAC,WAAW,EAAE,GAAG,CAAC;AAChD,CAAC;AAED;;;;GAIG;AACH,MAAM,CAAC,MAAM,aAAa,GAAG,SAAS,CAAC;AACvC,MAAM,CAAC,MAAM,eAAe,GAAG,WAAW,CAAC;AAE3C,MAAM,UAAU,qBAAqB;IACnC,MAAM,MAAM,GAAG,YAAY,EAAE,CAAC;IAC9B,OAAO,MAAM,CAAC,SAAS,EAAE;QACvB,EAAE,EAAE,GAAG,MAAM,CAAC,MAAM,IAAI,MAAM,CAAC,OAAO,IAAI,MAAM,CAAC,IAAI,EAAE;QACvD,KAAK,EAAE,MAAM,CAAC,KAAK;QACnB,GAAG,EAAE,MAAM,CAAC,GAAG;QACf,QAAQ,EAAE,sBAAsB,EAAE;QAClC,SAAS,EAAE,MAAM;KAClB,CAAC,CAAC;AACL,CAAC;AAED,MAAM,UAAU,uBAAuB,CAAC,QAAgB;IACtD,MAAM,MAAM,GAAG,YAAY,EAAE,CAAC;IAC9B,OAAO,MAAM,CAAC,WAAW,EAAE;QACzB,EAAE,EAAE,GAAG,MAAM,CAAC,MAAM,IAAI,MAAM,CAAC,OAAO,IAAI,MAAM,CAAC,IAAI,EAAE;QACvD,KAAK,EAAE,MAAM,CAAC,KAAK;QACnB,GAAG,EAAE,MAAM,CAAC,GAAG;QACf,QAAQ,EAAE,sBAAsB,EAAE;QAClC,SAAS,EAAE,QAAQ;KACpB,CAAC,CAAC;AACL,CAAC"}
|
package/dist/repl.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Mode, ProviderId } from "./types.js";
|
|
2
|
+
import { type FileSuggestion } from "./ui/mentions.js";
|
|
2
3
|
export interface ReplOptions {
|
|
3
4
|
mode?: Mode | undefined;
|
|
4
5
|
provider?: ProviderId | undefined;
|
|
@@ -13,4 +14,5 @@ export interface SlashCommand {
|
|
|
13
14
|
export declare function getKnownModels(provider: string): string[];
|
|
14
15
|
export declare function getSlashCommandSuggestions(line: string): SlashCommand[];
|
|
15
16
|
export declare function renderSlashCommandMenu(line: string, suggestions: SlashCommand[], selectedIndex: number): string[];
|
|
17
|
+
export declare function renderFileMentionMenu(query: string, suggestions: FileSuggestion[], selectedIndex: number): string[];
|
|
16
18
|
export declare function startRepl(options?: ReplOptions): Promise<void>;
|
package/dist/repl.js
CHANGED
|
@@ -16,6 +16,7 @@ import { modelSupportsThinking } from "./llm/capabilities.js";
|
|
|
16
16
|
import { clearViewports, getLastViewport, getViewport, isPagerActive, listViewports, openViewportPager, toggleViewport, } from "./ui/output-pane.js";
|
|
17
17
|
import { compactMessages, estimateMessagesTokens, } from "./agent/context-manager.js";
|
|
18
18
|
import { isCtrlC, isCtrlO, isCtrlT, isEscape } from "./ui/keys.js";
|
|
19
|
+
import { getMentionQuery, findFileSuggestions, expandMentions, } from "./ui/mentions.js";
|
|
19
20
|
const slashCommands = [
|
|
20
21
|
{ command: "/ask", description: "switch to ask mode" },
|
|
21
22
|
{ command: "/agent", description: "switch to agent mode" },
|
|
@@ -303,6 +304,31 @@ export function renderSlashCommandMenu(line, suggestions, selectedIndex) {
|
|
|
303
304
|
}
|
|
304
305
|
return items;
|
|
305
306
|
}
|
|
307
|
+
export function renderFileMentionMenu(query, suggestions, selectedIndex) {
|
|
308
|
+
const cols = terminalColumns();
|
|
309
|
+
const maxWidth = Math.max(1, cols - 1);
|
|
310
|
+
if (suggestions.length === 0) {
|
|
311
|
+
return [
|
|
312
|
+
chalk.dim(fitPlain(` no files matching @${query}`, maxWidth)),
|
|
313
|
+
];
|
|
314
|
+
}
|
|
315
|
+
const termRows = process.stdout.rows || 24;
|
|
316
|
+
const maxVisible = Math.max(5, termRows - 4);
|
|
317
|
+
const visible = suggestions.slice(0, maxVisible);
|
|
318
|
+
const items = visible.map((suggestion, index) => {
|
|
319
|
+
const markerPlain = index === selectedIndex ? "›" : " ";
|
|
320
|
+
const marker = index === selectedIndex ? chalk.magenta("›") : " ";
|
|
321
|
+
const prefix = ` ${markerPlain} `;
|
|
322
|
+
const labelBudget = Math.max(1, maxWidth - prefix.length);
|
|
323
|
+
const label = fitPlain(suggestion.label, labelBudget);
|
|
324
|
+
const colored = suggestion.isDir ? chalk.cyan(label) : chalk.white(label);
|
|
325
|
+
return ` ${marker} ${colored}`;
|
|
326
|
+
});
|
|
327
|
+
if (suggestions.length > maxVisible) {
|
|
328
|
+
items.push(chalk.dim(fitPlain(` … ${suggestions.length - maxVisible} more`, maxWidth)));
|
|
329
|
+
}
|
|
330
|
+
return items;
|
|
331
|
+
}
|
|
306
332
|
function isPrintableSequence(sequence) {
|
|
307
333
|
return sequence !== undefined && /^[^\x00-\x1f\x7f]+$/u.test(sequence);
|
|
308
334
|
}
|
|
@@ -353,6 +379,7 @@ async function readPromptLine(options) {
|
|
|
353
379
|
let selectedIndex = 0;
|
|
354
380
|
let menuNavigated = false;
|
|
355
381
|
let dismissedSlashLine = null;
|
|
382
|
+
let mentionDismissed = false;
|
|
356
383
|
let historyIndex = null;
|
|
357
384
|
let historyDraft = "";
|
|
358
385
|
let lastCtrlCAt = 0;
|
|
@@ -369,12 +396,57 @@ async function readPromptLine(options) {
|
|
|
369
396
|
selectedIndex = 0;
|
|
370
397
|
return { visible: true, suggestions };
|
|
371
398
|
};
|
|
399
|
+
// File @-mention autocomplete: active when the cursor sits inside an
|
|
400
|
+
// `@partial/path` token. Mutually exclusive with the slash menu (slash
|
|
401
|
+
// requires the line to start with "/" and contain no whitespace).
|
|
402
|
+
const getMentionState = () => {
|
|
403
|
+
if (mentionDismissed || line.startsWith("/")) {
|
|
404
|
+
return { visible: false, query: "", start: 0, suggestions: [] };
|
|
405
|
+
}
|
|
406
|
+
const q = getMentionQuery(line, cursor);
|
|
407
|
+
if (!q)
|
|
408
|
+
return { visible: false, query: "", start: 0, suggestions: [] };
|
|
409
|
+
const suggestions = findFileSuggestions(q.query);
|
|
410
|
+
if (suggestions.length === 0) {
|
|
411
|
+
return { visible: false, query: q.query, start: q.start, suggestions };
|
|
412
|
+
}
|
|
413
|
+
if (selectedIndex >= suggestions.length)
|
|
414
|
+
selectedIndex = 0;
|
|
415
|
+
return { visible: true, query: q.query, start: q.start, suggestions };
|
|
416
|
+
};
|
|
417
|
+
const applyMention = (suggestion, start) => {
|
|
418
|
+
const before = line.slice(0, start);
|
|
419
|
+
const after = line.slice(cursor);
|
|
420
|
+
let insert = `@${suggestion.value}`;
|
|
421
|
+
let newCursor = before.length + insert.length;
|
|
422
|
+
if (!suggestion.isDir) {
|
|
423
|
+
// Completed a file — add a trailing space and close the menu so the
|
|
424
|
+
// user can keep typing their request.
|
|
425
|
+
insert += " ";
|
|
426
|
+
newCursor = before.length + insert.length;
|
|
427
|
+
mentionDismissed = true;
|
|
428
|
+
}
|
|
429
|
+
else {
|
|
430
|
+
// Completed a directory — keep the menu open so the user drills in.
|
|
431
|
+
mentionDismissed = false;
|
|
432
|
+
}
|
|
433
|
+
line = before + insert + after;
|
|
434
|
+
cursor = newCursor;
|
|
435
|
+
selectedIndex = 0;
|
|
436
|
+
menuNavigated = false;
|
|
437
|
+
refresh();
|
|
438
|
+
};
|
|
372
439
|
const refresh = () => {
|
|
373
440
|
const cols = terminalColumns();
|
|
374
441
|
const menu = getMenuState();
|
|
442
|
+
const mention = menu.visible
|
|
443
|
+
? { visible: false, query: "", start: 0, suggestions: [] }
|
|
444
|
+
: getMentionState();
|
|
375
445
|
const menuLines = menu.visible
|
|
376
446
|
? renderSlashCommandMenu(line, menu.suggestions, selectedIndex)
|
|
377
|
-
:
|
|
447
|
+
: mention.visible
|
|
448
|
+
? renderFileMentionMenu(mention.query, mention.suggestions, selectedIndex)
|
|
449
|
+
: [];
|
|
378
450
|
const promptRows = buildPromptRows(line, cols, true);
|
|
379
451
|
const target = promptCursorPosition(cursor, cols);
|
|
380
452
|
const blockRows = [...promptRows, ...menuLines];
|
|
@@ -402,6 +474,7 @@ async function readPromptLine(options) {
|
|
|
402
474
|
selectedIndex = 0;
|
|
403
475
|
menuNavigated = false;
|
|
404
476
|
dismissedSlashLine = null;
|
|
477
|
+
mentionDismissed = false;
|
|
405
478
|
historyIndex = null;
|
|
406
479
|
refresh();
|
|
407
480
|
};
|
|
@@ -446,6 +519,9 @@ async function readPromptLine(options) {
|
|
|
446
519
|
if (isPagerActive())
|
|
447
520
|
return;
|
|
448
521
|
const menu = getMenuState();
|
|
522
|
+
const mention = menu.visible
|
|
523
|
+
? { visible: false, query: "", start: 0, suggestions: [] }
|
|
524
|
+
: getMentionState();
|
|
449
525
|
// Cmd+C on macOS terminals is handled by the OS (it never reaches us),
|
|
450
526
|
// but some Linux terminals forward Meta+C. Treat that as a no-op so
|
|
451
527
|
// selecting + copying never breaks the REPL.
|
|
@@ -485,6 +561,10 @@ async function readPromptLine(options) {
|
|
|
485
561
|
return;
|
|
486
562
|
}
|
|
487
563
|
if (key.name === "return" || key.name === "enter") {
|
|
564
|
+
if (mention.visible && mention.suggestions.length > 0) {
|
|
565
|
+
applyMention(mention.suggestions[selectedIndex] ?? mention.suggestions[0], mention.start);
|
|
566
|
+
return;
|
|
567
|
+
}
|
|
488
568
|
const useSelection = menu.visible && (line !== "/" || menuNavigated);
|
|
489
569
|
const selectedCommand = useSelection
|
|
490
570
|
? menu.suggestions[selectedIndex]
|
|
@@ -493,6 +573,10 @@ async function readPromptLine(options) {
|
|
|
493
573
|
return;
|
|
494
574
|
}
|
|
495
575
|
if (key.name === "tab") {
|
|
576
|
+
if (mention.visible && mention.suggestions.length > 0) {
|
|
577
|
+
applyMention(mention.suggestions[selectedIndex] ?? mention.suggestions[0], mention.start);
|
|
578
|
+
return;
|
|
579
|
+
}
|
|
496
580
|
if (menu.visible && menu.suggestions.length > 0) {
|
|
497
581
|
const target = menu.suggestions[selectedIndex] ?? menu.suggestions[0];
|
|
498
582
|
editLine(target.command, target.command.length);
|
|
@@ -500,6 +584,11 @@ async function readPromptLine(options) {
|
|
|
500
584
|
return;
|
|
501
585
|
}
|
|
502
586
|
if (isEscape(key)) {
|
|
587
|
+
if (mention.visible) {
|
|
588
|
+
mentionDismissed = true;
|
|
589
|
+
refresh();
|
|
590
|
+
return;
|
|
591
|
+
}
|
|
503
592
|
if (menu.visible) {
|
|
504
593
|
dismissedSlashLine = line;
|
|
505
594
|
refresh();
|
|
@@ -507,6 +596,14 @@ async function readPromptLine(options) {
|
|
|
507
596
|
return;
|
|
508
597
|
}
|
|
509
598
|
if (key.name === "up") {
|
|
599
|
+
if (mention.visible && mention.suggestions.length > 0) {
|
|
600
|
+
selectedIndex =
|
|
601
|
+
(selectedIndex - 1 + mention.suggestions.length) %
|
|
602
|
+
mention.suggestions.length;
|
|
603
|
+
menuNavigated = true;
|
|
604
|
+
refresh();
|
|
605
|
+
return;
|
|
606
|
+
}
|
|
510
607
|
if (menu.visible && menu.suggestions.length > 0) {
|
|
511
608
|
selectedIndex =
|
|
512
609
|
(selectedIndex - 1 + menu.suggestions.length) %
|
|
@@ -532,6 +629,12 @@ async function readPromptLine(options) {
|
|
|
532
629
|
return;
|
|
533
630
|
}
|
|
534
631
|
if (key.name === "down") {
|
|
632
|
+
if (mention.visible && mention.suggestions.length > 0) {
|
|
633
|
+
selectedIndex = (selectedIndex + 1) % mention.suggestions.length;
|
|
634
|
+
menuNavigated = true;
|
|
635
|
+
refresh();
|
|
636
|
+
return;
|
|
637
|
+
}
|
|
535
638
|
if (menu.visible && menu.suggestions.length > 0) {
|
|
536
639
|
selectedIndex = (selectedIndex + 1) % menu.suggestions.length;
|
|
537
640
|
menuNavigated = true;
|
|
@@ -1480,7 +1583,7 @@ async function handleSlash(line, state) {
|
|
|
1480
1583
|
mode: state.mode,
|
|
1481
1584
|
}));
|
|
1482
1585
|
console.log(renderSuggestions());
|
|
1483
|
-
console.log(chalk.dim(" ESC abort │ Ctrl+C clears input │ Ctrl+T
|
|
1586
|
+
console.log(chalk.dim(" ESC abort │ Ctrl+C clears input │ @ to attach files │ Ctrl+T thinking │ Ctrl+O tool output (q to close)\n"));
|
|
1484
1587
|
return true;
|
|
1485
1588
|
}
|
|
1486
1589
|
case "/update":
|
|
@@ -1635,7 +1738,7 @@ export async function startRepl(options = {}) {
|
|
|
1635
1738
|
mode: state.mode,
|
|
1636
1739
|
}));
|
|
1637
1740
|
console.log(renderSuggestions());
|
|
1638
|
-
console.log(chalk.dim(" ESC abort │ Ctrl+C clears input │ Ctrl+T
|
|
1741
|
+
console.log(chalk.dim(" ESC abort │ Ctrl+C clears input │ @ to attach files │ Ctrl+T thinking │ Ctrl+O tool output (q to close)\n"));
|
|
1639
1742
|
// Hint thinking-capable users that the toggle exists. We default it to
|
|
1640
1743
|
// off for speed, since on NIM many models route through a much slower
|
|
1641
1744
|
// chat-template path when reasoning is enabled.
|
|
@@ -1684,9 +1787,26 @@ export async function startRepl(options = {}) {
|
|
|
1684
1787
|
clearThinking();
|
|
1685
1788
|
abortPressCount = 0;
|
|
1686
1789
|
let assistantContent = "";
|
|
1790
|
+
// Expand @file mentions and drag-and-dropped paths into real context.
|
|
1791
|
+
// The user-visible `line` stays readable in history; the model gets
|
|
1792
|
+
// the line plus an appended block of file contents / path notes.
|
|
1793
|
+
const expansion = expandMentions(line);
|
|
1794
|
+
const modelInput = expansion.contextBlock.length > 0
|
|
1795
|
+
? `${line}\n\n${expansion.contextBlock}`
|
|
1796
|
+
: line;
|
|
1797
|
+
if (expansion.attachments.length > 0) {
|
|
1798
|
+
for (const att of expansion.attachments) {
|
|
1799
|
+
const tag = att.kind === "text"
|
|
1800
|
+
? chalk.green("attached")
|
|
1801
|
+
: att.kind === "missing"
|
|
1802
|
+
? chalk.red("not found")
|
|
1803
|
+
: chalk.yellow(att.kind);
|
|
1804
|
+
console.log(chalk.dim(` ↳ ${tag}: `) + chalk.dim(att.path));
|
|
1805
|
+
}
|
|
1806
|
+
}
|
|
1687
1807
|
if (state.mode === "ask") {
|
|
1688
1808
|
assistantContent = await withAbortableInput(async (signal) => streamWithAbort(async (runSignal, onToken) => {
|
|
1689
|
-
return await runAskStream(
|
|
1809
|
+
return await runAskStream(modelInput, onToken, {
|
|
1690
1810
|
provider: state.provider,
|
|
1691
1811
|
model: state.model,
|
|
1692
1812
|
history: state.messages,
|
|
@@ -1696,7 +1816,7 @@ export async function startRepl(options = {}) {
|
|
|
1696
1816
|
process.stdout.write("\n");
|
|
1697
1817
|
}
|
|
1698
1818
|
else {
|
|
1699
|
-
assistantContent = await withAbortableInput(async (signal) => runAgent(
|
|
1819
|
+
assistantContent = await withAbortableInput(async (signal) => runAgent(modelInput, {
|
|
1700
1820
|
provider: state.provider,
|
|
1701
1821
|
model: state.model,
|
|
1702
1822
|
history: state.messages,
|
|
@@ -1705,7 +1825,7 @@ export async function startRepl(options = {}) {
|
|
|
1705
1825
|
}));
|
|
1706
1826
|
}
|
|
1707
1827
|
console.log();
|
|
1708
|
-
state.messages.push({ role: "user", content:
|
|
1828
|
+
state.messages.push({ role: "user", content: modelInput }, { role: "assistant", content: assistantContent });
|
|
1709
1829
|
}
|
|
1710
1830
|
catch (error) {
|
|
1711
1831
|
if (error instanceof AbortRunError) {
|