svamp-cli 0.1.64 → 0.1.66
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-C6iGblcL.mjs +156 -0
- package/dist/cli.mjs +272 -172
- package/dist/{commands-BfMlD9o4.mjs → commands-CdQ8qr6l.mjs} +2 -2
- package/dist/{commands-Brx7D-77.mjs → commands-Dpc6QK0m.mjs} +1 -1
- package/dist/index.mjs +1 -1
- package/dist/{package-Dg0hQJRC.mjs → package-CSFQKq7F.mjs} +2 -2
- package/dist/{run-BImPgXHd.mjs → run-BDmLH9T8.mjs} +126 -29
- package/dist/{run-C7VxH4X8.mjs → run-ez-QlRKy.mjs} +1 -1
- package/dist/storageCommands-CKhntx1P.mjs +154 -0
- package/package.json +3 -3
|
@@ -2,7 +2,7 @@ import { existsSync, readFileSync } from 'node:fs';
|
|
|
2
2
|
import { execSync } from 'node:child_process';
|
|
3
3
|
import { resolve, join } from 'node:path';
|
|
4
4
|
import os from 'node:os';
|
|
5
|
-
import { l as loadSecurityContextConfig, e as resolveSecurityContext, f as buildSecurityContextFromFlags, m as mergeSecurityContexts, c as connectToHypha } from './run-
|
|
5
|
+
import { l as loadSecurityContextConfig, e as resolveSecurityContext, f as buildSecurityContextFromFlags, m as mergeSecurityContexts, c as connectToHypha } from './run-BDmLH9T8.mjs';
|
|
6
6
|
import 'os';
|
|
7
7
|
import 'fs/promises';
|
|
8
8
|
import 'fs';
|
package/dist/index.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { c as connectToHypha, d as daemonStatus, g as getHyphaServerUrl, r as registerMachineService, a as registerSessionService, s as startDaemon, b as stopDaemon } from './run-
|
|
1
|
+
export { c as connectToHypha, d as daemonStatus, g as getHyphaServerUrl, r as registerMachineService, a as registerSessionService, s as startDaemon, b as stopDaemon } from './run-BDmLH9T8.mjs';
|
|
2
2
|
import 'os';
|
|
3
3
|
import 'fs/promises';
|
|
4
4
|
import 'fs';
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
var name = "svamp-cli";
|
|
2
|
-
var version = "0.1.
|
|
2
|
+
var version = "0.1.66";
|
|
3
3
|
var description = "Svamp CLI — AI workspace daemon on Hypha Cloud";
|
|
4
4
|
var author = "Amun AI AB";
|
|
5
5
|
var license = "SEE LICENSE IN LICENSE";
|
|
@@ -19,7 +19,7 @@ var exports$1 = {
|
|
|
19
19
|
var scripts = {
|
|
20
20
|
build: "rm -rf dist && tsc --noEmit && pkgroll",
|
|
21
21
|
typecheck: "tsc --noEmit",
|
|
22
|
-
test: "npx tsx test/test-authorize.mjs && npx tsx test/test-session-helpers.mjs && npx tsx test/test-cli-routing.mjs && npx tsx test/test-security-context.mjs && npx tsx test/test-ralph-loop.mjs && npx tsx test/test-message-helpers.mjs && npx tsx test/test-agent-config.mjs && npx tsx test/test-wrap-command.mjs && npx tsx test/test-credential-staging.mjs && npx tsx test/test-output-formatters.mjs && npx tsx test/test-agent-types.mjs && npx tsx test/test-transport.mjs && npx tsx test/test-session-update-handlers.mjs && npx tsx test/test-session-scanner.mjs && npx tsx test/test-hypha-client.mjs && npx tsx test/test-hook-settings.mjs && npx tsx test/test-session-service-logic.mjs && npx tsx test/test-daemon-persistence.mjs && npx tsx test/test-detect-isolation.mjs && npx tsx test/test-machine-service-logic.mjs && npx tsx test/test-interactive-helpers.mjs && npx tsx test/test-codex-backend.mjs && npx tsx test/test-acp-backend.mjs && npx tsx test/test-acp-bridge.mjs && npx tsx test/test-hook-server.mjs && npx tsx test/test-session-commands.mjs && npx tsx test/test-interactive-console.mjs && npx tsx test/test-session-messages.mjs && npx tsx test/test-skills.mjs && npx tsx test/test-agent-grouping.mjs && npx tsx test/test-ralph-loop-integration.mjs && npx tsx test/test-ralph-loop-modes.mjs && npx tsx test/test-machine-list-directory.mjs && npx tsx test/test-service-commands.mjs && npx tsx test/test-supervisor.mjs",
|
|
22
|
+
test: "npx tsx test/test-authorize.mjs && npx tsx test/test-session-helpers.mjs && npx tsx test/test-cli-routing.mjs && npx tsx test/test-security-context.mjs && npx tsx test/test-ralph-loop.mjs && npx tsx test/test-message-helpers.mjs && npx tsx test/test-agent-config.mjs && npx tsx test/test-wrap-command.mjs && npx tsx test/test-credential-staging.mjs && npx tsx test/test-output-formatters.mjs && npx tsx test/test-agent-types.mjs && npx tsx test/test-transport.mjs && npx tsx test/test-session-update-handlers.mjs && npx tsx test/test-session-scanner.mjs && npx tsx test/test-hypha-client.mjs && npx tsx test/test-hook-settings.mjs && npx tsx test/test-session-service-logic.mjs && npx tsx test/test-daemon-persistence.mjs && npx tsx test/test-detect-isolation.mjs && npx tsx test/test-machine-service-logic.mjs && npx tsx test/test-interactive-helpers.mjs && npx tsx test/test-codex-backend.mjs && npx tsx test/test-acp-backend.mjs && npx tsx test/test-acp-bridge.mjs && npx tsx test/test-hook-server.mjs && npx tsx test/test-session-commands.mjs && npx tsx test/test-interactive-console.mjs && npx tsx test/test-session-messages.mjs && npx tsx test/test-skills.mjs && npx tsx test/test-agent-grouping.mjs && npx tsx test/test-ralph-loop-integration.mjs && npx tsx test/test-ralph-loop-modes.mjs && npx tsx test/test-machine-list-directory.mjs && npx tsx test/test-service-commands.mjs && npx tsx test/test-supervisor.mjs && npx tsx test/test-clear-detection.mjs",
|
|
23
23
|
"test:hypha": "node --no-warnings test/test-hypha-service.mjs",
|
|
24
24
|
dev: "tsx src/cli.ts",
|
|
25
25
|
"dev:daemon": "tsx src/cli.ts daemon start-sync",
|
|
@@ -385,21 +385,22 @@ async function registerMachineService(server, machineId, metadata, daemonState,
|
|
|
385
385
|
const machineOwner = currentMetadata.sharing?.owner;
|
|
386
386
|
const isSharedUser = callerEmail && machineOwner && callerEmail.toLowerCase() !== machineOwner.toLowerCase();
|
|
387
387
|
if (isSharedUser) {
|
|
388
|
-
const machineUser = currentMetadata.sharing?.allowedUsers?.find(
|
|
389
|
-
(u) => u.email.toLowerCase() === callerEmail.toLowerCase()
|
|
390
|
-
);
|
|
391
|
-
const callerRole = machineUser?.role || "interact";
|
|
392
388
|
const sharing = {
|
|
393
389
|
enabled: true,
|
|
394
|
-
owner:
|
|
390
|
+
owner: callerEmail,
|
|
391
|
+
// spawning user owns their session
|
|
395
392
|
allowedUsers: [
|
|
396
|
-
|
|
393
|
+
// Machine owner gets admin access (can monitor/control sessions on their machine)
|
|
397
394
|
{
|
|
398
|
-
email:
|
|
399
|
-
role:
|
|
395
|
+
email: machineOwner,
|
|
396
|
+
role: "admin",
|
|
400
397
|
addedAt: Date.now(),
|
|
401
398
|
addedBy: "machine-auto"
|
|
402
|
-
}
|
|
399
|
+
},
|
|
400
|
+
// Preserve any explicitly requested allowedUsers (e.g. additional collaborators)
|
|
401
|
+
...(options.sharing?.allowedUsers || []).filter(
|
|
402
|
+
(u) => u.email.toLowerCase() !== machineOwner.toLowerCase()
|
|
403
|
+
)
|
|
403
404
|
]
|
|
404
405
|
};
|
|
405
406
|
options = { ...options, sharing };
|
|
@@ -419,16 +420,11 @@ async function registerMachineService(server, machineId, metadata, daemonState,
|
|
|
419
420
|
...options,
|
|
420
421
|
securityContext: mergeSecurityContexts(machineCtx, options.securityContext)
|
|
421
422
|
};
|
|
422
|
-
if (machineCtx.role && options.sharing?.enabled) {
|
|
423
|
-
const user = options.sharing.allowedUsers?.find(
|
|
424
|
-
(u) => u.email.toLowerCase() === callerEmail.toLowerCase()
|
|
425
|
-
);
|
|
426
|
-
if (user && !user.role) {
|
|
427
|
-
user.role = machineCtx.role;
|
|
428
|
-
}
|
|
429
|
-
}
|
|
430
423
|
}
|
|
431
424
|
}
|
|
425
|
+
if (options.injectPlatformGuidance === void 0 && currentMetadata.injectPlatformGuidance !== void 0) {
|
|
426
|
+
options = { ...options, injectPlatformGuidance: currentMetadata.injectPlatformGuidance };
|
|
427
|
+
}
|
|
432
428
|
const result = await handlers.spawnSession({
|
|
433
429
|
...options,
|
|
434
430
|
machineId
|
|
@@ -486,7 +482,8 @@ async function registerMachineService(server, machineId, metadata, daemonState,
|
|
|
486
482
|
metadataVersion++;
|
|
487
483
|
savePersistedMachineMetadata(metadata.svampHomeDir, {
|
|
488
484
|
sharing: currentMetadata.sharing,
|
|
489
|
-
securityContextConfig: currentMetadata.securityContextConfig
|
|
485
|
+
securityContextConfig: currentMetadata.securityContextConfig,
|
|
486
|
+
injectPlatformGuidance: currentMetadata.injectPlatformGuidance
|
|
490
487
|
});
|
|
491
488
|
notifyListeners({
|
|
492
489
|
type: "update-machine",
|
|
@@ -546,7 +543,8 @@ async function registerMachineService(server, machineId, metadata, daemonState,
|
|
|
546
543
|
metadataVersion++;
|
|
547
544
|
savePersistedMachineMetadata(metadata.svampHomeDir, {
|
|
548
545
|
sharing: currentMetadata.sharing,
|
|
549
|
-
securityContextConfig: currentMetadata.securityContextConfig
|
|
546
|
+
securityContextConfig: currentMetadata.securityContextConfig,
|
|
547
|
+
injectPlatformGuidance: currentMetadata.injectPlatformGuidance
|
|
550
548
|
});
|
|
551
549
|
notifyListeners({
|
|
552
550
|
type: "update-machine",
|
|
@@ -567,7 +565,8 @@ async function registerMachineService(server, machineId, metadata, daemonState,
|
|
|
567
565
|
metadataVersion++;
|
|
568
566
|
savePersistedMachineMetadata(metadata.svampHomeDir, {
|
|
569
567
|
sharing: currentMetadata.sharing,
|
|
570
|
-
securityContextConfig: currentMetadata.securityContextConfig
|
|
568
|
+
securityContextConfig: currentMetadata.securityContextConfig,
|
|
569
|
+
injectPlatformGuidance: currentMetadata.injectPlatformGuidance
|
|
571
570
|
});
|
|
572
571
|
notifyListeners({
|
|
573
572
|
type: "update-machine",
|
|
@@ -3754,13 +3753,17 @@ async function verifyNonoIsolation(binaryPath) {
|
|
|
3754
3753
|
"-s",
|
|
3755
3754
|
"--allow",
|
|
3756
3755
|
workDir,
|
|
3757
|
-
|
|
3756
|
+
// NOTE: Do NOT add --allow-cwd here. If the daemon's CWD happens to be
|
|
3757
|
+
// $HOME (common when started interactively), --allow-cwd would grant
|
|
3758
|
+
// access to $HOME, allowing the probe file write to succeed and making
|
|
3759
|
+
// verification incorrectly fail ("file leaked to host filesystem").
|
|
3760
|
+
// We already grant --allow workDir explicitly, so --allow-cwd is redundant.
|
|
3758
3761
|
"--trust-override",
|
|
3759
3762
|
"--",
|
|
3760
3763
|
"sh",
|
|
3761
3764
|
"-c",
|
|
3762
3765
|
testScript
|
|
3763
|
-
], { timeout: 15e3 });
|
|
3766
|
+
], { timeout: 15e3, cwd: workDir });
|
|
3764
3767
|
return parseIsolationTestOutput(stdout, probeFile);
|
|
3765
3768
|
} catch (e) {
|
|
3766
3769
|
return { passed: false, error: e.message };
|
|
@@ -4408,6 +4411,83 @@ class ProcessSupervisor {
|
|
|
4408
4411
|
|
|
4409
4412
|
const __filename$1 = fileURLToPath(import.meta.url);
|
|
4410
4413
|
const __dirname$1 = dirname(__filename$1);
|
|
4414
|
+
const CLAUDE_SKILLS_DIR = join(os__default.homedir(), ".claude", "skills");
|
|
4415
|
+
async function installSkillFromEndpoint(name, baseUrl) {
|
|
4416
|
+
const resp = await fetch(baseUrl, { signal: AbortSignal.timeout(15e3) });
|
|
4417
|
+
if (!resp.ok) throw new Error(`HTTP ${resp.status} from ${baseUrl}`);
|
|
4418
|
+
const index = await resp.json();
|
|
4419
|
+
const files = index.files || [];
|
|
4420
|
+
if (files.length === 0) throw new Error(`Skill index at ${baseUrl} has no files`);
|
|
4421
|
+
const targetDir = join(CLAUDE_SKILLS_DIR, name);
|
|
4422
|
+
mkdirSync(targetDir, { recursive: true });
|
|
4423
|
+
for (const filePath of files) {
|
|
4424
|
+
if (!filePath) continue;
|
|
4425
|
+
const url = `${baseUrl}${filePath}`;
|
|
4426
|
+
const fileResp = await fetch(url, { signal: AbortSignal.timeout(3e4) });
|
|
4427
|
+
if (!fileResp.ok) throw new Error(`Failed to download ${filePath}: HTTP ${fileResp.status}`);
|
|
4428
|
+
const content = await fileResp.text();
|
|
4429
|
+
const localPath = join(targetDir, filePath);
|
|
4430
|
+
if (!localPath.startsWith(targetDir + "/")) continue;
|
|
4431
|
+
mkdirSync(dirname(localPath), { recursive: true });
|
|
4432
|
+
writeFileSync(localPath, content, "utf-8");
|
|
4433
|
+
}
|
|
4434
|
+
}
|
|
4435
|
+
async function installSkillFromMarketplace(name) {
|
|
4436
|
+
const BASE = `https://hypha.aicell.io/hypha-cloud/artifacts/${name}`;
|
|
4437
|
+
async function collectFiles(dir = "") {
|
|
4438
|
+
const url = dir ? `${BASE}/files/${dir}` : `${BASE}/files/`;
|
|
4439
|
+
const resp = await fetch(url, { signal: AbortSignal.timeout(15e3) });
|
|
4440
|
+
if (!resp.ok) throw new Error(`HTTP ${resp.status} listing files`);
|
|
4441
|
+
const data = await resp.json();
|
|
4442
|
+
const items = Array.isArray(data) ? data : data.items || [];
|
|
4443
|
+
const result = [];
|
|
4444
|
+
for (const item of items) {
|
|
4445
|
+
const itemPath = dir ? `${dir}/${item.name}` : item.name;
|
|
4446
|
+
if (item.type === "directory") {
|
|
4447
|
+
result.push(...await collectFiles(itemPath));
|
|
4448
|
+
} else {
|
|
4449
|
+
result.push(itemPath);
|
|
4450
|
+
}
|
|
4451
|
+
}
|
|
4452
|
+
return result;
|
|
4453
|
+
}
|
|
4454
|
+
const files = await collectFiles();
|
|
4455
|
+
if (files.length === 0) throw new Error(`Skill ${name} has no files in marketplace`);
|
|
4456
|
+
const targetDir = join(CLAUDE_SKILLS_DIR, name);
|
|
4457
|
+
mkdirSync(targetDir, { recursive: true });
|
|
4458
|
+
for (const filePath of files) {
|
|
4459
|
+
const url = `${BASE}/files/${filePath}`;
|
|
4460
|
+
const resp = await fetch(url, { signal: AbortSignal.timeout(3e4) });
|
|
4461
|
+
if (!resp.ok) throw new Error(`Failed to download ${filePath}: HTTP ${resp.status}`);
|
|
4462
|
+
const content = await resp.text();
|
|
4463
|
+
const localPath = join(targetDir, filePath);
|
|
4464
|
+
if (!localPath.startsWith(targetDir + "/")) continue;
|
|
4465
|
+
mkdirSync(dirname(localPath), { recursive: true });
|
|
4466
|
+
writeFileSync(localPath, content, "utf-8");
|
|
4467
|
+
}
|
|
4468
|
+
}
|
|
4469
|
+
async function ensureAutoInstalledSkills(logger) {
|
|
4470
|
+
const tasks = [
|
|
4471
|
+
{
|
|
4472
|
+
name: "svamp",
|
|
4473
|
+
install: () => installSkillFromMarketplace("svamp")
|
|
4474
|
+
},
|
|
4475
|
+
{
|
|
4476
|
+
name: "hypha",
|
|
4477
|
+
install: () => installSkillFromEndpoint("hypha", "https://hypha.aicell.io/ws/agent-skills/")
|
|
4478
|
+
}
|
|
4479
|
+
];
|
|
4480
|
+
for (const task of tasks) {
|
|
4481
|
+
const targetDir = join(CLAUDE_SKILLS_DIR, task.name);
|
|
4482
|
+
if (existsSync$1(targetDir)) continue;
|
|
4483
|
+
try {
|
|
4484
|
+
await task.install();
|
|
4485
|
+
logger.log(`[skills] Auto-installed: ${task.name}`);
|
|
4486
|
+
} catch (err) {
|
|
4487
|
+
logger.log(`[skills] Auto-install of "${task.name}" failed (non-fatal): ${err.message}`);
|
|
4488
|
+
}
|
|
4489
|
+
}
|
|
4490
|
+
}
|
|
4411
4491
|
function loadEnvFile(path) {
|
|
4412
4492
|
if (!existsSync$1(path)) return false;
|
|
4413
4493
|
const lines = readFileSync$1(path, "utf-8").split("\n");
|
|
@@ -5172,6 +5252,8 @@ async function startDaemon(options) {
|
|
|
5172
5252
|
let server = null;
|
|
5173
5253
|
const supervisor = new ProcessSupervisor(join(SVAMP_HOME, "processes"));
|
|
5174
5254
|
await supervisor.init();
|
|
5255
|
+
ensureAutoInstalledSkills(logger).catch(() => {
|
|
5256
|
+
});
|
|
5175
5257
|
try {
|
|
5176
5258
|
logger.log("Connecting to Hypha server...");
|
|
5177
5259
|
server = await connectToHypha({
|
|
@@ -5328,7 +5410,12 @@ async function startDaemon(options) {
|
|
|
5328
5410
|
sharing: options2.sharing,
|
|
5329
5411
|
securityContext: options2.securityContext,
|
|
5330
5412
|
tags: options2.tags,
|
|
5331
|
-
parentSessionId: options2.parentSessionId
|
|
5413
|
+
parentSessionId: options2.parentSessionId,
|
|
5414
|
+
...options2.parentSessionId && (() => {
|
|
5415
|
+
const parentTracked = Array.from(pidToTrackedSession.values()).find((t) => t.svampSessionId === options2.parentSessionId);
|
|
5416
|
+
return parentTracked?.directory ? { parentSessionPath: parentTracked.directory } : {};
|
|
5417
|
+
})(),
|
|
5418
|
+
...options2.injectPlatformGuidance !== void 0 && { injectPlatformGuidance: options2.injectPlatformGuidance }
|
|
5332
5419
|
};
|
|
5333
5420
|
let claudeProcess = null;
|
|
5334
5421
|
const allPersisted = loadPersistedSessions();
|
|
@@ -5338,11 +5425,15 @@ async function startDaemon(options) {
|
|
|
5338
5425
|
let lastSpawnMeta = persisted?.spawnMeta || {};
|
|
5339
5426
|
let sessionWasProcessing = !!options2.wasProcessing;
|
|
5340
5427
|
let lastAssistantText = "";
|
|
5428
|
+
let spawnHasReceivedInit = false;
|
|
5341
5429
|
const signalProcessing = (processing) => {
|
|
5342
5430
|
sessionService.sendKeepAlive(processing);
|
|
5343
5431
|
const newState = processing ? "running" : "idle";
|
|
5344
5432
|
if (sessionMetadata.lifecycleState !== newState) {
|
|
5345
5433
|
sessionMetadata = { ...sessionMetadata, lifecycleState: newState };
|
|
5434
|
+
if (!processing) {
|
|
5435
|
+
sessionMetadata = { ...sessionMetadata, unread: true };
|
|
5436
|
+
}
|
|
5346
5437
|
sessionService.updateMetadata(sessionMetadata);
|
|
5347
5438
|
}
|
|
5348
5439
|
};
|
|
@@ -5449,6 +5540,7 @@ async function startDaemon(options) {
|
|
|
5449
5540
|
shell: process.platform === "win32"
|
|
5450
5541
|
});
|
|
5451
5542
|
claudeProcess = child;
|
|
5543
|
+
spawnHasReceivedInit = false;
|
|
5452
5544
|
logger.log(`[Session ${sessionId}] Claude PID: ${child.pid}, stdin: ${!!child.stdin}, stdout: ${!!child.stdout}, stderr: ${!!child.stderr}`);
|
|
5453
5545
|
child.stdin?.on("error", (err) => {
|
|
5454
5546
|
logger.log(`[Session ${sessionId}] Claude stdin error: ${err.message}`);
|
|
@@ -5807,7 +5899,9 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
5807
5899
|
}
|
|
5808
5900
|
userMessagePending = false;
|
|
5809
5901
|
if (msg.session_id) {
|
|
5810
|
-
const
|
|
5902
|
+
const isResumeFailure = !spawnHasReceivedInit && claudeResumeId && msg.session_id !== claudeResumeId;
|
|
5903
|
+
const isConversationClear = spawnHasReceivedInit && claudeResumeId && msg.session_id !== claudeResumeId;
|
|
5904
|
+
spawnHasReceivedInit = true;
|
|
5811
5905
|
claudeResumeId = msg.session_id;
|
|
5812
5906
|
sessionMetadata = { ...sessionMetadata, claudeSessionId: msg.session_id };
|
|
5813
5907
|
sessionService.updateMetadata(sessionMetadata);
|
|
@@ -5825,7 +5919,9 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
5825
5919
|
});
|
|
5826
5920
|
artifactSync.scheduleDebouncedSync(sessionId, getSessionDir(directory, sessionId), sessionMetadata, machineId);
|
|
5827
5921
|
}
|
|
5828
|
-
if (
|
|
5922
|
+
if (isResumeFailure) {
|
|
5923
|
+
logger.log(`[Session ${sessionId}] Resume failed \u2014 Claude started fresh session (tried: ${persisted?.claudeResumeId ?? "unknown"}, got: ${msg.session_id})`);
|
|
5924
|
+
} else if (isConversationClear) {
|
|
5829
5925
|
logger.log(`[Session ${sessionId}] Conversation cleared (/clear) \u2014 new Claude session: ${msg.session_id}`);
|
|
5830
5926
|
sessionService.clearMessages();
|
|
5831
5927
|
sessionService.pushMessage(
|
|
@@ -5957,7 +6053,7 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
5957
6053
|
isRestartingClaude = false;
|
|
5958
6054
|
}
|
|
5959
6055
|
};
|
|
5960
|
-
if (sessionMetadata.sharing?.enabled
|
|
6056
|
+
if (sessionMetadata.sharing?.enabled) {
|
|
5961
6057
|
try {
|
|
5962
6058
|
stagedCredentials = await stageCredentialsForSharing(sessionId);
|
|
5963
6059
|
logger.log(`[Session ${sessionId}] Credentials staged at ${stagedCredentials.homePath}`);
|
|
@@ -6954,7 +7050,7 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
6954
7050
|
const defaultHomeDir = existsSync$1("/data") ? "/data" : os__default.homedir();
|
|
6955
7051
|
const persistedMachineMeta = loadPersistedMachineMetadata(SVAMP_HOME);
|
|
6956
7052
|
if (persistedMachineMeta) {
|
|
6957
|
-
logger.log(`Restored machine metadata (sharing=${!!persistedMachineMeta.sharing}, securityContextConfig=${!!persistedMachineMeta.securityContextConfig})`);
|
|
7053
|
+
logger.log(`Restored machine metadata (sharing=${!!persistedMachineMeta.sharing}, securityContextConfig=${!!persistedMachineMeta.securityContextConfig}, injectPlatformGuidance=${persistedMachineMeta.injectPlatformGuidance})`);
|
|
6958
7054
|
}
|
|
6959
7055
|
const machineMetadata = {
|
|
6960
7056
|
host: os__default.hostname(),
|
|
@@ -6965,9 +7061,10 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
6965
7061
|
svampLibDir: join(__dirname$1, ".."),
|
|
6966
7062
|
displayName: process.env.SVAMP_DISPLAY_NAME || void 0,
|
|
6967
7063
|
isolationCapabilities,
|
|
6968
|
-
// Restore persisted sharing
|
|
7064
|
+
// Restore persisted sharing, security context config, and platform guidance flag
|
|
6969
7065
|
...persistedMachineMeta?.sharing && { sharing: persistedMachineMeta.sharing },
|
|
6970
|
-
...persistedMachineMeta?.securityContextConfig && { securityContextConfig: persistedMachineMeta.securityContextConfig }
|
|
7066
|
+
...persistedMachineMeta?.securityContextConfig && { securityContextConfig: persistedMachineMeta.securityContextConfig },
|
|
7067
|
+
...persistedMachineMeta?.injectPlatformGuidance !== void 0 && { injectPlatformGuidance: persistedMachineMeta.injectPlatformGuidance }
|
|
6971
7068
|
};
|
|
6972
7069
|
const initialDaemonState = {
|
|
6973
7070
|
status: "running",
|
|
@@ -2,7 +2,7 @@ import{createRequire as _pkgrollCR}from"node:module";const require=_pkgrollCR(im
|
|
|
2
2
|
import os from 'node:os';
|
|
3
3
|
import { join, resolve } from 'node:path';
|
|
4
4
|
import { mkdirSync, writeFileSync, existsSync, unlinkSync, readFileSync, watch } from 'node:fs';
|
|
5
|
-
import { c as connectToHypha, a as registerSessionService } from './run-
|
|
5
|
+
import { c as connectToHypha, a as registerSessionService } from './run-BDmLH9T8.mjs';
|
|
6
6
|
import { createServer } from 'node:http';
|
|
7
7
|
import { spawn } from 'node:child_process';
|
|
8
8
|
import { createInterface } from 'node:readline';
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { getSandboxEnv } from './api-BRbsyqJ4.mjs';
|
|
2
|
+
|
|
3
|
+
async function adminFetch(path, method = "GET", body) {
|
|
4
|
+
const env = getSandboxEnv();
|
|
5
|
+
if (!env.apiKey) {
|
|
6
|
+
throw new Error('No API credentials. Run "svamp login" or set SANDBOX_API_KEY.');
|
|
7
|
+
}
|
|
8
|
+
const url = `${env.apiUrl.replace(/\/+$/, "")}${path}`;
|
|
9
|
+
const res = await fetch(url, {
|
|
10
|
+
method,
|
|
11
|
+
headers: {
|
|
12
|
+
"Authorization": `Bearer ${env.apiKey}`,
|
|
13
|
+
"Content-Type": "application/json"
|
|
14
|
+
},
|
|
15
|
+
...{}
|
|
16
|
+
});
|
|
17
|
+
if (!res.ok) {
|
|
18
|
+
const text = await res.text().catch(() => "");
|
|
19
|
+
throw new Error(`HTTP ${res.status} ${method} ${path}: ${text}`);
|
|
20
|
+
}
|
|
21
|
+
return res.json();
|
|
22
|
+
}
|
|
23
|
+
function fmtAge(ts) {
|
|
24
|
+
if (!ts) return "?";
|
|
25
|
+
try {
|
|
26
|
+
const dt = /^\d+$/.test(ts) ? new Date(parseInt(ts, 10) * 1e3) : new Date(ts);
|
|
27
|
+
const secs = (Date.now() - dt.getTime()) / 1e3;
|
|
28
|
+
const d = Math.floor(secs / 86400);
|
|
29
|
+
const h = Math.floor(secs % 86400 / 3600);
|
|
30
|
+
return d > 0 ? `${d}d ${h}h ago` : `${h}h ago`;
|
|
31
|
+
} catch {
|
|
32
|
+
return ts;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
function pad(s, n) {
|
|
36
|
+
return s.length >= n ? s : s + " ".repeat(n - s.length);
|
|
37
|
+
}
|
|
38
|
+
async function storageList() {
|
|
39
|
+
const data = await adminFetch("/admin/storage/pvcs");
|
|
40
|
+
const pvcs = data.pvcs ?? [];
|
|
41
|
+
if (!pvcs.length) {
|
|
42
|
+
console.log("No managed PVCs found.");
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
console.log("");
|
|
46
|
+
console.log(`${pad("NAMESPACE", 36)}${pad("SIZE", 9)}${pad("STATUS", 9)}${pad("CREATED", 20)}ORPHANED`);
|
|
47
|
+
console.log("\u2500".repeat(90));
|
|
48
|
+
for (const p of pvcs.sort((a, b) => a.namespace.localeCompare(b.namespace))) {
|
|
49
|
+
const orphan = p.orphaned_at ? `\u26A0 ${fmtAge(p.orphaned_at)}` : "\u2014";
|
|
50
|
+
console.log(`${pad(p.namespace, 36)}${pad(p.capacity ?? "?", 9)}${pad(p.status ?? "?", 9)}${pad(fmtAge(p.created_at), 20)}${orphan}`);
|
|
51
|
+
}
|
|
52
|
+
console.log(`
|
|
53
|
+
Total: ${data.count} PVC(s)`);
|
|
54
|
+
}
|
|
55
|
+
async function storageOrphans(mark = false) {
|
|
56
|
+
const data = await adminFetch(`/admin/orphaned-pvcs?dry_run=${mark ? "false" : "true"}`);
|
|
57
|
+
const orphans = data.orphaned_pvcs ?? [];
|
|
58
|
+
if (!orphans.length) {
|
|
59
|
+
console.log("\u2705 No orphaned PVCs found.");
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
console.log(`
|
|
63
|
+
\u26A0 ${orphans.length} orphaned PVC(s) (namespace has no running pods):
|
|
64
|
+
`);
|
|
65
|
+
console.log(`${pad("NAMESPACE", 36)}${pad("SIZE", 9)}${pad("MARKED", 9)}ORPHANED FOR`);
|
|
66
|
+
console.log("\u2500".repeat(72));
|
|
67
|
+
for (const o of orphans) {
|
|
68
|
+
const age = o.orphaned_for || (o.marked ? "?" : "not marked");
|
|
69
|
+
console.log(`${pad(o.namespace, 36)}${pad(o.storage ?? "?", 9)}${pad(o.marked ? "yes" : "no", 9)}${age}`);
|
|
70
|
+
}
|
|
71
|
+
if (!mark) {
|
|
72
|
+
console.log("\nTip: pass --mark to start the retention clock on unmarked orphans.");
|
|
73
|
+
}
|
|
74
|
+
console.log("\nTo delete: svamp machine storage delete <namespace>");
|
|
75
|
+
}
|
|
76
|
+
async function storageMarkOrphan(namespace) {
|
|
77
|
+
const data = await adminFetch(`/admin/storage/mark-orphan/${namespace}`, "POST");
|
|
78
|
+
if (data.marked) {
|
|
79
|
+
console.log(`\u2705 PVC for '${namespace}' marked as orphaned.`);
|
|
80
|
+
console.log(` To delete: svamp machine storage delete ${namespace}`);
|
|
81
|
+
} else {
|
|
82
|
+
console.log(`\u2139 PVC for '${namespace}' was already marked or not found.`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
async function storageUnmarkOrphan(namespace) {
|
|
86
|
+
const data = await adminFetch(`/admin/storage/unmark-orphan/${namespace}`, "POST");
|
|
87
|
+
if (data.unmarked) {
|
|
88
|
+
console.log(`\u2705 Orphan mark removed from '${namespace}'.`);
|
|
89
|
+
} else {
|
|
90
|
+
console.log(`\u2139 PVC for '${namespace}' was not marked or not found.`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
async function storageDelete(namespace) {
|
|
94
|
+
const readline = await import('readline');
|
|
95
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
96
|
+
const ask = (q) => new Promise((res) => rl.question(q, res));
|
|
97
|
+
try {
|
|
98
|
+
const data2 = await adminFetch("/admin/storage/pvcs");
|
|
99
|
+
const pvc = (data2.pvcs ?? []).find((p) => p.namespace === namespace);
|
|
100
|
+
if (!pvc) {
|
|
101
|
+
console.error(`No PVC found for namespace '${namespace}'.`);
|
|
102
|
+
rl.close();
|
|
103
|
+
process.exit(1);
|
|
104
|
+
}
|
|
105
|
+
console.log(`
|
|
106
|
+
\u26A0 Namespace: ${pvc.namespace}`);
|
|
107
|
+
console.log(` Size: ${pvc.capacity ?? "?"}`);
|
|
108
|
+
console.log(` Created: ${fmtAge(pvc.created_at)}`);
|
|
109
|
+
if (pvc.orphaned_at) console.log(` Orphaned: ${fmtAge(pvc.orphaned_at)}`);
|
|
110
|
+
} catch {
|
|
111
|
+
}
|
|
112
|
+
console.log("\n\u26A0 PERMANENTLY deleting this PVC will IRREVERSIBLY destroy all user data.\n");
|
|
113
|
+
const a1 = (await ask(`Type the namespace name to confirm: `)).trim();
|
|
114
|
+
if (a1 !== namespace) {
|
|
115
|
+
console.log("Confirmation mismatch \u2014 aborted.");
|
|
116
|
+
rl.close();
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
const a2 = (await ask(`Type it again to double-confirm: `)).trim();
|
|
120
|
+
rl.close();
|
|
121
|
+
if (a2 !== namespace) {
|
|
122
|
+
console.log("Confirmation mismatch \u2014 aborted.");
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
const data = await adminFetch(`/admin/storage/pvc/${namespace}`, "DELETE");
|
|
126
|
+
if (data.deleted) {
|
|
127
|
+
console.log(`\u2705 PVC for '${namespace}' permanently deleted.`);
|
|
128
|
+
} else {
|
|
129
|
+
console.log(`\u2139 PVC for '${namespace}' was not found or already deleted.`);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
function printStorageHelp() {
|
|
133
|
+
console.log(`
|
|
134
|
+
svamp machine storage \u2014 sandbox PVC lifecycle management (admin only)
|
|
135
|
+
|
|
136
|
+
Usage:
|
|
137
|
+
svamp machine storage list List all PVCs with status
|
|
138
|
+
svamp machine storage orphans [--mark] Show PVCs with no active namespace
|
|
139
|
+
svamp machine storage mark-orphan <ns> Flag a PVC as orphaned (no deletion)
|
|
140
|
+
svamp machine storage unmark-orphan <ns> Remove orphan flag
|
|
141
|
+
svamp machine storage delete <ns> Permanently delete a PVC (double confirmation)
|
|
142
|
+
|
|
143
|
+
Environment:
|
|
144
|
+
SANDBOX_API_URL Sandbox API base URL (default: https://agent-sandbox.aicell.io)
|
|
145
|
+
SANDBOX_API_KEY Admin API key (or use HYPHA_TOKEN from svamp login)
|
|
146
|
+
|
|
147
|
+
Safety rules:
|
|
148
|
+
- Automated systems NEVER delete PVCs
|
|
149
|
+
- delete requires typing the namespace name twice
|
|
150
|
+
- delete checks for running pods and refuses if any are active
|
|
151
|
+
`);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export { printStorageHelp, storageDelete, storageList, storageMarkOrphan, storageOrphans, storageUnmarkOrphan };
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "svamp-cli",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "Svamp CLI
|
|
3
|
+
"version": "0.1.66",
|
|
4
|
+
"description": "Svamp CLI \u2014 AI workspace daemon on Hypha Cloud",
|
|
5
5
|
"author": "Amun AI AB",
|
|
6
6
|
"license": "SEE LICENSE IN LICENSE",
|
|
7
7
|
"type": "module",
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"scripts": {
|
|
21
21
|
"build": "rm -rf dist && tsc --noEmit && pkgroll",
|
|
22
22
|
"typecheck": "tsc --noEmit",
|
|
23
|
-
"test": "npx tsx test/test-authorize.mjs && npx tsx test/test-session-helpers.mjs && npx tsx test/test-cli-routing.mjs && npx tsx test/test-security-context.mjs && npx tsx test/test-ralph-loop.mjs && npx tsx test/test-message-helpers.mjs && npx tsx test/test-agent-config.mjs && npx tsx test/test-wrap-command.mjs && npx tsx test/test-credential-staging.mjs && npx tsx test/test-output-formatters.mjs && npx tsx test/test-agent-types.mjs && npx tsx test/test-transport.mjs && npx tsx test/test-session-update-handlers.mjs && npx tsx test/test-session-scanner.mjs && npx tsx test/test-hypha-client.mjs && npx tsx test/test-hook-settings.mjs && npx tsx test/test-session-service-logic.mjs && npx tsx test/test-daemon-persistence.mjs && npx tsx test/test-detect-isolation.mjs && npx tsx test/test-machine-service-logic.mjs && npx tsx test/test-interactive-helpers.mjs && npx tsx test/test-codex-backend.mjs && npx tsx test/test-acp-backend.mjs && npx tsx test/test-acp-bridge.mjs && npx tsx test/test-hook-server.mjs && npx tsx test/test-session-commands.mjs && npx tsx test/test-interactive-console.mjs && npx tsx test/test-session-messages.mjs && npx tsx test/test-skills.mjs && npx tsx test/test-agent-grouping.mjs && npx tsx test/test-ralph-loop-integration.mjs && npx tsx test/test-ralph-loop-modes.mjs && npx tsx test/test-machine-list-directory.mjs && npx tsx test/test-service-commands.mjs && npx tsx test/test-supervisor.mjs",
|
|
23
|
+
"test": "npx tsx test/test-authorize.mjs && npx tsx test/test-session-helpers.mjs && npx tsx test/test-cli-routing.mjs && npx tsx test/test-security-context.mjs && npx tsx test/test-ralph-loop.mjs && npx tsx test/test-message-helpers.mjs && npx tsx test/test-agent-config.mjs && npx tsx test/test-wrap-command.mjs && npx tsx test/test-credential-staging.mjs && npx tsx test/test-output-formatters.mjs && npx tsx test/test-agent-types.mjs && npx tsx test/test-transport.mjs && npx tsx test/test-session-update-handlers.mjs && npx tsx test/test-session-scanner.mjs && npx tsx test/test-hypha-client.mjs && npx tsx test/test-hook-settings.mjs && npx tsx test/test-session-service-logic.mjs && npx tsx test/test-daemon-persistence.mjs && npx tsx test/test-detect-isolation.mjs && npx tsx test/test-machine-service-logic.mjs && npx tsx test/test-interactive-helpers.mjs && npx tsx test/test-codex-backend.mjs && npx tsx test/test-acp-backend.mjs && npx tsx test/test-acp-bridge.mjs && npx tsx test/test-hook-server.mjs && npx tsx test/test-session-commands.mjs && npx tsx test/test-interactive-console.mjs && npx tsx test/test-session-messages.mjs && npx tsx test/test-skills.mjs && npx tsx test/test-agent-grouping.mjs && npx tsx test/test-ralph-loop-integration.mjs && npx tsx test/test-ralph-loop-modes.mjs && npx tsx test/test-machine-list-directory.mjs && npx tsx test/test-service-commands.mjs && npx tsx test/test-supervisor.mjs && npx tsx test/test-clear-detection.mjs",
|
|
24
24
|
"test:hypha": "node --no-warnings test/test-hypha-service.mjs",
|
|
25
25
|
"dev": "tsx src/cli.ts",
|
|
26
26
|
"dev:daemon": "tsx src/cli.ts daemon start-sync",
|