@zq-silk/yui 1.1.0 → 1.1.2

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/README.md CHANGED
@@ -43,7 +43,7 @@ not from terminal windows you juggle or details you have to remember.
43
43
  ## Quick start
44
44
 
45
45
  You need Linux x64 (glibc) or macOS (x64 or Apple Silicon), Git, tmux, and
46
- Node.js `^20.17.0`, `^22.9.0` or `^24.0.0`. On macOS install tmux with
46
+ Node.js `^20.17.0`, `^22.9.0`, `^24.0.0` or `^26.0.0`. On macOS install tmux with
47
47
  `brew install tmux`. For the simplest setup, have Codex CLI or Claude Code CLI
48
48
  installed and ready to use with your own account. Yui coordinates those
49
49
  Agents; it does not supply model access.
@@ -1,6 +1,7 @@
1
1
  import { accessSync, constants, existsSync, lstatSync, realpathSync } from "node:fs";
2
2
  import { isAbsolute, join } from "node:path";
3
3
  import Database from "better-sqlite3";
4
+ import { inspectPty } from "./ptyProbe.js";
4
5
  import { configuredAgentToDefinition, resolveAgentEnvironment } from "../agent/agent.js";
5
6
  import { operationalAgentEnvironment } from "../agent/launchEnvironment.js";
6
7
  import { resolveTmuxBin } from "../config/yuiConfig.js";
@@ -137,6 +138,7 @@ async function inspectDoctor(env, executor) {
137
138
  controller,
138
139
  ...(domain === undefined ? [] : [domain]),
139
140
  ...toolChecks,
141
+ ...inspectPty(),
140
142
  ...agentChecks,
141
143
  ...review.checks
142
144
  ],
@@ -0,0 +1,74 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import { readFileSync } from "node:fs";
3
+ import { createRequire } from "node:module";
4
+ import { fileURLToPath } from "node:url";
5
+ /** Native loading and spawn happen outside Doctor: a native abort cannot kill the CLI. */
6
+ export function inspectPty(options = {}) {
7
+ const started = Date.now();
8
+ const checks = [];
9
+ const major = Number(process.versions.node.split(".")[0]);
10
+ if (![20, 22, 24, 26].includes(major)) {
11
+ checks.push({
12
+ name: "pty Node runtime", status: "unsupported",
13
+ detail: `Node ${process.versions.node} is outside Yui's supported Node 20/22/24/26 range; this is not evidence of a helper permission failure.`
14
+ });
15
+ }
16
+ let entry;
17
+ let identity;
18
+ try {
19
+ const require = createRequire(options.requireFrom ?? import.meta.url);
20
+ entry = require.resolve("node-pty");
21
+ const manifest = require.resolve("node-pty/package.json");
22
+ const version = JSON.parse(readFileSync(manifest, "utf8")).version;
23
+ identity = `node-pty ${version}; entry=${entry}; package=${manifest}`;
24
+ }
25
+ catch (error) {
26
+ return [...checks, {
27
+ name: "node-pty",
28
+ status: error.code === "MODULE_NOT_FOUND" ? "missing" : "invalid",
29
+ detail: `Dependency resolution failed: ${String(error)}`
30
+ }];
31
+ }
32
+ const timeoutMs = options.timeoutMs ?? 1_500;
33
+ const result = spawnSync(process.execPath, [
34
+ fileURLToPath(new URL("./ptyProbeChild.js", import.meta.url)),
35
+ entry, String(options.probeTimeoutMs ?? 750)
36
+ ], {
37
+ // No NODE_OPTIONS, shell startup files, preload hooks or user configuration.
38
+ env: { PATH: "/usr/bin:/bin", LANG: "C" },
39
+ cwd: "/",
40
+ encoding: "utf8",
41
+ timeout: timeoutMs,
42
+ killSignal: "SIGKILL",
43
+ maxBuffer: 64 * 1024
44
+ });
45
+ for (const line of (result.stdout ?? "").split("\n").filter(Boolean)) {
46
+ try {
47
+ const check = JSON.parse(line);
48
+ if (["node-pty", "pty helper", "pty spawn"].includes(check.name)
49
+ && ["ok", "missing", "invalid"].includes(check.status)
50
+ && typeof check.detail === "string") {
51
+ checks.push({ ...check, detail: `${identity}; ${check.detail}` });
52
+ }
53
+ }
54
+ catch { /* Native diagnostics on stdout are retained below on failure. */ }
55
+ }
56
+ const completed = checks.some(check => check.name === "pty spawn")
57
+ || checks.some(check => check.status !== "ok" && check.name !== "pty Node runtime");
58
+ if (result.error || result.signal || result.status !== 0 || !completed) {
59
+ const timedOut = result.error?.code === "ETIMEDOUT";
60
+ // A partial success line is not success if the isolated process failed to finish.
61
+ for (let i = 0; i < checks.length; i += 1) {
62
+ if (checks[i].name === "pty spawn" && checks[i].status === "ok") {
63
+ checks[i] = { ...checks[i], status: "invalid", detail: `${checks[i].detail}; isolated probe did not finish successfully` };
64
+ }
65
+ }
66
+ checks.push({
67
+ name: "pty probe", status: "invalid",
68
+ detail: `${identity}; ${timedOut ? `timeout after ${timeoutMs}ms` : "isolated probe failed"}; exit=${result.status} signal=${result.signal}; ${result.error?.message ?? ""} ${(result.stderr || result.stdout || "").trim()}`
69
+ });
70
+ }
71
+ return checks.map(check => ({
72
+ ...check, detail: `${check.detail}; elapsed=${Date.now() - started}ms`
73
+ }));
74
+ }
@@ -0,0 +1,90 @@
1
+ import { accessSync, constants, statSync, writeSync } from "node:fs";
2
+ import { createRequire } from "node:module";
3
+ import { basename, dirname, join } from "node:path";
4
+ const require = createRequire(import.meta.url);
5
+ const entry = process.argv[2];
6
+ const timeoutMs = Number(process.argv[3]);
7
+ const report = (check) => writeSync(1, `${JSON.stringify(check)}\n`);
8
+ const errorDetail = (error) => error instanceof Error ? `${error.name}: ${error.message}` : String(error);
9
+ async function probe() {
10
+ let pty;
11
+ try {
12
+ pty = require(entry);
13
+ if (typeof pty.spawn !== "function")
14
+ throw new Error("node-pty does not expose spawn");
15
+ report({ name: "node-pty", status: "ok", detail: "native module loaded" });
16
+ }
17
+ catch (error) {
18
+ report({
19
+ name: "node-pty", status: "invalid",
20
+ detail: `native load failed: ${errorDetail(error)}. If installation reported blocked lifecycle scripts, review npm allow-scripts separately; no install files were changed.`
21
+ });
22
+ return;
23
+ }
24
+ if (process.platform === "darwin") {
25
+ // The pinned node-pty selects build/Release, build/Debug or prebuilds.
26
+ // Inspect the module it actually loaded, not the first helper that exists.
27
+ const native = Object.keys(require.cache).find(path => basename(path) === "pty.node");
28
+ if (!native) {
29
+ report({ name: "pty helper", status: "invalid", detail: "Cannot identify the loaded pty.node; helper path is unverified." });
30
+ return;
31
+ }
32
+ const helper = join(dirname(native), "spawn-helper");
33
+ let mode = "unknown";
34
+ try {
35
+ mode = (statSync(helper).mode & 0o7777).toString(8).padStart(4, "0");
36
+ accessSync(helper, constants.X_OK);
37
+ report({ name: "pty helper", status: "ok", detail: `helper=${helper}; mode=${mode}; executable by current user` });
38
+ }
39
+ catch (error) {
40
+ const code = error.code;
41
+ report({
42
+ name: "pty helper", status: code === "ENOENT" ? "missing" : "invalid",
43
+ detail: `helper=${helper}; mode=${mode}; ${code === "EACCES" ? "permission denied for current user" : "helper check failed"}: ${errorDetail(error)}`
44
+ });
45
+ return;
46
+ }
47
+ }
48
+ await new Promise(resolve => {
49
+ const marker = "yui-pty-probe-ok";
50
+ let terminal;
51
+ let output = "";
52
+ let finished = false;
53
+ const finish = (status, detail, kill = false) => {
54
+ if (finished)
55
+ return;
56
+ finished = true;
57
+ clearTimeout(timer);
58
+ if (kill && terminal) {
59
+ try {
60
+ terminal.kill("SIGKILL");
61
+ }
62
+ catch (error) {
63
+ detail += `; cleanup: ${errorDetail(error)}`;
64
+ }
65
+ }
66
+ report({ name: "pty spawn", status, detail });
67
+ resolve();
68
+ };
69
+ const timer = setTimeout(() => finish("invalid", `timeout after ${timeoutMs}ms waiting for output and normal exit`, true), timeoutMs);
70
+ try {
71
+ // Absolute, fixed executable; no shell, Node preloads, Agent or account access.
72
+ terminal = pty.spawn("/bin/echo", [marker], {
73
+ name: "xterm", cols: 80, rows: 24, cwd: "/",
74
+ env: { PATH: "/usr/bin:/bin", LANG: "C" }
75
+ });
76
+ terminal.onData(data => { output = (output + data).slice(-4096); });
77
+ terminal.onExit(({ exitCode, signal }) => {
78
+ const ok = exitCode === 0 && !signal && output.replace(/\r/g, "").trim() === marker;
79
+ finish(ok ? "ok" : "invalid", `${ok ? "expected output and normal exit confirmed (PTY dependency only; Web Terminal and Provider protocols unverified)" : "unexpected output or exit"}; exit=${exitCode}; signal=${signal ?? 0}; output=${JSON.stringify(output)}`);
80
+ });
81
+ }
82
+ catch (error) {
83
+ finish("invalid", `spawn failed: ${errorDetail(error)}`, true);
84
+ }
85
+ });
86
+ }
87
+ await probe();
88
+ // node-pty's native watcher can retain handles; the fixed child has exited, or
89
+ // was explicitly killed on failure. The parent also bounds native hangs/aborts.
90
+ process.exit(0);
@@ -101,6 +101,8 @@ do not add prose-matching tests or claim model validation from static checks.
101
101
  `ci.yml` runs core plus one assembled-package normal-path smoke per supported
102
102
  platform (Linux x64, Mac Intel and Apple Silicon) on every PR, without another
103
103
  lint or broad regression suite.
104
+ Linux runs on Node 24 and 26; release fresh-install smoke covers Node 20, 22,
105
+ 24 and 26 on all three platforms.
104
106
  `node scripts/smoke-runtime-package.mjs --assembled .release-stage` exercises
105
107
  the actual CLI/Controller/Host/SQLite and isolated tmux, replacing only the
106
108
  external Provider with a deterministic fixture. It covers setup, durable input
@@ -118,6 +120,14 @@ freshly installed package through
118
120
  artifact and provenance boundaries. This validates runtime integration, not
119
121
  real-model behavior. Pure contract and safety tests remain in `test/core`;
120
122
  production wiring is exercised here rather than only through mocked ports.
123
+ The native dependency check launches a fixed local program through PTY and
124
+ requires its output and normal exit. Installed-package smoke explicitly checks
125
+ default Doctor's PTY results using dependencies resolved from the consumer.
126
+ On macOS it also tests a disposable copy with a non-executable spawn-helper:
127
+ Doctor must report the selected helper path and permissions without repairing it.
128
+ Doctor's isolated PTY probe waits up to 750 ms for output/exit, with a 1.5-second
129
+ outer native-process limit, and reports elapsed time. Existing Agent/Controller
130
+ diagnostics have their own costs; this is not a one-second whole-Doctor guarantee.
121
131
  The package smoke also checks unconditional status identity and update-owned
122
132
  resource/identity capture through the assembled package. Real lifecycle children
123
133
  stop the exact Controller and restore its captured launch identity while their
@@ -74,6 +74,8 @@ package-start 检查跟随已安装树中的本地 Skill 引用,包括跨 Role
74
74
 
75
75
  `ci.yml` 在每个 PR 上构建一次,运行 core 及一个组装包正常链路检查,不重复 lint,
76
76
  也不增加宽泛回归套件。
77
+ Linux CI 在 Node 24 和 26 上运行;发布时的全新安装 smoke 覆盖三个平台上的
78
+ Node 20、22、24 和 26。
77
79
  `node scripts/smoke-runtime-package.mjs --assembled .release-stage` 经过真实
78
80
  CLI/Controller/Host/SQLite 与隔离 tmux,仅用确定性夹具替换外部 Provider。它验证
79
81
  setup、输入跨重启持久化及幂等、scratch 激活、原生结果入库、完成后保留会话、
@@ -85,6 +87,12 @@ setup、输入跨重启持久化及幂等、scratch 激活、原生结果入库
85
87
  npm bin、依赖、受支持 Node 版本、产物和 provenance 边界。这证明运行时集成,不证明
86
88
  真实模型行为。纯契约与安全检查保留在 `test/core`,生产组件组装在这里验证,
87
89
  不只依赖模拟端口。
90
+ 原生依赖检查必须通过 PTY 启动固定本地程序,确认输出和正常退出。安装包 smoke
91
+ 明确断言默认 Doctor 的 PTY 检查成功,并从消费者安装目录解析依赖。macOS 还会
92
+ 在可丢弃的依赖副本中移除 spawn-helper 执行权限,验证 Doctor 报出实际 helper
93
+ 路径和权限原因且不自动修复。Doctor 的隔离 PTY 探针等待输出/退出最多 750 ms,
94
+ 外层原生进程限制为 1.5 秒,并报告耗时;原有 Agent/Controller 检查另有成本,
95
+ 这不是整个 Doctor 一秒内返回的保证。
88
96
  组装包检查还验证固定身份输出,以及升级侧通过组装包采集资源和精确 Controller 身份,
89
97
  并在父进程持有交接锁时,通过真实生命周期子进程停止精确 Controller、恢复其已捕获的
90
98
  启动身份。无关调用仍被锁阻止,锁保持由父进程持有,持久输入不变;
@@ -41,7 +41,7 @@ npm 包包含 Linux x64、Mac Intel 和 Apple Silicon 的 Yui 预编译程序。
41
41
  同一份包在三个平台验证后发布,运行时只选择对应平台的程序。
42
42
 
43
43
  需要 Linux x64 / glibc 或 macOS(x64 或 Apple Silicon)、Git、tmux,以及
44
- Node.js `^20.17.0`、`^22.9.0` 或 `^24.0.0`。macOS 上可用
44
+ Node.js `^20.17.0`、`^22.9.0`、`^24.0.0` 或 `^26.0.0`。macOS 上可用
45
45
  `brew install tmux` 安装 tmux。最简单的方式是先安装 Codex CLI 或 Claude
46
46
  Code CLI,并确保它已经可以使用你自己的账号正常工作。Yui 负责协调 Agent,
47
47
  不提供模型访问额度。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zq-silk/yui",
3
- "version": "1.1.0",
3
+ "version": "1.1.2",
4
4
  "description": "Local control plane for long-running native agent CLI sessions backed by tmux.",
5
5
  "license": "MIT",
6
6
  "private": false,
@@ -19,7 +19,7 @@
19
19
  "LICENSE"
20
20
  ],
21
21
  "engines": {
22
- "node": "^20.17.0 || ^22.9.0 || ^24.0.0"
22
+ "node": "^20.17.0 || ^22.9.0 || ^24.0.0 || ^26.0.0"
23
23
  },
24
24
  "os": [
25
25
  "linux",
@@ -49,7 +49,7 @@
49
49
  "@xterm/addon-fit": "^0.11.0",
50
50
  "@xterm/xterm": "^6.0.0",
51
51
  "better-sqlite3": "^12.11.1",
52
- "node-pty": "^1.1.0",
52
+ "node-pty": "1.2.0-beta.15",
53
53
  "smol-toml": "1.8.0",
54
54
  "ws": "^8.21.1"
55
55
  }