@tomflow/chatloop-tools 0.4.7 → 0.4.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,27 +1,29 @@
1
1
  # @tomflow/chatloop-tools — Chat Loop 工具包
2
2
 
3
- **v0.4.7 · Node.js >=20 · 当前验证平台 macOS;实际发布版本以 npm Registry 为准**
3
+ **v0.4.9 · Node.js >=20 · 当前验证平台 macOS;实际发布版本以 npm Registry 为准**
4
4
 
5
- 本包是**本地 CLI + 统一 stdio MCP Gateway**(以下 API 合同适用于 v0.4.7,低版本以各自安装版说明为准):一套命令管理多个工具模块的启动、停止、重启、状态;一个 MCP 入口统一对外暴露各模块的工具 API。目前代理 **Desktop Commander 0.2.44** 原有 26 个工具,新增 **Chat Loop Extension** 四个专用 Action CLI 工具(总计 30 个 MCP 工具),Repomix 尚未接入。它**不是** ChatGPT Plugin、Secure MCP Tunnel 或 Chrome 扩展;但 v0.4.6 **随 npm 自动安装一份 Chat Loop 私有的对应平台 tunnel-client 二进制依赖**,不会使用 Local Dev/Homebrew 上的程序,也不包含任何用户凭据。
5
+ 本包是**本地 CLI + 统一 stdio MCP Gateway**(以下 API 合同适用于 v0.4.9,低版本以各自安装版说明为准):一套命令管理多个工具模块的启动、停止、重启、状态;一个 MCP 入口统一对外暴露各模块的工具 API。目前代理 **Desktop Commander 0.2.44** 原有 26 个工具,新增 **Chat Loop Extension** 四个专用 Action CLI 工具(总计 30 个 MCP 工具),Repomix 尚未接入。它**不是** ChatGPT Plugin、Secure MCP Tunnel 或 Chrome 扩展;但从 v0.4.6 起**随 npm 自动安装一份 Chat Loop 私有的对应平台 tunnel-client 二进制依赖**,不会使用 Local Dev/Homebrew 上的程序,也不包含任何用户凭据。
6
6
 
7
7
  > **给人类和 AI 的 API 合同**:此 README 随 npm 包分发,可独立阅读;对外的稳定使用入口是 `chatloop-tools` CLI(兼容别名 `chat-loop-local`)和 `chatloop-tools mcp` 提供的 MCP 协议。**无承诺的 JavaScript import/exports API**,不要依赖 `lib/` 内部模块。研发架构文档在仓库 `docs/17-local-tools-multi-tool-plan.md`,不随 npm 包发行。
8
8
 
9
9
  ## Chat Loop 专属 tunnel-client:打包内置,不依赖 Local Dev
10
10
 
11
- v0.4.7 延续 v0.4.6 的独立 npm 平台二进制依赖分发 OpenAI 官方 [tunnel-client v0.0.19](https://github.com/openai/tunnel-client/releases/tag/v0.0.19) 的 macOS Intel(x64)/Apple Silicon(arm64) **完整发行二进制与 cloudflared 同伴程序**分别封装在同一 `@tomflow` scope 的 `@tomflow/chatloop-tunnel-darwin-{x64,arm64}@0.0.19` 平台子包,主包用 `optionalDependencies` 由 npm **自动只安装匹配当前 OS/CPU 的包**。这仍属于 Chat Loop Tools 的内部依赖,不安装到 PATH、Homebrew 或 Local Dev。两份官方 ZIP 均按上游 SHA256SUMS 验证;将许可证、NOTICE、第三方许可证、SPDX 和二进制 SHA256 溯源一并保留。启动只调用本包自身的绝对路径,不搜索 PATH、不依赖本机 Homebrew 或 Local Dev。
11
+ v0.4.9 延续 v0.4.6 的独立 npm 平台二进制依赖分发 OpenAI 官方 [tunnel-client v0.0.19](https://github.com/openai/tunnel-client/releases/tag/v0.0.19) 的 macOS Intel(x64)/Apple Silicon(arm64) **完整发行二进制与 cloudflared 同伴程序**分别封装在同一 `@tomflow` scope 的 `@tomflow/chatloop-tunnel-darwin-{x64,arm64}@0.0.19` 平台子包,主包用 `optionalDependencies` 由 npm **自动只安装匹配当前 OS/CPU 的包**。这仍属于 Chat Loop Tools 的内部依赖,不安装到 PATH、Homebrew 或 Local Dev。两份官方 ZIP 均按上游 SHA256SUMS 验证;将许可证、NOTICE、第三方许可证、SPDX 和二进制 SHA256 溯源一并保留。启动只调用本包自身的绝对路径,不搜索 PATH、不依赖本机 Homebrew 或 Local Dev。
12
12
 
13
- **版本注意**:`@latest` 始终取 npm Registry 当前最新版本;如 Registry 尚未提供 v0.4.7,下列生命周期命令只能在本仓源码或独立 v0.4.7 tarball 环境中使用。
13
+ **版本注意**:`@latest` 始终取 npm Registry 当前最新版本;如 Registry 尚未提供 v0.4.9,下列新认证命令只能在本仓源码或独立 v0.4.9 tarball 环境中使用。
14
14
 
15
15
  ```bash
16
16
  npx --yes --package=@tomflow/chatloop-tools@latest chatloop-tools tunnel doctor
17
17
  npx --yes --package=@tomflow/chatloop-tools@latest chatloop-tools tunnel --version
18
- # 用户完成专属 Tunnel 的账号授权、配置后,由统一 Supervisor 启动:
18
+ # 在 OpenAI Platform 为本用户的 Chat Loop 专属 Tunnel 创建 Restricted Runtime API key(Tunnels Read + Use)后:
19
+ npx --yes --package=@tomflow/chatloop-tools@latest chatloop-tools tunnel auth
20
+ # 上一命令要求真实终端,以隐藏输入方式安全保存 key,不在 argv/输出中出现;随后启动:
19
21
  npx --yes --package=@tomflow/chatloop-tools@latest chatloop-tools start tunnel
20
22
  # 兼容入口(等价于 start tunnel,不再另开未托管前台进程):
21
23
  npx --yes --package=@tomflow/chatloop-tools@latest chatloop-tools tunnel run
22
24
  ```
23
25
 
24
- `tunnel doctor` 校验当前架构的 tunnel-client/cloudflared 哈希,报告 `BUNDLED_READY` 不代表连接已认证。**v0.4.7** 注册唯一组件 ID `tunnel`,由同一个 Supervisor 执行 `start|stop|restart|status tunnel`;无参命令统一涵盖已启用的 Desktop Commander 与 Tunnel。启动时只读产品自己的 `~/.chat-loop/tunnel/profiles/chat-loop.yaml`,缺少配置以 `PROFILE_NOT_FOUND` 失败而不启动。启动的真实二进制路径固定为本包对应平台依赖,唯一管理器持有子进程句柄;重复启动不复制进程,失去所有权时保留 `owner.json` 证据并安全拒绝接管、发信号。仅能停止本管理器直接持有的子进程,绝不按进程名查杀其他 Tunnel。
26
+ `tunnel doctor` 校验当前架构的 tunnel-client/cloudflared 哈希,报告 `BUNDLED_READY` 不代表连接已认证。**v0.4.9** 注册唯一组件 ID `tunnel`,由同一个 Supervisor 执行 `start|stop|restart|status tunnel`;无参命令统一涵盖已启用的 Desktop Commander 与 Tunnel。启动时只读产品自己的 `~/.chat-loop/tunnel/profiles/chat-loop.yaml`,缺少配置以 `PROFILE_NOT_FOUND` 失败而不启动。启动的真实二进制路径固定为本包对应平台依赖,唯一管理器持有子进程句柄;重复启动不复制进程,失去所有权时保留 `owner.json` 证据并安全拒绝接管、发信号。仅能停止本管理器直接持有的子进程,绝不按进程名查杀其他 Tunnel。
25
27
 
26
28
  Supervisor 自己创建的独立 Unix 健康 Socket `<state-dir>/tools/tunnel/health.sock` 通过 `/readyz` 判断**本地启动就绪**;结果 `after=RUNNING / evidence=LOCAL_READYZ / connection=UNVERIFIED` **不代表** OpenAI 云端请求成功。最后仍必须经 ChatGPT 真实 tools/call 验证。统一 Tunnel 子进程不继承外部的 `OPENAI_ADMIN_KEY` 或 `CONTROL_PLANE_API_KEY`:若自己的 profile 使用 `env:CONTROL_PLANE_API_KEY`,用户须明确在启动该 CLI 时提供产品独立的 `CHAT_LOOP_TUNNEL_CONTROL_PLANE_API_KEY`,由适配器专门映射;也可让产品自己的 profile 指向用户自有受保护的 `file:` 密钥引用。不要把凭据放在 CLI 参数、聊天或仓库里。
27
29
 
@@ -31,7 +33,7 @@ Supervisor 自己创建的独立 Unix 健康 Socket `<state-dir>/tools/tunnel/he
31
33
 
32
34
  - **已经验证**:仓库内 CLI 生命周期、真实 Desktop Commander MCP 握手和只读调用、26 项工具发现、单工具故障隔离、Gateway 断线重连;全部在本机测试或隔离状态目录内完成。
33
35
  - **已验证(2026-11-05)**:v0.4.0 本地 tarball 在仓库外 macOS 临时目录用 npm 安装(`--ignore-scripts`),安装后的 `doctor/status/start/stop` 正常;MCP initialize、26 工具列表与 `read_file` 调用读回临时文件均通过。未改现有 Local Dev/Tunnel。
34
- - **已验收历史与当前缺口要区分**:旧 v0.4.3 通过用户独立 Tunnel → ChatGPT 的 26 工具发现和真实只读 tools/call;正式 npm v0.4.5 已发布、npx latest 本地 MCP 30 工具发现通过,但**当前专用 Tunnel 因安全工具阻断重连,处于 STOPPED**,v0.4.5 的远端 tools/call **仍未验收**;但真实 macOS Chrome 0.3.29 + Native Host + 本地 MCP tools/call 的 Text/DOM/PNG/Reload **已通过**,并在 Reload 后正式 Audit 复核版本/Owner 未变化。Windows/Linux、新版 MCP 2026 协议、长任务取消、最小权限及公开 Chat Loop 产品 Plugin 发布也未验收。
36
+ - **已验收历史与当前缺口要区分**:旧 v0.4.3 通过用户独立 Tunnel → ChatGPT 的 26 工具发现和真实只读 tools/call;正式 npm v0.4.5 已发布、npx latest 本地 MCP 30 工具发现通过,这些是旧版阶段的历史记录;**当前专用 Tunnel 已通过独立 Runtime Key 和新 Chat 随机只读挑战验收**(证据以仓库 `context/CURRENT.md` 为准,本文随 npm 包分发时不代表运行中的在线状态);真实 macOS Chrome 0.3.29 + Native Host + 本地 MCP tools/call 的 Text/DOM/PNG/Reload **已通过**,并在 Reload 后正式 Audit 复核版本/Owner 未变化。Windows/Linux、新版 MCP 2026 协议、长任务取消、最小权限及公开 Chat Loop 产品 Plugin 发布也未验收。
35
37
  - **高风险权限**:Desktop Commander 在独立 `HOME` 下运行,但仍继承**当前操作系统用户的文件、进程与命令权限**;这不是文件系统沙箱。只在获用户明确授权的机器上运行,勿以管理员权限启动,勿向不可信客户端暴露;生产级目录白名单/命令审批**尚未实现**。
36
38
 
37
39
  ## 2. 安装及入口
@@ -66,7 +68,7 @@ npx --yes --package=@tomflow/chatloop-tools@latest chatloop-tools mcp
66
68
  cd packages/local-tools
67
69
  npm pack
68
70
  # 在仓库外一个全新的目录:
69
- npm install /absolute/path/to/tomflow-chatloop-tools-0.4.7.tgz
71
+ npm install /absolute/path/to/tomflow-chatloop-tools-0.4.9.tgz
70
72
  ./node_modules/.bin/chat-loop-local doctor
71
73
  ./node_modules/.bin/chat-loop-local status
72
74
  ```
package/bin/cli.mjs CHANGED
@@ -9,6 +9,7 @@ import {CONTRACT,ACTIONS,registry,ensurePrivateDir,paths,
9
9
  safeFallback,recoverStaleLock,exitCode,talk} from '../lib/control.mjs';
10
10
  import {runGateway} from '../lib/gateway.mjs';
11
11
  import {tunnelDoctor,runTunnel} from '../lib/tunnel.mjs';
12
+ import {promptTunnelRuntimeKey,storeTunnelRuntimeKey} from '../lib/tunnel-auth.mjs';
12
13
 
13
14
  const require=createRequire(import.meta.url);
14
15
  const command=process.argv[2]??'doctor';
@@ -30,7 +31,7 @@ function inspect(){
30
31
  }catch{return {status:'BLOCKED',reason:'MCP_DEPENDENCY_NOT_INSTALLED'};}
31
32
  }
32
33
  async function connectOrLaunch(request){
33
- try{return await talk(request);}
34
+ try{return await talk(request,request.deadlineMs+2000);}
34
35
  catch(err){
35
36
  if(!['ENOENT','ECONNREFUSED','TIMEOUT','UNKNOWN_RESULT'].includes(err.code))throw err;
36
37
  }
@@ -51,7 +52,7 @@ async function connectOrLaunch(request){
51
52
  let last;
52
53
  for(let i=0;i<60;i++){
53
54
  await new Promise(resolve=>setTimeout(resolve,65));
54
- try{return await talk(request);}
55
+ try{return await talk(request,request.deadlineMs+2000);}
55
56
  catch(err){
56
57
  last=err;
57
58
  if(err.code==='INCOMPATIBLE_VERSION')throw err;
@@ -70,7 +71,9 @@ async function runControl(action=command, params=extra){
70
71
  if(action!=='status' && !target && !registry().some(t=>t.enabled)){
71
72
  const reply=fail('NO_ENABLED_TOOLS',action,target,requestId);emit(reply);process.exitCode=5;return;
72
73
  }
73
- const req={contract:CONTRACT,requestId,action,scope:target?'tool':'all',deadlineMs:12000};
74
+ // Real Desktop Commander cold startup can exceed 12s on loaded macOS.
75
+ const req={contract:CONTRACT,requestId,action,scope:target?'tool':'all',
76
+ deadlineMs:action==='start'||action==='restart'?25000:12000};
74
77
  if(target)req.toolId=target;
75
78
  let reply;
76
79
  try{
@@ -105,13 +108,23 @@ else if(command==='doctor'){
105
108
  }else if(extra.length===1&&extra[0]==='run'){
106
109
  // Compatibility alias: all tunnel execution now belongs to the Supervisor.
107
110
  await runControl('start',['tunnel']);
111
+ }else if(extra.length===1&&extra[0]==='auth'){
112
+ try{
113
+ const key=await promptTunnelRuntimeKey();
114
+ emit({contract:'chat-loop.local-tools.v1',command,action:'auth',
115
+ ...storeTunnelRuntimeKey(key)});
116
+ }catch(e){
117
+ emit({contract:'chat-loop.local-tools.v1',command,action:'auth',
118
+ status:'FAILED',code:e.code||'CREDENTIAL_SETUP_FAILED'});
119
+ process.exitCode=1;
120
+ }
108
121
  }else if(extra.length===1&&extra[0]==='--version'){
109
122
  try{await runTunnel(['--version']);}
110
123
  catch(e){emit({contract:'chat-loop.local-tools.v1',command,status:'FAILED',
111
124
  code:e.code||'TUNNEL_EXEC_FAILED'});process.exitCode=1;}
112
125
  }else{
113
126
  emit({contract:'chat-loop.local-tools.v1',command,status:'FAILED',
114
- code:'INVALID_ARGUMENT',usage:'tunnel doctor | tunnel --version | tunnel run (managed)'});
127
+ code:'INVALID_ARGUMENT',usage:'tunnel doctor | tunnel --version | tunnel auth | tunnel run (managed)'});
115
128
  process.exitCode=2;
116
129
  }
117
130
  }else if(command==='capabilities'){
@@ -138,6 +138,7 @@ export function createTunnelAdapter({records,log},options={}){
138
138
  const rec=records.get(t.id),before=rec?.state||'STOPPED';
139
139
  if(!rec||rec.exited){
140
140
  if(isMarked())return result(t,before,'UNKNOWN','OWNERSHIP_UNCERTAIN',err('OWNERSHIP_UNCERTAIN'),null,'ORPHAN_MARKER');
141
+ if(rec)rec.state='STOPPED';
141
142
  return result(t,before,'STOPPED','ALREADY_STOPPED',null,null,'NO_LIVE_OWNED_CHILD');
142
143
  }
143
144
  if(ctx.remaining()<100)return result(t,before,before,'DEADLINE_EXPIRED',err('TIMEOUT'),rec.pid,'NO_SIGNAL_SENT');
package/lib/gateway.mjs CHANGED
@@ -46,7 +46,7 @@ export function runGateway(){
46
46
  return reject(m.id,-32602,'UNSUPPORTED_MCP_VERSION');
47
47
  connected=true;
48
48
  return reply(m.id,{protocolVersion:m.params.protocolVersion,
49
- capabilities:{tools:{}},serverInfo:{name:'chat-loop-local-tools',version:'0.4.7'}});
49
+ capabilities:{tools:{}},serverInfo:{name:'chat-loop-local-tools',version:'0.4.8'}});
50
50
  }
51
51
  // A modern MCP client probes discovery before falling back to legacy initialize.
52
52
  // We only implement the legacy handshake era, so report unsupported method.
@@ -0,0 +1,64 @@
1
+ // One-time Chat Loop runtime credential storage. Never inspect Local Dev's secrets.
2
+ import {randomUUID} from 'node:crypto';
3
+ import {existsSync,lstatSync,mkdirSync,readFileSync,renameSync,unlinkSync,writeFileSync} from 'node:fs';
4
+ import {homedir} from 'node:os';
5
+ import {join} from 'node:path';
6
+ import {Writable} from 'node:stream';
7
+ import {createInterface} from 'node:readline';
8
+
9
+ const fail=(code)=>Object.assign(new Error(code),{code});
10
+ function privateFile(path){
11
+ const stat=lstatSync(path);
12
+ if(!stat.isFile()||stat.isSymbolicLink()||(stat.mode&0o077)||
13
+ (process.getuid&&stat.uid!==process.getuid()))throw fail('INSECURE_PROFILE');
14
+ }
15
+ function privateDirectory(path){
16
+ const stat=lstatSync(path);
17
+ if(!stat.isDirectory()||stat.isSymbolicLink()||(stat.mode&0o077)||
18
+ (process.getuid&&stat.uid!==process.getuid()))throw fail('INSECURE_CREDENTIAL_DIR');
19
+ }
20
+ function putAtomic(path,contents){
21
+ const temp=path+'.'+randomUUID()+'.tmp';
22
+ try{
23
+ writeFileSync(temp,contents,{flag:'wx',mode:0o600});
24
+ renameSync(temp,path);
25
+ }finally{if(existsSync(temp))unlinkSync(temp);}
26
+ }
27
+ export function storeTunnelRuntimeKey(key,{root=join(homedir(),'.chat-loop','tunnel')}={}){
28
+ if(typeof key!=='string'||!key||key!==key.trim()||key.length>4096||
29
+ /[\r\n\x00-\x1f\x7f]/.test(key))throw fail('INVALID_CREDENTIAL');
30
+ const profile=join(root,'profiles','chat-loop.yaml');
31
+ try{privateFile(profile);}catch(e){
32
+ throw fail(e.code==='ENOENT'?'PROFILE_NOT_FOUND':e.code||'INSECURE_PROFILE');
33
+ }
34
+ const original=readFileSync(profile,'utf8');
35
+ const secretDir=join(root,'secrets'),secretPath=join(secretDir,'runtime-api-key');
36
+ const oldRef='env:CONTROL_PLANE_API_KEY',fileRef='file:'+secretPath;
37
+ if(original.includes(oldRef)){
38
+ if(original.split(oldRef).length!==2)throw fail('AMBIGUOUS_AUTH_REFERENCE');
39
+ }else if(!original.includes(fileRef))throw fail('FOREIGN_AUTH_REFERENCE');
40
+ if(!existsSync(secretDir))mkdirSync(secretDir,{recursive:true,mode:0o700});
41
+ privateDirectory(secretDir);
42
+ if(existsSync(secretPath))privateFile(secretPath);
43
+ const updated=original.replace(oldRef,fileRef);
44
+ // The secret is never written to argv, the profile or the CLI result.
45
+ putAtomic(secretPath,key);
46
+ if(updated!==original)putAtomic(profile,updated);
47
+ return {status:'CREDENTIAL_STORED',connection:'UNVERIFIED',profile};
48
+ }
49
+ export async function promptTunnelRuntimeKey(){
50
+ if(!process.stdin.isTTY||!process.stderr.isTTY)throw fail('INTERACTIVE_TTY_REQUIRED');
51
+ // readline enters raw terminal mode; its output is muted so pasted keys are not echoed.
52
+ const silent=new Writable({write(_chunk,_encoding,callback){callback();}});
53
+ const input=createInterface({input:process.stdin,output:silent,terminal:true});
54
+ try{
55
+ process.stderr.write('Paste the restricted Chat Loop runtime key (input hidden), then Enter: ');
56
+ return await new Promise((resolve,reject)=>{
57
+ input.question('',answer=>resolve(answer));
58
+ input.once('SIGINT',()=>reject(fail('CANCELLED')));
59
+ });
60
+ }finally{
61
+ input.close();
62
+ process.stderr.write('\n');
63
+ }
64
+ }
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "0.4.7",
6
+ "version": "0.4.9",
7
7
  "type": "module",
8
8
  "description": "Chat Loop local tools: MCP gateway, isolated bundled tunnel-client and unified CLI",
9
9
  "bin": {