@oh-my-pi/pi-coding-agent 6.7.67 → 6.8.0

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.
Files changed (114) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/package.json +6 -7
  3. package/src/cli/session-picker.ts +27 -28
  4. package/src/cli/setup-cli.ts +7 -16
  5. package/src/cli/update-cli.ts +1 -1
  6. package/src/config.ts +1 -1
  7. package/src/core/agent-session.ts +202 -37
  8. package/src/core/agent-storage.ts +1 -1
  9. package/src/core/auth-storage.ts +15 -25
  10. package/src/core/bash-executor.ts +63 -105
  11. package/src/core/custom-commands/loader.ts +1 -1
  12. package/src/core/custom-tools/loader.ts +1 -1
  13. package/src/core/custom-tools/types.ts +1 -2
  14. package/src/core/exec.ts +16 -100
  15. package/src/core/extensions/index.ts +1 -7
  16. package/src/core/extensions/loader.ts +1 -1
  17. package/src/core/extensions/runner.ts +1 -1
  18. package/src/core/extensions/types.ts +2 -2
  19. package/src/core/extensions/wrapper.ts +15 -20
  20. package/src/core/frontmatter.ts +1 -1
  21. package/src/core/history-storage.ts +3 -6
  22. package/src/core/hooks/index.ts +2 -2
  23. package/src/core/hooks/loader.ts +1 -1
  24. package/src/core/hooks/tool-wrapper.ts +14 -26
  25. package/src/core/hooks/types.ts +1 -2
  26. package/src/core/keybindings.ts +1 -1
  27. package/src/core/mcp/client.ts +13 -13
  28. package/src/core/mcp/json-rpc.ts +1 -1
  29. package/src/core/mcp/loader.ts +1 -1
  30. package/src/core/mcp/manager.ts +2 -2
  31. package/src/core/mcp/tool-cache.ts +1 -1
  32. package/src/core/mcp/transports/http.ts +32 -70
  33. package/src/core/model-registry.ts +1 -1
  34. package/src/core/plugins/installer.ts +13 -11
  35. package/src/core/prompt-templates.ts +4 -9
  36. package/src/core/python-executor.ts +23 -18
  37. package/src/core/python-gateway-coordinator.ts +29 -28
  38. package/src/core/python-kernel.ts +230 -211
  39. package/src/core/sdk.ts +10 -13
  40. package/src/core/session-manager.ts +1 -1
  41. package/src/core/settings-manager.ts +22 -9
  42. package/src/core/skills.ts +1 -1
  43. package/src/core/ssh/connection-manager.ts +19 -33
  44. package/src/core/ssh/ssh-executor.ts +39 -35
  45. package/src/core/ssh/sshfs-mount.ts +14 -33
  46. package/src/core/storage-migration.ts +1 -1
  47. package/src/core/streaming-output.ts +183 -127
  48. package/src/core/system-prompt.ts +119 -79
  49. package/src/core/title-generator.ts +1 -1
  50. package/src/core/tools/ask.ts +2 -2
  51. package/src/core/tools/bash.ts +3 -3
  52. package/src/core/tools/calculator.ts +1 -1
  53. package/src/core/tools/exa/mcp-client.ts +1 -1
  54. package/src/core/tools/exa/render.ts +1 -1
  55. package/src/core/tools/find.ts +39 -71
  56. package/src/core/tools/gemini-image.ts +1 -1
  57. package/src/core/tools/grep.ts +88 -100
  58. package/src/core/tools/index.ts +1 -1
  59. package/src/core/tools/ls.ts +1 -1
  60. package/src/core/tools/lsp/client.ts +50 -50
  61. package/src/core/tools/lsp/clients/lsp-linter-client.ts +1 -1
  62. package/src/core/tools/lsp/config.ts +1 -1
  63. package/src/core/tools/lsp/index.ts +2 -4
  64. package/src/core/tools/lsp/lspmux.ts +1 -1
  65. package/src/core/tools/lsp/rust-analyzer.ts +2 -2
  66. package/src/core/tools/lsp/utils.ts +0 -14
  67. package/src/core/tools/notebook.ts +1 -1
  68. package/src/core/tools/patch/shared.ts +3 -4
  69. package/src/core/tools/python.ts +3 -3
  70. package/src/core/tools/read.ts +29 -68
  71. package/src/core/tools/render-utils.ts +0 -5
  72. package/src/core/tools/ssh.ts +3 -3
  73. package/src/core/tools/task/model-resolver.ts +7 -9
  74. package/src/core/tools/task/worker.ts +144 -139
  75. package/src/core/tools/todo-write.ts +1 -1
  76. package/src/core/tools/truncate.ts +2 -2
  77. package/src/core/tools/web-fetch.ts +13 -15
  78. package/src/core/tools/web-scrapers/types.ts +1 -3
  79. package/src/core/tools/web-scrapers/utils.ts +14 -13
  80. package/src/core/tools/web-scrapers/youtube.ts +39 -12
  81. package/src/core/tools/web-search/auth.ts +9 -45
  82. package/src/core/tools/write.ts +1 -1
  83. package/src/core/ttsr.ts +1 -1
  84. package/src/core/utils.ts +1 -187
  85. package/src/core/voice-controller.ts +1 -1
  86. package/src/core/voice-supervisor.ts +11 -38
  87. package/src/core/voice.ts +1 -8
  88. package/src/discovery/codex.ts +1 -1
  89. package/src/index.ts +4 -4
  90. package/src/main.ts +5 -10
  91. package/src/migrations.ts +1 -1
  92. package/src/modes/index.ts +7 -40
  93. package/src/modes/interactive/components/extensions/state-manager.ts +1 -1
  94. package/src/modes/interactive/components/hook-editor.ts +12 -9
  95. package/src/modes/interactive/components/login-dialog.ts +24 -11
  96. package/src/modes/interactive/components/settings-defs.ts +9 -0
  97. package/src/modes/interactive/components/status-line.ts +36 -35
  98. package/src/modes/interactive/components/todo-display.ts +1 -1
  99. package/src/modes/interactive/components/tool-execution.ts +1 -1
  100. package/src/modes/interactive/controllers/command-controller.ts +50 -84
  101. package/src/modes/interactive/controllers/extension-ui-controller.ts +76 -76
  102. package/src/modes/interactive/controllers/input-controller.ts +12 -11
  103. package/src/modes/interactive/interactive-mode.ts +10 -11
  104. package/src/modes/interactive/theme/theme.ts +1 -1
  105. package/src/modes/interactive/types.ts +1 -1
  106. package/src/modes/rpc/rpc-client.ts +91 -121
  107. package/src/modes/rpc/rpc-mode.ts +71 -79
  108. package/src/prompts/system/ttsr-interrupt.md +7 -0
  109. package/src/utils/clipboard.ts +57 -141
  110. package/src/utils/shell-snapshot.ts +12 -60
  111. package/src/utils/shell.ts +35 -56
  112. package/src/utils/tools-manager.ts +42 -71
  113. package/src/core/logger.ts +0 -111
  114. package/src/modes/cleanup.ts +0 -23
@@ -1,11 +1,11 @@
1
- import { existsSync, readFileSync, renameSync } from "node:fs";
1
+ import { rename } from "node:fs/promises";
2
2
  import { join } from "node:path";
3
+ import { logger } from "@oh-my-pi/pi-utils";
3
4
  import { type Settings as SettingsItem, settingsCapability } from "../capability/settings";
4
5
  import { getAgentDbPath, getAgentDir } from "../config";
5
6
  import { loadCapability } from "../discovery";
6
7
  import type { SymbolPreset } from "../modes/interactive/theme/theme";
7
8
  import { AgentStorage } from "./agent-storage";
8
- import { logger } from "./logger";
9
9
 
10
10
  export interface CompactionSettings {
11
11
  enabled?: boolean; // default: true
@@ -125,6 +125,7 @@ export interface EditSettings {
125
125
  fuzzyMatch?: boolean; // default: true (accept high-confidence fuzzy matches for whitespace/indentation)
126
126
  fuzzyThreshold?: number; // default: 0.95 (similarity threshold for fuzzy matching)
127
127
  patchMode?: boolean; // default: true (use codex-style apply-patch format instead of oldText/newText)
128
+ streamingAbort?: boolean; // default: false (abort streaming edit tool calls when patch preview fails)
128
129
  }
129
130
 
130
131
  export type { SymbolPreset };
@@ -322,7 +323,7 @@ const DEFAULT_SETTINGS: Settings = {
322
323
  mcp: { enableProjectConfig: true },
323
324
  lsp: { formatOnWrite: false, diagnosticsOnWrite: true, diagnosticsOnEdit: false },
324
325
  python: { toolMode: "both", kernelMode: "session", sharedGateway: true },
325
- edit: { fuzzyMatch: true, fuzzyThreshold: 0.95 },
326
+ edit: { fuzzyMatch: true, fuzzyThreshold: 0.95, streamingAbort: false },
326
327
  ttsr: { enabled: true, contextMode: "discard", repeatMode: "once", repeatGap: 10 },
327
328
  voice: {
328
329
  enabled: false,
@@ -514,7 +515,7 @@ export class SettingsManager {
514
515
  */
515
516
  static async create(cwd: string = process.cwd(), agentDir: string = getAgentDir()): Promise<SettingsManager> {
516
517
  const storage = AgentStorage.open(getAgentDbPath(agentDir));
517
- SettingsManager.migrateLegacySettingsFile(storage, agentDir);
518
+ await SettingsManager.migrateLegacySettingsFile(storage, agentDir);
518
519
 
519
520
  // Use capability API to load user-level settings from all providers
520
521
  const result = await loadCapability(settingsCapability.id, { cwd });
@@ -577,21 +578,21 @@ export class SettingsManager {
577
578
  return SettingsManager.migrateSettings(settings as Record<string, unknown>);
578
579
  }
579
580
 
580
- private static migrateLegacySettingsFile(storage: AgentStorage, agentDir: string): void {
581
+ private static async migrateLegacySettingsFile(storage: AgentStorage, agentDir: string): Promise<void> {
581
582
  const settingsPath = join(agentDir, "settings.json");
582
- if (!existsSync(settingsPath)) return;
583
+ const settingsFile = Bun.file(settingsPath);
584
+ if (!(await settingsFile.exists())) return;
583
585
  if (storage.getSettings() !== null) return;
584
586
 
585
587
  try {
586
- const content = readFileSync(settingsPath, "utf-8");
587
- const parsed = JSON.parse(content);
588
+ const parsed = JSON.parse(await settingsFile.text());
588
589
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
589
590
  return;
590
591
  }
591
592
  const migrated = SettingsManager.migrateSettings(parsed as Record<string, unknown>);
592
593
  storage.saveSettings(migrated);
593
594
  try {
594
- renameSync(settingsPath, `${settingsPath}.bak`);
595
+ await rename(settingsPath, `${settingsPath}.bak`);
595
596
  } catch (error) {
596
597
  logger.warn("SettingsManager failed to backup settings.json", { error: String(error) });
597
598
  }
@@ -1301,6 +1302,18 @@ export class SettingsManager {
1301
1302
  await this.save();
1302
1303
  }
1303
1304
 
1305
+ getEditStreamingAbort(): boolean {
1306
+ return this.settings.edit?.streamingAbort ?? false;
1307
+ }
1308
+
1309
+ async setEditStreamingAbort(enabled: boolean): Promise<void> {
1310
+ if (!this.globalSettings.edit) {
1311
+ this.globalSettings.edit = {};
1312
+ }
1313
+ this.globalSettings.edit.streamingAbort = enabled;
1314
+ await this.save();
1315
+ }
1316
+
1304
1317
  getNormativeRewrite(): boolean {
1305
1318
  return this.settings.normativeRewrite ?? false;
1306
1319
  }
@@ -1,13 +1,13 @@
1
1
  import { readdirSync, readFileSync, statSync } from "node:fs";
2
2
  import { realpath } from "node:fs/promises";
3
3
  import { basename, join } from "node:path";
4
+ import { logger } from "@oh-my-pi/pi-utils";
4
5
  import { minimatch } from "minimatch";
5
6
  import { skillCapability } from "../capability/skill";
6
7
  import type { SourceMeta } from "../capability/types";
7
8
  import type { Skill as CapabilitySkill, SkillFrontmatter as ImportedSkillFrontmatter } from "../discovery";
8
9
  import { loadCapability } from "../discovery";
9
10
  import { parseFrontmatter } from "./frontmatter";
10
- import { logger } from "./logger";
11
11
  import type { SkillsSettings } from "./settings-manager";
12
12
 
13
13
  // Re-export SkillFrontmatter for backward compatibility
@@ -1,8 +1,9 @@
1
1
  import { chmodSync, existsSync, mkdirSync, readFileSync, statSync } from "node:fs";
2
2
  import { homedir } from "node:os";
3
3
  import { join } from "node:path";
4
+ import { logger } from "@oh-my-pi/pi-utils";
5
+ import { $ } from "bun";
4
6
  import { CONFIG_DIR_NAME } from "../../config";
5
- import { logger } from "../logger";
6
7
 
7
8
  export interface SSHConnectionTarget {
8
9
  name: string;
@@ -107,32 +108,17 @@ function buildCommonArgs(host: SSHConnectionTarget): string[] {
107
108
  return args;
108
109
  }
109
110
 
110
- function decodeOutput(buffer?: Uint8Array): string {
111
- if (!buffer || buffer.length === 0) return "";
112
- return new TextDecoder().decode(buffer).trim();
111
+ async function runSshSync(args: string[]): Promise<{ exitCode: number | null; stderr: string }> {
112
+ const result = await $`ssh ${args}`.nothrow();
113
+ return { exitCode: result.exitCode, stderr: result.stderr.toString().trim() };
113
114
  }
114
115
 
115
- function runSshSync(args: string[]): { exitCode: number | null; stderr: string } {
116
- const result = Bun.spawnSync(["ssh", ...args], {
117
- stdin: "ignore",
118
- stdout: "ignore",
119
- stderr: "pipe",
120
- });
121
-
122
- return { exitCode: result.exitCode, stderr: decodeOutput(result.stderr) };
123
- }
124
-
125
- function runSshCaptureSync(args: string[]): { exitCode: number | null; stdout: string; stderr: string } {
126
- const result = Bun.spawnSync(["ssh", ...args], {
127
- stdin: "ignore",
128
- stdout: "pipe",
129
- stderr: "pipe",
130
- });
131
-
116
+ async function runSshCaptureSync(args: string[]): Promise<{ exitCode: number | null; stdout: string; stderr: string }> {
117
+ const result = await $`ssh ${args}`.nothrow();
132
118
  return {
133
119
  exitCode: result.exitCode,
134
- stdout: decodeOutput(result.stdout),
135
- stderr: decodeOutput(result.stderr),
120
+ stdout: result.stdout.toString().trim(),
121
+ stderr: result.stderr.toString().trim(),
136
122
  };
137
123
  }
138
124
 
@@ -266,7 +252,7 @@ async function persistHostInfo(host: SSHConnectionTarget, info: SSHHostInfo): Pr
266
252
 
267
253
  async function probeHostInfo(host: SSHConnectionTarget): Promise<SSHHostInfo> {
268
254
  const command = 'echo "$OSTYPE|$SHELL|$BASH_VERSION" 2>/dev/null || echo "%OS%|%COMSPEC%|"';
269
- const result = runSshCaptureSync(buildRemoteCommand(host, command));
255
+ const result = await runSshCaptureSync(buildRemoteCommand(host, command));
270
256
  if (result.exitCode !== 0 && !result.stdout) {
271
257
  logger.debug("SSH host probe failed", { host: host.name, error: result.stderr });
272
258
  const fallback: SSHHostInfo = {
@@ -329,11 +315,11 @@ async function probeHostInfo(host: SSHConnectionTarget): Promise<SSHHostInfo> {
329
315
  const hasBash = !unexpandedPosixVars && (Boolean(bashVersion) || shell === "bash");
330
316
  let compatShell: SSHHostInfo["compatShell"];
331
317
  if (os === "windows" && host.compat !== false) {
332
- const bashProbe = runSshCaptureSync(buildRemoteCommand(host, 'bash -lc "echo OMP_BASH_OK"'));
318
+ const bashProbe = await runSshCaptureSync(buildRemoteCommand(host, 'bash -lc "echo OMP_BASH_OK"'));
333
319
  if (bashProbe.exitCode === 0 && bashProbe.stdout.includes("OMP_BASH_OK")) {
334
320
  compatShell = "bash";
335
321
  } else {
336
- const shProbe = runSshCaptureSync(buildRemoteCommand(host, 'sh -lc "echo OMP_SH_OK"'));
322
+ const shProbe = await runSshCaptureSync(buildRemoteCommand(host, 'sh -lc "echo OMP_SH_OK"'));
337
323
  if (shProbe.exitCode === 0 && shProbe.stdout.includes("OMP_SH_OK")) {
338
324
  compatShell = "sh";
339
325
  }
@@ -406,7 +392,7 @@ export async function ensureConnection(host: SSHConnectionTarget): Promise<void>
406
392
  validateKeyPermissions(host.keyPath);
407
393
 
408
394
  const target = buildSshTarget(host);
409
- const check = runSshSync(["-O", "check", ...buildCommonArgs(host), target]);
395
+ const check = await runSshSync(["-O", "check", ...buildCommonArgs(host), target]);
410
396
  if (check.exitCode === 0) {
411
397
  activeHosts.set(key, host);
412
398
  if (!hostInfoCache.has(key) && !loadHostInfoFromDisk(host)) {
@@ -415,7 +401,7 @@ export async function ensureConnection(host: SSHConnectionTarget): Promise<void>
415
401
  return;
416
402
  }
417
403
 
418
- const start = runSshSync(["-M", "-N", "-f", ...buildCommonArgs(host), target]);
404
+ const start = await runSshSync(["-M", "-N", "-f", ...buildCommonArgs(host), target]);
419
405
  if (start.exitCode !== 0) {
420
406
  const detail = start.stderr ? `: ${start.stderr}` : "";
421
407
  throw new Error(`Failed to start SSH master for ${target}${detail}`);
@@ -435,24 +421,24 @@ export async function ensureConnection(host: SSHConnectionTarget): Promise<void>
435
421
  }
436
422
  }
437
423
 
438
- function closeConnectionInternal(host: SSHConnectionTarget): void {
424
+ async function closeConnectionInternal(host: SSHConnectionTarget): Promise<void> {
439
425
  const target = buildSshTarget(host);
440
- runSshSync(["-O", "exit", ...buildCommonArgs(host), target]);
426
+ await runSshSync(["-O", "exit", ...buildCommonArgs(host), target]);
441
427
  }
442
428
 
443
429
  export async function closeConnection(hostName: string): Promise<void> {
444
430
  const host = activeHosts.get(hostName);
445
431
  if (!host) {
446
- closeConnectionInternal({ name: hostName, host: hostName });
432
+ await closeConnectionInternal({ name: hostName, host: hostName });
447
433
  return;
448
434
  }
449
- closeConnectionInternal(host);
435
+ await closeConnectionInternal(host);
450
436
  activeHosts.delete(hostName);
451
437
  }
452
438
 
453
439
  export async function closeAllConnections(): Promise<void> {
454
440
  for (const [name, host] of Array.from(activeHosts.entries())) {
455
- closeConnectionInternal(host);
441
+ await closeConnectionInternal(host);
456
442
  activeHosts.delete(name);
457
443
  }
458
444
  }
@@ -1,9 +1,5 @@
1
- import type { Subprocess } from "bun";
2
- import { killProcessTree } from "../../utils/shell";
3
- import { logger } from "../logger";
4
- import { OutputSink, pumpStream } from "../streaming-output";
5
- import { DEFAULT_MAX_BYTES } from "../tools/truncate";
6
- import { ScopeSignal } from "../utils";
1
+ import { cspawn, logger, ptree } from "@oh-my-pi/pi-utils";
2
+ import { OutputSink } from "../streaming-output";
7
3
  import { buildRemoteCommand, ensureConnection, ensureHostInfo, type SSHConnectionTarget } from "./connection-manager";
8
4
  import { hasSshfs, mountRemote } from "./sshfs-mount";
9
5
 
@@ -59,8 +55,6 @@ export async function executeSSH(
59
55
  }
60
56
  }
61
57
 
62
- using signal = new ScopeSignal(options);
63
-
64
58
  let resolvedCommand = command;
65
59
  if (options?.compatEnabled) {
66
60
  const info = await ensureHostInfo(host);
@@ -70,43 +64,53 @@ export async function executeSSH(
70
64
  logger.warn("SSH compat enabled without detected compat shell", { host: host.name });
71
65
  }
72
66
  }
73
- const child: Subprocess = Bun.spawn(["ssh", ...buildRemoteCommand(host, resolvedCommand)], {
74
- stdin: "ignore",
75
- stdout: "pipe",
76
- stderr: "pipe",
77
- });
78
67
 
79
- signal.catch(() => {
80
- killProcessTree(child.pid);
68
+ const child = cspawn(["ssh", ...buildRemoteCommand(host, resolvedCommand)], {
69
+ signal: options?.signal,
70
+ timeout: options?.timeout,
81
71
  });
82
72
 
83
- const sink = new OutputSink(DEFAULT_MAX_BYTES, DEFAULT_MAX_BYTES * 2, options?.onChunk);
73
+ const sink = new OutputSink({ onLine: options?.onChunk });
84
74
 
85
- const writer = sink.getWriter();
86
75
  try {
87
- await Promise.all([
88
- pumpStream(child.stdout as ReadableStream<Uint8Array>, writer),
89
- pumpStream(child.stderr as ReadableStream<Uint8Array>, writer),
76
+ await Promise.allSettled([
77
+ child.stdout.pipeTo(sink.createWritable()),
78
+ child.stderr.pipeTo(sink.createWritable()),
90
79
  ]);
91
80
  } finally {
92
- await writer.close();
81
+ await sink.close();
93
82
  }
94
83
 
95
- const exitCode = await child.exited;
96
- const cancelled = exitCode === null || (exitCode !== 0 && (options?.signal?.aborted ?? false));
97
-
98
- if (signal.timedOut()) {
99
- const secs = Math.round(options!.timeout! / 1000);
84
+ try {
85
+ await child.exited;
86
+ const exitCode = child.exitCode ?? 0;
100
87
  return {
101
- exitCode: undefined,
102
- cancelled: true,
103
- ...sink.dump(`SSH command timed out after ${secs} seconds`),
88
+ exitCode,
89
+ cancelled: false,
90
+ ...sink.dump(),
104
91
  };
92
+ } catch (err) {
93
+ if (err instanceof ptree.Exception) {
94
+ if (err instanceof ptree.TimeoutError) {
95
+ return {
96
+ exitCode: undefined,
97
+ cancelled: true,
98
+ ...sink.dump(`SSH command timed out after ${Math.round(options!.timeout! / 1000)} seconds`),
99
+ };
100
+ }
101
+ if (err.aborted) {
102
+ return {
103
+ exitCode: undefined,
104
+ cancelled: true,
105
+ ...sink.dump(`SSH command aborted: ${err.message}`),
106
+ };
107
+ }
108
+ return {
109
+ exitCode: err.exitCode,
110
+ cancelled: false,
111
+ ...sink.dump(`Unexpected error: ${err.message}`),
112
+ };
113
+ }
114
+ throw err;
105
115
  }
106
-
107
- return {
108
- exitCode: cancelled ? undefined : exitCode,
109
- cancelled,
110
- ...sink.dump(),
111
- };
112
116
  }
@@ -1,8 +1,9 @@
1
1
  import { chmodSync, existsSync, mkdirSync } from "node:fs";
2
2
  import { homedir } from "node:os";
3
3
  import { join } from "node:path";
4
+ import { logger } from "@oh-my-pi/pi-utils";
5
+ import { $ } from "bun";
4
6
  import { CONFIG_DIR_NAME } from "../../config";
5
- import { logger } from "../logger";
6
7
  import { getControlDir, getControlPathTemplate, type SSHConnectionTarget } from "./connection-manager";
7
8
 
8
9
  const REMOTE_DIR = join(homedir(), CONFIG_DIR_NAME, "remote");
@@ -22,11 +23,6 @@ function ensureDir(path: string, mode = 0o700): void {
22
23
  }
23
24
  }
24
25
 
25
- function decodeOutput(buffer?: Uint8Array): string {
26
- if (!buffer || buffer.length === 0) return "";
27
- return new TextDecoder().decode(buffer).trim();
28
- }
29
-
30
26
  function getMountName(host: SSHConnectionTarget): string {
31
27
  const raw = (host.name ?? host.host).trim();
32
28
  const sanitized = raw.replace(/[^a-zA-Z0-9._-]+/g, "_");
@@ -72,24 +68,16 @@ function buildSshfsArgs(host: SSHConnectionTarget): string[] {
72
68
  return args;
73
69
  }
74
70
 
75
- function unmountPath(path: string): boolean {
71
+ async function unmountPath(path: string): Promise<boolean> {
76
72
  const fusermount = Bun.which("fusermount") ?? Bun.which("fusermount3");
77
73
  if (fusermount) {
78
- const result = Bun.spawnSync([fusermount, "-u", path], {
79
- stdin: "ignore",
80
- stdout: "ignore",
81
- stderr: "pipe",
82
- });
74
+ const result = await $`${fusermount} -u ${path}`.quiet().nothrow();
83
75
  if (result.exitCode === 0) return true;
84
76
  }
85
77
 
86
78
  const umount = Bun.which("umount");
87
79
  if (!umount) return false;
88
- const result = Bun.spawnSync([umount, path], {
89
- stdin: "ignore",
90
- stdout: "ignore",
91
- stderr: "pipe",
92
- });
80
+ const result = await $`${umount} ${path}`.quiet().nothrow();
93
81
  return result.exitCode === 0;
94
82
  }
95
83
 
@@ -97,14 +85,10 @@ export function hasSshfs(): boolean {
97
85
  return Bun.which("sshfs") !== null;
98
86
  }
99
87
 
100
- export function isMounted(path: string): boolean {
88
+ export async function isMounted(path: string): Promise<boolean> {
101
89
  const mountpoint = Bun.which("mountpoint");
102
90
  if (!mountpoint) return false;
103
- const result = Bun.spawnSync([mountpoint, "-q", path], {
104
- stdin: "ignore",
105
- stdout: "ignore",
106
- stderr: "ignore",
107
- });
91
+ const result = await $`${mountpoint} -q ${path}`.quiet().nothrow();
108
92
  return result.exitCode === 0;
109
93
  }
110
94
 
@@ -117,20 +101,17 @@ export async function mountRemote(host: SSHConnectionTarget, remotePath = "/"):
117
101
  const mountPath = getMountPath(host);
118
102
  ensureDir(mountPath);
119
103
 
120
- if (isMounted(mountPath)) {
104
+ if (await isMounted(mountPath)) {
121
105
  mountedPaths.add(mountPath);
122
106
  return mountPath;
123
107
  }
124
108
 
125
109
  const target = `${buildSshTarget(host)}:${remotePath}`;
126
- const result = Bun.spawnSync(["sshfs", ...buildSshfsArgs(host), target, mountPath], {
127
- stdin: "ignore",
128
- stdout: "pipe",
129
- stderr: "pipe",
130
- });
110
+ const args = buildSshfsArgs(host);
111
+ const result = await $`sshfs ${args} ${target} ${mountPath}`.nothrow();
131
112
 
132
113
  if (result.exitCode !== 0) {
133
- const detail = decodeOutput(result.stderr);
114
+ const detail = result.stderr.toString().trim();
134
115
  const suffix = detail ? `: ${detail}` : "";
135
116
  throw new Error(`Failed to mount ${target}${suffix}`);
136
117
  }
@@ -141,12 +122,12 @@ export async function mountRemote(host: SSHConnectionTarget, remotePath = "/"):
141
122
 
142
123
  export async function unmountRemote(host: SSHConnectionTarget): Promise<boolean> {
143
124
  const mountPath = getMountPath(host);
144
- if (!isMounted(mountPath)) {
125
+ if (!(await isMounted(mountPath))) {
145
126
  mountedPaths.delete(mountPath);
146
127
  return false;
147
128
  }
148
129
 
149
- const success = unmountPath(mountPath);
130
+ const success = await unmountPath(mountPath);
150
131
  if (success) {
151
132
  mountedPaths.delete(mountPath);
152
133
  }
@@ -156,7 +137,7 @@ export async function unmountRemote(host: SSHConnectionTarget): Promise<boolean>
156
137
 
157
138
  export async function unmountAll(): Promise<void> {
158
139
  for (const mountPath of Array.from(mountedPaths)) {
159
- unmountPath(mountPath);
140
+ await unmountPath(mountPath);
160
141
  }
161
142
  mountedPaths.clear();
162
143
  }
@@ -4,10 +4,10 @@
4
4
  * Original JSON files are backed up to .bak and removed after successful migration.
5
5
  */
6
6
 
7
+ import { logger } from "@oh-my-pi/pi-utils";
7
8
  import { getAgentDbPath } from "../config";
8
9
  import { AgentStorage } from "./agent-storage";
9
10
  import type { AuthCredential, AuthCredentialEntry, AuthStorageData } from "./auth-storage";
10
- import { logger } from "./logger";
11
11
  import type { Settings } from "./settings-manager";
12
12
 
13
13
  /** Paths configuration for the storage migration process. */