@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.
@@ -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 specialized in cybersecurity, pentesting, and sysadmin.
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
- - For complex multi-step tasks, break the work into logical steps yourself.
137
- Execute them one by one. You own the plan nothing is predetermined.
138
- - For simple tasks (single command, quick lookup), just execute immediately.
139
- - If a step fails, adapt your plan. Don't rigidly follow a broken path.
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
- LOCAL NETWORK DISCOVERY:
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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;+MAsK2L,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"}
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 or /think for thinking │ Ctrl+O opens full tool output (q to close)\n"));
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 or /think for thinking │ Ctrl+O opens full tool output (q to close)\n"));
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(line, onToken, {
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(line, {
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: line }, { role: "assistant", content: assistantContent });
1828
+ state.messages.push({ role: "user", content: modelInput }, { role: "assistant", content: assistantContent });
1709
1829
  }
1710
1830
  catch (error) {
1711
1831
  if (error instanceof AbortRunError) {