svamp-cli 0.2.45 → 0.2.47

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.
@@ -1,16 +1,16 @@
1
- import{createRequire as _pkgrollCR}from"node:module";const require=_pkgrollCR(import.meta.url);import os from 'os';
1
+ import{createRequire as _pkgrollCR}from"node:module";const require=_pkgrollCR(import.meta.url);import os$1 from 'os';
2
2
  import fs, { mkdir as mkdir$1, readdir as readdir$1, readFile, writeFile as writeFile$1, rename, unlink } from 'fs/promises';
3
- import { readFileSync as readFileSync$1, mkdirSync, writeFileSync, renameSync, existsSync as existsSync$1, copyFileSync, unlinkSync, watch, rmdirSync } from 'fs';
3
+ import { readFileSync as readFileSync$1, mkdirSync, writeFileSync, renameSync, existsSync as existsSync$1, copyFileSync, unlinkSync as unlinkSync$1, watch, rmdirSync } from 'fs';
4
4
  import path__default, { join, dirname, resolve, basename } from 'path';
5
5
  import { fileURLToPath } from 'url';
6
6
  import { spawn as spawn$1, execSync as execSync$1 } from 'child_process';
7
7
  import { randomUUID as randomUUID$1 } from 'crypto';
8
- import { existsSync, readFileSync, writeFileSync as writeFileSync$1, mkdirSync as mkdirSync$1, appendFileSync } from 'node:fs';
8
+ import { existsSync, readFileSync, writeFileSync as writeFileSync$1, mkdirSync as mkdirSync$1, appendFileSync, unlinkSync } from 'node:fs';
9
9
  import { randomUUID, createHash } from 'node:crypto';
10
10
  import { join as join$1 } from 'node:path';
11
11
  import { spawn, execSync, execFile, execFileSync } from 'node:child_process';
12
12
  import { ndJsonStream, ClientSideConnection } from '@agentclientprotocol/sdk';
13
- import { homedir, platform } from 'node:os';
13
+ import os, { homedir, platform } from 'node:os';
14
14
  import { Client } from '@modelcontextprotocol/sdk/client/index.js';
15
15
  import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
16
16
  import { ElicitRequestSchema } from '@modelcontextprotocol/sdk/types.js';
@@ -1107,7 +1107,7 @@ async function registerMachineService(server, machineId, metadata, daemonState,
1107
1107
  const tunnels = handlers.tunnels;
1108
1108
  if (!tunnels) throw new Error("Tunnel management not available");
1109
1109
  if (tunnels.has(params.name)) throw new Error(`Tunnel '${params.name}' already running`);
1110
- const { FrpcTunnel } = await import('./frpc-DzRFx60H.mjs');
1110
+ const { FrpcTunnel } = await import('./frpc-j60b46eU.mjs');
1111
1111
  const tunnel = new FrpcTunnel({
1112
1112
  name: params.name,
1113
1113
  ports: params.ports,
@@ -1149,6 +1149,26 @@ async function registerMachineService(server, machineId, metadata, daemonState,
1149
1149
  const access = params.access || "owner";
1150
1150
  return sm.addMount(params.name, params.directory, params.sessionId, access, ownerEmail);
1151
1151
  },
1152
+ /**
1153
+ * Apply a mount declaratively. Replaces existing mount with same name.
1154
+ * Supports static (directory) and managed-process (process) mounts.
1155
+ * Used by `svamp serve apply <yaml>`.
1156
+ */
1157
+ serveApply: async (params, context) => {
1158
+ authorizeRequest(context, currentMetadata.sharing, "interact");
1159
+ const sm = handlers.serveManager;
1160
+ if (!sm) throw new Error("Serve manager not available");
1161
+ const ownerEmail = params.ownerEmail || context?.user?.email || void 0;
1162
+ const access = params.access ?? "owner";
1163
+ return sm.applyMount({
1164
+ name: params.name,
1165
+ directory: params.directory,
1166
+ process: params.process,
1167
+ sessionId: params.sessionId,
1168
+ access,
1169
+ ownerEmail
1170
+ });
1171
+ },
1152
1172
  /** Remove a mount from the shared static file server. */
1153
1173
  serveRemove: async (params, context) => {
1154
1174
  authorizeRequest(context, currentMetadata.sharing, "interact");
@@ -1325,6 +1345,19 @@ function loadMessagesFromDiskReverse(messagesDir, beforeSeq, limit) {
1325
1345
  return { messages: [], hasMore: false };
1326
1346
  }
1327
1347
  }
1348
+ function countMessagesOnDisk(messagesDir, fallbackInMemory) {
1349
+ const filePath = join$1(messagesDir, "messages.jsonl");
1350
+ if (!existsSync(filePath)) return fallbackInMemory;
1351
+ try {
1352
+ const data = readFileSync(filePath, "utf-8");
1353
+ let count = 0;
1354
+ for (let i = 0; i < data.length; i++) if (data.charCodeAt(i) === 10) count++;
1355
+ if (data.length > 0 && data[data.length - 1] !== "\n") count++;
1356
+ return count;
1357
+ } catch {
1358
+ return fallbackInMemory;
1359
+ }
1360
+ }
1328
1361
  function appendMessage(messagesDir, sessionId, msg) {
1329
1362
  try {
1330
1363
  const filePath = join$1(messagesDir, "messages.jsonl");
@@ -1464,6 +1497,12 @@ function createSessionStore(server, sessionId, initialMetadata, initialAgentStat
1464
1497
  hasMore: filtered.length > lim
1465
1498
  };
1466
1499
  },
1500
+ getMessageCount: async (context) => {
1501
+ authorizeRequest(context, metadata.sharing, "view");
1502
+ const latestSeq = nextSeq - 1;
1503
+ const count = options?.messagesDir ? countMessagesOnDisk(options.messagesDir, messages.length) : messages.length;
1504
+ return { count, latestSeq };
1505
+ },
1467
1506
  getLatestMessages: async (beforeSeq, limit, context) => {
1468
1507
  authorizeRequest(context, metadata.sharing, "view");
1469
1508
  const lim = Math.min(limit ?? 100, 500);
@@ -5092,6 +5131,8 @@ const DEFAULT_PROBE_FAILURE_THRESHOLD = 3;
5092
5131
  const MAX_RESTART_DELAY_S = 300;
5093
5132
  const BACKOFF_RESET_WINDOW_MS = 6e4;
5094
5133
  const MAX_LOG_LINES = 300;
5134
+ const CRASH_LOOP_FAILURE_THRESHOLD = 10;
5135
+ const CRASH_LOOP_WINDOW_MS = 3e5;
5095
5136
  class ProcessSupervisor {
5096
5137
  entries = /* @__PURE__ */ new Map();
5097
5138
  persistDir;
@@ -5139,11 +5180,13 @@ class ProcessSupervisor {
5139
5180
  /** Start a stopped/failed process by id or name. */
5140
5181
  async start(idOrName) {
5141
5182
  const entry = this.require(idOrName);
5183
+ if (entry.deleted) throw new Error(`Process '${idOrName}' has been removed`);
5142
5184
  if (entry.child) {
5143
5185
  if (entry.stopping) throw new Error(`Process '${entry.spec.name}' is being stopped, try again shortly`);
5144
5186
  throw new Error(`Process '${entry.spec.name}' is already running`);
5145
5187
  }
5146
5188
  entry.stopping = false;
5189
+ this.resetFailureTracking(entry);
5147
5190
  await this.startEntry(entry, false);
5148
5191
  }
5149
5192
  /** Stop a running process. Does NOT remove it from supervision. */
@@ -5162,6 +5205,7 @@ class ProcessSupervisor {
5162
5205
  /** Restart a process (stop if running, then start again). */
5163
5206
  async restart(idOrName) {
5164
5207
  const entry = this.require(idOrName);
5208
+ if (entry.deleted) throw new Error(`Process '${idOrName}' has been removed`);
5165
5209
  if (entry.restarting) return;
5166
5210
  entry.restarting = true;
5167
5211
  try {
@@ -5171,7 +5215,9 @@ class ProcessSupervisor {
5171
5215
  await this.killChild(entry.child);
5172
5216
  entry.child = void 0;
5173
5217
  }
5218
+ if (entry.deleted) return;
5174
5219
  entry.stopping = false;
5220
+ this.resetFailureTracking(entry);
5175
5221
  entry.state.restartCount++;
5176
5222
  await this.startEntry(entry, false);
5177
5223
  } finally {
@@ -5182,7 +5228,16 @@ class ProcessSupervisor {
5182
5228
  async remove(idOrName) {
5183
5229
  const entry = this.require(idOrName);
5184
5230
  const id = entry.spec.id;
5185
- await this.stop(entry.spec.name);
5231
+ entry.deleted = true;
5232
+ entry.stopping = true;
5233
+ this.clearTimers(entry);
5234
+ if (entry.child) {
5235
+ await this.killChild(entry.child);
5236
+ entry.child = void 0;
5237
+ }
5238
+ entry.state.status = "stopped";
5239
+ entry.state.stoppedAt = Date.now();
5240
+ entry.state.pid = void 0;
5186
5241
  this.entries.delete(id);
5187
5242
  await this.deleteSpec(id);
5188
5243
  }
@@ -5232,7 +5287,9 @@ class ProcessSupervisor {
5232
5287
  await this.killChild(existing.child);
5233
5288
  existing.child = void 0;
5234
5289
  }
5290
+ if (existing.deleted) return { action: "updated", info: this.toInfo(existing) };
5235
5291
  existing.stopping = false;
5292
+ this.resetFailureTracking(existing);
5236
5293
  existing.state.status = "starting";
5237
5294
  this.spawnProcess(existing);
5238
5295
  return { action: "updated", info: this.toInfo(existing) };
@@ -5243,6 +5300,7 @@ class ProcessSupervisor {
5243
5300
  */
5244
5301
  async update(idOrName, partialSpec) {
5245
5302
  const entry = this.require(idOrName);
5303
+ if (entry.deleted) throw new Error(`Process '${idOrName}' has been removed`);
5246
5304
  const updatedSpec = { ...entry.spec, ...partialSpec };
5247
5305
  entry.spec = updatedSpec;
5248
5306
  await this.persistSpec(updatedSpec);
@@ -5252,7 +5310,9 @@ class ProcessSupervisor {
5252
5310
  await this.killChild(entry.child);
5253
5311
  entry.child = void 0;
5254
5312
  }
5313
+ if (entry.deleted) return this.toInfo(entry);
5255
5314
  entry.stopping = false;
5315
+ this.resetFailureTracking(entry);
5256
5316
  entry.state.status = "starting";
5257
5317
  this.spawnProcess(entry);
5258
5318
  return this.toInfo(entry);
@@ -5328,7 +5388,8 @@ class ProcessSupervisor {
5328
5388
  id: spec.id,
5329
5389
  status: "pending",
5330
5390
  restartCount: 0,
5331
- consecutiveProbeFailures: 0
5391
+ consecutiveProbeFailures: 0,
5392
+ consecutiveFailures: 0
5332
5393
  },
5333
5394
  logBuffer: [],
5334
5395
  stopping: false
@@ -5348,6 +5409,7 @@ class ProcessSupervisor {
5348
5409
  // ── Process spawning ──────────────────────────────────────────────────────
5349
5410
  async startEntry(entry, onRestore) {
5350
5411
  const { spec } = entry;
5412
+ if (entry.deleted) return;
5351
5413
  if (spec.ttl !== void 0 && spec.ttl > 0 && onRestore) {
5352
5414
  const elapsedS = (Date.now() - spec.createdAt) / 1e3;
5353
5415
  if (elapsedS >= spec.ttl) {
@@ -5366,6 +5428,7 @@ class ProcessSupervisor {
5366
5428
  }
5367
5429
  spawnProcess(entry) {
5368
5430
  const { spec, state } = entry;
5431
+ if (entry.deleted) return;
5369
5432
  try {
5370
5433
  const env = { ...process.env, ...spec.env ?? {} };
5371
5434
  const child = spawn$1(spec.command, spec.args, {
@@ -5408,6 +5471,7 @@ class ProcessSupervisor {
5408
5471
  state.pid = void 0;
5409
5472
  state.stoppedAt = Date.now();
5410
5473
  this.clearTimers(entry);
5474
+ if (entry.deleted) return;
5411
5475
  if (entry.stopping) {
5412
5476
  state.status = "stopped";
5413
5477
  console.log(`[SUPERVISOR] Process '${spec.name}' stopped (code=${code})`);
@@ -5417,31 +5481,65 @@ class ProcessSupervisor {
5417
5481
  state.status = crashed ? "failed" : "stopped";
5418
5482
  console.log(`[SUPERVISOR] Process '${spec.name}' exited (code=${code}, signal=${signal})`);
5419
5483
  if (!spec.keepAlive) return;
5484
+ const uptime = state.startedAt ? Date.now() - state.startedAt : 0;
5485
+ const now = Date.now();
5486
+ if (uptime > BACKOFF_RESET_WINDOW_MS) {
5487
+ state.restartCount = 0;
5488
+ state.consecutiveFailures = 0;
5489
+ entry.failureWindowStart = void 0;
5490
+ } else {
5491
+ if (!entry.failureWindowStart || now - entry.failureWindowStart > CRASH_LOOP_WINDOW_MS) {
5492
+ entry.failureWindowStart = now;
5493
+ state.consecutiveFailures = 1;
5494
+ } else {
5495
+ state.consecutiveFailures++;
5496
+ }
5497
+ }
5498
+ if (state.consecutiveFailures >= CRASH_LOOP_FAILURE_THRESHOLD) {
5499
+ const windowS = Math.round((now - (entry.failureWindowStart ?? now)) / 1e3);
5500
+ state.status = "crash-loop-backoff";
5501
+ state.crashLoopBackOff = {
5502
+ enteredAt: now,
5503
+ failureCount: state.consecutiveFailures,
5504
+ windowStart: entry.failureWindowStart ?? now
5505
+ };
5506
+ console.error(
5507
+ `[SUPERVISOR] '${spec.name}' entering CrashLoopBackOff: ${state.consecutiveFailures} failures in ${windowS}s. Restarts halted \u2014 run 'svamp process restart ${spec.name}' to retry.`
5508
+ );
5509
+ return;
5510
+ }
5420
5511
  if (spec.maxRestarts > 0 && state.restartCount >= spec.maxRestarts) {
5421
5512
  console.warn(`[SUPERVISOR] Process '${spec.name}' reached max restarts (${spec.maxRestarts}), not restarting`);
5422
5513
  state.status = "failed";
5423
5514
  return;
5424
5515
  }
5425
- const uptime = state.startedAt ? Date.now() - state.startedAt : 0;
5426
5516
  const baseDelay = spec.restartDelay * 1e3;
5427
5517
  let delayMs;
5428
5518
  if (uptime > BACKOFF_RESET_WINDOW_MS) {
5429
- state.restartCount = 0;
5430
5519
  delayMs = baseDelay;
5431
5520
  } else {
5432
- const backoffExponent = Math.min(state.restartCount, 10);
5521
+ const backoffExponent = Math.min(state.consecutiveFailures - 1, 10);
5433
5522
  delayMs = Math.min(baseDelay * Math.pow(2, backoffExponent), MAX_RESTART_DELAY_S * 1e3);
5434
5523
  const jitter = (Math.random() * 0.2 - 0.1) * delayMs;
5435
5524
  delayMs = Math.max(baseDelay, Math.round(delayMs + jitter));
5436
5525
  }
5437
- console.log(`[SUPERVISOR] Scheduling restart of '${spec.name}' in ${delayMs}ms (restart #${state.restartCount + 1}, uptime=${Math.round(uptime / 1e3)}s)`);
5526
+ console.log(
5527
+ `[SUPERVISOR] Scheduling restart of '${spec.name}' in ${delayMs}ms (failure ${state.consecutiveFailures}/${CRASH_LOOP_FAILURE_THRESHOLD}, restart #${state.restartCount + 1}, uptime=${Math.round(uptime / 1e3)}s)`
5528
+ );
5438
5529
  entry.restartTimer = setTimeout(() => {
5439
- if (entry.stopping) return;
5530
+ if (entry.deleted || entry.stopping) return;
5440
5531
  state.restartCount++;
5441
5532
  state.status = "starting";
5442
5533
  this.spawnProcess(entry);
5443
5534
  }, delayMs);
5444
5535
  }
5536
+ /** Reset all CrashLoopBackOff accounting. Called on user-initiated start/restart/spec-change. */
5537
+ resetFailureTracking(entry) {
5538
+ entry.state.consecutiveFailures = 0;
5539
+ entry.state.consecutiveProbeFailures = 0;
5540
+ entry.state.crashLoopBackOff = void 0;
5541
+ entry.failureWindowStart = void 0;
5542
+ }
5445
5543
  // ── Health probes ─────────────────────────────────────────────────────────
5446
5544
  setupProbe(entry) {
5447
5545
  const intervalMs = (entry.spec.probe.interval ?? DEFAULT_PROBE_INTERVAL_S) * 1e3;
@@ -5451,6 +5549,7 @@ class ProcessSupervisor {
5451
5549
  }, intervalMs);
5452
5550
  }
5453
5551
  async runHealthCheck(entry) {
5552
+ if (entry.deleted || entry.stopping) return;
5454
5553
  if (!entry.child || entry.state.status !== "running") return;
5455
5554
  const probe = entry.spec.probe;
5456
5555
  const urlPath = probe.path ?? "/";
@@ -5491,15 +5590,17 @@ class ProcessSupervisor {
5491
5590
  }
5492
5591
  }
5493
5592
  async triggerProbeRestart(entry) {
5494
- if (entry.restarting) return;
5593
+ if (entry.deleted) return;
5495
5594
  if (entry.stopping) return;
5496
- console.warn(`[SUPERVISOR] Restarting '${entry.spec.name}' due to probe failures`);
5595
+ if (entry.restarting) return;
5596
+ if (entry.state.status === "crash-loop-backoff") return;
5597
+ console.warn(`[SUPERVISOR] Probe failed for '${entry.spec.name}' \u2014 killing to trigger restart`);
5497
5598
  entry.state.consecutiveProbeFailures = 0;
5498
- this.clearTimers(entry);
5499
- try {
5500
- await this.restart(entry.spec.id);
5501
- } catch (err) {
5502
- console.error(`[SUPERVISOR] Probe-triggered restart failed for '${entry.spec.name}': ${err.message}`);
5599
+ if (entry.child) {
5600
+ try {
5601
+ entry.child.kill("SIGTERM");
5602
+ } catch {
5603
+ }
5503
5604
  }
5504
5605
  }
5505
5606
  // ── TTL ───────────────────────────────────────────────────────────────────
@@ -5514,9 +5615,11 @@ class ProcessSupervisor {
5514
5615
  entry.ttlTimer = setTimeout(() => this.expireProcess(entry), remainingS * 1e3);
5515
5616
  }
5516
5617
  expireProcess(entry) {
5618
+ if (entry.deleted) return;
5517
5619
  console.log(`[SUPERVISOR] Process '${entry.spec.name}' TTL expired`);
5518
5620
  entry.state.status = "expired";
5519
5621
  entry.stopping = true;
5622
+ entry.deleted = true;
5520
5623
  this.clearTimers(entry);
5521
5624
  const cleanup = async () => {
5522
5625
  if (entry.child) await this.killChild(entry.child);
@@ -5677,9 +5780,201 @@ You may be running in parallel with other agents \u2014 possibly sharing the sam
5677
5780
  `;
5678
5781
  }
5679
5782
 
5783
+ const SVAMP_HOME$1 = process.env.SVAMP_HOME || join$1(os.homedir(), ".svamp");
5784
+ function generateHookSettings(portOrOptions = {}) {
5785
+ const opts = typeof portOrOptions === "number" ? { sessionStartPort: portOrOptions } : portOrOptions;
5786
+ const hooksDir = join$1(SVAMP_HOME$1, "tmp", "hooks");
5787
+ mkdirSync$1(hooksDir, { recursive: true });
5788
+ const id = opts.id || String(process.pid);
5789
+ const validatorPath = join$1(hooksDir, `image-validator-${id}.cjs`);
5790
+ writeFileSync$1(validatorPath, IMAGE_VALIDATOR_SCRIPT, { mode: 493 });
5791
+ const cleanupPaths = [validatorPath];
5792
+ const hooks = {
5793
+ PreToolUse: [
5794
+ {
5795
+ matcher: "Read",
5796
+ hooks: [
5797
+ {
5798
+ type: "command",
5799
+ command: `node "${validatorPath}"`,
5800
+ timeout: 5
5801
+ }
5802
+ ]
5803
+ }
5804
+ ]
5805
+ };
5806
+ if (typeof opts.sessionStartPort === "number" && opts.sessionStartPort > 0) {
5807
+ const forwarderPath = join$1(hooksDir, `forwarder-${id}.cjs`);
5808
+ const forwarderCode = `#!/usr/bin/env node
5809
+ const http = require('http');
5810
+ const port = parseInt(process.argv[2], 10);
5811
+ if (!port || isNaN(port)) process.exit(1);
5812
+ const chunks = [];
5813
+ process.stdin.on('data', c => chunks.push(c));
5814
+ process.stdin.on('end', () => {
5815
+ const body = Buffer.concat(chunks);
5816
+ const req = http.request({
5817
+ host: '127.0.0.1', port, method: 'POST',
5818
+ path: '/hook/session-start',
5819
+ headers: { 'Content-Type': 'application/json', 'Content-Length': body.length }
5820
+ }, res => res.resume());
5821
+ req.on('error', () => {});
5822
+ req.end(body);
5823
+ });
5824
+ process.stdin.resume();
5825
+ `;
5826
+ writeFileSync$1(forwarderPath, forwarderCode, { mode: 493 });
5827
+ cleanupPaths.push(forwarderPath);
5828
+ hooks.SessionStart = [
5829
+ {
5830
+ matcher: "*",
5831
+ hooks: [
5832
+ {
5833
+ type: "command",
5834
+ command: `node "${forwarderPath}" ${opts.sessionStartPort}`
5835
+ }
5836
+ ]
5837
+ }
5838
+ ];
5839
+ }
5840
+ const settingsPath = join$1(hooksDir, `session-hook-${id}.json`);
5841
+ writeFileSync$1(settingsPath, JSON.stringify({ hooks }, null, 2));
5842
+ cleanupPaths.push(settingsPath);
5843
+ const cleanup = () => {
5844
+ for (const p of cleanupPaths) {
5845
+ try {
5846
+ if (existsSync(p)) unlinkSync(p);
5847
+ } catch {
5848
+ }
5849
+ }
5850
+ };
5851
+ return { settingsPath, validatorPath, hooksDir, cleanup };
5852
+ }
5853
+ const IMAGE_VALIDATOR_SCRIPT = `#!/usr/bin/env node
5854
+ 'use strict';
5855
+ const fs = require('node:fs');
5856
+
5857
+ const MAX_SIZE = 5 * 1024 * 1024;
5858
+ const IMAGE_EXTS = new Set(['.png', '.jpg', '.jpeg', '.gif', '.webp']);
5859
+
5860
+ function readStdin() {
5861
+ return new Promise((resolve) => {
5862
+ let data = '';
5863
+ process.stdin.setEncoding('utf-8');
5864
+ process.stdin.on('data', (c) => { data += c; });
5865
+ process.stdin.on('end', () => resolve(data));
5866
+ process.stdin.on('error', () => resolve(''));
5867
+ });
5868
+ }
5869
+
5870
+ function deny(reason) {
5871
+ process.stderr.write(reason);
5872
+ process.exit(2);
5873
+ }
5874
+
5875
+ function detectFormat(head) {
5876
+ if (head.length >= 8
5877
+ && head[0] === 0x89 && head[1] === 0x50 && head[2] === 0x4E && head[3] === 0x47
5878
+ && head[4] === 0x0D && head[5] === 0x0A && head[6] === 0x1A && head[7] === 0x0A) return 'png';
5879
+ if (head.length >= 3
5880
+ && head[0] === 0xFF && head[1] === 0xD8 && head[2] === 0xFF) return 'jpeg';
5881
+ if (head.length >= 4
5882
+ && head[0] === 0x47 && head[1] === 0x49 && head[2] === 0x46 && head[3] === 0x38) return 'gif';
5883
+ if (head.length >= 12
5884
+ && head[0] === 0x52 && head[1] === 0x49 && head[2] === 0x46 && head[3] === 0x46
5885
+ && head[8] === 0x57 && head[9] === 0x45 && head[10] === 0x42 && head[11] === 0x50) return 'webp';
5886
+ return null;
5887
+ }
5888
+
5889
+ function checkTruncation(format, tail, fileSize, head) {
5890
+ if (format === 'png') {
5891
+ const iend = Buffer.from([0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4E, 0x44, 0xAE, 0x42, 0x60, 0x82]);
5892
+ if (!tail.subarray(tail.length - 12).equals(iend)) {
5893
+ return 'PNG file is truncated or corrupt (missing IEND chunk).';
5894
+ }
5895
+ } else if (format === 'jpeg') {
5896
+ const last2 = tail.subarray(tail.length - 2);
5897
+ if (last2[0] !== 0xFF || last2[1] !== 0xD9) {
5898
+ return 'JPEG file is truncated (missing FF D9 EOI marker).';
5899
+ }
5900
+ } else if (format === 'gif') {
5901
+ if (tail[tail.length - 1] !== 0x3B) {
5902
+ return 'GIF file is truncated (missing 0x3B trailer).';
5903
+ }
5904
+ } else if (format === 'webp') {
5905
+ const riffSize = head.readUInt32LE(4);
5906
+ if (riffSize !== fileSize - 8) {
5907
+ return 'WebP file is corrupt (RIFF size ' + riffSize + ' != fileSize-8 ' + (fileSize - 8) + ').';
5908
+ }
5909
+ }
5910
+ return null;
5911
+ }
5912
+
5913
+ (async () => {
5914
+ const raw = await readStdin();
5915
+ let input;
5916
+ try { input = JSON.parse(raw); } catch { return; }
5917
+ if (!input || input.tool_name !== 'Read') return;
5918
+ const filePath = input.tool_input && input.tool_input.file_path;
5919
+ if (typeof filePath !== 'string' || !filePath) return;
5920
+
5921
+ const dot = filePath.lastIndexOf('.');
5922
+ if (dot < 0) return;
5923
+ const ext = filePath.slice(dot).toLowerCase();
5924
+ if (!IMAGE_EXTS.has(ext)) return;
5925
+
5926
+ let stat;
5927
+ try { stat = fs.statSync(filePath); } catch { return; } // missing \u2192 let Read tool report
5928
+ if (!stat.isFile()) return;
5929
+
5930
+ if (stat.size === 0) {
5931
+ deny('Image file is empty (0 bytes): ' + filePath + '. The Anthropic API will reject this image. Skip reading it.');
5932
+ }
5933
+ if (stat.size > MAX_SIZE) {
5934
+ deny('Image file is too large (' + (stat.size / 1024 / 1024).toFixed(1) + ' MB > 5 MB): ' + filePath + '. The Anthropic API rejects images larger than 5 MB. Skip reading it.');
5935
+ }
5936
+ if (stat.size < 12) {
5937
+ deny('Image file is too small to be a valid image (' + stat.size + ' bytes): ' + filePath + '. Skip reading it.');
5938
+ }
5939
+
5940
+ let head;
5941
+ let tail;
5942
+ try {
5943
+ const fd = fs.openSync(filePath, 'r');
5944
+ try {
5945
+ head = Buffer.alloc(32);
5946
+ const headLen = fs.readSync(fd, head, 0, 32, 0);
5947
+ head = head.subarray(0, headLen);
5948
+ const tailLen = Math.min(12, stat.size);
5949
+ tail = Buffer.alloc(tailLen);
5950
+ fs.readSync(fd, tail, 0, tailLen, stat.size - tailLen);
5951
+ } finally {
5952
+ fs.closeSync(fd);
5953
+ }
5954
+ } catch (err) {
5955
+ deny('Image file is unreadable: ' + filePath + ' \u2014 ' + (err && err.message || err) + '. Skip reading it.');
5956
+ }
5957
+
5958
+ const format = detectFormat(head);
5959
+ if (!format) {
5960
+ deny('Image file has invalid header \u2014 magic bytes do not match PNG/JPEG/GIF/WebP: ' + filePath + '. The file is corrupt or mislabeled. Skip reading it.');
5961
+ }
5962
+
5963
+ const expectedExts = { png: ['.png'], jpeg: ['.jpg', '.jpeg'], gif: ['.gif'], webp: ['.webp'] }[format];
5964
+ if (!expectedExts.includes(ext)) {
5965
+ deny('Image file extension "' + ext + '" does not match actual format "' + format + '": ' + filePath + '. Skip reading it.');
5966
+ }
5967
+
5968
+ const truncReason = checkTruncation(format, tail, stat.size, head);
5969
+ if (truncReason) {
5970
+ deny(truncReason + ' File: ' + filePath + '. Skip reading it.');
5971
+ }
5972
+ })().catch(() => { /* never block on validator errors */ });
5973
+ `;
5974
+
5680
5975
  const __filename$1 = fileURLToPath(import.meta.url);
5681
5976
  const __dirname$1 = dirname(__filename$1);
5682
- const CLAUDE_SKILLS_DIR = join(os.homedir(), ".claude", "skills");
5977
+ const CLAUDE_SKILLS_DIR = join(os$1.homedir(), ".claude", "skills");
5683
5978
  async function installSkillFromEndpoint(name, baseUrl) {
5684
5979
  const resp = await fetch(baseUrl, { signal: AbortSignal.timeout(15e3) });
5685
5980
  if (!resp.ok) throw new Error(`HTTP ${resp.status} from ${baseUrl}`);
@@ -5820,14 +6115,14 @@ function loadEnvFile(path) {
5820
6115
  return true;
5821
6116
  }
5822
6117
  function loadDotEnv() {
5823
- const svampEnv = join(process.env.SVAMP_HOME || os.homedir() + "/.svamp", ".env");
6118
+ const svampEnv = join(process.env.SVAMP_HOME || os$1.homedir() + "/.svamp", ".env");
5824
6119
  if (!loadEnvFile(svampEnv)) {
5825
- const hyphaEnv = join(process.env.HYPHA_HOME || os.homedir() + "/.hypha", ".env");
6120
+ const hyphaEnv = join(process.env.HYPHA_HOME || os$1.homedir() + "/.hypha", ".env");
5826
6121
  loadEnvFile(hyphaEnv);
5827
6122
  }
5828
6123
  }
5829
6124
  loadDotEnv();
5830
- const SVAMP_HOME = process.env.SVAMP_HOME || join(os.homedir(), ".svamp");
6125
+ const SVAMP_HOME = process.env.SVAMP_HOME || join(os$1.homedir(), ".svamp");
5831
6126
  const DAEMON_STATE_FILE = join(SVAMP_HOME, "daemon.state.json");
5832
6127
  const DAEMON_LOCK_FILE = join(SVAMP_HOME, "daemon.lock");
5833
6128
  const LOGS_DIR = join(SVAMP_HOME, "logs");
@@ -5941,7 +6236,7 @@ ${state.task}
5941
6236
  }
5942
6237
  function removeRalphState(filePath) {
5943
6238
  try {
5944
- unlinkSync(filePath);
6239
+ unlinkSync$1(filePath);
5945
6240
  } catch {
5946
6241
  }
5947
6242
  }
@@ -6313,27 +6608,27 @@ function deletePersistedSession(sessionId) {
6313
6608
  if (entry) {
6314
6609
  const sessionFile = getSessionFilePath(entry.directory, sessionId);
6315
6610
  try {
6316
- if (existsSync$1(sessionFile)) unlinkSync(sessionFile);
6611
+ if (existsSync$1(sessionFile)) unlinkSync$1(sessionFile);
6317
6612
  } catch {
6318
6613
  }
6319
6614
  const messagesFile = getSessionMessagesPath(entry.directory, sessionId);
6320
6615
  try {
6321
- if (existsSync$1(messagesFile)) unlinkSync(messagesFile);
6616
+ if (existsSync$1(messagesFile)) unlinkSync$1(messagesFile);
6322
6617
  } catch {
6323
6618
  }
6324
6619
  const configFile = getSvampConfigPath(entry.directory, sessionId);
6325
6620
  try {
6326
- if (existsSync$1(configFile)) unlinkSync(configFile);
6621
+ if (existsSync$1(configFile)) unlinkSync$1(configFile);
6327
6622
  } catch {
6328
6623
  }
6329
6624
  const ralphStateFile = getRalphStateFilePath(entry.directory, sessionId);
6330
6625
  try {
6331
- if (existsSync$1(ralphStateFile)) unlinkSync(ralphStateFile);
6626
+ if (existsSync$1(ralphStateFile)) unlinkSync$1(ralphStateFile);
6332
6627
  } catch {
6333
6628
  }
6334
6629
  const ralphProgressFile = getRalphProgressFilePath(entry.directory, sessionId);
6335
6630
  try {
6336
- if (existsSync$1(ralphProgressFile)) unlinkSync(ralphProgressFile);
6631
+ if (existsSync$1(ralphProgressFile)) unlinkSync$1(ralphProgressFile);
6337
6632
  } catch {
6338
6633
  }
6339
6634
  const sessionDir = getSessionDir(entry.directory, sessionId);
@@ -6603,7 +6898,7 @@ async function startDaemon(options) {
6603
6898
  machineId = readFileSync$1(machineIdFile, "utf-8").trim();
6604
6899
  }
6605
6900
  if (!machineId) {
6606
- machineId = `machine-${os.hostname()}-${randomUUID$1().slice(0, 8)}`;
6901
+ machineId = `machine-${os$1.hostname()}-${randomUUID$1().slice(0, 8)}`;
6607
6902
  try {
6608
6903
  writeFileSync(machineIdFile, machineId, "utf-8");
6609
6904
  } catch {
@@ -6618,7 +6913,7 @@ async function startDaemon(options) {
6618
6913
  const supervisor = new ProcessSupervisor(join(SVAMP_HOME, "processes"));
6619
6914
  await supervisor.init();
6620
6915
  const tunnels = /* @__PURE__ */ new Map();
6621
- const { ServeManager } = await import('./serveManager-DOXI2QzY.mjs');
6916
+ const { ServeManager } = await import('./serveManager-RvRL-weX.mjs');
6622
6917
  const serveManager = new ServeManager(SVAMP_HOME, (msg) => logger.log(`[SERVE] ${msg}`), hyphaServerUrl);
6623
6918
  ensureAutoInstalledSkills(logger).catch(() => {
6624
6919
  });
@@ -6708,6 +7003,7 @@ async function startDaemon(options) {
6708
7003
  return await spawnAgentSession(sessionId, directory, agentName, options2, resumeSessionId);
6709
7004
  }
6710
7005
  let stagedCredentials = null;
7006
+ let hookSettings = null;
6711
7007
  try {
6712
7008
  let parseBashPermission2 = function(permission) {
6713
7009
  if (permission === "Bash") return;
@@ -6792,10 +7088,10 @@ async function startDaemon(options) {
6792
7088
  var parseBashPermission = parseBashPermission2, shouldAutoAllow = shouldAutoAllow2, killAndWaitForExit = killAndWaitForExit2, buildIsolationConfig = buildIsolationConfig2;
6793
7089
  let sessionMetadata = {
6794
7090
  path: directory,
6795
- host: os.hostname(),
7091
+ host: os$1.hostname(),
6796
7092
  version: "0.1.0",
6797
7093
  machineId,
6798
- homeDir: os.homedir(),
7094
+ homeDir: os$1.homedir(),
6799
7095
  svampHomeDir: SVAMP_HOME,
6800
7096
  svampLibDir: join(__dirname$1, ".."),
6801
7097
  svampToolsDir: join(__dirname$1, "..", "tools"),
@@ -6916,10 +7212,22 @@ async function startDaemon(options) {
6916
7212
  let isSwitchingMode = false;
6917
7213
  let checkSvampConfig;
6918
7214
  let cleanupSvampConfig;
7215
+ const VALID_CLAUDE_PERMISSION_MODES = /* @__PURE__ */ new Set(["default", "acceptEdits", "plan", "bypassPermissions"]);
6919
7216
  const CLAUDE_PERMISSION_MODE_MAP = {
6920
- "auto-approve-all": "bypassPermissions"
7217
+ "auto-approve-all": "bypassPermissions",
7218
+ "yolo": "bypassPermissions",
7219
+ "safe-yolo": "acceptEdits",
7220
+ "read-only": "plan"
7221
+ };
7222
+ const toClaudePermissionMode = (mode) => {
7223
+ if (!mode) return "default";
7224
+ const mapped = CLAUDE_PERMISSION_MODE_MAP[mode] ?? mode;
7225
+ if (!VALID_CLAUDE_PERMISSION_MODES.has(mapped)) {
7226
+ logger.log(`[Session ${sessionId}] Unknown permission mode '${mode}' \u2014 falling back to 'default'`);
7227
+ return "default";
7228
+ }
7229
+ return mapped;
6921
7230
  };
6922
- const toClaudePermissionMode = (mode) => CLAUDE_PERMISSION_MODE_MAP[mode] || mode;
6923
7231
  const getActiveSecurityContext = () => sessionMetadata.securityContext ?? options2.securityContext;
6924
7232
  const shouldIsolateSession = () => shouldIsolate({
6925
7233
  forceIsolation: options2.forceIsolation,
@@ -6956,11 +7264,28 @@ async function startDaemon(options) {
6956
7264
  if (model) args.push("--model", model);
6957
7265
  if (appendSystemPrompt) args.push("--append-system-prompt", appendSystemPrompt);
6958
7266
  if (claudeResumeId) args.push("--resume", claudeResumeId);
7267
+ if (!hookSettings) {
7268
+ try {
7269
+ hookSettings = generateHookSettings({ id: sessionId });
7270
+ logger.log(`[Session ${sessionId}] Hook settings: ${hookSettings.settingsPath} (validator: ${hookSettings.validatorPath})`);
7271
+ } catch (err) {
7272
+ logger.log(`[Session ${sessionId}] Failed to generate hook settings: ${err?.message || err}`);
7273
+ }
7274
+ }
7275
+ if (hookSettings) args.push("--settings", hookSettings.settingsPath);
6959
7276
  let spawnCommand = "claude";
6960
7277
  let spawnArgs = args;
6961
7278
  let extraEnv = {};
6962
7279
  isolationCleanupFiles = [];
6963
7280
  const isoConfig = buildIsolationConfig2(directory);
7281
+ if (isoConfig && hookSettings) {
7282
+ if (isoConfig.method === "nono") {
7283
+ isoConfig.nonoConfig = isoConfig.nonoConfig || {};
7284
+ const readFiles = isoConfig.nonoConfig.readFiles || [];
7285
+ readFiles.push(hookSettings.validatorPath, hookSettings.settingsPath);
7286
+ isoConfig.nonoConfig.readFiles = readFiles;
7287
+ }
7288
+ }
6964
7289
  if (isoConfig) {
6965
7290
  const wrapped = wrapWithIsolation(spawnCommand, spawnArgs, isoConfig);
6966
7291
  spawnCommand = wrapped.command;
@@ -7316,7 +7641,7 @@ ${progressContent}
7316
7641
  </system-reminder>
7317
7642
 
7318
7643
  The automated loop has finished. Review the progress above and let me know if you need anything else.`;
7319
- unlinkSync(progressPath);
7644
+ unlinkSync$1(progressPath);
7320
7645
  logger.log(`[Session ${sessionId}] Injected progress file content and deleted ${progressPath}`);
7321
7646
  }
7322
7647
  }
@@ -7663,12 +7988,12 @@ The automated loop has finished. Review the progress above and let me know if yo
7663
7988
  } catch {
7664
7989
  text = typeof content === "string" ? content : JSON.stringify(content);
7665
7990
  }
7666
- if (msgMeta?.permissionMode) {
7667
- currentPermissionMode = toClaudePermissionMode(msgMeta.permissionMode);
7668
- logger.log(`[Session ${sessionId}] Permission mode updated to: ${currentPermissionMode}`);
7669
- }
7670
7991
  if (msgMeta) {
7671
- lastSpawnMeta = { ...lastSpawnMeta, ...msgMeta };
7992
+ const { permissionMode: incomingPermissionMode, ...safeMsgMeta } = msgMeta;
7993
+ if (incomingPermissionMode && toClaudePermissionMode(incomingPermissionMode) !== currentPermissionMode) {
7994
+ logger.log(`[Session ${sessionId}] Ignoring meta.permissionMode='${incomingPermissionMode}' from sendMessage (current: ${currentPermissionMode}; use switchMode to change)`);
7995
+ }
7996
+ lastSpawnMeta = { ...lastSpawnMeta, ...safeMsgMeta };
7672
7997
  }
7673
7998
  if (isKillingClaude || isRestartingClaude || isSwitchingMode) {
7674
7999
  logger.log(`[Session ${sessionId}] Message received while restarting Claude, queuing to prevent loss`);
@@ -7832,12 +8157,13 @@ The automated loop has finished. Review the progress above and let me know if yo
7832
8157
  }
7833
8158
  },
7834
8159
  onSwitchMode: async (mode) => {
7835
- logger.log(`[Session ${sessionId}] Switch mode: ${mode}`);
8160
+ const normalizedMode = toClaudePermissionMode(mode);
8161
+ logger.log(`[Session ${sessionId}] Switch mode: ${mode}${mode !== normalizedMode ? ` \u2192 ${normalizedMode}` : ""}`);
7836
8162
  if (isRestartingClaude || isSwitchingMode) {
7837
8163
  logger.log(`[Session ${sessionId}] Switch mode deferred \u2014 restart/switch already in progress`);
7838
8164
  return;
7839
8165
  }
7840
- currentPermissionMode = mode;
8166
+ currentPermissionMode = normalizedMode;
7841
8167
  if (claudeProcess && claudeProcess.exitCode === null) {
7842
8168
  isSwitchingMode = true;
7843
8169
  isKillingClaude = true;
@@ -7845,7 +8171,7 @@ The automated loop has finished. Review the progress above and let me know if yo
7845
8171
  await killAndWaitForExit2(claudeProcess);
7846
8172
  isKillingClaude = false;
7847
8173
  if (trackedSession?.stopped) return;
7848
- spawnClaude(void 0, { permissionMode: mode });
8174
+ spawnClaude(void 0, { permissionMode: normalizedMode });
7849
8175
  } finally {
7850
8176
  isKillingClaude = false;
7851
8177
  isSwitchingMode = false;
@@ -8142,6 +8468,12 @@ The automated loop has finished. Review the progress above and let me know if yo
8142
8468
  resumeSessionId: claudeResumeId,
8143
8469
  restartAgent: restartClaudeHandler,
8144
8470
  cleanupCredentials: stagedCredentials?.cleanup,
8471
+ // Closure: hookSettings is generated lazily on first spawnClaude,
8472
+ // after this trackedSession is registered, so resolve the current
8473
+ // value at cleanup time rather than capturing null now.
8474
+ cleanupHookSettings: () => {
8475
+ hookSettings?.cleanup();
8476
+ },
8145
8477
  get childProcess() {
8146
8478
  return claudeProcess || void 0;
8147
8479
  }
@@ -8161,6 +8493,8 @@ The automated loop has finished. Review the progress above and let me know if yo
8161
8493
  stagedCredentials.cleanup().catch(() => {
8162
8494
  });
8163
8495
  }
8496
+ const hs = hookSettings;
8497
+ if (hs) hs.cleanup();
8164
8498
  return {
8165
8499
  type: "error",
8166
8500
  errorMessage: `Failed to register session service: ${err?.message || String(err)}`
@@ -8200,10 +8534,10 @@ The automated loop has finished. Review the progress above and let me know if yo
8200
8534
  var parseBashPermission = parseBashPermission2, shouldAutoAllow = shouldAutoAllow2;
8201
8535
  let sessionMetadata = {
8202
8536
  path: directory,
8203
- host: os.hostname(),
8537
+ host: os$1.hostname(),
8204
8538
  version: "0.1.0",
8205
8539
  machineId,
8206
- homeDir: os.homedir(),
8540
+ homeDir: os$1.homedir(),
8207
8541
  svampHomeDir: SVAMP_HOME,
8208
8542
  svampLibDir: join(__dirname$1, ".."),
8209
8543
  svampToolsDir: join(__dirname$1, "..", "tools"),
@@ -8246,8 +8580,8 @@ The automated loop has finished. Review the progress above and let me know if yo
8246
8580
  } catch {
8247
8581
  text = typeof content === "string" ? content : JSON.stringify(content);
8248
8582
  }
8249
- if (msgMeta?.permissionMode) {
8250
- currentPermissionMode = msgMeta.permissionMode;
8583
+ if (msgMeta?.permissionMode && msgMeta.permissionMode !== currentPermissionMode) {
8584
+ logger.log(`[${agentName} Session ${sessionId}] Ignoring meta.permissionMode='${msgMeta.permissionMode}' from sendMessage (current: ${currentPermissionMode}; use switchMode to change)`);
8251
8585
  }
8252
8586
  if (!acpBackendReady) {
8253
8587
  logger.log(`[${agentName} Session ${sessionId}] Backend not ready \u2014 queuing message`);
@@ -8792,6 +9126,7 @@ The automated loop has finished. Review the progress above and let me know if yo
8792
9126
  }
8793
9127
  session.cleanupCredentials?.().catch(() => {
8794
9128
  });
9129
+ session.cleanupHookSettings?.();
8795
9130
  session.cleanupSvampConfig?.();
8796
9131
  artifactSync.cancelSync(sessionId);
8797
9132
  pidToTrackedSession.delete(pid);
@@ -8824,14 +9159,14 @@ The automated loop has finished. Review the progress above and let me know if yo
8824
9159
  logger.log(`Failed to detect isolation capabilities: ${err}`);
8825
9160
  isolationCapabilities = { available: [], preferred: null, details: { nono: { found: false }, srt: { found: false }, bwrap: { found: false }, docker: { found: false }, podman: { found: false } } };
8826
9161
  }
8827
- const defaultHomeDir = existsSync$1("/data") ? "/data" : os.homedir();
9162
+ const defaultHomeDir = existsSync$1("/data") ? "/data" : os$1.homedir();
8828
9163
  const persistedMachineMeta = loadPersistedMachineMetadata(SVAMP_HOME);
8829
9164
  if (persistedMachineMeta) {
8830
9165
  logger.log(`Restored machine metadata (sharing=${!!persistedMachineMeta.sharing}, securityContextConfig=${!!persistedMachineMeta.securityContextConfig}, injectPlatformGuidance=${persistedMachineMeta.injectPlatformGuidance})`);
8831
9166
  }
8832
9167
  const machineMetadata = {
8833
- host: os.hostname(),
8834
- platform: os.platform(),
9168
+ host: os$1.hostname(),
9169
+ platform: os$1.platform(),
8835
9170
  svampVersion: "0.1.0 (hypha)",
8836
9171
  homeDir: defaultHomeDir,
8837
9172
  svampHomeDir: SVAMP_HOME,
@@ -9111,6 +9446,7 @@ The automated loop has finished. Review the progress above and let me know if yo
9111
9446
  });
9112
9447
  session.cleanupCredentials?.().catch(() => {
9113
9448
  });
9449
+ session.cleanupHookSettings?.();
9114
9450
  session.cleanupSvampConfig?.();
9115
9451
  if (session.svampSessionId) artifactSync.cancelSync(session.svampSessionId);
9116
9452
  pidToTrackedSession.delete(key);
@@ -9185,18 +9521,32 @@ The automated loop has finished. Review the progress above and let me know if yo
9185
9521
  }
9186
9522
  }
9187
9523
  }
9188
- const FRPC_FAILING_THRESHOLD_MS = 5 * 6e4;
9524
+ const FRPC_FAILING_THRESHOLD_MS = 2 * 6e4;
9525
+ const PROBE_STALENESS_THRESHOLD_MS = 2 * 6e4;
9526
+ const tunnelLooksDead = (h) => {
9527
+ if (h.failingDurationMs > FRPC_FAILING_THRESHOLD_MS) {
9528
+ return `failing ${Math.round(h.failingDurationMs / 1e3)}s`;
9529
+ }
9530
+ if (h.probe && !h.probe.ok && h.probe.stalenessMs > PROBE_STALENESS_THRESHOLD_MS) {
9531
+ return `probe stale ${Math.round(h.probe.stalenessMs / 1e3)}s`;
9532
+ }
9533
+ return null;
9534
+ };
9189
9535
  const serveHealth = serveManager.getTunnelHealth();
9190
- if (serveHealth && serveHealth.failingDurationMs > FRPC_FAILING_THRESHOLD_MS) {
9191
- logger.log(`Serve manager tunnel failing for ${Math.round(serveHealth.failingDurationMs / 1e3)}s (${serveHealth.consecutiveErrors} errors, ${serveHealth.restartAttempts} restarts) \u2014 recreating`);
9192
- serveManager.recreateTunnel().catch((err) => {
9193
- logger.log(`Failed to recreate serve tunnel: ${err.message}`);
9194
- });
9536
+ if (serveHealth) {
9537
+ const reason = tunnelLooksDead(serveHealth);
9538
+ if (reason) {
9539
+ logger.log(`Serve manager tunnel ${reason} (${serveHealth.consecutiveErrors} errors, ${serveHealth.restartAttempts} restarts) \u2014 recreating`);
9540
+ serveManager.recreateTunnel().catch((err) => {
9541
+ logger.log(`Failed to recreate serve tunnel: ${err.message}`);
9542
+ });
9543
+ }
9195
9544
  }
9196
9545
  for (const [name, tunnel] of tunnels) {
9197
9546
  const health = tunnel.status;
9198
- if (health.failingDurationMs > FRPC_FAILING_THRESHOLD_MS) {
9199
- logger.log(`frpc tunnel '${name}' failing for ${Math.round(health.failingDurationMs / 1e3)}s \u2014 destroying stale tunnel`);
9547
+ const reason = tunnelLooksDead(health);
9548
+ if (reason) {
9549
+ logger.log(`frpc tunnel '${name}' ${reason} \u2014 destroying stale tunnel`);
9200
9550
  tunnel.destroy();
9201
9551
  tunnels.delete(name);
9202
9552
  }
@@ -9533,7 +9883,7 @@ async function restartDaemon() {
9533
9883
  function daemonStatus() {
9534
9884
  const state = readDaemonStateFile();
9535
9885
  if (!state) {
9536
- const plistPath = join(os.homedir(), "Library", "LaunchAgents", "io.hypha.svamp.daemon.plist");
9886
+ const plistPath = join(os$1.homedir(), "Library", "LaunchAgents", "io.hypha.svamp.daemon.plist");
9537
9887
  if (existsSync$1(plistPath)) {
9538
9888
  console.log("Status: Not running (launchd service installed \u2014 may be starting)");
9539
9889
  } else {
@@ -9572,4 +9922,4 @@ var run = /*#__PURE__*/Object.freeze({
9572
9922
  stopDaemon: stopDaemon
9573
9923
  });
9574
9924
 
9575
- export { DefaultTransport$1 as D, GeminiTransport$1 as G, registerSessionService as a, stopDaemon as b, connectToHypha as c, daemonStatus as d, resolveSecurityContext as e, buildSecurityContextFromFlags as f, getHyphaServerUrl as g, acpBackend as h, acpAgentConfig as i, codexMcpBackend as j, claudeAuth as k, loadSecurityContextConfig as l, mergeSecurityContexts as m, run as n, registerMachineService as r, startDaemon as s };
9925
+ export { DefaultTransport$1 as D, GeminiTransport$1 as G, registerSessionService as a, stopDaemon as b, connectToHypha as c, daemonStatus as d, resolveSecurityContext as e, buildSecurityContextFromFlags as f, getHyphaServerUrl as g, generateHookSettings as h, acpBackend as i, acpAgentConfig as j, codexMcpBackend as k, loadSecurityContextConfig as l, mergeSecurityContexts as m, claudeAuth as n, run as o, registerMachineService as r, startDaemon as s };