yaver-cli 1.99.10 → 1.99.12

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "yaver-cli",
3
- "version": "1.99.10",
3
+ "version": "1.99.12",
4
4
  "mcpName": "io.github.kivanccakmak/yaver",
5
5
  "description": "Unified npm bootstrap for the Yaver agent, SDK injection, and local-first developer runtime",
6
6
  "bin": {
@@ -1,7 +1,7 @@
1
1
  const fs = require('fs');
2
2
  const os = require('os');
3
3
  const path = require('path');
4
- const { spawn } = require('child_process');
4
+ const { spawn, spawnSync } = require('child_process');
5
5
  const { pipeline } = require('stream/promises');
6
6
  const https = require('https');
7
7
  const semver = require('semver');
@@ -14,6 +14,17 @@ const CACHE_ROOT = process.env.YAVER_AGENT_CACHE_DIR || path.join(os.homedir(),
14
14
  let resolvedAgentVersionPromise = null;
15
15
  const resolvedAssetPromiseByKey = new Map();
16
16
 
17
+ // Magic bytes for executable formats. Used to sanity-check a downloaded
18
+ // binary before we spawn it — an HTML error page or a half-written file
19
+ // will fail this and we'll redownload instead of producing SIGKILL.
20
+ const EXECUTABLE_MAGICS = [
21
+ Buffer.from([0xcf, 0xfa, 0xed, 0xfe]), // Mach-O 64-bit LE (macOS)
22
+ Buffer.from([0xce, 0xfa, 0xed, 0xfe]), // Mach-O 32-bit LE
23
+ Buffer.from([0xca, 0xfe, 0xba, 0xbe]), // Mach-O fat binary
24
+ Buffer.from([0x7f, 0x45, 0x4c, 0x46]), // ELF (Linux) — \x7fELF
25
+ Buffer.from([0x4d, 0x5a]), // PE (Windows) — MZ
26
+ ];
27
+
17
28
  async function ensureAgentBinary({ quiet = false } = {}) {
18
29
  const asset = await resolveAsset();
19
30
  const localAgentPath = resolveLocalAgentBinary(asset);
@@ -58,25 +69,122 @@ async function ensureAgentBinary({ quiet = false } = {}) {
58
69
  if (process.platform !== 'win32') {
59
70
  fs.chmodSync(binaryPath, 0o755);
60
71
  }
72
+ // Proactively strip macOS quarantine + re-adhoc-sign on
73
+ // freshly-downloaded binaries. Without this:
74
+ // - Gatekeeper SIGKILLs the first exec because Yaver release
75
+ // tarballs are not notarized (adhoc-signed at link time only).
76
+ // - If the Go linker's adhoc signature didn't survive the tarball
77
+ // round-trip, the kernel refuses to load it ("load code
78
+ // signature error 2"). codesign --force --sign - rebuilds a
79
+ // valid adhoc signature against the current bytes.
80
+ // Doing both here means the happy path never sees SIGKILL in the
81
+ // first place.
82
+ if (process.platform === 'darwin') {
83
+ try {
84
+ spawnSync('xattr', ['-dr', 'com.apple.quarantine', binaryPath], { stdio: 'ignore' });
85
+ } catch (_err) {}
86
+ try {
87
+ spawnSync('codesign', ['--force', '--sign', '-', binaryPath], { stdio: 'ignore' });
88
+ } catch (_err) {}
89
+ }
90
+ // Sanity-check the extracted binary. A truncated tarball or an
91
+ // HTML error page saved as ".tar.gz" can leave us with a file that
92
+ // exists but isn't actually executable — spawning it produces
93
+ // SIGKILL and a cryptic error. Fail loudly here with a message
94
+ // that points at the actual problem (bad download) so the caller
95
+ // can redownload.
96
+ if (!looksLikeExecutable(binaryPath)) {
97
+ try { fs.rmSync(binaryPath, { force: true }); } catch (_err) {}
98
+ throw new Error(
99
+ `downloaded agent binary at ${binaryPath} is not a valid executable ` +
100
+ `(likely a truncated download or GitHub rate-limit HTML page). ` +
101
+ `Retry in a minute, or set YAVER_AGENT_BIN to a local yaver binary.`,
102
+ );
103
+ }
61
104
  return binaryPath;
62
105
  }
63
106
 
64
107
  async function runAgentCommand(args, options = {}) {
108
+ // One auto-recovery retry on SIGKILL of a cached binary. SIGKILL on a
109
+ // freshly-downloaded binary almost always means macOS Gatekeeper
110
+ // quarantined it (no notarization), or the download was truncated.
111
+ // Both are self-healing: strip quarantine + redownload + retry once.
112
+ // We never retry for user-initiated signals or normal exits.
113
+ let attempt = 0;
114
+ const maxAttempts = 2;
115
+
116
+ while (true) {
117
+ attempt += 1;
118
+ const spawnSpec = await resolveSpawnSpec(args, options);
119
+ const isCachedBinary = spawnSpec.command.startsWith(CACHE_ROOT);
120
+
121
+ try {
122
+ await spawnAndWait(spawnSpec, args);
123
+ return;
124
+ } catch (err) {
125
+ const { signal, recoverable } = err;
126
+ if (
127
+ recoverable &&
128
+ signal === 'SIGKILL' &&
129
+ isCachedBinary &&
130
+ attempt < maxAttempts
131
+ ) {
132
+ const healed = await attemptSigkillRecovery(spawnSpec.command, { quiet: options.quiet });
133
+ if (healed) {
134
+ console.error(`Yaver agent: recovered from SIGKILL (${healed}). Retrying...`);
135
+ continue;
136
+ }
137
+ }
138
+ throw err;
139
+ }
140
+ }
141
+ }
142
+
143
+ async function resolveSpawnSpec(args, options) {
65
144
  const localAgent = resolveLocalAgentCommand(args);
66
- const spawnSpec = localAgent
67
- ? localAgent
68
- : { command: await ensureAgentBinary(options), args };
145
+ if (localAgent) return localAgent;
146
+ return { command: await ensureAgentBinary(options), args };
147
+ }
148
+
149
+ function spawnAndWait(spawnSpec, args) {
69
150
  const child = spawn(spawnSpec.command, spawnSpec.args, {
70
151
  stdio: 'inherit',
71
152
  env: process.env,
72
153
  cwd: spawnSpec.cwd || process.cwd(),
73
154
  });
74
155
 
75
- await new Promise((resolve, reject) => {
156
+ // Forward Ctrl-C / SIGTERM / SIGHUP from the wrapper to the child
157
+ // so the user's interrupt actually reaches the running binary
158
+ // (instead of killing the wrapper while leaving the child orphaned).
159
+ // Without this, Ctrl-C would just kill the Node wrapper and the
160
+ // wrapper's exit handler would then report "terminated by signal
161
+ // SIGINT" as if it were an error.
162
+ const forwarded = ['SIGINT', 'SIGTERM', 'SIGHUP'];
163
+ const forwarders = forwarded.map((sig) => {
164
+ const handler = () => {
165
+ try { child.kill(sig); } catch (_e) { /* child already gone */ }
166
+ };
167
+ process.on(sig, handler);
168
+ return [sig, handler];
169
+ });
170
+
171
+ return new Promise((resolve, reject) => {
76
172
  child.on('error', reject);
77
173
  child.on('exit', (code, signal) => {
174
+ for (const [sig, handler] of forwarders) {
175
+ process.removeListener(sig, handler);
176
+ }
177
+ // User-initiated interrupts — exit quietly. The user already
178
+ // knows they hit Ctrl-C; surfacing a red ❌ is noise.
179
+ if (signal === 'SIGINT' || signal === 'SIGTERM' || signal === 'SIGHUP') {
180
+ resolve();
181
+ return;
182
+ }
78
183
  if (signal) {
79
- reject(new Error(`agent terminated by signal ${signal}`));
184
+ const error = new Error(diagnoseAbnormalSignal(signal, args));
185
+ error.signal = signal;
186
+ error.recoverable = true;
187
+ reject(error);
80
188
  return;
81
189
  }
82
190
  if (code && code !== 0) {
@@ -90,6 +198,136 @@ async function runAgentCommand(args, options = {}) {
90
198
  });
91
199
  }
92
200
 
201
+ // attemptSigkillRecovery tries the three things that actually fix a
202
+ // SIGKILL on a cached Yaver binary:
203
+ // 1. strip macOS quarantine xattrs (Gatekeeper-killed Mach-Os)
204
+ // 2. re-adhoc-sign the Mach-O (fixes broken signature from the
205
+ // agent's in-place auto-update — kernel refuses to exec, logs
206
+ // "load code signature error 2")
207
+ // 3. redownload the binary if the magic bytes don't look like an
208
+ // executable (truncated download / HTML error page)
209
+ // Returns the recovery action taken, or null if nothing could be done.
210
+ async function attemptSigkillRecovery(binaryPath, { quiet = false } = {}) {
211
+ if (!fs.existsSync(binaryPath)) {
212
+ // The binary went away mid-flight; re-download by returning null
213
+ // so the caller retries the full resolve path next loop.
214
+ return null;
215
+ }
216
+
217
+ // 1. Sanity check first — if the binary isn't even executable-shaped,
218
+ // nothing we do to it will help. Redownload.
219
+ if (!looksLikeExecutable(binaryPath)) {
220
+ try {
221
+ fs.rmSync(binaryPath, { force: true });
222
+ resolvedAssetPromiseByKey.clear();
223
+ } catch (_err) {}
224
+ if (!quiet) console.error('Yaver agent: cached binary was corrupt, redownloading...');
225
+ return 'redownload-corrupt-binary';
226
+ }
227
+
228
+ if (process.platform === 'darwin') {
229
+ // 2. Strip quarantine. Unsigned binaries downloaded via https get
230
+ // tagged com.apple.quarantine and the kernel SIGKILLs them on
231
+ // first exec (no dialog, because the parent is a CLI).
232
+ try {
233
+ spawnSync('xattr', ['-dr', 'com.apple.quarantine', binaryPath], { stdio: 'ignore' });
234
+ } catch (_err) {}
235
+ try { fs.chmodSync(binaryPath, 0o755); } catch (_err) {}
236
+
237
+ // 3. Re-adhoc-sign. When the agent's own self-update rewrites the
238
+ // binary in place, the original adhoc signature no longer
239
+ // matches the new contents and the kernel refuses to exec it
240
+ // (dmesg: "load code signature error 2"). `codesign --force
241
+ // --sign -` rebuilds the adhoc signature against the current
242
+ // bytes, which is all the kernel needs to let exec proceed.
243
+ try {
244
+ const result = spawnSync('codesign', ['--force', '--sign', '-', binaryPath], {
245
+ stdio: ['ignore', 'pipe', 'pipe'],
246
+ encoding: 'utf8',
247
+ });
248
+ if (result.status === 0) {
249
+ // Verify the resigned binary actually passes the kernel check
250
+ // now. If codesign succeeded we're done — the retry will work.
251
+ return 'resign-macos-adhoc';
252
+ }
253
+ // If codesign failed (e.g. missing developer tools), fall through
254
+ // to the quarantine-only return below.
255
+ } catch (_err) {}
256
+
257
+ return 'strip-macos-quarantine';
258
+ }
259
+
260
+ // On Linux / Windows a SIGKILL of an intact binary usually means an
261
+ // OOM kill or external pkill; re-exec is unlikely to help, so
262
+ // return null and let the original diagnostic message stand.
263
+ return null;
264
+ }
265
+
266
+ function looksLikeExecutable(binaryPath) {
267
+ try {
268
+ const fd = fs.openSync(binaryPath, 'r');
269
+ const buf = Buffer.alloc(4);
270
+ const bytesRead = fs.readSync(fd, buf, 0, 4, 0);
271
+ fs.closeSync(fd);
272
+ if (bytesRead < 2) return false;
273
+ return EXECUTABLE_MAGICS.some((magic) => {
274
+ if (magic.length > bytesRead) return false;
275
+ return buf.slice(0, magic.length).equals(magic);
276
+ });
277
+ } catch (_err) {
278
+ return false;
279
+ }
280
+ }
281
+
282
+ // diagnoseAbnormalSignal turns a child-process termination signal into
283
+ // a concrete next-step the user can actually take. Each branch points
284
+ // at the most-common cause — pulled from real bug reports — and lists
285
+ // the exact command that resolves it.
286
+ function diagnoseAbnormalSignal(signal, args = []) {
287
+ const cmd = args[0] || 'agent';
288
+ switch (signal) {
289
+ case 'SIGKILL': {
290
+ const lines = [
291
+ `agent process was killed (SIGKILL) — this is usually NOT a bug, ` +
292
+ `the OS or another process forced it down.`,
293
+ ``,
294
+ `Most-likely cause first:`,
295
+ ` 1. Another yaver agent is already running (port 18080 is taken).`,
296
+ ` check: lsof -i :18080`,
297
+ ` fix: yaver stop (or: pkill -f 'yaver.*serve')`,
298
+ ];
299
+ if (cmd === 'auth') {
300
+ lines.push(
301
+ ` Note: \`yaver auth\` doesn't need a running agent. If one is`,
302
+ ` already up in bootstrap mode, pair from your Yaver mobile app instead`,
303
+ ` of running \`yaver auth\` again.`,
304
+ );
305
+ }
306
+ lines.push(
307
+ ``,
308
+ `Other possibilities:`,
309
+ ` 2. macOS Gatekeeper quarantined the binary. Run:`,
310
+ ` xattr -dr com.apple.quarantine ~/.yaver/bin`,
311
+ ` 3. The OS killed it for OOM / power. Check Console.app → Crash Reports.`,
312
+ ` 4. The binary is corrupt. Reinstall: npm i -g yaver-cli@latest`,
313
+ );
314
+ return lines.join('\n');
315
+ }
316
+ case 'SIGABRT':
317
+ return `agent aborted (SIGABRT). Most likely a Go panic — check ~/.yaver/agent.log for the stack trace.`;
318
+ case 'SIGSEGV':
319
+ case 'SIGBUS':
320
+ return (
321
+ `agent crashed (${signal}). Likely binary corruption or an arch mismatch.\n` +
322
+ `Try: npm i -g yaver-cli@latest to reinstall a clean binary.`
323
+ );
324
+ case 'SIGPIPE':
325
+ return `agent's stdout/stderr pipe was closed. If you piped through head/less, that's expected — re-run without the pipe.`;
326
+ default:
327
+ return `agent terminated by signal ${signal}. Check ~/.yaver/agent.log for details.`;
328
+ }
329
+ }
330
+
93
331
  function resolveAgentInfo() {
94
332
  return {
95
333
  version: process.env.YAVER_AGENT_VERSION || PACKAGE.version,