@tomflow/chatloop-tools 0.4.11 → 0.4.13

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.
@@ -3,7 +3,7 @@ import {createRequire} from 'node:module';
3
3
  import {dirname,join} from 'node:path';
4
4
  import {mkdirSync,readFileSync} from 'node:fs';
5
5
  import {randomUUID} from 'node:crypto';
6
- import {paths} from '../control.mjs';
6
+ import {paths,toolCallDeadlineMs} from '../control.mjs';
7
7
 
8
8
  const require=createRequire(import.meta.url);
9
9
  const catalog=JSON.parse(readFileSync(new URL('../desktop-commander-tools.json',import.meta.url),'utf8'));
@@ -11,7 +11,10 @@ const fingerprint=tools=>JSON.stringify(tools.map(({name,inputSchema})=>({name,i
11
11
  const expected=fingerprint(catalog);
12
12
  const err=code=>({code});
13
13
  const result=(tool,before,after,outcome,error=null,pid=null,evidence=null)=>({toolId:tool.id,enabled:tool.enabled,before,after,outcome,pid,evidence,error});
14
+ // Cold Desktop Commander initialization can exceed 9s on a busy macOS host.
15
+ // Startup has its own budget; ordinary tool calls keep the stricter deadline.
14
16
  const timeout=ctx=>Math.max(0,Math.min(ctx.remaining(),9000));
17
+ export const startupHandshakeBudget=ctx=>Math.max(0,Math.min(ctx.remaining(),24000));
15
18
  function entryFor(tool){
16
19
  if(tool.id!=='desktop-commander')throw Object.assign(Error('Unknown MCP child'),{code:'UNKNOWN_TOOL'});
17
20
  const pkg=require.resolve('@wonderwhy-er/desktop-commander/package.json');
@@ -69,7 +72,7 @@ export function createMcpChildAdapter({records,log}){
69
72
  if(health.after==='RUNNING')return result(tool,before,'RUNNING','ALREADY_RUNNING',null,old.pid,'MCP_TOOLS_LIST');
70
73
  return result(tool,before,'UNKNOWN','IDENTITY_UNCERTAIN',health.error,old.pid);
71
74
  }
72
- if(timeout(ctx)<100)return result(tool,before,before,'DEADLINE_EXPIRED',err('TIMEOUT'));
75
+ if(startupHandshakeBudget(ctx)<100)return result(tool,before,before,'DEADLINE_EXPIRED',err('TIMEOUT'));
73
76
  const home=join(paths().dir,'tools',tool.id,'home');
74
77
  mkdirSync(home,{recursive:true,mode:0o700});
75
78
  const env={HOME:home,PATH:process.env.PATH||'/usr/bin:/bin',LANG:process.env.LANG||'en_US.UTF-8',
@@ -80,10 +83,10 @@ export function createMcpChildAdapter({records,log}){
80
83
  const rec=setup(tool,child);
81
84
  try{
82
85
  const info=await request(rec,'initialize',{protocolVersion:'2025-03-26',
83
- capabilities:{},clientInfo:{name:'chat-loop-local-tools',version:'0.4.4'}},timeout(ctx));
86
+ capabilities:{},clientInfo:{name:'chat-loop-local-tools',version:'0.4.4'}},startupHandshakeBudget(ctx));
84
87
  if(!info?.protocolVersion)throw Object.assign(Error('Handshake failed'),{code:'MCP_HANDSHAKE_FAILED'});
85
88
  child.stdin.write(JSON.stringify({jsonrpc:'2.0',method:'notifications/initialized'})+'\n');
86
- const listed=await request(rec,'tools/list',{},timeout(ctx));
89
+ const listed=await request(rec,'tools/list',{},startupHandshakeBudget(ctx));
87
90
  if(!listed||fingerprint(listed.tools||[])!==expected)
88
91
  throw Object.assign(Error('Tool schema changed'),{code:'SCHEMA_MISMATCH'});
89
92
  rec.state='RUNNING';log('tool-start',{requestId:ctx.requestId,toolId:tool.id,pid:rec.pid});
@@ -128,7 +131,9 @@ export function createMcpChildAdapter({records,log}){
128
131
  if(!catalog.some(t=>t.name===name))throw Object.assign(Error('No such tool'),{code:'UNKNOWN_TOOL'});
129
132
  rec.active++;
130
133
  try{
131
- return await request(rec,'tools/call',{name,arguments:args},timeout(ctx));
134
+ // Leave time for IPC and Gateway acknowledgements; never exceed the caller's deadline.
135
+ const ms=Math.max(0,Math.min(ctx.remaining(),toolCallDeadlineMs(args)-2000));
136
+ return await request(rec,'tools/call',{name,arguments:args},ms);
132
137
  }catch(e){if(e.code==='TIMEOUT')rec.state='UNKNOWN';throw e;}
133
138
  finally{rec.active--;}
134
139
  }
package/lib/control.mjs CHANGED
@@ -5,6 +5,11 @@ import {join} from 'node:path';
5
5
 
6
6
  export const CONTRACT = 'chat-loop.local-control.v1';
7
7
  export const VERSION = '0.4.7';
8
+ // Bounded end-to-end tool calls: long-running Desktop Commander operations may wait up to timeout_ms.
9
+ export const toolCallDeadlineMs = args => {
10
+ const requested = Number.isSafeInteger(args?.timeout_ms) && args.timeout_ms > 0 ? args.timeout_ms : 0;
11
+ return Math.min(42000, Math.max(12000, requested + 8000));
12
+ };
8
13
  export const ACTIONS = new Set(['start','stop','restart','status']);
9
14
  export const directory = () => process.env.CHAT_LOOP_LOCAL_TOOLS_STATE_DIR || join(homedir(), '.chat-loop', 'local-tools');
10
15
  export const paths = () => ({dir: directory(), socket: join(directory(), 'control.sock'), lock: join(directory(), 'manager.lock'), log: join(directory(), 'events.jsonl')});
package/lib/gateway.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  // Stdio MCP entrypoint: protocol framing and routing only; never owns child processes.
2
2
  import {randomUUID} from 'node:crypto';
3
3
  import {readFileSync} from 'node:fs';
4
- import {CONTRACT,registry,talk} from './control.mjs';
4
+ import {CONTRACT,registry,talk,toolCallDeadlineMs} from './control.mjs';
5
5
  import {extensionTools,forwardExtensionAction} from './extension-proxy.mjs';
6
6
 
7
7
  const desktopTools=JSON.parse(readFileSync(new URL('./desktop-commander-tools.json',import.meta.url),'utf8'));
@@ -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.11'}});
49
+ capabilities:{tools:{}},serverInfo:{name:'chat-loop-local-tools',version:'0.4.13'}});
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.
@@ -63,8 +63,9 @@ export function runGateway(){
63
63
  try{
64
64
  if(routed.toolId==='chat-loop-extension')
65
65
  return reply(m.id,await forwardExtensionAction(routed.name,args));
66
+ const deadlineMs=toolCallDeadlineMs(args);
66
67
  const res=await talk({contract:CONTRACT,requestId:randomUUID(),action:'call',
67
- scope:'tool',toolId:routed.toolId,name:routed.name,args,deadlineMs:12000},13000);
68
+ scope:'tool',toolId:routed.toolId,name:routed.name,args,deadlineMs},deadlineMs+2000);
68
69
  if(res.status!=='OK')return reply(m.id,failure(res.error?.code||'TOOL_UNAVAILABLE'));
69
70
  return reply(m.id,res.result);
70
71
  }catch(e){
@@ -40,7 +40,7 @@ async function executeCall(req){
40
40
  if(typeof req.requestId!=='string'||req.requestId.length<6||req.requestId.length>128||
41
41
  req.scope!=='tool'||typeof req.toolId!=='string'||typeof req.name!=='string'||
42
42
  !req.args||typeof req.args!=='object'||Array.isArray(req.args)||
43
- !Number.isInteger(req.deadlineMs)||req.deadlineMs<1000||req.deadlineMs>30000)return invalid();
43
+ !Number.isInteger(req.deadlineMs)||req.deadlineMs<1000||req.deadlineMs>45000)return invalid();
44
44
  const tool=catalog.find(t=>t.id===req.toolId);
45
45
  if(!tool)return bad(req,'UNKNOWN_TOOL');
46
46
  if(!tool.enabled)return bad(req,'DISABLED_TOOL');
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "0.4.11",
6
+ "version": "0.4.13",
7
7
  "type": "module",
8
8
  "description": "Chat Loop local tools: MCP gateway, isolated bundled tunnel-client and unified CLI",
9
9
  "bin": {