pikiloom 0.4.15 → 0.4.16

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.
@@ -17,6 +17,7 @@ import os from 'node:os';
17
17
  import path from 'node:path';
18
18
  import { spawn } from 'node:child_process';
19
19
  import { loadUserConfig, saveUserConfig } from '../../core/config/user-config.js';
20
+ import { terminateProcessTree } from '../../core/process-control.js';
20
21
  import { getRecommendedMcpServers, } from './registry.js';
21
22
  import { hasValidMcpToken, injectOAuthHeaders } from './oauth.js';
22
23
  // ---------------------------------------------------------------------------
@@ -548,6 +549,7 @@ export async function checkMcpHealth(config, timeoutMs = 10_000) {
548
549
  return new Promise((resolve) => {
549
550
  const start = Date.now();
550
551
  let checkInterval = null;
552
+ let settled = false;
551
553
  const cleanup = () => {
552
554
  if (checkInterval) {
553
555
  clearInterval(checkInterval);
@@ -555,20 +557,29 @@ export async function checkMcpHealth(config, timeoutMs = 10_000) {
555
557
  }
556
558
  clearTimeout(timer);
557
559
  };
560
+ const stopChildTree = () => {
561
+ terminateProcessTree(child, { signal: 'SIGTERM', forceSignal: 'SIGKILL', forceAfterMs: 1500 });
562
+ };
563
+ const finish = (result) => {
564
+ if (settled)
565
+ return;
566
+ settled = true;
567
+ cleanup();
568
+ resolve(result);
569
+ };
558
570
  const child = spawn(config.command, config.args || [], {
559
571
  stdio: ['pipe', 'pipe', 'pipe'],
560
572
  env: { ...process.env, ...config.env },
573
+ detached: process.platform !== 'win32',
561
574
  });
562
575
  const timer = setTimeout(() => {
563
- cleanup();
564
- child.kill();
565
- resolve({ ok: false, error: `timeout after ${timeoutMs}ms`, elapsedMs: Date.now() - start });
576
+ stopChildTree();
577
+ finish({ ok: false, error: `timeout after ${timeoutMs}ms`, elapsedMs: Date.now() - start });
566
578
  }, timeoutMs);
567
579
  let stdout = '';
568
580
  child.stdout?.on('data', (data) => { stdout += data.toString(); });
569
581
  child.on('error', (err) => {
570
- cleanup();
571
- resolve({ ok: false, error: err.message, elapsedMs: Date.now() - start });
582
+ finish({ ok: false, error: err.message, elapsedMs: Date.now() - start });
572
583
  });
573
584
  const initRequest = JSON.stringify({
574
585
  jsonrpc: '2.0',
@@ -585,8 +596,8 @@ export async function checkMcpHealth(config, timeoutMs = 10_000) {
585
596
  child.stdin?.write(header + initRequest);
586
597
  }
587
598
  catch {
588
- cleanup();
589
- resolve({ ok: false, error: 'failed to write to stdin', elapsedMs: Date.now() - start });
599
+ stopChildTree();
600
+ finish({ ok: false, error: 'failed to write to stdin', elapsedMs: Date.now() - start });
590
601
  return;
591
602
  }
592
603
  checkInterval = setInterval(() => {
@@ -606,7 +617,7 @@ export async function checkMcpHealth(config, timeoutMs = 10_000) {
606
617
  }
607
618
  catch { /* best-effort */ }
608
619
  setTimeout(() => {
609
- child.kill();
620
+ stopChildTree();
610
621
  const tools = [];
611
622
  try {
612
623
  const jsonMatches = stdout.match(/\{[^{}]*"tools"\s*:\s*\[[\s\S]*?\]\s*[^{}]*\}/g);
@@ -630,7 +641,7 @@ export async function checkMcpHealth(config, timeoutMs = 10_000) {
630
641
  }
631
642
  }
632
643
  catch { /* best effort */ }
633
- resolve({ ok: true, tools: tools.length ? tools : undefined, elapsedMs: Date.now() - start });
644
+ finish({ ok: true, tools: tools.length ? tools : undefined, elapsedMs: Date.now() - start });
634
645
  }, 1500);
635
646
  }, 100);
636
647
  });
@@ -473,7 +473,7 @@ export async function doStream(opts) {
473
473
  // Start MCP bridge for IM tools (when sendFile is available) and/or supplemental servers (browser, etc.)
474
474
  let bridge = null;
475
475
  try {
476
- const { startMcpBridge } = await import('./mcp/bridge.js');
476
+ const { startMcpBridge, redactMcpConfigForLog } = await import('./mcp/bridge.js');
477
477
  const sessionDir = path.dirname(session.workspacePath);
478
478
  bridge = await startMcpBridge({
479
479
  sessionDir,
@@ -498,7 +498,8 @@ export async function doStream(opts) {
498
498
  else
499
499
  agentLog('[mcp] bridge registered with codex');
500
500
  try {
501
- agentLog(`[mcp] config content:\n${fs.readFileSync(bridge.configPath, 'utf-8')}`);
501
+ if (bridge.configPath)
502
+ agentLog(`[mcp] config content:\n${redactMcpConfigForLog(bridge.configPath)}`);
502
503
  }
503
504
  catch { }
504
505
  ;
@@ -6,8 +6,9 @@ import fs from 'node:fs';
6
6
  import path from 'node:path';
7
7
  import os from 'node:os';
8
8
  import { spawn, spawnSync } from 'node:child_process';
9
+ import { fileURLToPath } from 'node:url';
9
10
  import { loadUserConfig, saveUserConfig, applyUserConfig, hasUserConfigFile } from '../../core/config/user-config.js';
10
- import { expandTilde } from '../../core/platform.js';
11
+ import { expandTilde, whichSync } from '../../core/platform.js';
11
12
  import { readGitStatus } from '../../core/git.js';
12
13
  import { isSetupReady } from '../../cli/onboarding.js';
13
14
  import { validateDingtalkConfig, validateDiscordConfig, validateFeishuConfig, validateSlackConfig, validateTelegramConfig, validateWecomConfig, validateWeixinConfig, } from '../../core/config/validation.js';
@@ -66,7 +67,96 @@ function runOpenCommand(command, args) {
66
67
  throw new Error(detail || `Failed to run ${command} ${args.join(' ')}`);
67
68
  }
68
69
  }
69
- function openPathWithTarget(filePath, target, isDirectory) {
70
+ function stripOpenPathWrapping(value) {
71
+ let text = value.trim();
72
+ const pairs = [['`', '`'], ['"', '"'], ["'", "'"], ['<', '>']];
73
+ let changed = true;
74
+ while (changed && text.length >= 2) {
75
+ changed = false;
76
+ for (const [left, right] of pairs) {
77
+ if (text.startsWith(left) && text.endsWith(right)) {
78
+ text = text.slice(left.length, -right.length).trim();
79
+ changed = true;
80
+ }
81
+ }
82
+ }
83
+ return text;
84
+ }
85
+ function decodeOpenPathInput(raw) {
86
+ const text = stripOpenPathWrapping(raw);
87
+ if (text.startsWith('file://')) {
88
+ try {
89
+ return fileURLToPath(text);
90
+ }
91
+ catch {
92
+ return decodeURI(text.slice('file://'.length));
93
+ }
94
+ }
95
+ if (text.startsWith('vscode://file/')) {
96
+ return decodeURI(`/${text.slice('vscode://file/'.length)}`);
97
+ }
98
+ return text;
99
+ }
100
+ function resolveOpenBasePath(basePath) {
101
+ const base = typeof basePath === 'string' && basePath.trim()
102
+ ? basePath.trim()
103
+ : runtime.getRuntimeWorkdir(loadUserConfig());
104
+ return path.resolve(expandTilde(base || process.cwd()));
105
+ }
106
+ function splitExistingLineSuffix(candidate) {
107
+ const normalized = path.normalize(candidate);
108
+ if (fs.existsSync(normalized))
109
+ return { filePath: normalized, line: null, column: null };
110
+ const match = /^(.*?)(?::(\d+)(?::(\d+))?)$/.exec(normalized);
111
+ if (!match || !match[1])
112
+ return { filePath: normalized, line: null, column: null };
113
+ const filePath = path.normalize(match[1]);
114
+ if (!fs.existsSync(filePath))
115
+ return { filePath: normalized, line: null, column: null };
116
+ return {
117
+ filePath,
118
+ line: Number(match[2]),
119
+ column: match[3] ? Number(match[3]) : null,
120
+ };
121
+ }
122
+ export function resolveOpenPathLocator(rawPath, basePath) {
123
+ const decoded = decodeOpenPathInput(rawPath);
124
+ const expanded = expandTilde(decoded);
125
+ const absolute = path.isAbsolute(expanded)
126
+ ? path.resolve(expanded)
127
+ : path.resolve(resolveOpenBasePath(basePath), expanded);
128
+ return splitExistingLineSuffix(absolute);
129
+ }
130
+ function editorGotoArg(filePath, location) {
131
+ if (!location?.line)
132
+ return null;
133
+ return `${filePath}:${location.line}${location.column ? `:${location.column}` : ''}`;
134
+ }
135
+ function tryOpenCommand(command, args) {
136
+ if (!whichSync(command))
137
+ return false;
138
+ try {
139
+ runOpenCommand(command, args);
140
+ return true;
141
+ }
142
+ catch {
143
+ return false;
144
+ }
145
+ }
146
+ function tryOpenVSCodeUrl(filePath, location) {
147
+ if (!location?.line)
148
+ return false;
149
+ const suffix = `:${location.line}${location.column ? `:${location.column}` : ''}`;
150
+ try {
151
+ runOpenCommand('open', [`vscode://file${encodeURI(filePath)}${suffix}`]);
152
+ return true;
153
+ }
154
+ catch {
155
+ return false;
156
+ }
157
+ }
158
+ function openPathWithTarget(filePath, target, isDirectory, location) {
159
+ const gotoArg = isDirectory ? null : editorGotoArg(filePath, location);
70
160
  if (process.platform === 'darwin') {
71
161
  switch (target) {
72
162
  case 'finder':
@@ -76,13 +166,21 @@ function openPathWithTarget(filePath, target, isDirectory) {
76
166
  runOpenCommand('open', [filePath]);
77
167
  return;
78
168
  case 'cursor':
169
+ if (gotoArg && tryOpenCommand('cursor', ['-g', gotoArg]))
170
+ return;
79
171
  runOpenCommand('open', ['-a', 'Cursor', filePath]);
80
172
  return;
81
173
  case 'windsurf':
174
+ if (gotoArg && tryOpenCommand('windsurf', ['-g', gotoArg]))
175
+ return;
82
176
  runOpenCommand('open', ['-a', 'Windsurf', filePath]);
83
177
  return;
84
178
  case 'vscode':
85
179
  default:
180
+ if (gotoArg && tryOpenCommand('code', ['-g', gotoArg]))
181
+ return;
182
+ if (gotoArg && tryOpenVSCodeUrl(filePath, location))
183
+ return;
86
184
  runOpenCommand('open', ['-a', 'Visual Studio Code', filePath]);
87
185
  return;
88
186
  }
@@ -90,10 +188,16 @@ function openPathWithTarget(filePath, target, isDirectory) {
90
188
  if (process.platform === 'win32') {
91
189
  switch (target) {
92
190
  case 'cursor':
93
- runOpenCommand('cursor', [filePath]);
191
+ if (gotoArg)
192
+ runOpenCommand('cursor', ['-g', gotoArg]);
193
+ else
194
+ runOpenCommand('cursor', [filePath]);
94
195
  return;
95
196
  case 'windsurf':
96
- runOpenCommand('windsurf', [filePath]);
197
+ if (gotoArg)
198
+ runOpenCommand('windsurf', ['-g', gotoArg]);
199
+ else
200
+ runOpenCommand('windsurf', [filePath]);
97
201
  return;
98
202
  case 'finder':
99
203
  case 'default':
@@ -101,16 +205,25 @@ function openPathWithTarget(filePath, target, isDirectory) {
101
205
  return;
102
206
  case 'vscode':
103
207
  default:
104
- runOpenCommand('code', [filePath]);
208
+ if (gotoArg)
209
+ runOpenCommand('code', ['-g', gotoArg]);
210
+ else
211
+ runOpenCommand('code', [filePath]);
105
212
  return;
106
213
  }
107
214
  }
108
215
  switch (target) {
109
216
  case 'cursor':
110
- runOpenCommand('cursor', [filePath]);
217
+ if (gotoArg)
218
+ runOpenCommand('cursor', ['-g', gotoArg]);
219
+ else
220
+ runOpenCommand('cursor', [filePath]);
111
221
  return;
112
222
  case 'windsurf':
113
- runOpenCommand('windsurf', [filePath]);
223
+ if (gotoArg)
224
+ runOpenCommand('windsurf', ['-g', gotoArg]);
225
+ else
226
+ runOpenCommand('windsurf', [filePath]);
114
227
  return;
115
228
  case 'finder':
116
229
  case 'default':
@@ -118,7 +231,10 @@ function openPathWithTarget(filePath, target, isDirectory) {
118
231
  return;
119
232
  case 'vscode':
120
233
  default:
121
- runOpenCommand('code', [filePath]);
234
+ if (gotoArg)
235
+ runOpenCommand('code', ['-g', gotoArg]);
236
+ else
237
+ runOpenCommand('code', [filePath]);
122
238
  return;
123
239
  }
124
240
  }
@@ -453,14 +569,20 @@ app.post('/api/open-in-editor', async (c) => {
453
569
  try {
454
570
  const body = await c.req.json();
455
571
  const filePath = typeof body?.filePath === 'string' ? body.filePath.trim() : '';
572
+ const basePath = typeof body?.basePath === 'string' && body.basePath.trim()
573
+ ? body.basePath.trim()
574
+ : typeof body?.workdir === 'string' && body.workdir.trim()
575
+ ? body.workdir.trim()
576
+ : null;
456
577
  const target = isOpenTarget(body?.target) ? body.target : 'vscode';
457
578
  if (!filePath)
458
579
  return c.json({ ok: false, error: 'filePath is required' }, 400);
459
- if (!fs.existsSync(filePath))
580
+ const resolved = resolveOpenPathLocator(filePath, basePath);
581
+ if (!fs.existsSync(resolved.filePath))
460
582
  return c.json({ ok: false, error: 'Path not found' }, 404);
461
- const stat = fs.statSync(filePath);
462
- openPathWithTarget(filePath, target, stat.isDirectory());
463
- return c.json({ ok: true });
583
+ const stat = fs.statSync(resolved.filePath);
584
+ openPathWithTarget(resolved.filePath, target, stat.isDirectory(), resolved);
585
+ return c.json({ ok: true, filePath: resolved.filePath, line: resolved.line, column: resolved.column });
464
586
  }
465
587
  catch (err) {
466
588
  const detail = err instanceof Error ? err.message : String(err);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pikiloom",
3
- "version": "0.4.15",
3
+ "version": "0.4.16",
4
4
  "description": "Put the world's smartest AI agents in your pocket. Command local Claude & Gemini via IM. | 让最好用的 IM 变成你电脑上的顶级 Agent 控制台",
5
5
  "type": "module",
6
6
  "bin": {