office-core 0.1.4 → 0.2.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,199 @@
1
+ import { appendFile, cp, mkdir, mkdtemp, readFile, readdir, rm, stat, writeFile } from 'node:fs/promises';
2
+ import { existsSync } from 'node:fs';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+ import process from 'node:process';
6
+ import { spawn, spawnSync } from 'node:child_process';
7
+ export function parseCliArgs(argv, options = {}) {
8
+ const bools = new Set(options.boolean ?? []);
9
+ const values = new Set(options.value ?? []);
10
+ const aliases = options.aliases ?? {};
11
+ const out = {};
12
+ for (let i = 0; i < argv.length; i += 1) {
13
+ const item = argv[i];
14
+ if (!item.startsWith('--')) {
15
+ throw new Error(`Unexpected positional argument: ${item}`);
16
+ }
17
+ const rawKey = item.slice(2);
18
+ const key = aliases[rawKey] ?? rawKey;
19
+ if (!bools.has(key) && !values.has(key)) {
20
+ throw new Error(`Unknown option: --${rawKey}`);
21
+ }
22
+ if (bools.has(key)) {
23
+ const next = argv[i + 1];
24
+ if (next && !next.startsWith('--')) {
25
+ out[key] = next;
26
+ i += 1;
27
+ }
28
+ else {
29
+ out[key] = 'true';
30
+ }
31
+ continue;
32
+ }
33
+ const next = argv[i + 1];
34
+ if (!next || next.startsWith('--')) {
35
+ throw new Error(`Missing value for --${rawKey}`);
36
+ }
37
+ out[key] = next;
38
+ i += 1;
39
+ }
40
+ return out;
41
+ }
42
+ export function parseBooleanLike(value, fallback) {
43
+ if (value == null || value === '') {
44
+ return fallback;
45
+ }
46
+ const normalized = value.trim().toLowerCase();
47
+ if (['1', 'true', 'yes', 'y', 'on'].includes(normalized)) {
48
+ return true;
49
+ }
50
+ if (['0', 'false', 'no', 'n', 'off'].includes(normalized)) {
51
+ return false;
52
+ }
53
+ throw new Error(`Invalid boolean value: ${value}`);
54
+ }
55
+ export function parseNumberLike(value, fallback, label = 'number') {
56
+ if (value == null || value === '') {
57
+ return fallback;
58
+ }
59
+ const parsed = Number(value);
60
+ if (!Number.isFinite(parsed)) {
61
+ throw new Error(`Invalid ${label}: ${value}`);
62
+ }
63
+ return parsed;
64
+ }
65
+ export async function openExternal(target) {
66
+ const command = process.platform === 'win32'
67
+ ? 'cmd.exe'
68
+ : process.platform === 'darwin'
69
+ ? 'open'
70
+ : 'xdg-open';
71
+ const args = process.platform === 'win32' ? ['/c', 'start', '', target] : [target];
72
+ await new Promise((resolve, reject) => {
73
+ const child = spawn(command, args, {
74
+ detached: true,
75
+ stdio: 'ignore',
76
+ windowsHide: true,
77
+ });
78
+ child.on('error', reject);
79
+ child.unref();
80
+ resolve();
81
+ });
82
+ }
83
+ export function escapeBatch(value) {
84
+ return value.replace(/"/g, '""');
85
+ }
86
+ export function buildStartupLauncher(rootDir) {
87
+ const runtimeEntry = path.join(rootDir, 'bin', 'office-core.mjs');
88
+ return [
89
+ '@echo off',
90
+ 'setlocal',
91
+ `cd /d "${escapeBatch(rootDir)}"`,
92
+ 'where node >nul 2>nul',
93
+ 'if errorlevel 1 (',
94
+ ' echo Node.js was not found on PATH.',
95
+ ' exit /b 1',
96
+ ')',
97
+ `start "office core" cmd /c "cd /d ""${escapeBatch(rootDir)}"" && node ""${escapeBatch(runtimeEntry)}"" start"`,
98
+ 'endlocal',
99
+ '',
100
+ ].join('\r\n');
101
+ }
102
+ export function buildDesktopLauncher(rootDir) {
103
+ const runtimeEntry = path.join(rootDir, 'bin', 'office-core.mjs');
104
+ return [
105
+ '@echo off',
106
+ 'setlocal',
107
+ `cd /d "${escapeBatch(rootDir)}"`,
108
+ `node "${escapeBatch(runtimeEntry)}"`,
109
+ 'if errorlevel 1 pause',
110
+ 'endlocal',
111
+ '',
112
+ ].join('\r\n');
113
+ }
114
+ export function buildDashboardLauncher(rootDir) {
115
+ const runtimeEntry = path.join(rootDir, 'bin', 'office-core.mjs');
116
+ return [
117
+ '@echo off',
118
+ 'setlocal',
119
+ `cd /d "${escapeBatch(rootDir)}"`,
120
+ `node "${escapeBatch(runtimeEntry)}" open`,
121
+ 'if errorlevel 1 pause',
122
+ 'endlocal',
123
+ '',
124
+ ].join('\r\n');
125
+ }
126
+ export async function appendLogLine(logPath, line) {
127
+ await mkdir(path.dirname(logPath), { recursive: true });
128
+ await appendFile(logPath, `${new Date().toISOString()} ${line}\n`, 'utf8');
129
+ }
130
+ export async function readRecentLines(logPath, maxLines = 120) {
131
+ try {
132
+ const raw = await readFile(logPath, 'utf8');
133
+ return raw.split(/\r?\n/).filter(Boolean).slice(-maxLines);
134
+ }
135
+ catch {
136
+ return [];
137
+ }
138
+ }
139
+ export async function createSupportBundle(params) {
140
+ const stamp = new Date().toISOString().replace(/[:.]/g, '-');
141
+ const baseName = `${params.bundleNamePrefix}-${stamp}`;
142
+ const stagingRoot = await mkdtemp(path.join(os.tmpdir(), `${params.bundleNamePrefix}-`));
143
+ const bundleDir = path.join(stagingRoot, baseName);
144
+ await mkdir(bundleDir, { recursive: true });
145
+ await writeFile(path.join(bundleDir, 'bundle-metadata.json'), JSON.stringify(params.metadata, null, 2), 'utf8');
146
+ for (const entry of params.includePaths) {
147
+ if (!existsSync(entry.source)) {
148
+ continue;
149
+ }
150
+ const destination = path.join(bundleDir, entry.target ?? path.basename(entry.source));
151
+ await mkdir(path.dirname(destination), { recursive: true });
152
+ await cp(entry.source, destination, { recursive: true, force: true });
153
+ }
154
+ await mkdir(params.outputRoot, { recursive: true });
155
+ if (process.platform === 'win32') {
156
+ const zipPath = path.join(params.outputRoot, `${baseName}.zip`);
157
+ const compress = spawnSync('powershell.exe', [
158
+ '-NoProfile',
159
+ '-ExecutionPolicy',
160
+ 'Bypass',
161
+ '-Command',
162
+ `Compress-Archive -LiteralPath '${bundleDir.replace(/'/g, "''")}' -DestinationPath '${zipPath.replace(/'/g, "''")}' -Force`,
163
+ ], { encoding: 'utf8', windowsHide: true });
164
+ if (compress.status === 0 && existsSync(zipPath)) {
165
+ await rm(stagingRoot, { recursive: true, force: true });
166
+ return { outputPath: zipPath, mode: 'zip' };
167
+ }
168
+ }
169
+ else {
170
+ const zipPath = path.join(params.outputRoot, `${baseName}.zip`);
171
+ const zip = spawnSync('zip', ['-qr', zipPath, baseName], {
172
+ cwd: stagingRoot,
173
+ encoding: 'utf8',
174
+ windowsHide: true,
175
+ });
176
+ if (zip.status === 0 && existsSync(zipPath)) {
177
+ await rm(stagingRoot, { recursive: true, force: true });
178
+ return { outputPath: zipPath, mode: 'zip' };
179
+ }
180
+ }
181
+ const fallbackDir = path.join(params.outputRoot, baseName);
182
+ await rm(fallbackDir, { recursive: true, force: true }).catch(() => undefined);
183
+ await cp(bundleDir, fallbackDir, { recursive: true, force: true });
184
+ await rm(stagingRoot, { recursive: true, force: true });
185
+ return { outputPath: fallbackDir, mode: 'folder' };
186
+ }
187
+ export async function describePathState(targetPath) {
188
+ try {
189
+ const info = await stat(targetPath);
190
+ if (info.isDirectory()) {
191
+ const items = await readdir(targetPath);
192
+ return `${targetPath} (${items.length} items)`;
193
+ }
194
+ return `${targetPath} (${info.size} bytes)`;
195
+ }
196
+ catch {
197
+ return `${targetPath} (missing)`;
198
+ }
199
+ }
@@ -1,16 +1,29 @@
1
- import { existsSync } from "node:fs";
2
- import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
3
- import os from "node:os";
4
- import path from "node:path";
1
+ import { existsSync } from 'node:fs';
2
+ import { appendFile, mkdir, readFile, rename, writeFile } from 'node:fs/promises';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+ import process from 'node:process';
5
6
  const HOST_CONFIG_VERSION = 1;
6
- const HOST_CONFIG_DIRNAME = "office-host";
7
- const LEGACY_HOST_CONFIG_DIRNAME = "double-penetration";
8
- const HOST_CONFIG_FILENAME = "host-config.json";
9
- const HOST_SESSIONS_FILENAME = "host-sessions.json";
10
- const DEFAULT_LOCAL_HOST_TOKEN = "office-host-local-token";
7
+ const HOST_CONFIG_DIRNAME = 'office-host';
8
+ const LEGACY_HOST_CONFIG_DIRNAME = 'double-penetration';
9
+ const HOST_CONFIG_FILENAME = 'host-config.json';
10
+ const HOST_SESSIONS_FILENAME = 'host-sessions.json';
11
+ const DEFAULT_LOCAL_HOST_TOKEN = 'office-host-local-token';
11
12
  export function getHostConfigDir() {
12
13
  return path.join(baseAppDataDir(), HOST_CONFIG_DIRNAME);
13
14
  }
15
+ export function getHostLogsDir() {
16
+ return path.join(getHostConfigDir(), 'logs');
17
+ }
18
+ export function getHostDiagnosticsDir() {
19
+ return path.join(getHostConfigDir(), 'diagnostics');
20
+ }
21
+ export function getSupportBundlesDir() {
22
+ return path.join(getHostDiagnosticsDir(), 'support-bundles');
23
+ }
24
+ export function getHostLogPath() {
25
+ return path.join(getHostLogsDir(), 'office-host.log');
26
+ }
14
27
  export function getHostConfigPath() {
15
28
  return path.join(getHostConfigDir(), HOST_CONFIG_FILENAME);
16
29
  }
@@ -20,6 +33,10 @@ export function getHostSessionsPath() {
20
33
  export function getLegacyHostConfigDir() {
21
34
  return path.join(baseAppDataDir(), LEGACY_HOST_CONFIG_DIRNAME);
22
35
  }
36
+ export async function appendHostLog(message) {
37
+ await mkdir(getHostLogsDir(), { recursive: true });
38
+ await appendFile(getHostLogPath(), `${new Date().toISOString()} ${message}\n`, 'utf8');
39
+ }
23
40
  export async function loadHostConfig() {
24
41
  const filePath = firstExistingPath([
25
42
  path.join(getHostConfigDir(), HOST_CONFIG_FILENAME),
@@ -28,18 +45,19 @@ export async function loadHostConfig() {
28
45
  if (!filePath) {
29
46
  return null;
30
47
  }
31
- let parsed;
48
+ let raw = '';
32
49
  try {
33
- const raw = await readFile(filePath, "utf8");
34
- parsed = parseLooseJson(raw);
35
- }
36
- catch {
37
- return null;
50
+ raw = await readFile(filePath, 'utf8');
51
+ const parsed = parseLooseJson(raw);
52
+ if (!parsed || parsed.version !== HOST_CONFIG_VERSION) {
53
+ return null;
54
+ }
55
+ return normalizeConfig(parsed);
38
56
  }
39
- if (!parsed || parsed.version !== HOST_CONFIG_VERSION) {
57
+ catch (error) {
58
+ await quarantineCorruptFile(filePath, raw, error);
40
59
  return null;
41
60
  }
42
- return normalizeConfig(parsed);
43
61
  }
44
62
  export async function upsertHostConfig(overrides) {
45
63
  const existing = await loadHostConfig();
@@ -50,8 +68,8 @@ export async function upsertHostConfig(overrides) {
50
68
  host_id: overrides.host_id ?? existing?.host_id ?? `host_${sanitize(machineName)}_${crypto.randomUUID().slice(0, 8)}`,
51
69
  display_name: overrides.display_name ?? existing?.display_name ?? `${machineName} host`,
52
70
  machine_name: machineName,
53
- base_url: overrides.base_url ?? existing?.base_url ?? "http://127.0.0.1:8787",
54
- project_id: overrides.project_id ?? existing?.project_id ?? "prj_local",
71
+ base_url: overrides.base_url ?? existing?.base_url ?? 'http://127.0.0.1:8787',
72
+ project_id: overrides.project_id ?? existing?.project_id ?? 'prj_local',
55
73
  workdir: path.resolve(overrides.workdir ?? existing?.workdir ?? process.cwd()),
56
74
  token: overrides.token ?? existing?.token ?? DEFAULT_LOCAL_HOST_TOKEN,
57
75
  room_cursor_seq: overrides.room_cursor_seq ?? existing?.room_cursor_seq ?? 0,
@@ -62,6 +80,7 @@ export async function upsertHostConfig(overrides) {
62
80
  });
63
81
  await mkdir(getHostConfigDir(), { recursive: true });
64
82
  await writeAtomicJson(getHostConfigPath(), next);
83
+ await appendHostLog(`[config] updated project=${next.project_id} host=${next.host_id} workdir=${next.workdir}`);
65
84
  return next;
66
85
  }
67
86
  export async function loadPersistedHostSessions() {
@@ -73,16 +92,17 @@ export async function loadPersistedHostSessions() {
73
92
  return [];
74
93
  }
75
94
  try {
76
- const raw = await readFile(filePath, "utf8");
95
+ const raw = await readFile(filePath, 'utf8');
77
96
  const parsed = JSON.parse(raw);
78
97
  if (!Array.isArray(parsed)) {
79
98
  return [];
80
99
  }
81
100
  return parsed
82
- .filter((entry) => entry && typeof entry === "object" && entry.session_id)
101
+ .filter((entry) => entry && typeof entry === 'object' && entry.session_id)
83
102
  .map((entry) => normalizePersistedSession(entry));
84
103
  }
85
- catch {
104
+ catch (error) {
105
+ await quarantineCorruptFile(filePath, '', error);
86
106
  return [];
87
107
  }
88
108
  }
@@ -92,20 +112,23 @@ export async function savePersistedHostSessions(sessions) {
92
112
  await writeAtomicJson(getHostSessionsPath(), next);
93
113
  }
94
114
  export function getStartupLauncherPath() {
95
- return path.join(baseAppDataDir(), "Microsoft", "Windows", "Start Menu", "Programs", "Startup", "office-host.bat");
115
+ return path.join(baseAppDataDir(), 'Microsoft', 'Windows', 'Start Menu', 'Programs', 'Startup', 'office-host.bat');
116
+ }
117
+ export function getDesktopLauncherPath() {
118
+ return path.join(baseAppDataDir(), 'Microsoft', 'Windows', 'Start Menu', 'Programs', 'Office Core.bat');
96
119
  }
97
120
  function normalizeConfig(input) {
98
121
  return {
99
122
  version: HOST_CONFIG_VERSION,
100
- host_id: String(input.host_id ?? "").trim(),
101
- display_name: String(input.display_name ?? "").trim(),
123
+ host_id: String(input.host_id ?? '').trim(),
124
+ display_name: String(input.display_name ?? '').trim(),
102
125
  machine_name: String(input.machine_name ?? os.hostname()).trim(),
103
- base_url: String(input.base_url ?? "http://127.0.0.1:8787").trim().replace(/\/$/, ""),
104
- project_id: String(input.project_id ?? "prj_local").trim(),
126
+ base_url: String(input.base_url ?? 'http://127.0.0.1:8787').trim().replace(/\/$/, ''),
127
+ project_id: String(input.project_id ?? 'prj_local').trim(),
105
128
  workdir: path.resolve(String(input.workdir ?? process.cwd())),
106
129
  token: String(input.token ?? DEFAULT_LOCAL_HOST_TOKEN).trim(),
107
- room_cursor_seq: Number(input.room_cursor_seq ?? 0),
108
- poll_ms: Number(input.poll_ms ?? 5000),
130
+ room_cursor_seq: finiteNumber(input.room_cursor_seq, 0),
131
+ poll_ms: finiteNumber(input.poll_ms, 5000),
109
132
  auto_start: Boolean(input.auto_start ?? true),
110
133
  created_at: String(input.created_at ?? new Date().toISOString()),
111
134
  updated_at: String(input.updated_at ?? new Date().toISOString()),
@@ -113,34 +136,34 @@ function normalizeConfig(input) {
113
136
  }
114
137
  function normalizePersistedSession(input) {
115
138
  return {
116
- session_id: String(input.session_id ?? "").trim(),
139
+ session_id: String(input.session_id ?? '').trim(),
117
140
  project_id: input.project_id ? String(input.project_id) : null,
118
- runner: input.runner === "claude" ? "claude" : "codex",
119
- agent_id: String(input.agent_id ?? "").trim(),
120
- mode: input.mode === "execute" || input.mode === "review" || input.mode === "attach" ? input.mode : "brainstorm",
141
+ runner: input.runner === 'claude' ? 'claude' : 'codex',
142
+ agent_id: String(input.agent_id ?? '').trim(),
143
+ mode: input.mode === 'execute' || input.mode === 'review' || input.mode === 'attach' ? input.mode : 'brainstorm',
121
144
  workdir: path.resolve(String(input.workdir ?? process.cwd())),
122
- status: input.status === "completed" || input.status === "failed" ? input.status : "running",
145
+ status: input.status === 'completed' || input.status === 'failed' ? input.status : 'running',
123
146
  launched_at: String(input.launched_at ?? new Date().toISOString()),
124
147
  task_id: input.task_id ? String(input.task_id) : null,
125
- effort: input.effort === "low" || input.effort === "medium" || input.effort === "max" || input.effort === "high"
148
+ effort: input.effort === 'low' || input.effort === 'medium' || input.effort === 'max' || input.effort === 'high'
126
149
  ? input.effort
127
- : "high",
150
+ : 'high',
128
151
  shell_pid: Number.isFinite(Number(input.shell_pid)) ? Number(input.shell_pid) : null,
129
152
  script_dir: input.script_dir ? String(input.script_dir) : null,
130
153
  note: input.note ? String(input.note) : null,
131
154
  bootstrap: input.bootstrap
132
155
  ? {
133
- session_id: String(input.bootstrap.session_id ?? "").trim(),
134
- session_epoch: Number(input.bootstrap.session_epoch ?? 0),
135
- session_token: String(input.bootstrap.session_token ?? ""),
136
- bundle_seq: Number(input.bootstrap.bundle_seq ?? 1),
137
- task_version: Number(input.bootstrap.task_version ?? 1),
156
+ session_id: String(input.bootstrap.session_id ?? '').trim(),
157
+ session_epoch: finiteNumber(input.bootstrap.session_epoch, 0),
158
+ session_token: String(input.bootstrap.session_token ?? ''),
159
+ bundle_seq: finiteNumber(input.bootstrap.bundle_seq, 1),
160
+ task_version: finiteNumber(input.bootstrap.task_version, 1),
138
161
  }
139
162
  : null,
140
163
  };
141
164
  }
142
165
  function sanitize(value) {
143
- return value.replace(/[^a-zA-Z0-9_-]+/g, "-").replace(/^-+|-+$/g, "").toLowerCase();
166
+ return value.replace(/[^a-zA-Z0-9_-]+/g, '-').replace(/^-+|-+$/g, '').toLowerCase();
144
167
  }
145
168
  function firstExistingPath(paths) {
146
169
  for (const filePath of paths) {
@@ -155,7 +178,7 @@ function parseLooseJson(raw) {
155
178
  return JSON.parse(raw);
156
179
  }
157
180
  catch {
158
- for (let index = raw.lastIndexOf("}"); index >= 0; index = raw.lastIndexOf("}", index - 1)) {
181
+ for (let index = raw.lastIndexOf('}'); index >= 0; index = raw.lastIndexOf('}', index - 1)) {
159
182
  const trimmed = raw.slice(0, index + 1).trim();
160
183
  try {
161
184
  return JSON.parse(trimmed);
@@ -164,25 +187,36 @@ function parseLooseJson(raw) {
164
187
  // keep walking backward until we hit the real JSON terminator
165
188
  }
166
189
  }
167
- throw new SyntaxError("Unable to parse host config JSON");
190
+ throw new SyntaxError('Unable to parse host config JSON');
168
191
  }
169
192
  }
170
- async function writeAtomicJson(filePath, value) {
171
- const tempPath = `${filePath}.tmp`;
193
+ async function quarantineCorruptFile(filePath, raw, error) {
172
194
  try {
173
- await writeFile(tempPath, JSON.stringify(value, null, 2), "utf8");
174
- await rename(tempPath, filePath);
175
- }
176
- catch (error) {
177
- await rm(tempPath, { force: true }).catch(() => undefined);
178
- const code = error && typeof error === "object" && "code" in error ? String(error.code ?? "") : "";
179
- if (code === "EPERM" || code === "EACCES" || code === "EBUSY") {
180
- const fileLabel = path.basename(filePath);
181
- throw new Error(`Unable to write ${fileLabel} at ${filePath}. Remove read-only protection or close any program using that file.`);
195
+ await mkdir(getHostDiagnosticsDir(), { recursive: true });
196
+ const stamp = new Date().toISOString().replace(/[:.]/g, '-');
197
+ const target = path.join(getHostDiagnosticsDir(), `${path.basename(filePath)}.corrupt-${stamp}.json`);
198
+ if (raw) {
199
+ await writeFile(target, raw, 'utf8');
200
+ }
201
+ else if (existsSync(filePath)) {
202
+ await rename(filePath, target);
203
+ return;
182
204
  }
183
- throw error;
205
+ await appendHostLog(`[config] quarantined corrupt config from ${filePath}: ${error instanceof Error ? error.message : String(error)}`);
184
206
  }
207
+ catch {
208
+ // ignore quarantine failures; returning null is still safer than crashing the CLI
209
+ }
210
+ }
211
+ async function writeAtomicJson(filePath, value) {
212
+ const tempPath = `${filePath}.tmp`;
213
+ await writeFile(tempPath, JSON.stringify(value, null, 2), 'utf8');
214
+ await rename(tempPath, filePath);
215
+ }
216
+ function finiteNumber(value, fallback) {
217
+ const parsed = Number(value);
218
+ return Number.isFinite(parsed) ? parsed : fallback;
185
219
  }
186
220
  function baseAppDataDir() {
187
- return process.env.APPDATA ?? path.join(process.env.USERPROFILE ?? os.homedir(), "AppData", "Roaming");
221
+ return process.env.APPDATA ?? path.join(process.env.USERPROFILE ?? os.homedir(), 'AppData', 'Roaming');
188
222
  }
@@ -38,11 +38,21 @@ export function probeRunnerAvailability(kind, override) {
38
38
  if (process.platform === "win32") {
39
39
  return { available: existsSync(command) || queryWhere(command).length > 0, command };
40
40
  }
41
- return { available: true, command };
41
+ const result = spawnSync('sh', ['-lc', `command -v ${shellEscape(command)} >/dev/null 2>&1`], {
42
+ windowsHide: true,
43
+ });
44
+ return { available: result.status === 0, command };
45
+ }
46
+ export function assertRunnerAvailable(kind, override) {
47
+ const probe = probeRunnerAvailability(kind, override);
48
+ if (!probe.available) {
49
+ throw new Error(`${kind} is not installed or is not on PATH.`);
50
+ }
51
+ return probe.command;
42
52
  }
43
53
  async function runCodex(input) {
44
54
  const outputDir = await mkdtemp(path.join(os.tmpdir(), "office-codex-"));
45
- const command = resolveRunnerCommand("codex", process.env.CODEX_CMD);
55
+ const command = assertRunnerAvailable("codex", process.env.CODEX_CMD);
46
56
  const args = [
47
57
  "exec",
48
58
  "--json",
@@ -64,7 +74,7 @@ async function runCodex(input) {
64
74
  }
65
75
  }
66
76
  async function runClaude(input) {
67
- const command = resolveRunnerCommand("claude", process.env.CLAUDE_CMD);
77
+ const command = assertRunnerAvailable("claude", process.env.CLAUDE_CMD);
68
78
  const args = [
69
79
  "-p",
70
80
  "--verbose",
@@ -94,6 +104,9 @@ function queryWhere(command) {
94
104
  .map((line) => line.trim())
95
105
  .filter(Boolean);
96
106
  }
107
+ function shellEscape(value) {
108
+ return `'${String(value).replace(/'/g, `'\''`)}'`;
109
+ }
97
110
  function buildWindowsFallbacks(kind) {
98
111
  const localAppData = process.env.LOCALAPPDATA ?? "";
99
112
  const appData = process.env.APPDATA ?? "";