@cydm/magic-shell-agent-node 0.1.18 → 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/index.d.ts CHANGED
@@ -1,2 +1,3 @@
1
1
  export { AgentNode, type NodeOptions, main as runAgentNode } from "./node.js";
2
2
  export { getDefaultPluginDir } from "./plugin-loader.js";
3
+ export { getMagicShellCliPath, getPackagedPluginDir, getPrimaryPieExtensionDistDir, getWorkbenchRoot } from "./runtime-assets.js";
package/dist/index.js CHANGED
@@ -1,2 +1,3 @@
1
1
  export { AgentNode, main as runAgentNode } from "./node.js";
2
2
  export { getDefaultPluginDir } from "./plugin-loader.js";
3
+ export { getMagicShellCliPath, getPackagedPluginDir, getPrimaryPieExtensionDistDir, getWorkbenchRoot } from "./runtime-assets.js";
@@ -1,8 +1,8 @@
1
1
  import { createServer } from "node:http";
2
2
  import { existsSync, createReadStream } from "node:fs";
3
3
  import path from "node:path";
4
- import { fileURLToPath } from "node:url";
5
4
  import { WebSocketServer, WebSocket } from "ws";
5
+ import { getWorkbenchRoot } from "./runtime-assets.js";
6
6
  function createConnectionId() {
7
7
  return `local-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
8
8
  }
@@ -21,25 +21,6 @@ function guessContentType(filePath) {
21
21
  return "image/png";
22
22
  return "text/plain; charset=utf-8";
23
23
  }
24
- function findWorkbenchRoot() {
25
- const currentDir = path.dirname(fileURLToPath(import.meta.url));
26
- const envOverride = process.env.MAGIC_SHELL_WORKBENCH_ROOT;
27
- const candidates = [
28
- envOverride ? path.resolve(envOverride) : null,
29
- path.resolve(currentDir, "./workbench"),
30
- path.resolve(currentDir, "../dist/workbench"),
31
- path.resolve(currentDir, "../workbench"),
32
- path.resolve(currentDir, "../../../apps/web/src"),
33
- path.resolve(currentDir, "../../../../apps/web/src"),
34
- path.resolve(process.cwd(), "apps/web/src"),
35
- ].filter((value) => Boolean(value));
36
- for (const candidate of candidates) {
37
- if (existsSync(path.join(candidate, "index.html"))) {
38
- return candidate;
39
- }
40
- }
41
- return null;
42
- }
43
24
  export class LocalDirectServer {
44
25
  options;
45
26
  delegate;
@@ -47,7 +28,7 @@ export class LocalDirectServer {
47
28
  wsServer = new WebSocketServer({ noServer: true });
48
29
  connections = new Map();
49
30
  sessionToConnections = new Map();
50
- workbenchRoot = findWorkbenchRoot();
31
+ workbenchRoot = getWorkbenchRoot();
51
32
  constructor(options, delegate) {
52
33
  this.options = options;
53
34
  this.delegate = delegate;
package/dist/node.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { createRequire } from "node:module";
1
2
  import { WebSocketClient } from "./ws-client.js";
2
3
  import { SessionManager } from "./session-manager.js";
3
4
  import { LocalDirectServer } from "./local-direct-server.js";
@@ -16,9 +17,10 @@ import { runClaudeExec } from "./claude-exec.js";
16
17
  import { codexNeedsTrustConfirmation, getLastCodexWorkerMessage, isCodexReadyForTask, isCodexWorker, summarizeCodexWorkerOutput, } from "./codex-worker.js";
17
18
  import { runCodexExec } from "./codex-exec.js";
18
19
  import { extractTerminalMetadata, normalizeWorkerTitleCandidate, } from "./terminal-metadata.js";
19
- import { existsSync } from "node:fs";
20
+ import { chmodSync, existsSync, readdirSync } from "node:fs";
20
21
  import os from "node:os";
21
22
  import path from "node:path";
23
+ const require = createRequire(import.meta.url);
22
24
  function generateSessionId() {
23
25
  return `session-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
24
26
  }
@@ -84,6 +86,7 @@ export class AgentNode {
84
86
  console.log(`[AgentNode] Starting...`);
85
87
  console.log(`[AgentNode] Node ID: ${this.options.nodeId}`);
86
88
  console.log(`[AgentNode] Relay: ${this.options.relayUrl}`);
89
+ ensureSpawnHelperExecutable();
87
90
  // 1. 加载插件
88
91
  this.plugins = loadPlugins({ dir: this.options.pluginDir });
89
92
  if (this.plugins.size === 0) {
@@ -304,9 +307,7 @@ export class AgentNode {
304
307
  sessionManager: this.sessionManager,
305
308
  workerRegistry: this.workerRegistry,
306
309
  });
307
- if (!options.taskSummary) {
308
- void this.primeInteractiveWorkerSession(options.sessionId).catch(() => { });
309
- }
310
+ void this.primeInteractiveWorkerSession(options.sessionId).catch(() => { });
310
311
  if (options.taskSummary) {
311
312
  await this.dispatchInitialWorkerTask(options.sessionId, options.taskSummary);
312
313
  }
@@ -1933,6 +1934,34 @@ Environment Variables:
1933
1934
  MAGIC_SHELL_DISABLE_LOCAL_DIRECT Set to 1 to disable the local direct server
1934
1935
  `);
1935
1936
  }
1937
+ function ensureSpawnHelperExecutable() {
1938
+ try {
1939
+ const pkgPath = require.resolve("node-pty/package.json");
1940
+ const prebuildRoot = path.join(path.dirname(pkgPath), "prebuilds");
1941
+ if (!existsSync(prebuildRoot))
1942
+ return;
1943
+ for (const entry of walk(prebuildRoot)) {
1944
+ if (path.basename(entry) === "spawn-helper") {
1945
+ chmodSync(entry, 0o755);
1946
+ }
1947
+ }
1948
+ }
1949
+ catch {
1950
+ // Ignore best-effort fixups during startup.
1951
+ }
1952
+ }
1953
+ function walk(dir) {
1954
+ const files = [];
1955
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
1956
+ const fullPath = path.join(dir, entry.name);
1957
+ if (entry.isDirectory()) {
1958
+ files.push(...walk(fullPath));
1959
+ continue;
1960
+ }
1961
+ files.push(fullPath);
1962
+ }
1963
+ return files;
1964
+ }
1936
1965
  // 主函数
1937
1966
  async function main() {
1938
1967
  const options = parseArgs();
@@ -1,7 +1,7 @@
1
1
  import { readFileSync, readdirSync, existsSync } from "fs";
2
2
  import { join, extname, dirname, isAbsolute, resolve } from "path";
3
- import { fileURLToPath } from "url";
4
3
  import { createRequire } from "module";
4
+ import { getPackagedPluginDir } from "./runtime-assets.js";
5
5
  const require = createRequire(import.meta.url);
6
6
  /**
7
7
  * 加载单个插件配置
@@ -39,9 +39,6 @@ export function loadPlugin(path) {
39
39
  });
40
40
  }
41
41
  function normalizePluginConfigForRuntime(config) {
42
- if (process.platform !== "win32") {
43
- return config;
44
- }
45
42
  if (config.name === "pie") {
46
43
  const pieEntry = resolvePackageBinEntry("@cydm/pie", "pie");
47
44
  if (pieEntry) {
@@ -52,6 +49,9 @@ function normalizePluginConfigForRuntime(config) {
52
49
  };
53
50
  }
54
51
  }
52
+ if (process.platform !== "win32") {
53
+ return config;
54
+ }
55
55
  if ((config.name === "claude-code" || config.name === "codex") && isShellCommand(config.command)) {
56
56
  return {
57
57
  ...config,
@@ -174,18 +174,9 @@ export function loadPlugins(options) {
174
174
  * 获取默认插件目录
175
175
  */
176
176
  export function getDefaultPluginDir() {
177
- // 优先使用环境变量
178
- if (process.env.MAGIC_SHELL_PLUGINS_DIR) {
179
- return process.env.MAGIC_SHELL_PLUGINS_DIR;
180
- }
181
- const packagePluginDir = resolve(dirname(fileURLToPath(import.meta.url)), "..", "plugins");
182
- if (existsSync(packagePluginDir)) {
183
- return packagePluginDir;
184
- }
185
- const distPluginDir = resolve(dirname(fileURLToPath(import.meta.url)), "plugins");
186
- if (existsSync(distPluginDir)) {
187
- return distPluginDir;
177
+ const pluginDir = getPackagedPluginDir();
178
+ if (pluginDir) {
179
+ return pluginDir;
188
180
  }
189
- // 回退到当前工作目录的 plugins,兼容仓库内开发
190
- return join(process.cwd(), "plugins");
181
+ throw new Error("Magic Shell plugin assets are unavailable. Set MAGIC_SHELL_PLUGINS_DIR or run from a built source checkout.");
191
182
  }
@@ -55,7 +55,6 @@ export interface PrimarySessionSnapshot {
55
55
  lastMessageRole?: string;
56
56
  lastAssistantHasToolCall?: boolean;
57
57
  }
58
- export declare function getPrimaryPieExtensionDistDir(): string;
59
58
  export declare function withPrimaryPieExtensionPath(plugin: PluginConfig): PluginConfig;
60
59
  export declare function buildPrimaryPrompt(text: string, context: PrimaryAgentPromptContext): string;
61
60
  export declare function buildPrimarySessionPrompt(text: string, context: PrimarySessionPromptContext): string;
@@ -2,20 +2,50 @@ import { spawn } from "node:child_process";
2
2
  import fs from "node:fs";
3
3
  import os from "node:os";
4
4
  import path from "node:path";
5
- import { fileURLToPath } from "node:url";
6
- export function getPrimaryPieExtensionDistDir() {
7
- const currentDir = path.dirname(fileURLToPath(import.meta.url));
8
- const repoRoot = path.resolve(currentDir, "..", "..", "..");
9
- return path.join(repoRoot, "packages", "primary-pie-extension", "dist");
5
+ import { getMagicShellCliPath, getPrimaryPieExtensionDistDir } from "./runtime-assets.js";
6
+ function getPrimaryPieExtensionPackageDir(extensionRoot) {
7
+ const packagedDir = path.join(extensionRoot, "magic-shell-agent");
8
+ if (fs.existsSync(path.join(packagedDir, "package.json"))) {
9
+ return packagedDir;
10
+ }
11
+ if (fs.existsSync(path.join(extensionRoot, "package.json"))) {
12
+ return extensionRoot;
13
+ }
14
+ return null;
15
+ }
16
+ function ensurePieExtensionDiscovery(extensionRoot) {
17
+ const extensionPackageDir = getPrimaryPieExtensionPackageDir(extensionRoot);
18
+ if (!extensionPackageDir) {
19
+ throw new Error(`Primary PIE extension package is missing under ${extensionRoot}`);
20
+ }
21
+ const targetDir = path.join(os.homedir(), ".pie", "extensions", path.basename(extensionPackageDir));
22
+ fs.rmSync(targetDir, { recursive: true, force: true });
23
+ fs.mkdirSync(path.dirname(targetDir), { recursive: true });
24
+ fs.cpSync(extensionPackageDir, targetDir, { recursive: true });
25
+ return targetDir;
10
26
  }
11
27
  export function withPrimaryPieExtensionPath(plugin) {
12
28
  if (plugin.name !== "pie") {
13
29
  return plugin;
14
30
  }
15
31
  const extensionDir = getPrimaryPieExtensionDistDir();
32
+ if (!extensionDir) {
33
+ throw new Error("Primary PIE extension assets are unavailable. Use the packaged @cydm/magic-shell runtime or build packages/primary-pie-extension first.");
34
+ }
35
+ const cliPath = getMagicShellCliPath();
36
+ if (!cliPath) {
37
+ throw new Error("Magic Shell CLI runtime is unavailable for the primary PIE extension. Start the node via `magic-shell node start` or set MAGIC_SHELL_CLI_PATH.");
38
+ }
39
+ const installedExtensionDir = ensurePieExtensionDiscovery(extensionDir);
16
40
  return {
17
41
  ...plugin,
18
42
  args: [...(plugin.args || []), "--extension-path", extensionDir],
43
+ env: {
44
+ ...(plugin.env || {}),
45
+ MAGIC_SHELL_CLI_PATH: cliPath,
46
+ MAGIC_SHELL_PRIMARY_EXTENSION_DIR: extensionDir,
47
+ MAGIC_SHELL_PRIMARY_EXTENSION_PACKAGE_DIR: installedExtensionDir,
48
+ },
19
49
  };
20
50
  }
21
51
  export function buildPrimaryPrompt(text, context) {