fauxnix-cli 0.9.3 → 0.11.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.
@@ -0,0 +1,20 @@
1
+ export type DoctorOptions = {
2
+ home?: string;
3
+ cwd?: string;
4
+ env?: NodeJS.ProcessEnv;
5
+ nodeVersion?: string;
6
+ /** Injected MCP-module loader. Default: dynamic import of ./mcp.js (does not start the server). */
7
+ loadMcp?: () => Promise<unknown>;
8
+ };
9
+ export type DoctorReport = {
10
+ lines: string[];
11
+ ok: boolean;
12
+ };
13
+ export declare function collectDoctorReport(opts?: DoctorOptions): Promise<DoctorReport>;
14
+ export declare function claudeUserConfigPath(home: string, env: NodeJS.ProcessEnv): string;
15
+ export declare function codexConfigPath(home: string, env: NodeJS.ProcessEnv): string;
16
+ export declare function openCodeConfigPath(home: string, env: NodeJS.ProcessEnv): string;
17
+ export declare function hasCodexFauxnix(text: string): boolean;
18
+ export declare function hasOpenCodeFauxnix(data: unknown): boolean;
19
+ export declare function isServerMap(value: unknown): boolean;
20
+ export declare function serverMapHasFauxnix(value: unknown): boolean;
package/dist/doctor.js ADDED
@@ -0,0 +1,251 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import { homedir } from 'node:os';
3
+ import { join } from 'node:path';
4
+ const VALUE_INDENT = ' ';
5
+ export async function collectDoctorReport(opts = {}) {
6
+ const home = opts.home ?? homedir();
7
+ const cwd = opts.cwd ?? process.cwd();
8
+ const env = opts.env ?? process.env;
9
+ const nodeVersion = opts.nodeVersion ?? process.version;
10
+ const lines = [''];
11
+ lines.push(...encodingLines(env));
12
+ lines.push('');
13
+ lines.push(field('claude', detectClaude(home, cwd, env)));
14
+ lines.push(field('codex', detectCodex(home, env)));
15
+ lines.push(field('opencode', detectOpenCode(home, env)));
16
+ lines.push('');
17
+ const mcp = await mcpLines(nodeVersion, opts.loadMcp);
18
+ lines.push(...mcp.lines);
19
+ return { lines, ok: mcp.ok };
20
+ }
21
+ function field(label, value) {
22
+ return `${label.padEnd(10)} : ${value}`;
23
+ }
24
+ function encodingLines(env) {
25
+ const raw = env.FAUXNIX_NATIVE_ENCODING;
26
+ const current = raw === undefined || raw === ''
27
+ ? 'unset → utf8 (default)'
28
+ : raw === 'ansi'
29
+ ? 'ansi → GBK-native admin tools'
30
+ : `${raw} → utf8 (only ansi selects GBK)`;
31
+ return [
32
+ field('encoding', 'UTF-8 default for native-tool pipelines'),
33
+ VALUE_INDENT + `current FAUXNIX_NATIVE_ENCODING=${current}`,
34
+ VALUE_INDENT + 'set FAUXNIX_NATIVE_ENCODING=ansi for GBK-native admin tools (ipconfig, tasklist)',
35
+ ];
36
+ }
37
+ function detectClaude(home, cwd, env) {
38
+ const userPath = claudeUserConfigPath(home, env);
39
+ const projectPath = join(cwd, '.mcp.json');
40
+ const userExists = existsSync(userPath);
41
+ const projectExists = existsSync(projectPath);
42
+ let user;
43
+ let project;
44
+ if (userExists)
45
+ user = inspectClaudeJson(userPath);
46
+ if (projectExists) {
47
+ const inspected = inspectClaudeJson(projectPath);
48
+ // Project-scope Claude MCP is always a top-level mcpServers object.
49
+ if (!inspected.parseError && inspected.hasTopLevelMcpServers)
50
+ project = inspected;
51
+ }
52
+ if (!userExists && !project)
53
+ return 'not detected — see README';
54
+ if (user?.hasFauxnix)
55
+ return `fauxnix MCP configured (${userPath})`;
56
+ if (project?.hasFauxnix)
57
+ return `fauxnix MCP configured (${projectPath})`;
58
+ if (userExists && user?.parseError) {
59
+ return `found ${userPath} (unreadable JSON) — see README`;
60
+ }
61
+ if (userExists) {
62
+ return `found ${userPath}, fauxnix MCP not listed — run: claude mcp add fauxnix -- fauxnix mcp`;
63
+ }
64
+ return `found ${projectPath}, fauxnix MCP not listed — see README`;
65
+ }
66
+ export function claudeUserConfigPath(home, env) {
67
+ const dir = env.CLAUDE_CONFIG_DIR?.trim();
68
+ if (dir)
69
+ return join(dir, '.claude.json');
70
+ return join(home, '.claude.json');
71
+ }
72
+ export function codexConfigPath(home, env) {
73
+ const codexHome = env.CODEX_HOME?.trim() || join(home, '.codex');
74
+ return join(codexHome, 'config.toml');
75
+ }
76
+ export function openCodeConfigPath(home, env) {
77
+ const xdg = env.XDG_CONFIG_HOME?.trim() || join(home, '.config');
78
+ return join(xdg, 'opencode', 'opencode.json');
79
+ }
80
+ function inspectClaudeJson(path) {
81
+ const text = readText(path);
82
+ if (text === undefined) {
83
+ return { parseError: true, hasTopLevelMcpServers: false, hasFauxnix: false };
84
+ }
85
+ let data;
86
+ try {
87
+ data = JSON.parse(stripBom(text));
88
+ }
89
+ catch {
90
+ return { parseError: true, hasTopLevelMcpServers: false, hasFauxnix: false };
91
+ }
92
+ if (!data || typeof data !== 'object' || Array.isArray(data)) {
93
+ return { parseError: true, hasTopLevelMcpServers: false, hasFauxnix: false };
94
+ }
95
+ const rec = data;
96
+ const hasTopLevelMcpServers = isServerMap(rec.mcpServers);
97
+ let hasFauxnix = hasTopLevelMcpServers && serverMapHasFauxnix(rec.mcpServers);
98
+ const projects = rec.projects;
99
+ if (projects && typeof projects === 'object' && !Array.isArray(projects)) {
100
+ for (const proj of Object.values(projects)) {
101
+ if (!proj || typeof proj !== 'object' || Array.isArray(proj))
102
+ continue;
103
+ const servers = proj.mcpServers;
104
+ if (isServerMap(servers) && serverMapHasFauxnix(servers))
105
+ hasFauxnix = true;
106
+ }
107
+ }
108
+ return { parseError: false, hasTopLevelMcpServers, hasFauxnix };
109
+ }
110
+ function detectCodex(home, env) {
111
+ const path = codexConfigPath(home, env);
112
+ if (!existsSync(path))
113
+ return 'not detected — see README';
114
+ const text = readText(path);
115
+ if (text === undefined)
116
+ return `found ${path} (unreadable) — see README`;
117
+ if (hasCodexFauxnix(stripBom(text)))
118
+ return `fauxnix MCP configured (${path})`;
119
+ return `found ${path}, fauxnix MCP not listed — run: codex mcp add fauxnix -- fauxnix mcp`;
120
+ }
121
+ export function hasCodexFauxnix(text) {
122
+ if (/^\s*\[mcp_servers\.(?:fauxnix|"fauxnix"|'fauxnix')\]/im.test(text))
123
+ return true;
124
+ const tables = text.split(/^\s*\[/m);
125
+ for (const table of tables) {
126
+ if (!/^mcp_servers\./i.test(table))
127
+ continue;
128
+ const header = (table.split(/[\]\r\n]/, 1)[0] ?? '').trim();
129
+ if (/^mcp_servers\.(?:fauxnix|"fauxnix"|'fauxnix')$/i.test(header))
130
+ return true;
131
+ const cmd = /^\s*command\s*=\s*(?:"([^"]*)"|'([^']*)')/im.exec(table);
132
+ const command = cmd?.[1] ?? cmd?.[2];
133
+ if (command && isFauxnixExecutable(command))
134
+ return true;
135
+ const args = /^\s*args\s*=\s*\[([^\]]*)\]/im.exec(table);
136
+ if (args) {
137
+ const items = [...args[1].matchAll(/"([^"]*)"|'([^']*)'/g)].map((m) => m[1] ?? m[2] ?? '');
138
+ if (items.some(isFauxnixExecutable))
139
+ return true;
140
+ }
141
+ }
142
+ return false;
143
+ }
144
+ function detectOpenCode(home, env) {
145
+ const path = openCodeConfigPath(home, env);
146
+ if (!existsSync(path))
147
+ return 'not detected — see README';
148
+ const text = readText(path);
149
+ if (text === undefined)
150
+ return `found ${path} (unreadable) — see README`;
151
+ let data;
152
+ try {
153
+ data = JSON.parse(stripBom(text));
154
+ }
155
+ catch {
156
+ return `found ${path} (unreadable JSON) — see README`;
157
+ }
158
+ if (hasOpenCodeFauxnix(data))
159
+ return `fauxnix MCP configured (${path})`;
160
+ return `found ${path}, fauxnix MCP not listed — add mcp.fauxnix (see README)`;
161
+ }
162
+ export function hasOpenCodeFauxnix(data) {
163
+ if (!data || typeof data !== 'object' || Array.isArray(data))
164
+ return false;
165
+ const mcp = data.mcp;
166
+ if (!isServerMap(mcp))
167
+ return false;
168
+ if (serverMapHasFauxnix(mcp))
169
+ return true;
170
+ const nested = mcp.servers;
171
+ return isServerMap(nested) && serverMapHasFauxnix(nested);
172
+ }
173
+ export function isServerMap(value) {
174
+ return !!value && typeof value === 'object' && !Array.isArray(value);
175
+ }
176
+ export function serverMapHasFauxnix(value) {
177
+ if (!isServerMap(value))
178
+ return false;
179
+ for (const [name, cfg] of Object.entries(value)) {
180
+ if (name === 'servers')
181
+ continue;
182
+ if (looksLikeFauxnixServer(name, cfg))
183
+ return true;
184
+ }
185
+ return false;
186
+ }
187
+ function looksLikeFauxnixServer(name, cfg) {
188
+ if (/^fauxnix(-cli)?$/i.test(name))
189
+ return true;
190
+ if (!cfg || typeof cfg !== 'object' || Array.isArray(cfg))
191
+ return false;
192
+ const rec = cfg;
193
+ const chunks = [];
194
+ if (typeof rec.command === 'string')
195
+ chunks.push(rec.command);
196
+ if (Array.isArray(rec.command)) {
197
+ for (const part of rec.command)
198
+ if (typeof part === 'string')
199
+ chunks.push(part);
200
+ }
201
+ if (Array.isArray(rec.args)) {
202
+ for (const part of rec.args)
203
+ if (typeof part === 'string')
204
+ chunks.push(part);
205
+ }
206
+ return chunks.some(isFauxnixExecutable);
207
+ }
208
+ function isFauxnixExecutable(s) {
209
+ const base = s.replace(/\\/g, '/').split('/').pop()?.trim() ?? '';
210
+ return /^fauxnix(-cli)?(\.cmd|\.exe)?$/i.test(base);
211
+ }
212
+ async function mcpLines(nodeVersion, loadMcp) {
213
+ const major = nodeMajor(nodeVersion);
214
+ const nodeOk = major >= 18;
215
+ let moduleOk = false;
216
+ let moduleDetail = '';
217
+ try {
218
+ const mod = await (loadMcp ?? defaultLoadMcp)();
219
+ moduleOk =
220
+ !!mod && typeof mod.startMcpServer === 'function';
221
+ if (!moduleOk)
222
+ moduleDetail = 'startMcpServer export missing';
223
+ }
224
+ catch (e) {
225
+ moduleDetail = e instanceof Error ? e.message : String(e);
226
+ }
227
+ const lines = [
228
+ field('node', `${nodeVersion.startsWith('v') ? nodeVersion : 'v' + nodeVersion}${nodeOk ? ' (>=18 required)' : ' FAILED (requires >=18)'}`),
229
+ field('mcp', moduleOk ? 'module loads' : `FAILED to load${moduleDetail ? ': ' + moduleDetail : ''}`),
230
+ VALUE_INDENT + 'start with: fauxnix mcp',
231
+ ];
232
+ return { lines, ok: nodeOk && moduleOk };
233
+ }
234
+ async function defaultLoadMcp() {
235
+ return import('./mcp.js');
236
+ }
237
+ function nodeMajor(version) {
238
+ const m = /^v?(\d+)/.exec(version);
239
+ return m ? Number(m[1]) : 0;
240
+ }
241
+ function stripBom(text) {
242
+ return text.charCodeAt(0) === 0xfeff ? text.slice(1) : text;
243
+ }
244
+ function readText(path) {
245
+ try {
246
+ return readFileSync(path, 'utf8');
247
+ }
248
+ catch {
249
+ return undefined;
250
+ }
251
+ }
@@ -0,0 +1,15 @@
1
+ export type InstallOptions = {
2
+ home?: string;
3
+ /** Accepted for parity with collectDoctorReport; install writes user-level configs only. */
4
+ cwd?: string;
5
+ env?: NodeJS.ProcessEnv;
6
+ };
7
+ export type InstallReport = {
8
+ lines: string[];
9
+ ok: boolean;
10
+ };
11
+ export declare const INSTALL_FLAGS: readonly ["claude", "codex", "opencode", "kimi", "qwen"];
12
+ export type HarnessName = (typeof INSTALL_FLAGS)[number];
13
+ export declare function kimiConfigPath(home: string, env: NodeJS.ProcessEnv): string;
14
+ export declare function qwenConfigPath(home: string, env: NodeJS.ProcessEnv): string;
15
+ export declare function runInstall(argv: string[], opts?: InstallOptions): InstallReport;
@@ -0,0 +1,206 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
2
+ import { homedir } from 'node:os';
3
+ import { dirname, join } from 'node:path';
4
+ import { claudeUserConfigPath, codexConfigPath, hasCodexFauxnix, hasOpenCodeFauxnix, isServerMap, openCodeConfigPath, serverMapHasFauxnix, } from './doctor.js';
5
+ export const INSTALL_FLAGS = ['claude', 'codex', 'opencode', 'kimi', 'qwen'];
6
+ const STDIO = { command: 'fauxnix', args: ['mcp'] };
7
+ const OPENCODE_STDIO = { type: 'local', command: ['fauxnix', 'mcp'] };
8
+ export function kimiConfigPath(home, env) {
9
+ const root = env.KIMI_CODE_HOME?.trim() || join(home, '.kimi-code');
10
+ return join(root, 'mcp.json');
11
+ }
12
+ export function qwenConfigPath(home, env) {
13
+ return join(home, '.qwen', 'settings.json');
14
+ }
15
+ export function runInstall(argv, opts = {}) {
16
+ const parsed = parseHarnessFlags(argv);
17
+ if (parsed.help)
18
+ return { lines: installUsageLines(), ok: true };
19
+ if (parsed.error)
20
+ return { lines: [parsed.error, ...installUsageLines()], ok: false };
21
+ const ctx = {
22
+ home: opts.home ?? homedir(),
23
+ env: opts.env ?? process.env,
24
+ };
25
+ const lines = [];
26
+ let ok = true;
27
+ for (const name of parsed.harnesses) {
28
+ const one = installHarness(name, ctx);
29
+ lines.push(one.line);
30
+ if (!one.ok)
31
+ ok = false;
32
+ }
33
+ return { lines, ok };
34
+ }
35
+ function parseHarnessFlags(argv) {
36
+ if (argv.some((a) => a === '--help' || a === '-h'))
37
+ return { help: true, harnesses: [] };
38
+ if (argv.length === 0) {
39
+ return { error: 'select a harness: --claude --codex --opencode --kimi --qwen', harnesses: [] };
40
+ }
41
+ const harnesses = [];
42
+ const seen = new Set();
43
+ for (const a of argv) {
44
+ if (!a.startsWith('--') || a === '--') {
45
+ return { error: `unknown argument: ${a}`, harnesses: [] };
46
+ }
47
+ const name = a.slice(2);
48
+ if (!isHarness(name))
49
+ return { error: `unknown harness: ${a}`, harnesses: [] };
50
+ if (!seen.has(name)) {
51
+ seen.add(name);
52
+ harnesses.push(name);
53
+ }
54
+ }
55
+ return { harnesses };
56
+ }
57
+ function isHarness(s) {
58
+ return INSTALL_FLAGS.includes(s);
59
+ }
60
+ function installUsageLines() {
61
+ return ['Usage:', ' fauxnix install --claude|--codex|--opencode|--kimi|--qwen'];
62
+ }
63
+ function installHarness(name, ctx) {
64
+ switch (name) {
65
+ case 'claude':
66
+ return patchMcpServers(claudeUserConfigPath(ctx.home, ctx.env), 'claude');
67
+ case 'codex':
68
+ return patchCodex(codexConfigPath(ctx.home, ctx.env));
69
+ case 'opencode':
70
+ return patchOpenCode(openCodeConfigPath(ctx.home, ctx.env));
71
+ case 'kimi':
72
+ return patchMcpServers(kimiConfigPath(ctx.home, ctx.env), 'kimi');
73
+ case 'qwen':
74
+ return patchMcpServers(qwenConfigPath(ctx.home, ctx.env), 'qwen');
75
+ }
76
+ }
77
+ function patchMcpServers(path, harness) {
78
+ const read = readJsonObject(path);
79
+ if (read.state === 'invalid') {
80
+ return { ok: false, line: `${harness}: ${path} is ${read.reason} — not modified` };
81
+ }
82
+ const existed = read.state !== 'missing';
83
+ const data = read.state === 'ok' ? read.data : {};
84
+ if (serverMapHasFauxnix(data.mcpServers)) {
85
+ return { ok: true, line: `${harness}: already configured (${path})` };
86
+ }
87
+ if (data.mcpServers != null && !isServerMap(data.mcpServers)) {
88
+ return { ok: false, line: `${harness}: ${path} mcpServers is not an object — not modified` };
89
+ }
90
+ if (!isServerMap(data.mcpServers))
91
+ data.mcpServers = {};
92
+ data.mcpServers.fauxnix = {
93
+ command: STDIO.command,
94
+ args: [...STDIO.args],
95
+ };
96
+ return writeJson(path, data, harness, existed, 'added mcpServers.fauxnix');
97
+ }
98
+ function patchOpenCode(path) {
99
+ const read = readJsonObject(path);
100
+ if (read.state === 'invalid') {
101
+ return { ok: false, line: `opencode: ${path} is ${read.reason} — not modified` };
102
+ }
103
+ const existed = read.state !== 'missing';
104
+ const data = read.state === 'ok' ? read.data : {};
105
+ if (hasOpenCodeFauxnix(data)) {
106
+ return { ok: true, line: `opencode: already configured (${path})` };
107
+ }
108
+ if (data.mcp != null && !isServerMap(data.mcp)) {
109
+ return { ok: false, line: `opencode: ${path} mcp is not an object — not modified` };
110
+ }
111
+ if (!isServerMap(data.mcp))
112
+ data.mcp = {};
113
+ const mcp = data.mcp;
114
+ const payload = { type: OPENCODE_STDIO.type, command: [...OPENCODE_STDIO.command] };
115
+ if (isServerMap(mcp.servers)) {
116
+ mcp.servers.fauxnix = payload;
117
+ return writeJson(path, data, 'opencode', existed, 'added mcp.servers.fauxnix');
118
+ }
119
+ mcp.fauxnix = payload;
120
+ return writeJson(path, data, 'opencode', existed, 'added mcp.fauxnix');
121
+ }
122
+ function patchCodex(path) {
123
+ const existed = existsSync(path);
124
+ if (!existed) {
125
+ const written = writeText(path, tomlTable('\n'));
126
+ if (!written.ok)
127
+ return { ok: false, line: `codex: failed to write ${path}: ${written.error}` };
128
+ return { ok: true, line: `codex: created ${path}` };
129
+ }
130
+ const text = readText(path);
131
+ if (text === undefined) {
132
+ return { ok: false, line: `codex: ${path} is unreadable — not modified` };
133
+ }
134
+ if (hasCodexFauxnix(stripBom(text))) {
135
+ return { ok: true, line: `codex: already configured (${path})` };
136
+ }
137
+ const nl = text.includes('\r\n') ? '\r\n' : '\n';
138
+ if (stripBom(text).trim() === '') {
139
+ const written = writeText(path, tomlTable(nl));
140
+ if (!written.ok)
141
+ return { ok: false, line: `codex: failed to write ${path}: ${written.error}` };
142
+ return { ok: true, line: `codex: patched ${path} (appended [mcp_servers.fauxnix])` };
143
+ }
144
+ let body = text;
145
+ if (!body.endsWith('\n'))
146
+ body += nl;
147
+ if (!body.endsWith(nl + nl))
148
+ body += nl;
149
+ const written = writeText(path, body + tomlTable(nl));
150
+ if (!written.ok)
151
+ return { ok: false, line: `codex: failed to write ${path}: ${written.error}` };
152
+ return { ok: true, line: `codex: patched ${path} (appended [mcp_servers.fauxnix])` };
153
+ }
154
+ function tomlTable(nl) {
155
+ return `[mcp_servers.fauxnix]${nl}command = "fauxnix"${nl}args = ["mcp"]${nl}`;
156
+ }
157
+ function writeJson(path, data, harness, existed, change) {
158
+ const written = writeText(path, JSON.stringify(data, null, 2) + '\n');
159
+ if (!written.ok) {
160
+ return { ok: false, line: `${harness}: failed to write ${path}: ${written.error}` };
161
+ }
162
+ if (existed)
163
+ return { ok: true, line: `${harness}: patched ${path} (${change})` };
164
+ return { ok: true, line: `${harness}: created ${path}` };
165
+ }
166
+ function writeText(path, contents) {
167
+ try {
168
+ mkdirSync(dirname(path), { recursive: true });
169
+ writeFileSync(path, contents, 'utf8');
170
+ return { ok: true };
171
+ }
172
+ catch (e) {
173
+ return { ok: false, error: e instanceof Error ? e.message : String(e) };
174
+ }
175
+ }
176
+ function readJsonObject(path) {
177
+ if (!existsSync(path))
178
+ return { state: 'missing' };
179
+ const text = readText(path);
180
+ if (text === undefined)
181
+ return { state: 'invalid', reason: 'unreadable' };
182
+ const stripped = stripBom(text).trim();
183
+ if (stripped === '')
184
+ return { state: 'empty' };
185
+ try {
186
+ const data = JSON.parse(stripped);
187
+ if (!data || typeof data !== 'object' || Array.isArray(data)) {
188
+ return { state: 'invalid', reason: 'not a JSON object' };
189
+ }
190
+ return { state: 'ok', data: data };
191
+ }
192
+ catch {
193
+ return { state: 'invalid', reason: 'not valid JSON' };
194
+ }
195
+ }
196
+ function stripBom(text) {
197
+ return text.charCodeAt(0) === 0xfeff ? text.slice(1) : text;
198
+ }
199
+ function readText(path) {
200
+ try {
201
+ return readFileSync(path, 'utf8');
202
+ }
203
+ catch {
204
+ return undefined;
205
+ }
206
+ }
package/dist/mcp.d.ts CHANGED
@@ -19,5 +19,11 @@ export declare function bashToolResult(r: ExecResult, sessionId: string, infra:
19
19
  sessionId: string;
20
20
  };
21
21
  };
22
+ export declare function positionalCountFromEnv(env: Record<string, string>): number;
23
+ export declare function formatSessionStatus(session: {
24
+ cwd: string | null;
25
+ env: Record<string, string>;
26
+ id: string;
27
+ }): string;
22
28
  export declare function startMcpServer(): Promise<void>;
23
29
  export { translatePipelineBody };
package/dist/mcp.js CHANGED
@@ -31,11 +31,10 @@ const TOOL_DESCRIPTION = `Execute a Linux/bash-style command on this Windows mac
31
31
  Commands are deterministically translated to PowerShell and executed natively — no WSL or VM.
32
32
  Output is formatted to look like GNU/Linux tooling (ls -l, ps aux, df -h ...), errors look like bash errors, and text encoding (UTF-8/GBK) is handled automatically.
33
33
 
34
- Supported: pipes (|), && / || / ;, redirections (> >> 2> 2>&1 < /dev/null), variables ($VAR $HOME ~), command substitution $(...), and ${registeredNames().length}+ coreutils-style commands (${registeredNames().slice(0, 18).join(', ')}...).
34
+ Supported: pipes (|), && / || / ;, redirections (> >> 2> 2>&1 < /dev/null), variables ($VAR $HOME $1 $# "$@" ~), set -- / shift, array assignment A=(x y z), \${name[n]} \${#name[@]} \${name//pat/str} \${name:off:len}, command substitution $(...), and ${registeredNames().length}+ coreutils-style commands (${registeredNames().slice(0, 18).join(', ')}...).
35
35
  Unknown commands (git, node, npm, python, cargo...) are passed through and executed natively with argv-style quoting.
36
- Not supported: heredocs, while/until/case, env -i/--ignore-environment, background jobs. if/then/elif/else/fi, for-in loops, and word-level \$((...)) arithmetic expansion are supported.
37
-
38
- CWD, environment variables, export/unset and cd persist across calls within this session — a resident PowerShell 5.1 host is started when the MCP session begins (and after reset), so the first bash tool call is already warm.
36
+ Not supported: heredocs, env -i/--ignore-environment, background jobs. if/then/elif/else/fi, for-in loops, while/until, case ... esac, and word-level \$((...)) arithmetic expansion are supported.
37
+ CWD, environment variables, export/unset, cd, and positional parameters (set -- / $1 / "$@") persist across calls within this session — a resident PowerShell 5.1 host is started when the MCP session begins (and after reset), so the first bash tool call is already warm.
39
38
  Exit codes follow bash conventions (0 ok, 1 fail, 2 usage/serious, 127 command not found, 124 timeout, 130 cancelled). The tool also returns structuredContent (schemaVersion 1) with stdout/stderr/exitCode/timedOut/cancelled/truncated/sessionId.
40
39
 
41
40
  Platform requirement: the execution backend is native Windows PowerShell 5.1+. On hosts without PowerShell on PATH (e.g. Linux containers/sandboxes), the bash tool returns exit code 127 with an actionable error instead of running the command.`;
@@ -70,7 +69,35 @@ export function bashToolResult(r, sessionId, infra) {
70
69
  ...(infra ? { isError: true } : {}),
71
70
  };
72
71
  }
72
+ /** Packed FAUXNIX_POS uses char-30 separators (same as array sidecar). */
73
+ const POS_SEP = '\x1e';
74
+ export function positionalCountFromEnv(env) {
75
+ let packed;
76
+ for (const [k, v] of Object.entries(env)) {
77
+ if (k.toUpperCase() === 'FAUXNIX_POS') {
78
+ packed = v;
79
+ break;
80
+ }
81
+ }
82
+ if (packed == null || packed === '')
83
+ return 0;
84
+ return packed.split(POS_SEP).length;
85
+ }
86
+ export function formatSessionStatus(session) {
87
+ const envKeys = Object.keys(session.env).sort();
88
+ return ('cwd: ' +
89
+ (session.cwd ?? '(inherit from server start)') +
90
+ '\nenv keys: ' +
91
+ (envKeys.length ? envKeys.join(', ') : '(none tracked)') +
92
+ '\npositionals: ' +
93
+ positionalCountFromEnv(session.env) +
94
+ '\nsession: ' +
95
+ session.id +
96
+ '\ncommands registered: ' +
97
+ registeredNames().length);
98
+ }
73
99
  export async function startMcpServer() {
100
+ process.env.FAUXNIX_ARG0 = TOOL_NAME;
74
101
  const server = new McpServer({ name: 'fauxnix', version: packageVersion }, { capabilities: { tools: {} } });
75
102
  const session = new FauxnixSession();
76
103
  await session.prewarm();
@@ -117,22 +144,17 @@ export async function startMcpServer() {
117
144
  return { content: [{ type: 'text', text: msg }], isError: true };
118
145
  }
119
146
  });
120
- server.tool('fauxnix_session', 'Inspect or reset the persistent fauxnix shell session (current directory, environment, session id). Actions: "status" (default) or "reset".', {
147
+ server.tool('fauxnix_session', 'Inspect or reset the persistent fauxnix shell session (current directory, environment, positional count, session id). Actions: "status" (default) or "reset".', {
121
148
  action: z
122
149
  .enum(['status', 'reset'])
123
150
  .default('status')
124
- .describe('"status" shows the session state (cwd, tracked env keys); "reset" clears it back to a fresh shell'),
151
+ .describe('"status" shows the session state (cwd, tracked env keys, positional count); "reset" clears it back to a fresh shell'),
125
152
  }, SESSION_ANNOTATIONS, async ({ action }) => {
126
153
  if (action === 'reset') {
127
154
  await session.reset();
128
155
  return { content: [{ type: 'text', text: 'fauxnix: session reset' }] };
129
156
  }
130
- const envKeys = Object.keys(session.env).sort();
131
- const text = 'cwd: ' + (session.cwd ?? '(inherit from server start)') +
132
- '\nenv keys: ' + (envKeys.length ? envKeys.join(', ') : '(none tracked)') +
133
- '\nsession: ' + session.id +
134
- '\ncommands registered: ' + registeredNames().length;
135
- return { content: [{ type: 'text', text }] };
157
+ return { content: [{ type: 'text', text: formatSessionStatus(session) }] };
136
158
  });
137
159
  const transport = new StdioServerTransport();
138
160
  let shuttingDown = false;