@roll-agent/core 0.11.0 → 0.12.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.
Files changed (61) hide show
  1. package/bin/roll.js +18 -1
  2. package/dist/cli/chat/ink/app.d.ts +15 -0
  3. package/dist/cli/chat/ink/app.js +1 -0
  4. package/dist/cli/chat/ink/commands.d.ts +6 -0
  5. package/dist/cli/chat/ink/commands.js +1 -0
  6. package/dist/cli/chat/ink/confirm-select.d.ts +6 -0
  7. package/dist/cli/chat/ink/confirm-select.js +1 -0
  8. package/dist/cli/chat/ink/history-from-messages.d.ts +3 -0
  9. package/dist/cli/chat/ink/history-from-messages.js +1 -0
  10. package/dist/cli/chat/ink/history-item.d.ts +5 -0
  11. package/dist/cli/chat/ink/history-item.js +1 -0
  12. package/dist/cli/chat/ink/live-region.d.ts +5 -0
  13. package/dist/cli/chat/ink/live-region.js +1 -0
  14. package/dist/cli/chat/ink/markdown.d.ts +4 -0
  15. package/dist/cli/chat/ink/markdown.js +1 -0
  16. package/dist/cli/chat/ink/run-ink-repl.d.ts +13 -0
  17. package/dist/cli/chat/ink/run-ink-repl.js +1 -0
  18. package/dist/cli/chat/ink/slash-popup.d.ts +7 -0
  19. package/dist/cli/chat/ink/slash-popup.js +1 -0
  20. package/dist/cli/chat/ink/spinner.d.ts +2 -0
  21. package/dist/cli/chat/ink/spinner.js +1 -0
  22. package/dist/cli/chat/ink/state.d.ts +95 -0
  23. package/dist/cli/chat/ink/state.js +1 -0
  24. package/dist/cli/chat/ink/status-line.d.ts +5 -0
  25. package/dist/cli/chat/ink/status-line.js +1 -0
  26. package/dist/cli/chat/ink/text-prompt.d.ts +12 -0
  27. package/dist/cli/chat/ink/text-prompt.js +1 -0
  28. package/dist/cli/chat/ink/thinking-text.d.ts +9 -0
  29. package/dist/cli/chat/ink/thinking-text.js +1 -0
  30. package/dist/cli/chat/ink/thinking.d.ts +3 -0
  31. package/dist/cli/chat/ink/thinking.js +1 -0
  32. package/dist/cli/chat/ink/tool-label.d.ts +4 -0
  33. package/dist/cli/chat/ink/tool-label.js +1 -0
  34. package/dist/cli/chat/ink/use-session.d.ts +20 -0
  35. package/dist/cli/chat/ink/use-session.js +1 -0
  36. package/dist/cli/chat/title.d.ts +1 -0
  37. package/dist/cli/chat/title.js +1 -0
  38. package/dist/cli/commands/browser-stop.d.ts +4 -4
  39. package/dist/cli/commands/chat.d.ts +9 -0
  40. package/dist/cli/commands/chat.js +1 -1
  41. package/dist/cli/commands/config-guidance.d.ts +2 -2
  42. package/dist/cli/utils/chat-renderer.d.ts +7 -1
  43. package/dist/cli/utils/chat-renderer.js +1 -1
  44. package/dist/cli/utils/output.d.ts +1 -0
  45. package/dist/cli/utils/output.js +1 -1
  46. package/dist/cli/utils/package-manager.d.ts +5 -0
  47. package/dist/cli/utils/package-manager.js +1 -1
  48. package/dist/cli/utils/token-format.d.ts +18 -0
  49. package/dist/cli/utils/token-format.js +1 -0
  50. package/dist/cli/utils/tool-format.d.ts +1 -0
  51. package/dist/cli/utils/tool-format.js +1 -0
  52. package/dist/config/defaults.js +1 -1
  53. package/dist/config/key-codec.js +1 -1
  54. package/dist/config/schema.d.ts +164 -40
  55. package/dist/config/schema.js +1 -1
  56. package/dist/llm/providers.d.ts +5 -2
  57. package/dist/llm/providers.js +1 -1
  58. package/dist/mcp/client-manager.d.ts +2 -0
  59. package/dist/mcp/client-manager.js +1 -1
  60. package/dist/types/chat.d.ts +12 -0
  61. package/package.json +9 -4
package/bin/roll.js CHANGED
@@ -21,11 +21,28 @@ function hasExecFlag(flag) {
21
21
  return process.execArgv.includes(flag);
22
22
  }
23
23
 
24
- const needsSqliteFlag = !(await sqliteAvailable());
24
+ function hasWarningSuppression() {
25
+ return (
26
+ process.execArgv.includes("--no-warnings") ||
27
+ process.execArgv.includes("--disable-warning=ExperimentalWarning")
28
+ );
29
+ }
30
+
31
+ function resolveCommandName(argv) {
32
+ return argv.find((arg) => !arg.startsWith("-"));
33
+ }
34
+
35
+ const commandName = resolveCommandName(process.argv.slice(2));
36
+ const shouldEnableSqlite = commandName === "chat";
37
+ const hasSqliteFlag = hasExecFlag("--experimental-sqlite");
38
+ const needsSqliteFlag = shouldEnableSqlite && !hasSqliteFlag && !(await sqliteAvailable());
25
39
  const needsTypeStripFlag = sourceTreeAvailable() && !hasExecFlag("--experimental-strip-types");
40
+ const needsExperimentalWarningSuppression =
41
+ shouldEnableSqlite && (needsSqliteFlag || hasSqliteFlag) && !hasWarningSuppression();
26
42
 
27
43
  if (!process.env.ROLL_SQLITE_RESPAWNED && (needsSqliteFlag || needsTypeStripFlag)) {
28
44
  const nodeFlags = [
45
+ ...(needsExperimentalWarningSuppression ? ["--disable-warning=ExperimentalWarning"] : []),
29
46
  ...(needsSqliteFlag ? ["--experimental-sqlite"] : []),
30
47
  ...(needsTypeStripFlag ? ["--experimental-strip-types"] : []),
31
48
  ];
@@ -0,0 +1,15 @@
1
+ import type { ReactElement } from "react";
2
+ import type { AgentSession } from "@roll-agent/runtime";
3
+ import type { ThinkingLevel } from "../../../llm/providers.ts";
4
+ import type { HistoryItem } from "./state.ts";
5
+ export interface ChatAppProps {
6
+ readonly session: AgentSession;
7
+ readonly model: string;
8
+ readonly contextWindow: number | undefined;
9
+ readonly initialHistory?: readonly HistoryItem[];
10
+ readonly initialThinkingLevel?: ThinkingLevel;
11
+ readonly onThinkingChange?: (level: ThinkingLevel) => void;
12
+ readonly onUserSubmit: (text: string) => void;
13
+ readonly onExit: () => void;
14
+ }
15
+ export declare function ChatApp(props: ChatAppProps): ReactElement;
@@ -0,0 +1 @@
1
+ import{randomUUID as i}from"node:crypto";import{createElement as t,useState as n}from"react";import{Box as o,useInput as e}from"ink";import{useSession as m}from"./use-session.js";import{HistoryItemView as s}from"./history-item.js";import{LiveRegion as r}from"./live-region.js";import{StatusLine as a}from"./status-line.js";import{TextPrompt as l}from"./text-prompt.js";import{ConfirmSelect as f}from"./confirm-select.js";import{SlashPopup as h}from"./slash-popup.js";import{filterCommands as p,SLASH_COMMANDS as d}from"./commands.js";import{cycleThinking as c}from"./thinking.js";function u(){return d.map(i=>`${i.name} — ${i.description}`).join("\n")}export function ChatApp(g){const{session:k,model:v,contextWindow:x,onUserSubmit:j,onExit:C}=g,{state:y,submit:L,compact:T,resolveConfirm:M,setDraft:w,setThinking:S,commitHistory:b}=m(k,{model:v,contextWindow:x,...g.initialHistory?{initialHistory:g.initialHistory}:{},...g.initialThinkingLevel?{initialThinkingLevel:g.initialThinkingLevel}:{},...g.onThinkingChange?{onThinkingChange:g.onThinkingChange}:{}}),[D,H]=n(0),$="idle"===y.phase&&y.draft.startsWith("/"),W=$?p(y.draft):[],A=Math.max(W.length-1,0),E=Math.min(D,A);e((i,t)=>{t.meta&&"."===i?S(c(y.status.thinkingLevel,1)):t.meta&&","===i&&S(c(y.status.thinkingLevel,-1))});const R="confirm"===y.phase&&void 0!==y.pendingConfirm?t(f,{prompt:y.pendingConfirm.prompt,onDecide:M}):t(l,{value:y.draft,disabled:"idle"!==y.phase,slashActive:$,onChange:w,onSubmit:i=>{const t=i.trim();0!==t.length?(j(t),L(t)):w("")},onSlashMove:i=>{H(t=>Math.min(Math.max(Math.min(t,A)+i,0),A))},onSlashComplete:()=>{const i=W[E];i&&(w(`${i.name} `),H(0))},onSlashRun:()=>{const t=y.draft.trim().split(/\s+/,1)[0]??"";(t=>{w("");const n=t.trim().split(/\s+/),o=n[0]??"",e=(n[1]??"").toLowerCase(),m=y.status.thinkingLevel;"/compact"!==o?"/think"!==o?"/effort"!==o?"/help"!==o?"/exit"!==o?b({kind:"notice",id:i(),text:`未知命令 ${o}`}):C():b({kind:"notice",id:i(),text:u()}):"low"===e||"medium"===e||"high"===e?S(e):b({kind:"notice",id:i(),text:"用法: /effort low | medium | high"}):S("off"===e?"off":"on"===e?"off"===m?"medium":m:"off"===m?"medium":"off"):T()})(d.some(i=>i.name===t)?y.draft:W[E]?.name??y.draft)}});return t(o,{flexDirection:"column"},t(o,{flexDirection:"column"},...y.history.map(i=>{const n="user"===i.kind||"assistant"===i.kind,e="tool"===i.kind;return t(o,{key:i.id,marginTop:n?1:0,marginLeft:e?2:0},t(s,{item:i}))})),t(r,{live:y.live}),t(a,{status:y.status}),$?t(h,{matches:W,selected:E}):null,R)}
@@ -0,0 +1,6 @@
1
+ export interface SlashCommand {
2
+ readonly name: string;
3
+ readonly description: string;
4
+ }
5
+ export declare const SLASH_COMMANDS: readonly SlashCommand[];
6
+ export declare function filterCommands(input: string): SlashCommand[];
@@ -0,0 +1 @@
1
+ export const SLASH_COMMANDS=[{name:"/compact",description:"压缩上下文,释放 token"},{name:"/think",description:"开关 thinking/reasoning (on | off)"},{name:"/effort",description:"设置推理努力程度 (low | medium | high)"},{name:"/help",description:"列出可用命令"},{name:"/exit",description:"退出对话"}];export function filterCommands(e){const t=(e.split(/\s+/,1)[0]??"").toLowerCase();return SLASH_COMMANDS.filter(e=>e.name.toLowerCase().startsWith(t))}
@@ -0,0 +1,6 @@
1
+ import type { ReactElement } from "react";
2
+ export interface ConfirmSelectProps {
3
+ readonly prompt: string;
4
+ readonly onDecide: (approved: boolean) => void;
5
+ }
6
+ export declare function ConfirmSelect({ prompt, onDecide }: ConfirmSelectProps): ReactElement;
@@ -0,0 +1 @@
1
+ import{createElement as e,useState as r}from"react";import{Box as o,Text as n,useInput as t}from"ink";export function ConfirmSelect({prompt:c,onDecide:l}){const[i,s]=r("no");return t((e,r)=>{r.leftArrow||r.rightArrow||r.upArrow||r.downArrow?s(e=>"yes"===e?"no":"yes"):r.return||e.includes("\r")||e.includes("\n")?l("yes"===i):r.escape||"n"===e?l(!1):"y"===e&&l(!0)}),e(o,{flexDirection:"column"},e(n,null,c),e(o,null,e(n,"yes"===i?{color:"green"}:{},("yes"===i?"❯ ":" ")+"Yes"),e(n,"no"===i?{color:"green"}:{},` ${"no"===i?"❯ ":" "}No`)))}
@@ -0,0 +1,3 @@
1
+ import type { ModelMessage } from "ai";
2
+ import type { HistoryItem } from "./state.ts";
3
+ export declare function messagesToHistory(messages: readonly ModelMessage[], idPrefix?: string): HistoryItem[];
@@ -0,0 +1 @@
1
+ import{SUMMARY_ACK as t,SUMMARY_PREFIX as o}from"@roll-agent/runtime";import{formatToolInput as n}from"../../utils/tool-format.js";const e=new Set(["error-text","error-json","execution-denied"]);function r(t){return"string"==typeof t?t:t.map(t=>"text"===t.type?t.text:"").join("")}function s(t){const o=t.indexOf("__");return o>=0?`${t.slice(0,o)}.${t.slice(o+2)}`:t}export function messagesToHistory(i,c="h"){const l=new Map;for(const t of i)if("tool"===t.role&&"string"!=typeof t.content)for(const o of t.content)if("tool-result"===o.type){const t=o.output.type;l.set(o.toolCallId,!e.has(t))}const a=[];return i.forEach((e,i)=>{const f=`${c}-${String(i)}`;if("user"===e.role){const t=r(e.content);return t.startsWith(o)?void a.push({kind:"compaction",id:f,notice:"🗜 已恢复的上下文摘要"}):void(t.length>0&&a.push({kind:"user",id:f,text:t}))}if("assistant"===e.role){const o=r(e.content);if(o.startsWith(t))return;o.length>0&&a.push({kind:"assistant",id:f,text:o}),"string"!=typeof e.content&&e.content.forEach((t,o)=>{"tool-call"===t.type&&a.push({kind:"tool",id:`${f}-${String(o)}`,name:s(t.toolName),args:n(t.input),ok:l.get(t.toolCallId)??!0})})}}),a}
@@ -0,0 +1,5 @@
1
+ import type { ReactElement } from "react";
2
+ import type { HistoryItem } from "./state.ts";
3
+ export declare function HistoryItemView({ item }: {
4
+ item: HistoryItem;
5
+ }): ReactElement;
@@ -0,0 +1 @@
1
+ import{createElement as e}from"react";import{Box as t,Text as r}from"ink";import{parseThinking as o}from"./thinking-text.js";import{Markdown as n}from"./markdown.js";import{ToolLabel as i}from"./tool-label.js";export function HistoryItemView({item:l}){switch(l.kind){case"user":return e(t,null,e(r,{color:"cyan",bold:!0},"▌ "),e(r,{color:"cyan"},l.text));case"assistant":return e(t,{flexDirection:"column"},...o(l.text).map((t,o)=>t.thinking?e(r,{key:String(o),dimColor:!0},t.text):e(n,{key:String(o),text:t.text})));case"tool":return e(r,null,e(r,l.ok?{color:"green"}:{color:"red"},l.ok?"✓ ":"✗ "),e(i,{name:l.name}));case"compaction":return e(r,{dimColor:!0},l.notice);case"notice":return e(r,{color:"yellow"},`⚠ ${l.text}`);case"error":return e(r,{color:"red"},`✗ ${l.message}`);default:return e(r,null,"")}}
@@ -0,0 +1,5 @@
1
+ import type { ReactElement } from "react";
2
+ import type { LiveState } from "./state.ts";
3
+ export declare function LiveRegion({ live }: {
4
+ live: LiveState;
5
+ }): ReactElement;
@@ -0,0 +1 @@
1
+ import{createElement as l}from"react";import{Box as n,Text as o}from"ink";import{Spinner as t}from"./spinner.js";import{ThinkingText as e}from"./thinking-text.js";import{ToolLabel as i}from"./tool-label.js";export function LiveRegion({live:r}){return l(n,{flexDirection:"column"},r.streamingText.length>0?l(e,{text:r.streamingText}):null,r.thinking?l(n,{marginTop:1},l(t,null),l(o,{dimColor:!0}," 思考中…")):null,...r.activeTools.map(e=>l(n,{key:e.toolCallId},l(t,null),l(o,null," "),l(i,{name:e.name}),e.args.length>0?l(o,{dimColor:!0},` ${e.args}`):null)),r.compacting?l(n,null,l(t,null),l(o,{dimColor:!0}," 压缩上下文中…")):null)}
@@ -0,0 +1,4 @@
1
+ import type { ReactElement } from "react";
2
+ export declare function Markdown({ text }: {
3
+ text: string;
4
+ }): ReactElement;
@@ -0,0 +1 @@
1
+ import{createElement as e}from"react";import{Box as t,Text as r}from"ink";import{marked as n}from"marked";const o={"&amp;":"&","&lt;":"<","&gt;":">","&quot;":'"',"&#39;":"'","&#x27;":"'"};function c(e){return e.replace(/&(?:amp|lt|gt|quot|#39|#x27);/g,e=>o[e]??e)}function a(t,n){return void 0===t?[]:t.map((t,o)=>{const i=`${n}-${String(o)}`;switch(t.type){case"strong":return e(r,{key:i,bold:!0},...a(t.tokens,i));case"em":return e(r,{key:i,italic:!0},...a(t.tokens,i));case"codespan":return e(r,{key:i,color:"yellow"},c(t.text));case"del":return e(r,{key:i,strikethrough:!0},...a(t.tokens,i));case"link":return e(r,{key:i},e(r,{color:"cyan",underline:!0},t.text),e(r,{dimColor:!0},` (${t.href})`));case"br":return"\n";case"text":return void 0!==t.tokens?e(r,{key:i},...a(t.tokens,i)):c(t.text);default:return c(t.raw)}})}function i(e){let t=0;for(const r of e)t+=(r.codePointAt(0)??0)<128?1:2;return t}function u(n,o,c,i){return e(t,{key:c},...n.map((n,u)=>e(t,{key:`${c}-${String(u)}`,width:(o[u]??0)+2},e(r,i?{bold:!0}:{},...a(n.tokens,`${c}-${String(u)}`)))))}function l(n,o){const c=n.header.map((e,t)=>{let r=i(e.text);for(const e of n.rows)r=Math.max(r,i(e[t]?.text??""));return r}),a=e(t,{key:`${o}-sep`},...c.map((n,c)=>e(t,{key:`${o}-sep-${String(c)}`,width:n+2},e(r,{dimColor:!0},"─".repeat(n+1)))));return e(t,{key:o,flexDirection:"column"},u(n.header,c,`${o}-h`,!0),a,...n.rows.map((e,t)=>u(e,c,`${o}-r${String(t)}`,!1)))}function s(n,o){switch(n.type){case"space":return null;case"heading":return e(r,{key:o,bold:!0,color:"cyan"},...a(n.tokens,o));case"paragraph":return e(r,{key:o},...a(n.tokens,o));case"blockquote":return e(t,{key:o},e(r,{color:"gray"},"│ "),e(t,{flexDirection:"column"},...k(n.tokens??[],o)));case"code":return e(t,{key:o,paddingLeft:2},e(r,{dimColor:!0},n.text));case"list":return e(t,{key:o,flexDirection:"column"},...n.items.map((c,a)=>{const i=`${o}-${String(a)}`,u=n.ordered?`${String(Number(n.start||1)+a)}. `:"• ";return e(t,{key:i},e(r,{color:"cyan"},u),e(t,{flexDirection:"column"},...k(c.tokens,i)))}));case"hr":return e(r,{key:o,dimColor:!0},"────────");case"table":return l(n,o);case"text":return void 0!==n.tokens?e(r,{key:o},...a(n.tokens,o)):e(r,{key:o},c(n.text));default:return e(r,{key:o},n.raw)}}function k(e,t){return e.map((e,r)=>s(e,`${t}-${String(r)}`)).filter(e=>null!==e)}export function Markdown({text:o}){let c;try{c=n.lexer(o)}catch{return e(r,null,o)}return e(t,{flexDirection:"column"},...k(c,"md"))}
@@ -0,0 +1,13 @@
1
+ import type { AgentSession } from "@roll-agent/runtime";
2
+ import type { ThinkingLevel } from "../../../llm/providers.ts";
3
+ export interface InkReplStore {
4
+ updateTitle(threadId: string, title: string): void;
5
+ countMessages(threadId: string): number;
6
+ deleteThread(threadId: string): void;
7
+ }
8
+ export interface RunInkReplOptions {
9
+ readonly model: string;
10
+ readonly initialThinkingLevel?: ThinkingLevel;
11
+ readonly onThinkingChange?: (level: ThinkingLevel) => void;
12
+ }
13
+ export declare function runInkRepl(session: AgentSession, store: InkReplStore, isNewSession: boolean, options: RunInkReplOptions): Promise<void>;
@@ -0,0 +1 @@
1
+ import{createElement as i}from"react";import{render as t}from"ink";import{ChatApp as e}from"./app.js";import{messagesToHistory as n}from"./history-from-messages.js";import{titleFromMessage as o}from"../title.js";import{log as a}from"../../utils/output.js";export async function runInkRepl(r,s,l,m){let g=!1,d=!l;a.info("多轮对话已就绪(/exit 退出 · / 命令 · Shift+Enter/Ctrl+J 换行 · Alt+./Alt+, 调推理)");const p=t(i(e,{session:r,model:m.model,contextWindow:r.getContextWindow(),onUserSubmit:i=>{g=!0,d||(s.updateTitle(r.id,o(i)),d=!0)},onExit:()=>{p.unmount()},initialHistory:n(r.getMessages()),...m.initialThinkingLevel?{initialThinkingLevel:m.initialThinkingLevel}:{},...m.onThinkingChange?{onThinkingChange:m.onThinkingChange}:{}}),{kittyKeyboard:{mode:"auto",flags:["disambiguateEscapeCodes","reportAlternateKeys"]}});await p.waitUntilExit(),r.abort(),l&&!g&&0===s.countMessages(r.id)&&s.deleteThread(r.id)}
@@ -0,0 +1,7 @@
1
+ import type { ReactElement } from "react";
2
+ import type { SlashCommand } from "./commands.ts";
3
+ export interface SlashPopupProps {
4
+ readonly matches: readonly SlashCommand[];
5
+ readonly selected: number;
6
+ }
7
+ export declare function SlashPopup({ matches, selected }: SlashPopupProps): ReactElement;
@@ -0,0 +1 @@
1
+ import{createElement as r}from"react";import{Box as o,Text as e}from"ink";const n=8;export function SlashPopup({matches:n,selected:t}){if(0===n.length)return r(o,{borderStyle:"round",borderColor:"gray",paddingX:1},r(e,{dimColor:!0},"无匹配命令"));const d=n.slice(0,8);return r(o,{flexDirection:"column",borderStyle:"round",borderColor:"gray",paddingX:1},...d.map((n,d)=>{const l=d===t;return r(o,{key:n.name},r(e,l?{color:"cyan",bold:!0}:{dimColor:!0},`${l?"❯ ":" "}${n.name}`),r(e,{dimColor:!0},` ${n.description}`))}))}
@@ -0,0 +1,2 @@
1
+ import type { ReactElement } from "react";
2
+ export declare function Spinner(): ReactElement;
@@ -0,0 +1 @@
1
+ import{createElement as r,useEffect as t,useState as n}from"react";import{Text as e}from"ink";const o=["⠋","⠙","⠹","⠸","⠼","⠴","⠦","⠧","⠇","⠏"],c=80;export function Spinner(){const[a,l]=n(0);return t(()=>{const r=setInterval(()=>{l(r=>(r+1)%o.length)},c);return()=>{clearInterval(r)}},[]),r(e,{color:"cyan"},o[a]??o[0])}
@@ -0,0 +1,95 @@
1
+ import type { SessionEvent, SessionTokenUsage } from "@roll-agent/runtime";
2
+ import type { ThinkingLevel } from "../../../llm/providers.ts";
3
+ export interface ToolRowState {
4
+ readonly toolCallId: string;
5
+ readonly name: string;
6
+ readonly args: string;
7
+ }
8
+ export type HistoryItem = {
9
+ readonly kind: "user";
10
+ readonly id: string;
11
+ readonly text: string;
12
+ } | {
13
+ readonly kind: "assistant";
14
+ readonly id: string;
15
+ readonly text: string;
16
+ } | {
17
+ readonly kind: "tool";
18
+ readonly id: string;
19
+ readonly name: string;
20
+ readonly args: string;
21
+ readonly ok: boolean;
22
+ } | {
23
+ readonly kind: "compaction";
24
+ readonly id: string;
25
+ readonly notice: string;
26
+ } | {
27
+ readonly kind: "notice";
28
+ readonly id: string;
29
+ readonly text: string;
30
+ } | {
31
+ readonly kind: "error";
32
+ readonly id: string;
33
+ readonly message: string;
34
+ };
35
+ export interface LiveState {
36
+ readonly streamingText: string;
37
+ readonly thinking: boolean;
38
+ readonly activeTools: readonly ToolRowState[];
39
+ readonly compacting: boolean;
40
+ readonly producedOutput: boolean;
41
+ }
42
+ export interface StatusState {
43
+ readonly model: string;
44
+ readonly contextWindow: number | undefined;
45
+ readonly turnUsage: SessionTokenUsage | undefined;
46
+ readonly sessionUsage: SessionTokenUsage | undefined;
47
+ readonly contextInputTokens: number | undefined;
48
+ readonly thinkingLevel: ThinkingLevel;
49
+ }
50
+ export type ChatPhase = "idle" | "busy" | "confirm";
51
+ export interface PendingConfirm {
52
+ readonly approvalId: string;
53
+ readonly prompt: string;
54
+ }
55
+ export interface ChatUiState {
56
+ readonly history: readonly HistoryItem[];
57
+ readonly draft: string;
58
+ readonly live: LiveState;
59
+ readonly status: StatusState;
60
+ readonly phase: ChatPhase;
61
+ readonly pendingConfirm: PendingConfirm | undefined;
62
+ }
63
+ export type ChatUiAction = {
64
+ readonly type: "submit-user";
65
+ readonly id: string;
66
+ readonly text: string;
67
+ } | {
68
+ readonly type: "set-draft";
69
+ readonly value: string;
70
+ } | {
71
+ readonly type: "set-thinking";
72
+ readonly level: ThinkingLevel;
73
+ } | {
74
+ readonly type: "commit-history";
75
+ readonly item: HistoryItem;
76
+ } | {
77
+ readonly type: "start-compaction";
78
+ } | {
79
+ readonly type: "session-event";
80
+ readonly id: string;
81
+ readonly event: SessionEvent;
82
+ } | {
83
+ readonly type: "confirm-resolved";
84
+ } | {
85
+ readonly type: "turn-end";
86
+ };
87
+ export interface InitialStateOptions {
88
+ readonly history?: readonly HistoryItem[];
89
+ readonly thinkingLevel?: ThinkingLevel;
90
+ }
91
+ export declare function createInitialState(model: string, contextWindow: number | undefined, options?: InitialStateOptions): ChatUiState;
92
+ export declare function buildConfirmPrompt(event: Extract<SessionEvent, {
93
+ type: "confirmation-required";
94
+ }>): string;
95
+ export declare function chatReducer(state: ChatUiState, action: ChatUiAction): ChatUiState;
@@ -0,0 +1 @@
1
+ import{formatToolInput as t}from"../../utils/tool-format.js";const e={streamingText:"",thinking:!1,activeTools:[],compacting:!1,producedOutput:!1};export function createInitialState(t,i,n){return{history:n?.history??[],draft:"",live:e,status:{model:t,contextWindow:i,turnUsage:void 0,sessionUsage:void 0,contextInputTokens:void 0,thinkingLevel:n?.thinkingLevel??"medium"},phase:"idle",pendingConfirm:void 0}}export function buildConfirmPrompt(t){const e=t.reason?`(${t.reason})`:"";return`执行 ${t.agentName}.${t.toolName}${e}?`}function i(t){const e="auto"===t.reason?"自动压缩":"手动压缩",i=t.truncatedTools?`,精简 ${String(t.truncatedTools)} 个工具结果`:"";return 0!==t.removed||t.truncatedTools?`🗜 ${e}(${t.strategy}):移除 ${String(t.removed)} 条 → 保留 ${String(t.kept)} 条${i}`:`🗜 ${e}:无需压缩`}function n(t,e,i){const n=t.live.activeTools.find(t=>t.toolCallId===i.toolCallId),o=n?.name??`${i.agentName}.${i.toolName}`,s=n?.args??"";return{...t,history:[...t.history,{kind:"tool",id:e,name:o,args:s,ok:!i.isError}],live:{...t.live,activeTools:t.live.activeTools.filter(t=>t.toolCallId!==i.toolCallId)}}}function o(o,s,r){switch(r.type){case"message-start":return{...o,live:{...o.live,thinking:!0}};case"text-delta":return{...o,live:{...o.live,thinking:!1,producedOutput:!0,streamingText:o.live.streamingText+r.delta}};case"tool-call":{const e=o.live.streamingText.length>0?[{kind:"assistant",id:s,text:o.live.streamingText}]:[];return{...o,history:[...o.history,...e],live:{...o.live,thinking:!1,producedOutput:!0,streamingText:"",activeTools:[...o.live.activeTools,{toolCallId:r.toolCallId,name:`${r.agentName}.${r.toolName}`,args:t(r.input)}]}}}case"tool-result":return n(o,s,r);case"confirmation-required":return{...o,phase:"confirm",pendingConfirm:{approvalId:r.approvalId,prompt:buildConfirmPrompt(r)}};case"compaction-start":return{...o,live:{...o.live,compacting:!0}};case"context-compacted":return{...o,live:{...o.live,compacting:!1},history:[...o.history,{kind:"compaction",id:s,notice:i(r)}]};case"message-finish":{const t=[];return o.live.streamingText.length>0?t.push({kind:"assistant",id:s,text:o.live.streamingText}):!o.live.producedOutput&&(r.totalUsage?.outputTokens??0)>0&&t.push({kind:"notice",id:s,text:"模型本轮只返回了 thinking/reasoning,没有生成可见回复"}),r.stoppedAtStepLimit&&t.push({kind:"notice",id:`${s}-step-limit`,text:"已达单轮最大工具步数,任务可能未完成 — 继续追问即可接着做,或调高 runtime.max-steps"}),{...o,history:[...o.history,...t],live:{...e},status:{...o.status,turnUsage:r.totalUsage,sessionUsage:r.sessionUsage,contextInputTokens:r.contextInputTokens}}}case"error":return{...o,history:[...o.history,{kind:"error",id:s,message:r.message}],live:{...e}};default:return o}}export function chatReducer(t,i){switch(i.type){case"submit-user":return{...t,history:[...t.history,{kind:"user",id:i.id,text:i.text}],draft:"",live:{...e},phase:"busy",pendingConfirm:void 0};case"set-draft":return{...t,draft:i.value};case"set-thinking":return{...t,status:{...t.status,thinkingLevel:i.level}};case"commit-history":return{...t,history:[...t.history,i.item],draft:""};case"start-compaction":return{...t,live:{...e},phase:"busy",pendingConfirm:void 0};case"session-event":return o(t,i.id,i.event);case"confirm-resolved":return{...t,phase:"busy",pendingConfirm:void 0};case"turn-end":return{...t,phase:"idle",live:{...e}};default:return t}}
@@ -0,0 +1,5 @@
1
+ import type { ReactElement } from "react";
2
+ import type { StatusState } from "./state.ts";
3
+ export declare function StatusLine({ status }: {
4
+ status: StatusState;
5
+ }): ReactElement;
@@ -0,0 +1 @@
1
+ import{createElement as o}from"react";import{Box as e,Text as n,useStdout as t}from"ink";import{computeUsageParts as s,formatTokens as i}from"../../utils/token-format.js";import{thinkingLabel as r}from"./thinking.js";export function StatusLine({status:k}){const{stdout:d}=t(),u=d.columns??80,c=s(k.turnUsage,k.sessionUsage,k.contextWindow,k.contextInputTokens),m=o(n,"off"===k.thinkingLevel?{key:"think",color:"yellow"}:{key:"think",dimColor:!0},` · ${r(k.thinkingLevel)}`),p=[o(n,{key:"model",color:"magenta"},k.model),m];if(void 0!==c.percentLeft&&void 0!==c.usedTokens&&void 0!==c.contextWindow&&p.push(o(n,{key:"ctx",dimColor:!0},` · ${String(c.percentLeft)}% left (${i(c.usedTokens)}/${i(c.contextWindow)})`)),void 0!==c.inputTokens){const e=void 0!==c.cachedInputTokens?` (+${i(c.cachedInputTokens)} cached)`:"";p.push(o(n,{key:"in",dimColor:!0},` · in ${i(c.inputTokens)}${e}`))}if(void 0!==c.outputTokens){const e=void 0!==c.reasoningTokens?` (+${i(c.reasoningTokens)} reasoning)`:"";p.push(o(n,{key:"out",dimColor:!0},` · out ${i(c.outputTokens)}${e}`))}return void 0!==c.sessionTokens&&p.push(o(n,{key:"session",dimColor:!0},` · session ${i(c.sessionTokens)}`)),o(e,{width:u},o(n,{wrap:"truncate-end"},...p))}
@@ -0,0 +1,12 @@
1
+ import type { ReactElement } from "react";
2
+ export interface TextPromptProps {
3
+ readonly value: string;
4
+ readonly disabled: boolean;
5
+ readonly slashActive: boolean;
6
+ readonly onChange: (value: string) => void;
7
+ readonly onSubmit: (value: string) => void;
8
+ readonly onSlashMove: (direction: 1 | -1) => void;
9
+ readonly onSlashComplete: () => void;
10
+ readonly onSlashRun: () => void;
11
+ }
12
+ export declare function TextPrompt(props: TextPromptProps): ReactElement;
@@ -0,0 +1 @@
1
+ import{createElement as r,useRef as t}from"react";import{Box as e,Text as n,useInput as o,useStdout as i}from"ink";function u(r){return/^\[\?\d+u$/.test(r)||/^\x9b\?\d+u$/.test(r)}export function TextPrompt(l){const{value:c,disabled:s,slashActive:d,onChange:a,onSubmit:f}=l,{stdout:m}=i(),p=m.columns??80,v=t(c);v.current=c;const h=r=>{v.current=r,a(r)};o((r,t)=>{if(t.meta&&("."===r||","===r))return;if(u(r))return;if(t.backspace||t.delete)return void h(v.current.slice(0,-1));if("\n"===r||t.ctrl&&"j"===r||t.return&&(t.shift||t.meta))return void h(`${v.current}\n`);if(t.return||r.includes("\r")){if(d)return void l.onSlashRun();const t=r.split("\r",1)[0]??"";return void f(v.current+t)}if(d){if(t.upArrow)return void l.onSlashMove(-1);if(t.downArrow)return void l.onSlashMove(1);if(t.tab)return void l.onSlashComplete();if(t.escape)return void h("")}t.ctrl||t.escape||t.tab||t.upArrow||t.downArrow||t.leftArrow||t.rightArrow||r.length>0&&h(v.current+r)},{isActive:!s});const b=c.split("\n"),w=r(e,{flexDirection:"column"},...b.map((t,o)=>{const i=o===b.length-1,u=0===o?r(n,{color:"green"},"› "):r(n,null," ");return r(e,{key:String(o)},u,r(n,s?{dimColor:!0}:{},t),i&&!s?r(n,null,"▏"):null)})),g=d?"↑↓ 选择 · Tab 补全 · Enter 执行 · Esc 取消":"Enter 发送 · Shift+Enter/Ctrl+J 换行 · / 命令";return r(e,{flexDirection:"column",width:p},r(e,{borderStyle:"round",borderColor:s?"gray":"cyan",paddingX:1},w),r(n,{dimColor:!0},g))}
@@ -0,0 +1,9 @@
1
+ import type { ReactElement } from "react";
2
+ export interface TextSegment {
3
+ readonly text: string;
4
+ readonly thinking: boolean;
5
+ }
6
+ export declare function parseThinking(text: string): TextSegment[];
7
+ export declare function ThinkingText({ text }: {
8
+ text: string;
9
+ }): ReactElement;
@@ -0,0 +1 @@
1
+ import{createElement as t}from"react";import{Text as n}from"ink";export function parseThinking(t){const n=[],e=/<think>([\s\S]*?)(?:<\/think>|$)/gi;let i=0,r=e.exec(t);for(;null!==r;)r.index>i&&n.push({text:t.slice(i,r.index),thinking:!1}),n.push({text:r[1]??"",thinking:!0}),i=e.lastIndex,r=e.exec(t);return i<t.length&&n.push({text:t.slice(i),thinking:!1}),n.filter(t=>t.text.length>0)}export function ThinkingText({text:e}){const i=parseThinking(e);return 0===i.length?t(n,null,""):t(n,null,...i.map((e,i)=>t(n,{key:String(i),...e.thinking?{dimColor:!0}:{}},e.text)))}
@@ -0,0 +1,3 @@
1
+ import type { ThinkingLevel } from "../../../llm/providers.ts";
2
+ export declare function cycleThinking(level: ThinkingLevel, direction: 1 | -1): ThinkingLevel;
3
+ export declare function thinkingLabel(level: ThinkingLevel): string;
@@ -0,0 +1 @@
1
+ const n=["off","low","medium","high"];export function cycleThinking(t,i){const e=n.indexOf(t),o=Math.min(Math.max(e+i,0),n.length-1);return n[o]??t}export function thinkingLabel(n){return`🧠 ${n}`}
@@ -0,0 +1,4 @@
1
+ import type { ReactElement } from "react";
2
+ export declare function ToolLabel({ name }: {
3
+ name: string;
4
+ }): ReactElement;
@@ -0,0 +1 @@
1
+ import{createElement as o}from"react";import{Text as e}from"ink";export function ToolLabel({name:r}){const n=r.indexOf(".");return n<0?o(e,{color:"cyan"},r):o(e,null,o(e,{dimColor:!0},r.slice(0,n+1)),o(e,{color:"cyan"},r.slice(n+1)))}
@@ -0,0 +1,20 @@
1
+ import type { AgentSession } from "@roll-agent/runtime";
2
+ import { type ChatUiState, type HistoryItem } from "./state.ts";
3
+ import type { ThinkingLevel } from "../../../llm/providers.ts";
4
+ export interface UseSessionOptions {
5
+ readonly model: string;
6
+ readonly contextWindow: number | undefined;
7
+ readonly initialHistory?: readonly HistoryItem[];
8
+ readonly initialThinkingLevel?: ThinkingLevel;
9
+ readonly onThinkingChange?: (level: ThinkingLevel) => void;
10
+ }
11
+ export interface UseSessionResult {
12
+ readonly state: ChatUiState;
13
+ readonly submit: (text: string) => void;
14
+ readonly compact: () => void;
15
+ readonly resolveConfirm: (approved: boolean) => void;
16
+ readonly setDraft: (value: string) => void;
17
+ readonly setThinking: (level: ThinkingLevel) => void;
18
+ readonly commitHistory: (item: HistoryItem) => void;
19
+ }
20
+ export declare function useSession(session: AgentSession, options: UseSessionOptions): UseSessionResult;
@@ -0,0 +1 @@
1
+ import{randomUUID as e}from"node:crypto";import{useCallback as t,useReducer as n,useRef as i}from"react";import{chatReducer as r,createInitialState as o}from"./state.js";import{log as s}from"../../utils/output.js";const a=32;function c(e){return e instanceof Error?e.message:String(e)}export function useSession(l,p){const[u,m]=n(r,o(p.model,p.contextWindow,{...p.initialHistory?{history:p.initialHistory}:{},...p.initialThinkingLevel?{thinkingLevel:p.initialThinkingLevel}:{}})),y=p.onThinkingChange,d=i(null),v=i(!1),f=t(async t=>{let n,i="";const r=()=>{if(void 0!==n&&(clearTimeout(n),n=void 0),i.length>0){const t=i;i="",m({type:"session-event",id:e(),event:{type:"text-delta",delta:t}})}};try{for await(const o of t)if("debug"!==o.type)if("text-delta"!==o.type){if(r(),"confirmation-required"===o.type){const t=new Promise(e=>{d.current=e});m({type:"session-event",id:e(),event:o});const n=await t;d.current=null,n?l.approve(o.approvalId):l.reject(o.approvalId,"用户取消"),m({type:"confirm-resolved"});continue}m({type:"session-event",id:e(),event:o})}else i+=o.delta,void 0===n&&(n=setTimeout(r,a));else s.debug(`chat.${o.stage} ${o.message}`)}catch(t){r(),m({type:"session-event",id:e(),event:{type:"error",stage:"execute",message:c(t)}})}finally{r(),m({type:"turn-end"}),v.current=!1}},[l]);return{state:u,submit:t(t=>{v.current||(v.current=!0,m({type:"submit-user",id:e(),text:t}),f(l.send(t)).catch(()=>{}))},[f,l]),compact:t(()=>{v.current||(v.current=!0,m({type:"start-compaction"}),f(l.compact("manual")).catch(()=>{}))},[f,l]),resolveConfirm:t(e=>{d.current?.(e)},[]),setDraft:t(e=>{m({type:"set-draft",value:e})},[]),setThinking:t(e=>{m({type:"set-thinking",level:e}),y?.(e)},[y]),commitHistory:t(e=>{m({type:"commit-history",item:e})},[])}}
@@ -0,0 +1 @@
1
+ export declare function titleFromMessage(message: string): string;
@@ -0,0 +1 @@
1
+ export function titleFromMessage(e){const t=e.trim().replace(/\s+/g," ");return t.length>40?`${t.slice(0,39)}…`:t}
@@ -15,13 +15,13 @@ declare const BrowserStopToolResultSchema: z.ZodObject<{
15
15
  }, "strip", z.ZodTypeAny, {
16
16
  status: "stopped" | "failed" | "not_running" | "not_found";
17
17
  browserInstance: string;
18
- message?: string | undefined;
19
18
  mode?: "managed-cdp" | "remote-cdp" | "existing-session" | undefined;
19
+ message?: string | undefined;
20
20
  }, {
21
21
  status: "stopped" | "failed" | "not_running" | "not_found";
22
22
  browserInstance: string;
23
- message?: string | undefined;
24
23
  mode?: "managed-cdp" | "remote-cdp" | "existing-session" | undefined;
24
+ message?: string | undefined;
25
25
  }>, "many">;
26
26
  }, "strip", z.ZodTypeAny, {
27
27
  stopped: number;
@@ -29,8 +29,8 @@ declare const BrowserStopToolResultSchema: z.ZodObject<{
29
29
  results: {
30
30
  status: "stopped" | "failed" | "not_running" | "not_found";
31
31
  browserInstance: string;
32
- message?: string | undefined;
33
32
  mode?: "managed-cdp" | "remote-cdp" | "existing-session" | undefined;
33
+ message?: string | undefined;
34
34
  }[];
35
35
  }, {
36
36
  stopped: number;
@@ -38,8 +38,8 @@ declare const BrowserStopToolResultSchema: z.ZodObject<{
38
38
  results: {
39
39
  status: "stopped" | "failed" | "not_running" | "not_found";
40
40
  browserInstance: string;
41
- message?: string | undefined;
42
41
  mode?: "managed-cdp" | "remote-cdp" | "existing-session" | undefined;
42
+ message?: string | undefined;
43
43
  }[];
44
44
  }>;
45
45
  type BrowserStopToolResult = z.infer<typeof BrowserStopToolResultSchema>;
@@ -1,6 +1,15 @@
1
1
  import type { AgentSession } from "@roll-agent/runtime";
2
+ import { type ChatConfirm } from "../utils/chat-renderer.ts";
2
3
  import type { ChatCommandResult } from "../../types/chat.ts";
4
+ type RuntimeModule = typeof import("@roll-agent/runtime");
5
+ type ThreadStoreInstance = InstanceType<RuntimeModule["ThreadStore"]>;
6
+ interface ReplIo {
7
+ readonly input: NodeJS.ReadableStream;
8
+ readonly output: NodeJS.WritableStream;
9
+ readonly confirm?: ChatConfirm;
10
+ }
3
11
  export declare function runJsonTurn(session: AgentSession, message: string): Promise<ChatCommandResult>;
12
+ export declare function runRepl(session: AgentSession, store: ThreadStoreInstance, isNewSession: boolean, io?: ReplIo): Promise<void>;
4
13
  declare const _default: import("citty").CommandDef<{
5
14
  message: {
6
15
  type: "positional";
@@ -1 +1 @@
1
- import{defineCommand as e}from"citty";import{loadConfig as s}from"../../config/loader.js";import{createProviderModel as t}from"../../llm/providers.js";import{createInterface as o}from"node:readline/promises";import r from"chalk";import i from"cli-table3";import{log as n}from"../utils/output.js";import{ChatRenderer as a,clackConfirm as l}from"../utils/chat-renderer.js";function c(e,s){return new e.ConfigurableToolPolicy({defaultMode:s.runtime.approval.default,overrides:s.runtime.approval.overrides})}function d(e){const s=e.trim().replace(/\s+/g," ");return s.length>40?`${s.slice(0,39)}…`:s}function u(e){console.log(JSON.stringify(e,null,2))}function m(e){n.warn(`Agent "${e.agentName}" 启动失败:${e.message}`)}async function p(){return import("@roll-agent/runtime")}async function f(e){const s=e.runtime.provider??e.llm.defaultProvider,o=e.runtime.model??e.llm.defaultModel,r=e.llm.providers[s];if(!r)return n.error(`LLM provider "${s}" 未配置。请检查 roll.config.yaml`),void(process.exitCode=1);const i=await p(),{ConversationEngine:a,ThreadStore:l,RuntimeServer:d,createStdioConnection:u}=i,f=t(s,o,r.apiKey,r.baseUrl),g=new l(e.runtime.threadsDir),y=new a({config:e,model:f,store:g,policy:c(i,e),maxSteps:e.runtime.maxSteps,onAgentBootstrapIssue:m}),h=u(process.stdin,process.stdout),w=new d(y,h);h.onClose(()=>{w.abortAll(),y.dispose().catch(()=>{}).finally(()=>{g.close(),process.exit(0)})}),n.info("roll runtime-server 已启动(stdio JSON-RPC,等待客户端连接)")}async function g(e,s){const{ThreadStore:t}=await p(),o=new t(e.runtime.threadsDir);try{const e=o.listThreads();if(s){const s=e.map(e=>({...e,messageCount:o.countMessages(e.id)}));return void console.log(JSON.stringify(s,null,2))}if(0===e.length)return void console.log('暂无会话。用 `roll chat "<message>"` 开始一个。');const t=new i({head:["Session ID","标题","消息","更新时间"],style:{head:["cyan"]}});for(const s of e)t.push([s.id,s.title??"-",String(o.countMessages(s.id)),s.updatedAt]);console.log(t.toString())}finally{o.close()}}export async function runJsonTurn(e,s){const t=[],o=[],r=[];let i,n,a="";for await(const l of e.send(s))switch(l.type){case"text-delta":a+=l.delta;break;case"tool-call":t.push({summary:`${l.agentName}.${l.toolName}`,agentName:l.agentName,toolName:l.toolName});break;case"confirmation-required":r.push({summary:`${l.agentName}.${l.toolName}`,agentName:l.agentName,toolName:l.toolName}),e.reject(l.approvalId,"json 模式不支持交互确认");break;case"step-finish":o.push({finishReason:l.finishReason,...l.usage?{usage:l.usage}:{}});break;case"message-finish":n=l.totalUsage;break;case"error":i=l.message}return void 0!==i?{status:"failed",stage:"execute",message:i,sessionId:e.id}:r.length>0?{status:"needs_confirmation",sessionId:e.id,message:"存在需要确认的工具调用,请在交互模式下执行或显式批准",pendingActions:r}:{status:"completed",sessionId:e.id,output:a,steps:t,...o.length>0?{stepUsages:o}:{},...n?{totalUsage:n}:{}}}async function y(e,s,t){const i=o({input:process.stdin,output:process.stdout});i.on("SIGINT",()=>i.close());const l=new a(async e=>{const s=(await i.question(`${e} (y/N) `)).trim().toLowerCase();return"y"===s||"yes"===s});n.info("进入多轮对话(输入 exit / quit 或 Ctrl-C 退出)");let c=!t,u=!1;try{for(;;){let t;try{t=(await i.question(r.green("› "))).trim()}catch{break}if(0!==t.length){if("exit"===t||"quit"===t)break;c||(s.updateTitle(e.id,d(t)),c=!0),u=!0;for await(const s of e.send(t))await l.handle(s,e)}}}finally{i.close(),t&&!u&&0===s.countMessages(e.id)&&s.deleteThread(e.id)}}export default e({meta:{description:"会话式 AI 助手(多轮对话 + 自动工具调用)"},args:{message:{type:"positional",description:"起始消息",required:!1},session:{type:"string",description:"继续已有会话的 session ID"},last:{type:"boolean",description:"继续最近一个会话",default:!1},list:{type:"boolean",description:"列出已有会话",default:!1},json:{type:"boolean",description:"JSON 格式输出",default:!1},server:{type:"boolean",description:"以 JSON-RPC daemon 模式运行(stdio,供 GUI/前端接入)",default:!1}},async run({args:e}){const{config:o}=s();if(e.server)return void await f(o);if(e.list)return void await g(o,e.json);const r=o.runtime.provider??o.llm.defaultProvider,i=o.runtime.model??o.llm.defaultModel,h=o.llm.providers[r];if(!h)return n.error(`LLM provider "${r}" 未配置。请检查 roll.config.yaml`),void(process.exitCode=1);if(e.json&&!e.message)return n.error('--json 模式需要消息:roll chat "<message>" --json'),void(process.exitCode=1);const w=await p(),{ConversationEngine:v,ThreadStore:S}=w,N=t(r,i,h.apiKey,h.baseUrl),x=new S(o.runtime.threadsDir),C=new v({config:o,model:N,store:x,policy:c(w,o),maxSteps:o.runtime.maxSteps,onAgentBootstrapIssue:m});try{let s;if(e.session)s=await C.resumeSession(e.session);else if(e.last){const e=x.listThreads()[0];if(!e)return n.error('暂无可继续的会话,先用 `roll chat "<message>"` 开始一个'),void(process.exitCode=1);s=await C.resumeSession(e.id)}else s=await C.createSession(e.message?{title:d(e.message)}:{});if(e.json&&e.message){const t=await runJsonTurn(s,e.message);return u(t),void("completed"!==t.status&&(process.exitCode=1))}if(n.info(`会话 ${s.id}`),e.message){const t=new a(l);for await(const o of s.send(e.message))await t.handle(o,s)}else await y(s,x,!e.session&&!e.last)}catch(e){n.error(e instanceof Error?e.message:String(e)),process.exitCode=1}finally{await C.dispose(),x.close()}}});
1
+ import{defineCommand as e}from"citty";import{loadConfig as t}from"../../config/loader.js";import{resolveLLMCall as o,thinkingProviderOptions as s}from"../../llm/providers.js";import{createInterface as n}from"node:readline/promises";import i from"chalk";import r from"cli-table3";import{isDebugLogEnabled as a,log as l}from"../utils/output.js";import{ChatRenderer as c,clackConfirm as d}from"../utils/chat-renderer.js";import{titleFromMessage as u}from"../chat/title.js";const p=import.meta.url.endsWith(".ts")?"ts":"js";function m(e,t){return new e.ConfigurableToolPolicy({defaultMode:t.runtime.approval.default,overrides:t.runtime.approval.overrides})}function g(e){console.log(JSON.stringify(e,null,2))}function f(e){l.warn(`Agent "${e.agentName}" 启动失败:${e.message}`)}async function h(e,t,o){l.debug(`chat.repl input waiting · ${o}`),e.resume();try{const s=await e.question(t);return l.debug(`chat.repl input received · ${o} · chars=${String(s.length)}`),s}catch(e){const t=e instanceof Error?e.message:String(e);return void l.debug(`chat.repl input closed · ${o} · ${t}`)}finally{e.pause()}}async function v(){return import("@roll-agent/runtime")}async function w(e){const t=e.runtime.provider??e.llm.defaultProvider,s=e.runtime.model??e.llm.defaultModel,n=e.llm.providers[t];if(!n)return l.error(`LLM provider "${t}" 未配置。请检查 roll.config.yaml`),void(process.exitCode=1);const i=await v(),{ConversationEngine:r,ThreadStore:c,RuntimeServer:d,createStdioConnection:u}=i,{model:p,providerOptions:g}=o(t,s,n.apiKey,"chat",n.baseUrl,e.runtime.thinkingLevel),h=new c(e.runtime.threadsDir),w=new r({config:e,model:p,store:h,policy:m(i,e),maxSteps:e.runtime.maxSteps,...g?{providerOptions:g}:{},debugEvents:a(),onAgentBootstrapIssue:f}),y=u(process.stdin,process.stdout),b=new d(w,y);y.onClose(()=>{b.abortAll(),w.dispose().catch(()=>{}).finally(()=>{h.close(),process.exit(0)})}),l.info("roll runtime-server 已启动(stdio JSON-RPC,等待客户端连接)")}async function y(e,t){const{ThreadStore:o}=await v(),s=new o(e.runtime.threadsDir);try{const e=s.listThreads();if(t){const t=e.map(e=>({...e,messageCount:s.countMessages(e.id)}));return void console.log(JSON.stringify(t,null,2))}if(0===e.length)return void console.log('暂无会话。用 `roll chat "<message>"` 开始一个。');const o=new r({head:["Session ID","标题","消息","更新时间"],style:{head:["cyan"]}});for(const t of e)o.push([t.id,t.title??"-",String(s.countMessages(t.id)),t.updatedAt]);console.log(o.toString())}finally{s.close()}}export async function runJsonTurn(e,t){const o=[],s=[],n=[],i=[];let r,a,l,c,d="";for await(const u of e.send(t))switch(u.type){case"text-delta":d+=u.delta;break;case"tool-call":o.push({summary:`${u.agentName}.${u.toolName}`,agentName:u.agentName,toolName:u.toolName});break;case"confirmation-required":i.push({summary:`${u.agentName}.${u.toolName}`,agentName:u.agentName,toolName:u.toolName}),e.reject(u.approvalId,"json 模式不支持交互确认");break;case"step-finish":s.push({finishReason:u.finishReason,...u.usage?{usage:u.usage}:{}});break;case"message-finish":a=u.totalUsage,l=u.sessionUsage,c=u.contextInputTokens;break;case"context-compacted":n.push({reason:u.reason,strategy:u.strategy,removed:u.removed,kept:u.kept,...void 0!==u.truncatedTools?{truncatedTools:u.truncatedTools}:{},...void 0!==u.beforeInputTokens?{beforeInputTokens:u.beforeInputTokens}:{}});break;case"error":r=u.message}const u=e.getContextWindow();return void 0!==r?{status:"failed",stage:"execute",message:r,sessionId:e.id}:i.length>0?{status:"needs_confirmation",sessionId:e.id,message:"存在需要确认的工具调用,请在交互模式下执行或显式批准",pendingActions:i}:{status:"completed",sessionId:e.id,output:d,steps:o,...s.length>0?{stepUsages:s}:{},...a?{totalUsage:a}:{},...l?{sessionUsage:l}:{},...void 0!==u?{contextWindow:u}:{},...void 0!==c?{contextInputTokens:c}:{},...n.length>0?{compactions:n}:{}}}export async function runRepl(e,t,o,s={input:process.stdin,output:process.stdout}){const r=n({input:s.input,output:s.output});r.on("SIGINT",()=>r.close());const a=s.confirm??(async e=>{l.debug("chat.repl input waiting · confirm"),r.pause();const t=await d(e);return l.debug(`chat.repl input received · confirm · approved=${String(t)}`),t}),p=new c(a,e.getContextWindow());l.info("进入多轮对话(输入 exit / quit 或 Ctrl-C 退出,/compact 手动压缩上下文)");let m=!o,g=!1;try{for(;;){const o=await h(r,i.green("› "),"prompt");if(void 0===o)break;const s=o.trim();if(0!==s.length){if("exit"===s||"quit"===s)break;if("/compact"!==s){m||(t.updateTitle(e.id,u(s)),m=!0),g=!0,l.debug(`chat.repl send start · chars=${String(s.length)}`);for await(const t of e.send(s))await p.handle(t,e);l.debug("chat.repl send completed")}else{l.debug("chat.repl manual compact requested");for await(const t of e.compact("manual"))await p.handle(t,e);l.debug("chat.repl manual compact completed")}}}}finally{r.close(),o&&!g&&0===t.countMessages(e.id)&&t.deleteThread(e.id)}}export default e({meta:{description:"会话式 AI 助手(多轮对话 + 自动工具调用)"},args:{message:{type:"positional",description:"起始消息",required:!1},session:{type:"string",description:"继续已有会话的 session ID"},last:{type:"boolean",description:"继续最近一个会话",default:!1},list:{type:"boolean",description:"列出已有会话",default:!1},json:{type:"boolean",description:"JSON 格式输出",default:!1},server:{type:"boolean",description:"以 JSON-RPC daemon 模式运行(stdio,供 GUI/前端接入)",default:!1}},async run({args:e}){const{config:n}=t();if(e.server)return void await w(n);if(e.list)return void await y(n,e.json);const i=n.runtime.provider??n.llm.defaultProvider,r=n.runtime.model??n.llm.defaultModel,h=n.llm.providers[i];if(!h)return l.error(`LLM provider "${i}" 未配置。请检查 roll.config.yaml`),void(process.exitCode=1);if(e.json&&!e.message)return l.error('--json 模式需要消息:roll chat "<message>" --json'),void(process.exitCode=1);const b=await v(),{ConversationEngine:x,ThreadStore:S}=b,{model:T,providerOptions:k}=o(i,r,h.apiKey,"chat",h.baseUrl,n.runtime.thinkingLevel),C=new S(n.runtime.threadsDir),I=new x({config:n,model:T,store:C,policy:m(b,n),maxSteps:n.runtime.maxSteps,...k?{providerOptions:k}:{},debugEvents:a(),onAgentBootstrapIssue:f});try{let t;if(e.session)t=await I.resumeSession(e.session);else if(e.last){const e=C.listThreads()[0];if(!e)return l.error('暂无可继续的会话,先用 `roll chat "<message>"` 开始一个'),void(process.exitCode=1);t=await I.resumeSession(e.id)}else t=await I.createSession(e.message?{title:u(e.message)}:{});if(e.json&&e.message){const o=await runJsonTurn(t,e.message);return g(o),void("completed"!==o.status&&(process.exitCode=1))}if(l.info(`会话 ${t.id}`),n.runtime.compaction.enabled&&void 0===t.getContextWindow()&&l.warn(`未知模型 "${r}" 的 context window,阈值自动压缩不可用。可在 roll.config.yaml 设置 runtime.context-window`),e.message){const o=new c(d,t.getContextWindow());for await(const s of t.send(e.message))await o.handle(s,t)}else{const o=!e.session&&!e.last,a=Boolean(process.stdout.isTTY&&process.stdin.isTTY&&"function"==typeof process.stdin.setRawMode);let c=!1;if(a)try{const e=new URL(`../chat/ink/run-ink-repl.${p}`,import.meta.url).href,{runInkRepl:a}=await import(e);await a(t,C,o,{model:r,initialThinkingLevel:n.runtime.thinkingLevel,onThinkingChange:e=>t.setProviderOptions(s(i,r,e))}),c=!0}catch(e){l.warn(`Ink TUI 不可用,回退到基础多轮模式:${e instanceof Error?e.message:String(e)}`)}c||await runRepl(t,C,o)}}catch(e){l.error(e instanceof Error?e.message:String(e)),process.exitCode=1}finally{await I.dispose(),C.close()}}});
@@ -92,8 +92,8 @@ export declare const CONFIG_GUIDANCE_ENTRIES: readonly [{
92
92
  readonly path: "runtime.approval.default";
93
93
  readonly title: "Chat 工具确认默认策略";
94
94
  readonly purpose: "`roll chat` 调用工具时的默认确认策略。`guarded` 使用内置读写启发式,`auto` 默认放行非破坏性工具,`deny` 默认拒绝。";
95
- readonly defaultBehavior: "默认值为 `guarded`。此配置不影响 `roll ask`。" | "默认值为 `auto`。此配置不影响 `roll ask`。" | "默认值为 `deny`。此配置不影响 `roll ask`。";
96
- readonly example: "runtime:\n approval:\n default: guarded" | "runtime:\n approval:\n default: auto" | "runtime:\n approval:\n default: deny";
95
+ readonly defaultBehavior: "默认值为 `auto`。此配置不影响 `roll ask`。" | "默认值为 `guarded`。此配置不影响 `roll ask`。" | "默认值为 `deny`。此配置不影响 `roll ask`。";
96
+ readonly example: "runtime:\n approval:\n default: auto" | "runtime:\n approval:\n default: guarded" | "runtime:\n approval:\n default: deny";
97
97
  }, {
98
98
  readonly path: "runtime.approval.overrides";
99
99
  readonly title: "Chat 工具确认精确覆盖";
@@ -7,9 +7,15 @@ export type ChatConfirm = (message: string) => Promise<boolean>;
7
7
  export declare const clackConfirm: ChatConfirm;
8
8
  export declare class ChatRenderer {
9
9
  private readonly confirm;
10
+ private readonly contextWindow;
10
11
  private readonly spinners;
12
+ private compactionSpinner;
13
+ private messageSpinner;
11
14
  private streaming;
12
- constructor(confirm: ChatConfirm);
15
+ constructor(confirm: ChatConfirm, contextWindow?: number);
13
16
  handle(event: SessionEvent, approver: ChatApprover): Promise<void>;
17
+ private stopCompactionSpinner;
18
+ private startMessageSpinner;
19
+ private stopMessageSpinner;
14
20
  private flushLine;
15
21
  }
@@ -1 +1 @@
1
- import t from"chalk";import{confirm as s,isCancel as e}from"@clack/prompts";import{createSpinner as o,log as n,redactToolArgsForLog as r}from"./output.js";export const clackConfirm=async t=>{const o=await s({message:t});return!e(o)&&!0===o};function a(t){const s=JSON.stringify(r(t));return s.length>80?`${s.slice(0,79)}…`:s}function i(t){const s=[];return void 0!==t.inputTokens&&s.push(`in ${String(t.inputTokens)}`),void 0!==t.outputTokens&&s.push(`out ${String(t.outputTokens)}`),void 0!==t.totalTokens&&s.push(`total ${String(t.totalTokens)}`),s.join(" · ")}export class ChatRenderer{confirm;spinners=new Map;streaming=!1;constructor(t){this.confirm=t}async handle(s,e){switch(s.type){case"text-delta":process.stdout.write(s.delta),this.streaming=!0;break;case"tool-call":{this.flushLine();const e=o(`${t.cyan(`${s.agentName}.${s.toolName}`)} ${t.gray(a(s.input))}`);e.start(),this.spinners.set(s.toolCallId,e);break}case"tool-result":{const t=this.spinners.get(s.toolCallId);t&&(s.isError?t.fail():t.succeed(),this.spinners.delete(s.toolCallId));break}case"confirmation-required":{this.flushLine();const t=s.reason?`(${s.reason})`:"";await this.confirm(`执行 ${s.agentName}.${s.toolName}${t}?`)?e.approve(s.approvalId):e.reject(s.approvalId,"用户取消");break}case"error":this.flushLine(),n.error(s.message);break;case"message-finish":this.flushLine(),s.totalUsage&&n.debug(`tokens: ${i(s.totalUsage)}`)}}flushLine(){this.streaming&&(process.stdout.write("\n"),this.streaming=!1)}}
1
+ import s from"chalk";import{isCancel as e,select as t}from"@clack/prompts";import{createSpinner as n,log as i}from"./output.js";import{computeUsageParts as a,formatUsageLine as o}from"./token-format.js";import{formatToolInput as r}from"./tool-format.js";export const clackConfirm=async s=>{const n=await t({message:s,options:[{value:"yes",label:"Yes"},{value:"no",label:"No"}],initialValue:"no"});return!e(n)&&"yes"===n};function p(s){const e=[`chat.${s.stage}`,s.message];return void 0!==s.elapsedMs&&e.push(`${String(s.elapsedMs)}ms`),void 0!==s.data&&e.push(JSON.stringify(s.data)),e.join(" · ")}export class ChatRenderer{confirm;contextWindow;spinners=new Map;compactionSpinner;messageSpinner;streaming=!1;constructor(s,e){this.confirm=s,this.contextWindow=e}async handle(e,t){switch(e.type){case"debug":i.debug(p(e));break;case"message-start":this.startMessageSpinner();break;case"text-delta":this.stopMessageSpinner(),process.stdout.write(e.delta),this.streaming=!0;break;case"tool-call":{this.stopMessageSpinner(),this.flushLine();const t=n(`${s.cyan(`${e.agentName}.${e.toolName}`)} ${s.gray(r(e.input))}`);t.start(),this.spinners.set(e.toolCallId,t);break}case"tool-result":{const s=this.spinners.get(e.toolCallId);s&&(e.isError?s.fail():s.succeed(),this.spinners.delete(e.toolCallId));break}case"confirmation-required":{this.stopMessageSpinner(),this.flushLine();const s=e.reason?`(${e.reason})`:"";await this.confirm(`执行 ${e.agentName}.${e.toolName}${s}?`)?t.approve(e.approvalId):t.reject(e.approvalId,"用户取消");break}case"compaction-start":this.stopMessageSpinner(),this.flushLine(),this.compactionSpinner=n(s.gray("压缩上下文中…")),this.compactionSpinner.start();break;case"context-compacted":{this.stopCompactionSpinner(),this.flushLine();const t="auto"===e.reason?"自动压缩":"手动压缩",n=e.truncatedTools?`,精简 ${String(e.truncatedTools)} 个工具结果`:"",i=0!==e.removed||e.truncatedTools?`🗜 ${t}(${e.strategy}):移除 ${String(e.removed)} 条 → 保留 ${String(e.kept)} 条${n}`:`🗜 ${t}:无需压缩`;process.stderr.write(`${s.gray(i)}\n`);break}case"error":this.stopMessageSpinner(),this.stopCompactionSpinner(),this.flushLine(),i.error(e.message);break;case"message-finish":{this.stopMessageSpinner(),this.flushLine(),0===e.text.length&&(e.totalUsage?.outputTokens??0)>0&&i.warn("模型本轮只返回了 thinking/reasoning,没有生成可见回复"),e.stoppedAtStepLimit&&i.warn("已达单轮最大工具步数,任务可能未完成 — 继续追问即可接着做,或调高 runtime.max-steps");const t=o(a(e.totalUsage,e.sessionUsage,this.contextWindow,e.contextInputTokens));t&&process.stderr.write(`${s.gray(t)}\n`);break}}}stopCompactionSpinner(){this.compactionSpinner&&(this.compactionSpinner.stop(),this.compactionSpinner=void 0)}startMessageSpinner(){this.stopMessageSpinner(),this.messageSpinner=n(s.gray("思考中…")),this.messageSpinner.start()}stopMessageSpinner(){this.messageSpinner&&(this.messageSpinner.stop(),this.messageSpinner=void 0)}flushLine(){this.streaming&&(process.stdout.write("\n"),this.streaming=!1)}}
@@ -10,6 +10,7 @@ declare const LOG_LEVELS: {
10
10
  export type LogLevel = keyof typeof LOG_LEVELS;
11
11
  /** 设置全局日志级别 */
12
12
  export declare function setLogLevel(level: LogLevel): void;
13
+ export declare function isDebugLogEnabled(): boolean;
13
14
  export declare function resolveLogLevelFromArgv(argv: readonly string[]): LogLevel;
14
15
  export declare function redactToolArgsForLog(value: unknown): unknown;
15
16
  /** 结构化日志输出(写入 stderr,不干扰 stdout 数据流) */
@@ -1 +1 @@
1
- import r from"chalk";import e from"ora";const o={debug:0,info:1,warn:2,error:3,silent:4},n=new Set(["--verbose","-v"]),t=/signed[-_]?envelope|token|secret|password|cookie|authorization|api[-_]?key/i;let i="info";export function setLogLevel(r){i=r}export function resolveLogLevelFromArgv(r){return r.some(r=>n.has(r))?"debug":"info"}export function redactToolArgsForLog(r){return c(r)}function c(r,e){if(e&&t.test(e))return s(r);if(Array.isArray(r))return r.map(r=>c(r));if(f(r)){const e={};for(const[o,n]of Object.entries(r))e[o]=c(n,o);return e}return r}function s(r){if("string"==typeof r)return`[redacted,len=${r.length}]`;const e=u(r);return void 0===e?"[redacted]":`[redacted,len=${e.length}]`}function u(r){try{return JSON.stringify(r)}catch{return}}function f(r){return"object"==typeof r&&null!==r&&!Array.isArray(r)}function a(r){return o[r]>=o[i]}export const log={debug(e){a("debug")&&console.error(r.gray(`[debug] ${e}`))},info(e){a("info")&&console.error(r.blue("→")+` ${e}`)},success(e){a("info")&&console.error(r.green("✓")+` ${e}`)},warn(e){a("warn")&&console.error(r.yellow("⚠")+` ${e}`)},error(e){a("error")&&console.error(r.red("✗")+` ${e}`)}};export function createSpinner(r){return e({text:r,stream:process.stderr})}
1
+ import r from"chalk";import e from"ora";const o={debug:0,info:1,warn:2,error:3,silent:4},n=new Set(["--verbose","-v"]),t=/signed[-_]?envelope|token|secret|password|cookie|authorization|api[-_]?key/i;let i="info";export function setLogLevel(r){i=r}export function isDebugLogEnabled(){return a("debug")}export function resolveLogLevelFromArgv(r){return r.some(r=>n.has(r))?"debug":"info"}export function redactToolArgsForLog(r){return c(r)}function c(r,e){if(e&&t.test(e))return s(r);if(Array.isArray(r))return r.map(r=>c(r));if(f(r)){const e={};for(const[o,n]of Object.entries(r))e[o]=c(n,o);return e}return r}function s(r){if("string"==typeof r)return`[redacted,len=${r.length}]`;const e=u(r);return void 0===e?"[redacted]":`[redacted,len=${e.length}]`}function u(r){try{return JSON.stringify(r)}catch{return}}function f(r){return"object"==typeof r&&null!==r&&!Array.isArray(r)}function a(r){return o[r]>=o[i]}export const log={debug(e){a("debug")&&console.error(r.gray(`[debug] ${e}`))},info(e){a("info")&&console.error(r.blue("→")+` ${e}`)},success(e){a("info")&&console.error(r.green("✓")+` ${e}`)},warn(e){a("warn")&&console.error(r.yellow("⚠")+` ${e}`)},error(e){a("error")&&console.error(r.red("✗")+` ${e}`)}};export function createSpinner(r){return e({text:r,stream:process.stderr})}
@@ -76,4 +76,9 @@ export declare function formatPackageManagerCommand(spec: PackageManagerRunSpec)
76
76
  export declare function isLikelyNetworkError(error: unknown): boolean;
77
77
  /** 网络/超时类错误才值得对整条命令重试。 */
78
78
  export declare function isRetryablePackageManagerError(error: unknown): boolean;
79
+ /**
80
+ * npm 新 package / 新 scoped package 发布后,registry edge 有时会短暂返回 package
81
+ * metadata 404。只对 @roll-agent scope 放行重试,避免把普通包名写错也当成网络问题。
82
+ */
83
+ export declare function isLikelyRollAgentRegistryPropagationError(error: unknown): boolean;
79
84
  export declare function formatPackageManagerError(spec: PackageManagerRunSpec, error: unknown, platform?: NodeJS.Platform): string;
@@ -1 +1 @@
1
- import{execFile as e}from"node:child_process";import{existsSync as r,readFileSync as n}from"node:fs";import{resolve as t}from"node:path";import{promisify as o}from"node:util";const a=o(e),i=67108864;export const PACKAGE_MANAGER_COMMANDS=["bun","npm","pnpm","yarn"];export function detectInstallCommand(e){const n=t(e,"package.json");if(r(n)){const e=g(n);if(e)return{command:e,args:["install"]}}const o=[["pnpm-lock.yaml","pnpm"],["package-lock.json","npm"],["npm-shrinkwrap.json","npm"],["yarn.lock","yarn"],["bun.lock","bun"],["bun.lockb","bun"]];for(const[n,a]of o)if(r(t(e,n)))return{command:a,args:["install"]}}export function createInstallCommand(e="pnpm"){return{command:e,args:["install"]}}export async function runPackageManager(e,r={}){const{platform:n,...t}=r,o=createPackageManagerExecInvocation(e,n??process.platform),s=await a(o.file,[...o.args],{maxBuffer:i,...t,encoding:"utf-8",...o.shell?{shell:o.shell}:{}});return{stdout:s.stdout,stderr:s.stderr}}function s(e){return new Promise(r=>setTimeout(r,e))}export async function runPackageManagerWithRetry(e,r={},n){const t=Math.max(1,n.attempts),o=n.isRetryable??isRetryablePackageManagerError;let a;for(let i=1;i<=t;i+=1)try{return await runPackageManager(e,r)}catch(e){a=e;if(!(i<t)||!o(e))throw e;const r=n.backoffMs??[],c=r[i-1]??r[r.length-1]??2e3;n.onRetry?.({attempt:i,error:e,delayMs:c}),await s(c)}throw a}export function buildNpmRetryPolicy(e){const r=Math.min(Math.max(e,0)+1,3);return{attempts:r,backoffMs:[2e3,5e3].slice(0,Math.max(r-1,0)),isRetryable:isRetryablePackageManagerError}}export function createPackageManagerExecInvocation(e,r=process.platform){return shouldRunPackageManagerViaShell(r)?{file:c(),args:["/d","/s","/c",E(e)],shell:!1}:{file:e.command,args:e.args,shell:!1}}export function shouldRunPackageManagerViaShell(e){return"win32"===e}function c(){const e=process.env.ComSpec??process.env.COMSPEC;return e&&e.trim().length>0?e:"cmd.exe"}export function npmInstallNetworkArgs(e={}){const r=["--no-audit","--no-fund"];return e.registry&&r.push(`--registry=${e.registry}`),void 0!==e.fetchRetries&&r.push(`--fetch-retries=${String(e.fetchRetries)}`),e.preferOffline&&r.push("--prefer-offline"),r}export function npmViewNetworkArgs(e={}){const r=[];return e.registry&&r.push(`--registry=${e.registry}`),void 0!==e.fetchRetries&&r.push(`--fetch-retries=${String(e.fetchRetries)}`),r}export function formatPackageManagerCommand(e){return[e.command,...e.args].map(d).join(" ")}const u="疑似网络或 npm 源不稳定。可在 roll.config.yaml 配置 `install.registry` 指向可用镜像(如 https://registry.npmmirror.com),或检查 VPN / 代理后重试。",l=new Set(["ETIMEDOUT","ECONNRESET","ECONNREFUSED","ENOTFOUND","EAI_AGAIN","ENETUNREACH","EHOSTUNREACH","EPIPE","ERR_SOCKET_TIMEOUT","UND_ERR_CONNECT_TIMEOUT","UND_ERR_HEADERS_TIMEOUT"]),f=[/\bnetwork\b/i,/ETIMEDOUT/i,/ECONNRESET/i,/ECONNREFUSED/i,/ENOTFOUND/i,/EAI_AGAIN/i,/socket hang up/i,/fetch failed/i,/getaddrinfo/i,/tunneling socket could not be established/i,/request to .+ failed/i,/npm\s+(?:error|ERR!)\s+code\s+E429/i,/\bregistry\b.*\b(timeout|timed out|unreachable)\b/i,/\bproxy\b/i];function m(e){if("object"!=typeof e||null===e)return!1;const r="killed"in e&&!0===e.killed,n="signal"in e&&"SIGTERM"===e.signal,t=!("code"in e)||null===e.code||void 0===e.code;return r&&n&&t}export function isLikelyNetworkError(e){const r=M(e);if(r&&l.has(r))return!0;const n=N(e);return f.some(e=>e.test(n))}export function isRetryablePackageManagerError(e){return m(e)||isLikelyNetworkError(e)}export function formatPackageManagerError(e,r,n=process.platform){const t=formatPackageManagerCommand(e);if(y(e.command,r,n))return[`未找到 ${e.command},无法执行 \`${t}\`。`,"win32"===n?`请确认 ${e.command}.cmd 位于 PATH 中;npm 全局目录通常是 C:\\Users\\<you>\\AppData\\Roaming\\npm。`:`请确认已安装 ${e.command},并且当前 shell 的 PATH 可以访问它。`].join("");if(m(r))return`\`${t}\` 执行超时被终止。${u}`;if(isLikelyNetworkError(r)){return`\`${t}\` 执行失败:${r instanceof Error?r.message:String(r)}\n${u}`}return r instanceof Error?r.message:String(r)}function g(e){try{const r=JSON.parse(n(e,"utf-8"));if("string"!=typeof r.packageManager||0===r.packageManager.length)return;const[t]=r.packageManager.split("@",1);if(!t)return;return p(t)?t:void 0}catch{return}}function p(e){return PACKAGE_MANAGER_COMMANDS.some(r=>r===e)}function d(e){return/\s/.test(e)?JSON.stringify(e):e}function E(e){return[e.command,...e.args].map(h).join(" ")}function h(e){if(0===e.length)return'""';if(/["\r\n]/.test(e))throw new Error(`Unsupported Windows shell argument: ${e}`);return/[\s"&|<>^()%!]/.test(e)?`"${k(e)}"`:e}function k(e){return e.replace(/[\^&|<>%!]/g,e=>`^${e}`)}function y(e,r,n){if("ENOENT"===M(r))return!0;if("win32"!==n)return!1;const t=N(r).toLowerCase();return t.includes(`'${e}' is not recognized`)||t.includes(`"${e}" is not recognized`)||t.includes(`${e} is not recognized`)||t.includes(`'${e}' 不是内部或外部命令`)||t.includes(`"${e}" 不是内部或外部命令`)||t.includes(`${e} 不是内部或外部命令`)}function M(e){if("object"==typeof e&&null!==e&&"code"in e)return"string"==typeof e.code?e.code:void 0}function N(e){return e instanceof Error?e.message:String(e)}
1
+ import{execFile as r}from"node:child_process";import{existsSync as e,readFileSync as n}from"node:fs";import{resolve as t}from"node:path";import{promisify as o}from"node:util";const i=o(r),a=67108864;export const PACKAGE_MANAGER_COMMANDS=["bun","npm","pnpm","yarn"];export function detectInstallCommand(r){const n=t(r,"package.json");if(e(n)){const r=m(n);if(r)return{command:r,args:["install"]}}const o=[["pnpm-lock.yaml","pnpm"],["package-lock.json","npm"],["npm-shrinkwrap.json","npm"],["yarn.lock","yarn"],["bun.lock","bun"],["bun.lockb","bun"]];for(const[n,i]of o)if(e(t(r,n)))return{command:i,args:["install"]}}export function createInstallCommand(r="pnpm"){return{command:r,args:["install"]}}export async function runPackageManager(r,e={}){const{platform:n,...t}=e,o=createPackageManagerExecInvocation(r,n??process.platform),s=await i(o.file,[...o.args],{maxBuffer:a,...t,encoding:"utf-8",...o.shell?{shell:o.shell}:{}});return{stdout:s.stdout,stderr:s.stderr}}function s(r){return new Promise(e=>setTimeout(e,r))}export async function runPackageManagerWithRetry(r,e={},n){const t=Math.max(1,n.attempts),o=n.isRetryable??isRetryablePackageManagerError;let i;for(let a=1;a<=t;a+=1)try{return await runPackageManager(r,e)}catch(r){i=r;if(!(a<t)||!o(r))throw r;const e=n.backoffMs??[],c=e[a-1]??e[e.length-1]??2e3;n.onRetry?.({attempt:a,error:r,delayMs:c}),await s(c)}throw i}export function buildNpmRetryPolicy(r){const e=Math.min(Math.max(r,0)+1,3);return{attempts:e,backoffMs:[2e3,5e3].slice(0,Math.max(e-1,0)),isRetryable:isRetryablePackageManagerError}}export function createPackageManagerExecInvocation(r,e=process.platform){return shouldRunPackageManagerViaShell(e)?{file:c(),args:["/d","/s","/c",E(r)],shell:!1}:{file:r.command,args:r.args,shell:!1}}export function shouldRunPackageManagerViaShell(r){return"win32"===r}function c(){const r=process.env.ComSpec??process.env.COMSPEC;return r&&r.trim().length>0?r:"cmd.exe"}export function npmInstallNetworkArgs(r={}){const e=["--no-audit","--no-fund"];return r.registry&&e.push(`--registry=${r.registry}`),void 0!==r.fetchRetries&&e.push(`--fetch-retries=${String(r.fetchRetries)}`),r.preferOffline&&e.push("--prefer-offline"),e}export function npmViewNetworkArgs(r={}){const e=[];return r.registry&&e.push(`--registry=${r.registry}`),void 0!==r.fetchRetries&&e.push(`--fetch-retries=${String(r.fetchRetries)}`),e}export function formatPackageManagerCommand(r){return[r.command,...r.args].map(d).join(" ")}const u="疑似网络或 npm 源不稳定。可在 roll.config.yaml 配置 `install.registry` 指向可用镜像(如 https://registry.npmmirror.com),或检查 VPN / 代理后重试。",l=new Set(["ETIMEDOUT","ECONNRESET","ECONNREFUSED","ENOTFOUND","EAI_AGAIN","ENETUNREACH","EHOSTUNREACH","EPIPE","ERR_SOCKET_TIMEOUT","UND_ERR_CONNECT_TIMEOUT","UND_ERR_HEADERS_TIMEOUT"]),f=[/\bnetwork\b/i,/ETIMEDOUT/i,/ECONNRESET/i,/ECONNREFUSED/i,/ENOTFOUND/i,/EAI_AGAIN/i,/socket hang up/i,/fetch failed/i,/getaddrinfo/i,/tunneling socket could not be established/i,/request to .+ failed/i,/npm\s+(?:error|ERR!)\s+code\s+E429/i,/\bregistry\b.*\b(timeout|timed out|unreachable)\b/i,/\bproxy\b/i];function g(r){if("object"!=typeof r||null===r)return!1;const e="killed"in r&&!0===r.killed,n="signal"in r&&"SIGTERM"===r.signal,t=!("code"in r)||null===r.code||void 0===r.code;return e&&n&&t}export function isLikelyNetworkError(r){const e=R(r);if(e&&l.has(e))return!0;const n=N(r);return f.some(r=>r.test(n))}export function isRetryablePackageManagerError(r){return g(r)||isLikelyNetworkError(r)||isLikelyRollAgentRegistryPropagationError(r)}export function isLikelyRollAgentRegistryPropagationError(r){const e=N(r);return/\bnpm\s+(?:error|ERR!)\s+code\s+E404\b/i.test(e)&&/(?:@roll-agent%2f|@roll-agent\/)[a-z0-9._-]+/i.test(e)&&/(?:Not Found - GET|is not in this registry)/i.test(e)}export function formatPackageManagerError(r,e,n=process.platform){const t=formatPackageManagerCommand(r);if(k(r.command,e,n))return[`未找到 ${r.command},无法执行 \`${t}\`。`,"win32"===n?`请确认 ${r.command}.cmd 位于 PATH 中;npm 全局目录通常是 C:\\Users\\<you>\\AppData\\Roaming\\npm。`:`请确认已安装 ${r.command},并且当前 shell 的 PATH 可以访问它。`].join("");if(g(e))return`\`${t}\` 执行超时被终止。${u}`;if(isLikelyNetworkError(e)){return`\`${t}\` 执行失败:${e instanceof Error?e.message:String(e)}\n${u}`}return e instanceof Error?e.message:String(e)}function m(r){try{const e=JSON.parse(n(r,"utf-8"));if("string"!=typeof e.packageManager||0===e.packageManager.length)return;const[t]=e.packageManager.split("@",1);if(!t)return;return p(t)?t:void 0}catch{return}}function p(r){return PACKAGE_MANAGER_COMMANDS.some(e=>e===r)}function d(r){return/\s/.test(r)?JSON.stringify(r):r}function E(r){return[r.command,...r.args].map(h).join(" ")}function h(r){if(0===r.length)return'""';if(/["\r\n]/.test(r))throw new Error(`Unsupported Windows shell argument: ${r}`);return/[\s"&|<>^()%!]/.test(r)?`"${y(r)}"`:r}function y(r){return r.replace(/[\^&|<>%!]/g,r=>`^${r}`)}function k(r,e,n){if("ENOENT"===R(e))return!0;if("win32"!==n)return!1;const t=N(e).toLowerCase();return t.includes(`'${r}' is not recognized`)||t.includes(`"${r}" is not recognized`)||t.includes(`${r} is not recognized`)||t.includes(`'${r}' 不是内部或外部命令`)||t.includes(`"${r}" 不是内部或外部命令`)||t.includes(`${r} 不是内部或外部命令`)}function R(r){if("object"==typeof r&&null!==r&&"code"in r)return"string"==typeof r.code?r.code:void 0}function N(r){return r instanceof Error?r.message:String(r)}