svamp-cli 0.2.48 → 0.2.50
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/README.md +17 -8
- package/dist/{agentCommands-BuGwfYhd.mjs → agentCommands-CoozsCNg.mjs} +2 -2
- package/dist/cli.mjs +68 -33
- package/dist/{commands-BJR_98XX.mjs → commands-Cb0wg6xA.mjs} +2 -2
- package/dist/{commands-JWrmpGcs.mjs → commands-CvWyrVnY.mjs} +46 -32
- package/dist/{commands-TyAIFJx-.mjs → commands-FDeLH4pp.mjs} +2 -2
- package/dist/index.mjs +1 -1
- package/dist/package-D2al7V1C.mjs +63 -0
- package/dist/{run-6umeTX-K.mjs → run-CIPCavEp.mjs} +436 -13
- package/dist/{run-DR7E3IZL.mjs → run-CPV6r3M6.mjs} +1 -1
- package/dist/{serveCommands-FUE8m232.mjs → serveCommands-Bzmgmhxk.mjs} +5 -5
- package/dist/{serveManager-RvRL-weX.mjs → serveManager-rHhOPkW4.mjs} +17 -230
- package/package.json +2 -2
- package/dist/package-DVfaovNL.mjs +0 -63
|
@@ -15,7 +15,7 @@ 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';
|
|
17
17
|
import { z } from 'zod';
|
|
18
|
-
import { mkdir, rm, chmod, access, mkdtemp, copyFile, writeFile,
|
|
18
|
+
import { mkdir, rm, chmod, access, mkdtemp, copyFile, writeFile, readdir, stat, readFile as readFile$1 } from 'node:fs/promises';
|
|
19
19
|
import { promisify } from 'node:util';
|
|
20
20
|
|
|
21
21
|
let connectToServerFn = null;
|
|
@@ -71,6 +71,14 @@ const ROLE_HIERARCHY = {
|
|
|
71
71
|
interact: 1,
|
|
72
72
|
admin: 2
|
|
73
73
|
};
|
|
74
|
+
function normalizeAllowedUser(input, addedBy) {
|
|
75
|
+
return {
|
|
76
|
+
email: input.email.trim(),
|
|
77
|
+
role: "admin",
|
|
78
|
+
addedAt: typeof input.addedAt === "number" ? input.addedAt : Date.now(),
|
|
79
|
+
addedBy: input.addedBy ?? addedBy
|
|
80
|
+
};
|
|
81
|
+
}
|
|
74
82
|
const ISOLATION_PREFERENCE = ["nono", "docker", "podman"];
|
|
75
83
|
|
|
76
84
|
function resolveRoleLevel(sharing, userEmail) {
|
|
@@ -80,7 +88,7 @@ function resolveRoleLevel(sharing, userEmail) {
|
|
|
80
88
|
(u) => u.email.toLowerCase() === userEmail.toLowerCase()
|
|
81
89
|
);
|
|
82
90
|
if (sharedUser) {
|
|
83
|
-
level = ROLE_HIERARCHY
|
|
91
|
+
level = ROLE_HIERARCHY.admin;
|
|
84
92
|
}
|
|
85
93
|
}
|
|
86
94
|
if (sharing.publicAccess) {
|
|
@@ -737,6 +745,14 @@ async function registerMachineService(server, machineId, metadata, daemonState,
|
|
|
737
745
|
if (newSharing.enabled && !newSharing.owner && context?.user?.email) {
|
|
738
746
|
newSharing = { ...newSharing, owner: context.user.email };
|
|
739
747
|
}
|
|
748
|
+
const ownerEmail = newSharing.owner || context?.user?.email || "";
|
|
749
|
+
newSharing = {
|
|
750
|
+
...newSharing,
|
|
751
|
+
allowedUsers: (newSharing.allowedUsers || []).map(
|
|
752
|
+
(u) => normalizeAllowedUser(u, ownerEmail)
|
|
753
|
+
),
|
|
754
|
+
publicAccess: null
|
|
755
|
+
};
|
|
740
756
|
const oldSharing = currentMetadata.sharing;
|
|
741
757
|
currentMetadata = { ...currentMetadata, sharing: newSharing };
|
|
742
758
|
metadataVersion++;
|
|
@@ -1748,6 +1764,13 @@ function createSessionStore(server, sessionId, initialMetadata, initialAgentStat
|
|
|
1748
1764
|
if (newSharing.enabled && !newSharing.owner && context?.user?.email) {
|
|
1749
1765
|
newSharing = { ...newSharing, owner: context.user.email };
|
|
1750
1766
|
}
|
|
1767
|
+
const ownerEmail = newSharing.owner || context?.user?.email || "";
|
|
1768
|
+
newSharing = {
|
|
1769
|
+
...newSharing,
|
|
1770
|
+
allowedUsers: (newSharing.allowedUsers || []).map(
|
|
1771
|
+
(u) => normalizeAllowedUser(u, ownerEmail)
|
|
1772
|
+
)
|
|
1773
|
+
};
|
|
1751
1774
|
metadata = { ...metadata, sharing: newSharing };
|
|
1752
1775
|
metadataVersion++;
|
|
1753
1776
|
notifyListeners({
|
|
@@ -2576,7 +2599,8 @@ class SharingNotificationSync {
|
|
|
2576
2599
|
userEmail: params.ownerEmail
|
|
2577
2600
|
},
|
|
2578
2601
|
title: `${typeLabel} shared with you`,
|
|
2579
|
-
|
|
2602
|
+
// Per-user tiers were retired; explicit users always have full access.
|
|
2603
|
+
body: `${params.ownerEmail} shared "${params.label || "Untitled"}" with you (full access)`,
|
|
2580
2604
|
level: "info",
|
|
2581
2605
|
action: shareType === "session" ? {
|
|
2582
2606
|
type: "add-session-bookmark",
|
|
@@ -4939,6 +4963,28 @@ function sanitizeEnvForSharing(env) {
|
|
|
4939
4963
|
}
|
|
4940
4964
|
return sanitized;
|
|
4941
4965
|
}
|
|
4966
|
+
async function sweepOrphanedStagedHomes(activeSessionIds) {
|
|
4967
|
+
const removed = [];
|
|
4968
|
+
if (!existsSync(STAGED_HOMES_DIR)) return { removed };
|
|
4969
|
+
const active = new Set(activeSessionIds);
|
|
4970
|
+
let entries;
|
|
4971
|
+
try {
|
|
4972
|
+
entries = await readdir(STAGED_HOMES_DIR, { withFileTypes: true });
|
|
4973
|
+
} catch {
|
|
4974
|
+
return { removed };
|
|
4975
|
+
}
|
|
4976
|
+
for (const entry of entries) {
|
|
4977
|
+
if (!entry.isDirectory()) continue;
|
|
4978
|
+
if (active.has(entry.name)) continue;
|
|
4979
|
+
const path = join$1(STAGED_HOMES_DIR, entry.name);
|
|
4980
|
+
try {
|
|
4981
|
+
await rm(path, { recursive: true, force: true });
|
|
4982
|
+
removed.push(entry.name);
|
|
4983
|
+
} catch {
|
|
4984
|
+
}
|
|
4985
|
+
}
|
|
4986
|
+
return { removed };
|
|
4987
|
+
}
|
|
4942
4988
|
async function copyDirRecursive(src, dest) {
|
|
4943
4989
|
const srcStat = await stat(src);
|
|
4944
4990
|
if (!srcStat.isDirectory()) return;
|
|
@@ -4958,7 +5004,8 @@ async function copyDirRecursive(src, dest) {
|
|
|
4958
5004
|
var credentialStaging = /*#__PURE__*/Object.freeze({
|
|
4959
5005
|
__proto__: null,
|
|
4960
5006
|
sanitizeEnvForSharing: sanitizeEnvForSharing,
|
|
4961
|
-
stageCredentialsForSharing: stageCredentialsForSharing
|
|
5007
|
+
stageCredentialsForSharing: stageCredentialsForSharing,
|
|
5008
|
+
sweepOrphanedStagedHomes: sweepOrphanedStagedHomes
|
|
4962
5009
|
});
|
|
4963
5010
|
|
|
4964
5011
|
function shouldIsolate(input) {
|
|
@@ -5125,6 +5172,287 @@ var claudeAuth = /*#__PURE__*/Object.freeze({
|
|
|
5125
5172
|
updateEnvFile: updateEnvFile
|
|
5126
5173
|
});
|
|
5127
5174
|
|
|
5175
|
+
const DEFAULT_WEB_BASE = "https://svamp.hypha.aicell.io";
|
|
5176
|
+
function getShareBaseUrl() {
|
|
5177
|
+
const fromEnv = (process.env.SVAMP_WEB_URL || "").trim();
|
|
5178
|
+
const base = fromEnv || DEFAULT_WEB_BASE;
|
|
5179
|
+
return base.replace(/\/+$/, "");
|
|
5180
|
+
}
|
|
5181
|
+
function buildSessionShareUrl(input) {
|
|
5182
|
+
const params = new URLSearchParams();
|
|
5183
|
+
if (input.workspace) params.set("ws", input.workspace);
|
|
5184
|
+
if (input.machineServiceId) params.set("svc", input.machineServiceId);
|
|
5185
|
+
const query = params.toString();
|
|
5186
|
+
return `${getShareBaseUrl()}/share/${input.sessionId}${query ? `?${query}` : ""}`;
|
|
5187
|
+
}
|
|
5188
|
+
function buildMachineShareUrl(input) {
|
|
5189
|
+
const params = new URLSearchParams();
|
|
5190
|
+
if (input.workspace) params.set("ws", input.workspace);
|
|
5191
|
+
const query = params.toString();
|
|
5192
|
+
return `${getShareBaseUrl()}/share/machine/${input.machineId}${query ? `?${query}` : ""}`;
|
|
5193
|
+
}
|
|
5194
|
+
const DAEMON_SHARE_EMAILS_ENV = "SVAMP_SHARE_EMAILS";
|
|
5195
|
+
function parseDaemonShareEmails(raw) {
|
|
5196
|
+
if (!raw) return [];
|
|
5197
|
+
const seen = /* @__PURE__ */ new Set();
|
|
5198
|
+
const out = [];
|
|
5199
|
+
for (const part of raw.split(",")) {
|
|
5200
|
+
const email = part.trim();
|
|
5201
|
+
if (!email || !email.includes("@")) continue;
|
|
5202
|
+
const k = email.toLowerCase();
|
|
5203
|
+
if (seen.has(k)) continue;
|
|
5204
|
+
seen.add(k);
|
|
5205
|
+
out.push(email);
|
|
5206
|
+
}
|
|
5207
|
+
return out;
|
|
5208
|
+
}
|
|
5209
|
+
function applyDaemonShareSeed(sharing, seedEmails, ownerEmail) {
|
|
5210
|
+
const base = sharing ? { ...sharing, allowedUsers: [...sharing.allowedUsers || []] } : { enabled: true, owner: ownerEmail || "", allowedUsers: [] };
|
|
5211
|
+
base.enabled = true;
|
|
5212
|
+
if (!base.owner && ownerEmail) base.owner = ownerEmail;
|
|
5213
|
+
const existing = new Set(base.allowedUsers.map((u) => u.email.toLowerCase()));
|
|
5214
|
+
const addedEmails = [];
|
|
5215
|
+
for (const email of seedEmails) {
|
|
5216
|
+
if (existing.has(email.toLowerCase())) continue;
|
|
5217
|
+
base.allowedUsers.push(
|
|
5218
|
+
normalizeAllowedUser({ email, addedBy: "daemon-share-flag" }, base.owner || "daemon-share-flag")
|
|
5219
|
+
);
|
|
5220
|
+
existing.add(email.toLowerCase());
|
|
5221
|
+
addedEmails.push(email);
|
|
5222
|
+
}
|
|
5223
|
+
return { sharing: base, addedEmails };
|
|
5224
|
+
}
|
|
5225
|
+
|
|
5226
|
+
const AUTH0_NAMESPACES = ["https://api.imjoy.io/", "https://amun.ai/"];
|
|
5227
|
+
const COOKIE_NAME = "svamp_serve_token";
|
|
5228
|
+
const DEFAULT_CACHE_TTL_MS = 5 * 60 * 1e3;
|
|
5229
|
+
const DEFAULT_CACHE_MAX_SIZE = 1e3;
|
|
5230
|
+
class TokenCache {
|
|
5231
|
+
cache = /* @__PURE__ */ new Map();
|
|
5232
|
+
ttlMs;
|
|
5233
|
+
maxSize;
|
|
5234
|
+
constructor(ttlMs = DEFAULT_CACHE_TTL_MS, maxSize = DEFAULT_CACHE_MAX_SIZE) {
|
|
5235
|
+
this.ttlMs = ttlMs;
|
|
5236
|
+
this.maxSize = maxSize;
|
|
5237
|
+
}
|
|
5238
|
+
get(token) {
|
|
5239
|
+
const info = this.cache.get(token);
|
|
5240
|
+
if (!info) return null;
|
|
5241
|
+
if (Date.now() > info.expiresAt) {
|
|
5242
|
+
this.cache.delete(token);
|
|
5243
|
+
return null;
|
|
5244
|
+
}
|
|
5245
|
+
return info;
|
|
5246
|
+
}
|
|
5247
|
+
set(token, email) {
|
|
5248
|
+
if (this.cache.size >= this.maxSize) {
|
|
5249
|
+
const oldest = this.cache.keys().next().value;
|
|
5250
|
+
if (oldest) this.cache.delete(oldest);
|
|
5251
|
+
}
|
|
5252
|
+
this.cache.set(token, {
|
|
5253
|
+
email,
|
|
5254
|
+
expiresAt: Date.now() + this.ttlMs
|
|
5255
|
+
});
|
|
5256
|
+
}
|
|
5257
|
+
clear() {
|
|
5258
|
+
this.cache.clear();
|
|
5259
|
+
}
|
|
5260
|
+
}
|
|
5261
|
+
function hasCookieToken(req) {
|
|
5262
|
+
return !!parseCookies(req.headers.cookie || "")[COOKIE_NAME];
|
|
5263
|
+
}
|
|
5264
|
+
function extractToken(req) {
|
|
5265
|
+
const cookies = parseCookies(req.headers.cookie || "");
|
|
5266
|
+
if (cookies[COOKIE_NAME]) return cookies[COOKIE_NAME];
|
|
5267
|
+
const auth = req.headers.authorization;
|
|
5268
|
+
if (auth?.startsWith("Bearer ") || auth?.startsWith("bearer ")) {
|
|
5269
|
+
return auth.slice(7).trim();
|
|
5270
|
+
}
|
|
5271
|
+
const url = new URL(req.url || "/", "http://localhost");
|
|
5272
|
+
const qToken = url.searchParams.get("token");
|
|
5273
|
+
if (qToken) return qToken;
|
|
5274
|
+
return null;
|
|
5275
|
+
}
|
|
5276
|
+
function parseCookies(header) {
|
|
5277
|
+
const cookies = {};
|
|
5278
|
+
for (const pair of header.split(";")) {
|
|
5279
|
+
const eq = pair.indexOf("=");
|
|
5280
|
+
if (eq > 0) {
|
|
5281
|
+
const key = pair.slice(0, eq).trim();
|
|
5282
|
+
const value = pair.slice(eq + 1).trim();
|
|
5283
|
+
cookies[key] = value;
|
|
5284
|
+
}
|
|
5285
|
+
}
|
|
5286
|
+
return cookies;
|
|
5287
|
+
}
|
|
5288
|
+
function parseJwtEmail(token) {
|
|
5289
|
+
try {
|
|
5290
|
+
const parts = token.split(".");
|
|
5291
|
+
if (parts.length !== 3) return null;
|
|
5292
|
+
const payload = JSON.parse(
|
|
5293
|
+
Buffer.from(parts[1], "base64url").toString("utf-8")
|
|
5294
|
+
);
|
|
5295
|
+
if (payload.exp && payload.exp * 1e3 < Date.now()) return null;
|
|
5296
|
+
for (const ns of AUTH0_NAMESPACES) {
|
|
5297
|
+
const email = payload[ns + "email"];
|
|
5298
|
+
if (typeof email === "string" && email) return email.toLowerCase();
|
|
5299
|
+
}
|
|
5300
|
+
if (typeof payload.email === "string" && payload.email) return payload.email.toLowerCase();
|
|
5301
|
+
return null;
|
|
5302
|
+
} catch {
|
|
5303
|
+
return null;
|
|
5304
|
+
}
|
|
5305
|
+
}
|
|
5306
|
+
async function verifyTokenViaHypha(token, hyphaServerUrl) {
|
|
5307
|
+
try {
|
|
5308
|
+
const baseUrl = hyphaServerUrl.replace(/\/$/, "");
|
|
5309
|
+
const url = `${baseUrl}/public/services/ws/get_user_info`;
|
|
5310
|
+
const resp = await fetch(url, {
|
|
5311
|
+
method: "GET",
|
|
5312
|
+
headers: {
|
|
5313
|
+
Authorization: `Bearer ${token}`
|
|
5314
|
+
},
|
|
5315
|
+
signal: AbortSignal.timeout(1e4)
|
|
5316
|
+
});
|
|
5317
|
+
if (!resp.ok) return null;
|
|
5318
|
+
const data = await resp.json();
|
|
5319
|
+
const email = data?.email;
|
|
5320
|
+
if (typeof email === "string" && email) return email.toLowerCase();
|
|
5321
|
+
return null;
|
|
5322
|
+
} catch {
|
|
5323
|
+
return null;
|
|
5324
|
+
}
|
|
5325
|
+
}
|
|
5326
|
+
class ServeAuth {
|
|
5327
|
+
cache;
|
|
5328
|
+
hyphaServerUrl;
|
|
5329
|
+
constructor(options) {
|
|
5330
|
+
this.hyphaServerUrl = options.hyphaServerUrl.replace(/\/$/, "");
|
|
5331
|
+
this.cache = new TokenCache(
|
|
5332
|
+
options.cacheTtlMs || DEFAULT_CACHE_TTL_MS,
|
|
5333
|
+
options.cacheMaxSize || DEFAULT_CACHE_MAX_SIZE
|
|
5334
|
+
);
|
|
5335
|
+
}
|
|
5336
|
+
/**
|
|
5337
|
+
* Authenticate a request and return the user's email, or null if not authenticated.
|
|
5338
|
+
*/
|
|
5339
|
+
async authenticate(req) {
|
|
5340
|
+
const token = extractToken(req);
|
|
5341
|
+
if (!token) return null;
|
|
5342
|
+
const cached = this.cache.get(token);
|
|
5343
|
+
if (cached) return cached.email;
|
|
5344
|
+
const localEmail = parseJwtEmail(token);
|
|
5345
|
+
if (localEmail) {
|
|
5346
|
+
this.cache.set(token, localEmail);
|
|
5347
|
+
return localEmail;
|
|
5348
|
+
}
|
|
5349
|
+
const serverEmail = await verifyTokenViaHypha(token, this.hyphaServerUrl);
|
|
5350
|
+
if (serverEmail) {
|
|
5351
|
+
this.cache.set(token, serverEmail);
|
|
5352
|
+
return serverEmail;
|
|
5353
|
+
}
|
|
5354
|
+
return null;
|
|
5355
|
+
}
|
|
5356
|
+
/**
|
|
5357
|
+
* Check if a user email is authorized for a mount's access level.
|
|
5358
|
+
*/
|
|
5359
|
+
isAuthorized(email, access, ownerEmail) {
|
|
5360
|
+
if (access === "public") return true;
|
|
5361
|
+
if (!email) return false;
|
|
5362
|
+
if (access === "owner") {
|
|
5363
|
+
return !!ownerEmail && email.toLowerCase() === ownerEmail.toLowerCase();
|
|
5364
|
+
}
|
|
5365
|
+
return access.some((e) => e.toLowerCase() === email.toLowerCase());
|
|
5366
|
+
}
|
|
5367
|
+
/**
|
|
5368
|
+
* Generate the login page HTML. Loads the `hypha-rpc` JS SDK from CDN and
|
|
5369
|
+
* calls `hyphaWebsocketClient.login({ server_url, login_callback })`, which
|
|
5370
|
+
* handles opening the login URL, polling, and token retrieval internally.
|
|
5371
|
+
* Matches the pattern used by bioimage.io — proven to work.
|
|
5372
|
+
*/
|
|
5373
|
+
getLoginPageHtml(redirectUrl) {
|
|
5374
|
+
return `<!DOCTYPE html>
|
|
5375
|
+
<html><head>
|
|
5376
|
+
<meta charset="utf-8">
|
|
5377
|
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
5378
|
+
<title>Sign in \u2014 Svamp File Server</title>
|
|
5379
|
+
<style>
|
|
5380
|
+
*{box-sizing:border-box;margin:0;padding:0}
|
|
5381
|
+
body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;display:flex;align-items:center;justify-content:center;min-height:100vh;background:#f6f8fa;color:#24292f;padding:16px}
|
|
5382
|
+
.card{background:#fff;border:1px solid #d0d7de;border-radius:12px;padding:32px;max-width:420px;width:100%;text-align:center;box-shadow:0 4px 12px rgba(31,35,40,0.06)}
|
|
5383
|
+
h1{font-size:1.25rem;margin-bottom:8px}
|
|
5384
|
+
.subtitle{color:#656d76;font-size:0.9rem;margin-bottom:24px}
|
|
5385
|
+
button{background:#0969da;color:#fff;border:none;border-radius:8px;padding:12px 24px;font-size:1rem;cursor:pointer;font-weight:500;width:100%}
|
|
5386
|
+
button:hover{background:#0860c4}
|
|
5387
|
+
button:disabled{background:#94d3a2;cursor:wait}
|
|
5388
|
+
.status{margin-top:16px;color:#656d76;font-size:0.85rem;min-height:20px;word-break:break-word}
|
|
5389
|
+
.error{color:#cf222e}
|
|
5390
|
+
.ok{color:#1a7f37}
|
|
5391
|
+
a{color:#0969da;text-decoration:none}
|
|
5392
|
+
a:hover{text-decoration:underline}
|
|
5393
|
+
</style>
|
|
5394
|
+
<script src="https://cdn.jsdelivr.net/npm/hypha-rpc@0.21.28/dist/hypha-rpc-websocket.min.js"><\/script>
|
|
5395
|
+
</head><body>
|
|
5396
|
+
<div class="card">
|
|
5397
|
+
<h1>Sign in required</h1>
|
|
5398
|
+
<p class="subtitle">This resource is private. Sign in with your Hypha account to continue.</p>
|
|
5399
|
+
<button id="login-btn">Sign in with Hypha</button>
|
|
5400
|
+
<div class="status" id="status"></div>
|
|
5401
|
+
</div>
|
|
5402
|
+
<script>
|
|
5403
|
+
const hyphaServer = ${JSON.stringify(this.hyphaServerUrl)};
|
|
5404
|
+
const redirectUrl = ${JSON.stringify(redirectUrl)};
|
|
5405
|
+
const cookieName = ${JSON.stringify(COOKIE_NAME)};
|
|
5406
|
+
|
|
5407
|
+
const btn = document.getElementById('login-btn');
|
|
5408
|
+
const statusEl = document.getElementById('status');
|
|
5409
|
+
|
|
5410
|
+
function setError(msg) {
|
|
5411
|
+
statusEl.innerHTML = '<span class="error">' + msg + '</span>';
|
|
5412
|
+
btn.disabled = false;
|
|
5413
|
+
}
|
|
5414
|
+
|
|
5415
|
+
btn.addEventListener('click', async () => {
|
|
5416
|
+
if (typeof hyphaWebsocketClient === 'undefined' || !hyphaWebsocketClient.login) {
|
|
5417
|
+
setError('Hypha SDK failed to load. Check your network connection and retry.');
|
|
5418
|
+
return;
|
|
5419
|
+
}
|
|
5420
|
+
|
|
5421
|
+
btn.disabled = true;
|
|
5422
|
+
statusEl.textContent = 'Opening Hypha sign-in\u2026';
|
|
5423
|
+
|
|
5424
|
+
try {
|
|
5425
|
+
const token = await hyphaWebsocketClient.login({
|
|
5426
|
+
server_url: hyphaServer,
|
|
5427
|
+
login_callback: (context) => {
|
|
5428
|
+
statusEl.textContent = 'Waiting for you to sign in\u2026';
|
|
5429
|
+
// Open the login URL in a new tab. Called synchronously from
|
|
5430
|
+
// inside the SDK's login() after the user's button click, so
|
|
5431
|
+
// popup blockers generally allow it.
|
|
5432
|
+
window.open(context.login_url, '_blank');
|
|
5433
|
+
},
|
|
5434
|
+
});
|
|
5435
|
+
|
|
5436
|
+
if (!token) throw new Error('No token returned from login');
|
|
5437
|
+
|
|
5438
|
+
// Set the cookie so subsequent requests to this origin are authenticated.
|
|
5439
|
+
const secure = location.protocol === 'https:' ? '; Secure' : '';
|
|
5440
|
+
document.cookie = cookieName + '=' + token + '; path=/; SameSite=Lax' + secure;
|
|
5441
|
+
|
|
5442
|
+
statusEl.innerHTML = '<span class="ok">Signed in. Redirecting\u2026</span>';
|
|
5443
|
+
setTimeout(() => { window.location.replace(redirectUrl); }, 300);
|
|
5444
|
+
} catch (err) {
|
|
5445
|
+
setError('Login failed: ' + (err && err.message ? err.message : err));
|
|
5446
|
+
}
|
|
5447
|
+
});
|
|
5448
|
+
<\/script>
|
|
5449
|
+
</body></html>`;
|
|
5450
|
+
}
|
|
5451
|
+
destroy() {
|
|
5452
|
+
this.cache.clear();
|
|
5453
|
+
}
|
|
5454
|
+
}
|
|
5455
|
+
|
|
5128
5456
|
const DEFAULT_PROBE_INTERVAL_S = 10;
|
|
5129
5457
|
const DEFAULT_PROBE_TIMEOUT_S = 5;
|
|
5130
5458
|
const DEFAULT_PROBE_FAILURE_THRESHOLD = 3;
|
|
@@ -5975,6 +6303,28 @@ function checkTruncation(format, tail, fileSize, head) {
|
|
|
5975
6303
|
const __filename$1 = fileURLToPath(import.meta.url);
|
|
5976
6304
|
const __dirname$1 = dirname(__filename$1);
|
|
5977
6305
|
const CLAUDE_SKILLS_DIR = join(os$1.homedir(), ".claude", "skills");
|
|
6306
|
+
function readSkillVersion(skillDir) {
|
|
6307
|
+
try {
|
|
6308
|
+
const md = readFileSync$1(join(skillDir, "SKILL.md"), "utf-8");
|
|
6309
|
+
const m = md.match(/^---\s*\n([\s\S]*?)\n---/);
|
|
6310
|
+
if (!m) return null;
|
|
6311
|
+
const versionLine = m[1].split("\n").find((l) => /^\s*version\s*:/.test(l));
|
|
6312
|
+
if (!versionLine) return null;
|
|
6313
|
+
return versionLine.split(":").slice(1).join(":").trim().replace(/^["']|["']$/g, "");
|
|
6314
|
+
} catch {
|
|
6315
|
+
return null;
|
|
6316
|
+
}
|
|
6317
|
+
}
|
|
6318
|
+
function compareVersions(a, b) {
|
|
6319
|
+
const pa = a.split(".").map((p) => parseInt(p, 10) || 0);
|
|
6320
|
+
const pb = b.split(".").map((p) => parseInt(p, 10) || 0);
|
|
6321
|
+
for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
|
|
6322
|
+
const ai = pa[i] ?? 0;
|
|
6323
|
+
const bi = pb[i] ?? 0;
|
|
6324
|
+
if (ai !== bi) return ai < bi ? -1 : 1;
|
|
6325
|
+
}
|
|
6326
|
+
return 0;
|
|
6327
|
+
}
|
|
5978
6328
|
async function installSkillFromEndpoint(name, baseUrl) {
|
|
5979
6329
|
const resp = await fetch(baseUrl, { signal: AbortSignal.timeout(15e3) });
|
|
5980
6330
|
if (!resp.ok) throw new Error(`HTTP ${resp.status} from ${baseUrl}`);
|
|
@@ -6076,25 +6426,56 @@ function preventMachineSleep(logger) {
|
|
|
6076
6426
|
return;
|
|
6077
6427
|
}
|
|
6078
6428
|
}
|
|
6429
|
+
async function fetchMarketplaceSkillVersion(name) {
|
|
6430
|
+
try {
|
|
6431
|
+
const url = `https://hypha.aicell.io/hypha-cloud/artifacts/${name}/files/SKILL.md`;
|
|
6432
|
+
const resp = await fetch(url, { signal: AbortSignal.timeout(1e4) });
|
|
6433
|
+
if (!resp.ok) return null;
|
|
6434
|
+
const md = await resp.text();
|
|
6435
|
+
const m = md.match(/^---\s*\n([\s\S]*?)\n---/);
|
|
6436
|
+
if (!m) return null;
|
|
6437
|
+
const versionLine = m[1].split("\n").find((l) => /^\s*version\s*:/.test(l));
|
|
6438
|
+
if (!versionLine) return null;
|
|
6439
|
+
return versionLine.split(":").slice(1).join(":").trim().replace(/^["']|["']$/g, "");
|
|
6440
|
+
} catch {
|
|
6441
|
+
return null;
|
|
6442
|
+
}
|
|
6443
|
+
}
|
|
6079
6444
|
async function ensureAutoInstalledSkills(logger) {
|
|
6080
6445
|
const tasks = [
|
|
6081
6446
|
{
|
|
6082
6447
|
name: "svamp",
|
|
6083
|
-
install: () => installSkillFromMarketplace("svamp")
|
|
6448
|
+
install: () => installSkillFromMarketplace("svamp"),
|
|
6449
|
+
marketplaceVersion: () => fetchMarketplaceSkillVersion("svamp")
|
|
6084
6450
|
},
|
|
6085
6451
|
{
|
|
6086
6452
|
name: "hypha",
|
|
6087
6453
|
install: () => installSkillFromEndpoint("hypha", "https://hypha.aicell.io/ws/agent-skills/")
|
|
6454
|
+
// hypha skill ships from a different endpoint without a version probe yet.
|
|
6088
6455
|
}
|
|
6089
6456
|
];
|
|
6090
6457
|
for (const task of tasks) {
|
|
6091
6458
|
const targetDir = join(CLAUDE_SKILLS_DIR, task.name);
|
|
6092
|
-
|
|
6459
|
+
const installed = existsSync$1(targetDir);
|
|
6460
|
+
if (!installed) {
|
|
6461
|
+
try {
|
|
6462
|
+
await task.install();
|
|
6463
|
+
logger.log(`[skills] Auto-installed: ${task.name}`);
|
|
6464
|
+
} catch (err) {
|
|
6465
|
+
logger.log(`[skills] Auto-install of "${task.name}" failed (non-fatal): ${err.message}`);
|
|
6466
|
+
}
|
|
6467
|
+
continue;
|
|
6468
|
+
}
|
|
6469
|
+
if (!task.marketplaceVersion) continue;
|
|
6093
6470
|
try {
|
|
6094
|
-
await task.
|
|
6095
|
-
|
|
6471
|
+
const remote = await task.marketplaceVersion();
|
|
6472
|
+
const local = readSkillVersion(targetDir);
|
|
6473
|
+
if (remote && local && compareVersions(remote, local) > 0) {
|
|
6474
|
+
logger.log(`[skills] Refreshing "${task.name}" ${local} \u2192 ${remote}...`);
|
|
6475
|
+
await task.install();
|
|
6476
|
+
logger.log(`[skills] Refreshed: ${task.name} \u2192 ${remote}`);
|
|
6477
|
+
}
|
|
6096
6478
|
} catch (err) {
|
|
6097
|
-
logger.log(`[skills] Auto-install of "${task.name}" failed (non-fatal): ${err.message}`);
|
|
6098
6479
|
}
|
|
6099
6480
|
}
|
|
6100
6481
|
}
|
|
@@ -6913,7 +7294,7 @@ async function startDaemon(options) {
|
|
|
6913
7294
|
const supervisor = new ProcessSupervisor(join(SVAMP_HOME, "processes"));
|
|
6914
7295
|
await supervisor.init();
|
|
6915
7296
|
const tunnels = /* @__PURE__ */ new Map();
|
|
6916
|
-
const { ServeManager } = await import('./serveManager-
|
|
7297
|
+
const { ServeManager } = await import('./serveManager-rHhOPkW4.mjs');
|
|
6917
7298
|
const serveManager = new ServeManager(SVAMP_HOME, (msg) => logger.log(`[SERVE] ${msg}`), hyphaServerUrl);
|
|
6918
7299
|
ensureAutoInstalledSkills(logger).catch(() => {
|
|
6919
7300
|
});
|
|
@@ -9164,6 +9545,29 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
9164
9545
|
if (persistedMachineMeta) {
|
|
9165
9546
|
logger.log(`Restored machine metadata (sharing=${!!persistedMachineMeta.sharing}, securityContextConfig=${!!persistedMachineMeta.securityContextConfig}, injectPlatformGuidance=${persistedMachineMeta.injectPlatformGuidance})`);
|
|
9166
9547
|
}
|
|
9548
|
+
let seededSharing = persistedMachineMeta?.sharing;
|
|
9549
|
+
const seedEmails = parseDaemonShareEmails(process.env[DAEMON_SHARE_EMAILS_ENV]);
|
|
9550
|
+
if (seedEmails.length > 0) {
|
|
9551
|
+
const ownerEmail = parseJwtEmail(process.env.HYPHA_TOKEN || "") || void 0;
|
|
9552
|
+
const result = applyDaemonShareSeed(seededSharing, seedEmails, ownerEmail);
|
|
9553
|
+
seededSharing = result.sharing;
|
|
9554
|
+
if (result.addedEmails.length > 0) {
|
|
9555
|
+
logger.log(`Seeded machine sharing with ${result.addedEmails.length} user(s): ${result.addedEmails.join(", ")}`);
|
|
9556
|
+
} else {
|
|
9557
|
+
logger.log(`Daemon --share seed: no new users (all already in allowedUsers).`);
|
|
9558
|
+
}
|
|
9559
|
+
savePersistedMachineMetadata(SVAMP_HOME, {
|
|
9560
|
+
sharing: seededSharing,
|
|
9561
|
+
securityContextConfig: persistedMachineMeta?.securityContextConfig,
|
|
9562
|
+
injectPlatformGuidance: persistedMachineMeta?.injectPlatformGuidance
|
|
9563
|
+
});
|
|
9564
|
+
try {
|
|
9565
|
+
updateEnvFile({ [DAEMON_SHARE_EMAILS_ENV]: void 0 });
|
|
9566
|
+
process.env[DAEMON_SHARE_EMAILS_ENV] = "";
|
|
9567
|
+
} catch (err) {
|
|
9568
|
+
logger.log(`[share-seed] Failed to clear ${DAEMON_SHARE_EMAILS_ENV} from .env: ${err.message}`);
|
|
9569
|
+
}
|
|
9570
|
+
}
|
|
9167
9571
|
const machineMetadata = {
|
|
9168
9572
|
host: os$1.hostname(),
|
|
9169
9573
|
platform: os$1.platform(),
|
|
@@ -9173,8 +9577,9 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
9173
9577
|
svampLibDir: join(__dirname$1, ".."),
|
|
9174
9578
|
displayName: process.env.SVAMP_DISPLAY_NAME || void 0,
|
|
9175
9579
|
isolationCapabilities,
|
|
9176
|
-
// Restore persisted sharing
|
|
9177
|
-
|
|
9580
|
+
// Restore persisted sharing (possibly augmented with --share seed above),
|
|
9581
|
+
// security context config, and platform guidance flag.
|
|
9582
|
+
...seededSharing && { sharing: seededSharing },
|
|
9178
9583
|
...persistedMachineMeta?.securityContextConfig && { securityContextConfig: persistedMachineMeta.securityContextConfig },
|
|
9179
9584
|
...persistedMachineMeta?.injectPlatformGuidance !== void 0 && { injectPlatformGuidance: persistedMachineMeta.injectPlatformGuidance }
|
|
9180
9585
|
};
|
|
@@ -9223,6 +9628,15 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
9223
9628
|
}
|
|
9224
9629
|
);
|
|
9225
9630
|
logger.log(`Machine service registered: svamp-machine-${machineId}`);
|
|
9631
|
+
if (seededSharing?.enabled) {
|
|
9632
|
+
const shareUrl = buildMachineShareUrl({
|
|
9633
|
+
machineId,
|
|
9634
|
+
workspace: server.config?.workspace
|
|
9635
|
+
});
|
|
9636
|
+
logger.log(`Machine share URL: ${shareUrl}`);
|
|
9637
|
+
const userList = (seededSharing.allowedUsers || []).map((u) => u.email).join(", ");
|
|
9638
|
+
if (userList) logger.log(` Shared with: ${userList}`);
|
|
9639
|
+
}
|
|
9226
9640
|
machineServiceRef = machineService;
|
|
9227
9641
|
const artifactSync = new SessionArtifactSync(server, logger.log);
|
|
9228
9642
|
const debugService = await registerDebugService(server, machineId, {
|
|
@@ -9243,6 +9657,15 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
9243
9657
|
logger.log(`Debug service registered: svamp-debug-${machineId}`);
|
|
9244
9658
|
await serveManager.restore();
|
|
9245
9659
|
const persistedSessions = loadPersistedSessions();
|
|
9660
|
+
try {
|
|
9661
|
+
const knownIds = new Set(persistedSessions.map((s) => s.sessionId).concat(Object.keys(loadSessionIndex())));
|
|
9662
|
+
const { removed } = await sweepOrphanedStagedHomes(knownIds);
|
|
9663
|
+
if (removed.length > 0) {
|
|
9664
|
+
logger.log(`Cleaned up ${removed.length} orphaned staged home(s) from prior runs.`);
|
|
9665
|
+
}
|
|
9666
|
+
} catch (err) {
|
|
9667
|
+
logger.log(`[staged-homes] sweep failed: ${err.message}`);
|
|
9668
|
+
}
|
|
9246
9669
|
const sessionsToAutoContinue = [];
|
|
9247
9670
|
const sessionsToRalphResume = [];
|
|
9248
9671
|
if (persistedSessions.length > 0) {
|
|
@@ -9922,4 +10345,4 @@ var run = /*#__PURE__*/Object.freeze({
|
|
|
9922
10345
|
stopDaemon: stopDaemon
|
|
9923
10346
|
});
|
|
9924
10347
|
|
|
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,
|
|
10348
|
+
export { DefaultTransport$1 as D, GeminiTransport$1 as G, ServeAuth as S, registerSessionService as a, stopDaemon as b, connectToHypha as c, daemonStatus as d, resolveSecurityContext as e, buildSecurityContextFromFlags as f, getHyphaServerUrl as g, hasCookieToken as h, buildSessionShareUrl as i, buildMachineShareUrl as j, generateHookSettings as k, loadSecurityContextConfig as l, mergeSecurityContexts as m, normalizeAllowedUser as n, acpBackend as o, acpAgentConfig as p, codexMcpBackend as q, registerMachineService as r, startDaemon as s, claudeAuth as t, run as u };
|
|
@@ -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 { resolve, join } from 'node:path';
|
|
4
4
|
import { existsSync, readFileSync, watch } from 'node:fs';
|
|
5
|
-
import { c as connectToHypha, a as registerSessionService,
|
|
5
|
+
import { c as connectToHypha, a as registerSessionService, k as generateHookSettings } from './run-CIPCavEp.mjs';
|
|
6
6
|
import { createServer } from 'node:http';
|
|
7
7
|
import { spawn } from 'node:child_process';
|
|
8
8
|
import { createInterface } from 'node:readline';
|
|
@@ -54,7 +54,7 @@ async function handleServeCommand() {
|
|
|
54
54
|
}
|
|
55
55
|
}
|
|
56
56
|
async function serveAdd(args, machineId) {
|
|
57
|
-
const { connectAndGetMachine } = await import('./commands-
|
|
57
|
+
const { connectAndGetMachine } = await import('./commands-CvWyrVnY.mjs');
|
|
58
58
|
const pos = positionalArgs(args);
|
|
59
59
|
const name = pos[0];
|
|
60
60
|
if (!name) {
|
|
@@ -86,7 +86,7 @@ async function serveAdd(args, machineId) {
|
|
|
86
86
|
}
|
|
87
87
|
}
|
|
88
88
|
async function serveApply(args, machineId) {
|
|
89
|
-
const { connectAndGetMachine } = await import('./commands-
|
|
89
|
+
const { connectAndGetMachine } = await import('./commands-CvWyrVnY.mjs');
|
|
90
90
|
const fs = await import('fs');
|
|
91
91
|
const yaml = await import('yaml');
|
|
92
92
|
const file = positionalArgs(args)[0];
|
|
@@ -171,7 +171,7 @@ async function serveApply(args, machineId) {
|
|
|
171
171
|
}
|
|
172
172
|
}
|
|
173
173
|
async function serveRemove(args, machineId) {
|
|
174
|
-
const { connectAndGetMachine } = await import('./commands-
|
|
174
|
+
const { connectAndGetMachine } = await import('./commands-CvWyrVnY.mjs');
|
|
175
175
|
const pos = positionalArgs(args);
|
|
176
176
|
const name = pos[0];
|
|
177
177
|
if (!name) {
|
|
@@ -191,7 +191,7 @@ async function serveRemove(args, machineId) {
|
|
|
191
191
|
}
|
|
192
192
|
}
|
|
193
193
|
async function serveList(args, machineId) {
|
|
194
|
-
const { connectAndGetMachine } = await import('./commands-
|
|
194
|
+
const { connectAndGetMachine } = await import('./commands-CvWyrVnY.mjs');
|
|
195
195
|
const all = hasFlag(args, "--all", "-a");
|
|
196
196
|
const json = hasFlag(args, "--json");
|
|
197
197
|
const sessionId = getFlag(args, "--session");
|
|
@@ -224,7 +224,7 @@ async function serveList(args, machineId) {
|
|
|
224
224
|
}
|
|
225
225
|
}
|
|
226
226
|
async function serveInfo(machineId) {
|
|
227
|
-
const { connectAndGetMachine } = await import('./commands-
|
|
227
|
+
const { connectAndGetMachine } = await import('./commands-CvWyrVnY.mjs');
|
|
228
228
|
const { machine, server } = await connectAndGetMachine(machineId);
|
|
229
229
|
try {
|
|
230
230
|
const info = await machine.serveInfo();
|