machine-bridge-mcp 0.2.4 → 0.3.3

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.
@@ -5,44 +5,98 @@ import { packageRoot } from "./state.mjs";
5
5
 
6
6
  export function run(command, args = [], options = {}) {
7
7
  return new Promise((resolve, reject) => {
8
+ const capture = Boolean(options.capture);
9
+ const maxOutputBytes = Number.isFinite(Number(options.maxOutputBytes)) ? Math.max(1024, Number(options.maxOutputBytes)) : 2 * 1024 * 1024;
8
10
  const child = spawn(command, args, {
9
11
  cwd: options.cwd || process.cwd(),
10
12
  env: options.env || process.env,
11
- stdio: options.capture ? ["ignore", "pipe", "pipe"] : "inherit",
13
+ stdio: capture ? ["ignore", "pipe", "pipe"] : "inherit",
12
14
  shell: false,
15
+ windowsHide: true,
13
16
  });
14
17
  let stdout = "";
15
18
  let stderr = "";
16
- if (options.capture) {
17
- child.stdout?.on("data", chunk => { stdout += String(chunk); });
18
- child.stderr?.on("data", chunk => { stderr += String(chunk); });
19
+ let stdoutTruncated = 0;
20
+ let stderrTruncated = 0;
21
+ let settled = false;
22
+ let timedOut = false;
23
+ let timer = null;
24
+ let killTimer = null;
25
+ const timeoutMs = Number(options.timeoutMs);
26
+ if (Number.isFinite(timeoutMs) && timeoutMs > 0) {
27
+ timer = setTimeout(() => {
28
+ timedOut = true;
29
+ try { child.kill("SIGTERM"); } catch {}
30
+ killTimer = setTimeout(() => { try { child.kill("SIGKILL"); } catch {} }, 2000);
31
+ killTimer.unref?.();
32
+ }, timeoutMs);
33
+ timer.unref?.();
19
34
  }
20
- child.on("error", error => {
21
- if (options.allowFailure) resolve({ code: 127, stdout, stderr: error.message });
35
+ const finish = callback => {
36
+ if (settled) return;
37
+ settled = true;
38
+ if (timer) clearTimeout(timer);
39
+ if (killTimer) clearTimeout(killTimer);
40
+ callback();
41
+ };
42
+ if (capture) {
43
+ child.stdout?.on("data", chunk => {
44
+ const next = appendLimited(stdout, chunk, maxOutputBytes);
45
+ stdout = next.value;
46
+ stdoutTruncated += next.truncated;
47
+ });
48
+ child.stderr?.on("data", chunk => {
49
+ const next = appendLimited(stderr, chunk, maxOutputBytes);
50
+ stderr = next.value;
51
+ stderrTruncated += next.truncated;
52
+ });
53
+ }
54
+ child.on("error", error => finish(() => {
55
+ const result = { code: 127, stdout: finalizeOutput(stdout, stdoutTruncated), stderr: finalizeOutput(error.message || stderr, stderrTruncated) };
56
+ if (options.allowFailure) resolve(result);
22
57
  else reject(error);
23
- });
24
- child.on("close", code => {
25
- const result = { code, stdout, stderr };
26
- if (code === 0 || options.allowFailure) resolve(result);
58
+ }));
59
+ child.on("close", code => finish(() => {
60
+ const timeoutMessage = timedOut ? `command timed out after ${timeoutMs}ms` : "";
61
+ const result = {
62
+ code: timedOut ? 124 : code,
63
+ stdout: finalizeOutput(stdout, stdoutTruncated),
64
+ stderr: finalizeOutput([stderr, timeoutMessage].filter(Boolean).join("\n"), stderrTruncated),
65
+ };
66
+ if ((!timedOut && code === 0) || options.allowFailure) resolve(result);
27
67
  else {
28
- const error = new Error((stderr || stdout || `${command} exited ${code}`).trim());
68
+ const error = new Error((result.stderr || result.stdout || `${command} exited ${result.code}`).trim());
29
69
  error.result = result;
30
70
  reject(error);
31
71
  }
32
- });
72
+ }));
33
73
  });
34
74
  }
35
75
 
76
+ function appendLimited(current, chunk, max) {
77
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk ?? ""));
78
+ const budget = Math.max(0, max - Buffer.byteLength(current));
79
+ if (buffer.length <= budget) return { value: current + buffer.toString("utf8"), truncated: 0 };
80
+ const slice = buffer.subarray(0, budget).toString("utf8");
81
+ return { value: current + slice, truncated: buffer.length - Buffer.byteLength(slice) };
82
+ }
83
+
84
+ function finalizeOutput(value, truncated) {
85
+ return truncated > 0 ? `${value}\n\n[truncated ${truncated} bytes]` : value;
86
+ }
87
+
36
88
  export function findWranglerCommand() {
37
89
  const suffix = process.platform === "win32" ? ".cmd" : "";
38
90
  const local = path.join(packageRoot, "node_modules", ".bin", `wrangler${suffix}`);
39
91
  if (existsSync(local)) return { cmd: local, argsPrefix: [] };
40
- return { cmd: "npx", argsPrefix: ["wrangler"] };
92
+ throw new Error("Wrangler dependency is not installed. Run `npm install` in the package/source directory and retry.");
41
93
  }
42
94
 
43
95
  export async function runWrangler(args, options = {}) {
44
96
  const wrangler = findWranglerCommand();
45
- return run(wrangler.cmd, [...wrangler.argsPrefix, ...args], { cwd: packageRoot, ...options });
97
+ const operation = String(args[0] || "");
98
+ const timeoutMs = options.timeoutMs ?? (operation === "login" || operation === "deploy" ? 10 * 60 * 1000 : 2 * 60 * 1000);
99
+ return run(wrangler.cmd, [...wrangler.argsPrefix, ...args], { cwd: packageRoot, timeoutMs, ...options });
46
100
  }
47
101
 
48
102
  export function workspaceShellCommand(command) {
@@ -1,11 +1,12 @@
1
1
  import { createHash, randomBytes } from "node:crypto";
2
- import { closeSync, existsSync, mkdirSync, openSync, readFileSync, renameSync, writeFileSync, chmodSync, realpathSync, rmSync, unlinkSync } from "node:fs";
2
+ import { closeSync, existsSync, fsyncSync, lstatSync, mkdirSync, openSync, readFileSync, renameSync, writeFileSync, chmodSync, realpathSync, rmSync, unlinkSync, readdirSync, statSync } from "node:fs";
3
3
  import os from "node:os";
4
4
  import path from "node:path";
5
5
  import { fileURLToPath } from "node:url";
6
6
 
7
7
  export const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
8
8
  export const appName = "machine-bridge-mcp";
9
+ const STATE_MARKER = ".machine-bridge-mcp-state";
9
10
 
10
11
  export function expandHome(input = "") {
11
12
  if (!input || input === "~") return os.homedir();
@@ -35,20 +36,14 @@ export function configPath(stateRoot = defaultStateRoot()) {
35
36
  export function loadGlobalConfig(stateRoot = defaultStateRoot()) {
36
37
  const file = configPath(stateRoot);
37
38
  if (!existsSync(file)) return {};
38
- try {
39
- const parsed = JSON.parse(readFileSync(file, "utf8"));
40
- return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
41
- } catch {
42
- return {};
43
- }
39
+ ownerOnlyFile(file);
40
+ return readJsonObjectOrBackup(file);
44
41
  }
45
42
 
46
43
  export function saveGlobalConfig(config, stateRoot = defaultStateRoot()) {
47
- const root = expandHome(stateRoot);
48
- ensureOwnerOnlyDir(root);
44
+ const root = ensureStateRoot(expandHome(stateRoot));
49
45
  const file = configPath(root);
50
- writeFileSync(file, `${JSON.stringify({ ...config, updatedAt: new Date().toISOString() }, null, 2)}\n`, { mode: 0o600 });
51
- ownerOnlyFile(file);
46
+ atomicWriteJson(file, { ...config, updatedAt: new Date().toISOString() });
52
47
  }
53
48
 
54
49
  export function setSelectedWorkspace(workspace, stateRoot = defaultStateRoot()) {
@@ -65,9 +60,18 @@ export function selectedWorkspace(stateRoot = defaultStateRoot()) {
65
60
  return typeof value === "string" && value.trim() ? value : "";
66
61
  }
67
62
 
63
+ export function validateStateRootForRemoval(stateRoot = defaultStateRoot()) {
64
+ const root = path.resolve(expandHome(stateRoot));
65
+ if (!existsSync(root)) return { exists: false, root };
66
+ const canonical = assertSafeStateRootForRemoval(root);
67
+ return { exists: true, root: canonical };
68
+ }
69
+
68
70
  export function removeStateRoot(stateRoot = defaultStateRoot()) {
69
- const root = expandHome(stateRoot);
70
- if (existsSync(root)) rmSync(root, { recursive: true, force: true });
71
+ const validation = validateStateRootForRemoval(stateRoot);
72
+ if (!validation.exists) return false;
73
+ rmSync(validation.root, { recursive: true, force: true });
74
+ return true;
71
75
  }
72
76
 
73
77
  export function workspaceHash(workspace) {
@@ -83,15 +87,16 @@ export function statePathForWorkspace(workspace, stateRoot = defaultStateRoot())
83
87
  }
84
88
 
85
89
  export function loadState(workspace, options = {}) {
86
- const stateRoot = options.stateDir ? expandHome(options.stateDir) : defaultStateRoot();
90
+ const stateRoot = ensureStateRoot(options.stateDir ? expandHome(options.stateDir) : defaultStateRoot());
87
91
  const profileDir = profileDirForWorkspace(workspace, stateRoot);
88
92
  const statePath = path.join(profileDir, "state.json");
89
93
  ensureOwnerOnlyDir(profileDir);
90
94
  let state = {};
91
95
  if (existsSync(statePath)) {
96
+ ownerOnlyFile(statePath);
92
97
  state = readJsonObjectOrBackup(statePath);
93
98
  }
94
- state.schemaVersion = 1;
99
+ state.schemaVersion = 2;
95
100
  state.workspace = {
96
101
  path: workspace,
97
102
  hash: workspaceHash(workspace),
@@ -100,39 +105,46 @@ export function loadState(workspace, options = {}) {
100
105
  state.paths = { stateRoot, profileDir, statePath };
101
106
  state.worker ||= {};
102
107
  state.policy ||= {};
103
- migrateLocalApiState(state);
108
+ delete state.localApi;
104
109
  return state;
105
110
  }
106
111
 
107
- function migrateLocalApiState(state) {
108
- if (!state.localApi || typeof state.localApi !== "object" || Array.isArray(state.localApi)) return;
109
- delete state.localApi.upstreamUrl;
110
- delete state.localApi.upstreamKey;
111
- delete state.localApi.upstreamModel;
112
- }
113
-
114
112
  export function saveState(state) {
115
113
  const statePath = state?.paths?.statePath;
116
114
  if (!statePath) throw new Error("state path is missing");
117
115
  ensureOwnerOnlyDir(path.dirname(statePath));
118
- const serializable = { ...state };
119
- writeFileSync(statePath, `${JSON.stringify(serializable, null, 2)}\n`, { mode: 0o600 });
120
- ownerOnlyFile(statePath);
116
+ atomicWriteJson(statePath, { ...state });
121
117
  }
122
118
 
123
119
  export function daemonLockPathForState(state) {
120
+ return lockPathForState(state, "daemon.lock");
121
+ }
122
+
123
+ export function startupLockPathForState(state) {
124
+ return lockPathForState(state, "startup.lock");
125
+ }
126
+
127
+ function lockPathForState(state, name) {
124
128
  const profileDir = state?.paths?.profileDir;
125
129
  if (!profileDir) throw new Error("state profile dir is missing");
126
- return path.join(profileDir, "daemon.lock");
130
+ return path.join(profileDir, name);
127
131
  }
128
132
 
129
133
  export function acquireDaemonLock(state) {
130
- const lockPath = daemonLockPathForState(state);
134
+ return acquireProcessLock(daemonLockPathForState(state), state, "daemon");
135
+ }
136
+
137
+ export function acquireStartupLock(state) {
138
+ return acquireProcessLock(startupLockPathForState(state), state, "startup");
139
+ }
140
+
141
+ function acquireProcessLock(lockPath, state, purpose) {
131
142
  ensureOwnerOnlyDir(path.dirname(lockPath));
132
143
  const token = randomBytes(16).toString("hex");
133
144
  const payload = {
134
145
  pid: process.pid,
135
146
  token,
147
+ purpose,
136
148
  workspace: state?.workspace?.path || "",
137
149
  startedAt: new Date().toISOString(),
138
150
  entryScript: process.argv[1] || "",
@@ -143,15 +155,15 @@ export function acquireDaemonLock(state) {
143
155
  try {
144
156
  fd = openSync(lockPath, "wx", 0o600);
145
157
  writeFileSync(fd, JSON.stringify(payload, null, 2) + "\n");
158
+ fsyncSync(fd);
146
159
  closeSync(fd);
160
+ fd = undefined;
147
161
  ownerOnlyFile(lockPath);
148
162
  return {
149
163
  acquired: true,
150
164
  path: lockPath,
151
165
  owner: payload,
152
- release() {
153
- releaseDaemonLock(lockPath, token);
154
- },
166
+ release() { releaseProcessLock(lockPath, token); },
155
167
  };
156
168
  } catch (error) {
157
169
  if (fd !== undefined) {
@@ -169,7 +181,7 @@ export function acquireDaemonLock(state) {
169
181
  return { acquired: false, path: lockPath, owner, release() {} };
170
182
  }
171
183
 
172
- function releaseDaemonLock(lockPath, token) {
184
+ function releaseProcessLock(lockPath, token) {
173
185
  const owner = readDaemonLockOwner(lockPath);
174
186
  if (owner?.token !== token) return;
175
187
  try { unlinkSync(lockPath); } catch {}
@@ -198,14 +210,199 @@ function isPidAlive(pid) {
198
210
  function readJsonObjectOrBackup(filePath) {
199
211
  try {
200
212
  const parsed = JSON.parse(readFileSync(filePath, "utf8"));
201
- return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
213
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error("JSON root must be an object");
214
+ return parsed;
202
215
  } catch {
203
216
  const backupPath = `${filePath}.corrupt-${Date.now()}`;
204
- try { renameSync(filePath, backupPath); } catch {}
217
+ try {
218
+ renameSync(filePath, backupPath);
219
+ ownerOnlyFile(backupPath);
220
+ pruneBackups(filePath, 3);
221
+ } catch {}
205
222
  return {};
206
223
  }
207
224
  }
208
225
 
226
+ function ensureStateRoot(inputRoot) {
227
+ const root = path.resolve(expandHome(inputRoot));
228
+ if (existsSync(root)) {
229
+ const info = lstatSync(root);
230
+ if (info.isSymbolicLink()) throw new Error(`state root must not be a symbolic link: ${root}`);
231
+ if (!info.isDirectory()) throw new Error(`state root is not a directory: ${root}`);
232
+ } else {
233
+ ensureOwnerOnlyDir(root);
234
+ }
235
+ const marker = path.join(root, STATE_MARKER);
236
+ if (!existsSync(marker)) {
237
+ const entries = readdirSync(root);
238
+ if (entries.length) {
239
+ if (!hasOnlyStateEntries(entries)) {
240
+ throw new Error(`state root must be a dedicated directory; unexpected entries found in ${root}`);
241
+ }
242
+ if (!isRecognizableLegacyStateRoot(root)) {
243
+ throw new Error(`state root is non-empty but does not contain recognizable Machine Bridge state: ${root}`);
244
+ }
245
+ }
246
+ try {
247
+ writeFileSync(marker, `${JSON.stringify({ app: appName, schema: 1 })}\n`, { mode: 0o600, flag: "wx" });
248
+ } catch (error) {
249
+ if (error?.code !== "EEXIST") throw error;
250
+ assertValidStateMarker(marker, { migrateLegacy: true });
251
+ }
252
+ } else {
253
+ assertValidStateMarker(marker, { migrateLegacy: true });
254
+ }
255
+ ensureOwnerOnlyDir(root);
256
+ ownerOnlyFile(marker);
257
+ cleanupStaleAtomicTemps(root);
258
+ return realpathSync(root);
259
+ }
260
+
261
+ function assertSafeStateRootForRemoval(root) {
262
+ const info = lstatSync(root);
263
+ if (info.isSymbolicLink()) throw new Error(`refusing to remove symbolic-link state root: ${root}`);
264
+ const canonical = realpathSync(root);
265
+ const forbidden = new Set([
266
+ path.parse(canonical).root,
267
+ path.resolve(os.homedir()),
268
+ path.resolve(process.cwd()),
269
+ path.resolve(packageRoot),
270
+ ]);
271
+ if (forbidden.has(canonical)) throw new Error(`refusing to remove unsafe state root: ${canonical}`);
272
+ if (looksLikeSourceTree(canonical) || stateRootMatchesRecordedWorkspace(canonical)) {
273
+ throw new Error(`refusing to remove state root that appears to be a workspace: ${canonical}`);
274
+ }
275
+ const entries = readdirSync(canonical);
276
+ if (!hasOnlyStateEntries(entries)) {
277
+ throw new Error(`refusing to remove state root containing unrelated entries: ${canonical}`);
278
+ }
279
+ const marker = path.join(canonical, STATE_MARKER);
280
+ if (existsSync(marker)) {
281
+ assertValidStateMarker(marker);
282
+ return canonical;
283
+ }
284
+ if (isRecognizableLegacyStateRoot(canonical)) return canonical;
285
+ throw new Error(`refusing to remove unrecognized state root without ${STATE_MARKER}: ${canonical}`);
286
+ }
287
+
288
+ function assertValidStateMarker(marker, options = {}) {
289
+ const content = readFileSync(marker, "utf8");
290
+ try {
291
+ const value = JSON.parse(content);
292
+ if (value?.app === appName && value?.schema === 1) return;
293
+ } catch {}
294
+ if (content.trim() === `${appName} state root`) {
295
+ if (options.migrateLegacy) atomicWriteJson(marker, { app: appName, schema: 1 });
296
+ return;
297
+ }
298
+ throw new Error(`invalid state root marker: ${marker}`);
299
+ }
300
+
301
+ function hasOnlyStateEntries(entries) {
302
+ const allowed = new Set([STATE_MARKER, "config.json", "profiles", "logs"]);
303
+ return entries.every((entry) => allowed.has(entry) || /^config\.json\.corrupt-\d+$/.test(entry));
304
+ }
305
+
306
+ function looksLikeSourceTree(root) {
307
+ return [".git", ".hg", ".svn", "package.json", "pyproject.toml", "Cargo.toml", "go.mod"].some((name) => existsSync(path.join(root, name)));
308
+ }
309
+
310
+ function stateRootMatchesRecordedWorkspace(root) {
311
+ const profiles = path.join(root, "profiles");
312
+ if (!existsSync(profiles)) return false;
313
+ try {
314
+ for (const entry of readdirSync(profiles, { withFileTypes: true })) {
315
+ if (!entry.isDirectory()) continue;
316
+ const stateFile = path.join(profiles, entry.name, "state.json");
317
+ if (!existsSync(stateFile)) continue;
318
+ const value = JSON.parse(readFileSync(stateFile, "utf8"));
319
+ if (typeof value?.workspace?.path !== "string") continue;
320
+ try { if (realpathSync(value.workspace.path) === root) return true; } catch {}
321
+ }
322
+ } catch {}
323
+ return false;
324
+ }
325
+
326
+ function isRecognizableLegacyStateRoot(root) {
327
+ const config = path.join(root, "config.json");
328
+ try {
329
+ if (existsSync(config)) {
330
+ const value = JSON.parse(readFileSync(config, "utf8"));
331
+ if (
332
+ value &&
333
+ typeof value.selectedWorkspace === "string" &&
334
+ typeof value.selectedWorkspaceHash === "string" &&
335
+ workspaceHash(value.selectedWorkspace) === value.selectedWorkspaceHash
336
+ ) return true;
337
+ }
338
+ } catch {}
339
+ const profiles = path.join(root, "profiles");
340
+ if (!existsSync(profiles)) return false;
341
+ try {
342
+ for (const entry of readdirSync(profiles, { withFileTypes: true })) {
343
+ if (!entry.isDirectory() || !/^[a-f0-9]{24}$/.test(entry.name)) continue;
344
+ const stateFile = path.join(profiles, entry.name, "state.json");
345
+ if (!existsSync(stateFile)) continue;
346
+ const value = JSON.parse(readFileSync(stateFile, "utf8"));
347
+ if (value?.workspace?.hash === entry.name && typeof value?.workspace?.path === "string") return true;
348
+ }
349
+ } catch {}
350
+ return false;
351
+ }
352
+
353
+ function atomicWriteJson(filePath, value) {
354
+ const dir = path.dirname(filePath);
355
+ ensureOwnerOnlyDir(dir);
356
+ cleanupStaleAtomicTemps(dir, path.basename(filePath));
357
+ const tempPath = path.join(dir, `.${path.basename(filePath)}.${process.pid}.${randomBytes(6).toString("hex")}.tmp`);
358
+ let fd;
359
+ try {
360
+ fd = openSync(tempPath, "wx", 0o600);
361
+ writeFileSync(fd, `${JSON.stringify(value, null, 2)}\n`, "utf8");
362
+ fsyncSync(fd);
363
+ closeSync(fd);
364
+ fd = undefined;
365
+ renameSync(tempPath, filePath);
366
+ ownerOnlyFile(filePath);
367
+ } catch (error) {
368
+ if (fd !== undefined) {
369
+ try { closeSync(fd); } catch {}
370
+ }
371
+ try { unlinkSync(tempPath); } catch {}
372
+ throw error;
373
+ }
374
+ }
375
+
376
+ function cleanupStaleAtomicTemps(dir, baseName = "") {
377
+ const now = Date.now();
378
+ let entries = [];
379
+ try { entries = readdirSync(dir); } catch { return; }
380
+ for (const name of entries) {
381
+ if (!/^\..+\.\d+\.[a-f0-9]+\.tmp$/.test(name)) continue;
382
+ if (baseName && !name.startsWith(`.${baseName}.`)) continue;
383
+ const file = path.join(dir, name);
384
+ try {
385
+ if (now - statSync(file).mtimeMs > 60 * 60 * 1000) unlinkSync(file);
386
+ } catch {}
387
+ }
388
+ }
389
+
390
+ function pruneBackups(filePath, keep) {
391
+ const dir = path.dirname(filePath);
392
+ const prefix = `${path.basename(filePath)}.corrupt-`;
393
+ let backups = [];
394
+ try {
395
+ backups = readdirSync(dir)
396
+ .filter(name => name.startsWith(prefix))
397
+ .map(name => ({ path: path.join(dir, name), mtime: statSync(path.join(dir, name)).mtimeMs }))
398
+ .sort((a, b) => b.mtime - a.mtime);
399
+ } catch {
400
+ return;
401
+ }
402
+ for (const backup of backups.slice(keep)) {
403
+ try { unlinkSync(backup.path); } catch {}
404
+ }
405
+ }
209
406
 
210
407
  export function ensureWorkerSecrets(state, options = {}) {
211
408
  state.worker ||= {};
@@ -215,14 +412,6 @@ export function ensureWorkerSecrets(state, options = {}) {
215
412
  if (!state.worker.name || options.workerName) state.worker.name = options.workerName || defaultWorkerName(state.workspace.hash);
216
413
  }
217
414
 
218
- export function ensureLocalApiKey(state, options = {}) {
219
- state.localApi ||= {};
220
- if (options.apiKey && options.apiKey !== true) state.localApi.apiKey = String(options.apiKey);
221
- if (!state.localApi.apiKey || options.rotateApiKey) state.localApi.apiKey = randomToken("local_api_key");
222
- state.localApi.updatedAt = new Date().toISOString();
223
- return state.localApi.apiKey;
224
- }
225
-
226
415
  export function defaultWorkerName(hash) {
227
416
  return `mbm-${String(hash || "default").slice(0, 12)}`;
228
417
  }
@@ -250,16 +439,18 @@ export function ownerOnlyFile(filePath) {
250
439
 
251
440
  export function redactState(state) {
252
441
  const clone = redactHomeInValue(JSON.parse(JSON.stringify(state)));
253
- if (clone.worker?.oauthPassword) clone.worker.oauthPassword = previewSecret(clone.worker.oauthPassword);
254
- if (clone.worker?.daemonSecret) clone.worker.daemonSecret = previewSecret(clone.worker.daemonSecret);
255
- if (clone.worker?.oauthTokenVersion) clone.worker.oauthTokenVersion = previewSecret(clone.worker.oauthTokenVersion);
256
- if (clone.localApi?.apiKey) clone.localApi.apiKey = previewSecret(clone.localApi.apiKey);
442
+ if (clone.worker?.oauthPassword) clone.worker.oauthPassword = "<redacted>";
443
+ if (clone.worker?.daemonSecret) clone.worker.daemonSecret = "<redacted>";
444
+ if (clone.worker?.oauthTokenVersion) clone.worker.oauthTokenVersion = "<redacted>";
257
445
  return clone;
258
446
  }
259
447
 
260
448
  function redactHomeInValue(value) {
261
449
  const home = os.homedir();
262
- if (typeof value === "string") return home && value.startsWith(home) ? `~${value.slice(home.length)}` : value;
450
+ if (typeof value === "string") {
451
+ const insideHome = home && (value === home || value.startsWith(`${home}${path.sep}`));
452
+ return insideHome ? `~${value.slice(home.length)}` : value;
453
+ }
263
454
  if (Array.isArray(value)) return value.map(redactHomeInValue);
264
455
  if (value && typeof value === "object") {
265
456
  for (const key of Object.keys(value)) value[key] = redactHomeInValue(value[key]);
@@ -267,8 +458,6 @@ function redactHomeInValue(value) {
267
458
  return value;
268
459
  }
269
460
 
270
- export function previewSecret(value) {
271
- const text = String(value || "");
272
- if (text.length <= 12) return "<redacted>";
273
- return `${text.slice(0, 10)}...${text.slice(-6)}`;
461
+ export function previewSecret(_value) {
462
+ return "<redacted>";
274
463
  }