@syrkos/filaxis 0.1.0-alpha.10

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.
Files changed (85) hide show
  1. package/AGENTS.md +46 -0
  2. package/LICENSE +202 -0
  3. package/NOTICE +10 -0
  4. package/README.md +61 -0
  5. package/bin/filaxis.ts +112 -0
  6. package/dist/agent-toolkit.d.ts +99 -0
  7. package/dist/dialect.d.ts +31 -0
  8. package/dist/driver.d.ts +130 -0
  9. package/dist/events.d.ts +26 -0
  10. package/dist/factory.d.ts +16 -0
  11. package/dist/file-edit.d.ts +43 -0
  12. package/dist/file-transfer.d.ts +44 -0
  13. package/dist/index.d.ts +38 -0
  14. package/dist/index.js +4200 -0
  15. package/dist/layer-discovery.d.ts +30 -0
  16. package/dist/layer-env.d.ts +95 -0
  17. package/dist/local-driver.d.ts +47 -0
  18. package/dist/local-session-manager.d.ts +98 -0
  19. package/dist/logger.d.ts +12 -0
  20. package/dist/remote-session-manager.d.ts +64 -0
  21. package/dist/runtime-stack.d.ts +52 -0
  22. package/dist/session-buffer.d.ts +73 -0
  23. package/dist/session-types.d.ts +74 -0
  24. package/dist/shell-discovery.d.ts +4 -0
  25. package/dist/ssh-helpers.d.ts +88 -0
  26. package/dist/ssh-pty-driver.d.ts +74 -0
  27. package/dist/stream-session-manager.d.ts +83 -0
  28. package/dist/stream-window.d.ts +124 -0
  29. package/dist/terminal/command-executer.d.ts +73 -0
  30. package/dist/terminal/output-buffer.d.ts +49 -0
  31. package/dist/terminal/session-launcher.d.ts +28 -0
  32. package/dist/terminal/session-registry.d.ts +29 -0
  33. package/dist/terminal/tmux-invoker.d.ts +8 -0
  34. package/dist/terminal/types.d.ts +56 -0
  35. package/dist/terminal-manager.d.ts +30 -0
  36. package/dist/tmux-driver.d.ts +49 -0
  37. package/dist/vt100-screen.d.ts +46 -0
  38. package/docs/DEVELOPMENT.md +38 -0
  39. package/docs/KNOWN-ISSUES.md +35 -0
  40. package/docs/implementation-notes.md +57 -0
  41. package/docs/skills/cross-host-diagnosis.md +21 -0
  42. package/docs/skills/file-sync.md +22 -0
  43. package/docs/skills/longrun-observe.md +20 -0
  44. package/docs/skills/stream-window.md +28 -0
  45. package/docs//345/256/214/346/225/264/345/267/241/346/243/200/346/212/245/345/221/212.md +141 -0
  46. package/docs//346/265/213/350/257/2252.md +112 -0
  47. package/docs//346/265/213/350/257/2253.md +73 -0
  48. package/docs//346/265/213/350/257/2254.md +138 -0
  49. package/docs//346/265/213/350/257/2255.md +124 -0
  50. package/docs//347/254/254/344/272/214/344/273/275/345/267/241/346/243/200/346/212/245/345/221/212.md +103 -0
  51. package/examples/agent-tools.ts +74 -0
  52. package/examples/cross-host-diagnosis.ts +49 -0
  53. package/examples/longrun-observe.ts +46 -0
  54. package/package.json +47 -0
  55. package/src/agent-toolkit.ts +247 -0
  56. package/src/dialect.ts +100 -0
  57. package/src/driver.ts +142 -0
  58. package/src/events.ts +96 -0
  59. package/src/factory.ts +51 -0
  60. package/src/file-edit.ts +195 -0
  61. package/src/file-transfer.ts +196 -0
  62. package/src/index.ts +38 -0
  63. package/src/layer-discovery.ts +79 -0
  64. package/src/layer-env.ts +331 -0
  65. package/src/local-driver.ts +325 -0
  66. package/src/local-session-manager.ts +397 -0
  67. package/src/logger.ts +19 -0
  68. package/src/remote-session-manager.ts +286 -0
  69. package/src/runtime-stack.ts +128 -0
  70. package/src/session-buffer.ts +183 -0
  71. package/src/session-types.ts +78 -0
  72. package/src/shell-discovery.ts +52 -0
  73. package/src/ssh-helpers.ts +225 -0
  74. package/src/ssh-pty-driver.ts +655 -0
  75. package/src/stream-session-manager.ts +296 -0
  76. package/src/stream-window.ts +309 -0
  77. package/src/terminal/command-executer.ts +374 -0
  78. package/src/terminal/output-buffer.ts +126 -0
  79. package/src/terminal/session-launcher.ts +146 -0
  80. package/src/terminal/session-registry.ts +98 -0
  81. package/src/terminal/tmux-invoker.ts +65 -0
  82. package/src/terminal/types.ts +85 -0
  83. package/src/terminal-manager.ts +83 -0
  84. package/src/tmux-driver.ts +167 -0
  85. package/src/vt100-screen.ts +166 -0
@@ -0,0 +1,52 @@
1
+ /**
2
+ * shell-discovery.ts — 本地 shell 发现(Windows WSL 入口坑防护——Gemini 试用反馈 #1)
3
+ *
4
+ * 痛点:win32 PATH 上的 `bash` 可能是 `C:\Windows\System32\bash.exe`(WSL 转发入口,
5
+ * 未配置默认分发时 spawn 即挂)。发现序:
6
+ * 1. 显式传入(opts.shell / FILAXIS_SHELL 环境变量)
7
+ * 2. PATH 上的 bash——**若非 System32 WSL 入口则直接用**(并发 spawn 热路径:
8
+ * 完整路径 exe 在 Windows Defender 并发扫描下首启可达 20s+,PATH 名 spawn 无此坑——生产实证)
9
+ * 3. Git Bash 标准路径嗅探(PATH bash 即 WSL 入口的兜底)
10
+ * 4. 回退 "bash" + 诊断提示(用户可能真有 WSL 发行版)
11
+ */
12
+ import { existsSync } from "node:fs";
13
+ import { execSync } from "node:child_process";
14
+
15
+ const GIT_BASH_CANDIDATES = [
16
+ "C:\\Program Files\\Git\\bin\\bash.exe",
17
+ "C:\\Program Files\\Git\\usr\\bin\\bash.exe",
18
+ "C:\\Program Files (x86)\\Git\\bin\\bash.exe",
19
+ ];
20
+
21
+ /** PATH 上 bash 的解析结果(无则 undefined) */
22
+ function bashOnPath(): string | undefined {
23
+ try {
24
+ return execSync("where bash", { stdio: ["ignore", "pipe", "ignore"] })
25
+ .toString().split("\n").map((l) => l.trim()).filter(Boolean)[0];
26
+ } catch {
27
+ return undefined;
28
+ }
29
+ }
30
+
31
+ /** 发现本执行 shell(Linux/macOS 恒 "bash") */
32
+ export function resolveLocalShell(preferred?: string, log?: { warn(msg: string): void }): string {
33
+ if (preferred) return preferred;
34
+ if (process.platform !== "win32") return "bash";
35
+
36
+ const fromEnv = process.env.FILAXIS_SHELL;
37
+ if (fromEnv) return fromEnv;
38
+
39
+ const p = bashOnPath();
40
+ if (p && !/System32/i.test(p)) return "bash"; // PATH 上的真 bash(Git Bash 等)——并发热路径
41
+
42
+ for (const c of GIT_BASH_CANDIDATES) {
43
+ if (existsSync(c)) return c;
44
+ }
45
+
46
+ log?.warn(
47
+ "[Filaxis] Windows 未找到可用 bash(PATH " +
48
+ (p ? "上是 WSL 转发入口" : "上无 bash") +
49
+ "且未嗅探到 Git Bash)——请安装 Git(https://git-scm.com)或设 FILAXIS_SHELL 指向 bash.exe 完整路径",
50
+ );
51
+ return "bash";
52
+ }
@@ -0,0 +1,225 @@
1
+ /**
2
+ * ssh-helpers.ts — SSH 连接参数渲染/进入管线/层状态机/层选择(纯函数)
3
+ *
4
+ * 净室平移自 Aethelum-Nexus `src/core/tools/runtime/ssh-pty-driver.ts` 纯函数段
5
+ * (源锚 8977ff3),OSS 化:剥 Aethelum 概念,marker 前缀换 Filaxis 命名空间。
6
+ */
7
+ import { homedir } from "node:os";
8
+ import { join } from "node:path";
9
+ import type { RuntimeDescriptor } from "./driver.js";
10
+
11
+ /** 端口透传(随层连接生命周期——层在隧道在,层断即撤) */
12
+ export interface SshPortForward {
13
+ kind: "L" | "R" | "D";
14
+ port: number;
15
+ target?: string;
16
+ bind?: string;
17
+ }
18
+
19
+ export function renderForwardArg(f: SshPortForward): string {
20
+ if (f.kind === "D") return String(f.port);
21
+ return `${f.bind ? `${f.bind}:` : ""}${f.port}:${f.target ?? ""}`;
22
+ }
23
+
24
+ /** SSH 连接参数(结构化——渲染进 ssh 命令行与身份解析) */
25
+ export interface SshConnParams {
26
+ user?: string;
27
+ port?: number;
28
+ keyFile?: string;
29
+ /** 跳板(-J;途径机不占栈层,由 ssh 自身代理) */
30
+ jump?: string;
31
+ forwards?: SshPortForward[];
32
+ /**
33
+ * 主机密钥策略(FEAT-12 / AC-12):
34
+ * - "tofu"(缺省)——accept-new:首连自动收主机键,已知主机不匹配则拒绝
35
+ * - "strict"——StrictHostKeyChecking=yes:仅已知主机可连(配合 knownHostsFile 自定义信任锚)
36
+ */
37
+ hostKeyPolicy?: "tofu" | "strict";
38
+ /** 自定义 known_hosts 路径(UserKnownHostsFile;strict 模式的信任锚,缺省用户默认) */
39
+ knownHostsFile?: string;
40
+ }
41
+
42
+ export type SshHop = string | { host: string; conn?: SshConnParams };
43
+
44
+ const isHopObj = (h: SshHop): h is { host: string; conn?: SshConnParams } => typeof h === "object";
45
+
46
+ export function hopHost(hop: SshHop): string {
47
+ return isHopObj(hop) ? hop.host : hop;
48
+ }
49
+
50
+ /** 人读形式:root@10.0.20.1:2222 -J jump -L8080→localhost:80 */
51
+ export function hopLabel(hop: SshHop): string {
52
+ if (!isHopObj(hop) || !hop.conn) return hopHost(hop);
53
+ const { user, port, jump, forwards } = hop.conn;
54
+ let s = `${user ? `${user}@` : ""}${hopHost(hop)}${port ? `:${port}` : ""}`;
55
+ if (jump) s += ` -J ${jump}`;
56
+ if (forwards?.length) {
57
+ s += ` ${forwards.map((f) => (f.kind === "D" ? `-D${f.port}` : `-${f.kind}${f.port}→${f.target}`)).join(" ")}`;
58
+ }
59
+ return s;
60
+ }
61
+
62
+ /** ssh 命令行参数(spawn 数组与 ssh -G 解析共用) */
63
+ export function sshArgsFor(hop: SshHop): string[] {
64
+ if (!isHopObj(hop) || !hop.conn) return [hopHost(hop)];
65
+ const { user, port, keyFile, jump, forwards } = hop.conn;
66
+ const args: string[] = [];
67
+ if (jump) args.push("-J", jump);
68
+ if (keyFile) args.push("-i", expandHomePath(keyFile));
69
+ if (port) args.push("-p", String(port));
70
+ for (const f of forwards ?? []) {
71
+ args.push(f.kind === "L" ? "-L" : f.kind === "R" ? "-R" : "-D", renderForwardArg(f));
72
+ }
73
+ args.push(user ? `${user}@${hopHost(hop)}` : hopHost(hop));
74
+ return args;
75
+ }
76
+
77
+ /** PTY 内单行发送所需的引用形式(私钥路径含空格等场景) */
78
+ export function sshArgsQuoted(hop: SshHop): string {
79
+ return sshArgsFor(hop)
80
+ .map((a) => (/^[A-Za-z0-9_@:./-]+$/.test(a) ? a : `"${a.replace(/"/g, '\\"')}"`))
81
+ .join(" ");
82
+ }
83
+
84
+ /** `~` 展开——spawn 参数数组不经过 shell,tilde 不会被展开(生产实证:公钥认证被拒 exit 255) */
85
+ export function expandHomePath(p: string): string {
86
+ if (p === "~") return homedir();
87
+ if (p.startsWith("~/") || p.startsWith("~\\")) return join(homedir(), p.slice(2));
88
+ return p;
89
+ }
90
+
91
+ /** 进入管线一步:建层时在链路末端落进内层 shell 的一条命令(docker exec / kubectl exec / su 等) */
92
+ export interface EnterStep {
93
+ /** 进入命令原文(重放用) */
94
+ cmd: string;
95
+ /** 显示/层选择标签(缺省截取 cmd) */
96
+ label?: string;
97
+ }
98
+
99
+ /** 层的完整进入管线(归一化读取):config.enterPipeline 为准;legacy enterCmd/enterLabel 就地归一 */
100
+ export function enterStepsOf(config: Record<string, unknown> | undefined): EnterStep[] {
101
+ if (Array.isArray(config?.enterPipeline)) {
102
+ return (config.enterPipeline as unknown[])
103
+ .filter((s) => s && typeof (s as EnterStep).cmd === "string" && (s as EnterStep).cmd)
104
+ .map((s) => ({ cmd: String((s as EnterStep).cmd), label: (s as EnterStep).label ? String((s as EnterStep).label) : undefined }));
105
+ }
106
+ const legacy = config?.enterCmd ? String(config.enterCmd) : "";
107
+ return legacy ? [{ cmd: legacy, label: config?.enterLabel ? String(config.enterLabel) : undefined }] : [];
108
+ }
109
+
110
+ /**
111
+ * 自治建链选项(AC-12:host key 策略显式可配):
112
+ * - ConnectTimeout=10(先于握手超时暴露真相);ServerAlive 30×3(静默死亡 ~90s 内暴露)
113
+ * - hostKeyPolicy:tofu(缺省,accept-new)/ strict(yes + 可选 UserKnownHostsFile)
114
+ * 不加 BatchMode:禁交互提示会抹掉「等待输入」这个可感知状态——感知到 prompt 后
115
+ * 把原文直交调用方,而不是假装它不存在。
116
+ * 渲染只含密钥/known_hosts **路径**,永不包含其内容(AC-12 无泄漏红线)。
117
+ */
118
+ export function autonomousSshOpts(conn?: SshConnParams): string[] {
119
+ const opts = ["-o", "ConnectTimeout=10", "-o", "ServerAliveInterval=30", "-o", "ServerAliveCountMax=3"];
120
+ if (conn?.hostKeyPolicy === "strict") {
121
+ opts.push("-o", "StrictHostKeyChecking=yes");
122
+ if (conn.knownHostsFile) {
123
+ opts.push("-o", `UserKnownHostsFile=${expandHomePath(conn.knownHostsFile)}`);
124
+ }
125
+ } else {
126
+ opts.push("-o", "StrictHostKeyChecking=accept-new"); // TOFU 缺省
127
+ }
128
+ return opts;
129
+ }
130
+
131
+ /** root 层 spawn 全参数(含 -tt 强制 PTY) */
132
+ export function rootSshSpawnArgs(destArgs: string[], conn?: SshConnParams): string[] {
133
+ return ["-tt", ...autonomousSshOpts(conn), ...destArgs];
134
+ }
135
+
136
+ /** PTY 内 hop 登录命令(选项语义与 root 对齐) */
137
+ export function hopSshCommand(hop: SshHop): string {
138
+ const conn = typeof hop === "object" ? hop.conn : undefined;
139
+ return `ssh ${autonomousSshOpts(conn).join(" ")} ${sshArgsQuoted(hop)}`;
140
+ }
141
+
142
+ /** 「等待输入」感知特征:ssh 停在交互提示(密码/口令/yes-no)——检测到即失败交还原文 */
143
+ export const SSH_INPUT_WAIT_RE =
144
+ /(?:password|passphrase|verification\s+code|login|密码|口令)[^\n]*[::]\s*$|yes\s*\/\s*no|are you sure|continue connecting/i;
145
+
146
+ const identityCache = new Map<string, string>();
147
+ /** 连接身份解析(ssh -G 本地解析不连网);失败回退 unresolved: 前缀(视为不相同,不误拦) */
148
+ export async function resolveSshIdentity(hop: SshHop): Promise<string> {
149
+ const cacheKey = typeof hop === "string" ? hop : JSON.stringify(hop);
150
+ const cached = identityCache.get(cacheKey);
151
+ if (cached) return cached;
152
+ try {
153
+ const { execFile } = await import("node:child_process");
154
+ const { promisify } = await import("node:util");
155
+ const X = promisify(execFile);
156
+ const { stdout } = await X("ssh", ["-G", ...sshArgsFor(hop)], { timeout: 8000 });
157
+ const h = stdout.match(/^hostname (\S+)$/m)?.[1] ?? hopHost(hop);
158
+ const p = stdout.match(/^port (\d+)$/m)?.[1] ?? "22";
159
+ const u = stdout.match(/^user (\S+)$/m)?.[1] ?? "";
160
+ const id = `${u ? u + "@" : ""}${h}:${p}`;
161
+ identityCache.set(cacheKey, id);
162
+ return id;
163
+ } catch {
164
+ return `unresolved:${hopHost(hop)}`;
165
+ }
166
+ }
167
+
168
+ /** 层的完整途经身份链(target + hops),供回环检测 */
169
+ export function layerPathIdentities(desc: RuntimeDescriptor): SshHop[] {
170
+ const hops: SshHop[] = Array.isArray(desc.config?.sshHops) ? (desc.config.sshHops as SshHop[]) : [];
171
+ const connParams = desc.config?.connParams as SshConnParams | undefined;
172
+ const root: SshHop = desc.target ? (connParams ? { host: desc.target, conn: connParams } : desc.target) : "";
173
+ return [root, ...hops].filter(Boolean);
174
+ }
175
+
176
+ /** 层显示名(root 含端口/用户 + 途经链 + 进入标签) */
177
+ export function layerDisplayName(desc: RuntimeDescriptor): string {
178
+ const hops = (Array.isArray(desc.config?.sshHops) ? (desc.config.sshHops as SshHop[]) : []).map((h) => hopLabel(h));
179
+ const connParams = desc.config?.connParams as SshConnParams | undefined;
180
+ const root = hopLabel(connParams ? { host: desc.target ?? "?", conn: connParams } : desc.target ?? "?");
181
+ const parts = [root, ...hops];
182
+ parts.push(...enterStepsOf(desc.config as Record<string, unknown> | undefined).map((s) => s.label ?? s.cmd.slice(0, 24)));
183
+ return parts.join(" → ");
184
+ }
185
+
186
+ /** 层显示标签(descriptor 链 + 容器短 ID 后缀) */
187
+ export function layerDisplayLabel(d: { descriptor: RuntimeDescriptor; containerId?: string }): string {
188
+ const base = layerDisplayName(d.descriptor);
189
+ return d.containerId ? `${base}(${d.containerId})` : base;
190
+ }
191
+
192
+ /**
193
+ * exec 通道(非 PTY)进入管线重放渲染(FEAT-03 层上下文拼装):
194
+ * 每步清洗 -it → -i(非交互)后要求以 shell 结尾(docker exec -it ctr bash 形态)——
195
+ * 渲染为 stdin 管道链:`… bash -s` 逐层读 stdin,命令文本从本地注入,零嵌套引用。
196
+ * 任一步非 shell 结尾形态(如裸 su -)→ 不可直达层(exec 通道拒绝,用 pty 通道)。
197
+ */
198
+ const SHELL_TAIL_RE = /(?:^|\s)(bash|sh|zsh|ash|dash)\s*$/;
199
+
200
+ export type ExecChannelRender =
201
+ | { kind: "ok"; remoteArgs: string[]; stdinCmd: string }
202
+ | { kind: "unreachable"; reason: string; step: string };
203
+
204
+ export function renderExecChannel(
205
+ steps: EnterStep[],
206
+ command: string,
207
+ opts: { cwd?: string; marker: string; nonceWrapped: string },
208
+ ): ExecChannelRender {
209
+ const remoteArgs: string[] = [];
210
+ for (const s of steps) {
211
+ // -it / -t → -i(非交互);进入命令须以 shell 结尾(内层读 stdin)
212
+ const cleaned = s.cmd.replace(/(^|\s)-(it|ti)\b/g, "$1-i").replace(/(^|\s)-t\b/g, "$1-i").trim();
213
+ const m = SHELL_TAIL_RE.exec(cleaned);
214
+ if (!m) {
215
+ return { kind: "unreachable", reason: "进入命令非「… shell」形态(无法非交互重放)", step: s.cmd };
216
+ }
217
+ remoteArgs.push(...cleaned.split(/\s+/), "-s"); // shell -s:从 stdin 读命令
218
+ }
219
+ // 终层远端 shell:POSIX sh 通吃(包装脚本纯 POSIX——报告 #3:FreeBSD/Alpine 无 bash
220
+ // 也能跑;bash 系统上 sh 通常为 bash/ash 链接,行为兼容)
221
+ remoteArgs.push("sh", "-s");
222
+ const cd = opts.cwd ? `cd ${JSON.stringify(opts.cwd)} 2>/dev/null || true; ` : "";
223
+ void command;
224
+ return { kind: "ok", remoteArgs, stdinCmd: `${cd}${opts.nonceWrapped}` };
225
+ }