@cydm/magic-shell-agent-node 0.1.19 → 0.1.20

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.
@@ -1,97 +1,29 @@
1
1
  import { spawn } from "node-pty";
2
- import { existsSync, readFileSync } from "node:fs";
3
- import { createRequire } from "node:module";
4
- import path, { dirname } from "node:path";
5
- import { fileURLToPath } from "url";
6
- const __filename = fileURLToPath(import.meta.url);
7
- const __dirname = dirname(__filename);
8
- const require = createRequire(import.meta.url);
9
- const DEFAULT_WINDOWS_SHELL = process.env.ComSpec || "C:\\Windows\\System32\\cmd.exe";
10
- function quoteWindowsArg(value) {
11
- if (!value)
12
- return "\"\"";
13
- if (!/[\s"]/u.test(value))
14
- return value;
15
- return `"${value.replace(/"/g, '\\"')}"`;
16
- }
17
- function resolvePackageBinScript(packageName, binName) {
18
- try {
19
- const packageJsonPath = require.resolve(`${packageName}/package.json`);
20
- const packageDir = path.dirname(packageJsonPath);
21
- const pkg = JSON.parse(readFileSync(packageJsonPath, "utf8"));
22
- const binField = typeof pkg.bin === "string"
23
- ? pkg.bin
24
- : pkg.bin && typeof pkg.bin[binName] === "string"
25
- ? pkg.bin[binName]
26
- : null;
27
- if (!binField)
28
- return null;
29
- const entry = path.resolve(packageDir, binField);
30
- return existsSync(entry) ? entry : null;
31
- }
32
- catch {
33
- return null;
34
- }
35
- }
36
- function resolveWindowsCommand(command, args) {
37
- if (!command || process.platform !== "win32") {
38
- return { command, args };
39
- }
40
- if (command === "pie") {
41
- const pieEntry = resolvePackageBinScript("@cydm/pie", "pie");
42
- if (pieEntry) {
43
- return {
44
- command: process.execPath,
45
- args: [pieEntry, ...args],
46
- };
47
- }
48
- }
49
- const lowerCommand = command.toLowerCase();
50
- if (lowerCommand.endsWith(".cmd") || lowerCommand.endsWith(".bat")) {
51
- return {
52
- command: DEFAULT_WINDOWS_SHELL,
53
- args: ["/d", "/s", "/c", [quoteWindowsArg(command), ...args.map(quoteWindowsArg)].join(" ")],
2
+ import { resolveRuntimeCommand } from "../command-resolution.js";
3
+ const STOP_TIMEOUT_MS = 5_000;
4
+ function waitForPtyExit(agent) {
5
+ if (agent.stopPromise) {
6
+ return agent.stopPromise;
7
+ }
8
+ agent.stopPromise = new Promise((resolve) => {
9
+ let settled = false;
10
+ const finish = () => {
11
+ if (settled)
12
+ return;
13
+ settled = true;
14
+ resolve();
54
15
  };
55
- }
56
- if (command.includes("\\") || command.includes("/") || /^[a-zA-Z]:/.test(command)) {
57
- return { command, args };
58
- }
59
- const pathEntries = (process.env.PATH || "").split(";").filter(Boolean);
60
- const pathext = (process.env.PATHEXT || ".COM;.EXE;.BAT;.CMD")
61
- .split(";")
62
- .map((value) => value.trim())
63
- .filter(Boolean);
64
- for (const entry of pathEntries) {
65
- const bareCandidate = `${entry}\\${command}`;
66
- if (existsSync(bareCandidate)) {
67
- return { command: bareCandidate, args };
68
- }
69
- for (const ext of pathext) {
70
- const candidate = `${entry}\\${command}${ext.toLowerCase()}`;
71
- if (existsSync(candidate)) {
72
- const lowerExt = ext.toLowerCase();
73
- if (lowerExt === ".cmd" || lowerExt === ".bat") {
74
- return {
75
- command: DEFAULT_WINDOWS_SHELL,
76
- args: ["/d", "/s", "/c", [quoteWindowsArg(candidate), ...args.map(quoteWindowsArg)].join(" ")],
77
- };
78
- }
79
- return { command: candidate, args };
80
- }
81
- const candidateUpper = `${entry}\\${command}${ext.toUpperCase()}`;
82
- if (existsSync(candidateUpper)) {
83
- const upperExt = ext.toUpperCase();
84
- if (upperExt === ".CMD" || upperExt === ".BAT") {
85
- return {
86
- command: DEFAULT_WINDOWS_SHELL,
87
- args: ["/d", "/s", "/c", [quoteWindowsArg(candidateUpper), ...args.map(quoteWindowsArg)].join(" ")],
88
- };
89
- }
90
- return { command: candidateUpper, args };
91
- }
92
- }
93
- }
94
- return { command, args };
16
+ const timeout = setTimeout(() => {
17
+ console.warn(`[PtyAdapter] Timed out waiting for ${agent.id} to exit`);
18
+ finish();
19
+ }, STOP_TIMEOUT_MS);
20
+ timeout.unref?.();
21
+ agent.pty.onExit(() => {
22
+ clearTimeout(timeout);
23
+ finish();
24
+ });
25
+ });
26
+ return agent.stopPromise;
95
27
  }
96
28
  export class PtyAdapter {
97
29
  name = "pty";
@@ -106,7 +38,7 @@ export class PtyAdapter {
106
38
  }
107
39
  async start(config) {
108
40
  const agentId = `pty-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
109
- const resolved = resolveWindowsCommand(config.command, config.args || []);
41
+ const resolved = resolveRuntimeCommand(config.command, config.args || []);
110
42
  const pty = spawn(resolved.command, resolved.args, {
111
43
  name: "xterm-256color",
112
44
  cols: 80,
@@ -152,7 +84,9 @@ export class PtyAdapter {
152
84
  const agent = this.agents.get(agentId);
153
85
  if (!agent)
154
86
  return;
87
+ const exitPromise = waitForPtyExit(agent);
155
88
  agent.pty.kill();
89
+ await exitPromise;
156
90
  this.agents.delete(agentId);
157
91
  console.log(`[PtyAdapter] Stopped ${agentId}`);
158
92
  }
@@ -1,4 +1,41 @@
1
1
  import { spawn } from "child_process";
2
+ const STOP_TIMEOUT_MS = 5_000;
3
+ function waitForProcessExit(agent) {
4
+ if (agent.stopPromise) {
5
+ return agent.stopPromise;
6
+ }
7
+ agent.stopPromise = new Promise((resolve) => {
8
+ if (agent.process.exitCode !== null || agent.process.killed) {
9
+ resolve();
10
+ return;
11
+ }
12
+ let settled = false;
13
+ const finish = () => {
14
+ if (settled)
15
+ return;
16
+ settled = true;
17
+ resolve();
18
+ };
19
+ const timeout = setTimeout(() => {
20
+ console.warn(`[RpcAdapter] Timed out waiting for ${agent.id} to exit`);
21
+ finish();
22
+ }, STOP_TIMEOUT_MS);
23
+ timeout.unref?.();
24
+ agent.process.once("close", () => {
25
+ clearTimeout(timeout);
26
+ finish();
27
+ });
28
+ agent.process.once("exit", () => {
29
+ clearTimeout(timeout);
30
+ finish();
31
+ });
32
+ agent.process.once("error", () => {
33
+ clearTimeout(timeout);
34
+ finish();
35
+ });
36
+ });
37
+ return agent.stopPromise;
38
+ }
2
39
  export class RpcAdapter {
3
40
  name = "rpc";
4
41
  description = "JSON-RPC adapter for line-oriented RPC workers";
@@ -141,7 +178,9 @@ export class RpcAdapter {
141
178
  const agent = this.agents.get(agentId);
142
179
  if (!agent)
143
180
  return;
181
+ const exitPromise = waitForProcessExit(agent);
144
182
  agent.process.kill();
183
+ await exitPromise;
145
184
  this.agents.delete(agentId);
146
185
  console.log(`[RpcAdapter] Stopped ${agentId}`);
147
186
  }
@@ -1,4 +1,41 @@
1
1
  import { spawn } from "child_process";
2
+ const STOP_TIMEOUT_MS = 5_000;
3
+ function waitForProcessExit(agent) {
4
+ if (agent.stopPromise) {
5
+ return agent.stopPromise;
6
+ }
7
+ agent.stopPromise = new Promise((resolve) => {
8
+ if (agent.process.exitCode !== null) {
9
+ resolve();
10
+ return;
11
+ }
12
+ let settled = false;
13
+ const finish = () => {
14
+ if (settled)
15
+ return;
16
+ settled = true;
17
+ resolve();
18
+ };
19
+ const timeout = setTimeout(() => {
20
+ console.warn(`[StdioAdapter] Timed out waiting for ${agent.id} to exit`);
21
+ finish();
22
+ }, STOP_TIMEOUT_MS);
23
+ timeout.unref?.();
24
+ agent.process.once("close", () => {
25
+ clearTimeout(timeout);
26
+ finish();
27
+ });
28
+ agent.process.once("exit", () => {
29
+ clearTimeout(timeout);
30
+ finish();
31
+ });
32
+ agent.process.once("error", () => {
33
+ clearTimeout(timeout);
34
+ finish();
35
+ });
36
+ });
37
+ return agent.stopPromise;
38
+ }
2
39
  export class StdioAdapter {
3
40
  name = "stdio";
4
41
  description = "Stdio pipe adapter for CLI agents";
@@ -72,7 +109,9 @@ export class StdioAdapter {
72
109
  const agent = this.agents.get(agentId);
73
110
  if (!agent)
74
111
  return;
112
+ const exitPromise = waitForProcessExit(agent);
75
113
  agent.process.kill();
114
+ await exitPromise;
76
115
  this.agents.delete(agentId);
77
116
  console.log(`[StdioAdapter] Stopped ${agentId}`);
78
117
  }
@@ -1,18 +1,22 @@
1
1
  import { spawn } from "node:child_process";
2
+ import { resolveRuntimeCommand } from "./command-resolution.js";
3
+ import { claudeNeedsTrustConfirmation } from "./claude-worker.js";
2
4
  export async function runClaudeExec(options) {
3
5
  return new Promise((resolve, reject) => {
4
- const child = spawn("claude", [
6
+ const resolved = resolveRuntimeCommand("claude", [
5
7
  "-p",
6
8
  "--dangerously-skip-permissions",
7
9
  options.message,
8
- ], {
10
+ ]);
11
+ const child = spawn(resolved.command, resolved.args, {
9
12
  cwd: options.cwd,
10
13
  env: process.env,
11
- stdio: ["ignore", "pipe", "pipe"],
14
+ stdio: ["pipe", "pipe", "pipe"],
12
15
  });
13
16
  let stdout = "";
14
17
  let stderr = "";
15
18
  let settled = false;
19
+ let trustConfirmed = false;
16
20
  const finish = (handler) => {
17
21
  if (settled)
18
22
  return;
@@ -22,9 +26,17 @@ export async function runClaudeExec(options) {
22
26
  };
23
27
  child.stdout.on("data", (chunk) => {
24
28
  stdout += chunk.toString();
29
+ if (!trustConfirmed && claudeNeedsTrustConfirmation([stdout, stderr].join("\n"))) {
30
+ trustConfirmed = true;
31
+ child.stdin?.write("\r");
32
+ }
25
33
  });
26
34
  child.stderr.on("data", (chunk) => {
27
35
  stderr += chunk.toString();
36
+ if (!trustConfirmed && claudeNeedsTrustConfirmation([stdout, stderr].join("\n"))) {
37
+ trustConfirmed = true;
38
+ child.stdin?.write("\r");
39
+ }
28
40
  });
29
41
  child.on("error", (error) => {
30
42
  finish(() => reject(error));
@@ -1,4 +1,6 @@
1
1
  import { spawn } from "node:child_process";
2
+ import { resolveRuntimeCommand } from "./command-resolution.js";
3
+ import { codexNeedsTrustConfirmation } from "./codex-worker.js";
2
4
  function isRecord(value) {
3
5
  return !!value && typeof value === "object";
4
6
  }
@@ -32,7 +34,7 @@ export function extractCodexExecResult(rawOutput) {
32
34
  }
33
35
  export async function runCodexExec(options) {
34
36
  return new Promise((resolve, reject) => {
35
- const child = spawn("codex", [
37
+ const resolved = resolveRuntimeCommand("codex", [
36
38
  "exec",
37
39
  "--json",
38
40
  "--skip-git-repo-check",
@@ -40,14 +42,16 @@ export async function runCodexExec(options) {
40
42
  "-C",
41
43
  options.cwd,
42
44
  options.message,
43
- ], {
45
+ ]);
46
+ const child = spawn(resolved.command, resolved.args, {
44
47
  cwd: options.cwd,
45
48
  env: process.env,
46
- stdio: ["ignore", "pipe", "pipe"],
49
+ stdio: ["pipe", "pipe", "pipe"],
47
50
  });
48
51
  let stdout = "";
49
52
  let stderr = "";
50
53
  let settled = false;
54
+ let trustConfirmed = false;
51
55
  const finish = (handler) => {
52
56
  if (settled)
53
57
  return;
@@ -57,9 +61,17 @@ export async function runCodexExec(options) {
57
61
  };
58
62
  child.stdout.on("data", (chunk) => {
59
63
  stdout += chunk.toString();
64
+ if (!trustConfirmed && codexNeedsTrustConfirmation([stdout, stderr].join("\n"))) {
65
+ trustConfirmed = true;
66
+ child.stdin?.write("\r");
67
+ }
60
68
  });
61
69
  child.stderr.on("data", (chunk) => {
62
70
  stderr += chunk.toString();
71
+ if (!trustConfirmed && codexNeedsTrustConfirmation([stdout, stderr].join("\n"))) {
72
+ trustConfirmed = true;
73
+ child.stdin?.write("\r");
74
+ }
63
75
  });
64
76
  child.on("error", (error) => {
65
77
  finish(() => reject(error));
@@ -0,0 +1,4 @@
1
+ export declare function resolveRuntimeCommand(command: string, args?: string[]): {
2
+ command: string;
3
+ args: string[];
4
+ };
@@ -0,0 +1,85 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { createRequire } from "node:module";
3
+ import path from "node:path";
4
+ const require = createRequire(import.meta.url);
5
+ const DEFAULT_WINDOWS_SHELL = process.env.ComSpec || "C:\\Windows\\System32\\cmd.exe";
6
+ function quoteWindowsArg(value) {
7
+ if (!value)
8
+ return "\"\"";
9
+ if (!/[\s"]/u.test(value))
10
+ return value;
11
+ return `"${value.replace(/"/g, '\\"')}"`;
12
+ }
13
+ function resolvePackageBinScript(packageName, binName) {
14
+ try {
15
+ const packageJsonPath = require.resolve(`${packageName}/package.json`);
16
+ const packageDir = path.dirname(packageJsonPath);
17
+ const pkg = JSON.parse(readFileSync(packageJsonPath, "utf8"));
18
+ const binField = typeof pkg.bin === "string"
19
+ ? pkg.bin
20
+ : pkg.bin && typeof pkg.bin[binName] === "string"
21
+ ? pkg.bin[binName]
22
+ : null;
23
+ if (!binField)
24
+ return null;
25
+ const entry = path.resolve(packageDir, binField);
26
+ return existsSync(entry) ? entry : null;
27
+ }
28
+ catch {
29
+ return null;
30
+ }
31
+ }
32
+ export function resolveRuntimeCommand(command, args = []) {
33
+ if (!command || process.platform !== "win32") {
34
+ return { command, args };
35
+ }
36
+ if (command === "pie") {
37
+ const pieEntry = resolvePackageBinScript("@cydm/pie", "pie");
38
+ if (pieEntry) {
39
+ return {
40
+ command: process.execPath,
41
+ args: [pieEntry, ...args],
42
+ };
43
+ }
44
+ }
45
+ const lowerCommand = command.toLowerCase();
46
+ if (lowerCommand.endsWith(".cmd") || lowerCommand.endsWith(".bat")) {
47
+ return {
48
+ command: DEFAULT_WINDOWS_SHELL,
49
+ args: ["/d", "/s", "/c", [quoteWindowsArg(command), ...args.map(quoteWindowsArg)].join(" ")],
50
+ };
51
+ }
52
+ if (command.includes("\\") || command.includes("/") || /^[a-zA-Z]:/.test(command)) {
53
+ return { command, args };
54
+ }
55
+ const pathEntries = (process.env.PATH || "").split(";").filter(Boolean);
56
+ const pathext = (process.env.PATHEXT || ".COM;.EXE;.BAT;.CMD")
57
+ .split(";")
58
+ .map((value) => value.trim())
59
+ .filter(Boolean);
60
+ for (const entry of pathEntries) {
61
+ const bareCandidate = `${entry}\\${command}`;
62
+ if (existsSync(bareCandidate)) {
63
+ return { command: bareCandidate, args };
64
+ }
65
+ for (const ext of pathext) {
66
+ const candidates = [
67
+ `${entry}\\${command}${ext.toLowerCase()}`,
68
+ `${entry}\\${command}${ext.toUpperCase()}`,
69
+ ];
70
+ for (const candidate of candidates) {
71
+ if (!existsSync(candidate))
72
+ continue;
73
+ const lowerExt = path.extname(candidate).toLowerCase();
74
+ if (lowerExt === ".cmd" || lowerExt === ".bat") {
75
+ return {
76
+ command: DEFAULT_WINDOWS_SHELL,
77
+ args: ["/d", "/s", "/c", [quoteWindowsArg(candidate), ...args.map(quoteWindowsArg)].join(" ")],
78
+ };
79
+ }
80
+ return { command: candidate, args };
81
+ }
82
+ }
83
+ }
84
+ return { command, args };
85
+ }
package/dist/node.js CHANGED
@@ -307,9 +307,7 @@ export class AgentNode {
307
307
  sessionManager: this.sessionManager,
308
308
  workerRegistry: this.workerRegistry,
309
309
  });
310
- if (!options.taskSummary) {
311
- void this.primeInteractiveWorkerSession(options.sessionId).catch(() => { });
312
- }
310
+ void this.primeInteractiveWorkerSession(options.sessionId).catch(() => { });
313
311
  if (options.taskSummary) {
314
312
  await this.dispatchInitialWorkerTask(options.sessionId, options.taskSummary);
315
313
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cydm/magic-shell-agent-node",
3
- "version": "0.1.19",
3
+ "version": "0.1.20",
4
4
  "description": "Magic Shell Agent Node - Local agent connector",
5
5
  "homepage": "https://magicshell.ai",
6
6
  "keywords": [