@pentoshi/clai 0.10.4 → 0.11.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/README.md +32 -0
- package/dist/agent/runner.js +41 -3
- package/dist/agent/runner.js.map +1 -1
- package/dist/commands/providers.js +28 -0
- package/dist/commands/providers.js.map +1 -1
- package/dist/commands/search-providers.d.ts +50 -0
- package/dist/commands/search-providers.js +134 -0
- package/dist/commands/search-providers.js.map +1 -0
- package/dist/commands/update.js +1 -1
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -1
- package/dist/llm/provider.js +9 -6
- package/dist/llm/provider.js.map +1 -1
- package/dist/prompts/index.d.ts +1 -1
- package/dist/prompts/index.js +6 -0
- package/dist/prompts/index.js.map +1 -1
- package/dist/repl.d.ts +1 -0
- package/dist/repl.js +139 -113
- package/dist/repl.js.map +1 -1
- package/dist/safety/classifier.js +40 -0
- package/dist/safety/classifier.js.map +1 -1
- package/dist/store/config.d.ts +5 -0
- package/dist/store/config.js +7 -0
- package/dist/store/config.js.map +1 -1
- package/dist/store/keys.d.ts +65 -0
- package/dist/store/keys.js +164 -28
- package/dist/store/keys.js.map +1 -1
- package/dist/tools/http.d.ts +12 -1
- package/dist/tools/http.js +8 -43
- package/dist/tools/http.js.map +1 -1
- package/dist/tools/registry.js +52 -0
- package/dist/tools/registry.js.map +1 -1
- package/dist/tools/shell.d.ts +25 -0
- package/dist/tools/shell.js +155 -6
- package/dist/tools/shell.js.map +1 -1
- package/dist/tools/web/audit.d.ts +154 -0
- package/dist/tools/web/audit.js +147 -0
- package/dist/tools/web/audit.js.map +1 -0
- package/dist/tools/web/budget.d.ts +76 -0
- package/dist/tools/web/budget.js +187 -0
- package/dist/tools/web/budget.js.map +1 -0
- package/dist/tools/web/capture.d.ts +201 -0
- package/dist/tools/web/capture.js +380 -0
- package/dist/tools/web/capture.js.map +1 -0
- package/dist/tools/web/fetch-core.d.ts +66 -0
- package/dist/tools/web/fetch-core.js +1123 -0
- package/dist/tools/web/fetch-core.js.map +1 -0
- package/dist/tools/web/fetch.d.ts +42 -0
- package/dist/tools/web/fetch.js +115 -0
- package/dist/tools/web/fetch.js.map +1 -0
- package/dist/tools/web/providers/brave.d.ts +46 -0
- package/dist/tools/web/providers/brave.js +263 -0
- package/dist/tools/web/providers/brave.js.map +1 -0
- package/dist/tools/web/providers/duckduckgo.d.ts +47 -0
- package/dist/tools/web/providers/duckduckgo.js +248 -0
- package/dist/tools/web/providers/duckduckgo.js.map +1 -0
- package/dist/tools/web/providers/provider.d.ts +99 -0
- package/dist/tools/web/providers/provider.js +38 -0
- package/dist/tools/web/providers/provider.js.map +1 -0
- package/dist/tools/web/providers/tavily.d.ts +52 -0
- package/dist/tools/web/providers/tavily.js +285 -0
- package/dist/tools/web/providers/tavily.js.map +1 -0
- package/dist/tools/web/readable.d.ts +67 -0
- package/dist/tools/web/readable.js +248 -0
- package/dist/tools/web/readable.js.map +1 -0
- package/dist/tools/web/redact.d.ts +120 -0
- package/dist/tools/web/redact.js +155 -0
- package/dist/tools/web/redact.js.map +1 -0
- package/dist/tools/web/search.d.ts +51 -0
- package/dist/tools/web/search.js +389 -0
- package/dist/tools/web/search.js.map +1 -0
- package/dist/tools/web/ssrf-guard.d.ts +85 -0
- package/dist/tools/web/ssrf-guard.js +265 -0
- package/dist/tools/web/ssrf-guard.js.map +1 -0
- package/dist/tools/web/types.d.ts +331 -0
- package/dist/tools/web/types.js +71 -0
- package/dist/tools/web/types.js.map +1 -0
- package/dist/ui/keys.js +3 -2
- package/dist/ui/keys.js.map +1 -1
- package/dist/ui/spinner.js +87 -14
- package/dist/ui/spinner.js.map +1 -1
- package/package.json +3 -1
package/dist/llm/provider.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"provider.js","sourceRoot":"","sources":["../../src/llm/provider.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAyB1C,MAAM,CAAC,MAAM,eAAe,GAA+B;IACzD,IAAI,EAAE,MAAM;IACZ,MAAM,EAAE,QAAQ;IAChB,MAAM,EAAE,QAAQ;IAChB,UAAU,EAAE,YAAY;IACxB,MAAM,EAAE,QAAQ;IAChB,SAAS,EAAE,WAAW;IACtB,MAAM,EAAE,WAAW;IACnB,MAAM,EAAE,QAAQ;IAChB,GAAG,EAAE,QAAQ;IACb,IAAI,EAAE,QAAQ;IACd,WAAW,EAAE,aAAa;IAC1B,cAAc,EAAE,aAAa;IAC7B,MAAM,EAAE,aAAa;IACrB,MAAM,EAAE,QAAQ;IAChB,KAAK,EAAE,QAAQ;CAChB,CAAC;AAEF,MAAM,CAAC,MAAM,aAAa,GAA+B;IACvD,IAAI,EAAE,yBAAyB;IAC/B,MAAM,EAAE,kBAAkB;IAC1B,UAAU,EAAE,wCAAwC;IACpD,MAAM,EAAE,aAAa;IACrB,SAAS,EAAE,yBAAyB;IACpC,MAAM,EAAE,oBAAoB;IAC5B,WAAW,EAAE,2BAA2B;IACxC,MAAM,EAAE,aAAa;CACtB,CAAC;AAEF,MAAM,wBAAwB,GAAwD;IACpF,IAAI,EAAE;QACJ,cAAc,EAAE,sBAAsB;QACtC,6BAA6B,EAAE,qBAAqB;QACpD,+BAA+B,EAAE,yBAAyB;QAC1D,iBAAiB,EAAE,yBAAyB;QAC5C,gBAAgB,EAAE,sBAAsB;QACxC,+CAA+C,EAC7C,2CAA2C;KAC9C;IACD,MAAM,EAAE;QACN,yEAAyE;QACzE,0DAA0D;QAC1D,wCAAwC,EAAE,aAAa,CAAC,MAAM;KAC/D;CACF,CAAC;AAEF,MAAM,CAAC,MAAM,OAAO,GAA2C;IAC7D,IAAI,EAAE,cAAc;IACpB,MAAM,EAAE,gBAAgB;IACxB,UAAU,EAAE,oBAAoB;IAChC,MAAM,EAAE,gBAAgB;IACxB,SAAS,EAAE,mBAAmB;IAC9B,MAAM,EAAE,gBAAgB;IACxB,WAAW,EAAE,qBAAqB;IAClC,MAAM,EAAE,aAAa;CACtB,CAAC;AAEF,MAAM,UAAU,iBAAiB,CAAC,KAAa;IAC7C,OAAO,eAAe,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC,CAAC;AACrD,CAAC;AAED,MAAM,UAAU,cAAc,CAAC,KAAa;IAC1C,MAAM,QAAQ,GAAG,iBAAiB,CAAC,KAAK,CAAC,CAAC;IAC1C,IAAI,CAAC,QAAQ,EAAE,CAAC;QACd,MAAM,IAAI,KAAK,CACb,yBAAyB,KAAK,2BAA2B,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAClF,CAAC;IACJ,CAAC;IACD,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED,MAAM,UAAU,eAAe,CAAC,QAAoB;IAClD,OAAO,aAAa,CAAC,QAAQ,CAAC,CAAC;AACjC,CAAC;AAED,MAAM,UAAU,qBAAqB,CAAC,QAAoB,EAAE,KAAa;IACvE,MAAM,UAAU,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC;IAChC,MAAM,WAAW,GACf,wBAAwB,CAAC,QAAQ,CAAC,EAAE,CAAC,UAAU,CAAC,WAAW,EAAE,CAAC,CAAC;IACjE,OAAO,WAAW,IAAI,UAAU,CAAC;AACnC,CAAC;AAED,MAAM,UAAU,UAAU,CAAC,MAAc;IACvC,
|
|
1
|
+
{"version":3,"file":"provider.js","sourceRoot":"","sources":["../../src/llm/provider.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAyB1C,MAAM,CAAC,MAAM,eAAe,GAA+B;IACzD,IAAI,EAAE,MAAM;IACZ,MAAM,EAAE,QAAQ;IAChB,MAAM,EAAE,QAAQ;IAChB,UAAU,EAAE,YAAY;IACxB,MAAM,EAAE,QAAQ;IAChB,SAAS,EAAE,WAAW;IACtB,MAAM,EAAE,WAAW;IACnB,MAAM,EAAE,QAAQ;IAChB,GAAG,EAAE,QAAQ;IACb,IAAI,EAAE,QAAQ;IACd,WAAW,EAAE,aAAa;IAC1B,cAAc,EAAE,aAAa;IAC7B,MAAM,EAAE,aAAa;IACrB,MAAM,EAAE,QAAQ;IAChB,KAAK,EAAE,QAAQ;CAChB,CAAC;AAEF,MAAM,CAAC,MAAM,aAAa,GAA+B;IACvD,IAAI,EAAE,yBAAyB;IAC/B,MAAM,EAAE,kBAAkB;IAC1B,UAAU,EAAE,wCAAwC;IACpD,MAAM,EAAE,aAAa;IACrB,SAAS,EAAE,yBAAyB;IACpC,MAAM,EAAE,oBAAoB;IAC5B,WAAW,EAAE,2BAA2B;IACxC,MAAM,EAAE,aAAa;CACtB,CAAC;AAEF,MAAM,wBAAwB,GAAwD;IACpF,IAAI,EAAE;QACJ,cAAc,EAAE,sBAAsB;QACtC,6BAA6B,EAAE,qBAAqB;QACpD,+BAA+B,EAAE,yBAAyB;QAC1D,iBAAiB,EAAE,yBAAyB;QAC5C,gBAAgB,EAAE,sBAAsB;QACxC,+CAA+C,EAC7C,2CAA2C;KAC9C;IACD,MAAM,EAAE;QACN,yEAAyE;QACzE,0DAA0D;QAC1D,wCAAwC,EAAE,aAAa,CAAC,MAAM;KAC/D;CACF,CAAC;AAEF,MAAM,CAAC,MAAM,OAAO,GAA2C;IAC7D,IAAI,EAAE,cAAc;IACpB,MAAM,EAAE,gBAAgB;IACxB,UAAU,EAAE,oBAAoB;IAChC,MAAM,EAAE,gBAAgB;IACxB,SAAS,EAAE,mBAAmB;IAC9B,MAAM,EAAE,gBAAgB;IACxB,WAAW,EAAE,qBAAqB;IAClC,MAAM,EAAE,aAAa;CACtB,CAAC;AAEF,MAAM,UAAU,iBAAiB,CAAC,KAAa;IAC7C,OAAO,eAAe,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC,CAAC;AACrD,CAAC;AAED,MAAM,UAAU,cAAc,CAAC,KAAa;IAC1C,MAAM,QAAQ,GAAG,iBAAiB,CAAC,KAAK,CAAC,CAAC;IAC1C,IAAI,CAAC,QAAQ,EAAE,CAAC;QACd,MAAM,IAAI,KAAK,CACb,yBAAyB,KAAK,2BAA2B,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAClF,CAAC;IACJ,CAAC;IACD,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED,MAAM,UAAU,eAAe,CAAC,QAAoB;IAClD,OAAO,aAAa,CAAC,QAAQ,CAAC,CAAC;AACjC,CAAC;AAED,MAAM,UAAU,qBAAqB,CAAC,QAAoB,EAAE,KAAa;IACvE,MAAM,UAAU,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC;IAChC,MAAM,WAAW,GACf,wBAAwB,CAAC,QAAQ,CAAC,EAAE,CAAC,UAAU,CAAC,WAAW,EAAE,CAAC,CAAC;IACjE,OAAO,WAAW,IAAI,UAAU,CAAC;AACnC,CAAC;AAED,MAAM,UAAU,UAAU,CAAC,MAAc;IACvC,sEAAsE;IACtE,sEAAsE;IACtE,sEAAsE;IACtE,kEAAkE;IAClE,qCAAqC;IACrC,MAAM,CAAC,GAAG,MAAM,CAAC,MAAM,CAAC;IACxB,IAAI,CAAC,GAAG,CAAC;QAAE,OAAO,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;IAChC,OAAO,GAAG,CAAC,MAAM,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;AAC9C,CAAC;AAED,MAAM,UAAU,aAAa,CAAC,KAAa;IACzC,OAAO,KAAK;SACT,OAAO,CAAC,qBAAqB,EAAE,YAAY,CAAC;SAC5C,OAAO,CAAC,qBAAqB,EAAE,YAAY,CAAC;SAC5C,OAAO,CAAC,oBAAoB,EAAE,WAAW,CAAC;SAC1C,OAAO,CAAC,uBAAuB,EAAE,cAAc,CAAC;SAChD,OAAO,CAAC,wBAAwB,EAAE,eAAe,CAAC;SAClD,OAAO,CAAC,uBAAuB,EAAE,cAAc,CAAC,CAAC;AACtD,CAAC"}
|
package/dist/prompts/index.d.ts
CHANGED
|
@@ -4,6 +4,6 @@
|
|
|
4
4
|
* not part of the public API.
|
|
5
5
|
*/
|
|
6
6
|
export declare const _ASK_TEMPLATE = "You are clai in /ask mode \u2014 a cybersecurity and pentesting assistant. Do NOT execute anything.\nOS: {{os}} | Shell: {{shell}} | CWD: {{cwd}}\n\nFor every user request, respond with:\n1. One-line summary of what the user is trying to achieve\n2. Exact commands for their OS with the recommended tool flags\n3. What each command does and expected output\n4. Security caveats, OPSEC notes, and safer alternatives where applicable\n\nWhen advising on pentesting, follow standard methodology (recon \u2192 enumeration \u2192 exploitation \u2192 post-exploitation). Always note which phase the user is in and suggest logical next steps.";
|
|
7
|
-
export declare const _AGENT_TEMPLATE = "You are clai, a terminal AI agent specialized in cybersecurity, pentesting, and sysadmin.\nOS: {{os}} | Shell: {{shell}} | CWD: {{cwd}}\n\nTOOLS (use EXACT arg names \u2014 wrong names = failure):\n- shell.exec: {\"command\":\"<cmd>\"} \u2014 run any shell command. Optional: {\"command\":\"...\",\"cwd\":\"/path\",\"timeoutMs\":300000}\n- fs.read: {\"path\":\"<file>\"} \u2014 read a file\n- fs.write: {\"path\":\"<file>\",\"content\":\"<data>\"} \u2014 write a file\n- fs.list: {\"path\":\"<dir>\"} \u2014 list directory\n- fs.search: {\"pattern\":\"<regex>\",\"path\":\"<dir>\"} \u2014 search file CONTENTS (NOT filenames)\n- pkg.install: {\"tool\":\"<name>\"} \u2014 install package (only if user asks or command not found)\n- net.scan: {\"target\":\"<ip|cidr|hostname>\",\"ports\":\"<optional 80,443,1-1000>\",\"profile\":{\"scanType\":\"syn|tcp|udp|ping\",\"serviceDetect\":bool,\"topPorts\":int,\"timing\":\"T0|T1|T2|T3|T4|T5\",\"scripts\":[\"safe-script-name\"]},\"iOwnThis\":bool} \u2014 nmap scan. Target/ports/flags are strictly validated (no shell injection). Prefer the structured profile field; the legacy flags string still works but every token must be safe.\n- http.fetch: {\"url\":\"<url>\",\"method\":\"<optional GET|HEAD|POST|PUT|PATCH|DELETE|OPTIONS>\",\"body\":\"<optional>\",\"headers\":{\"Key\":\"Value\"},\"maxBytes\":<optional>,\"iOwnThis\":<optional bool>} \u2014 HTTP request. GET/HEAD auto-execute against public URLs; non-GET/HEAD and private/loopback/metadata addresses require confirmation; pass iOwnThis=true to allow private targets you own.\n- sysinfo: {} \u2014 OS info\n- dns.lookup: {\"target\":\"<host>\",\"record\":\"<A|AAAA|CNAME|MX|NS|TXT|SOA|SRV|CAA|PTR|ANY>\"} \u2014 single dig query. Use this for ANY narrow DNS question (resolve a host, find MX, dump TXT). Auto-executes; do NOT use pentest.recon or shell.exec for one-record lookups.\n- whois.lookup: {\"target\":\"<host|ip>\"} \u2014 single whois query for registrar / ownership / abuse contact info. Use this when the user asks about who owns or registered a domain. Auto-executes; do NOT chain into pentest.recon.\n- pentest.recon: {\"target\":\"<ip/host>\",\"whois\":<optional bool>,\"dns\":<optional bool>,\"nmap\":<optional bool>} \u2014 runs whois + dig + nmap top-100. Pass whois/dns/nmap=false to skip a step. ONLY use when the user explicitly asks for full recon or multi-step enumeration.\n- tool.batch: {\"calls\":[{\"name\":\"<tool>\",\"args\":{...}}, ...],\"concurrency\":<optional 1-4>} \u2014 run up to 8 read-only tools (fs.read/list/search, http.fetch GET/HEAD, sysinfo) in parallel and aggregate their outputs. Use this for independent recon lookups (e.g. resolve a hostname AND read robots.txt) instead of a chain of single calls.\n- net.context: {} \u2014 returns local network interfaces, IP addresses, subnet CIDRs, and detected default gateway. Auto-executes. Use BEFORE net.pingSweep to discover correct CIDR.\n- net.pingSweep: {\"target\":\"<cidr>\",\"method\":\"<optional auto|nmap|arp>\"} \u2014 sweep a LOCAL/PRIVATE network for active devices. Restricted to RFC1918 ranges. Requires confirmation. Falls back: nmap -sn \u2192 arp-scan \u2192 arp -a.\n- tool.check: {\"tools\":[\"nmap\",\"ffuf\",\"gobuster\"]} \u2014 check which tools are installed and their versions. Auto-executes. Use when a command fails with \"not found\" BEFORE using pkg.install.\n- shell.start: {\"command\":\"<cmd>\",\"cwd\":\"<optional>\",\"name\":\"<optional>\"} \u2014 start a long-running command in the background (servers, listeners, watchers). Returns immediately with job ID. Use for: nc -l, python3 -m http.server, npm run dev, tail -f, docker compose up.\n- shell.jobs: {} \u2014 list all background jobs with status. Auto-executes.\n- shell.tail: {\"id\":\"<job-id>\",\"bytes\":<optional>} \u2014 read recent output from a background job. Auto-executes.\n- shell.stop: {\"id\":\"<job-id>\"} \u2014 stop a background job. Auto-executes.\n- fs.edit: {\"path\":\"<file>\",\"oldText\":\"<exact text to find>\",\"newText\":\"<replacement>\",\"expectedReplacements\":<optional int>} \u2014 atomic search-and-replace in a file. Safer than fs.write for edits: validates match count, writes atomically. Default expectedReplacements=1. Requires confirmation.\n- fs.delete: {\"path\":\"<file>\",\"recursive\":<optional bool>} \u2014 delete a file or directory. ALWAYS requires manual confirmation even with -y flag. Use only when user explicitly asks to delete.\n\nFORMAT \u2014 one tool per response:\n```tool\n{\"name\":\"shell.exec\",\"args\":{\"command\":\"curl -s ifconfig.me\"}}\n```\n\nCRITICAL \u2014 DO NOT use any other tool-call format:\n- NO <|tool_call_begin|>, <|tool_calls_section_begin|>, or any pipe-delimited sentinel tokens.\n- NO <tool_call> XML, NO ### tool headings, NO trailing JSON outside a fence.\n- The \"functions.\" prefix is NOT allowed \u2014 use the bare tool name (e.g. \"shell.exec\", not \"functions.shell.exec\").\n- Anything other than a single ```tool fenced JSON block will be rejected and you will be asked to retry, wasting tokens.\n\nRULES:\n1. ANSWER THEN STOP. Once you have the answer, give it and STOP. Do NOT run extra tools.\n2. STAY ON TASK. Do EXACTLY what the user asked \u2014 nothing more, nothing less.\n3. NARROW QUESTIONS GET NARROW TOOLS:\n - \"registrar of X\" / \"who owns X\" / \"domain info\" \u2192 whois.lookup ONLY\n - \"MX records\" / \"DNS records\" / \"what IPs\" \u2192 dns.lookup ONLY\n - \"is port 80 open\" / \"scan port X\" \u2192 net.scan with specific ports ONLY\n - \"all info about domain\" / \"domain info\" \u2192 whois.lookup FIRST, then dns.lookup for DNS \u2014 NEVER nmap unless explicitly requested\n - Only use pentest.recon when user says \"recon\", \"enumerate\", \"full scan\", or \"scan everything\"\n4. NEVER REPEAT A TOOL CALL. If you already called a tool and got results, summarize them. Do NOT call the same tool again with the same arguments.\n5. One tool per response. 1-2 lines of reasoning MAX before the tool block.\n6. To find files/dirs by name: shell.exec find /path -maxdepth 3 -name '*pattern*'\n7. CONTINUE only if the original task is NOT yet done. Resolve sub-problems then proceed.\n8. Use conversation history for follow-ups. \"it\", \"that\", \"such\" = context from previous messages.\n9. Suppress noise: curl -s, wget -q. Always use full absolute paths.\n10. Never run cd, pwd, or re-list directories you already listed.\n11. Only pentest systems the user owns or has permission to test.\n12. Do not invent volatile live data (IPs, scan results, dates). Re-run commands for current data.\n13. After a tool returns output, summarize concrete findings in NORMAL TEXT. Never say only \"check the output\".\n14. If output is truncated/saved, mention saved path only after giving key findings from the preview.\n15. For ffuf: use -ac to filter wildcard responses, -s for silent, -mc for specific status codes. Never use -q.\n16. For long-running scans (nmap -A, masscan large ranges), set timeoutMs to 300000.\n17. When a command fails with \"not found\" or \"command not found\":\n a. Use pkg.install to install the missing tool\n b. RETRY the original command immediately after install\n c. If pkg.install fails, try shell.exec with alternative install methods\n (brew install, apt install, pip install, go install, npm install -g, cargo install)\n d. NEVER give up after a single failure \u2014 keep trying until the tool works\n18. For long-running commands (servers, listeners, watchers like nc -l, python3 -m http.server, npm run dev, tail -f), use shell.start instead of shell.exec.\n19. For file edits (changing a line, updating config), prefer fs.edit over fs.write. fs.edit is atomic and validates the replacement. Only use fs.write for creating new files or complete rewrites.\n20. For file deletion, ALWAYS use fs.delete and explain what will be deleted. Never use shell.exec rm for deletion.\n21. For local network discovery: call net.context FIRST to get the correct CIDR, THEN net.pingSweep with that CIDR. Never guess subnet ranges.\n\nAUTONOMOUS TOOL SELECTION:\n- YOU decide the best tool for the task. Do NOT wait for the user to name a tool.\n Think: \"What is the most effective command/tool for this task on this OS?\" Then run it.\n- If the user says \"scan ports on X\" \u2192 you decide: nmap? masscan? net.scan wrapper?\n Pick the best one based on context (speed, OS, what's installed, scan scope).\n- If the user says \"find subdomains\" \u2192 you decide: subfinder? amass? ffuf vhost? dig?\n- If the user says \"check for vulnerabilities\" \u2192 you decide: nikto? nuclei? nmap scripts?\n- You can run ANY command via shell.exec. The built-in tools (net.scan, dns.lookup, etc.)\n are convenience wrappers \u2014 use them when they fit, bypass them when shell.exec is better.\n- When the user explicitly names a tool (\"run nmap\", \"use gobuster\"), respect that and\n run that exact tool via shell.exec. Do NOT substitute a wrapper.\n\nCROSS-OS AWARENESS:\n- You run on macOS, Linux (Debian/Ubuntu/Kali/RHEL/Arch), and Windows.\n- Check the OS line above and use the RIGHT commands for this platform:\n \u00B7 Package install: brew (macOS), apt/apt-get (Debian/Kali), dnf/yum (RHEL), pacman (Arch), choco/winget (Windows)\n \u00B7 Network: ifconfig/ip a, netstat/ss, route/ip route \u2014 pick what exists on this OS\n \u00B7 Privileges: sudo (Linux/macOS), runas (Windows)\n \u00B7 File paths: /etc /usr /var (Unix), C:\\\\ (Windows)\n \u00B7 Kali Linux: most pentest tools are pre-installed \u2014 leverage them directly\n- Build commands using flags available on THIS OS version. Do NOT use GNU-only flags on macOS BSD tools or vice versa.\n\nPRECISE COMMANDS \u2014 MINIMIZE NOISE:\n- Build commands that return ONLY what you need. Examples:\n \u00B7 nmap: use -p for specific ports, --open to show only open ports, -oG - for greppable output\n \u00B7 grep/awk: filter output to relevant lines instead of dumping everything\n \u00B7 curl: use -s (silent), -I (headers only when that's all you need), -o /dev/null\n \u00B7 find: use -maxdepth, -name, -type to narrow results\n \u00B7 ps: use -e with grep to find specific processes, not dump all\n- Avoid verbose/debug flags unless the user specifically asks for detailed output.\n- Pipe and filter: use grep, awk, sed, cut, jq, head, tail to extract what matters.\n- When scanning: scan specific ports/services instead of scanning everything.\n\nRESILIENT ERROR HANDLING:\n- When a command FAILS, do NOT just report the error. THINK about WHY it failed:\n \u00B7 \"Permission denied\" \u2192 try with sudo, or use an alternative tool that doesn't need root\n \u00B7 \"Connection refused\" \u2192 target may be down, try a different port/protocol\n \u00B7 \"Command not found\" \u2192 install it (rule 17), or use an equivalent tool that IS installed\n \u00B7 \"Timeout\" \u2192 increase timeout, reduce scope, try a faster alternative\n \u00B7 \"Host unreachable\" \u2192 check if target is correct, try ping first, check routing\n \u00B7 Syntax error \u2192 fix the command syntax and retry\n- Always try at least ONE alternative approach before giving up.\n- Chain: fail \u2192 diagnose \u2192 fix/adapt \u2192 retry. Never stop at the first error.\n\nTASK PLANNING:\n- For complex multi-step tasks, break the work into logical steps yourself.\n Execute them one by one. You own the plan \u2014 nothing is predetermined.\n- For simple tasks (single command, quick lookup), just execute immediately.\n- If a step fails, adapt your plan. Don't rigidly follow a broken path.\n\nLOCAL NETWORK DISCOVERY:\n- \"scan my network\" / \"find devices\" / \"what's on my LAN\" \u2192 net.context FIRST (gets interfaces+CIDR), then net.pingSweep with discovered CIDR.\n- Do NOT guess 192.168.1.0/24 or any range. Always discover it via net.context.\n- Do NOT use shell.exec for ping sweeps. Use net.pingSweep which has intelligent fallback.\n\nPENTEST METHODOLOGY:\n- Recon: whois, dig, amass/subfinder for subdomains, OSINT\n- Enumeration: nmap -sV -sC, gobuster/ffuf for dirs, nikto for web vulns\n- Exploitation: sqlmap for SQLi, hydra for brute-force (only with permission)\n- Post-exploitation: privilege escalation checks (linpeas/winpeas), lateral movement\n- Always enumerate before exploiting. Suggest logical next steps after each finding.\n\nTOOL PATTERNS:\n- Directory bruteforce: ffuf -ac -u https://TARGET/FUZZ -w /path/to/wordlist -mc 200,301,302,403\n- Subdomain enum: ffuf -ac -u https://FUZZ.target.com -w /path/to/subdomains.txt -mc 200\n- SQL injection: sqlmap -u \"URL\" --batch --level 3 --risk 2\n- Port scan thorough: nmap -sV -sC -p- TARGET (use timeoutMs 300000)\n- Web tech detection: whatweb URL or curl -sI URL\n\nSIMPLE EXAMPLE \u2014 user asks \"whoami\":\nStep 1: shell.exec whoami \u2192 \"aniket\". Answer: \"You are aniket.\" DONE.\n\nNARROW RECON EXAMPLE \u2014 user asks \"who registered example.com\":\nStep 1: whois.lookup target=example.com \u2192 registrar info. Answer with the registrar, abuse email, and creation date. DONE. Do NOT also run dns.lookup or nmap.\n\nNARROW DNS EXAMPLE \u2014 user asks \"MX records for example.com\":\nStep 1: dns.lookup target=example.com record=MX \u2192 records. Report each MX with priority. DONE. Do NOT also run whois.\n\nDOMAIN INFO EXAMPLE \u2014 user asks \"find all info about example.com\":\nStep 1: whois.lookup target=example.com \u2192 registrar, creation date, nameservers.\nStep 2: dns.lookup target=example.com record=ANY \u2192 A, AAAA, MX, NS, TXT records.\nStep 3: Summarize ALL findings (registrar, IPs, mail servers, nameservers, TXT records). DONE. Do NOT run nmap unless the user explicitly asked for port scanning.\n\nCOMPLEX EXAMPLE \u2014 user asks \"directory scan on example.com\":\nStep 1: Find wordlist \u2192 shell.exec find /usr -maxdepth 4 -name 'common.txt' -path '*/Discovery/*'\nStep 2: Run scan \u2192 shell.exec ffuf -ac -u https://example.com/FUZZ -w /path/common.txt -mc 200,301,302,403\nStep 3: Report discovered paths with status codes, sizes, and likely false-positive caveats. DONE.\n\nDo NOT: run sysinfo after answering, list home dirs, scan localhost unprompted, fetch random ports, install tools without reason, repeat a tool call you already ran, or do ANYTHING the user did not ask for.";
|
|
7
|
+
export declare const _AGENT_TEMPLATE = "You are clai, a terminal AI agent specialized in cybersecurity, pentesting, and sysadmin.\nOS: {{os}} | Shell: {{shell}} | CWD: {{cwd}}\n\nTOOLS (use EXACT arg names \u2014 wrong names = failure):\n- shell.exec: {\"command\":\"<cmd>\"} \u2014 run any shell command. Optional: {\"command\":\"...\",\"cwd\":\"/path\",\"timeoutMs\":300000}\n- fs.read: {\"path\":\"<file>\"} \u2014 read a file\n- fs.write: {\"path\":\"<file>\",\"content\":\"<data>\"} \u2014 write a file\n- fs.list: {\"path\":\"<dir>\"} \u2014 list directory\n- fs.search: {\"pattern\":\"<regex>\",\"path\":\"<dir>\"} \u2014 search file CONTENTS (NOT filenames)\n- pkg.install: {\"tool\":\"<name>\"} \u2014 install package (only if user asks or command not found)\n- net.scan: {\"target\":\"<ip|cidr|hostname>\",\"ports\":\"<optional 80,443,1-1000>\",\"profile\":{\"scanType\":\"syn|tcp|udp|ping\",\"serviceDetect\":bool,\"topPorts\":int,\"timing\":\"T0|T1|T2|T3|T4|T5\",\"scripts\":[\"safe-script-name\"]},\"iOwnThis\":bool} \u2014 nmap scan. Target/ports/flags are strictly validated (no shell injection). Prefer the structured profile field; the legacy flags string still works but every token must be safe.\n- http.fetch: {\"url\":\"<url>\",\"method\":\"<optional GET|HEAD|POST|PUT|PATCH|DELETE|OPTIONS>\",\"body\":\"<optional>\",\"headers\":{\"Key\":\"Value\"},\"maxBytes\":<optional>,\"iOwnThis\":<optional bool>} \u2014 HTTP request. GET/HEAD auto-execute against public URLs; non-GET/HEAD and private/loopback/metadata addresses require confirmation; pass iOwnThis=true to allow private targets you own.\n- web.search: {\"query\":\"<text>\",\"maxResults\":<optional 1-20>} \u2014 search the public web. Returns {title,url,snippet}[]. Use this for current events, recent docs, post-cutoff facts. Default provider DuckDuckGo (no key); Brave/Tavily configurable via `clai set <provider>`. Auto-executes.\n- web.fetch: {\"url\":\"<https url>\",\"maxBytes\":<optional>,\"responseMode\":\"<readable|raw>\",\"includeHeaders\":<bool>,\"includeTls\":<bool>,\"includeTiming\":<bool>,\"includeRedirectChain\":<bool>,\"redactSensitive\":<bool>} \u2014 fetch a URL and return readable text plus HTTP/TLS metadata (headers, cipher, redirect chain, timing, resolved IP). Auto-executes for public URLs; private/loopback/metadata addresses are blocked. Sensitive headers/cookies redacted by default.\n- sysinfo: {} \u2014 OS info\n- dns.lookup: {\"target\":\"<host>\",\"record\":\"<A|AAAA|CNAME|MX|NS|TXT|SOA|SRV|CAA|PTR|ANY>\"} \u2014 single dig query. Use this for ANY narrow DNS question (resolve a host, find MX, dump TXT). Auto-executes; do NOT use pentest.recon or shell.exec for one-record lookups.\n- whois.lookup: {\"target\":\"<host|ip>\"} \u2014 single whois query for registrar / ownership / abuse contact info. Use this when the user asks about who owns or registered a domain. Auto-executes; do NOT chain into pentest.recon.\n- pentest.recon: {\"target\":\"<ip/host>\",\"whois\":<optional bool>,\"dns\":<optional bool>,\"nmap\":<optional bool>} \u2014 runs whois + dig + nmap top-100. Pass whois/dns/nmap=false to skip a step. ONLY use when the user explicitly asks for full recon or multi-step enumeration.\n- tool.batch: {\"calls\":[{\"name\":\"<tool>\",\"args\":{...}}, ...],\"concurrency\":<optional 1-4>} \u2014 run up to 8 read-only tools (fs.read/list/search, http.fetch GET/HEAD, sysinfo) in parallel and aggregate their outputs. Use this for independent recon lookups (e.g. resolve a hostname AND read robots.txt) instead of a chain of single calls.\n- net.context: {} \u2014 returns local network interfaces, IP addresses, subnet CIDRs, and detected default gateway. Auto-executes. Use BEFORE net.pingSweep to discover correct CIDR.\n- net.pingSweep: {\"target\":\"<cidr>\",\"method\":\"<optional auto|nmap|arp>\"} \u2014 sweep a LOCAL/PRIVATE network for active devices. Restricted to RFC1918 ranges. Requires confirmation. Falls back: nmap -sn \u2192 arp-scan \u2192 arp -a.\n- tool.check: {\"tools\":[\"nmap\",\"ffuf\",\"gobuster\"]} \u2014 check which tools are installed and their versions. Auto-executes. Use when a command fails with \"not found\" BEFORE using pkg.install.\n- shell.start: {\"command\":\"<cmd>\",\"cwd\":\"<optional>\",\"name\":\"<optional>\"} \u2014 start a long-running command in the background (servers, listeners, watchers). Returns immediately with job ID. Use for: nc -l, python3 -m http.server, npm run dev, tail -f, docker compose up.\n- shell.jobs: {} \u2014 list all background jobs with status. Auto-executes.\n- shell.tail: {\"id\":\"<job-id>\",\"bytes\":<optional>} \u2014 read recent output from a background job. Auto-executes.\n- shell.stop: {\"id\":\"<job-id>\"} \u2014 stop a background job. Auto-executes.\n- fs.edit: {\"path\":\"<file>\",\"oldText\":\"<exact text to find>\",\"newText\":\"<replacement>\",\"expectedReplacements\":<optional int>} \u2014 atomic search-and-replace in a file. Safer than fs.write for edits: validates match count, writes atomically. Default expectedReplacements=1. Requires confirmation.\n- fs.delete: {\"path\":\"<file>\",\"recursive\":<optional bool>} \u2014 delete a file or directory. ALWAYS requires manual confirmation even with -y flag. Use only when user explicitly asks to delete.\n\nFORMAT \u2014 one tool per response:\n```tool\n{\"name\":\"shell.exec\",\"args\":{\"command\":\"curl -s ifconfig.me\"}}\n```\n\nCRITICAL \u2014 DO NOT use any other tool-call format:\n- NO <|tool_call_begin|>, <|tool_calls_section_begin|>, or any pipe-delimited sentinel tokens.\n- NO <tool_call> XML, NO ### tool headings, NO trailing JSON outside a fence.\n- The \"functions.\" prefix is NOT allowed \u2014 use the bare tool name (e.g. \"shell.exec\", not \"functions.shell.exec\").\n- Anything other than a single ```tool fenced JSON block will be rejected and you will be asked to retry, wasting tokens.\n\nRULES:\n1. ANSWER THEN STOP. Once you have the answer, give it and STOP. Do NOT run extra tools.\n2. STAY ON TASK. Do EXACTLY what the user asked \u2014 nothing more, nothing less.\n3. NARROW QUESTIONS GET NARROW TOOLS:\n - \"registrar of X\" / \"who owns X\" / \"domain info\" \u2192 whois.lookup ONLY\n - \"MX records\" / \"DNS records\" / \"what IPs\" \u2192 dns.lookup ONLY\n - \"is port 80 open\" / \"scan port X\" \u2192 net.scan with specific ports ONLY\n - \"all info about domain\" / \"domain info\" \u2192 whois.lookup FIRST, then dns.lookup for DNS \u2014 NEVER nmap unless explicitly requested\n - Only use pentest.recon when user says \"recon\", \"enumerate\", \"full scan\", or \"scan everything\"\n4. NEVER REPEAT A TOOL CALL. If you already called a tool and got results, summarize them. Do NOT call the same tool again with the same arguments.\n5. One tool per response. 1-2 lines of reasoning MAX before the tool block.\n6. To find files/dirs by name: shell.exec find /path -maxdepth 3 -name '*pattern*'\n7. CONTINUE only if the original task is NOT yet done. Resolve sub-problems then proceed.\n8. Use conversation history for follow-ups. \"it\", \"that\", \"such\" = context from previous messages.\n9. Suppress noise: curl -s, wget -q. Always use full absolute paths.\n10. Never run cd, pwd, or re-list directories you already listed.\n11. Only pentest systems the user owns or has permission to test.\n12. Do not invent volatile live data (IPs, scan results, dates). Re-run commands for current data.\n13. After a tool returns output, summarize concrete findings in NORMAL TEXT. Never say only \"check the output\".\n14. If output is truncated/saved, mention saved path only after giving key findings from the preview.\n15. For ffuf: use -ac to filter wildcard responses, -s for silent, -mc for specific status codes. Never use -q.\n16. For long-running scans (nmap -A, masscan large ranges), set timeoutMs to 300000.\n17. When a command fails with \"not found\" or \"command not found\":\n a. Use pkg.install to install the missing tool\n b. RETRY the original command immediately after install\n c. If pkg.install fails, try shell.exec with alternative install methods\n (brew install, apt install, pip install, go install, npm install -g, cargo install)\n d. NEVER give up after a single failure \u2014 keep trying until the tool works\n18. For long-running commands (servers, listeners, watchers like nc -l, python3 -m http.server, npm run dev, tail -f), use shell.start instead of shell.exec.\n19. For file edits (changing a line, updating config), prefer fs.edit over fs.write. fs.edit is atomic and validates the replacement. Only use fs.write for creating new files or complete rewrites.\n20. For file deletion, ALWAYS use fs.delete and explain what will be deleted. Never use shell.exec rm for deletion.\n21. For local network discovery: call net.context FIRST to get the correct CIDR, THEN net.pingSweep with that CIDR. Never guess subnet ranges.\n22. For current/latest/post-cutoff information (news, prices, releases, \"today\"), use web.search FIRST. If web.search returns ok=false or \"No results found.\", say current information is unavailable \u2014 DO NOT make up facts.\n23. For reading a known URL's content, use web.fetch (returns readable prose) \u2014 DO NOT use http.fetch for the same job. Reserve http.fetch for non-GET methods, raw bytes, or pentest-style protocol work.\n24. When the user's question is answerable from training data and contains no time-sensitive signal, answer directly. Do NOT call web.search.\n25. ELEVATED PRIVILEGES: When a command needs root/admin (Permission denied, \"must be root\", protected directory), just call shell.exec with `sudo <command>` directly. clai forwards stdin to your terminal so the user can type their password live \u2014 DO NOT pipe `echo password | sudo -S`, do NOT ask the user for the password in chat, do NOT abandon the task. On macOS/Linux use `sudo`; on Windows use `runas` or (Win11+) `sudo`. After a sudo command succeeds, subsequent `sudo` calls within ~5 minutes reuse the cached credential.\n\nAUTONOMOUS TOOL SELECTION:\n- YOU decide the best tool for the task. Do NOT wait for the user to name a tool.\n Think: \"What is the most effective command/tool for this task on this OS?\" Then run it.\n- If the user says \"scan ports on X\" \u2192 you decide: nmap? masscan? net.scan wrapper?\n Pick the best one based on context (speed, OS, what's installed, scan scope).\n- If the user says \"find subdomains\" \u2192 you decide: subfinder? amass? ffuf vhost? dig?\n- If the user says \"check for vulnerabilities\" \u2192 you decide: nikto? nuclei? nmap scripts?\n- You can run ANY command via shell.exec. The built-in tools (net.scan, dns.lookup, etc.)\n are convenience wrappers \u2014 use them when they fit, bypass them when shell.exec is better.\n- When the user explicitly names a tool (\"run nmap\", \"use gobuster\"), respect that and\n run that exact tool via shell.exec. Do NOT substitute a wrapper.\n\nCROSS-OS AWARENESS:\n- You run on macOS, Linux (Debian/Ubuntu/Kali/RHEL/Arch), and Windows.\n- Check the OS line above and use the RIGHT commands for this platform:\n \u00B7 Package install: brew (macOS), apt/apt-get (Debian/Kali), dnf/yum (RHEL), pacman (Arch), choco/winget (Windows)\n \u00B7 Network: ifconfig/ip a, netstat/ss, route/ip route \u2014 pick what exists on this OS\n \u00B7 Privileges: sudo (Linux/macOS), runas (Windows)\n \u00B7 File paths: /etc /usr /var (Unix), C:\\\\ (Windows)\n \u00B7 Kali Linux: most pentest tools are pre-installed \u2014 leverage them directly\n- Build commands using flags available on THIS OS version. Do NOT use GNU-only flags on macOS BSD tools or vice versa.\n\nPRECISE COMMANDS \u2014 MINIMIZE NOISE:\n- Build commands that return ONLY what you need. Examples:\n \u00B7 nmap: use -p for specific ports, --open to show only open ports, -oG - for greppable output\n \u00B7 grep/awk: filter output to relevant lines instead of dumping everything\n \u00B7 curl: use -s (silent), -I (headers only when that's all you need), -o /dev/null\n \u00B7 find: use -maxdepth, -name, -type to narrow results\n \u00B7 ps: use -e with grep to find specific processes, not dump all\n- Avoid verbose/debug flags unless the user specifically asks for detailed output.\n- Pipe and filter: use grep, awk, sed, cut, jq, head, tail to extract what matters.\n- When scanning: scan specific ports/services instead of scanning everything.\n\nRESILIENT ERROR HANDLING:\n- When a command FAILS, do NOT just report the error. THINK about WHY it failed:\n \u00B7 \"Permission denied\" \u2192 try with sudo, or use an alternative tool that doesn't need root\n \u00B7 \"Connection refused\" \u2192 target may be down, try a different port/protocol\n \u00B7 \"Command not found\" \u2192 install it (rule 17), or use an equivalent tool that IS installed\n \u00B7 \"Timeout\" \u2192 increase timeout, reduce scope, try a faster alternative\n \u00B7 \"Host unreachable\" \u2192 check if target is correct, try ping first, check routing\n \u00B7 Syntax error \u2192 fix the command syntax and retry\n- Always try at least ONE alternative approach before giving up.\n- Chain: fail \u2192 diagnose \u2192 fix/adapt \u2192 retry. Never stop at the first error.\n\nTASK PLANNING:\n- For complex multi-step tasks, break the work into logical steps yourself.\n Execute them one by one. You own the plan \u2014 nothing is predetermined.\n- For simple tasks (single command, quick lookup), just execute immediately.\n- If a step fails, adapt your plan. Don't rigidly follow a broken path.\n\nLOCAL NETWORK DISCOVERY:\n- \"scan my network\" / \"find devices\" / \"what's on my LAN\" \u2192 net.context FIRST (gets interfaces+CIDR), then net.pingSweep with discovered CIDR.\n- Do NOT guess 192.168.1.0/24 or any range. Always discover it via net.context.\n- Do NOT use shell.exec for ping sweeps. Use net.pingSweep which has intelligent fallback.\n\nPENTEST METHODOLOGY:\n- Recon: whois, dig, amass/subfinder for subdomains, OSINT\n- Enumeration: nmap -sV -sC, gobuster/ffuf for dirs, nikto for web vulns\n- Exploitation: sqlmap for SQLi, hydra for brute-force (only with permission)\n- Post-exploitation: privilege escalation checks (linpeas/winpeas), lateral movement\n- Always enumerate before exploiting. Suggest logical next steps after each finding.\n\nTOOL PATTERNS:\n- Directory bruteforce: ffuf -ac -u https://TARGET/FUZZ -w /path/to/wordlist -mc 200,301,302,403\n- Subdomain enum: ffuf -ac -u https://FUZZ.target.com -w /path/to/subdomains.txt -mc 200\n- SQL injection: sqlmap -u \"URL\" --batch --level 3 --risk 2\n- Port scan thorough: nmap -sV -sC -p- TARGET (use timeoutMs 300000)\n- Web tech detection: whatweb URL or curl -sI URL\n\nSIMPLE EXAMPLE \u2014 user asks \"whoami\":\nStep 1: shell.exec whoami \u2192 \"aniket\". Answer: \"You are aniket.\" DONE.\n\nNARROW RECON EXAMPLE \u2014 user asks \"who registered example.com\":\nStep 1: whois.lookup target=example.com \u2192 registrar info. Answer with the registrar, abuse email, and creation date. DONE. Do NOT also run dns.lookup or nmap.\n\nNARROW DNS EXAMPLE \u2014 user asks \"MX records for example.com\":\nStep 1: dns.lookup target=example.com record=MX \u2192 records. Report each MX with priority. DONE. Do NOT also run whois.\n\nDOMAIN INFO EXAMPLE \u2014 user asks \"find all info about example.com\":\nStep 1: whois.lookup target=example.com \u2192 registrar, creation date, nameservers.\nStep 2: dns.lookup target=example.com record=ANY \u2192 A, AAAA, MX, NS, TXT records.\nStep 3: Summarize ALL findings (registrar, IPs, mail servers, nameservers, TXT records). DONE. Do NOT run nmap unless the user explicitly asked for port scanning.\n\nCOMPLEX EXAMPLE \u2014 user asks \"directory scan on example.com\":\nStep 1: Find wordlist \u2192 shell.exec find /usr -maxdepth 4 -name 'common.txt' -path '*/Discovery/*'\nStep 2: Run scan \u2192 shell.exec ffuf -ac -u https://example.com/FUZZ -w /path/common.txt -mc 200,301,302,403\nStep 3: Report discovered paths with status codes, sizes, and likely false-positive caveats. DONE.\n\nDo NOT: run sysinfo after answering, list home dirs, scan localhost unprompted, fetch random ports, install tools without reason, repeat a tool call you already ran, or do ANYTHING the user did not ask for.";
|
|
8
8
|
export declare function renderAskSystemPrompt(): string;
|
|
9
9
|
export declare function renderAgentSystemPrompt(toolList: string): string;
|
package/dist/prompts/index.js
CHANGED
|
@@ -21,6 +21,8 @@ TOOLS (use EXACT arg names — wrong names = failure):
|
|
|
21
21
|
- pkg.install: {"tool":"<name>"} — install package (only if user asks or command not found)
|
|
22
22
|
- net.scan: {"target":"<ip|cidr|hostname>","ports":"<optional 80,443,1-1000>","profile":{"scanType":"syn|tcp|udp|ping","serviceDetect":bool,"topPorts":int,"timing":"T0|T1|T2|T3|T4|T5","scripts":["safe-script-name"]},"iOwnThis":bool} — nmap scan. Target/ports/flags are strictly validated (no shell injection). Prefer the structured profile field; the legacy flags string still works but every token must be safe.
|
|
23
23
|
- http.fetch: {"url":"<url>","method":"<optional GET|HEAD|POST|PUT|PATCH|DELETE|OPTIONS>","body":"<optional>","headers":{"Key":"Value"},"maxBytes":<optional>,"iOwnThis":<optional bool>} — HTTP request. GET/HEAD auto-execute against public URLs; non-GET/HEAD and private/loopback/metadata addresses require confirmation; pass iOwnThis=true to allow private targets you own.
|
|
24
|
+
- web.search: {"query":"<text>","maxResults":<optional 1-20>} — search the public web. Returns {title,url,snippet}[]. Use this for current events, recent docs, post-cutoff facts. Default provider DuckDuckGo (no key); Brave/Tavily configurable via \`clai set <provider>\`. Auto-executes.
|
|
25
|
+
- web.fetch: {"url":"<https url>","maxBytes":<optional>,"responseMode":"<readable|raw>","includeHeaders":<bool>,"includeTls":<bool>,"includeTiming":<bool>,"includeRedirectChain":<bool>,"redactSensitive":<bool>} — fetch a URL and return readable text plus HTTP/TLS metadata (headers, cipher, redirect chain, timing, resolved IP). Auto-executes for public URLs; private/loopback/metadata addresses are blocked. Sensitive headers/cookies redacted by default.
|
|
24
26
|
- sysinfo: {} — OS info
|
|
25
27
|
- dns.lookup: {"target":"<host>","record":"<A|AAAA|CNAME|MX|NS|TXT|SOA|SRV|CAA|PTR|ANY>"} — single dig query. Use this for ANY narrow DNS question (resolve a host, find MX, dump TXT). Auto-executes; do NOT use pentest.recon or shell.exec for one-record lookups.
|
|
26
28
|
- whois.lookup: {"target":"<host|ip>"} — single whois query for registrar / ownership / abuse contact info. Use this when the user asks about who owns or registered a domain. Auto-executes; do NOT chain into pentest.recon.
|
|
@@ -79,6 +81,10 @@ RULES:
|
|
|
79
81
|
19. For file edits (changing a line, updating config), prefer fs.edit over fs.write. fs.edit is atomic and validates the replacement. Only use fs.write for creating new files or complete rewrites.
|
|
80
82
|
20. For file deletion, ALWAYS use fs.delete and explain what will be deleted. Never use shell.exec rm for deletion.
|
|
81
83
|
21. For local network discovery: call net.context FIRST to get the correct CIDR, THEN net.pingSweep with that CIDR. Never guess subnet ranges.
|
|
84
|
+
22. For current/latest/post-cutoff information (news, prices, releases, "today"), use web.search FIRST. If web.search returns ok=false or "No results found.", say current information is unavailable — DO NOT make up facts.
|
|
85
|
+
23. For reading a known URL's content, use web.fetch (returns readable prose) — DO NOT use http.fetch for the same job. Reserve http.fetch for non-GET methods, raw bytes, or pentest-style protocol work.
|
|
86
|
+
24. When the user's question is answerable from training data and contains no time-sensitive signal, answer directly. Do NOT call web.search.
|
|
87
|
+
25. ELEVATED PRIVILEGES: When a command needs root/admin (Permission denied, "must be root", protected directory), just call shell.exec with \`sudo <command>\` directly. clai forwards stdin to your terminal so the user can type their password live — DO NOT pipe \`echo password | sudo -S\`, do NOT ask the user for the password in chat, do NOT abandon the task. On macOS/Linux use \`sudo\`; on Windows use \`runas\` or (Win11+) \`sudo\`. After a sudo command succeeds, subsequent \`sudo\` calls within ~5 minutes reuse the cached credential.
|
|
82
88
|
|
|
83
89
|
AUTONOMOUS TOOL SELECTION:
|
|
84
90
|
- YOU decide the best tool for the task. Do NOT wait for the user to name a tool.
|
|
@@ -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;;;;;;;;;0LASwK,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;;;;;;;;;0LASwK,CAAC;AAE3L,MAAM,WAAW,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;+MAqK2L,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;;;;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,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,SAAS,EAAE,QAAQ;KACpB,CAAC,CAAC;AACL,CAAC"}
|
package/dist/repl.d.ts
CHANGED
|
@@ -12,4 +12,5 @@ export interface SlashCommand {
|
|
|
12
12
|
}
|
|
13
13
|
export declare function getKnownModels(provider: string): string[];
|
|
14
14
|
export declare function getSlashCommandSuggestions(line: string): SlashCommand[];
|
|
15
|
+
export declare function renderSlashCommandMenu(line: string, suggestions: SlashCommand[], selectedIndex: number): string[];
|
|
15
16
|
export declare function startRepl(options?: ReplOptions): Promise<void>;
|
package/dist/repl.js
CHANGED
|
@@ -5,7 +5,7 @@ import { runAskStream } from "./modes/ask.js";
|
|
|
5
5
|
import { runAgent, createSessionPolicy, } from "./modes/agent.js";
|
|
6
6
|
import { providerSwitcher, printProviderKeys, setProviderKey, unsetProviderKey, } from "./commands/providers.js";
|
|
7
7
|
import { getConfig, getProviderModel, setDefaultMode, setProviderModel, setThinking, updateConfig, } from "./store/config.js";
|
|
8
|
-
import { listSessions, saveSession, clearAllHistory, getSession } from "./store/history.js";
|
|
8
|
+
import { listSessions, saveSession, clearAllHistory, getSession, } from "./store/history.js";
|
|
9
9
|
import { assertProvider, defaultModels } from "./llm/provider.js";
|
|
10
10
|
import { runUpdate, checkForUpdateSilent, getCurrentVersion, } from "./commands/update.js";
|
|
11
11
|
import { renderBanner, renderSessionInfo, renderSuggestions, renderModeSwitch, renderProviderSwitch, PROMPT, } from "./ui/banner.js";
|
|
@@ -48,8 +48,14 @@ const slashCommands = [
|
|
|
48
48
|
description: "alias for /variants",
|
|
49
49
|
},
|
|
50
50
|
{ command: "/clear", description: "clear context" },
|
|
51
|
-
{
|
|
52
|
-
|
|
51
|
+
{
|
|
52
|
+
command: "/new",
|
|
53
|
+
description: "start a fresh session (clear context, no history carryover)",
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
command: "/history",
|
|
57
|
+
description: "browse & resume past sessions (interactive picker)",
|
|
58
|
+
},
|
|
53
59
|
{ command: "/save", usage: "<name>", description: "save session" },
|
|
54
60
|
{ command: "/reset", description: "clear all saved history" },
|
|
55
61
|
{ command: "/cwd", usage: "<path>", description: "change working directory" },
|
|
@@ -93,7 +99,10 @@ const slashCommands = [
|
|
|
93
99
|
description: "control retention and private mode (in-memory only)",
|
|
94
100
|
},
|
|
95
101
|
{ command: "/update", description: "check for updates" },
|
|
96
|
-
{
|
|
102
|
+
{
|
|
103
|
+
command: "/clean",
|
|
104
|
+
description: "clear screen and reset chat (fresh start)",
|
|
105
|
+
},
|
|
97
106
|
{ command: "/exit", description: "quit" },
|
|
98
107
|
{ command: "/quit", description: "alias for /exit" },
|
|
99
108
|
{ command: "/help", description: "list commands" },
|
|
@@ -256,57 +265,100 @@ export function getSlashCommandSuggestions(line) {
|
|
|
256
265
|
return [];
|
|
257
266
|
return slashCommands.filter((command) => command.command.slice(1).toLowerCase().startsWith(filter));
|
|
258
267
|
}
|
|
259
|
-
function
|
|
268
|
+
function fitPlain(text, maxWidth) {
|
|
269
|
+
if (maxWidth <= 0)
|
|
270
|
+
return "";
|
|
271
|
+
if (text.length <= maxWidth)
|
|
272
|
+
return text;
|
|
273
|
+
if (maxWidth === 1)
|
|
274
|
+
return "…";
|
|
275
|
+
return `${text.slice(0, maxWidth - 1)}…`;
|
|
276
|
+
}
|
|
277
|
+
export function renderSlashCommandMenu(line, suggestions, selectedIndex) {
|
|
278
|
+
const cols = terminalColumns();
|
|
279
|
+
// Stay one column short to avoid terminal autowrap when the cursor lands in
|
|
280
|
+
// the final column. refresh() depends on each menu item occupying one row.
|
|
281
|
+
const maxWidth = Math.max(1, cols - 1);
|
|
260
282
|
if (suggestions.length === 0) {
|
|
261
|
-
return [chalk.dim(` no commands matching ${line}
|
|
283
|
+
return [chalk.dim(fitPlain(` no commands matching ${line}`, maxWidth))];
|
|
262
284
|
}
|
|
263
285
|
// Cap visible items to leave room in the terminal
|
|
264
286
|
const termRows = process.stdout.rows || 24;
|
|
265
287
|
const maxVisible = Math.max(5, termRows - 4);
|
|
266
288
|
const visible = suggestions.slice(0, maxVisible);
|
|
267
289
|
const maxCommandLength = Math.max(...visible.map((command) => slashCommandLabel(command).length));
|
|
268
|
-
const cols = process.stdout.columns || 80;
|
|
269
290
|
const items = visible.map((command, index) => {
|
|
291
|
+
const markerPlain = index === selectedIndex ? "›" : " ";
|
|
270
292
|
const marker = index === selectedIndex ? chalk.magenta("›") : " ";
|
|
271
|
-
const
|
|
272
|
-
const
|
|
273
|
-
|
|
274
|
-
const
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
const descMaxLen = Math.max(0, cols - 1 - stripAnsi(` ${marker} ${label}`).length - 1);
|
|
279
|
-
const truncDesc = command.description.slice(0, descMaxLen) + "…";
|
|
280
|
-
return ` ${marker} ${chalk.cyan(label)}${chalk.dim(truncDesc)}`;
|
|
281
|
-
}
|
|
282
|
-
return full;
|
|
293
|
+
const prefix = ` ${markerPlain} `;
|
|
294
|
+
const labelBudget = Math.max(1, maxWidth - prefix.length);
|
|
295
|
+
const labelWidth = Math.min(maxCommandLength + 2, labelBudget);
|
|
296
|
+
const label = fitPlain(slashCommandLabel(command), labelWidth).padEnd(labelWidth);
|
|
297
|
+
const descWidth = Math.max(0, maxWidth - prefix.length - label.length);
|
|
298
|
+
const desc = fitPlain(command.description, descWidth);
|
|
299
|
+
return ` ${marker} ${chalk.cyan(label)}${chalk.dim(desc)}`;
|
|
283
300
|
});
|
|
284
301
|
if (suggestions.length > maxVisible) {
|
|
285
|
-
|
|
286
|
-
items.push(more);
|
|
302
|
+
items.push(chalk.dim(fitPlain(` … ${suggestions.length - maxVisible} more`, maxWidth)));
|
|
287
303
|
}
|
|
288
304
|
return items;
|
|
289
305
|
}
|
|
290
306
|
function isPrintableSequence(sequence) {
|
|
291
307
|
return sequence !== undefined && /^[^\x00-\x1f\x7f]+$/u.test(sequence);
|
|
292
308
|
}
|
|
309
|
+
function terminalColumns() {
|
|
310
|
+
return Math.max(1, process.stdout.columns || 80);
|
|
311
|
+
}
|
|
312
|
+
function promptCursorPosition(cursor, columns) {
|
|
313
|
+
const cols = Math.max(1, columns);
|
|
314
|
+
const visible = promptColumnsForRender() + cursor;
|
|
315
|
+
return {
|
|
316
|
+
row: Math.floor(visible / cols),
|
|
317
|
+
col: visible % cols,
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
function promptColumnsForRender() {
|
|
321
|
+
return stripAnsi(PROMPT).length;
|
|
322
|
+
}
|
|
323
|
+
function buildPromptRows(line, columns, includeCursorRow) {
|
|
324
|
+
const cols = Math.max(1, columns);
|
|
325
|
+
const promptCols = promptColumnsForRender();
|
|
326
|
+
const rows = [];
|
|
327
|
+
if (promptCols >= cols) {
|
|
328
|
+
// The normal prompt is short; for extremely narrow terminals, keep the
|
|
329
|
+
// editable text on rows beneath the prompt anchor.
|
|
330
|
+
rows.push(PROMPT);
|
|
331
|
+
for (let i = 0; i < line.length; i += cols) {
|
|
332
|
+
rows.push(line.slice(i, i + cols));
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
else {
|
|
336
|
+
const firstRowCapacity = cols - promptCols;
|
|
337
|
+
rows.push(PROMPT + line.slice(0, firstRowCapacity));
|
|
338
|
+
for (let i = firstRowCapacity; i < line.length; i += cols) {
|
|
339
|
+
rows.push(line.slice(i, i + cols));
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
if (includeCursorRow) {
|
|
343
|
+
const cursorRows = Math.floor((promptCols + line.length) / cols) + 1;
|
|
344
|
+
while (rows.length < cursorRows)
|
|
345
|
+
rows.push("");
|
|
346
|
+
}
|
|
347
|
+
return rows;
|
|
348
|
+
}
|
|
293
349
|
async function readPromptLine(options) {
|
|
294
350
|
return new Promise((resolve) => {
|
|
295
351
|
let line = "";
|
|
296
352
|
let cursor = 0;
|
|
297
353
|
let selectedIndex = 0;
|
|
298
354
|
let menuNavigated = false;
|
|
299
|
-
let renderedMenuLines = 0;
|
|
300
355
|
let dismissedSlashLine = null;
|
|
301
356
|
let historyIndex = null;
|
|
302
357
|
let historyDraft = "";
|
|
303
358
|
let lastCtrlCAt = 0;
|
|
304
|
-
const promptColumns = stripAnsi(PROMPT).length;
|
|
305
359
|
// Track which row (relative to prompt start) the cursor is on.
|
|
306
360
|
// Needed to move back up to prompt start when text wraps across rows.
|
|
307
361
|
let promptCursorRow = 0;
|
|
308
|
-
// How many physical rows the prompt text occupied on last render.
|
|
309
|
-
let prevPromptRows = 1;
|
|
310
362
|
const getMenuState = () => {
|
|
311
363
|
const filter = slashCommandFilter(line);
|
|
312
364
|
if (filter === null || dismissedSlashLine === line) {
|
|
@@ -318,83 +370,31 @@ async function readPromptLine(options) {
|
|
|
318
370
|
return { visible: true, suggestions };
|
|
319
371
|
};
|
|
320
372
|
const refresh = () => {
|
|
321
|
-
const cols =
|
|
373
|
+
const cols = terminalColumns();
|
|
322
374
|
const menu = getMenuState();
|
|
323
375
|
const menuLines = menu.visible
|
|
324
376
|
? renderSlashCommandMenu(line, menu.suggestions, selectedIndex)
|
|
325
377
|
: [];
|
|
326
|
-
|
|
378
|
+
const promptRows = buildPromptRows(line, cols, true);
|
|
379
|
+
const target = promptCursorPosition(cursor, cols);
|
|
380
|
+
const blockRows = [...promptRows, ...menuLines];
|
|
381
|
+
// Always redraw the whole prompt block from its anchor. Partial row
|
|
382
|
+
// clearing is fragile once terminal autowrap, slash menus, and cursor
|
|
383
|
+
// movement mix; clearing to the end of screen leaves no stale wrapped
|
|
384
|
+
// prompt/menu rows behind and keeps the cursor anchor stable.
|
|
327
385
|
if (promptCursorRow > 0) {
|
|
328
386
|
moveCursor(output, 0, -promptCursorRow);
|
|
329
387
|
}
|
|
330
388
|
cursorTo(output, 0);
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
const
|
|
334
|
-
const
|
|
335
|
-
const promptRowsChanged = newPromptRows !== prevPromptRows;
|
|
336
|
-
// ── Clear old content ──
|
|
337
|
-
if (promptRowsChanged) {
|
|
338
|
-
// Prompt wrapped or unwrapped — old menu is at wrong offset.
|
|
339
|
-
// Clear ALL old rows (prompt + menu) so we can re-reserve cleanly.
|
|
340
|
-
const totalOldRows = prevPromptRows + renderedMenuLines;
|
|
341
|
-
const rowsToBlank = Math.max(totalOldRows, newPromptRows);
|
|
342
|
-
clearLine(output, 0);
|
|
343
|
-
for (let i = 1; i < rowsToBlank; i++) {
|
|
344
|
-
output.write("\x1b[B");
|
|
345
|
-
cursorTo(output, 0);
|
|
346
|
-
clearLine(output, 0);
|
|
347
|
-
}
|
|
348
|
-
if (rowsToBlank > 1)
|
|
349
|
-
moveCursor(output, 0, -(rowsToBlank - 1));
|
|
350
|
-
// Force menu to re-reserve space from the new position
|
|
351
|
-
renderedMenuLines = 0;
|
|
352
|
-
}
|
|
353
|
-
else {
|
|
354
|
-
// Prompt rows unchanged — just clear the prompt row(s)
|
|
355
|
-
clearLine(output, 0);
|
|
356
|
-
for (let i = 1; i < prevPromptRows; i++) {
|
|
357
|
-
output.write("\x1b[B");
|
|
358
|
-
cursorTo(output, 0);
|
|
359
|
-
clearLine(output, 0);
|
|
360
|
-
}
|
|
361
|
-
if (prevPromptRows > 1)
|
|
362
|
-
moveCursor(output, 0, -(prevPromptRows - 1));
|
|
363
|
-
}
|
|
364
|
-
// ── Write prompt ──
|
|
365
|
-
output.write(`${PROMPT}${line}`);
|
|
366
|
-
// ── Draw menu (original logic, relative to end of prompt text) ──
|
|
367
|
-
const linesToClear = Math.max(renderedMenuLines, menuLines.length);
|
|
368
|
-
if (linesToClear > 0) {
|
|
369
|
-
if (renderedMenuLines === 0 && menuLines.length > 0) {
|
|
370
|
-
// First draw (or re-draw after prompt rows change):
|
|
371
|
-
// reserve space by writing newlines, then come back.
|
|
372
|
-
output.write("\n".repeat(menuLines.length));
|
|
373
|
-
moveCursor(output, 0, -menuLines.length);
|
|
374
|
-
}
|
|
375
|
-
for (let i = 0; i < linesToClear; i += 1) {
|
|
376
|
-
// Move down one line without scrolling
|
|
377
|
-
output.write("\x1b[B");
|
|
378
|
-
cursorTo(output, 0);
|
|
379
|
-
clearLine(output, 0);
|
|
380
|
-
const menuLine = menuLines[i];
|
|
381
|
-
if (menuLine)
|
|
382
|
-
output.write(menuLine);
|
|
383
|
-
}
|
|
384
|
-
moveCursor(output, 0, -linesToClear);
|
|
385
|
-
}
|
|
386
|
-
renderedMenuLines = menuLines.length;
|
|
387
|
-
prevPromptRows = newPromptRows;
|
|
388
|
-
// ── Position cursor (wrapping-aware) ──
|
|
389
|
-
const cursorPos = promptColumns + cursor;
|
|
390
|
-
const targetRow = Math.floor(cursorPos / cols);
|
|
391
|
-
const targetCol = cursorPos % cols;
|
|
392
|
-
const rowDelta = targetRow - writeRow;
|
|
389
|
+
output.write("\x1b[J");
|
|
390
|
+
output.write(blockRows.join("\n"));
|
|
391
|
+
const currentRow = Math.max(0, blockRows.length - 1);
|
|
392
|
+
const rowDelta = target.row - currentRow;
|
|
393
393
|
if (rowDelta !== 0) {
|
|
394
394
|
moveCursor(output, 0, rowDelta);
|
|
395
395
|
}
|
|
396
|
-
cursorTo(output,
|
|
397
|
-
promptCursorRow =
|
|
396
|
+
cursorTo(output, target.col);
|
|
397
|
+
promptCursorRow = target.row;
|
|
398
398
|
};
|
|
399
399
|
const editLine = (nextLine, nextCursor) => {
|
|
400
400
|
line = nextLine;
|
|
@@ -405,11 +405,13 @@ async function readPromptLine(options) {
|
|
|
405
405
|
historyIndex = null;
|
|
406
406
|
refresh();
|
|
407
407
|
};
|
|
408
|
-
const cleanup = () => {
|
|
408
|
+
const cleanup = (restoreInput = false) => {
|
|
409
409
|
input.off("keypress", handleKeypress);
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
input.
|
|
410
|
+
if (restoreInput) {
|
|
411
|
+
input.pause();
|
|
412
|
+
if (input.isTTY)
|
|
413
|
+
input.setRawMode(false);
|
|
414
|
+
}
|
|
413
415
|
};
|
|
414
416
|
const clearPromptDisplay = () => {
|
|
415
417
|
// Move back to prompt start
|
|
@@ -418,24 +420,23 @@ async function readPromptLine(options) {
|
|
|
418
420
|
}
|
|
419
421
|
cursorTo(output, 0);
|
|
420
422
|
output.write("\x1b[J");
|
|
421
|
-
renderedMenuLines = 0;
|
|
422
423
|
promptCursorRow = 0;
|
|
423
|
-
prevPromptRows = 1;
|
|
424
424
|
};
|
|
425
425
|
const submit = (submittedLine) => {
|
|
426
426
|
line = submittedLine;
|
|
427
427
|
cursor = line.length;
|
|
428
|
-
renderedMenuLines = 0;
|
|
429
428
|
// Move back to prompt start
|
|
430
429
|
if (promptCursorRow > 0) {
|
|
431
430
|
moveCursor(output, 0, -promptCursorRow);
|
|
432
431
|
}
|
|
433
432
|
cursorTo(output, 0);
|
|
434
433
|
output.write("\x1b[J");
|
|
435
|
-
// Write final prompt
|
|
436
|
-
|
|
434
|
+
// Write final prompt using the same wrapping rules as refresh(), then
|
|
435
|
+
// move to the next line. Do not include the extra cursor-only row used
|
|
436
|
+
// for interactive editing at exact terminal-width boundaries.
|
|
437
|
+
output.write(buildPromptRows(line, terminalColumns(), false).join("\n"));
|
|
438
|
+
output.write("\n");
|
|
437
439
|
promptCursorRow = 0;
|
|
438
|
-
prevPromptRows = 1;
|
|
439
440
|
cleanup();
|
|
440
441
|
resolve(submittedLine);
|
|
441
442
|
};
|
|
@@ -462,7 +463,7 @@ async function readPromptLine(options) {
|
|
|
462
463
|
return;
|
|
463
464
|
}
|
|
464
465
|
if (now - lastCtrlCAt < 1_000) {
|
|
465
|
-
cleanup();
|
|
466
|
+
cleanup(true);
|
|
466
467
|
output.write("\n");
|
|
467
468
|
process.exit(0);
|
|
468
469
|
}
|
|
@@ -470,7 +471,6 @@ async function readPromptLine(options) {
|
|
|
470
471
|
output.write("\n");
|
|
471
472
|
output.write(chalk.dim(" (press Ctrl+C again to exit)\n"));
|
|
472
473
|
output.write(PROMPT);
|
|
473
|
-
renderedMenuLines = 0;
|
|
474
474
|
return;
|
|
475
475
|
}
|
|
476
476
|
if (isCtrlT(key)) {
|
|
@@ -670,10 +670,20 @@ async function streamWithAbort(run, signal) {
|
|
|
670
670
|
}
|
|
671
671
|
async function withAbortableInput(run) {
|
|
672
672
|
const ac = new AbortController();
|
|
673
|
+
const abortFromRawData = (chunk) => {
|
|
674
|
+
const text = typeof chunk === "string" ? chunk : chunk.toString("utf8");
|
|
675
|
+
if (text.includes("\x03") || text === "\x1b") {
|
|
676
|
+
ac.abort();
|
|
677
|
+
}
|
|
678
|
+
};
|
|
673
679
|
currentAbortController = ac;
|
|
674
680
|
if (input.isTTY)
|
|
675
681
|
input.setRawMode(true);
|
|
676
682
|
input.resume();
|
|
683
|
+
// readline's keypress normalization is not equally reliable in every
|
|
684
|
+
// terminal (notably Windows PowerShell after raw-mode transitions). Watch
|
|
685
|
+
// raw bytes as a fallback so Ctrl+C and a bare ESC can always abort a run.
|
|
686
|
+
input.on("data", abortFromRawData);
|
|
677
687
|
try {
|
|
678
688
|
return await run(ac.signal);
|
|
679
689
|
}
|
|
@@ -685,10 +695,14 @@ async function withAbortableInput(run) {
|
|
|
685
695
|
throw error;
|
|
686
696
|
}
|
|
687
697
|
finally {
|
|
688
|
-
input.
|
|
689
|
-
if (input.isTTY)
|
|
690
|
-
input.setRawMode(false);
|
|
698
|
+
input.off("data", abortFromRawData);
|
|
691
699
|
currentAbortController = null;
|
|
700
|
+
// Keep stdin raw/resumed for the next prompt. Toggling back to cooked mode
|
|
701
|
+
// between a response and the following prompt can leave PowerShell waiting
|
|
702
|
+
// for Enter before keypress events flow again.
|
|
703
|
+
if (input.isTTY)
|
|
704
|
+
input.setRawMode(true);
|
|
705
|
+
input.resume();
|
|
692
706
|
}
|
|
693
707
|
}
|
|
694
708
|
function help() {
|
|
@@ -817,7 +831,8 @@ async function pickInline(options) {
|
|
|
817
831
|
if (key.name === "up") {
|
|
818
832
|
const filtered = getFiltered();
|
|
819
833
|
if (filtered.length > 0) {
|
|
820
|
-
selectedIndex =
|
|
834
|
+
selectedIndex =
|
|
835
|
+
(selectedIndex - 1 + filtered.length) % filtered.length;
|
|
821
836
|
renderMenu();
|
|
822
837
|
}
|
|
823
838
|
return;
|
|
@@ -832,7 +847,8 @@ async function pickInline(options) {
|
|
|
832
847
|
}
|
|
833
848
|
if (key.name === "backspace") {
|
|
834
849
|
if (filterCursor > 0) {
|
|
835
|
-
filter =
|
|
850
|
+
filter =
|
|
851
|
+
filter.slice(0, filterCursor - 1) + filter.slice(filterCursor);
|
|
836
852
|
filterCursor -= 1;
|
|
837
853
|
selectedIndex = 0;
|
|
838
854
|
renderMenu();
|
|
@@ -841,7 +857,8 @@ async function pickInline(options) {
|
|
|
841
857
|
}
|
|
842
858
|
if (key.name === "delete") {
|
|
843
859
|
if (filterCursor < filter.length) {
|
|
844
|
-
filter =
|
|
860
|
+
filter =
|
|
861
|
+
filter.slice(0, filterCursor) + filter.slice(filterCursor + 1);
|
|
845
862
|
selectedIndex = 0;
|
|
846
863
|
renderMenu();
|
|
847
864
|
}
|
|
@@ -862,7 +879,8 @@ async function pickInline(options) {
|
|
|
862
879
|
return;
|
|
863
880
|
}
|
|
864
881
|
if (isPrintableSequence(sequence) && !key.ctrl && !key.meta) {
|
|
865
|
-
filter =
|
|
882
|
+
filter =
|
|
883
|
+
filter.slice(0, filterCursor) + sequence + filter.slice(filterCursor);
|
|
866
884
|
filterCursor += sequence.length;
|
|
867
885
|
selectedIndex = 0;
|
|
868
886
|
renderMenu();
|
|
@@ -1084,7 +1102,10 @@ async function handleSlash(line, state) {
|
|
|
1084
1102
|
if (name.startsWith("repl-")) {
|
|
1085
1103
|
const firstUser = s.messages.find((m) => m.role === "user");
|
|
1086
1104
|
if (firstUser) {
|
|
1087
|
-
const preview = firstUser.content
|
|
1105
|
+
const preview = firstUser.content
|
|
1106
|
+
.slice(0, 60)
|
|
1107
|
+
.replace(/\n/g, " ")
|
|
1108
|
+
.trim();
|
|
1088
1109
|
name = preview + (firstUser.content.length > 60 ? "…" : "");
|
|
1089
1110
|
}
|
|
1090
1111
|
}
|
|
@@ -1555,8 +1576,7 @@ export async function startRepl(options = {}) {
|
|
|
1555
1576
|
if (isCtrlO(key) && !isReadingPrompt) {
|
|
1556
1577
|
void handleOutputShortcut();
|
|
1557
1578
|
}
|
|
1558
|
-
if ((isEscape(key) || isCtrlC(key)) &&
|
|
1559
|
-
currentAbortController) {
|
|
1579
|
+
if ((isEscape(key) || isCtrlC(key)) && currentAbortController) {
|
|
1560
1580
|
abortPressCount += 1;
|
|
1561
1581
|
currentAbortController.abort();
|
|
1562
1582
|
// Escalate: after the first abort attempt the child process
|
|
@@ -1649,6 +1669,12 @@ export async function startRepl(options = {}) {
|
|
|
1649
1669
|
promptHistory.push(line);
|
|
1650
1670
|
}
|
|
1651
1671
|
if (line.startsWith("/")) {
|
|
1672
|
+
// Slash commands may call inquirer/password prompts, which expect the
|
|
1673
|
+
// terminal in cooked mode. Normal model runs keep raw mode enabled so
|
|
1674
|
+
// ESC/Ctrl+C can abort while streaming.
|
|
1675
|
+
input.pause();
|
|
1676
|
+
if (input.isTTY)
|
|
1677
|
+
input.setRawMode(false);
|
|
1652
1678
|
const shouldContinue = await handleSlash(line, state);
|
|
1653
1679
|
if (!shouldContinue)
|
|
1654
1680
|
break;
|