@ganglion/xacpx 0.16.0 → 0.17.0-beta.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.
package/dist/cli.js CHANGED
@@ -4727,7 +4727,13 @@ function parseConfig(raw, options = {}) {
4727
4727
  workspaces,
4728
4728
  orchestration: orchestrationConfig,
4729
4729
  later: laterConfig,
4730
- ...language ? { language } : {}
4730
+ ...language ? { language } : {},
4731
+ ...raw.terminal && typeof raw.terminal === "object" ? {
4732
+ terminal: {
4733
+ enabled: raw.terminal.enabled === true,
4734
+ ...typeof raw.terminal.idleTimeoutSeconds === "number" ? { idleTimeoutSeconds: raw.terminal.idleTimeoutSeconds } : {}
4735
+ }
4736
+ } : {}
4731
4737
  };
4732
4738
  }
4733
4739
  function parsePluginConfig(raw, index) {
@@ -25571,6 +25577,15 @@ var init_console_agent = __esm(() => {
25571
25577
  init_i18n();
25572
25578
  });
25573
25579
 
25580
+ // src/config/types.ts
25581
+ function terminalEnabled(config4) {
25582
+ return config4.terminal?.enabled === true;
25583
+ }
25584
+ function terminalIdleTimeoutSeconds(config4) {
25585
+ const v = config4.terminal?.idleTimeoutSeconds;
25586
+ return typeof v === "number" && v > 0 ? v : 900;
25587
+ }
25588
+
25574
25589
  // src/orchestration/orchestration-server.ts
25575
25590
  import { chmod as chmod4, rm as rm8 } from "node:fs/promises";
25576
25591
  import { createServer } from "node:net";
@@ -34273,6 +34288,23 @@ ${chunk}` : chunk
34273
34288
  return chunks.join(`
34274
34289
  `);
34275
34290
  }
34291
+ async createTerminal(chatKey, sessionAlias, cols, rows) {
34292
+ if (!this.deps.terminalEnabled())
34293
+ throw new Error("terminal-disabled");
34294
+ const session3 = await this.resolveControlSession(chatKey, sessionAlias);
34295
+ if (!session3)
34296
+ throw new Error("session-not-found");
34297
+ return this.deps.terminal.create({ cwd: session3.cwd, cols, rows });
34298
+ }
34299
+ writeTerminal(terminalId, data) {
34300
+ this.deps.terminal.write(terminalId, data);
34301
+ }
34302
+ resizeTerminal(terminalId, cols, rows) {
34303
+ this.deps.terminal.resize(terminalId, cols, rows);
34304
+ }
34305
+ closeTerminal(terminalId) {
34306
+ this.deps.terminal.close(terminalId);
34307
+ }
34276
34308
  }
34277
34309
  async function raceWithTimeout(promise2, ms) {
34278
34310
  let timer;
@@ -34307,6 +34339,133 @@ var init_control_service = __esm(() => {
34307
34339
  init_workspace_fs();
34308
34340
  });
34309
34341
 
34342
+ // src/control/terminal-service.ts
34343
+ import { randomUUID as randomUUID3 } from "node:crypto";
34344
+ import { createRequire as createRequire6 } from "node:module";
34345
+ import { spawn as spawnPty2 } from "node-pty";
34346
+ function scrubEnv() {
34347
+ const out = {};
34348
+ for (const [k, v] of Object.entries(process.env)) {
34349
+ if (v === undefined)
34350
+ continue;
34351
+ if (SENSITIVE_ENV_KEYS.includes(k) || k.startsWith("XACPX_"))
34352
+ continue;
34353
+ out[k] = v;
34354
+ }
34355
+ out.TERM = "xterm-256color";
34356
+ out.LANG = out.LANG ?? "en_US.UTF-8";
34357
+ return out;
34358
+ }
34359
+ function defaultShell(platform) {
34360
+ if (process.env.SHELL)
34361
+ return process.env.SHELL;
34362
+ return platform === "darwin" ? "/bin/zsh" : "/bin/bash";
34363
+ }
34364
+ function realPtySpawn(file, args, opts) {
34365
+ const helperPath = resolveNodePtyHelperPath(require5.resolve("node-pty/package.json"), process.platform, process.arch);
34366
+ ensureNodePtyHelperExecutable(helperPath);
34367
+ return spawnPty2(file, args, opts);
34368
+ }
34369
+ function createTerminalService(deps) {
34370
+ const spawn11 = deps.spawn ?? realPtySpawn;
34371
+ const platform = deps.platform ?? process.platform;
34372
+ const sessions = new Map;
34373
+ const setTimer = deps.setTimer ?? ((fn, ms) => setTimeout(fn, ms));
34374
+ const clearTimer = deps.clearTimer ?? ((id) => clearTimeout(id));
34375
+ const resetIdle = (terminalId) => {
34376
+ const s = sessions.get(terminalId);
34377
+ if (!s)
34378
+ return;
34379
+ if (s.idleTimer)
34380
+ clearTimer(s.idleTimer);
34381
+ const ms = deps.idleTimeoutSeconds() * 1000;
34382
+ s.idleTimer = setTimer(() => {
34383
+ try {
34384
+ s.handle.kill();
34385
+ } catch {}
34386
+ }, ms);
34387
+ const t2 = s.idleTimer;
34388
+ if (typeof t2.unref === "function")
34389
+ t2.unref();
34390
+ };
34391
+ return {
34392
+ create({ cwd, cols, rows }) {
34393
+ if (platform === "win32")
34394
+ throw new Error("terminal-unsupported-platform");
34395
+ const terminalId = randomUUID3();
34396
+ const handle = spawn11(defaultShell(platform), [], { name: "xterm-256color", cols, rows, cwd, env: scrubEnv() });
34397
+ const session3 = { handle, seq: 0, idleTimer: null };
34398
+ sessions.set(terminalId, session3);
34399
+ handle.onData((data) => {
34400
+ deps.events.emit({ type: "terminal-output", terminalId, seq: session3.seq++, data });
34401
+ });
34402
+ handle.onExit(({ exitCode }) => {
34403
+ if (session3.idleTimer)
34404
+ clearTimer(session3.idleTimer);
34405
+ sessions.delete(terminalId);
34406
+ deps.events.emit({ type: "terminal-exit", terminalId, code: exitCode });
34407
+ });
34408
+ resetIdle(terminalId);
34409
+ return { terminalId };
34410
+ },
34411
+ write(terminalId, data) {
34412
+ const s = sessions.get(terminalId);
34413
+ if (!s)
34414
+ return;
34415
+ try {
34416
+ s.handle.write(data);
34417
+ } catch {}
34418
+ resetIdle(terminalId);
34419
+ },
34420
+ resize(terminalId, cols, rows) {
34421
+ const s = sessions.get(terminalId);
34422
+ if (!s)
34423
+ return;
34424
+ try {
34425
+ s.handle.resize(cols, rows);
34426
+ } catch {}
34427
+ resetIdle(terminalId);
34428
+ },
34429
+ close(terminalId) {
34430
+ const s = sessions.get(terminalId);
34431
+ if (!s)
34432
+ return;
34433
+ try {
34434
+ s.handle.kill();
34435
+ } catch {}
34436
+ },
34437
+ disposeAll() {
34438
+ for (const s of sessions.values()) {
34439
+ if (s.idleTimer)
34440
+ clearTimer(s.idleTimer);
34441
+ try {
34442
+ s.handle.kill();
34443
+ } catch {}
34444
+ }
34445
+ sessions.clear();
34446
+ }
34447
+ };
34448
+ }
34449
+ var require5, SENSITIVE_ENV_KEYS;
34450
+ var init_terminal_service = __esm(() => {
34451
+ init_node_pty_helper();
34452
+ require5 = createRequire6(import.meta.url);
34453
+ SENSITIVE_ENV_KEYS = [
34454
+ "ANTHROPIC_API_KEY",
34455
+ "OPENAI_API_KEY",
34456
+ "OPENROUTER_API_KEY",
34457
+ "GEMINI_API_KEY",
34458
+ "GOOGLE_API_KEY",
34459
+ "DEEPSEEK_API_KEY",
34460
+ "GROQ_API_KEY",
34461
+ "AWS_SECRET_ACCESS_KEY",
34462
+ "AWS_SESSION_TOKEN",
34463
+ "GH_TOKEN",
34464
+ "GITHUB_TOKEN",
34465
+ "NPM_TOKEN"
34466
+ ];
34467
+ });
34468
+
34310
34469
  // src/control/upload-store.ts
34311
34470
  import { mkdtemp as mkdtemp2, readdir as readdir5, rm as rm10, stat as stat4, writeFile as writeFile8 } from "node:fs/promises";
34312
34471
  import { homedir as homedir13 } from "node:os";
@@ -34487,7 +34646,7 @@ __export(exports_main, {
34487
34646
  main: () => main,
34488
34647
  buildApp: () => buildApp
34489
34648
  });
34490
- import { randomUUID as randomUUID3 } from "node:crypto";
34649
+ import { randomUUID as randomUUID4 } from "node:crypto";
34491
34650
  import { homedir as homedir14 } from "node:os";
34492
34651
  import { dirname as dirname13, join as join23 } from "node:path";
34493
34652
  import { fileURLToPath as fileURLToPath5 } from "node:url";
@@ -34867,7 +35026,7 @@ async function buildApp(paths, deps = {}) {
34867
35026
  };
34868
35027
  orchestration3 = new OrchestrationService({
34869
35028
  now: deps.loggerNow ?? (() => new Date),
34870
- createId: () => randomUUID3(),
35029
+ createId: () => randomUUID4(),
34871
35030
  config: config4,
34872
35031
  loadState: async () => JSON.parse(JSON.stringify(state)),
34873
35032
  saveState: async (nextState) => {
@@ -34980,6 +35139,10 @@ async function buildApp(paths, deps = {}) {
34980
35139
  const router3 = new CommandRouter(sessions, transport, config4, configStore, logger2, undefined, orchestration3, quota, scheduledService, deps.channel?.supportsScheduledMessages ? { supportsScheduledMessages: deps.channel.supportsScheduledMessages.bind(deps.channel) } : undefined, deps.channel?.nativeSessionListFormat ? deps.channel.nativeSessionListFormat.bind(deps.channel) : undefined, activeTurns);
34981
35140
  const agent3 = new ConsoleAgent(router3, logger2);
34982
35141
  const controlEvents = createControlEventBus(logger2);
35142
+ const terminalService = createTerminalService({
35143
+ events: controlEvents,
35144
+ idleTimeoutSeconds: () => terminalIdleTimeoutSeconds(config4)
35145
+ });
34983
35146
  const uploadStore = new UploadStore;
34984
35147
  uploadStore.cleanup();
34985
35148
  const uploadCleanupInterval = setInterval(() => void uploadStore.cleanup().catch(() => {}), 60 * 60 * 1000);
@@ -35028,7 +35191,9 @@ async function buildApp(paths, deps = {}) {
35028
35191
  controlEvents.emit({ type: "workspaces-changed" });
35029
35192
  }
35030
35193
  },
35031
- uploadStore
35194
+ uploadStore,
35195
+ terminal: terminalService,
35196
+ terminalEnabled: () => terminalEnabled(config4)
35032
35197
  });
35033
35198
  const workspaceSignature = (cfg) => JSON.stringify(Object.keys(cfg.workspaces).sort().map((name) => {
35034
35199
  const ws = cfg.workspaces[name];
@@ -35120,6 +35285,7 @@ async function buildApp(paths, deps = {}) {
35120
35285
  scheduledScheduler.stop();
35121
35286
  configWatcher.close();
35122
35287
  clearInterval(uploadCleanupInterval);
35288
+ terminalService.disposeAll();
35123
35289
  if (progressHeartbeatInterval !== undefined) {
35124
35290
  clearInterval(progressHeartbeatInterval);
35125
35291
  }
@@ -35262,6 +35428,7 @@ var init_main = __esm(async () => {
35262
35428
  init_render_text();
35263
35429
  init_quota_manager();
35264
35430
  init_control_service();
35431
+ init_terminal_service();
35265
35432
  init_upload_store();
35266
35433
  init_agent_catalog();
35267
35434
  init_config_watcher();
@@ -36963,7 +37130,7 @@ var init_doctor2 = __esm(async () => {
36963
37130
 
36964
37131
  // src/cli.ts
36965
37132
  init_core_home();
36966
- import { randomUUID as randomUUID4 } from "node:crypto";
37133
+ import { randomUUID as randomUUID5 } from "node:crypto";
36967
37134
  import { homedir as homedir20 } from "node:os";
36968
37135
  import { dirname as dirname15, join as join27, sep as sep2 } from "node:path";
36969
37136
  import { fileURLToPath as fileURLToPath7 } from "node:url";
@@ -52639,7 +52806,7 @@ async function prepareMcpCoordinatorStartup(input) {
52639
52806
  return { kind: "external-coordinator" };
52640
52807
  }
52641
52808
  function createMcpStdioIdentityResolver(input) {
52642
- const instanceId = randomUUID4().slice(0, 8);
52809
+ const instanceId = randomUUID5().slice(0, 8);
52643
52810
  return async (context) => {
52644
52811
  const parsedCoordinatorSession = input.parsedCoordinatorSession?.trim() || null;
52645
52812
  const workspace3 = input.workspace?.trim() || null;
@@ -42,6 +42,12 @@ export interface TransportConfig {
42
42
  */
43
43
  preferLocalAgents?: boolean;
44
44
  }
45
+ export interface TerminalConfig {
46
+ /** Default false. When false, control.terminal.create is rejected before any PTY spawns. */
47
+ enabled: boolean;
48
+ /** Idle seconds before a terminal PTY is auto-killed. Defaults to 900 (15 min). */
49
+ idleTimeoutSeconds?: number;
50
+ }
45
51
  export type LoggingLevel = "error" | "info" | "debug";
46
52
  export interface PerfLogConfig {
47
53
  enabled: boolean;
@@ -103,4 +109,7 @@ export interface AppConfig {
103
109
  orchestration: OrchestrationConfig;
104
110
  later?: LaterConfig;
105
111
  language?: Locale;
112
+ terminal?: TerminalConfig;
106
113
  }
114
+ export declare function terminalEnabled(config: AppConfig): boolean;
115
+ export declare function terminalIdleTimeoutSeconds(config: AppConfig): number;
@@ -64,6 +64,15 @@ export type ControlEvent = {
64
64
  chatKey: string;
65
65
  sessionAlias: string;
66
66
  messages: NativeHistoryMessage[];
67
+ } | {
68
+ type: "terminal-output";
69
+ terminalId: string;
70
+ seq: number;
71
+ data: string;
72
+ } | {
73
+ type: "terminal-exit";
74
+ terminalId: string;
75
+ code: number;
67
76
  } | {
68
77
  type: "orchestration-changed";
69
78
  };
@@ -77,6 +77,8 @@ export interface ControlServiceDeps {
77
77
  remove(name: string): Promise<void>;
78
78
  };
79
79
  uploadStore: UploadStore;
80
+ terminal: import("./terminal-service").TerminalService;
81
+ terminalEnabled: () => boolean;
80
82
  }
81
83
  export interface ControlPromptInput {
82
84
  chatKey: string;
@@ -180,4 +182,11 @@ export declare class ControlService {
180
182
  private executeTurn;
181
183
  cancelTurn(chatKey: string, sessionAlias: string): boolean;
182
184
  executeCommand(input: ControlExecuteCommandInput): Promise<string>;
185
+ /** Open an interactive terminal in the session's workspace cwd. Rejected when terminal is disabled. */
186
+ createTerminal(chatKey: string, sessionAlias: string, cols: number, rows: number): Promise<{
187
+ terminalId: string;
188
+ }>;
189
+ writeTerminal(terminalId: string, data: string): void;
190
+ resizeTerminal(terminalId: string, cols: number, rows: number): void;
191
+ closeTerminal(terminalId: string): void;
183
192
  }
@@ -0,0 +1,48 @@
1
+ import type { ControlEventBus } from "./control-event-bus";
2
+ /**
3
+ * Secret-bearing env keys stripped before handing the shell its environment.
4
+ * Best-effort denylist only — a real shell still inherits the full process env;
5
+ * custom secrets (DATABASE_URL, *_TOKEN, ~/.ssh keys, etc.) not on this list
6
+ * pass through. Do not treat env scrubbing as a security guarantee.
7
+ */
8
+ export declare const SENSITIVE_ENV_KEYS: string[];
9
+ export interface PtyHandle {
10
+ onData(cb: (data: string) => void): void;
11
+ onExit(cb: (e: {
12
+ exitCode: number;
13
+ }) => void): void;
14
+ write(data: string): void;
15
+ resize(cols: number, rows: number): void;
16
+ kill(): void;
17
+ }
18
+ export type PtySpawn = (file: string, args: string[], opts: {
19
+ name: string;
20
+ cols: number;
21
+ rows: number;
22
+ cwd: string;
23
+ env: Record<string, string>;
24
+ }) => PtyHandle;
25
+ export interface TerminalCreateInput {
26
+ cwd: string;
27
+ cols: number;
28
+ rows: number;
29
+ }
30
+ export interface TerminalService {
31
+ create(input: TerminalCreateInput): {
32
+ terminalId: string;
33
+ };
34
+ write(terminalId: string, data: string): void;
35
+ resize(terminalId: string, cols: number, rows: number): void;
36
+ close(terminalId: string): void;
37
+ disposeAll(): void;
38
+ }
39
+ export interface TerminalServiceDeps {
40
+ events: ControlEventBus;
41
+ idleTimeoutSeconds: () => number;
42
+ spawn?: PtySpawn;
43
+ platform?: NodeJS.Platform;
44
+ /** Injectable timer primitives; defaults to global setTimeout/clearTimeout. */
45
+ setTimer?: (fn: () => void, ms: number) => unknown;
46
+ clearTimer?: (id: unknown) => void;
47
+ }
48
+ export declare function createTerminalService(deps: TerminalServiceDeps): TerminalService;
@@ -0,0 +1,4 @@
1
+ type ChmodFunction = (path: string, mode: number) => Promise<void>;
2
+ export declare function resolveNodePtyHelperPath(packageJsonPath: string, platform: NodeJS.Platform, arch: string): string | null;
3
+ export declare function ensureNodePtyHelperExecutable(helperPath: string | null, chmod?: ChmodFunction): Promise<void>;
4
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ganglion/xacpx",
3
- "version": "0.16.0",
3
+ "version": "0.17.0-beta.0",
4
4
  "description": "随时随地通过聊天频道(微信 / 飞书 / 元宝等)远程控制 `acpx` 上的 Claude Code、Codex 等 Agents。",
5
5
  "keywords": [
6
6
  "acpx",