viber-channel 0.8.20 → 0.8.21

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.
@@ -14,7 +14,7 @@
14
14
  * the bridge's default "register a fresh instance" path.
15
15
  */
16
16
 
17
- import { spawn as nodeSpawn } from "node:child_process";
17
+ import { spawn as nodeSpawn } from "./hidden_proc.js";
18
18
  import type { AgentSpec, ChildHandle, SpawnFn } from "./supervisor.ts";
19
19
 
20
20
  /** Minimal child surface the adapter consumes (matches node's ChildProcess). */
@@ -1,4 +1,4 @@
1
- import { execSync } from "node:child_process";
1
+ import { execSync } from "./hidden_proc.js";
2
2
  import { createHash } from "node:crypto";
3
3
  import { realpathSync } from "node:fs";
4
4
 
@@ -0,0 +1,99 @@
1
+ /**
2
+ * hidden_proc.ts — the ONLY door to `node:child_process` and `Bun.spawn*` in this package (#638).
3
+ * vibe-master and viber-gateway re-export it, so there is ONE `hide()` for the three packages.
4
+ *
5
+ * The Viber app (`viber-gateway.exe`) is a GUI-subsystem binary: it has no console. On Windows a
6
+ * console child of a console-less parent gets a NEW, VISIBLE console unless the spawn passes
7
+ * `windowsHide: true`. The runner heartbeat (every 30 s) enumerates processes through PowerShell,
8
+ * so each beat flashed a window.
9
+ *
10
+ * Measured (#638 step-01, bun 1.3.14, compiled `--windows-hide-console` exe started through WMI):
11
+ *
12
+ * ```
13
+ * execFileSync / execSync / exec / Bun.spawnSync hide=false -> child console visible=True
14
+ * hide=true -> child hwnd=0 (NO console at all)
15
+ * ```
16
+ *
17
+ * `hwnd=0` is a console WITHOUT a window (CREATE_NO_WINDOW), and it is inherited: the `exec` rows are
18
+ * already a GRANDCHILD (`cmd.exe /c powershell`) and read `hwnd=0` too. So hiding the first edge may be
19
+ * enough for console descendants — but not for a Bun/Node parent that spawns with its own flags.
20
+ * Measured again for a Bun parent (step-03): compiled GUI exe -> Bun console child spawned with
21
+ * hide=true -> its grandchild spawned WITHOUT hide reads `hwnd=0 visible=False`; child hide=false ->
22
+ * grandchild `visible=True`. So hiding the first edge is enough for console descendants.
23
+ * Every spawn still goes through this helper BY DECISION (plan 638, defence in depth): the next first
24
+ * edge is written by someone who has not read this.
25
+ * No-op outside Windows.
26
+ */
27
+ import * as cp from "node:child_process";
28
+
29
+ /** Every option object that reaches `child_process` passes here. Exported for its witness. */
30
+ export function hide<T extends object>(opts: T | undefined): T & { windowsHide: true } {
31
+ return { ...(opts ?? ({} as T)), windowsHide: true };
32
+ }
33
+
34
+ type Backend = Pick<typeof cp, "exec" | "execSync" | "execFile" | "execFileSync" | "spawn">;
35
+
36
+ /**
37
+ * `execFile` / `execFileSync` / `spawn` accept `args` OR options in 2nd place (and `execFile` a
38
+ * callback anywhere). Split them so a call without `args` still carries its options through `hide`.
39
+ */
40
+ function splitArgs(rest: unknown[]): { args: readonly string[]; tail: unknown[] } {
41
+ return Array.isArray(rest[0]) ? { args: rest[0] as readonly string[], tail: rest.slice(1) } : { args: [], tail: rest };
42
+ }
43
+
44
+ function optsAndCallback(tail: unknown[]): { opts: object | undefined; cb: unknown } {
45
+ const cb = tail.find((t) => typeof t === "function");
46
+ const opts = tail.find((t) => t !== null && typeof t === "object") as object | undefined;
47
+ return { opts, cb };
48
+ }
49
+
50
+ /** The wrappers over a given backend. Exported so the witness can hand a recording fake (a
51
+ * `mock.module` of `node:child_process` leaks into every other test file of the run). */
52
+ export function hiddenOver(b: Backend): Backend {
53
+ return {
54
+ exec: ((command: string, ...rest: unknown[]) => {
55
+ const { opts, cb } = optsAndCallback(rest);
56
+ return b.exec(command, hide(opts as cp.ExecOptions | undefined), cb as Parameters<typeof cp.exec>[2]);
57
+ }) as typeof cp.exec,
58
+ execSync: ((command: string, options?: cp.ExecSyncOptions) => b.execSync(command, hide(options))) as typeof cp.execSync,
59
+ execFile: ((file: string, ...rest: unknown[]) => {
60
+ const { args, tail } = splitArgs(rest);
61
+ const { opts, cb } = optsAndCallback(tail);
62
+ return b.execFile(file, args, hide(opts as cp.ExecFileOptions | undefined), cb as Parameters<typeof cp.execFile>[3]);
63
+ }) as typeof cp.execFile,
64
+ execFileSync: ((file: string, ...rest: unknown[]) => {
65
+ const { args, tail } = splitArgs(rest);
66
+ return b.execFileSync(file, args, hide(tail[0] as cp.ExecFileSyncOptions | undefined));
67
+ }) as typeof cp.execFileSync,
68
+ spawn: ((command: string, ...rest: unknown[]) => {
69
+ const { args, tail } = splitArgs(rest);
70
+ return b.spawn(command, args, hide(tail[0] as cp.SpawnOptions | undefined));
71
+ }) as typeof cp.spawn,
72
+ };
73
+ }
74
+
75
+ export const { exec, execSync, execFile, execFileSync, spawn } = hiddenOver(cp);
76
+
77
+ // ── Bun.spawn* (the channel, vibe-master and the gateway all run on Bun) ──
78
+
79
+ type BunSpawn = typeof Bun.spawn;
80
+ type BunSpawnSync = typeof Bun.spawnSync;
81
+
82
+ /**
83
+ * Both forms of `Bun.spawn*`: `(cmd[], opts)` and `({ cmd, ...opts })` — in the second the options live
84
+ * in the FIRST argument, so hiding only the second would silently leave it visible.
85
+ */
86
+ function hiddenArgs(cmd: unknown, opts: unknown): [unknown, unknown?] {
87
+ return Array.isArray(cmd) ? [cmd, hide(opts as object | undefined)] : [hide(cmd as object)];
88
+ }
89
+
90
+ /** The wrappers over a given `Bun`. Exported so the witness can hand a recording fake. */
91
+ export function hiddenBunOver(b: { spawn: BunSpawn; spawnSync: BunSpawnSync }): { bunSpawn: BunSpawn; bunSpawnSync: BunSpawnSync } {
92
+ const call = (f: (...a: unknown[]) => unknown) => (cmd: unknown, opts?: unknown) => f(...hiddenArgs(cmd, opts));
93
+ return {
94
+ bunSpawn: call(b.spawn as (...a: unknown[]) => unknown) as BunSpawn,
95
+ bunSpawnSync: call(b.spawnSync as (...a: unknown[]) => unknown) as BunSpawnSync,
96
+ };
97
+ }
98
+
99
+ export const { bunSpawn, bunSpawnSync } = hiddenBunOver(Bun);
@@ -20,7 +20,7 @@
20
20
  * still alive". `signal 0` probes existence without delivering a signal.
21
21
  */
22
22
 
23
- import { execSync } from "node:child_process";
23
+ import { execSync } from "./hidden_proc.js";
24
24
 
25
25
  /**
26
26
  * Shim processes that sit BETWEEN the real agent (e.g. claude.exe) and us in the
@@ -22,7 +22,7 @@
22
22
  * Returning `undefined` anywhere keeps the caller's conservative answer, but it is now
23
23
  * the exception rather than the rule off Windows.
24
24
  */
25
- import { exec, execSync } from "node:child_process";
25
+ import { exec, execSync } from "./hidden_proc.js";
26
26
  import { readdirSync, readFileSync } from "node:fs";
27
27
 
28
28
  /**
@@ -13,7 +13,7 @@
13
13
  * injectable so the flow is unit-testable without shelling or a live server.
14
14
  */
15
15
 
16
- import { execFile } from "node:child_process";
16
+ import { execFile } from "./hidden_proc.js";
17
17
  import { randomBytes } from "node:crypto";
18
18
  import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
19
19
  import { join } from "node:path";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "viber-channel",
3
- "version": "0.8.20",
3
+ "version": "0.8.21",
4
4
  "description": "Voice + text MCP channel between a Claude Code session and the Viber UI (https://viber.dgypx.dev). Push transcripts to Claude; send_message tool delivers text back to the UI.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -6,7 +6,8 @@
6
6
  * messages into a bridge-owned persistent Codex app-server thread, and posts
7
7
  * Codex's final assistant reply back into Viber.
8
8
  */
9
- import { spawn, type ChildProcessWithoutNullStreams } from "node:child_process";
9
+ import type { ChildProcessWithoutNullStreams } from "node:child_process";
10
+ import { spawn } from "./lib/hidden_proc.js";
10
11
  import { createHash } from "node:crypto";
11
12
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
12
13
  import { dirname, join } from "node:path";