@roll-agent/core 0.9.0 → 0.10.1

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.
@@ -1 +1 @@
1
- import{defineCommand as e}from"citty";import{resolve as t}from"node:path";import{existsSync as r,mkdirSync as i}from"node:fs";import{execFile as o}from"node:child_process";import{promisify as n}from"node:util";import{inspectAgentEnvRequirements as s}from"../../config/helpers.js";import{loadAgentsConfig as a}from"../../config/loader.js";import{discoverAgent as p}from"../../registry/discovery.js";import{writeRemoteSkillManifest as c}from"../../registry/manifest.js";import{AgentStore as d}from"../../registry/store.js";import{log as m}from"../utils/output.js";import{createInstallCommand as l,detectInstallCommand as g,formatPackageManagerCommand as f,formatPackageManagerError as u,runPackageManager as h}from"../utils/package-manager.js";const y=n(o);function $(e){return e.startsWith("https://")||e.startsWith("http://")||e.startsWith("git@")||e.endsWith(".git")}function v(e){return(e.split("/").pop()??e).replace(/\.git$/,"")}export default e({meta:{description:"注册本地目录、Git 仓库或远程 MCP endpoint"},args:{path:{type:"positional",description:"本地 Agent 目录或 Git 仓库 URL;远程 endpoint 请用 --remote",required:!1},remote:{type:"string",description:"远程 MCP endpoint;需要同时提供 --name 和 --description"},name:{type:"string",description:"远程 Agent 名称(仅 --remote 时使用)"},description:{type:"string",description:"远程 Agent 描述(仅 --remote 时使用)"}},async run({args:e}){const{agentsConfig:o}=a();let n;if(e.remote){if(!e.name||!e.description)return m.error("远程注册需要同时提供 --name 和 --description"),void(process.exitCode=1);n=c({dataDir:o.dataDir,name:e.name,description:e.description,endpoint:e.remote})}else{if(!e.path)return m.error("请提供本地路径、Git URL,或使用 --remote <endpoint> 注册远程 Agent"),void(process.exitCode=1);if($(e.path)){const s=v(e.path),a=t(o.dataDir,"repos",s);if(r(a)){m.info(`仓库目录已存在,拉取最新代码: ${a}`);try{await y("git",["pull"],{cwd:a})}catch(e){return m.error(`git pull 失败: ${e instanceof Error?e.message:String(e)}`),void(process.exitCode=1)}}else{m.info(`克隆 ${e.path}...`);const n=t(o.dataDir,"repos");r(n)||i(n,{recursive:!0});try{await y("git",["clone",e.path,a]),m.success("克隆完成")}catch(e){return m.error(`git clone 失败: ${e instanceof Error?e.message:String(e)}`),void(process.exitCode=1)}}n=a}else if(n=t(e.path),!r(n))return m.error(`路径不存在: ${n}`),void(process.exitCode=1)}m.info("解析 SKILL.md...");const s=p(n);m.debug(`名称: ${s.skill.name}`),m.debug(`描述: ${s.skill.description}`),m.debug(`传输: ${s.transport.type}`);const L=t(n,"package.json"),A="1"===process.env.ROLL_SKIP_INSTALL;if(e.remote)m.info("远程 Agent 使用本地 manifest,无需安装依赖。");else if(r(L)&&!A){const e=g(n)??l("pnpm");m.info(`安装依赖 (${f(e)})...`);try{await h(e,{cwd:n}),m.success("依赖安装完成")}catch(t){return m.error(`依赖安装失败: ${u(e,t)}`),void(process.exitCode=1)}}else r(L)&&A&&m.warn("检测到 ROLL_SKIP_INSTALL=1,跳过依赖安装。");const j=e.remote?{type:"remote-manifest",endpoint:e.remote}:e.path&&$(e.path)?{type:"git",url:e.path}:{type:"local-path",path:n},w=new d(o.dataDir),C={skill:s.skill,transport:s.transport,runtime:s.runtime,installPath:n,registeredAt:(new Date).toISOString(),status:"idle",source:j,...s.skillBody.length>0?{skillBody:s.skillBody}:{}};try{w.add(C),m.success(`Agent "${s.skill.name}" 注册成功`),k(s.skill.name,s.skill.env,o.env)}catch(e){m.error(e instanceof Error?e.message:String(e)),process.exitCode=1}}});function k(e,t,r){const i=s(e,t,r);if(i)return i.missingRequired.length>0?(m.warn(`Agent "${e}" 仍缺少必填环境变量: ${i.missingRequired.map(e=>e.name).join(", ")}`),void m.info(`请在 roll.config.yaml agents.env.${e} 中显式配置这些项。`)):void(i.processEnvOnlyRequired.length>0&&(m.warn(`Agent "${e}" 当前依赖 shell 环境变量: ${i.processEnvOnlyRequired.map(e=>e.name).join(", ")}`),m.info(`建议将这些项写入 roll.config.yaml agents.env.${e}。`)))}
1
+ import{defineCommand as e}from"citty";import{resolve as t}from"node:path";import{existsSync as r,mkdirSync as i}from"node:fs";import{execFile as o}from"node:child_process";import{promisify as n}from"node:util";import{inspectAgentEnvRequirements as s}from"../../config/helpers.js";import{loadAgentsConfig as a}from"../../config/loader.js";import{discoverAgent as p}from"../../registry/discovery.js";import{writeRemoteSkillManifest as c}from"../../registry/manifest.js";import{AgentStore as d}from"../../registry/store.js";import{log as l}from"../utils/output.js";import{createInstallCommand as m,detectInstallCommand as g,formatPackageManagerCommand as f,formatPackageManagerError as u,runPackageManager as h}from"../utils/package-manager.js";const y=n(o);function $(e){return e.startsWith("https://")||e.startsWith("http://")||e.startsWith("git@")||e.endsWith(".git")}function v(e){return(e.split("/").pop()??e).replace(/\.git$/,"")}export default e({meta:{description:"注册本地目录、Git 仓库或远程 MCP endpoint"},args:{path:{type:"positional",description:"本地 Agent 目录或 Git 仓库 URL;远程 endpoint 请用 --remote",required:!1},remote:{type:"string",description:"远程 MCP endpoint;需要同时提供 --name 和 --description"},name:{type:"string",description:"远程 Agent 名称(仅 --remote 时使用)"},description:{type:"string",description:"远程 Agent 描述(仅 --remote 时使用)"}},async run({args:e}){const{agentsConfig:o}=a();let n;if(e.remote){if(!e.name||!e.description)return l.error("远程注册需要同时提供 --name 和 --description"),void(process.exitCode=1);n=c({dataDir:o.dataDir,name:e.name,description:e.description,endpoint:e.remote})}else{if(!e.path)return l.error("请提供本地路径、Git URL,或使用 --remote <endpoint> 注册远程 Agent"),void(process.exitCode=1);if($(e.path)){const s=v(e.path),a=t(o.dataDir,"repos",s);if(r(a)){l.info(`仓库目录已存在,拉取最新代码: ${a}`);try{await y("git",["pull"],{cwd:a})}catch(e){return l.error(`git pull 失败: ${e instanceof Error?e.message:String(e)}`),void(process.exitCode=1)}}else{l.info(`克隆 ${e.path}...`);const n=t(o.dataDir,"repos");r(n)||i(n,{recursive:!0});try{await y("git",["clone",e.path,a]),l.success("克隆完成")}catch(e){return l.error(`git clone 失败: ${e instanceof Error?e.message:String(e)}`),void(process.exitCode=1)}}n=a}else if(n=t(e.path),!r(n))return l.error(`路径不存在: ${n}`),void(process.exitCode=1)}l.info("解析 SKILL.md...");const s=p(n);l.debug(`名称: ${s.skill.name}`),l.debug(`描述: ${s.skill.description}`),l.debug(`传输: ${s.transport.type}`);const L=t(n,"package.json"),A="1"===process.env.ROLL_SKIP_INSTALL;if(e.remote)l.info("远程 Agent 使用本地 manifest,无需安装依赖。");else if(r(L)&&!A){const e=g(n)??m("pnpm");l.info(`安装依赖 (${f(e)})...`);try{await h(e,{cwd:n}),l.success("依赖安装完成")}catch(t){return l.error(`依赖安装失败: ${u(e,t)}`),void(process.exitCode=1)}}else r(L)&&A&&l.warn("检测到 ROLL_SKIP_INSTALL=1,跳过依赖安装。");const j=e.remote?{type:"remote-manifest",endpoint:e.remote}:e.path&&$(e.path)?{type:"git",url:e.path}:{type:"local-path",path:n},w=new d(o.dataDir),C={skill:s.skill,transport:s.transport,runtime:s.runtime,installPath:n,registeredAt:(new Date).toISOString(),status:"idle",source:j,...s.skillBody.length>0?{skillBody:s.skillBody}:{}};try{w.add(C),l.success(`Agent "${s.skill.name}" 注册成功`),k(s.skill.name,s.skill.env,o.env)}catch(e){l.error(e instanceof Error?e.message:String(e)),process.exitCode=1}}});function k(e,t,r){const i=s(e,t,r);if(i)return i.missingRequired.length>0?(l.warn(`Agent "${e}" 仍缺少必填环境变量: ${i.missingRequired.map(e=>e.name).join(", ")}`),l.info(`运行 \`roll config setup agent ${e}\` 交互式配置。`),void l.info(`运行 \`roll config explain agents.env.${e}\` 查看配置说明。`)):void(i.processEnvOnlyRequired.length>0&&(l.warn(`Agent "${e}" 当前依赖 shell 环境变量: ${i.processEnvOnlyRequired.map(e=>e.name).join(", ")}`),l.info(`建议运行 \`roll config setup agent ${e}\` 持久写入 roll.config.yaml。`)))}
@@ -1 +1 @@
1
- import{defineCommand as e}from"citty";import{existsSync as t,mkdirSync as r,statSync as i}from"node:fs";import{resolve as s}from"node:path";import{getAgentEnv as n,inspectAgentEnvRequirements as o}from"../../config/helpers.js";import{loadAgentsConfig as a,loadConfig as l}from"../../config/loader.js";import{discoverAgent as m}from"../../registry/discovery.js";import{startAgent as p,stopAgentGracefully as c,waitForAgentReady as d}from"../../registry/process-manager.js";import{runAgentSetup as g}from"../../registry/runtime-setup.js";import{AgentStore as u}from"../../registry/store.js";import{parsePackageName as f,readInstalledPackageManifest as k,resolveInstalledPackageRoot as y,sanitizeInstallId as h}from"../../registry/source.js";import{log as w}from"../utils/output.js";import{formatPackageManagerError as v,runPackageManager as $}from"../utils/package-manager.js";function j(e){return e.startsWith("git@")||e.startsWith("git+")||e.startsWith("github:")||e.startsWith("gitlab:")||e.startsWith("bitbucket:")||e.endsWith(".git")}export default e({meta:{description:"安装已发布的 Agent npm 包并注册到本地"},args:{package:{type:"positional",description:"npm 包名、版本范围或 .tgz 路径(源码目录请用 roll agent add)",required:!0},"skip-browser-setup":{type:"boolean",description:"跳过 Playwright 浏览器运行时安装/校验",default:!1},start:{type:"boolean",description:"安装后不自动启动 core-managed Agent",default:!0}},async run({args:e}){const{agentsConfig:o}=a(),S=e.package;if(j(S))return w.error(`Git URL 请使用 \`roll agent add ${S}\` 注册,不要使用 \`roll agent install\``),void(process.exitCode=1);const C=s(S);if(t(C)&&i(C).isDirectory())return w.error(`本地源码目录请使用 \`roll agent add ${S}\` 注册,不要使用 \`roll agent install\``),void(process.exitCode=1);const x=f(S),A=s(o.dataDir,"installed",h(x));t(A)||r(A,{recursive:!0}),w.info(`安装 ${S}...`);const D={command:"npm",args:["install","--prefix",A,S]};try{await $(D,{timeout:12e4})}catch(e){return w.error(`安装失败: ${v(D,e)}`),void(process.exitCode=1)}const W=y(A,x);if(!t(W))return w.error(`安装完成但未找到包目录: ${W}`),void(process.exitCode=1);const q=k(W);w.info("解析已安装 Agent 的 SKILL.md...");const B=m(W),R=new u(o.dataDir),E={skill:B.skill,transport:B.transport,runtime:B.runtime,installPath:W,registeredAt:(new Date).toISOString(),status:"idle",source:{type:"installed-package",packageName:q?.name??x,packageSpec:S,installDir:A,...q?.version?{installedVersion:q.version}:{}},...B.skillBody.length>0?{skillBody:B.skillBody}:{}};"core-managed"===E.runtime.ownership&&E.runtime.setup?.playwright&&!e["skip-browser-setup"]&&w.info(`即将安装浏览器运行时 (${E.runtime.setup.playwright.browsers.join(", ")}),这可能需要一些时间...`);const L=await g(E,{skipBrowserSetup:e["skip-browser-setup"]});L.ok?L.skipped?w.info(L.message):w.success(L.message):(w.warn(`Agent setup 失败:${L.message}`),L.retryCommand&&w.info(`重试命令: ${L.retryCommand}`));const O=R.findByName(B.skill.name);try{const t="core-managed"===O?.runtime.ownership&&"online"===O.status;if("installed-package"===O?.source?.type?R.replace(O.skill.name,E):R.add(E),!L.ok)return R.updateStatus(B.skill.name,"error"),void(process.exitCode=1);if("core-managed"===E.runtime.ownership&&e.start){t&&await c(o.dataDir,E.skill.name),R.updateStatus(E.skill.name,"starting");let e=!1;try{p(E,o.dataDir,n(l().config,E.skill.name)),e=!0,await d(E,{startupTimeoutMs:15e3,probeTimeoutMs:2e3}),R.updateStatus(E.skill.name,"online")}catch(t){return e&&await c(o.dataDir,E.skill.name).catch(()=>{}),R.updateStatus(E.skill.name,"error"),w.error(`Agent "${B.skill.name}" 已安装,但自动启动失败:${t instanceof Error?t.message:String(t)}`),void(process.exitCode=1)}}w.success(`Agent "${B.skill.name}" 安装并注册成功`),b(B.skill.name,B.skill.env,o.env)}catch(e){w.error(e instanceof Error?e.message:String(e)),process.exitCode=1}}});function b(e,t,r){const i=o(e,t,r);if(i)return i.missingRequired.length>0?(w.warn(`Agent "${e}" 仍缺少必填环境变量: ${i.missingRequired.map(e=>e.name).join(", ")}`),void w.info(`请在 roll.config.yaml agents.env.${e} 中显式配置这些项。`)):void(i.processEnvOnlyRequired.length>0&&(w.warn(`Agent "${e}" 当前依赖 shell 环境变量: ${i.processEnvOnlyRequired.map(e=>e.name).join(", ")}`),w.info(`建议将这些项写入 roll.config.yaml agents.env.${e}。`)))}
1
+ import{defineCommand as e}from"citty";import{existsSync as t,mkdirSync as r,statSync as i}from"node:fs";import{resolve as s}from"node:path";import{getAgentEnv as n,inspectAgentEnvRequirements as o}from"../../config/helpers.js";import{loadAgentsConfig as a,loadConfig as l,loadInstallConfig as p}from"../../config/loader.js";import{discoverAgent as m}from"../../registry/discovery.js";import{startAgent as c,stopAgentGracefully as g,waitForAgentReady as d}from"../../registry/process-manager.js";import{runAgentSetup as u}from"../../registry/runtime-setup.js";import{AgentStore as f}from"../../registry/store.js";import{parsePackageName as y,readInstalledPackageManifest as k,resolveInstalledPackageRoot as h,sanitizeInstallId as w}from"../../registry/source.js";import{log as $}from"../utils/output.js";import{buildNpmRetryPolicy as v,formatPackageManagerError as j,npmInstallNetworkArgs as C,runPackageManagerWithRetry as S}from"../utils/package-manager.js";function b(e){return C({...e.registry?{registry:e.registry}:{},fetchRetries:e.fetchRetries,preferOffline:e.preferOffline})}function x(e){return e.startsWith("git@")||e.startsWith("git+")||e.startsWith("github:")||e.startsWith("gitlab:")||e.startsWith("bitbucket:")||e.endsWith(".git")}export default e({meta:{description:"安装已发布的 Agent npm 包并注册到本地"},args:{package:{type:"positional",description:"npm 包名、版本范围或 .tgz 路径(源码目录请用 roll agent add)",required:!0},"skip-browser-setup":{type:"boolean",description:"跳过 Playwright 浏览器运行时安装/校验",default:!1},start:{type:"boolean",description:"安装后不自动启动 core-managed Agent",default:!0}},async run({args:e}){const{agentsConfig:o}=a();let C;try{C=p().installConfig}catch(e){return $.error(`install 配置无效,已停止安装:${e instanceof Error?e.message:String(e)}`),void(process.exitCode=1)}const R=e.package;if(x(R))return $.error(`Git URL 请使用 \`roll agent add ${R}\` 注册,不要使用 \`roll agent install\``),void(process.exitCode=1);const D=s(R);if(t(D)&&i(D).isDirectory())return $.error(`本地源码目录请使用 \`roll agent add ${R}\` 注册,不要使用 \`roll agent install\``),void(process.exitCode=1);const W=y(R),q=s(o.dataDir,"installed",w(W));t(q)||r(q,{recursive:!0}),C.registry&&$.info(`使用 npm registry: ${C.registry}(roll.config.yaml install.registry)`),$.info(`安装 ${R}...`);const B={command:"npm",args:["install","--prefix",q,R,...b(C)]};try{await S(B,{timeout:C.networkTimeoutMs},{...v(C.fetchRetries),onRetry:({attempt:e,delayMs:t})=>{$.warn(`安装遇到网络问题,${Math.round(t/1e3)}s 后重试(第 ${e+1} 次)...`)}})}catch(e){return $.error(`安装失败: ${j(B,e)}`),void(process.exitCode=1)}const E=h(q,W);if(!t(E))return $.error(`安装完成但未找到包目录: ${E}`),void(process.exitCode=1);const M=k(E);$.info("解析已安装 Agent 的 SKILL.md...");const O=m(E),L=new f(o.dataDir),T={skill:O.skill,transport:O.transport,runtime:O.runtime,installPath:E,registeredAt:(new Date).toISOString(),status:"idle",source:{type:"installed-package",packageName:M?.name??W,packageSpec:R,installDir:q,...M?.version?{installedVersion:M.version}:{}},...O.skillBody.length>0?{skillBody:O.skillBody}:{}};"core-managed"===T.runtime.ownership&&T.runtime.setup?.playwright&&!e["skip-browser-setup"]&&$.info(`即将安装浏览器运行时 (${T.runtime.setup.playwright.browsers.join(", ")}),这可能需要一些时间...`);const I=await u(T,{skipBrowserSetup:e["skip-browser-setup"]});I.ok?I.skipped?$.info(I.message):$.success(I.message):($.warn(`Agent setup 失败:${I.message}`),I.retryCommand&&$.info(`重试命令: ${I.retryCommand}`));const N=L.findByName(O.skill.name);try{const t="core-managed"===N?.runtime.ownership&&"online"===N.status;if("installed-package"===N?.source?.type?L.replace(N.skill.name,T):L.add(T),!I.ok)return L.updateStatus(O.skill.name,"error"),void(process.exitCode=1);if("core-managed"===T.runtime.ownership&&e.start){t&&await g(o.dataDir,T.skill.name),L.updateStatus(T.skill.name,"starting");let e=!1;try{c(T,o.dataDir,n(l().config,T.skill.name)),e=!0,await d(T,{startupTimeoutMs:15e3,probeTimeoutMs:2e3}),L.updateStatus(T.skill.name,"online")}catch(t){return e&&await g(o.dataDir,T.skill.name).catch(()=>{}),L.updateStatus(T.skill.name,"error"),$.error(`Agent "${O.skill.name}" 已安装,但自动启动失败:${t instanceof Error?t.message:String(t)}`),void(process.exitCode=1)}}$.success(`Agent "${O.skill.name}" 安装并注册成功`),A(O.skill.name,O.skill.env,o.env)}catch(e){$.error(e instanceof Error?e.message:String(e)),process.exitCode=1}}});function A(e,t,r){const i=o(e,t,r);if(i)return i.missingRequired.length>0?($.warn(`Agent "${e}" 仍缺少必填环境变量: ${i.missingRequired.map(e=>e.name).join(", ")}`),$.info(`运行 \`roll config setup agent ${e}\` 交互式配置。`),void $.info(`运行 \`roll config explain agents.env.${e}\` 查看配置说明。`)):void(i.processEnvOnlyRequired.length>0&&($.warn(`Agent "${e}" 当前依赖 shell 环境变量: ${i.processEnvOnlyRequired.map(e=>e.name).join(", ")}`),$.info(`建议运行 \`roll config setup agent ${e}\` 持久写入 roll.config.yaml。`)))}
@@ -0,0 +1 @@
1
+ export declare function explainConfig(path: string | undefined): void;
@@ -0,0 +1 @@
1
+ import{getAgentEnvFromAgentsConfig as e,inspectAgentEnvRequirements as n}from"../../config/helpers.js";import{loadAgentsConfig as o}from"../../config/loader.js";import{normalizeUserPath as t}from"../../config/key-codec.js";import{AgentStore as l}from"../../registry/store.js";import{findConfigGuidance as s,flattenAgentEnvDeclarations as r,listConfigGuidanceEntries as i}from"./config-guidance.js";export function explainConfig(e){if(void 0===e){console.log("可解释的配置路径:\n");for(const e of i())console.log(`- ${e.path}: ${e.title}`);return console.log("\n示例:"),console.log(" roll config explain install.registry"),void console.log(" roll config explain agents.env.browser-use-agent.REPLY_AUTHORITY_URL")}const n=g(e);if(n)return void console.log(n);const o=s(e);if(!o)return console.error(`✗ 未找到配置说明: ${e}`),console.error(" 运行 `roll config explain` 查看可用路径。"),void(process.exitCode=1);console.log(`${o.title}\n`),console.log(`路径: ${o.path}`),console.log(`用途: ${o.purpose}`),o.defaultBehavior&&console.log(`默认行为: ${o.defaultBehavior}`),o.example&&console.log(`示例:\n${o.example}`),o.setupCommand&&console.log(`配置向导: ${o.setupCommand}`)}function g(s){const i=t(s.split("."));if("agents"!==i[0]||"env"!==i[1])return;const g=i[2];if(void 0===g)return;const c=i[3],{agentsConfig:a}=o(),f=new l(a.dataDir).findByName(g);if(!f)return`Agent 环境变量\n\nAgent "${g}" 未注册。\n配置向导: roll config setup agent ${g}`;const u=r(f.skill.env);if(0===u.length)return`Agent 环境变量\n\nAgent "${g}" 未声明环境变量需求。`;if(void 0===c){const e=n(f.skill.name,f.skill.env,a.env),o=["Agent 环境变量","",`路径: agents.env.${g}`,`用途: 注入到 Agent "${g}" 进程的环境变量。`,`配置向导: roll config setup agent ${g}`,"","变量:"];for(const n of e?.items??u)o.push(`- ${n.name}: ${n.required?"必填":"可选"}${"source"in n?`,来源 ${n.source}`:""}`);return o.join("\n")}const p=u.find(e=>e.name===c);if(!p)return`Agent 环境变量\n\nAgent "${g}" 未声明环境变量 ${c}。\n配置向导: roll config setup agent ${g}`;const $=n(f.skill.name,f.skill.env,a.env),m=$?.items.find(e=>e.name===c),d=e(a,f.skill.name),v=[`Agent 环境变量: ${c}`,"",`路径: agents.env.${g}.${c}`,`Agent: ${g}`,"类型: "+(p.required?"必填":"可选")];return p.purpose&&v.push(`用途: ${p.purpose}`),p.default&&v.push(`默认值: ${p.default}`),p.example&&v.push(`示例: ${p.example}`),m&&v.push(`当前来源: ${m.source}`),d?.[c]&&v.push("当前 YAML: 已配置"),v.push(`配置向导: roll config setup agent ${g}`),v.join("\n")}
@@ -0,0 +1,109 @@
1
+ import type { AgentEnvDeclaration, AgentSkillEnvDeclarations } from "../../types/agent.ts";
2
+ export declare const INSTALL_SCENARIOS: readonly ["default-network", "china-dev", "private-registry", "advanced"];
3
+ export type InstallScenario = (typeof INSTALL_SCENARIOS)[number];
4
+ export interface ConfigGuidanceEntry {
5
+ readonly path: string;
6
+ readonly title: string;
7
+ readonly purpose: string;
8
+ readonly defaultBehavior?: string;
9
+ readonly example?: string;
10
+ readonly setupCommand?: string;
11
+ }
12
+ export declare const CONFIG_GUIDANCE_ENTRIES: readonly [{
13
+ readonly path: "llm.default-provider";
14
+ readonly title: "默认 LLM Provider";
15
+ readonly purpose: "`roll ask` / `roll chat` 默认使用的模型提供商。";
16
+ readonly defaultBehavior: `\u9ED8\u8BA4\u503C\u4E3A \`${string}\`\u3002`;
17
+ readonly example: `llm:
18
+ default-provider: ${string}`;
19
+ readonly setupCommand: "roll config setup llm";
20
+ }, {
21
+ readonly path: "llm.default-model";
22
+ readonly title: "默认 LLM Model";
23
+ readonly purpose: "`roll ask` / `roll chat` 默认使用的模型名称。";
24
+ readonly defaultBehavior: `\u9ED8\u8BA4\u503C\u4E3A \`${string}\`\u3002`;
25
+ readonly example: `llm:
26
+ default-model: ${string}`;
27
+ readonly setupCommand: "roll config setup llm";
28
+ }, {
29
+ readonly path: "llm.providers.<provider>.api-key";
30
+ readonly title: "Provider API Key";
31
+ readonly purpose: string;
32
+ readonly example: string;
33
+ readonly setupCommand: "roll config setup llm";
34
+ }, {
35
+ readonly path: "llm.providers.<provider>.base-url";
36
+ readonly title: "Provider Base URL";
37
+ readonly purpose: "指定 LLM provider 的自定义 API base URL,通常只在代理、兼容网关或私有部署时需要。";
38
+ readonly defaultBehavior: "不配置时使用 provider SDK 默认地址。";
39
+ readonly example: "llm:\n providers:\n openai:\n base-url: https://api.openai.com/v1";
40
+ readonly setupCommand: "roll config setup llm";
41
+ }, {
42
+ readonly path: "install.registry";
43
+ readonly title: "npm Registry";
44
+ readonly purpose: "`roll agent install` / `roll update` 查询和安装 npm 包时使用的 registry。";
45
+ readonly defaultBehavior: "不配置时走 npm 自身默认源;Roll 不做隐式镜像 fallback。";
46
+ readonly example: "install:\n registry: https://registry.npmmirror.com";
47
+ readonly setupCommand: "roll config setup install";
48
+ }, {
49
+ readonly path: "install.fetch-retries";
50
+ readonly title: "npm Fetch Retries";
51
+ readonly purpose: "透传给 npm 的 `--fetch-retries`,同时影响 Roll 层整体网络重试次数。";
52
+ readonly defaultBehavior: `\u9ED8\u8BA4\u503C\u4E3A \`${number}\`\u3002`;
53
+ readonly example: `install:
54
+ fetch-retries: ${number}`;
55
+ readonly setupCommand: "roll config setup install";
56
+ }, {
57
+ readonly path: "install.prefer-offline";
58
+ readonly title: "Prefer Offline";
59
+ readonly purpose: "安装时是否附加 `--prefer-offline`。";
60
+ readonly defaultBehavior: "默认值为 `false`,避免更新时复用过期 npm 元数据。" | "默认值为 `true`,避免更新时复用过期 npm 元数据。";
61
+ readonly example: "install:\n prefer-offline: false" | "install:\n prefer-offline: true";
62
+ readonly setupCommand: "roll config setup install";
63
+ }, {
64
+ readonly path: "install.network-timeout-ms";
65
+ readonly title: "Install Network Timeout";
66
+ readonly purpose: "单次 npm install 命令的超时时间,单位毫秒。";
67
+ readonly defaultBehavior: `\u9ED8\u8BA4\u503C\u4E3A \`${number}\`\uFF0C\u5373 120 \u79D2\u3002`;
68
+ readonly example: `install:
69
+ network-timeout-ms: ${number}`;
70
+ readonly setupCommand: "roll config setup install";
71
+ }, {
72
+ readonly path: "agents.data-dir";
73
+ readonly title: "Agent 数据目录";
74
+ readonly purpose: "Roll 持久化已注册 Agent、PID、日志和 runtime sidecar 的目录。";
75
+ readonly defaultBehavior: `\u9ED8\u8BA4\u503C\u4E3A \`${string}\`\u3002`;
76
+ readonly example: `agents:
77
+ data-dir: ${string}`;
78
+ }, {
79
+ readonly path: "agents.env.<agent-name>";
80
+ readonly title: "Agent 环境变量";
81
+ readonly purpose: "按 Agent 名称声明注入到子 Agent 进程的环境变量。";
82
+ readonly defaultBehavior: "未配置时只会继承当前 shell 环境变量;core-managed Agent 需要重启才会看到新值。";
83
+ readonly example: "agents:\n env:\n browser-use-agent:\n REPLY_AUTHORITY_URL: https://example.com";
84
+ readonly setupCommand: "roll config setup agent <agent-name>";
85
+ }, {
86
+ readonly path: "ask.confirm-threshold";
87
+ readonly title: "Ask 确认阈值";
88
+ readonly purpose: "`roll ask` 路由置信度低于该阈值时,倾向要求用户确认。";
89
+ readonly defaultBehavior: "未配置时使用内置路由默认行为。";
90
+ readonly example: "ask:\n confirm-threshold: 0.5";
91
+ }, {
92
+ readonly path: "browser.default-instance";
93
+ readonly title: "默认浏览器实例";
94
+ readonly purpose: "多浏览器实例配置下,未显式指定 browserInstance 时使用的默认实例。";
95
+ readonly defaultBehavior: "不配置且只有一个实例时可自动推断;多个实例时工具可能返回 needs_input。";
96
+ readonly example: "browser:\n default-instance: boss-a";
97
+ }, {
98
+ readonly path: "browser.instances";
99
+ readonly title: "浏览器实例声明";
100
+ readonly purpose: "声明 browser-use-agent 可使用的浏览器 profile、CDP 端口、会话目录和业务归因信息。";
101
+ readonly defaultBehavior: "不配置时 browser-use-agent 使用 legacy 单实例环境变量路径。";
102
+ readonly example: "browser:\n instances:\n boss-a:\n platform: zhipin\n mode: managed-cdp\n cdp-port: 9222\n user-data-dir: ~/.roll-agent/browser/boss-a";
103
+ }];
104
+ export declare function listConfigGuidanceEntries(): readonly ConfigGuidanceEntry[];
105
+ export declare function findConfigGuidance(path: string): ConfigGuidanceEntry | undefined;
106
+ export declare function flattenAgentEnvDeclarations(declarations: AgentSkillEnvDeclarations | undefined): ReadonlyArray<AgentEnvDeclaration & {
107
+ readonly required: boolean;
108
+ }>;
109
+ export declare function isSecretEnvName(name: string): boolean;
@@ -0,0 +1 @@
1
+ import{DEFAULT_CONFIG as e}from"../../config/defaults.js";import{normalizeUserPath as t}from"../../config/key-codec.js";export const INSTALL_SCENARIOS=["default-network","china-dev","private-registry","advanced"];export const CONFIG_GUIDANCE_ENTRIES=[{path:"llm.default-provider",title:"默认 LLM Provider",purpose:"`roll ask` / `roll chat` 默认使用的模型提供商。",defaultBehavior:`默认值为 \`${e.llm.defaultProvider}\`。`,example:`llm:\n default-provider: ${e.llm.defaultProvider}`,setupCommand:"roll config setup llm"},{path:"llm.default-model",title:"默认 LLM Model",purpose:"`roll ask` / `roll chat` 默认使用的模型名称。",defaultBehavior:`默认值为 \`${e.llm.defaultModel}\`。`,example:`llm:\n default-model: ${e.llm.defaultModel}`,setupCommand:"roll config setup llm"},{path:"llm.providers.<provider>.api-key",title:"Provider API Key",purpose:"指定 LLM provider 的 API key。可以写真实 key,也可以写 `${ENV_VAR}` 占位符。",example:"llm:\n providers:\n anthropic:\n api-key: ${ANTHROPIC_API_KEY}",setupCommand:"roll config setup llm"},{path:"llm.providers.<provider>.base-url",title:"Provider Base URL",purpose:"指定 LLM provider 的自定义 API base URL,通常只在代理、兼容网关或私有部署时需要。",defaultBehavior:"不配置时使用 provider SDK 默认地址。",example:"llm:\n providers:\n openai:\n base-url: https://api.openai.com/v1",setupCommand:"roll config setup llm"},{path:"install.registry",title:"npm Registry",purpose:"`roll agent install` / `roll update` 查询和安装 npm 包时使用的 registry。",defaultBehavior:"不配置时走 npm 自身默认源;Roll 不做隐式镜像 fallback。",example:"install:\n registry: https://registry.npmmirror.com",setupCommand:"roll config setup install"},{path:"install.fetch-retries",title:"npm Fetch Retries",purpose:"透传给 npm 的 `--fetch-retries`,同时影响 Roll 层整体网络重试次数。",defaultBehavior:`默认值为 \`${e.install.fetchRetries}\`。`,example:`install:\n fetch-retries: ${e.install.fetchRetries}`,setupCommand:"roll config setup install"},{path:"install.prefer-offline",title:"Prefer Offline",purpose:"安装时是否附加 `--prefer-offline`。",defaultBehavior:`默认值为 \`${e.install.preferOffline}\`,避免更新时复用过期 npm 元数据。`,example:`install:\n prefer-offline: ${e.install.preferOffline}`,setupCommand:"roll config setup install"},{path:"install.network-timeout-ms",title:"Install Network Timeout",purpose:"单次 npm install 命令的超时时间,单位毫秒。",defaultBehavior:`默认值为 \`${e.install.networkTimeoutMs}\`,即 120 秒。`,example:`install:\n network-timeout-ms: ${e.install.networkTimeoutMs}`,setupCommand:"roll config setup install"},{path:"agents.data-dir",title:"Agent 数据目录",purpose:"Roll 持久化已注册 Agent、PID、日志和 runtime sidecar 的目录。",defaultBehavior:`默认值为 \`${e.agents.dataDir}\`。`,example:`agents:\n data-dir: ${e.agents.dataDir}`},{path:"agents.env.<agent-name>",title:"Agent 环境变量",purpose:"按 Agent 名称声明注入到子 Agent 进程的环境变量。",defaultBehavior:"未配置时只会继承当前 shell 环境变量;core-managed Agent 需要重启才会看到新值。",example:"agents:\n env:\n browser-use-agent:\n REPLY_AUTHORITY_URL: https://example.com",setupCommand:"roll config setup agent <agent-name>"},{path:"ask.confirm-threshold",title:"Ask 确认阈值",purpose:"`roll ask` 路由置信度低于该阈值时,倾向要求用户确认。",defaultBehavior:"未配置时使用内置路由默认行为。",example:"ask:\n confirm-threshold: 0.5"},{path:"browser.default-instance",title:"默认浏览器实例",purpose:"多浏览器实例配置下,未显式指定 browserInstance 时使用的默认实例。",defaultBehavior:"不配置且只有一个实例时可自动推断;多个实例时工具可能返回 needs_input。",example:"browser:\n default-instance: boss-a"},{path:"browser.instances",title:"浏览器实例声明",purpose:"声明 browser-use-agent 可使用的浏览器 profile、CDP 端口、会话目录和业务归因信息。",defaultBehavior:"不配置时 browser-use-agent 使用 legacy 单实例环境变量路径。",example:"browser:\n instances:\n boss-a:\n platform: zhipin\n mode: managed-cdp\n cdp-port: 9222\n user-data-dir: ~/.roll-agent/browser/boss-a"}];export function listConfigGuidanceEntries(){return CONFIG_GUIDANCE_ENTRIES}export function findConfigGuidance(e){const r=t(e.split(".")).join(".");return CONFIG_GUIDANCE_ENTRIES.find(e=>l(e.path,r))}function l(e,l){const r=t(e.split(".")).join(".");if(r===l)return!0;const a=r.split("."),n=l.split(".");return a.length===n.length&&a.every((e,t)=>e.startsWith("<")||e===n[t])}export function flattenAgentEnvDeclarations(e){return[...(e?.required??[]).map(e=>({...e,required:!0})),...(e?.optional??[]).map(e=>({...e,required:!1}))]}export function isSecretEnvName(e){return/(?:TOKEN|KEY|SECRET|PASSWORD)/iu.test(e)}
@@ -0,0 +1,37 @@
1
+ export declare class ConfigSetupCancelledError extends Error {
2
+ constructor();
3
+ }
4
+ export interface PromptOption<Value extends string> {
5
+ readonly value: Value;
6
+ readonly label: string;
7
+ readonly hint?: string;
8
+ }
9
+ export interface ConfigPromptAdapter {
10
+ intro(title: string): void;
11
+ outro(message: string): void;
12
+ info(message: string): void;
13
+ warn(message: string): void;
14
+ select<Value extends string>(options: {
15
+ readonly message: string;
16
+ readonly options: readonly PromptOption<Value>[];
17
+ readonly initialValue?: Value;
18
+ }): Promise<Value>;
19
+ text(options: {
20
+ readonly message: string;
21
+ readonly placeholder?: string;
22
+ readonly defaultValue?: string;
23
+ readonly initialValue?: string;
24
+ readonly required?: boolean;
25
+ readonly validate?: (value: string) => string | undefined;
26
+ }): Promise<string>;
27
+ password(options: {
28
+ readonly message: string;
29
+ readonly required?: boolean;
30
+ readonly validate?: (value: string) => string | undefined;
31
+ }): Promise<string>;
32
+ confirm(options: {
33
+ readonly message: string;
34
+ readonly initialValue?: boolean;
35
+ }): Promise<boolean>;
36
+ }
37
+ export declare const clackPromptAdapter: ConfigPromptAdapter;
@@ -0,0 +1 @@
1
+ import{cancel as e,confirm as t,intro as a,isCancel as l,log as i,outro as u,password as o,select as r,text as n}from"@clack/prompts";const s=process.stderr;export class ConfigSetupCancelledError extends Error{constructor(){super("配置向导已取消")}}export const clackPromptAdapter={intro(e){a(e,{output:s})},outro(e){u(e,{output:s})},info(e){i.info(e,{output:s})},warn(e){i.warn(e,{output:s})},async select(e){const t=e.options.map(e=>e.hint?{value:e.value,label:e.label,hint:e.hint}:{value:e.value,label:e.label});return p(await r({message:e.message,options:t,output:s,...e.initialValue?{initialValue:e.initialValue}:{}}))},text:async e=>p(await n({message:e.message,output:s,...e.placeholder?{placeholder:e.placeholder}:{},...void 0!==e.defaultValue?{defaultValue:e.defaultValue}:{},...void 0!==e.initialValue?{initialValue:e.initialValue}:{},validate(t){const a=t??"";return e.required&&0===a.trim().length?"此项不能为空":e.validate?.(a)}})),password:async e=>p(await o({message:e.message,output:s,validate(t){const a=t??"";return e.required&&0===a.trim().length?"此项不能为空":e.validate?.(a)}})),confirm:async e=>p(await t({message:e.message,output:s,...void 0!==e.initialValue?{initialValue:e.initialValue}:{}}))};function p(t){if(l(t))throw e("已取消",{output:s}),new ConfigSetupCancelledError;return t}
@@ -0,0 +1,3 @@
1
+ import { type ConfigPromptAdapter } from "./config-prompts.ts";
2
+ export type ConfigSetupModule = "llm" | "install" | "agent";
3
+ export declare function runConfigSetup(moduleArg: string | undefined, valueArg: string | undefined, prompts?: ConfigPromptAdapter): Promise<void>;
@@ -0,0 +1 @@
1
+ import{existsSync as e,readFileSync as t,writeFileSync as r}from"node:fs";import{resolve as n}from"node:path";import{stringify as i}from"yaml";import{DEFAULT_CONFIG as a,DEFAULT_LLM_MODELS as o,DEFAULT_LLM_PROVIDER as l,LLM_PROVIDER_OPTIONS as s}from"../../config/defaults.js";import{inspectAgentEnvRequirements as c}from"../../config/helpers.js";import{loadAgentsConfig as u,parseConfigDocument as m,resolveConfigPath as f,validateConfigText as d}from"../../config/loader.js";import{encodePathToYaml as g}from"../../config/key-codec.js";import{AgentStore as p}from"../../registry/store.js";import{flattenAgentEnvDeclarations as v,isSecretEnvName as w}from"./config-guidance.js";import{ConfigSetupCancelledError as h,clackPromptAdapter as y}from"./config-prompts.js";const k=/\$\{[^}]+\}/u;export async function runConfigSetup(e,t,r=y){try{r.intro("Roll 配置向导");switch(await $(e,r)){case"llm":await b(r);break;case"install":await A(r);break;case"agent":await V(t,r)}}catch(e){if(e instanceof h)return void(process.exitCode=1);throw e}}async function $(e,t){if(void 0===e)return t.select({message:"要配置哪一类内容?",options:[{value:"llm",label:"LLM",hint:"默认模型和 provider key"},{value:"install",label:"Install network",hint:"npm registry、重试和超时"},{value:"agent",label:"Agent env",hint:"配置某个 Agent 需要的环境变量"}]});if("llm"===e||"install"===e||"agent"===e)return e;throw new Error(`未知 setup 模块: ${e}。可用: llm, install, agent`)}async function b(e){const t=await e.select({message:"选择默认 LLM provider",options:s.map(e=>({value:e,label:e,hint:o[e]})),initialValue:l}),r=await e.text({message:"默认 model",defaultValue:o[t],required:!0}),n=await e.password({message:`${t} API key(可直接输入真实 key,也可输入 \${ENV_VAR})`,required:!0}),i=await e.text({message:"自定义 base-url(可选,留空则不写)",required:!1}),a=q();I(a.document,["llm","defaultProvider"],t),I(a.document,["llm","defaultModel"],r),I(a.document,["llm","providers",t,"apiKey"],n),i.trim().length>0?I(a.document,["llm","providers",t,"baseUrl"],i.trim()):N(a.document,["llm","providers",t,"baseUrl"]),P(a),T(e,`${t} API key`,n),e.outro(`已配置 LLM: ${t}/${r}`)}async function A(e){const t=await e.select({message:"当前安装/更新网络更接近哪种场景?",options:[{value:"default-network",label:"默认网络 / 海外网络",hint:"不设置 registry"},{value:"china-dev",label:"国内开发机",hint:"使用 https://registry.npmmirror.com"},{value:"private-registry",label:"企业内网 / 私有 registry"},{value:"advanced",label:"高级自定义"}]}),r=q();switch(t){case"default-network":j(r.document),N(r.document,["install","registry"]);break;case"china-dev":j(r.document),I(r.document,["install","registry"],"https://registry.npmmirror.com");break;case"private-registry":{const t=await e.text({message:"私有 registry URL",placeholder:"https://registry.example.com",required:!0});j(r.document),I(r.document,["install","registry"],t.trim());break}case"advanced":await x(r.document,e)}P(r),e.outro("已配置 install 网络参数")}async function x(e,t){const r=await t.text({message:"npm registry(留空则不写)",placeholder:"https://registry.npmmirror.com"}),n=await t.text({message:"fetch-retries",defaultValue:"3",required:!0,validate:D(0,10)}),i=await t.confirm({message:"是否启用 prefer-offline?",initialValue:!1}),a=await t.text({message:"network-timeout-ms",defaultValue:"120000",required:!0,validate:D(1e4,Number.MAX_SAFE_INTEGER)});I(e,["install","fetchRetries"],Number.parseInt(n,10)),I(e,["install","preferOffline"],i),I(e,["install","networkTimeoutMs"],Number.parseInt(a,10)),r.trim().length>0?I(e,["install","registry"],r.trim()):N(e,["install","registry"])}async function V(e,t){const{agentsConfig:r}=u(),n=new p(r.dataDir),i=await S(n.list(),e,t),a=c(i.skill.name,i.skill.env,r.env);if(!a)throw new Error(`Agent "${i.skill.name}" 未声明环境变量需求。`);const o=q(),l={...r.env?.[i.skill.name]??{}},s=v(i.skill.env),m=new Map(a.items.map(e=>[e.name,e]));for(const e of s.filter(e=>e.required)){const r=m.get(e.name),n=await M({declaration:e,yamlValue:l[e.name],source:r?.source,processEnvValue:process.env[e.name]},t);void 0!==n&&(l[e.name]=n)}if(await t.confirm({message:"是否配置可选环境变量?",initialValue:!1}))for(const e of s.filter(e=>!e.required)){const r=m.get(e.name),n=await M({declaration:e,yamlValue:l[e.name],source:r?.source,processEnvValue:process.env[e.name]},t);void 0!==n&&(l[e.name]=n)}I(o.document,["agents","env",i.skill.name],l),P(o);for(const[e,r]of Object.entries(l))w(e)&&T(t,e,r);Y(t,i),t.outro(`已配置 Agent 环境变量: ${i.skill.name}`)}async function S(e,t,r){if(void 0!==t){const r=e.find(e=>e.skill.name===t);if(!r)throw new Error(`Agent "${t}" 未找到`);if(!E(r))throw new Error(`Agent "${t}" 未声明环境变量需求。`);return r}const n=e.filter(E);if(0===n.length)throw new Error("没有已注册且声明环境变量需求的 Agent。");const i=await r.select({message:"选择要配置的 Agent",options:n.map(e=>({value:e.skill.name,label:e.skill.name,hint:e.skill.description}))}),a=n.find(e=>e.skill.name===i);if(!a)throw new Error(`Agent "${i}" 未找到`);return a}function E(e){return v(e.skill.env).length>0}async function M(e,t){const{declaration:r,processEnvValue:n,source:i,yamlValue:a}=e,o="agents.env"===i&&O(a),l=[r.required?"必填":"可选",o?"回车保留当前值":void 0].filter(e=>void 0!==e).join(","),s=`${r.name}(${l})`,c=[r.purpose?`用途: ${r.purpose}`:void 0,r.example?`示例: ${r.example}`:void 0,r.default?`默认: ${r.default}`:void 0,o?"当前 YAML 已配置,回车保留当前值。":void 0,void 0===a||o?void 0:"当前 YAML 值为空或占位符未解析,会被视为缺失。","process.env"===i?"当前来源: 当前 shell 临时环境变量,尚未持久写入 YAML。":void 0].filter(e=>void 0!==e).join("\n");if(c.length>0&&t.info(c),"process.env"===i&&"string"==typeof n&&n.length>0){if(await t.confirm({message:`是否将 ${r.name} 的当前 shell 值写入 roll.config.yaml?`,initialValue:!0}))return n}const u=r.required&&!o&&void 0===r.default&&"process.env"!==i,m={message:s,...o&&void 0!==a?{defaultValue:a}:{},...void 0!==r.example?{placeholder:r.example}:{},required:u},f=w(r.name)?await t.password({message:s,required:u}):await t.text(m);return 0===f.trim().length?o?a:void 0:f}function j(e){I(e,["install","fetchRetries"],a.install.fetchRetries),I(e,["install","preferOffline"],a.install.preferOffline),I(e,["install","networkTimeoutMs"],a.install.networkTimeoutMs)}function q(){const r=f(),i=r??n(process.cwd(),"roll.config.yaml");if(void 0===r||!e(i))return{configPath:i,existed:!1,document:L()};const a=t(i,"utf-8");return{configPath:i,existed:!0,raw:a,document:m(a,i)}}function L(){return{llm:{"default-provider":a.llm.defaultProvider,"default-model":a.llm.defaultModel,providers:{}},ask:{},agents:{"data-dir":a.agents.dataDir}}}function P(e){const t=i(e.document,{lineWidth:0});d(t,e.configPath),e.existed&&void 0!==e.raw&&r(R(e.configPath),e.raw,"utf-8"),r(e.configPath,t,"utf-8")}function R(e){const t=new Date;return`${e}.bak.${[t.getFullYear().toString().padStart(4,"0"),(t.getMonth()+1).toString().padStart(2,"0"),t.getDate().toString().padStart(2,"0"),"-",t.getHours().toString().padStart(2,"0"),t.getMinutes().toString().padStart(2,"0"),t.getSeconds().toString().padStart(2,"0")].join("")}`}function I(e,t,r){const n=g(t),i=n[n.length-1];if(void 0===i)throw new Error("配置路径不能为空");let a=e;for(const e of n.slice(0,-1)){const t=a[e];("object"!=typeof t||null===t||Array.isArray(t))&&(a[e]={}),a=a[e]}a[i]=r}function N(e,t){const r=g(t),n=r[r.length-1];if(void 0===n)return;let i=e;for(const e of r.slice(0,-1)){if("object"!=typeof i||null===i||Array.isArray(i))return;i=i[e]}"object"!=typeof i||null===i||Array.isArray(i)||delete i[n]}function D(e,t){return r=>{if(!/^\d+$/u.test(r))return"请输入整数";const n=Number.parseInt(r,10);return n<e||n>t?`请输入 ${String(e)} 到 ${String(t)} 之间的整数`:void 0}}function O(e){return"string"==typeof e&&e.length>0&&!k.test(e)}function T(e,t,r){0===r.trim().length||k.test(r)||e.warn(`${t} 将以明文写入 roll.config.yaml,建议改用 \${ENV_VAR} 引用并避免把配置文件提交到版本库。`)}function Y(e,t){const r=t.skill.name;switch(t.runtime.ownership){case"core-managed":e.info(`需重启后生效:roll agent stop ${r} && roll agent start ${r}`);break;case"external-managed":e.info(`请重启你自行管理的 "${r}" 进程以加载新环境变量。`);break;case"on-demand":e.info(`下次 roll run / roll ask 调用 "${r}" 时自动生效。`)}}
@@ -1 +1 @@
1
- import{defineCommand as o}from"citty";import{readFileSync as t,writeFileSync as r,existsSync as e}from"node:fs";import{resolve as n}from"node:path";import{createInterface as i}from"node:readline/promises";import{stringify as s}from"yaml";import{inspectConfigFile as c,loadConfig as a,parseConfigDocument as l,validateConfigText as d}from"../../config/loader.js";import{encodePathToYaml as f,normalizeUserPath as u}from"../../config/key-codec.js";import{applyKnownConfigMigrations as g}from"../../config/migration.js";export default o({meta:{description:"管理全局配置"},args:{action:{type:"positional",description:"操作(init/get/set/migrate)",required:!0},key:{type:"positional",description:"配置键(get/set 时使用,用英文句点 `.` 分隔,如 ask.confirm-threshold)",required:!1},value:{type:"positional",description:"配置值(set 时使用)",required:!1}},async run({args:o}){try{if("init"===o.action)return void await v();if("get"===o.action)return void w(o.key);if("set"===o.action)return void S(o.key,o.value);if("migrate"===o.action)return void $();console.error(`✗ 未知操作: ${o.action}。可用: init, get, set, migrate`),process.exitCode=1}catch(o){const t=o instanceof Error?o.message:String(o);console.error(`✗ ${t}`),process.exitCode=1}}});function p(o,t){const r=o?.trim();return r||t}function m({provider:o,model:t,apiKeyEnv:r}){return`llm:\n default-provider: ${o}\n default-model: ${t}\n providers:\n ${o}:\n api-key: \${${r}}\n\nask:\n confirm-threshold: 0.5\n\nagents:\n data-dir: ~/.roll-agent/agents\n`}async function h(){if(!process.stdin.isTTY){const[o,r,e]=t(0,"utf-8").split(/\r?\n/u);return{provider:p(o,"anthropic"),model:p(r,"claude-sonnet-4-20250514"),apiKeyEnv:p(e,"ANTHROPIC_API_KEY")}}const o=i({input:process.stdin,output:process.stderr}),r=p(await o.question("默认 LLM provider (anthropic/openai/qwen) [anthropic]: "),"anthropic"),e=p(await o.question("默认 model [claude-sonnet-4-20250514]: "),"claude-sonnet-4-20250514"),n=p(await o.question("API Key 环境变量名 [ANTHROPIC_API_KEY]: "),"ANTHROPIC_API_KEY");return o.close(),{provider:r,model:e,apiKeyEnv:n}}async function v(){const o=n(process.cwd(),"roll.config.yaml");if(e(o)){const t=c({configPath:o});switch(t.status){case"needs-migration":console.error(`⚠ 现有配置文件需要迁移: ${o}`),console.error(" 建议先运行 `roll config migrate`,再决定是否重新初始化。");break;case"invalid":console.error(`⚠ 现有配置文件存在问题:\n${t.error.message}`)}if(console.error(`⚠ 配置文件已存在: ${o}`),!process.stdin.isTTY)throw new Error("非交互模式下不会覆盖现有配置文件,请手动删除后重试。");const r=i({input:process.stdin,output:process.stderr}),e=await r.question("是否覆盖?(y/N) ");if(r.close(),"y"!==e.toLowerCase())return void console.error("已取消。")}const t=m(await h());d(t,o),r(o,t,"utf-8"),console.log(`✓ 配置文件已创建: ${o}`)}function y(o){const t=new Date;return`${o}.bak.${[t.getFullYear().toString().padStart(4,"0"),(t.getMonth()+1).toString().padStart(2,"0"),t.getDate().toString().padStart(2,"0"),"-",t.getHours().toString().padStart(2,"0"),t.getMinutes().toString().padStart(2,"0"),t.getSeconds().toString().padStart(2,"0")].join("")}`}function $(){const o=c();if("not-found"===o.status)throw new Error("未找到配置文件。请先运行 roll config init");if("valid"===o.status)return void console.log(`✓ 配置文件已是最新格式,无需迁移: ${o.configPath}`);if("invalid"===o.status)throw o.error;const t=l(o.raw,o.configPath),e=g(t);if(!e.ok){const o=e.issues.map(o=>` - ${o.message}`).join("\n");throw new Error(`配置无法自动迁移:\n${o}`)}if(!e.changed)return void console.log(`✓ 配置文件已是最新格式,无需迁移: ${o.configPath}`);const n=y(o.configPath);r(n,o.raw,"utf-8");const i=s(e.document,{lineWidth:0});d(i,o.configPath),r(o.configPath,i,"utf-8"),console.log(`✓ 配置文件已迁移: ${o.configPath}`),console.log(`✓ 已备份原文件: ${n}`);for(const o of e.summary)console.log(` - ${o}`)}function w(o){const{config:t,configPath:r}=a();if(!o)return console.log(JSON.stringify(t,null,2)),void(r&&console.error(`(来源: ${r})`));const e=u(o.split("."));let n=t;for(const t of e){if("object"!=typeof n||null===n)return console.error(`✗ 配置键 "${o}" 不存在`),void(process.exitCode=1);n=n[t]}if(void 0===n)return console.error(`✗ 配置键 "${o}" 不存在`),void(process.exitCode=1);console.log("object"==typeof n?JSON.stringify(n,null,2):String(n))}function S(o,e){if(!o||void 0===e)return console.error("✗ 用法: roll config set <key> <value>"),console.error(" 示例: roll config set ask.confirm-threshold 0.5"),void(process.exitCode=1);const{configPath:n}=a();if(!n)return console.error("✗ 未找到配置文件。请先运行 roll config init"),void(process.exitCode=1);const i=t(n,"utf-8"),c=l(i,n),u=f(o.split(".")),g=u[u.length-1];if(void 0===g)return console.error("✗ 配置键不能为空"),void(process.exitCode=1);let p=c;for(let o=0;o<u.length-1;o++){const t=u[o],r=p[t];("object"!=typeof r||null===r||Array.isArray(r))&&(p[t]={}),p=p[t]}let m=e;"true"===e?m=!0:"false"===e?m=!1:/^\d+(\.\d+)?$/.test(e)&&(m=Number(e)),p[g]=m;const h=s(c,{lineWidth:0});d(h,n),r(n,h,"utf-8"),console.log(`✓ ${o} = ${String(m)}`),console.error(` (已写入: ${n})`)}
1
+ import{defineCommand as o}from"citty";import{readFileSync as e,writeFileSync as t,existsSync as r}from"node:fs";import{resolve as n}from"node:path";import{createInterface as i}from"node:readline/promises";import{stringify as s}from"yaml";import{DEFAULT_CONFIG as c,DEFAULT_LLM_MODELS as l,LLM_PROVIDER_OPTIONS as a}from"../../config/defaults.js";import{inspectConfigFile as f,loadConfig as d,parseConfigDocument as u,validateConfigText as p}from"../../config/loader.js";import{encodePathToYaml as g,normalizeUserPath as m}from"../../config/key-codec.js";import{applyKnownConfigMigrations as v}from"../../config/migration.js";import{explainConfig as y}from"./config-explain.js";import{runConfigSetup as h}from"./config-setup.js";export default o({meta:{description:"管理全局配置"},args:{action:{type:"positional",description:"操作(init/get/set/migrate/setup/explain)",required:!0},key:{type:"positional",description:"配置键(get/set 时使用,用英文句点 `.` 分隔,如 ask.confirm-threshold)",required:!1},value:{type:"positional",description:"配置值(set 时使用)",required:!1}},async run({args:o}){try{if("init"===o.action)return void await j();if("get"===o.action)return void K(o.key);if("set"===o.action)return void q(o.key,o.value);if("migrate"===o.action)return void A();if("setup"===o.action)return process.stdin.isTTY?void await h(o.key,o.value):(console.error("✗ `roll config setup` 需要交互式终端。"),console.error(" 非交互环境请改用 `roll config set <key> <value>` 或直接编辑 roll.config.yaml。"),console.error(" 查看配置说明:`roll config explain <key>`。"),void(process.exitCode=1));if("explain"===o.action)return void y(o.key);console.error(`✗ 未知操作: ${o.action}。可用: init, get, set, migrate, setup, explain`),process.exitCode=1}catch(o){const e=o instanceof Error?o.message:String(o);console.error(`✗ ${e}`),process.exitCode=1}}});const $={anthropic:"ANTHROPIC_API_KEY",openai:"OPENAI_API_KEY",qwen:"DASHSCOPE_API_KEY",deepseek:"DEEPSEEK_API_KEY"};function P(o,e){const t=o?.trim();return t||e}function w(o){return a.some(e=>e===o)}function S(o){return w(o)?l[o]:c.llm.defaultModel}function k(o){return w(o)?$[o]:$.anthropic}function E({provider:o,model:e,apiKeyEnv:t}){return`llm:\n default-provider: ${o}\n default-model: ${e}\n providers:\n ${o}:\n api-key: \${${t}}\n\nask:\n confirm-threshold: 0.5\n\nagents:\n data-dir: ~/.roll-agent/agents\n`}async function x(){if(!process.stdin.isTTY){const[o,t,r]=e(0,"utf-8").split(/\r?\n/u),n=P(o,c.llm.defaultProvider);return{provider:n,model:P(t,S(n)),apiKeyEnv:P(r,k(n))}}const o=i({input:process.stdin,output:process.stderr}),t=P(await o.question(`默认 LLM provider (${a.join("/")}) [${c.llm.defaultProvider}]: `),c.llm.defaultProvider),r=S(t),n=P(await o.question(`默认 model [${r}]: `),r),s=k(t),l=P(await o.question(`API Key 环境变量名 [${s}]: `),s);return o.close(),{provider:t,model:n,apiKeyEnv:l}}async function j(){const o=n(process.cwd(),"roll.config.yaml");if(r(o)){const e=f({configPath:o});switch(e.status){case"needs-migration":console.error(`⚠ 现有配置文件需要迁移: ${o}`),console.error(" 建议先运行 `roll config migrate`,再决定是否重新初始化。");break;case"invalid":console.error(`⚠ 现有配置文件存在问题:\n${e.error.message}`)}if(console.error(`⚠ 配置文件已存在: ${o}`),!process.stdin.isTTY)throw new Error("非交互模式下不会覆盖现有配置文件,请手动删除后重试。");const t=i({input:process.stdin,output:process.stderr}),r=await t.question("是否覆盖?(y/N) ");if(t.close(),"y"!==r.toLowerCase())return void console.error("已取消。")}const e=E(await x());p(e,o),t(o,e,"utf-8"),console.log(`✓ 配置文件已创建: ${o}`)}function C(o){const e=new Date;return`${o}.bak.${[e.getFullYear().toString().padStart(4,"0"),(e.getMonth()+1).toString().padStart(2,"0"),e.getDate().toString().padStart(2,"0"),"-",e.getHours().toString().padStart(2,"0"),e.getMinutes().toString().padStart(2,"0"),e.getSeconds().toString().padStart(2,"0")].join("")}`}function A(){const o=f();if("not-found"===o.status)throw new Error("未找到配置文件。请先运行 roll config init");if("valid"===o.status)return void console.log(`✓ 配置文件已是最新格式,无需迁移: ${o.configPath}`);if("invalid"===o.status)throw o.error;const e=u(o.raw,o.configPath),r=v(e);if(!r.ok){const o=r.issues.map(o=>` - ${o.message}`).join("\n");throw new Error(`配置无法自动迁移:\n${o}`)}if(!r.changed)return void console.log(`✓ 配置文件已是最新格式,无需迁移: ${o.configPath}`);const n=C(o.configPath);t(n,o.raw,"utf-8");const i=s(r.document,{lineWidth:0});p(i,o.configPath),t(o.configPath,i,"utf-8"),console.log(`✓ 配置文件已迁移: ${o.configPath}`),console.log(`✓ 已备份原文件: ${n}`);for(const o of r.summary)console.log(` - ${o}`)}function K(o){const{config:e,configPath:t}=d();if(!o)return console.log(JSON.stringify(e,null,2)),void(t&&console.error(`(来源: ${t})`));const r=m(o.split("."));let n=e;for(const e of r){if("object"!=typeof n||null===n)return console.error(`✗ 配置键 "${o}" 不存在`),void(process.exitCode=1);n=n[e]}if(void 0===n)return console.error(`✗ 配置键 "${o}" 不存在`),void(process.exitCode=1);console.log("object"==typeof n?JSON.stringify(n,null,2):String(n))}function q(o,r){if(!o||void 0===r)return console.error("✗ 用法: roll config set <key> <value>"),console.error(" 示例: roll config set ask.confirm-threshold 0.5"),void(process.exitCode=1);const{configPath:n}=d();if(!n)return console.error("✗ 未找到配置文件。请先运行 roll config init"),void(process.exitCode=1);const i=e(n,"utf-8"),c=u(i,n),l=g(o.split(".")),a=l[l.length-1];if(void 0===a)return console.error("✗ 配置键不能为空"),void(process.exitCode=1);let f=c;for(let o=0;o<l.length-1;o++){const e=l[o],t=f[e];("object"!=typeof t||null===t||Array.isArray(t))&&(f[e]={}),f=f[e]}let m=r;"true"===r?m=!0:"false"===r?m=!1:/^\d+(\.\d+)?$/.test(r)&&(m=Number(r)),f[a]=m;const v=s(c,{lineWidth:0});p(v,n),t(n,v,"utf-8"),console.log(`✓ ${o} = ${String(m)}`),console.error(` (已写入: ${n})`)}
@@ -1 +1 @@
1
- import{defineCommand as e}from"citty";import{resolve as a}from"node:path";import{existsSync as t}from"node:fs";import{execFile as r}from"node:child_process";import{promisify as n}from"node:util";import{inspectConfigFile as s,loadAgentsConfig as i,loadConfig as o}from"../../config/loader.js";import{getAgentEnv as l}from"../../config/helpers.js";import{getAgentPid as c,startAgent as u,stopAgentGracefully as m,waitForAgentReady as p}from"../../registry/process-manager.js";import{resolveTransportWithDevSpawnSpec as k}from"../../registry/dev-spawn.js";import{runAgentSetup as d}from"../../registry/runtime-setup.js";import{AgentStore as g}from"../../registry/store.js";import{discoverAgent as f}from"../../registry/discovery.js";import{inferAgentSourceType as $,readInstalledPackageManifest as h,resolveInstalledPackageRoot as w}from"../../registry/source.js";import{McpClientManager as y}from"../../mcp/client-manager.js";import{log as v,createSpinner as S}from"../utils/output.js";import{checkForUpdate as V,checkPublishedPackageUpdate as b,getCurrentVersion as j}from"../utils/update-checker.js";import{detectInstallCommand as P,formatPackageManagerError as B,runPackageManager as A}from"../utils/package-manager.js";const D=n(r);export{inferAgentSourceType as inferSourceType}from"../../registry/source.js";export{detectInstallCommand}from"../utils/package-manager.js";function N(e,a){switch(e.status){case"needs-migration":{const t="post-update"===a?"升级后需要迁移本地配置":"检测到本地配置需要迁移";v.warn(`${t}: ${e.configPath}`);for(const a of e.report.issues)v.warn(` - ${a.message}`);e.report.canAutoMigrate&&v.info("建议命令: roll config migrate");break}case"invalid":v.warn(`本地配置存在问题: ${e.configPath??"(unknown path)"}`),v.warn(` - ${e.error.message}`),"post-update"===a&&v.info("请修复配置文件后再继续使用相关命令。")}}function T(e){switch(e){case"up-to-date":return"✅";case"update-available":return"⬆";case"pinned-behind":return"📌";case"unsupported-spec":case"unknown":return"?"}}function E(e,a){switch(e.status){case"up-to-date":return e.currentVersion?`已是最新版本 (v${e.currentVersion})`:"已是最新版本";case"update-available":return e.currentVersion&&e.latestVersion?`可更新 v${e.currentVersion} → v${e.latestVersion}`:"检测到可用更新";case"pinned-behind":return e.currentVersion&&e.latestVersion?`固定版本 v${e.currentVersion};latest=v${e.latestVersion}`:"固定版本,需手动调整 package spec";case"unsupported-spec":return`不支持检查此 package spec: ${a}`;case"unknown":return e.currentVersion?`无法检查最新版本 (current=v${e.currentVersion})`:"无法检查最新版本"}}function C(e,a){if("installed-package"!==e.source?.type)return e;const t=w(e.source.installDir,e.source.packageName),r=h(t);if(!r)return e;const n={...e.source,...r.name?{packageName:r.name}:{},...r.version?{installedVersion:r.version}:{}},s=n.packageName!==e.source.packageName||n.installedVersion!==e.source.installedVersion,i=t!==e.installPath;if(!s&&!i)return e;const o={...e,installPath:t,source:n};return a&&a.replace(e.skill.name,o),o}async function M(e,a){const t=j();if(t===e)return v.info(`roll 已是最新版本 (v${t})`),!1;if(v.info(`roll v${t} → v${e}`),a)return v.info("[dry-run] 跳过实际更新"),!0;const r=S("正在更新 @roll-agent/core...").start(),n={command:"npm",args:["install","-g",`@roll-agent/core@${e}`]};try{return await A(n,{timeout:6e4}),r.succeed(`roll 已更新到 v${e}`),!0}catch(e){return r.fail("更新失败"),v.error(B(n,e)),!1}}async function x(e){const r=S(`更新 ${e.skill.name} (git pull)...`).start();try{await D("git",["pull"],{cwd:e.installPath,timeout:3e4}),r.succeed(`${e.skill.name} 代码已更新`);const n=a(e.installPath,"package.json");if(t(n)){const a=P(e.installPath);if(a){const t=S(`安装 ${e.skill.name} 依赖...`).start();try{await A(a,{cwd:e.installPath,timeout:6e4}),t.succeed(`${e.skill.name} 依赖已更新 (${a.command} ${a.args.join(" ")})`)}catch(r){return t.fail(`${e.skill.name} 依赖安装失败`),v.error(B(a,r)),!1}}else v.warn(`${e.skill.name} 未检测到 packageManager 或 lockfile,跳过依赖安装。`)}return!0}catch(a){return r.fail(`${e.skill.name} 更新失败`),v.error(a instanceof Error?a.message:String(a)),!1}}async function R(e){if("installed-package"!==e.source?.type)return;const a=S(`更新 ${e.skill.name} (npm install)...`).start(),r={command:"npm",args:["install","--prefix",e.source.installDir,e.source.packageSpec]};try{await A(r,{timeout:12e4});const n=w(e.source.installDir,e.source.packageName);if(!t(n))throw new Error(`Installed package root not found: ${n}`);const s=h(n);return a.succeed(`${e.skill.name} 已重新安装`),{packageRoot:n,packageName:s?.name??e.source.packageName,...s?.version?{installedVersion:s.version}:{}}}catch(t){return a.fail(`${e.skill.name} 更新失败`),void v.error(B(r,t))}}async function I(e){const a=S(`刷新 ${e.skill.name} (MCP tools/list)...`).start(),t=new y;try{const r=k(e),n=await t.connect(e.skill.name,r,e.installPath),{tools:s}=await n.listTools();return a.succeed(`${e.skill.name} 元数据已刷新(${s.length} 个 tool)`),!0}catch(t){return a.fail(`${e.skill.name} 刷新失败`),v.error(t instanceof Error?t.message:String(t)),!1}finally{await t.disconnectAll()}}export default e({meta:{description:"检查并更新 roll 及已注册 Agent"},args:{check:{type:"boolean",description:"仅检查 roll/Agent 可用更新,不执行安装或刷新",default:!1},"skip-browser-setup":{type:"boolean",description:"跳过 Playwright 浏览器运行时安装/校验",default:!1}},async run({args:e}){const a=e.check,t=s();v.info("检查 roll 更新...");const r=await V({forceRefresh:!0});let n,o;r.hasUpdate?v.success(`roll 有新版本: v${r.current} → v${r.latest}`):v.info(`roll 已是最新版本 (v${r.current})`);let l=[];try{n=i().agentsConfig,o=new g(n.dataDir),l=o.list()}catch(e){v.warn(`无法读取 Agent 配置,跳过已注册 Agent 检查:${e instanceof Error?e.message:String(e)}`)}const u=[];for(const e of l){let a=e;"installed-package"===$(a)&&(a=C(a,o));const t=$(a);switch(t){case"git":u.push({name:a.skill.name,sourceType:t,icon:"~",action:"可执行 git pull + 重新安装依赖"});break;case"installed-package":{if("installed-package"!==a.source?.type){u.push({name:a.skill.name,sourceType:t,icon:"?",action:"无法检查已安装包版本"});break}const e=await b({packageName:a.source.packageName,packageSpec:a.source.packageSpec,...a.source.installedVersion?{currentVersion:a.source.installedVersion}:{}},{forceRefresh:!0});u.push({name:a.skill.name,sourceType:t,icon:T(e.status),action:E(e,a.source.packageSpec)});break}case"remote-manifest":u.push({name:a.skill.name,sourceType:t,icon:"~",action:"可刷新本地 manifest + MCP 元数据"});break;case"local-path":u.push({name:a.skill.name,sourceType:t,icon:"⏭",action:"刷新本地 SKILL/manifest"})}}if(l.length>0){v.info(`\n已注册 Agent (${l.length}):`);for(const e of u)v.info(` ${e.icon} ${e.name} [${e.sourceType}] — ${e.action}`)}else n&&v.info("无已注册 Agent");if("needs-migration"!==t.status&&"invalid"!==t.status||(v.info(""),N(t,a?"check":"pre-update")),a)return;v.info("");let p=!1,k=!1;r.hasUpdate&&(p=await M(r.latest,!1),p||(k=!0)),!p||"needs-migration"!==t.status&&"invalid"!==t.status||(v.info(""),N(t,"post-update"));let h=0,w=0;for(const a of l){if(!o||!n)break;switch($(a)){case"git":{const e="core-managed"===a.runtime.ownership&&void 0!==c(n.dataDir,a.skill.name);if(await x(a))try{const t=f(a.installPath),r={...a,skill:t.skill,transport:t.transport,runtime:t.runtime,...t.skillBody.length>0?{skillBody:t.skillBody}:{}};o.replace(a.skill.name,r)?(await L(r,e,n.dataDir),h++):(v.warn(`${a.skill.name} 已从注册表中移除,跳过元数据刷新`),w++)}catch(e){o.updateStatus(a.skill.name,"error"),v.warn(`${a.skill.name} metadata 刷新或重启失败: ${e instanceof Error?e.message:String(e)}`),w++}else w++;break}case"installed-package":{const t="core-managed"===a.runtime.ownership&&void 0!==c(n.dataDir,a.skill.name);if(t)try{await m(n.dataDir,a.skill.name)}catch(e){o.updateStatus(a.skill.name,"error"),v.warn(`${a.skill.name} 停止失败,无法继续升级: ${e instanceof Error?e.message:String(e)}`),w++;break}const r=await R(a);if(r)try{const s=f(r.packageRoot),i="installed-package"===a.source?.type?{...a.source,packageName:r.packageName,...r.installedVersion?{installedVersion:r.installedVersion}:{}}:void 0,l={...a,skill:s.skill,transport:s.transport,runtime:s.runtime,installPath:r.packageRoot,...i?{source:i}:{},...s.skillBody.length>0?{skillBody:s.skillBody}:{}},c=await d(l,{skipBrowserSetup:e["skip-browser-setup"]});c.ok||(v.warn(`${l.skill.name} setup 失败:${c.message}`),c.retryCommand&&v.info(`重试命令: ${c.retryCommand}`));o.replace(a.skill.name,l)?c.ok?(t&&await U(l,n.dataDir),h++):(o.updateStatus(l.skill.name,"error"),w++):(v.warn(`${a.skill.name} 已从注册表中移除,跳过元数据刷新`),w++)}catch(e){o.updateStatus(a.skill.name,"error"),v.warn(`${a.skill.name} metadata 刷新、setup 或重启失败: ${e instanceof Error?e.message:String(e)}`),w++}else{if(t)try{await U(a,n.dataDir)}catch{o.updateStatus(a.skill.name,"error")}w++}break}case"remote-manifest":try{const e=f(a.installPath),t={...a,skill:e.skill,transport:e.transport,runtime:e.runtime,...e.skillBody.length>0?{skillBody:e.skillBody}:{}};if(!o.replace(a.skill.name,t)){v.warn(`${a.skill.name} 已从注册表中移除,跳过元数据刷新`),w++;break}await I(t)?h++:w++}catch(e){v.warn(`${a.skill.name} manifest 刷新失败: ${e instanceof Error?e.message:String(e)}`),w++}break;case"local-path":{const e="core-managed"===a.runtime.ownership&&void 0!==c(n.dataDir,a.skill.name);try{const t=f(a.installPath),r={...a,skill:t.skill,transport:t.transport,runtime:t.runtime,...t.skillBody.length>0?{skillBody:t.skillBody}:{}};o.replace(a.skill.name,r)?(await L(r,e,n.dataDir),h++):(v.warn(`${a.skill.name} 已从注册表中移除,跳过元数据刷新`),w++)}catch(e){o.updateStatus(a.skill.name,"error"),v.warn(`${a.skill.name} 本地 metadata 刷新或重启失败: ${e instanceof Error?e.message:String(e)}`),w++}break}}}v.info("");if(w+(k?1:0)>0){process.exitCode=1;const e=k?"roll 更新失败":p?"roll ✓":"roll 无更新";return void v.warn(`更新完成但有失败:${e}${w>0?`,${w} 个 Agent 更新失败`:""}${h>0?`,${h} 个 Agent 已更新`:""}`)}p||h>0?v.success(`更新完成:${p?"roll ✓":"roll 无更新"}${h>0?`,${h} 个 Agent 已更新`:""}`):v.success("一切都已是最新版本")}});async function L(e,a,t){a&&"core-managed"===e.runtime.ownership&&(await m(t,e.skill.name),await U(e,t))}async function U(e,a){let t=!1;try{u(e,a,l(o().config,e.skill.name)),t=!0,await p(e,{startupTimeoutMs:15e3,probeTimeoutMs:2e3})}catch(r){throw t&&await m(a,e.skill.name).catch(()=>{}),r}}
1
+ import{defineCommand as e}from"citty";import{resolve as t}from"node:path";import{existsSync as r}from"node:fs";import{execFile as a}from"node:child_process";import{promisify as s}from"node:util";import{inspectConfigFile as n,loadAgentsConfig as i,loadConfig as o,loadInstallConfig as l}from"../../config/loader.js";import{DEFAULT_CONFIG as c}from"../../config/defaults.js";import{getAgentEnv as u}from"../../config/helpers.js";import{getAgentPid as m,startAgent as p,stopAgentGracefully as f,waitForAgentReady as g}from"../../registry/process-manager.js";import{resolveTransportWithDevSpawnSpec as k}from"../../registry/dev-spawn.js";import{runAgentSetup as d}from"../../registry/runtime-setup.js";import{AgentStore as $}from"../../registry/store.js";import{discoverAgent as y}from"../../registry/discovery.js";import{inferAgentSourceType as h,readInstalledPackageManifest as w,resolveInstalledPackageRoot as v}from"../../registry/source.js";import{McpClientManager as S}from"../../mcp/client-manager.js";import{log as b,createSpinner as V}from"../utils/output.js";import{checkForUpdate as j,checkPublishedPackageUpdate as P,getCurrentVersion as A,isPinnedPublishedPackageSpec as M}from"../utils/update-checker.js";import{buildNpmRetryPolicy as B,detectInstallCommand as T,formatPackageManagerError as D,npmInstallNetworkArgs as N,runPackageManager as R,runPackageManagerWithRetry as E}from"../utils/package-manager.js";function x(e){return N({...e.registry?{registry:e.registry}:{},fetchRetries:e.fetchRetries,preferOffline:e.preferOffline})}function C(e){return{forceRefresh:!0,fetchRetries:e.fetchRetries,...e.registry?{registry:e.registry}:{}}}function I(e){return"invalid"===e.status&&(e.error.message.startsWith("Invalid YAML syntax")||e.error.message.includes("(expected YAML object)"))}const L=s(a);export{inferAgentSourceType as inferSourceType}from"../../registry/source.js";export{detectInstallCommand}from"../utils/package-manager.js";function O(e,t){switch(e.status){case"needs-migration":{const r="post-update"===t?"升级后需要迁移本地配置":"检测到本地配置需要迁移";b.warn(`${r}: ${e.configPath}`);for(const t of e.report.issues)b.warn(` - ${t.message}`);e.report.canAutoMigrate&&b.info("建议命令: roll config migrate");break}case"invalid":b.warn(`本地配置存在问题: ${e.configPath??"(unknown path)"}`),b.warn(` - ${e.error.message}`),"post-update"===t&&b.info("请修复配置文件后再继续使用相关命令。")}}function U(e){switch(e){case"up-to-date":return"✅";case"update-available":return"⬆";case"pinned-behind":return"📌";case"unsupported-spec":case"unknown":return"?"}}function W(e,t){switch(e.status){case"up-to-date":return e.currentVersion?`已是最新版本 (v${e.currentVersion})`:"已是最新版本";case"update-available":return e.currentVersion&&e.latestVersion?`可更新 v${e.currentVersion} → v${e.latestVersion}`:"检测到可用更新";case"pinned-behind":return e.currentVersion&&e.latestVersion?`固定版本 v${e.currentVersion};latest=v${e.latestVersion}`:"固定版本,需手动调整 package spec";case"unsupported-spec":return`不支持检查此 package spec: ${t}`;case"unknown":return e.currentVersion?`无法检查最新版本 (current=v${e.currentVersion})`:"无法检查最新版本"}}function Y(e,t){if("installed-package"!==e.source?.type)return e;const r=v(e.source.installDir,e.source.packageName),a=w(r);if(!a)return e;const s={...e.source,...a.name?{packageName:a.name}:{},...a.version?{installedVersion:a.version}:{}},n=s.packageName!==e.source.packageName||s.installedVersion!==e.source.installedVersion,i=r!==e.installPath;if(!n&&!i)return e;const o={...e,installPath:r,source:s};return t&&t.replace(e.skill.name,o),o}function K(e){if("installed-package"!==e.source?.type)return;const{packageName:t,packageSpec:r}=e.source;return r===t||r.startsWith(`${t}@`)&&!M(t,r)?`${t}@latest`:r}async function _(e,t,r){const a=A();if(a===e)return b.info(`roll 已是最新版本 (v${a})`),!1;if(b.info(`roll v${a} → v${e}`),t)return b.info("[dry-run] 跳过实际更新"),!0;const s=V("正在更新 @roll-agent/core...").start(),n={command:"npm",args:["install","-g",`@roll-agent/core@${e}`,...x(r)]};try{return await E(n,{timeout:r.networkTimeoutMs},{...B(r.fetchRetries),onRetry:({attempt:e,delayMs:t})=>{s.text=`roll 更新遇到网络问题,${Math.round(t/1e3)}s 后重试(第 ${e+1} 次)...`}}),s.succeed(`roll 已更新到 v${e}`),!0}catch(e){return s.fail("更新失败"),b.error(D(n,e)),!1}}async function q(e){const a=V(`更新 ${e.skill.name} (git pull)...`).start();try{await L("git",["pull"],{cwd:e.installPath,timeout:3e4}),a.succeed(`${e.skill.name} 代码已更新`);const s=t(e.installPath,"package.json");if(r(s)){const t=T(e.installPath);if(t){const r=V(`安装 ${e.skill.name} 依赖...`).start();try{await R(t,{cwd:e.installPath,timeout:6e4}),r.succeed(`${e.skill.name} 依赖已更新 (${t.command} ${t.args.join(" ")})`)}catch(a){return r.fail(`${e.skill.name} 依赖安装失败`),b.error(D(t,a)),!1}}else b.warn(`${e.skill.name} 未检测到 packageManager 或 lockfile,跳过依赖安装。`)}return!0}catch(t){return a.fail(`${e.skill.name} 更新失败`),b.error(t instanceof Error?t.message:String(t)),!1}}async function z(e,t){if("installed-package"!==e.source?.type)return;const a=V(`更新 ${e.skill.name} (npm install)...`).start(),s=K(e);if(!s)return void a.fail(`${e.skill.name} 更新失败`);const n={command:"npm",args:["install","--prefix",e.source.installDir,s,...x(t)]};try{await E(n,{timeout:t.networkTimeoutMs},{...B(t.fetchRetries),onRetry:({attempt:t,delayMs:r})=>{a.text=`更新 ${e.skill.name} 遇到网络问题,${Math.round(r/1e3)}s 后重试(第 ${t+1} 次)...`}});const s=v(e.source.installDir,e.source.packageName);if(!r(s))throw new Error(`Installed package root not found: ${s}`);const i=w(s);return a.succeed(`${e.skill.name} 已重新安装`),{packageRoot:s,packageName:i?.name??e.source.packageName,...i?.version?{installedVersion:i.version}:{}}}catch(t){return a.fail(`${e.skill.name} 更新失败`),void b.error(D(n,t))}}async function F(e){const t=V(`刷新 ${e.skill.name} (MCP tools/list)...`).start(),r=new S;try{const a=k(e),s=await r.connect(e.skill.name,a,e.installPath),{tools:n}=await s.listTools();return t.succeed(`${e.skill.name} 元数据已刷新(${n.length} 个 tool)`),!0}catch(r){return t.fail(`${e.skill.name} 刷新失败`),b.error(r instanceof Error?r.message:String(r)),!1}finally{await r.disconnectAll()}}export default e({meta:{description:"检查并更新 roll 及已注册 Agent"},args:{check:{type:"boolean",description:"仅检查 roll/Agent 可用更新,不执行安装或刷新",default:!1},"skip-browser-setup":{type:"boolean",description:"跳过 Playwright 浏览器运行时安装/校验",default:!1}},async run({args:e}){const t=e.check,r=n();let a;try{a=l().installConfig}catch(e){if(!I(r))return b.error(`install 配置无效,已停止更新:${e instanceof Error?e.message:String(e)}`),void(process.exitCode=1);a=c.install,b.warn(`无法读取 install 配置,使用默认值:${e instanceof Error?e.message:String(e)}`)}a.registry&&b.info(`使用 npm registry: ${a.registry}(roll.config.yaml install.registry)`);const s=C(a);b.info("检查 roll 更新...");const o=await j(s);let u,p;o.hasUpdate?b.success(`roll 有新版本: v${o.current} → v${o.latest}`):b.info(`roll 已是最新版本 (v${o.current})`);let g=[];try{u=i().agentsConfig,p=new $(u.dataDir),g=p.list()}catch(e){b.warn(`无法读取 Agent 配置,跳过已注册 Agent 检查:${e instanceof Error?e.message:String(e)}`)}const k=[];for(const e of g){let t=e;"installed-package"===h(t)&&(t=Y(t,p));const r=h(t);switch(r){case"git":k.push({name:t.skill.name,sourceType:r,icon:"~",action:"可执行 git pull + 重新安装依赖"});break;case"installed-package":{if("installed-package"!==t.source?.type){k.push({name:t.skill.name,sourceType:r,icon:"?",action:"无法检查已安装包版本"});break}const e=await P({packageName:t.source.packageName,packageSpec:t.source.packageSpec,...t.source.installedVersion?{currentVersion:t.source.installedVersion}:{}},s);k.push({name:t.skill.name,sourceType:r,icon:U(e.status),action:W(e,t.source.packageSpec)});break}case"remote-manifest":k.push({name:t.skill.name,sourceType:r,icon:"~",action:"可刷新本地 manifest + MCP 元数据"});break;case"local-path":k.push({name:t.skill.name,sourceType:r,icon:"⏭",action:"刷新本地 SKILL/manifest"})}}if(g.length>0){b.info(`\n已注册 Agent (${g.length}):`);for(const e of k)b.info(` ${e.icon} ${e.name} [${e.sourceType}] — ${e.action}`)}else u&&b.info("无已注册 Agent");if("needs-migration"!==r.status&&"invalid"!==r.status||(b.info(""),O(r,t?"check":"pre-update")),t)return;b.info("");let w=!1,v=!1;o.hasUpdate&&(w=await _(o.latest,!1,a),w||(v=!0)),!w||"needs-migration"!==r.status&&"invalid"!==r.status||(b.info(""),O(r,"post-update"));let S=0,V=0;for(const t of g){if(!p||!u)break;switch(h(t)){case"git":{const e="core-managed"===t.runtime.ownership&&void 0!==m(u.dataDir,t.skill.name);if(await q(t))try{const r=y(t.installPath),a={...t,skill:r.skill,transport:r.transport,runtime:r.runtime,...r.skillBody.length>0?{skillBody:r.skillBody}:{}};p.replace(t.skill.name,a)?(await G(a,e,u.dataDir),S++):(b.warn(`${t.skill.name} 已从注册表中移除,跳过元数据刷新`),V++)}catch(e){p.updateStatus(t.skill.name,"error"),b.warn(`${t.skill.name} metadata 刷新或重启失败: ${e instanceof Error?e.message:String(e)}`),V++}else V++;break}case"installed-package":{const r="core-managed"===t.runtime.ownership&&void 0!==m(u.dataDir,t.skill.name);if(r)try{await f(u.dataDir,t.skill.name)}catch(e){p.updateStatus(t.skill.name,"error"),b.warn(`${t.skill.name} 停止失败,无法继续升级: ${e instanceof Error?e.message:String(e)}`),V++;break}const s=await z(t,a);if(s)try{const a=y(s.packageRoot),n="installed-package"===t.source?.type?{...t.source,packageName:s.packageName,...s.installedVersion?{installedVersion:s.installedVersion}:{}}:void 0,i={...t,skill:a.skill,transport:a.transport,runtime:a.runtime,installPath:s.packageRoot,...n?{source:n}:{},...a.skillBody.length>0?{skillBody:a.skillBody}:{}},o=await d(i,{skipBrowserSetup:e["skip-browser-setup"]});o.ok||(b.warn(`${i.skill.name} setup 失败:${o.message}`),o.retryCommand&&b.info(`重试命令: ${o.retryCommand}`));p.replace(t.skill.name,i)?o.ok?(r&&await H(i,u.dataDir),S++):(p.updateStatus(i.skill.name,"error"),V++):(b.warn(`${t.skill.name} 已从注册表中移除,跳过元数据刷新`),V++)}catch(e){p.updateStatus(t.skill.name,"error"),b.warn(`${t.skill.name} metadata 刷新、setup 或重启失败: ${e instanceof Error?e.message:String(e)}`),V++}else{if(r)try{await H(t,u.dataDir)}catch{p.updateStatus(t.skill.name,"error")}V++}break}case"remote-manifest":try{const e=y(t.installPath),r={...t,skill:e.skill,transport:e.transport,runtime:e.runtime,...e.skillBody.length>0?{skillBody:e.skillBody}:{}};if(!p.replace(t.skill.name,r)){b.warn(`${t.skill.name} 已从注册表中移除,跳过元数据刷新`),V++;break}await F(r)?S++:V++}catch(e){b.warn(`${t.skill.name} manifest 刷新失败: ${e instanceof Error?e.message:String(e)}`),V++}break;case"local-path":{const e="core-managed"===t.runtime.ownership&&void 0!==m(u.dataDir,t.skill.name);try{const r=y(t.installPath),a={...t,skill:r.skill,transport:r.transport,runtime:r.runtime,...r.skillBody.length>0?{skillBody:r.skillBody}:{}};p.replace(t.skill.name,a)?(await G(a,e,u.dataDir),S++):(b.warn(`${t.skill.name} 已从注册表中移除,跳过元数据刷新`),V++)}catch(e){p.updateStatus(t.skill.name,"error"),b.warn(`${t.skill.name} 本地 metadata 刷新或重启失败: ${e instanceof Error?e.message:String(e)}`),V++}break}}}b.info("");if(V+(v?1:0)>0){process.exitCode=1;const e=v?"roll 更新失败":w?"roll ✓":"roll 无更新";return void b.warn(`更新完成但有失败:${e}${V>0?`,${V} 个 Agent 更新失败`:""}${S>0?`,${S} 个 Agent 已更新`:""}`)}w||S>0?b.success(`更新完成:${w?"roll ✓":"roll 无更新"}${S>0?`,${S} 个 Agent 已更新`:""}`):b.success("一切都已是最新版本")}});async function G(e,t,r){t&&"core-managed"===e.runtime.ownership&&(await f(r,e.skill.name),await H(e,r))}async function H(e,t){let r=!1;try{p(e,t,u(o().config,e.skill.name)),r=!0,await g(e,{startupTimeoutMs:15e3,probeTimeoutMs:2e3})}catch(a){throw r&&await f(t,e.skill.name).catch(()=>{}),a}}
@@ -24,7 +24,56 @@ export interface PackageManagerExecInvocation {
24
24
  export declare function detectInstallCommand(projectDir: string): InstallCommandSpec | undefined;
25
25
  export declare function createInstallCommand(command?: PackageManagerCommandName): InstallCommandSpec;
26
26
  export declare function runPackageManager(spec: PackageManagerRunSpec, options?: PackageManagerRunOptions): Promise<PackageManagerRunResult>;
27
+ export interface PackageManagerRetryPolicy {
28
+ /** 整体命令的最大尝试次数(>=1)。 */
29
+ readonly attempts: number;
30
+ /** 每次重试前的等待时长(毫秒),长度不足时复用最后一项。 */
31
+ readonly backoffMs?: readonly number[];
32
+ /** 判定某次失败是否值得重试,默认仅对网络/超时类错误重试。 */
33
+ readonly isRetryable?: (error: unknown) => boolean;
34
+ /** 每次重试前回调(用于日志)。 */
35
+ readonly onRetry?: (info: {
36
+ attempt: number;
37
+ error: unknown;
38
+ delayMs: number;
39
+ }) => void;
40
+ }
41
+ /**
42
+ * 在 {@link runPackageManager} 之上叠加“仅对网络/超时类错误重试”的能力。
43
+ *
44
+ * 与 npm 自身的 `--fetch-retries`(针对单个 tarball 抓取)互补:
45
+ * 这里重试的是整条命令,覆盖 registry 完全不可达、超时被 kill 等硬失败场景。
46
+ */
47
+ export declare function runPackageManagerWithRetry(spec: PackageManagerRunSpec, options: PackageManagerRunOptions | undefined, policy: PackageManagerRetryPolicy): Promise<PackageManagerRunResult>;
48
+ /**
49
+ * 构造一个 npm 安装重试策略。
50
+ *
51
+ * `fetchRetries` 同时用于 npm 的 `--fetch-retries` 与 roll 层整体重试:
52
+ * 整体尝试次数 = `min(fetchRetries + 1, 3)`,避免弱网下重试时间无界增长。
53
+ */
54
+ export declare function buildNpmRetryPolicy(fetchRetries: number): PackageManagerRetryPolicy;
27
55
  export declare function createPackageManagerExecInvocation(spec: PackageManagerRunSpec, platform?: NodeJS.Platform): PackageManagerExecInvocation;
28
56
  export declare function shouldRunPackageManagerViaShell(platform: NodeJS.Platform): boolean;
57
+ export interface NpmNetworkOptions {
58
+ /** 显式 registry(镜像源)。未提供时沿用 npm 默认源。 */
59
+ readonly registry?: string;
60
+ /** 透传给 npm 的 `--fetch-retries`。 */
61
+ readonly fetchRetries?: number;
62
+ /** 是否附加 `--prefer-offline`。 */
63
+ readonly preferOffline?: boolean;
64
+ }
65
+ /**
66
+ * 构造 `npm install` 的网络韧性参数。
67
+ *
68
+ * 始终附加 `--no-audit --no-fund`(减少额外网络请求、提速),并按需附加
69
+ * `--registry` / `--fetch-retries` / `--prefer-offline`。
70
+ */
71
+ export declare function npmInstallNetworkArgs(options?: NpmNetworkOptions): string[];
72
+ /** 构造 `npm view` 的网络参数(只需 registry + fetch-retries)。 */
73
+ export declare function npmViewNetworkArgs(options?: NpmNetworkOptions): string[];
29
74
  export declare function formatPackageManagerCommand(spec: PackageManagerRunSpec): string;
75
+ /** 判断错误是否疑似网络/镜像源问题。 */
76
+ export declare function isLikelyNetworkError(error: unknown): boolean;
77
+ /** 网络/超时类错误才值得对整条命令重试。 */
78
+ export declare function isRetryablePackageManagerError(error: unknown): boolean;
30
79
  export declare function formatPackageManagerError(spec: PackageManagerRunSpec, error: unknown, platform?: NodeJS.Platform): string;
@@ -1 +1 @@
1
- import{execFile as n}from"node:child_process";import{existsSync as r,readFileSync as e}from"node:fs";import{resolve as o}from"node:path";import{promisify as t}from"node:util";const a=t(n);export const PACKAGE_MANAGER_COMMANDS=["bun","npm","pnpm","yarn"];export function detectInstallCommand(n){const e=o(n,"package.json");if(r(e)){const n=c(e);if(n)return{command:n,args:["install"]}}const t=[["pnpm-lock.yaml","pnpm"],["package-lock.json","npm"],["npm-shrinkwrap.json","npm"],["yarn.lock","yarn"],["bun.lock","bun"],["bun.lockb","bun"]];for(const[e,a]of t)if(r(o(n,e)))return{command:a,args:["install"]}}export function createInstallCommand(n="pnpm"){return{command:n,args:["install"]}}export async function runPackageManager(n,r={}){const{platform:e,...o}=r,t=createPackageManagerExecInvocation(n,e??process.platform),c=await a(t.file,[...t.args],{...o,encoding:"utf-8",...t.shell?{shell:t.shell}:{}});return{stdout:c.stdout,stderr:c.stderr}}export function createPackageManagerExecInvocation(n,r=process.platform){return shouldRunPackageManagerViaShell(r)?{file:"cmd.exe",args:["/d","/s","/c",u(n)],shell:!1}:{file:n.command,args:n.args,shell:!1}}export function shouldRunPackageManagerViaShell(n){return"win32"===n}export function formatPackageManagerCommand(n){return[n.command,...n.args].map(i).join(" ")}export function formatPackageManagerError(n,r,e=process.platform){const o=formatPackageManagerCommand(n);return p(n.command,r,e)?[`未找到 ${n.command},无法执行 \`${o}\`。`,"win32"===e?`请确认 ${n.command}.cmd 位于 PATH 中;npm 全局目录通常是 C:\\Users\\<you>\\AppData\\Roaming\\npm。`:`请确认已安装 ${n.command},并且当前 shell 的 PATH 可以访问它。`].join(""):r instanceof Error?r.message:String(r)}function c(n){try{const r=JSON.parse(e(n,"utf-8"));if("string"!=typeof r.packageManager||0===r.packageManager.length)return;const[o]=r.packageManager.split("@",1);if(!o)return;return s(o)?o:void 0}catch{return}}function s(n){return PACKAGE_MANAGER_COMMANDS.some(r=>r===n)}function i(n){return/\s/.test(n)?JSON.stringify(n):n}function u(n){return[n.command,...n.args].map(m).join(" ")}function m(n){if(0===n.length)return'""';if(/["\r\n]/.test(n))throw new Error(`Unsupported Windows shell argument: ${n}`);return/[\s"&|<>^()%!]/.test(n)?`"${l(n)}"`:n}function l(n){return n.replace(/[\^&|<>%!]/g,n=>`^${n}`)}function p(n,r,e){if("ENOENT"===f(r))return!0;if("win32"!==e)return!1;const o=g(r).toLowerCase();return o.includes(`'${n}' is not recognized`)||o.includes(`"${n}" is not recognized`)||o.includes(`${n} is not recognized`)||o.includes(`'${n}' 不是内部或外部命令`)||o.includes(`"${n}" 不是内部或外部命令`)||o.includes(`${n} 不是内部或外部命令`)}function f(n){if("object"==typeof n&&null!==n&&"code"in n)return"string"==typeof n.code?n.code:void 0}function g(n){return n instanceof Error?n.message:String(n)}
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)}
@@ -3,6 +3,12 @@ export type PublishedPackageUpdateStatus = (typeof PUBLISHED_PACKAGE_UPDATE_STAT
3
3
  interface PackageVersionQueryOptions {
4
4
  readonly forceRefresh?: boolean;
5
5
  readonly allowNetwork?: boolean;
6
+ /** 显式 registry(镜像源),透传给 `npm view`。 */
7
+ readonly registry?: string;
8
+ /** 透传给 npm 的 `--fetch-retries`。 */
9
+ readonly fetchRetries?: number;
10
+ /** `npm view` 单次超时(毫秒),默认 10s。慢网下仍有缓存兜底。 */
11
+ readonly timeoutMs?: number;
6
12
  }
7
13
  export interface PublishedPackageUpdateInfo {
8
14
  readonly packageName: string;
@@ -18,6 +24,7 @@ export interface UpdateInfo {
18
24
  export declare function fetchLatestPublishedVersion(packageName: string, options?: PackageVersionQueryOptions): Promise<string | undefined>;
19
25
  export declare function getCurrentVersion(): string;
20
26
  export declare function isNewerVersion(latest: string, current: string): boolean;
27
+ export declare function isPinnedPublishedPackageSpec(packageName: string, packageSpec: string): boolean;
21
28
  export declare function checkPublishedPackageUpdate(input: {
22
29
  packageName: string;
23
30
  packageSpec: string;
@@ -1 +1 @@
1
- import{readFileSync as t,writeFileSync as e,mkdirSync as n,existsSync as r}from"node:fs";import{resolve as s,dirname as a}from"node:path";import{homedir as o}from"node:os";import{runPackageManager as c}from"./package-manager.js";const i=864e5,u="@roll-agent/core";export const PUBLISHED_PACKAGE_UPDATE_STATUSES=["up-to-date","update-available","pinned-behind","unsupported-spec","unknown"];function p(){return s(o(),".roll-agent","update-check.json")}function f(t){return"object"==typeof t&&null!==t&&("latestVersion"in t&&"checkedAt"in t&&("string"==typeof t.latestVersion&&t.latestVersion.length>0&&"number"==typeof t.checkedAt))}function l(t){return"object"==typeof t&&null!==t&&"packages"in t&&("object"==typeof t.packages&&null!==t.packages&&Object.values(t.packages).every(t=>f(t)))}function h(t){return f(t)}function d(){const e=p();if(r(e))try{const n=t(e,"utf-8"),r=JSON.parse(n);if(l(r))return r;if(h(r))return{packages:{[u]:{latestVersion:r.latestVersion,checkedAt:r.checkedAt}}}}catch{}}function g(t){try{const s=p(),o=a(s);r(o)||n(o,{recursive:!0}),e(s,JSON.stringify(t),"utf-8")}catch{}}function k(t,e){const n=d();g({packages:{...n?.packages??{},[t]:{latestVersion:e,checkedAt:Date.now()}}})}function m(t){return d()?.packages[t]}async function V(t){try{const{stdout:e}=await c({command:"npm",args:["view",t,"version","--json"]},{timeout:5e3}),n=e.trim();if(0===n.length)return;const r=JSON.parse(n);return"string"==typeof r&&r.length>0?r:void 0}catch{return}}export async function fetchLatestPublishedVersion(t,e={}){const n=e.forceRefresh??!1,r=e.allowNetwork??!0,s=m(t);if(!n&&s&&Date.now()-s.checkedAt<i)return s.latestVersion;if(!r)return s?.latestVersion;const a=await V(t);return a?(k(t,a),a):s?.latestVersion}export function getCurrentVersion(){try{const e=s(import.meta.dirname,"../../../package.json"),n=t(e,"utf-8"),r=JSON.parse(n);if("object"==typeof r&&null!==r&&"version"in r){const t=r.version;if("string"==typeof t)return t}}catch{}return"0.0.0"}export function isNewerVersion(t,e){const n=t=>t.replace(/^v/,"").split(".").map(Number),r=n(t),s=n(e);for(let t=0;t<3;t+=1){const e=r[t]??0,n=s[t]??0;if(e>n)return!0;if(e<n)return!1}return!1}function y(t,e){return!["file:","git+","http://","https://","link:","workspace:","npm:"].some(t=>e.startsWith(t))&&(!(e.startsWith(".")||e.startsWith("/")||e.startsWith("~")||e.endsWith(".tgz")||e.endsWith(".tar.gz"))&&(e===t||e.startsWith(`${t}@`)))}function w(t,e){if(e.startsWith(`${t}@`))return e.slice(t.length+1)}function N(t,e){const n=w(t,e);if(!n)return!1;const r=n.replace(/^v/,"");return/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/.test(r)}export async function checkPublishedPackageUpdate(t,e={}){const{packageName:n,packageSpec:r,currentVersion:s}=t;if(!y(n,r))return{packageName:n,...s?{currentVersion:s}:{},status:"unsupported-spec"};if(!s)return{packageName:n,status:"unknown"};const a=await fetchLatestPublishedVersion(n,e);return a?isNewerVersion(a,s)?N(n,r)?{packageName:n,currentVersion:s,latestVersion:a,status:"pinned-behind"}:{packageName:n,currentVersion:s,latestVersion:a,status:"update-available"}:{packageName:n,currentVersion:s,latestVersion:a,status:"up-to-date"}:{packageName:n,currentVersion:s,status:"unknown"}}export async function checkForUpdate(t={}){const e=getCurrentVersion(),n=await fetchLatestPublishedVersion(u,t)??e;return{current:e,latest:n,hasUpdate:isNewerVersion(n,e)}}
1
+ import{readFileSync as e,writeFileSync as t,mkdirSync as n,existsSync as r}from"node:fs";import{resolve as s,dirname as a}from"node:path";import{homedir as o}from"node:os";import{npmViewNetworkArgs as i,runPackageManager as c}from"./package-manager.js";const u=864e5,p="@roll-agent/core",f=1e4;export const PUBLISHED_PACKAGE_UPDATE_STATUSES=["up-to-date","update-available","pinned-behind","unsupported-spec","unknown"];function l(){return s(o(),".roll-agent","update-check.json")}function h(e){return"object"==typeof e&&null!==e&&("latestVersion"in e&&"checkedAt"in e&&("string"==typeof e.latestVersion&&e.latestVersion.length>0&&"number"==typeof e.checkedAt))}function d(e){return"object"==typeof e&&null!==e&&"packages"in e&&("object"==typeof e.packages&&null!==e.packages&&Object.values(e.packages).every(e=>h(e)))}function g(e){return h(e)}function k(){const t=l();if(r(t))try{const n=e(t,"utf-8"),r=JSON.parse(n);if(d(r))return r;if(g(r))return{packages:{[p]:{latestVersion:r.latestVersion,checkedAt:r.checkedAt}}}}catch{}}function m(e){try{const s=l(),o=a(s);r(o)||n(o,{recursive:!0}),t(s,JSON.stringify(e),"utf-8")}catch{}}function V(e,t){const n=k();m({packages:{...n?.packages??{},[e]:{latestVersion:t,checkedAt:Date.now()}}})}function y(e){return k()?.packages[e]}async function b(e,t={}){try{const n=i({...t.registry?{registry:t.registry}:{},...void 0!==t.fetchRetries?{fetchRetries:t.fetchRetries}:{}}),{stdout:r}=await c({command:"npm",args:["view",e,"version","--json",...n]},{timeout:t.timeoutMs??f}),s=r.trim();if(0===s.length)return;const a=JSON.parse(s);return"string"==typeof a&&a.length>0?a:void 0}catch{return}}export async function fetchLatestPublishedVersion(e,t={}){const n=t.forceRefresh??!1,r=t.allowNetwork??!0,s=y(e);if(!n&&s&&Date.now()-s.checkedAt<u)return s.latestVersion;if(!r)return s?.latestVersion;const a=await b(e,t);return a?(V(e,a),a):s?.latestVersion}export function getCurrentVersion(){try{const t=s(import.meta.dirname,"../../../package.json"),n=e(t,"utf-8"),r=JSON.parse(n);if("object"==typeof r&&null!==r&&"version"in r){const e=r.version;if("string"==typeof e)return e}}catch{}return"0.0.0"}export function isNewerVersion(e,t){const n=e=>e.replace(/^v/,"").split(".").map(Number),r=n(e),s=n(t);for(let e=0;e<3;e+=1){const t=r[e]??0,n=s[e]??0;if(t>n)return!0;if(t<n)return!1}return!1}function w(e,t){return!["file:","git+","http://","https://","link:","workspace:","npm:"].some(e=>t.startsWith(e))&&(!(t.startsWith(".")||t.startsWith("/")||t.startsWith("~")||t.endsWith(".tgz")||t.endsWith(".tar.gz"))&&(t===e||t.startsWith(`${e}@`)))}function N(e,t){if(t.startsWith(`${e}@`))return t.slice(e.length+1)}export function isPinnedPublishedPackageSpec(e,t){const n=N(e,t);if(!n)return!1;const r=n.replace(/^v/,"");return/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/.test(r)}export async function checkPublishedPackageUpdate(e,t={}){const{packageName:n,packageSpec:r,currentVersion:s}=e;if(!w(n,r))return{packageName:n,...s?{currentVersion:s}:{},status:"unsupported-spec"};if(!s)return{packageName:n,status:"unknown"};const a=await fetchLatestPublishedVersion(n,t);return a?isNewerVersion(a,s)?isPinnedPublishedPackageSpec(n,r)?{packageName:n,currentVersion:s,latestVersion:a,status:"pinned-behind"}:{packageName:n,currentVersion:s,latestVersion:a,status:"update-available"}:{packageName:n,currentVersion:s,latestVersion:a,status:"up-to-date"}:{packageName:n,currentVersion:s,status:"unknown"}}export async function checkForUpdate(e={}){const t=getCurrentVersion(),n=await fetchLatestPublishedVersion(p,e)??t;return{current:t,latest:n,hasUpdate:isNewerVersion(n,t)}}
@@ -1,4 +1,13 @@
1
1
  import type { RollConfig } from "./schema.ts";
2
+ export declare const LLM_PROVIDER_OPTIONS: readonly ["anthropic", "openai", "qwen", "deepseek"];
3
+ export type LlmProviderOption = (typeof LLM_PROVIDER_OPTIONS)[number];
4
+ export declare const DEFAULT_LLM_PROVIDER: LlmProviderOption;
5
+ export declare const DEFAULT_LLM_MODELS: {
6
+ readonly anthropic: "claude-sonnet-4-6";
7
+ readonly openai: "gpt-5.5";
8
+ readonly qwen: "qwen3.6-plus";
9
+ readonly deepseek: "deepseek-v4-flash";
10
+ };
2
11
  /** 默认配置值 */
3
12
  export declare const DEFAULT_CONFIG: RollConfig;
4
13
  /** 配置文件查找顺序 */
@@ -1 +1 @@
1
- export const DEFAULT_CONFIG={llm:{defaultProvider:"anthropic",defaultModel:"claude-sonnet-4-20250514",providers:{}},ask:{},agents:{dataDir:"~/.roll-agent/agents"},browser:{instances:{}}};export const CONFIG_FILE_NAMES=["roll.config.yaml","roll.config.yml"];
1
+ export const LLM_PROVIDER_OPTIONS=["anthropic","openai","qwen","deepseek"];export const DEFAULT_LLM_PROVIDER="anthropic";export const DEFAULT_LLM_MODELS={anthropic:"claude-sonnet-4-6",openai:"gpt-5.5",qwen:"qwen3.6-plus",deepseek:"deepseek-v4-flash"};export const DEFAULT_CONFIG={llm:{defaultProvider:"anthropic",defaultModel:DEFAULT_LLM_MODELS.anthropic,providers:{}},ask:{},agents:{dataDir:"~/.roll-agent/agents"},install:{fetchRetries:3,preferOffline:!1,networkTimeoutMs:12e4},browser:{instances:{}}};export const CONFIG_FILE_NAMES=["roll.config.yaml","roll.config.yml"];
@@ -1 +1 @@
1
- const e={kind:"leaf"},o={kind:"object",fields:{apiKey:e,baseUrl:e}},r={kind:"object",fields:{platform:e,mode:e,headless:e,cdpUrl:e,cdpHost:e,cdpPort:e,channel:e,executablePath:e,userDataDir:e,sessionsDir:e,args:e,profileName:e,profileColor:e,windowBounds:{kind:"object",fields:{x:e,y:e,width:e,height:e}},trackingAgentId:e}};export const CONFIG_KEY_CODEC={kind:"object",fields:{llm:{kind:"object",fields:{defaultProvider:e,defaultModel:e,providers:{kind:"record",value:o}}},ask:{kind:"object",fields:{llmModel:e,confirmThreshold:e}},agents:{kind:"object",fields:{dataDir:e,env:{kind:"record",value:{kind:"record",value:e}}}},browser:{kind:"object",fields:{defaultInstance:e,instances:{kind:"record",value:r}}}}};export function kebabToCamel(e){return e.replace(/-([a-z])/g,(e,o)=>o.toUpperCase())}export function camelToKebab(e){return e.replace(/[A-Z]/g,e=>`-${e.toLowerCase()}`)}function t(e){return"object"==typeof e&&null!==e&&!Array.isArray(e)}export function decodeFromYaml(o,r=CONFIG_KEY_CODEC){if(Array.isArray(o))return o.map(e=>decodeFromYaml(e,r));if(!t(o))return o;if("object"===r.kind){const t={};for(const[n,d]of Object.entries(o)){const o=kebabToCamel(n),a=r.fields[o]??e;t[o]=decodeFromYaml(d,a)}return t}if("record"===r.kind){const e={};for(const[t,n]of Object.entries(o))e[t]=decodeFromYaml(n,r.value);return e}return o}function n(o,r){const t=[];let n=CONFIG_KEY_CODEC;for(const d of o)if("object"===n.kind){const o=kebabToCamel(d),a=n.fields[o];a?(t.push("camel"===r?o:camelToKebab(o)),n=a):(t.push(d),n=e)}else"record"===n.kind?(t.push(d),n=n.value):t.push(d);return t}export function encodePathToYaml(e){return n(e,"kebab")}export function normalizeUserPath(e){return n(e,"camel")}
1
+ const e={kind:"leaf"},r={kind:"object",fields:{apiKey:e,baseUrl:e}},o={kind:"object",fields:{platform:e,mode:e,headless:e,cdpUrl:e,cdpHost:e,cdpPort:e,channel:e,executablePath:e,userDataDir:e,sessionsDir:e,args:e,profileName:e,profileColor:e,windowBounds:{kind:"object",fields:{x:e,y:e,width:e,height:e}},trackingAgentId:e}};export const CONFIG_KEY_CODEC={kind:"object",fields:{llm:{kind:"object",fields:{defaultProvider:e,defaultModel:e,providers:{kind:"record",value:r}}},ask:{kind:"object",fields:{llmModel:e,confirmThreshold:e}},agents:{kind:"object",fields:{dataDir:e,env:{kind:"record",value:{kind:"record",value:e}}}},install:{kind:"object",fields:{registry:e,fetchRetries:e,preferOffline:e,networkTimeoutMs:e}},browser:{kind:"object",fields:{defaultInstance:e,instances:{kind:"record",value:o}}}}};export function kebabToCamel(e){return e.replace(/-([a-z])/g,(e,r)=>r.toUpperCase())}export function camelToKebab(e){return e.replace(/[A-Z]/g,e=>`-${e.toLowerCase()}`)}function t(e){return"object"==typeof e&&null!==e&&!Array.isArray(e)}export function decodeFromYaml(r,o=CONFIG_KEY_CODEC){if(Array.isArray(r))return r.map(e=>decodeFromYaml(e,o));if(!t(r))return r;if("object"===o.kind){const t={};for(const[n,i]of Object.entries(r)){const r=kebabToCamel(n),d=o.fields[r]??e;t[r]=decodeFromYaml(i,d)}return t}if("record"===o.kind){const e={};for(const[t,n]of Object.entries(r))e[t]=decodeFromYaml(n,o.value);return e}return r}function n(r,o){const t=[];let n=CONFIG_KEY_CODEC;for(const i of r)if("object"===n.kind){const r=kebabToCamel(i),d=n.fields[r];d?(t.push("camel"===o?r:camelToKebab(r)),n=d):(t.push(i),n=e)}else"record"===n.kind?(t.push(i),n=n.value):t.push(i);return t}export function encodePathToYaml(e){return n(e,"kebab")}export function normalizeUserPath(e){return n(e,"camel")}
@@ -16,6 +16,11 @@ export interface LoadAgentsConfigResult {
16
16
  /** 实际加载的配置文件路径,undefined 表示使用默认配置 */
17
17
  readonly configPath: string | undefined;
18
18
  }
19
+ export interface LoadInstallConfigResult {
20
+ readonly installConfig: RollConfig["install"];
21
+ /** 实际加载的配置文件路径,undefined 表示使用默认配置 */
22
+ readonly configPath: string | undefined;
23
+ }
19
24
  declare const CONFIG_INSPECTION_STATUSES: {
20
25
  readonly notFound: "not-found";
21
26
  readonly valid: "valid";
@@ -50,6 +55,13 @@ export interface ConfigInspectionInvalid extends ConfigInspectionBase {
50
55
  export type ConfigInspectionResult = ConfigInspectionNotFound | ConfigInspectionValid | ConfigInspectionNeedsMigration | ConfigInspectionInvalid;
51
56
  export declare function parseConfigDocument(raw: string, configPath: string): Record<string, unknown>;
52
57
  export declare function validateConfigText(raw: string, configPath: string): RollConfig;
58
+ /**
59
+ * 加载 `install` 段配置(npm 安装/更新行为)。
60
+ *
61
+ * 解析失败(缺失文件、YAML 语法错误、install 段非法)时抛错,
62
+ * 避免用户显式配置 registry 时因局部错误静默回退到 npm 默认源。
63
+ */
64
+ export declare function loadInstallConfig(options?: LoadConfigOptions): LoadInstallConfigResult;
53
65
  /**
54
66
  * 加载并校验 Roll 配置。
55
67
  *
@@ -1 +1 @@
1
- import{readFileSync as n,existsSync as r}from"node:fs";import{resolve as t}from"node:path";import{parse as o}from"yaml";import{agentsConfigSchema as e,rollConfigSchema as i}from"./schema.js";import{DEFAULT_CONFIG as s,CONFIG_FILE_NAMES as a}from"./defaults.js";import{decodeFromYaml as f}from"./key-codec.js";import{detectKnownConfigMigrations as c,formatConfigMigrationError as u}from"./migration.js";function g(n){if("string"==typeof n)return n.replace(/\$\{([^}]+)\}/g,(n,r)=>process.env[r]??n);if(Array.isArray(n))return n.map(g);if(l(n)){const r={};for(const[t,o]of Object.entries(n))r[t]=g(o);return r}return n}function l(n){return"object"==typeof n&&null!==n&&!Array.isArray(n)}function d(n){return l(n)&&"number"==typeof n.line&&"number"==typeof n.col}function p(n){return n instanceof Error&&"linePos"in n&&Array.isArray(n.linePos)&&n.linePos.length>0&&n.linePos.every(d)}function h(n,r){const t=`Invalid YAML syntax in config file: ${n}`;if(!(r instanceof Error))return`${t}\n${String(r)}`;if(p(r)){const[n]=r.linePos;if(n)return`${t} at line ${n.line}, column ${n.col}\n${r.message}`}return`${t}\n${r.message}`}function m(n){let o=t(n);const e=t("/");for(;o!==e;){for(const n of a){const e=t(o,n);if(r(e))return e}const n=t(o,"..");if(n===o)break;o=n}}function v(n){if(n.startsWith("~/")){const r=process.env.HOME??process.env.USERPROFILE??"";return t(r,n.slice(2))}return n}function w(n){return{...n,agents:{...n.agents,dataDir:v(n.agents.dataDir)},browser:{...n.browser,instances:Object.fromEntries(Object.entries(n.browser.instances).map(([n,r])=>[n,{...r,userDataDir:v(r.userDataDir),...void 0!==r.sessionsDir?{sessionsDir:v(r.sessionsDir)}:{}}]))}}}const $={notFound:"not-found",valid:"valid",needsMigration:"needs-migration",invalid:"invalid"};export function parseConfigDocument(n,r){let t;try{t=o(n)}catch(n){throw new Error(h(r,n),{cause:n instanceof Error?n:void 0})}if(!l(t))throw new Error(`Invalid config file: ${r} (expected YAML object)`);return t}function y(n,r,t={}){const o=parseConfigDocument(n,r),e=c(o,t);if(e.needsMigration)throw new Error(u(r,e));return o}export function validateConfigText(n,r){const t=y(n,r),o=g(f(t));if(!l(o))throw new Error(`Invalid config file: ${r} (expected YAML object)`);const e=E(s,o),a=i.safeParse(e);if(!a.success){const n=a.error.issues.map(n=>` - ${n.path.join(".")}: ${n.message}`).join("\n");throw new Error(`Config validation failed (${r}):\n${n}`)}return w(a.data)}function P(n,r){const t=y(n,r,{scope:"agents"}),o=g(f(t));if(!l(o))throw new Error(`Invalid config file: ${r} (expected YAML object)`);const i=l(o.agents)?o.agents:{},a=E(s.agents,i),c=e.safeParse(a);if(!c.success){const n=c.error.issues.map(n=>` - agents.${n.path.join(".")}: ${n.message}`).join("\n");throw new Error(`Config validation failed (${r}):\n${n}`)}return{...c.data,dataDir:v(c.data.dataDir)}}export function loadConfig(t={}){const o=resolveConfigPath(t);if(!o)return{config:w(s),configPath:void 0};if(!r(o))throw new Error(`Config file not found: ${o}`);return{config:validateConfigText(n(o,"utf-8"),o),configPath:o}}export function loadAgentsConfig(t={}){const o=resolveConfigPath(t);if(!o)return{agentsConfig:{...s.agents,dataDir:v(s.agents.dataDir)},configPath:void 0};if(!r(o))throw new Error(`Config file not found: ${o}`);return{agentsConfig:P(n(o,"utf-8"),o),configPath:o}}export function resolveConfigPath(n={}){const{configPath:r,cwd:t=process.cwd()}=n;return r??m(t)}export function inspectConfigFile(t={}){const o=resolveConfigPath(t);if(!o)return{status:$.notFound,configPath:void 0};if(!r(o))return{status:$.invalid,configPath:o,raw:"",error:new Error(`Config file not found: ${o}`)};const e=n(o,"utf-8");let i;try{i=parseConfigDocument(e,o)}catch(n){return{status:$.invalid,configPath:o,raw:e,error:n instanceof Error?n:new Error(String(n))}}const s=c(i);if(s.needsMigration)return{status:$.needsMigration,configPath:o,raw:e,report:s};try{return{status:$.valid,configPath:o,config:validateConfigText(e,o)}}catch(n){return{status:$.invalid,configPath:o,raw:e,error:n instanceof Error?n:new Error(String(n))}}}function E(n,r){const t={...n};for(const[o,e]of Object.entries(r)){const r=n[o];"object"!=typeof e||null===e||Array.isArray(e)||"object"!=typeof r||null===r||Array.isArray(r)?t[o]=e:t[o]=E(r,e)}return t}
1
+ import{readFileSync as n,existsSync as r}from"node:fs";import{resolve as t}from"node:path";import{parse as o}from"yaml";import{agentsConfigSchema as e,installConfigSchema as i,rollConfigSchema as s}from"./schema.js";import{DEFAULT_CONFIG as a,CONFIG_FILE_NAMES as f}from"./defaults.js";import{decodeFromYaml as c}from"./key-codec.js";import{detectKnownConfigMigrations as u,formatConfigMigrationError as l}from"./migration.js";function g(n){if("string"==typeof n)return n.replace(/\$\{([^}]+)\}/g,(n,r)=>process.env[r]??n);if(Array.isArray(n))return n.map(g);if(d(n)){const r={};for(const[t,o]of Object.entries(n))r[t]=g(o);return r}return n}function d(n){return"object"==typeof n&&null!==n&&!Array.isArray(n)}function p(n){return d(n)&&"number"==typeof n.line&&"number"==typeof n.col}function h(n){return n instanceof Error&&"linePos"in n&&Array.isArray(n.linePos)&&n.linePos.length>0&&n.linePos.every(p)}function m(n,r){const t=`Invalid YAML syntax in config file: ${n}`;if(!(r instanceof Error))return`${t}\n${String(r)}`;if(h(r)){const[n]=r.linePos;if(n)return`${t} at line ${n.line}, column ${n.col}\n${r.message}`}return`${t}\n${r.message}`}function w(n){let o=t(n);const e=t("/");for(;o!==e;){for(const n of f){const e=t(o,n);if(r(e))return e}const n=t(o,"..");if(n===o)break;o=n}}function v(n){if(n.startsWith("~/")){const r=process.env.HOME??process.env.USERPROFILE??"";return t(r,n.slice(2))}return n}function $(n){return{...n,agents:{...n.agents,dataDir:v(n.agents.dataDir)},browser:{...n.browser,instances:Object.fromEntries(Object.entries(n.browser.instances).map(([n,r])=>[n,{...r,userDataDir:v(r.userDataDir),...void 0!==r.sessionsDir?{sessionsDir:v(r.sessionsDir)}:{}}]))}}}const C={notFound:"not-found",valid:"valid",needsMigration:"needs-migration",invalid:"invalid"};export function parseConfigDocument(n,r){let t;try{t=o(n)}catch(n){throw new Error(m(r,n),{cause:n instanceof Error?n:void 0})}if(!d(t))throw new Error(`Invalid config file: ${r} (expected YAML object)`);return t}function P(n,r,t={}){const o=parseConfigDocument(n,r),e=u(o,t);if(e.needsMigration)throw new Error(l(r,e));return o}export function validateConfigText(n,r){const t=P(n,r),o=g(c(t));if(!d(o))throw new Error(`Invalid config file: ${r} (expected YAML object)`);const e=j(a,o),i=s.safeParse(e);if(!i.success){const n=i.error.issues.map(n=>` - ${n.path.join(".")}: ${n.message}`).join("\n");throw new Error(`Config validation failed (${r}):\n${n}`)}return $(i.data)}function E(n,r){const t=P(n,r,{scope:"agents"}),o=g(c(t));if(!d(o))throw new Error(`Invalid config file: ${r} (expected YAML object)`);const i=d(o.agents)?o.agents:{},s=j(a.agents,i),f=e.safeParse(s);if(!f.success){const n=f.error.issues.map(n=>` - agents.${n.path.join(".")}: ${n.message}`).join("\n");throw new Error(`Config validation failed (${r}):\n${n}`)}return{...f.data,dataDir:v(f.data.dataDir)}}function y(n,r){const t=parseConfigDocument(n,r),o=g(c(t));if(!d(o))throw new Error(`Invalid config file: ${r} (expected YAML object)`);const e=d(o.install)?o.install:{},s=j(a.install,e),f=i.safeParse(s);if(!f.success){const n=f.error.issues.map(n=>` - install.${n.path.join(".")}: ${n.message}`).join("\n");throw new Error(`Config validation failed (${r}):\n${n}`)}return f.data}export function loadInstallConfig(t={}){const o=resolveConfigPath(t);if(!o)return{installConfig:a.install,configPath:void 0};if(!r(o))throw new Error(`Config file not found: ${o}`);return{installConfig:y(n(o,"utf-8"),o),configPath:o}}export function loadConfig(t={}){const o=resolveConfigPath(t);if(!o)return{config:$(a),configPath:void 0};if(!r(o))throw new Error(`Config file not found: ${o}`);return{config:validateConfigText(n(o,"utf-8"),o),configPath:o}}export function loadAgentsConfig(t={}){const o=resolveConfigPath(t);if(!o)return{agentsConfig:{...a.agents,dataDir:v(a.agents.dataDir)},configPath:void 0};if(!r(o))throw new Error(`Config file not found: ${o}`);return{agentsConfig:E(n(o,"utf-8"),o),configPath:o}}export function resolveConfigPath(n={}){const{configPath:r,cwd:t=process.cwd()}=n;return r??w(t)}export function inspectConfigFile(t={}){const o=resolveConfigPath(t);if(!o)return{status:C.notFound,configPath:void 0};if(!r(o))return{status:C.invalid,configPath:o,raw:"",error:new Error(`Config file not found: ${o}`)};const e=n(o,"utf-8");let i;try{i=parseConfigDocument(e,o)}catch(n){return{status:C.invalid,configPath:o,raw:e,error:n instanceof Error?n:new Error(String(n))}}const s=u(i);if(s.needsMigration)return{status:C.needsMigration,configPath:o,raw:e,report:s};try{return{status:C.valid,configPath:o,config:validateConfigText(e,o)}}catch(n){return{status:C.invalid,configPath:o,raw:e,error:n instanceof Error?n:new Error(String(n))}}}function j(n,r){const t={...n};for(const[o,e]of Object.entries(r)){const r=n[o];"object"!=typeof e||null===e||Array.isArray(e)||"object"!=typeof r||null===r||Array.isArray(r)?t[o]=e:t[o]=j(r,e)}return t}
@@ -74,6 +74,32 @@ export declare const agentsConfigSchema: z.ZodObject<{
74
74
  dataDir: string;
75
75
  env?: Record<string, Record<string, string>> | undefined;
76
76
  }>;
77
+ /**
78
+ * npm 安装/更新行为配置(影响 `roll agent install` / `roll update`)。
79
+ *
80
+ * 安全口径:默认走 npm 自身的源(通常官方源),只有用户显式配置 `registry`
81
+ * 才切换镜像源;roll 不做隐式自动 fallback。
82
+ */
83
+ export declare const installConfigSchema: z.ZodObject<{
84
+ /** 显式 opt-in 的 npm registry(如国内镜像)。未配置时使用 npm 默认源。 */
85
+ registry: z.ZodOptional<z.ZodString>;
86
+ /** 透传给 npm 的 `--fetch-retries`,并用于 roll 层整体重试次数。 */
87
+ fetchRetries: z.ZodDefault<z.ZodNumber>;
88
+ /** 安装时附加 `--prefer-offline`,默认关闭以避免更新时复用过期 npm 元数据。 */
89
+ preferOffline: z.ZodDefault<z.ZodBoolean>;
90
+ /** 单次 npm 安装命令的超时(毫秒)。 */
91
+ networkTimeoutMs: z.ZodDefault<z.ZodNumber>;
92
+ }, "strip", z.ZodTypeAny, {
93
+ fetchRetries: number;
94
+ preferOffline: boolean;
95
+ networkTimeoutMs: number;
96
+ registry?: string | undefined;
97
+ }, {
98
+ registry?: string | undefined;
99
+ fetchRetries?: number | undefined;
100
+ preferOffline?: boolean | undefined;
101
+ networkTimeoutMs?: number | undefined;
102
+ }>;
77
103
  export declare const browserInstanceConfigSchema: z.ZodEffects<z.ZodObject<{
78
104
  platform: z.ZodOptional<z.ZodEnum<["zhipin", "yupao"]>>;
79
105
  mode: z.ZodDefault<z.ZodEnum<["managed-cdp", "remote-cdp", "existing-session"]>>;
@@ -455,6 +481,26 @@ export declare const rollConfigSchema: z.ZodObject<{
455
481
  dataDir: string;
456
482
  env?: Record<string, Record<string, string>> | undefined;
457
483
  }>;
484
+ install: z.ZodDefault<z.ZodObject<{
485
+ /** 显式 opt-in 的 npm registry(如国内镜像)。未配置时使用 npm 默认源。 */
486
+ registry: z.ZodOptional<z.ZodString>;
487
+ /** 透传给 npm 的 `--fetch-retries`,并用于 roll 层整体重试次数。 */
488
+ fetchRetries: z.ZodDefault<z.ZodNumber>;
489
+ /** 安装时附加 `--prefer-offline`,默认关闭以避免更新时复用过期 npm 元数据。 */
490
+ preferOffline: z.ZodDefault<z.ZodBoolean>;
491
+ /** 单次 npm 安装命令的超时(毫秒)。 */
492
+ networkTimeoutMs: z.ZodDefault<z.ZodNumber>;
493
+ }, "strip", z.ZodTypeAny, {
494
+ fetchRetries: number;
495
+ preferOffline: boolean;
496
+ networkTimeoutMs: number;
497
+ registry?: string | undefined;
498
+ }, {
499
+ registry?: string | undefined;
500
+ fetchRetries?: number | undefined;
501
+ preferOffline?: boolean | undefined;
502
+ networkTimeoutMs?: number | undefined;
503
+ }>>;
458
504
  browser: z.ZodDefault<z.ZodEffects<z.ZodObject<{
459
505
  defaultInstance: z.ZodOptional<z.ZodString>;
460
506
  instances: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodEffects<z.ZodObject<{
@@ -671,6 +717,12 @@ export declare const rollConfigSchema: z.ZodObject<{
671
717
  }> | undefined;
672
718
  }>>;
673
719
  }, "strip", z.ZodTypeAny, {
720
+ install: {
721
+ fetchRetries: number;
722
+ preferOffline: boolean;
723
+ networkTimeoutMs: number;
724
+ registry?: string | undefined;
725
+ };
674
726
  ask: {
675
727
  llmModel?: string | undefined;
676
728
  confirmThreshold?: number | undefined;
@@ -729,6 +781,12 @@ export declare const rollConfigSchema: z.ZodObject<{
729
781
  dataDir: string;
730
782
  env?: Record<string, Record<string, string>> | undefined;
731
783
  };
784
+ install?: {
785
+ registry?: string | undefined;
786
+ fetchRetries?: number | undefined;
787
+ preferOffline?: boolean | undefined;
788
+ networkTimeoutMs?: number | undefined;
789
+ } | undefined;
732
790
  browser?: {
733
791
  defaultInstance?: string | undefined;
734
792
  instances?: Record<string, {
@@ -757,3 +815,4 @@ export declare const rollConfigSchema: z.ZodObject<{
757
815
  }>;
758
816
  export type RollConfig = z.infer<typeof rollConfigSchema>;
759
817
  export type BrowserConfig = z.infer<typeof browserConfigSchema>;
818
+ export type InstallConfig = z.infer<typeof installConfigSchema>;
@@ -1 +1 @@
1
- import{z as e}from"zod";const o=["zhipin","yupao"],t=["managed-cdp","remote-cdp","existing-session"],n=["chrome","chromium","msedge"],s=e.string().trim().regex(/^#[\da-fA-F]{6}$/,"profileColor must be a hex RGB color such as #2563EB").transform(e=>e.toUpperCase());export const browserWindowBoundsSchema=e.object({x:e.number().int().optional(),y:e.number().int().optional(),width:e.number().int().positive().optional(),height:e.number().int().positive().optional()});export const providerConfigSchema=e.object({apiKey:e.string(),baseUrl:e.string().optional()});export const llmConfigSchema=e.object({defaultProvider:e.string(),defaultModel:e.string(),providers:e.record(e.string(),providerConfigSchema)});export const askConfigSchema=e.object({llmModel:e.string().optional(),confirmThreshold:e.number().optional()});export const agentsConfigSchema=e.object({dataDir:e.string(),env:e.record(e.string(),e.record(e.string(),e.string())).optional()});export const browserInstanceConfigSchema=e.object({platform:e.enum(o).optional(),mode:e.enum(t).default("managed-cdp"),headless:e.boolean().optional(),cdpUrl:e.string().optional(),cdpHost:e.string().default("127.0.0.1"),cdpPort:e.number().int().min(1).max(65535).optional(),channel:e.enum(n).default("chrome"),executablePath:e.string().optional(),userDataDir:e.string().trim().min(1),sessionsDir:e.string().trim().min(1).optional(),args:e.array(e.string()).optional(),profileName:e.string().trim().min(1).optional(),profileColor:s.optional(),windowBounds:browserWindowBoundsSchema.optional(),trackingAgentId:e.string().trim().min(1).optional()}).superRefine((o,t)=>{"managed-cdp"===o.mode&&void 0===o.cdpPort&&t.addIssue({code:e.ZodIssueCode.custom,path:["cdpPort"],message:"managed-cdp browser instance requires cdpPort"}),"remote-cdp"!==o.mode&&"existing-session"!==o.mode||void 0!==o.cdpUrl||t.addIssue({code:e.ZodIssueCode.custom,path:["cdpUrl"],message:`${o.mode} browser instance requires cdpUrl`})});export const browserConfigSchema=e.object({defaultInstance:e.string().trim().min(1).optional(),instances:e.record(e.string(),browserInstanceConfigSchema).default({})}).superRefine((o,t)=>{const n=Object.entries(o.instances);void 0!==o.defaultInstance&&void 0===o.instances[o.defaultInstance]&&t.addIssue({code:e.ZodIssueCode.custom,path:["defaultInstance"],message:`defaultInstance "${o.defaultInstance}" is not declared in browser.instances`});const s=new Map,r=new Map;for(const[o,i]of n){if(void 0!==i.cdpPort){const n=s.get(i.cdpPort);void 0!==n?t.addIssue({code:e.ZodIssueCode.custom,path:["instances",o,"cdpPort"],message:`cdpPort ${String(i.cdpPort)} is already used by browser instance "${n}"`}):s.set(i.cdpPort,o)}const n=r.get(i.userDataDir);void 0!==n?t.addIssue({code:e.ZodIssueCode.custom,path:["instances",o,"userDataDir"],message:`userDataDir is already used by browser instance "${n}"`}):r.set(i.userDataDir,o)}});export const rollConfigSchema=e.object({llm:llmConfigSchema,ask:askConfigSchema,agents:agentsConfigSchema,browser:browserConfigSchema.default({})});
1
+ import{z as e}from"zod";const o=["zhipin","yupao"],t=["managed-cdp","remote-cdp","existing-session"],n=["chrome","chromium","msedge"],s=e.string().trim().regex(/^#[\da-fA-F]{6}$/,"profileColor must be a hex RGB color such as #2563EB").transform(e=>e.toUpperCase());export const browserWindowBoundsSchema=e.object({x:e.number().int().optional(),y:e.number().int().optional(),width:e.number().int().positive().optional(),height:e.number().int().positive().optional()});export const providerConfigSchema=e.object({apiKey:e.string(),baseUrl:e.string().optional()});export const llmConfigSchema=e.object({defaultProvider:e.string(),defaultModel:e.string(),providers:e.record(e.string(),providerConfigSchema)});export const askConfigSchema=e.object({llmModel:e.string().optional(),confirmThreshold:e.number().optional()});export const agentsConfigSchema=e.object({dataDir:e.string(),env:e.record(e.string(),e.record(e.string(),e.string())).optional()});export const installConfigSchema=e.object({registry:e.string().trim().url().optional(),fetchRetries:e.number().int().min(0).max(10).default(3),preferOffline:e.boolean().default(!1),networkTimeoutMs:e.number().int().min(1e4).default(12e4)});export const browserInstanceConfigSchema=e.object({platform:e.enum(o).optional(),mode:e.enum(t).default("managed-cdp"),headless:e.boolean().optional(),cdpUrl:e.string().optional(),cdpHost:e.string().default("127.0.0.1"),cdpPort:e.number().int().min(1).max(65535).optional(),channel:e.enum(n).default("chrome"),executablePath:e.string().optional(),userDataDir:e.string().trim().min(1),sessionsDir:e.string().trim().min(1).optional(),args:e.array(e.string()).optional(),profileName:e.string().trim().min(1).optional(),profileColor:s.optional(),windowBounds:browserWindowBoundsSchema.optional(),trackingAgentId:e.string().trim().min(1).optional()}).superRefine((o,t)=>{"managed-cdp"===o.mode&&void 0===o.cdpPort&&t.addIssue({code:e.ZodIssueCode.custom,path:["cdpPort"],message:"managed-cdp browser instance requires cdpPort"}),"remote-cdp"!==o.mode&&"existing-session"!==o.mode||void 0!==o.cdpUrl||t.addIssue({code:e.ZodIssueCode.custom,path:["cdpUrl"],message:`${o.mode} browser instance requires cdpUrl`})});export const browserConfigSchema=e.object({defaultInstance:e.string().trim().min(1).optional(),instances:e.record(e.string(),browserInstanceConfigSchema).default({})}).superRefine((o,t)=>{const n=Object.entries(o.instances);void 0!==o.defaultInstance&&void 0===o.instances[o.defaultInstance]&&t.addIssue({code:e.ZodIssueCode.custom,path:["defaultInstance"],message:`defaultInstance "${o.defaultInstance}" is not declared in browser.instances`});const s=new Map,r=new Map;for(const[o,i]of n){if(void 0!==i.cdpPort){const n=s.get(i.cdpPort);void 0!==n?t.addIssue({code:e.ZodIssueCode.custom,path:["instances",o,"cdpPort"],message:`cdpPort ${String(i.cdpPort)} is already used by browser instance "${n}"`}):s.set(i.cdpPort,o)}const n=r.get(i.userDataDir);void 0!==n?t.addIssue({code:e.ZodIssueCode.custom,path:["instances",o,"userDataDir"],message:`userDataDir is already used by browser instance "${n}"`}):r.set(i.userDataDir,o)}});export const rollConfigSchema=e.object({llm:llmConfigSchema,ask:askConfigSchema,agents:agentsConfigSchema,install:installConfigSchema.default({}),browser:browserConfigSchema.default({})});
@@ -1 +1 @@
1
- import{spawn as t}from"node:child_process";import{closeSync as e,existsSync as n,mkdirSync as r,openSync as i,readFileSync as o,unlinkSync as s,writeFileSync as a}from"node:fs";import{resolve as c,dirname as l}from"node:path";import{McpClientManager as p}from"../mcp/client-manager.js";import{resolveDevSpawnSpec as d}from"./dev-spawn.js";import{inferAgentSourceType as m}from"./source.js";const u=1,g="unknown";function f(t,e){return c(t,"pids",`${e}.pid`)}function h(t,e){return c(t,"pids",`${e}.runtime.json`)}function w(t,e){for(const r of[f(t,e),h(t,e)])n(r)&&s(r)}export function cleanupOrphanAgentRuntimeMetadata(t,e){return void 0===getAgentPid(t,e)&&(w(t,e),!0)}export function getAgentLogPath(t,e){return c(t,"logs",`${e}.log`)}function $(t){try{return process.kill(t,0),!0}catch{return!1}}export function getAgentPid(t,e){const r=f(t,e);if(!n(r))return;const i=Number(o(r,"utf-8").trim());if(!Number.isNaN(i)&&$(i))return i;w(t,e)}export function getRollCoreVersion(){try{const t=c(import.meta.dirname,"../../package.json"),e=JSON.parse(o(t,"utf-8"));return A(e)&&"string"==typeof e.version?e.version:g}catch{return g}}export function writeAgentRuntimeSidecar(t,e,i){const o=h(e,t.skill.name),s=l(o);n(s)||r(s,{recursive:!0});const c={schemaVersion:1,agentName:t.skill.name,pid:i,coreVersion:getRollCoreVersion(),startedAt:(new Date).toISOString(),..."streamable-http"===t.transport.type?{endpoint:t.transport.endpoint}:{}};a(o,`${JSON.stringify(c,null,2)}\n`,"utf-8")}export function inspectManagedAgentRuntime(t,e){const n=getAgentPid(e,t.skill.name),r=getRollCoreVersion(),i="streamable-http"===t.transport.type?t.transport.endpoint:void 0;if(void 0===n){const n=v(e,t.skill.name);return void 0!==n?{..."invalid"!==n?{sidecar:n}:{},expectedCoreVersion:r,...i?{expectedEndpoint:i}:{},issues:[{code:"orphan-sidecar",message:"invalid"===n?"runtime sidecar 存在但没有活动 PID,且无法解析":`runtime sidecar 记录 PID ${String(n.pid)},但没有活动 PID`,fix:`运行 \`roll doctor --fix\` 清理 ${t.skill.name} 的过期 runtime 元数据`}]}:{expectedCoreVersion:r,...i?{expectedEndpoint:i}:{},issues:[]}}const o=[],s=v(e,t.skill.name);return"invalid"===s?(o.push({code:"invalid-sidecar",message:"runtime sidecar 无法解析",fix:`运行 \`roll agent stop ${t.skill.name}\` 后重新 \`roll agent start ${t.skill.name}\``}),{pid:n,expectedCoreVersion:r,...i?{expectedEndpoint:i}:{},issues:o}):s?(s.pid!==n&&o.push({code:"pid-mismatch",message:`runtime sidecar PID ${String(s.pid)} 与活动 PID ${String(n)} 不一致`,fix:`运行 \`roll agent stop ${t.skill.name}\` 后重新 \`roll agent start ${t.skill.name}\``}),s.coreVersion!==r&&o.push({code:"version-mismatch",message:`runtime sidecar 来自 core ${s.coreVersion},当前 core 是 ${r}`,fix:`运行 \`roll agent stop ${t.skill.name}\` 后重新 \`roll agent start ${t.skill.name}\``}),i&&s.endpoint!==i&&o.push({code:"endpoint-mismatch",message:`runtime sidecar endpoint 是 ${s.endpoint??"n/a"},当前配置是 ${i}`,fix:`运行 \`roll agent stop ${t.skill.name}\` 后重新 \`roll agent start ${t.skill.name}\``}),{pid:n,sidecar:s,expectedCoreVersion:r,...i?{expectedEndpoint:i}:{},issues:o}):(o.push({code:"missing-sidecar",message:"进程存在但缺少 runtime sidecar",fix:`运行 \`roll agent stop ${t.skill.name}\` 后重新 \`roll agent start ${t.skill.name}\``}),{pid:n,expectedCoreVersion:r,...i?{expectedEndpoint:i}:{},issues:o})}export async function probeAgentEndpoint(t,e={}){const n=new p;try{const r=await n.connect(t.skill.name,t.transport,t.installPath,void 0!==e.timeoutMs?{timeoutMs:e.timeoutMs}:{});await r.listTools()}finally{await n.disconnectAll()}}export async function waitForAgentReady(t,e={}){const n=e.startupTimeoutMs??15e3,r=e.probeTimeoutMs??2e3,i=e.intervalMs??500,o=Date.now()+n;let s;for(;Date.now()<o;)try{return void await probeAgentEndpoint(t,{timeoutMs:r})}catch(t){s=t,await y(i)}throw new Error(`Agent "${t.skill.name}" did not become ready within ${n}ms${s?`: ${s instanceof Error?s.message:String(s)}`:""}`)}export function startAgent(o,s,c){if("streamable-http"===o.transport.type&&"core-managed"!==o.runtime.ownership)throw new Error(`Agent "${o.skill.name}" 使用 streamable-http 传输且非 core-managed,请手动启动服务。\n 端点: ${o.transport.endpoint}`);const p=getAgentPid(s,o.skill.name);if(void 0!==p)throw new Error(`Agent "${o.skill.name}" 已在运行 (PID: ${String(p)})`);const d=k(o),m=getAgentLogPath(s,o.skill.name),u=l(m);n(u)||r(u,{recursive:!0});const g=i(m,"a"),h=t(d.command,[...d.args??[]],{cwd:o.installPath,detached:!0,stdio:["ignore",g,g],...c?{env:{...process.env,...c}}:{}});if(e(g),!h.pid)throw new Error(`Failed to start agent "${o.skill.name}"`);const $=f(s,o.skill.name),y=l($);n(y)||r(y,{recursive:!0});try{a($,String(h.pid),"utf-8"),writeAgentRuntimeSidecar(o,s,h.pid)}catch(t){try{process.kill(h.pid,"SIGTERM")}catch{}throw w(s,o.skill.name),new Error(`Failed to persist runtime metadata for agent "${o.skill.name}"`,{cause:t})}return h.unref(),h.pid}export function stopAgent(t,e){const n=getAgentPid(t,e);if(void 0===n)return!1;try{process.kill(n,"SIGTERM")}catch{}return w(t,e),!0}export async function stopAgentGracefully(t,e,n={}){const r=getAgentPid(t,e);if(void 0===r)return!1;try{process.kill(r,"SIGTERM")}catch{return w(t,e),!0}const i=n.timeoutMs??15e3,o=n.intervalMs??200,s=Date.now()+i;for(;Date.now()<s;){if(!$(r))return w(t,e),!0;await y(o)}throw new Error(`Agent "${e}" did not stop within ${i}ms`)}function k(t){if("stdio"===t.transport.type){const e=d(t.transport.command,t.transport.args,t.installPath,m(t));return e||{command:t.transport.command,...t.transport.args?{args:t.transport.args}:{}}}if("core-managed"===t.runtime.ownership){const e=d(t.runtime.start.command,t.runtime.start.args,t.installPath,m(t));return e||{command:t.runtime.start.command,...t.runtime.start.args?{args:t.runtime.start.args}:{}}}throw new Error(`Agent "${t.skill.name}" does not have a managed runtime start command`)}function y(t){return new Promise(e=>{setTimeout(e,t)})}function v(t,e){const r=h(t,e);if(!n(r))return;let i;try{i=JSON.parse(o(r,"utf-8"))}catch{return"invalid"}return x(i)?i:"invalid"}function x(t){return!!A(t)&&(1===t.schemaVersion&&(!("string"!=typeof t.agentName||"number"!=typeof t.pid||!Number.isInteger(t.pid)||"string"!=typeof t.coreVersion||"string"!=typeof t.startedAt)&&(void 0===t.endpoint||"string"==typeof t.endpoint)))}function A(t){return"object"==typeof t&&null!==t&&!Array.isArray(t)}
1
+ import{spawn as t}from"node:child_process";import{closeSync as e,existsSync as n,mkdirSync as r,openSync as i,readFileSync as o,unlinkSync as s,writeFileSync as a}from"node:fs";import{resolve as c,dirname as l}from"node:path";import{McpClientManager as m}from"../mcp/client-manager.js";import{resolveDevSpawnSpec as p}from"./dev-spawn.js";import{inferAgentSourceType as d}from"./source.js";const u=1,g="unknown",f=15e3,h=2e3,w=500;function A(t,e){return c(t,"pids",`${e}.pid`)}function $(t,e){return c(t,"pids",`${e}.runtime.json`)}function k(t,e){for(const r of[A(t,e),$(t,e)])n(r)&&s(r)}export function cleanupOrphanAgentRuntimeMetadata(t,e){return void 0===getAgentPid(t,e)&&(k(t,e),!0)}export function getAgentLogPath(t,e){return c(t,"logs",`${e}.log`)}function v(t){try{return process.kill(t,0),!0}catch{return!1}}export function getAgentPid(t,e){const r=A(t,e);if(!n(r))return;const i=Number(o(r,"utf-8").trim());if(!Number.isNaN(i)&&v(i))return i;k(t,e)}export function getRollCoreVersion(){try{const t=c(import.meta.dirname,"../../package.json"),e=JSON.parse(o(t,"utf-8"));return R(e)&&"string"==typeof e.version?e.version:g}catch{return g}}export function writeAgentRuntimeSidecar(t,e,i){const o=$(e,t.skill.name),s=l(o);n(s)||r(s,{recursive:!0});const c={schemaVersion:1,agentName:t.skill.name,pid:i,coreVersion:getRollCoreVersion(),startedAt:(new Date).toISOString(),..."streamable-http"===t.transport.type?{endpoint:t.transport.endpoint}:{}};a(o,`${JSON.stringify(c,null,2)}\n`,"utf-8")}export function inspectManagedAgentRuntime(t,e){const n=getAgentPid(e,t.skill.name),r=getRollCoreVersion(),i="streamable-http"===t.transport.type?t.transport.endpoint:void 0;if(void 0===n){const n=P(e,t.skill.name);return void 0!==n?{..."invalid"!==n?{sidecar:n}:{},expectedCoreVersion:r,...i?{expectedEndpoint:i}:{},issues:[{code:"orphan-sidecar",message:"invalid"===n?"runtime sidecar 存在但没有活动 PID,且无法解析":`runtime sidecar 记录 PID ${String(n.pid)},但没有活动 PID`,fix:`运行 \`roll doctor --fix\` 清理 ${t.skill.name} 的过期 runtime 元数据`}]}:{expectedCoreVersion:r,...i?{expectedEndpoint:i}:{},issues:[]}}const o=[],s=P(e,t.skill.name);return"invalid"===s?(o.push({code:"invalid-sidecar",message:"runtime sidecar 无法解析",fix:`运行 \`roll agent stop ${t.skill.name}\` 后重新 \`roll agent start ${t.skill.name}\``}),{pid:n,expectedCoreVersion:r,...i?{expectedEndpoint:i}:{},issues:o}):s?(s.pid!==n&&o.push({code:"pid-mismatch",message:`runtime sidecar PID ${String(s.pid)} 与活动 PID ${String(n)} 不一致`,fix:`运行 \`roll agent stop ${t.skill.name}\` 后重新 \`roll agent start ${t.skill.name}\``}),s.coreVersion!==r&&o.push({code:"version-mismatch",message:`runtime sidecar 来自 core ${s.coreVersion},当前 core 是 ${r}`,fix:`运行 \`roll agent stop ${t.skill.name}\` 后重新 \`roll agent start ${t.skill.name}\``}),i&&s.endpoint!==i&&o.push({code:"endpoint-mismatch",message:`runtime sidecar endpoint 是 ${s.endpoint??"n/a"},当前配置是 ${i}`,fix:`运行 \`roll agent stop ${t.skill.name}\` 后重新 \`roll agent start ${t.skill.name}\``}),{pid:n,sidecar:s,expectedCoreVersion:r,...i?{expectedEndpoint:i}:{},issues:o}):(o.push({code:"missing-sidecar",message:"进程存在但缺少 runtime sidecar",fix:`运行 \`roll agent stop ${t.skill.name}\` 后重新 \`roll agent start ${t.skill.name}\``}),{pid:n,expectedCoreVersion:r,...i?{expectedEndpoint:i}:{},issues:o})}export async function probeAgentEndpoint(t,e={}){const n=new m;try{const r=await n.connect(t.skill.name,t.transport,t.installPath,void 0!==e.timeoutMs?{timeoutMs:e.timeoutMs}:{});await r.listTools()}finally{await n.disconnectAll()}}function y(t){const e=process.env[t];if(void 0===e||0===e.trim().length)return;const n=Number.parseInt(e,10);return!Number.isInteger(n)||n<=0||String(n)!==e.trim()?void 0:n}export async function waitForAgentReady(t,e={}){const n=y("ROLL_AGENT_READY_STARTUP_TIMEOUT_MS")??e.startupTimeoutMs??f,r=y("ROLL_AGENT_READY_PROBE_TIMEOUT_MS")??e.probeTimeoutMs??h,i=y("ROLL_AGENT_READY_INTERVAL_MS")??e.intervalMs??w,o=Date.now()+n;let s;for(;Date.now()<o;)try{return void await probeAgentEndpoint(t,{timeoutMs:r})}catch(t){s=t,await E(i)}throw new Error(`Agent "${t.skill.name}" did not become ready within ${n}ms${s?`: ${s instanceof Error?s.message:String(s)}`:""}`)}export function startAgent(o,s,c){if("streamable-http"===o.transport.type&&"core-managed"!==o.runtime.ownership)throw new Error(`Agent "${o.skill.name}" 使用 streamable-http 传输且非 core-managed,请手动启动服务。\n 端点: ${o.transport.endpoint}`);const m=getAgentPid(s,o.skill.name);if(void 0!==m)throw new Error(`Agent "${o.skill.name}" 已在运行 (PID: ${String(m)})`);const p=x(o),d=getAgentLogPath(s,o.skill.name),u=l(d);n(u)||r(u,{recursive:!0});const g=i(d,"a"),f=t(p.command,[...p.args??[]],{cwd:o.installPath,detached:!0,stdio:["ignore",g,g],...c?{env:{...process.env,...c}}:{}});if(e(g),!f.pid)throw new Error(`Failed to start agent "${o.skill.name}"`);const h=A(s,o.skill.name),w=l(h);n(w)||r(w,{recursive:!0});try{a(h,String(f.pid),"utf-8"),writeAgentRuntimeSidecar(o,s,f.pid)}catch(t){try{process.kill(f.pid,"SIGTERM")}catch{}throw k(s,o.skill.name),new Error(`Failed to persist runtime metadata for agent "${o.skill.name}"`,{cause:t})}return f.unref(),f.pid}export function stopAgent(t,e){const n=getAgentPid(t,e);if(void 0===n)return!1;try{process.kill(n,"SIGTERM")}catch{}return k(t,e),!0}export async function stopAgentGracefully(t,e,n={}){const r=getAgentPid(t,e);if(void 0===r)return!1;try{process.kill(r,"SIGTERM")}catch{return k(t,e),!0}const i=n.timeoutMs??15e3,o=n.intervalMs??200,s=Date.now()+i;for(;Date.now()<s;){if(!v(r))return k(t,e),!0;await E(o)}throw new Error(`Agent "${e}" did not stop within ${i}ms`)}function x(t){if("stdio"===t.transport.type){const e=p(t.transport.command,t.transport.args,t.installPath,d(t));return e||{command:t.transport.command,...t.transport.args?{args:t.transport.args}:{}}}if("core-managed"===t.runtime.ownership){const e=p(t.runtime.start.command,t.runtime.start.args,t.installPath,d(t));return e||{command:t.runtime.start.command,...t.runtime.start.args?{args:t.runtime.start.args}:{}}}throw new Error(`Agent "${t.skill.name}" does not have a managed runtime start command`)}function E(t){return new Promise(e=>{setTimeout(e,t)})}function P(t,e){const r=$(t,e);if(!n(r))return;let i;try{i=JSON.parse(o(r,"utf-8"))}catch{return"invalid"}return S(i)?i:"invalid"}function S(t){return!!R(t)&&(1===t.schemaVersion&&(!("string"!=typeof t.agentName||"number"!=typeof t.pid||!Number.isInteger(t.pid)||"string"!=typeof t.coreVersion||"string"!=typeof t.startedAt)&&(void 0===t.endpoint||"string"==typeof t.endpoint)))}function R(t){return"object"==typeof t&&null!==t&&!Array.isArray(t)}
@@ -1 +1 @@
1
- import{execFile as e}from"node:child_process";import{existsSync as r}from"node:fs";import{createRequire as t}from"node:module";import{resolve as o}from"node:path";import{promisify as s}from"node:util";const n=s(e);export async function runAgentSetup(e,r={}){const t="core-managed"===e.runtime.ownership?e.runtime.setup?.playwright?.browsers:void 0;if(!t||0===t.length)return{ok:!0,skipped:!0,message:"该 Agent 无额外 setup 步骤"};if(r.skipBrowserSetup)return{ok:!0,skipped:!0,message:`已跳过浏览器运行时安装 (${t.join(", ")})`};const o=i(e.installPath);if(!o)return{ok:!1,skipped:!1,message:"未找到 playwright-core CLI,无法安装浏览器运行时"};try{return await n(process.execPath,[o,"install",...t],{cwd:e.installPath,timeout:3e5}),{ok:!0,skipped:!1,message:`浏览器运行时安装完成 (${t.join(", ")})`,retryCommand:`${process.execPath} ${o} install ${t.join(" ")}`}}catch(e){return{ok:!1,skipped:!1,message:e instanceof Error?e.message:String(e),retryCommand:`${process.execPath} ${o} install ${t.join(" ")}`}}}function i(e){const s=o(e,"package.json");if(r(s))try{return t(s).resolve("playwright-core/cli.js")}catch{return}}
1
+ import{execFile as e}from"node:child_process";import{existsSync as r}from"node:fs";import{createRequire as t}from"node:module";import{resolve as o}from"node:path";import{promisify as s}from"node:util";const n=s(e);export async function runAgentSetup(e,r={}){const t="core-managed"===e.runtime.ownership?e.runtime.setup?.playwright?.browsers:void 0;if(!t||0===t.length)return{ok:!0,skipped:!0,message:"该 Agent 无额外 setup 步骤"};if(r.skipBrowserSetup)return{ok:!0,skipped:!0,message:`已跳过浏览器运行时安装 (${t.join(", ")})`};const o=i(e.installPath);if(!o)return{ok:!1,skipped:!1,message:"未找到 playwright-core CLI,无法安装浏览器运行时"};try{return await n(process.execPath,[o,"install",...t],{cwd:e.installPath,timeout:3e5,maxBuffer:67108864}),{ok:!0,skipped:!1,message:`浏览器运行时安装完成 (${t.join(", ")})`,retryCommand:`${process.execPath} ${o} install ${t.join(" ")}`}}catch(e){return{ok:!1,skipped:!1,message:e instanceof Error?e.message:String(e),retryCommand:`${process.execPath} ${o} install ${t.join(" ")}`}}}function i(e){const s=o(e,"package.json");if(r(s))try{return t(s).resolve("playwright-core/cli.js")}catch{return}}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@roll-agent/core",
3
- "version": "0.9.0",
3
+ "version": "0.10.1",
4
4
  "type": "module",
5
5
  "repository": {
6
6
  "type": "git",
@@ -35,6 +35,7 @@
35
35
  "@ai-sdk/deepseek": "^2.0.29",
36
36
  "@ai-sdk/openai": "^3.0.52",
37
37
  "@ai-sdk/provider": "^3.0.8",
38
+ "@clack/prompts": "1.4.0",
38
39
  "@modelcontextprotocol/sdk": "^1.12.0",
39
40
  "ai": "^6.0.154",
40
41
  "chalk": "^5.4.0",