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.
- package/dist/{agentCommands-dlpOoDcq.mjs → agentCommands-BuGwfYhd.mjs} +2 -2
- package/dist/cli.mjs +32 -32
- package/dist/{commands-0xDVhPKr.mjs → commands-BJR_98XX.mjs} +12 -3
- package/dist/{commands-Cd_I1MXo.mjs → commands-JWrmpGcs.mjs} +1 -1
- package/dist/{commands-C6D6TMSl.mjs → commands-TyAIFJx-.mjs} +4 -4
- package/dist/{frpc-DzRFx60H.mjs → frpc-j60b46eU.mjs} +120 -4
- package/dist/index.mjs +1 -1
- package/dist/{package-Cx2tEoke.mjs → package-CNFS7wvh.mjs} +1 -1
- package/dist/{run-D59qJKn_.mjs → run-6umeTX-K.mjs} +411 -61
- package/dist/{run-DZhogQUH.mjs → run-DR7E3IZL.mjs} +3 -53
- package/dist/{serveCommands-DtKlt1DY.mjs → serveCommands-FUE8m232.mjs} +107 -4
- package/dist/{serveManager-DOXI2QzY.mjs → serveManager-RvRL-weX.mjs} +284 -28
- package/package.json +1 -1
|
@@ -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-
|
|
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
|
-
|
|
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.
|
|
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(
|
|
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.
|
|
5593
|
+
if (entry.deleted) return;
|
|
5495
5594
|
if (entry.stopping) return;
|
|
5496
|
-
|
|
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
|
-
|
|
5499
|
-
|
|
5500
|
-
|
|
5501
|
-
|
|
5502
|
-
|
|
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-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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:
|
|
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
|
-
|
|
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 =
|
|
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
|
|
9191
|
-
|
|
9192
|
-
|
|
9193
|
-
logger.log(`
|
|
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
|
-
|
|
9199
|
-
|
|
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,
|
|
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 };
|