svamp-cli 0.2.107 → 0.2.109

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.
Files changed (27) hide show
  1. package/bin/skills/loop/SKILL.md +1 -1
  2. package/bin/skills/loop/bin/inject-loop.mjs +20 -6
  3. package/bin/skills/loop/bin/loop-init.mjs +21 -8
  4. package/bin/skills/loop/bin/loop-status.mjs +17 -4
  5. package/bin/skills/loop/bin/precompact.mjs +5 -2
  6. package/bin/skills/loop/bin/state-fp.mjs +5 -7
  7. package/bin/skills/loop/bin/stop-gate.mjs +17 -4
  8. package/bin/skills/loop/test/test-loop-gate.mjs +55 -18
  9. package/dist/{agentCommands-CduKZKhS.mjs → agentCommands-CoGlvh8y.mjs} +37 -7
  10. package/dist/{auth-Df9Vbe1o.mjs → auth-DMERa7I8.mjs} +2 -2
  11. package/dist/cli.mjs +60 -52
  12. package/dist/{commands-DkDAm71w.mjs → commands-BhkiEmV8.mjs} +5 -5
  13. package/dist/{commands-Db5oGiGF.mjs → commands-D2kGC5mL.mjs} +3 -3
  14. package/dist/{commands-DMfzyn8l.mjs → commands-DJgTzFWk.mjs} +38 -17
  15. package/dist/{commands-B6uZpAXr.mjs → commands-Dy6X_MM5.mjs} +2 -2
  16. package/dist/{commands-Bq2anCn7.mjs → commands-j38M6llT.mjs} +2 -2
  17. package/dist/{fleet-kxxGXRSB.mjs → fleet-Pg9X2izv.mjs} +2 -2
  18. package/dist/{frpc-C9lPF2nK.mjs → frpc-BmpNco2u.mjs} +2 -2
  19. package/dist/{headlessCli-BgWGL3_l.mjs → headlessCli-6WuIXZ9F.mjs} +3 -3
  20. package/dist/index.mjs +2 -2
  21. package/dist/{package-VWSohGLN.mjs → package-Bz0dbAvV.mjs} +2 -2
  22. package/dist/{run-CvT9o581.mjs → run-B3epzMIw.mjs} +3 -3
  23. package/dist/{run-CCsUvTEL.mjs → run-CvXWD1x2.mjs} +313 -58
  24. package/dist/{serveCommands-tid4kA9J.mjs → serveCommands-J44oCE_D.mjs} +5 -5
  25. package/dist/{serveManager-CeM1exTU.mjs → serveManager-DU4OuM57.mjs} +3 -3
  26. package/dist/{sideband-fu9NMI64.mjs → sideband-Dt9N8vh6.mjs} +2 -2
  27. package/package.json +2 -2
@@ -1,15 +1,15 @@
1
1
  import{createRequire as _pkgrollCR}from"node:module";const require=_pkgrollCR(import.meta.url);import os$1, { homedir as homedir$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 as mkdirSync$1, writeFileSync as writeFileSync$1, renameSync as renameSync$1, existsSync as existsSync$1, rmSync as rmSync$1, unlinkSync as unlinkSync$1, copyFileSync, watch, rmdirSync, readdirSync as readdirSync$1 } from 'fs';
4
- import path__default, { join as join$1, dirname, basename, resolve } from 'path';
3
+ import { readFileSync as readFileSync$1, mkdirSync as mkdirSync$1, writeFileSync as writeFileSync$1, renameSync as renameSync$1, existsSync as existsSync$1, rmSync as rmSync$1, unlinkSync as unlinkSync$1, copyFileSync, readdirSync as readdirSync$1, watch, rmdirSync } from 'fs';
4
+ import path__default, { join as join$1, dirname as dirname$1, basename as basename$1, resolve } from 'path';
5
5
  import { fileURLToPath } from 'url';
6
6
  import { execFile, spawn as spawn$1, execSync as execSync$1, spawnSync } from 'child_process';
7
7
  import { randomUUID as randomUUID$1 } from 'crypto';
8
+ import { randomBytes, randomUUID, createHash } from 'node:crypto';
8
9
  import { existsSync, readFileSync, mkdirSync, readdirSync, writeFileSync, renameSync, rmSync, appendFileSync, unlinkSync } from 'node:fs';
9
- import { exec, spawn, execSync, execFile as execFile$1, execFileSync } from 'node:child_process';
10
+ import { exec, execSync, spawn, execFile as execFile$1, execFileSync } from 'node:child_process';
10
11
  import { promisify } from 'util';
11
- import { join } from 'node:path';
12
- import { randomBytes, randomUUID, createHash } from 'node:crypto';
12
+ import { join, basename, dirname } from 'node:path';
13
13
  import os, { homedir, platform } from 'node:os';
14
14
  import { EventEmitter } from 'node:events';
15
15
  import { ndJsonStream, ClientSideConnection } from '@agentclientprotocol/sdk';
@@ -2527,7 +2527,7 @@ async function registerMachineService(server, machineId, metadata, daemonState,
2527
2527
  const tunnels = handlers.tunnels;
2528
2528
  if (!tunnels) throw new Error("Tunnel management not available");
2529
2529
  if (tunnels.has(params.name)) throw new Error(`Tunnel '${params.name}' already running`);
2530
- const { FrpcTunnel } = await import('./frpc-C9lPF2nK.mjs');
2530
+ const { FrpcTunnel } = await import('./frpc-BmpNco2u.mjs');
2531
2531
  const tunnel = new FrpcTunnel({
2532
2532
  name: params.name,
2533
2533
  ports: params.ports,
@@ -2788,7 +2788,7 @@ QUESTION: ${params.question || "Summarize this concisely."}` }
2788
2788
  }
2789
2789
  const deps = buildSessionDeps(rpc, { cwd, ownerEmail: owner });
2790
2790
  const sender = { name: context?.user?.email || context?.user?.id || "user", kind: "user", verified: true };
2791
- const { toolsForRole } = await import('./sideband-fu9NMI64.mjs');
2791
+ const { toolsForRole } = await import('./sideband-Dt9N8vh6.mjs');
2792
2792
  const r2 = await runWiseAgent({ message: params.message, sender, config: { tools: toolsForRole(role2) }, deps, transport, model: resolved.model });
2793
2793
  return fmt(r2);
2794
2794
  }
@@ -2887,7 +2887,7 @@ QUESTION: ${params.question || "Summarize this concisely."}` }
2887
2887
  if (r.error || !r.sender) return { error: r.error || "unauthorized" };
2888
2888
  const callId = "call_" + Math.random().toString(16).slice(2, 12);
2889
2889
  const rendered = renderMessage(c, { sender: r.sender, body: { message: kwargs.message }, callId });
2890
- const { queryCore } = await import('./commands-DMfzyn8l.mjs');
2890
+ const { queryCore } = await import('./commands-DJgTzFWk.mjs');
2891
2891
  const timeout = c.reply?.timeout_sec || 120;
2892
2892
  let result;
2893
2893
  try {
@@ -3349,7 +3349,6 @@ function formatInboxMessageXml(msg) {
3349
3349
  if (msg.from) attrs.push(`from="${escapeXml(msg.from)}"`);
3350
3350
  if (msg.channel) attrs.push(`channel="${escapeXml(msg.channel)}"`);
3351
3351
  if (msg.verified !== void 0) attrs.push(`verified="${msg.verified === true}"`);
3352
- if (msg.fromSession) attrs.push(`from-session="${escapeXml(msg.fromSession)}"`);
3353
3352
  if (msg.to) attrs.push(`to="${escapeXml(msg.to)}"`);
3354
3353
  if (msg.subject) attrs.push(`subject="${escapeXml(msg.subject)}"`);
3355
3354
  if (msg.urgency) attrs.push(`urgency="${msg.urgency}"`);
@@ -3989,6 +3988,16 @@ function createSessionStore(server, sessionId, initialMetadata, initialAgentStat
3989
3988
  callbacks.onArchiveSession();
3990
3989
  return { success: true };
3991
3990
  },
3991
+ // Manual ✨ trigger from the UI: regenerate the session topic + shared project
3992
+ // description via a forked btw (Claude sessions only).
3993
+ regenerateSummary: async (context) => {
3994
+ authorizeRequest(context, metadata.sharing, "interact");
3995
+ if (!callbacks.onRegenerateSummary) {
3996
+ return { success: false, error: "Summary regeneration is not supported for this agent." };
3997
+ }
3998
+ const result = await callbacks.onRegenerateSummary();
3999
+ return { success: true, ...result };
4000
+ },
3992
4001
  // ── Activity ──
3993
4002
  keepAlive: async (thinking, mode, context) => {
3994
4003
  authorizeRequest(context, metadata.sharing, "interact");
@@ -4384,6 +4393,20 @@ function createSessionStore(server, sessionId, initialMetadata, initialAgentStat
4384
4393
  return { store, rpcHandlers };
4385
4394
  }
4386
4395
 
4396
+ const ALPHABET = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
4397
+ const MAX_UNBIASED = Math.floor(256 / ALPHABET.length) * ALPHABET.length;
4398
+ function shortId(length = 10) {
4399
+ let out = "";
4400
+ while (out.length < length) {
4401
+ const buf = randomBytes(length - out.length);
4402
+ for (let i = 0; i < buf.length && out.length < length; i++) {
4403
+ const b = buf[i];
4404
+ if (b < MAX_UNBIASED) out += ALPHABET[b % ALPHABET.length];
4405
+ }
4406
+ }
4407
+ return out;
4408
+ }
4409
+
4387
4410
  const SVAMP_HOME$2 = process.env.SVAMP_HOME || join(os.homedir(), ".svamp");
4388
4411
  const num = (key, def) => {
4389
4412
  const v = Number(process.env[key]);
@@ -4503,6 +4526,97 @@ function classifyInbound(message, now = Date.now()) {
4503
4526
  return { action: "wake", reason: "urgent", breakerTripped: tripped };
4504
4527
  }
4505
4528
 
4529
+ function resolveProjectRoot(directory) {
4530
+ try {
4531
+ const top = execSync("git rev-parse --show-toplevel", {
4532
+ cwd: directory,
4533
+ stdio: ["ignore", "pipe", "ignore"]
4534
+ }).toString().trim();
4535
+ if (top) return top;
4536
+ } catch {
4537
+ }
4538
+ return directory.replace(/[/\\]+$/, "") || directory;
4539
+ }
4540
+ function projectInfoPath(directory) {
4541
+ return join(resolveProjectRoot(directory), ".svamp", "project.json");
4542
+ }
4543
+ function projectName(directory) {
4544
+ return basename(resolveProjectRoot(directory).replace(/[/\\]+$/, "")) || "project";
4545
+ }
4546
+ function readProjectInfo(directory) {
4547
+ try {
4548
+ const p = projectInfoPath(directory);
4549
+ if (!existsSync(p)) return null;
4550
+ const info = JSON.parse(readFileSync(p, "utf-8"));
4551
+ if (info && typeof info === "object") return info;
4552
+ } catch {
4553
+ }
4554
+ return null;
4555
+ }
4556
+ function writeProjectInfo(directory, info) {
4557
+ const p = projectInfoPath(directory);
4558
+ mkdirSync(dirname(p), { recursive: true });
4559
+ const tmp = p + ".tmp";
4560
+ writeFileSync(tmp, JSON.stringify(info, null, 2));
4561
+ renameSync(tmp, p);
4562
+ }
4563
+ function sanitizeDescription(raw, maxLen = 240) {
4564
+ if (!raw) return null;
4565
+ let t = String(raw).replace(/\s+/g, " ").trim();
4566
+ t = t.replace(/^["'`*\s]+|["'`*\s]+$/g, "").trim();
4567
+ if (t.length > maxLen) t = t.slice(0, maxLen - 1).trimEnd() + "\u2026";
4568
+ return t || null;
4569
+ }
4570
+ function extractDescriptionFromDocs(directory) {
4571
+ const root = resolveProjectRoot(directory);
4572
+ const candidates = [
4573
+ [join(root, "CLAUDE.md"), "claude.md"],
4574
+ [join(root, "README.md"), "readme"],
4575
+ [join(root, "readme.md"), "readme"]
4576
+ ];
4577
+ for (const [file, source] of candidates) {
4578
+ try {
4579
+ if (!existsSync(file)) continue;
4580
+ const desc = sanitizeDescription(leadParagraph(readFileSync(file, "utf-8")));
4581
+ if (desc) return { description: desc, source };
4582
+ } catch {
4583
+ }
4584
+ }
4585
+ return null;
4586
+ }
4587
+ function leadParagraph(md) {
4588
+ const buf = [];
4589
+ for (const raw of md.split(/\r?\n/)) {
4590
+ const line = raw.trim();
4591
+ if (!line) {
4592
+ if (buf.length) break;
4593
+ else continue;
4594
+ }
4595
+ if (/^#{1,6}\s/.test(line)) {
4596
+ if (buf.length) break;
4597
+ else continue;
4598
+ }
4599
+ if (/^([![]|<|\||>)/.test(line)) continue;
4600
+ if (/^[-*_=]{3,}$/.test(line)) continue;
4601
+ buf.push(line);
4602
+ if (buf.join(" ").length > 240) break;
4603
+ }
4604
+ const d = buf.join(" ").replace(/\s+/g, " ").trim();
4605
+ return d || null;
4606
+ }
4607
+
4608
+ var projectInfo = /*#__PURE__*/Object.freeze({
4609
+ __proto__: null,
4610
+ extractDescriptionFromDocs: extractDescriptionFromDocs,
4611
+ leadParagraph: leadParagraph,
4612
+ projectInfoPath: projectInfoPath,
4613
+ projectName: projectName,
4614
+ readProjectInfo: readProjectInfo,
4615
+ resolveProjectRoot: resolveProjectRoot,
4616
+ sanitizeDescription: sanitizeDescription,
4617
+ writeProjectInfo: writeProjectInfo
4618
+ });
4619
+
4506
4620
  async function registerDebugService(server, machineId, deps) {
4507
4621
  const serviceInfo = await server.registerService(
4508
4622
  {
@@ -9014,7 +9128,7 @@ function buildBaselineSystemPrompt(sessionId) {
9014
9128
  You are running inside a Svamp session (id: ${sessionId}) on Hypha Cloud. Use the \`svamp\` CLI to manage session state (title, link, notify) and the \`hypha\` CLI for artifacts, tokens, and RPC services. Installed skills live in \`~/.claude/skills/\` \u2014 read them for full references.
9015
9129
 
9016
9130
  **Session state:**
9017
- - \`svamp session set-title "<title>"\` \u2014 set a concise 3-8 word title after the first response, and update it whenever the topic shifts. This is how the user and other agents recognize you in lists \u2014 keep it current.
9131
+ - \`svamp session set-title "<topic>"\` \u2014 your session topic: a short sentence describing what you're working on. It's the index the user and other agents see when listing sessions, so set it after your first reply and update it as focus shifts (else it's auto-summarized for you).
9018
9132
  - \`svamp session set-link "<url>" "<label>"\` \u2014 surface any viewable artifact as a button
9019
9133
  - \`svamp session notify "<msg>" [--level info|warning|error]\` \u2014 send a user notification
9020
9134
 
@@ -9024,7 +9138,7 @@ You are running inside a Svamp session (id: ${sessionId}) on Hypha Cloud. Use th
9024
9138
 
9025
9139
  You share this machine and project folder with other agent sessions (same user, possibly other machines too). When collaboration, coordination, or avoiding edit conflicts becomes relevant, run \`svamp session whoami\` \u2014 it shows who's around and how to reach them. Keep cross-agent contact purposeful: only initiate with a concrete reason, avoid ping-pong loops.
9026
9140
 
9027
- **Inbox messages from other agents** arrive wrapped as \`<svamp-message message-id="\u2026" from="agent:\u2026" from-session="\u2026" \u2026>BODY</svamp-message>\` (a plain user turn has no wrapper). Reply only when useful: \`svamp session inbox reply <message-id> "<body>"\`.
9141
+ **Inbox messages from other agents** arrive wrapped as \`<svamp-message message-id="\u2026" from="agent:\u2026" \u2026>BODY</svamp-message>\` (a plain user turn has no wrapper). Reply only when useful: \`svamp session inbox reply <message-id> "<body>"\` \u2014 the message-id is all you need; routing back to the sender is automatic.
9028
9142
  `;
9029
9143
  }
9030
9144
 
@@ -9312,7 +9426,7 @@ async function readSessionFileBase64(resolvedPath) {
9312
9426
  }
9313
9427
 
9314
9428
  const __filename$1 = fileURLToPath(import.meta.url);
9315
- const __dirname$1 = dirname(__filename$1);
9429
+ const __dirname$1 = dirname$1(__filename$1);
9316
9430
  const CLAUDE_SKILLS_DIR = join$1(os$1.homedir(), ".claude", "skills");
9317
9431
  function looksLikeClaudeError(line) {
9318
9432
  const l = line.toLowerCase();
@@ -9389,7 +9503,7 @@ async function installSkillFromEndpoint(name, baseUrl) {
9389
9503
  const content = await fileResp.text();
9390
9504
  const localPath = join$1(targetDir, filePath);
9391
9505
  if (!localPath.startsWith(targetDir + "/")) continue;
9392
- mkdirSync$1(dirname(localPath), { recursive: true });
9506
+ mkdirSync$1(dirname$1(localPath), { recursive: true });
9393
9507
  writeFileSync$1(localPath, content, "utf-8");
9394
9508
  }
9395
9509
  }
@@ -9423,7 +9537,7 @@ async function installSkillFromMarketplace(name) {
9423
9537
  const content = await resp.text();
9424
9538
  const localPath = join$1(targetDir, filePath);
9425
9539
  if (!localPath.startsWith(targetDir + "/")) continue;
9426
- mkdirSync$1(dirname(localPath), { recursive: true });
9540
+ mkdirSync$1(dirname$1(localPath), { recursive: true });
9427
9541
  writeFileSync$1(localPath, content, "utf-8");
9428
9542
  }
9429
9543
  }
@@ -9431,9 +9545,9 @@ function getBundledSkillsDir() {
9431
9545
  try {
9432
9546
  const here = fileURLToPath(import.meta.url);
9433
9547
  const candidates = [
9434
- join$1(dirname(here), "..", "bin", "skills"),
9548
+ join$1(dirname$1(here), "..", "bin", "skills"),
9435
9549
  // built dist/ layout
9436
- join$1(dirname(here), "..", "..", "bin", "skills")
9550
+ join$1(dirname$1(here), "..", "..", "bin", "skills")
9437
9551
  // src/daemon → bin layout via tsx
9438
9552
  ];
9439
9553
  for (const c of candidates) {
@@ -9691,36 +9805,43 @@ function readSvampConfig(configPath) {
9691
9805
  return {};
9692
9806
  }
9693
9807
  function writeSvampConfig(configPath, config) {
9694
- mkdirSync$1(dirname(configPath), { recursive: true });
9808
+ mkdirSync$1(dirname$1(configPath), { recursive: true });
9695
9809
  const content = JSON.stringify(config, null, 2);
9696
9810
  const tmpPath = configPath + ".tmp";
9697
9811
  writeFileSync$1(tmpPath, content);
9698
9812
  renameSync$1(tmpPath, configPath);
9699
9813
  return content;
9700
9814
  }
9701
- function getLoopDir(directory) {
9815
+ function getLoopDir(directory, sessionId) {
9816
+ if (sessionId) {
9817
+ const scoped = join$1(directory, ".svamp", sessionId, "loop");
9818
+ if (existsSync$1(join$1(scoped, "loop-state.json"))) return scoped;
9819
+ const legacy = join$1(directory, ".claude", "loop");
9820
+ if (existsSync$1(join$1(legacy, "loop-state.json"))) return legacy;
9821
+ return scoped;
9822
+ }
9702
9823
  return join$1(directory, ".claude", "loop");
9703
9824
  }
9704
- function readLoopState(directory) {
9825
+ function readLoopState(directory, sessionId) {
9705
9826
  try {
9706
- const p = join$1(getLoopDir(directory), "loop-state.json");
9827
+ const p = join$1(getLoopDir(directory, sessionId), "loop-state.json");
9707
9828
  if (!existsSync$1(p)) return null;
9708
9829
  return JSON.parse(readFileSync$1(p, "utf-8"));
9709
9830
  } catch {
9710
9831
  return null;
9711
9832
  }
9712
9833
  }
9713
- function isLoopActive(directory) {
9714
- const s = readLoopState(directory);
9834
+ function isLoopActive(directory, sessionId) {
9835
+ const s = readLoopState(directory, sessionId);
9715
9836
  return !!s && s.active !== false && s.phase !== "done" && s.phase !== "gave_up" && s.phase !== "cancelled";
9716
9837
  }
9717
- function loopOwnerSession(directory) {
9718
- const s = readLoopState(directory);
9838
+ function loopOwnerSession(directory, sessionId) {
9839
+ const s = readLoopState(directory, sessionId);
9719
9840
  if (!s || s.active === false || s.phase === "done" || s.phase === "gave_up" || s.phase === "cancelled") return null;
9720
9841
  return typeof s.session_id === "string" ? s.session_id : null;
9721
9842
  }
9722
9843
  function isLoopActiveForSession(directory, sessionId) {
9723
- const s = readLoopState(directory);
9844
+ const s = readLoopState(directory, sessionId);
9724
9845
  if (!s || s.active === false || s.phase === "done" || s.phase === "gave_up" || s.phase === "cancelled") return false;
9725
9846
  if (typeof s.session_id !== "string") return true;
9726
9847
  return s.session_id === sessionId;
@@ -9746,9 +9867,9 @@ function initLoop(directory, cfg) {
9746
9867
  const res = spawnSync(process.execPath, args, { encoding: "utf-8", timeout: 3e4 });
9747
9868
  return res.status === 0;
9748
9869
  }
9749
- function deactivateLoop(directory) {
9870
+ function deactivateLoop(directory, sessionId) {
9750
9871
  try {
9751
- const p = join$1(getLoopDir(directory), "loop-state.json");
9872
+ const p = join$1(getLoopDir(directory, sessionId), "loop-state.json");
9752
9873
  if (!existsSync$1(p)) return;
9753
9874
  const s = JSON.parse(readFileSync$1(p, "utf-8"));
9754
9875
  s.active = false;
@@ -9873,7 +9994,7 @@ function createSvampConfigChecker(directory, sessionId, getMetadata, setMetadata
9873
9994
  logger.log(`[svampConfig] Loop init failed \u2014 loop-init.mjs not found`);
9874
9995
  }
9875
9996
  } else {
9876
- deactivateLoop(directory);
9997
+ deactivateLoop(directory, sessionId);
9877
9998
  sessionService.pushMessage({ type: "message", message: "Loop cancelled." }, "event");
9878
9999
  logger.log(`[svampConfig] Loop cancelled`);
9879
10000
  }
@@ -9892,7 +10013,7 @@ function createSvampConfigChecker(directory, sessionId, getMetadata, setMetadata
9892
10013
  };
9893
10014
  let watcher = null;
9894
10015
  try {
9895
- const configDir = dirname(configPath);
10016
+ const configDir = dirname$1(configPath);
9896
10017
  mkdirSync$1(configDir, { recursive: true });
9897
10018
  watcher = watch(configDir, (eventType, filename) => {
9898
10019
  if (filename === "config.json") configChecker();
@@ -10307,7 +10428,7 @@ async function startDaemon(options) {
10307
10428
  const list = loadExposedTunnels().filter((t) => t.name !== name);
10308
10429
  saveExposedTunnels(list);
10309
10430
  }
10310
- const { ServeManager } = await import('./serveManager-CeM1exTU.mjs');
10431
+ const { ServeManager } = await import('./serveManager-DU4OuM57.mjs');
10311
10432
  const serveManager = new ServeManager(SVAMP_HOME, (msg) => logger.log(`[SERVE] ${msg}`), hyphaServerUrl);
10312
10433
  ensureAutoInstalledSkills(logger).catch(() => {
10313
10434
  });
@@ -10399,7 +10520,7 @@ async function startDaemon(options) {
10399
10520
  return { type: "error", errorMessage: `Failed to create directory: ${err.message}` };
10400
10521
  }
10401
10522
  }
10402
- const sessionId = options2.sessionId || randomUUID$1();
10523
+ const sessionId = options2.sessionId || shortId();
10403
10524
  const agentName = options2.agent || agentConfig.agent_type || "claude";
10404
10525
  if (agentName !== "claude" && (KNOWN_ACP_AGENTS[agentName] || KNOWN_MCP_AGENTS[agentName])) {
10405
10526
  return await spawnAgentSession(sessionId, directory, agentName, options2, resumeSessionId);
@@ -10524,6 +10645,135 @@ async function startDaemon(options) {
10524
10645
  let lastSpawnMeta = persisted?.spawnMeta || {};
10525
10646
  let sessionWasProcessing = !!options2.wasProcessing;
10526
10647
  let lastMainModel;
10648
+ const AUTO_TOPIC_DISABLED = process.env.SVAMP_AUTO_TOPIC === "0";
10649
+ const projName = projectName(directory);
10650
+ let bootstrapAttempted = false;
10651
+ let topicBtwInFlight = false;
10652
+ const sanitizeTopic = (raw) => sanitizeDescription(raw == null ? null : String(raw).split(/\r?\n/).find((l) => l.trim()) || "", 140);
10653
+ const applyTopic = (topic) => {
10654
+ sessionMetadata = { ...sessionMetadata, summary: { text: topic, updatedAt: Date.now() } };
10655
+ sessionService.updateMetadata(sessionMetadata);
10656
+ sessionService.pushMessage({ type: "summary", summary: topic }, "session");
10657
+ };
10658
+ const applyProjectToMeta = (name, description) => {
10659
+ if (sessionMetadata.projectName === name && sessionMetadata.projectDescription === description) return;
10660
+ sessionMetadata = { ...sessionMetadata, projectName: name, projectDescription: description };
10661
+ sessionService.updateMetadata(sessionMetadata);
10662
+ };
10663
+ const forkBtw = (question) => new Promise((resolve2) => {
10664
+ const claudeSessionId = sessionMetadata.claudeSessionId;
10665
+ if (!claudeSessionId) {
10666
+ resolve2(null);
10667
+ return;
10668
+ }
10669
+ const cwd = sessionMetadata.path || directory;
10670
+ const model = process.env.SVAMP_TOPIC_MODEL;
10671
+ try {
10672
+ const child = spawn$1("claude", [
10673
+ "--print",
10674
+ question,
10675
+ "--resume",
10676
+ claudeSessionId,
10677
+ "--fork-session",
10678
+ "--no-session-persistence",
10679
+ "--permission-mode",
10680
+ "bypassPermissions",
10681
+ ...model ? ["--model", model] : [],
10682
+ "--output-format",
10683
+ "json",
10684
+ "--max-turns",
10685
+ "6"
10686
+ ], { cwd, timeout: 6e4, stdio: ["ignore", "pipe", "pipe"], env: { ...process.env, CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: "1" } });
10687
+ let stdout = "";
10688
+ child.stdout?.on("data", (d) => {
10689
+ stdout += d.toString();
10690
+ });
10691
+ child.on("close", () => {
10692
+ try {
10693
+ const r = JSON.parse(stdout);
10694
+ resolve2(r.result || r.text || null);
10695
+ } catch {
10696
+ resolve2(null);
10697
+ }
10698
+ });
10699
+ child.on("error", () => resolve2(null));
10700
+ } catch {
10701
+ resolve2(null);
10702
+ }
10703
+ });
10704
+ const parseLabeled = (raw, label) => {
10705
+ if (!raw) return null;
10706
+ const m = raw.match(new RegExp(`${label}\\s*:\\s*(.+)`, "i"));
10707
+ return m ? m[1].trim() : null;
10708
+ };
10709
+ const generateSummary = async (force) => {
10710
+ if (topicBtwInFlight) return {};
10711
+ const existing = readProjectInfo(directory);
10712
+ applyProjectToMeta(existing?.name || projName, existing?.description);
10713
+ let projectDescription = existing?.description;
10714
+ let haveProject = !!projectDescription;
10715
+ if (force || !haveProject) {
10716
+ const docs = extractDescriptionFromDocs(directory);
10717
+ if (docs) {
10718
+ writeProjectInfo(directory, { name: projName, description: docs.description, source: docs.source, updatedAt: Date.now() });
10719
+ applyProjectToMeta(projName, docs.description);
10720
+ projectDescription = docs.description;
10721
+ haveProject = true;
10722
+ }
10723
+ }
10724
+ const needTopic = force || !sessionMetadata.summary?.text?.trim();
10725
+ const needProjectBtw = !haveProject;
10726
+ let topic = sessionMetadata.summary?.text;
10727
+ if (!needTopic && !needProjectBtw || trackedSession?.stopped) return { topic, projectDescription };
10728
+ topicBtwInFlight = true;
10729
+ try {
10730
+ const parts = [];
10731
+ if (needTopic) parts.push("TOPIC: <one sentence (10-18 words) on what THIS session is currently working on>");
10732
+ if (needProjectBtw) parts.push("PROJECT: <one sentence on what this project/folder is about overall \u2014 its purpose, not this session's task>");
10733
+ const raw = await forkBtw(`Based only on the conversation you can already see (do NOT use any tools), output EXACTLY the following line(s), nothing else:
10734
+ ${parts.join("\n")}`);
10735
+ if (needTopic) {
10736
+ const t = sanitizeTopic(needProjectBtw ? parseLabeled(raw, "TOPIC") : parseLabeled(raw, "TOPIC") || raw);
10737
+ if (t) {
10738
+ applyTopic(t);
10739
+ topic = t;
10740
+ logger.log(`[Session ${sessionId}] ${force ? "Regenerated" : "Auto"}-topic \u2192 "${t}"`);
10741
+ } else if (!sessionMetadata.summary?.text?.trim()) {
10742
+ const b = basename$1(directory.replace(/[/\\]+$/, "")) || "session";
10743
+ applyTopic(b);
10744
+ topic = b;
10745
+ }
10746
+ }
10747
+ if (needProjectBtw) {
10748
+ const d = sanitizeDescription(parseLabeled(raw, "PROJECT") || (!needTopic ? raw : null));
10749
+ if (d) {
10750
+ writeProjectInfo(directory, { name: projName, description: d, source: "btw", updatedAt: Date.now() });
10751
+ applyProjectToMeta(projName, d);
10752
+ projectDescription = d;
10753
+ logger.log(`[Session ${sessionId}] Project description (btw) \u2192 "${d}"`);
10754
+ }
10755
+ }
10756
+ } catch {
10757
+ } finally {
10758
+ topicBtwInFlight = false;
10759
+ }
10760
+ return { topic, projectDescription };
10761
+ };
10762
+ const maybeBootstrap = () => {
10763
+ try {
10764
+ const existing = readProjectInfo(directory);
10765
+ applyProjectToMeta(existing?.name || projName, existing?.description);
10766
+ if (bootstrapAttempted || topicBtwInFlight) return;
10767
+ if (AUTO_TOPIC_DISABLED || trackedSession?.stopped || isLoopActive(directory)) return;
10768
+ bootstrapAttempted = true;
10769
+ void generateSummary(false);
10770
+ } catch {
10771
+ }
10772
+ };
10773
+ const regenerateSummaryNow = async () => {
10774
+ bootstrapAttempted = true;
10775
+ return await generateSummary(true);
10776
+ };
10527
10777
  let spawnHasReceivedInit = false;
10528
10778
  let startupFailureRetryPending = false;
10529
10779
  let startupRetryMessage;
@@ -10546,7 +10796,7 @@ async function startDaemon(options) {
10546
10796
  stuckWatchdogTimer = setInterval(() => {
10547
10797
  if (!claudeProcess || claudeProcess.exitCode !== null) return;
10548
10798
  if (!sessionWasProcessing) return;
10549
- if (!isLoopActive(directory)) return;
10799
+ if (!isLoopActive(directory, sessionId)) return;
10550
10800
  if (claudeProcess.pid && hasActiveChildren(claudeProcess.pid)) {
10551
10801
  lastOutputTime = Date.now();
10552
10802
  return;
@@ -10560,7 +10810,7 @@ async function startDaemon(options) {
10560
10810
  );
10561
10811
  claudeProcess.kill("SIGTERM");
10562
10812
  setTimeout(() => {
10563
- if (!trackedSession.stopped && isLoopActive(directory)) {
10813
+ if (!trackedSession.stopped && isLoopActive(directory, sessionId)) {
10564
10814
  logger.log(`[Session ${sessionId}] Stuck watchdog: nudging loop to resume`);
10565
10815
  enqueueLoopContinue();
10566
10816
  processMessageQueueRef?.();
@@ -10668,7 +10918,7 @@ async function startDaemon(options) {
10668
10918
  if (options2.forceIsolation || sessionMetadata.sharing?.enabled) {
10669
10919
  rawPermissionMode = rawPermissionMode === "default" ? "auto-approve-all" : rawPermissionMode;
10670
10920
  }
10671
- if (isLoopActive(directory)) {
10921
+ if (isLoopActiveForSession(directory, sessionId)) {
10672
10922
  rawPermissionMode = "bypassPermissions";
10673
10923
  }
10674
10924
  const permissionMode = toClaudePermissionMode(rawPermissionMode);
@@ -10941,7 +11191,7 @@ async function startDaemon(options) {
10941
11191
  }
10942
11192
  }
10943
11193
  if (msg.type === "result") {
10944
- const loopActive = isLoopActive(directory);
11194
+ const loopActive = isLoopActiveForSession(directory, sessionId);
10945
11195
  if (!turnInitiatedByUser && !loopActive) {
10946
11196
  logger.log(`[Session ${sessionId}] Skipping stale result from SDK-initiated turn`);
10947
11197
  const hasBackgroundTasks = backgroundTaskCount > 0;
@@ -11005,17 +11255,7 @@ async function startDaemon(options) {
11005
11255
  }
11006
11256
  checkSvampConfig?.();
11007
11257
  clearInboundContext(sessionId);
11008
- try {
11009
- const hasTitle = !!sessionMetadata.summary?.text?.trim() || !!sessionMetadata.customTitle?.toString().trim();
11010
- if (!hasTitle) {
11011
- const base = basename(directory.replace(/[/\\]+$/, "")) || "session";
11012
- sessionMetadata = { ...sessionMetadata, summary: { text: base, updatedAt: Date.now() } };
11013
- sessionService.updateMetadata(sessionMetadata);
11014
- sessionService.pushMessage({ type: "summary", summary: base }, "session");
11015
- logger.log(`[Session ${sessionId}] Auto-title fallback \u2192 "${base}"`);
11016
- }
11017
- } catch {
11018
- }
11258
+ maybeBootstrap();
11019
11259
  if (backgroundTaskCount > 0) {
11020
11260
  const taskInfo = `Background tasks still running (${backgroundTaskCount}): ${backgroundTaskNames.join(", ")}`;
11021
11261
  logger.log(`[Session ${sessionId}] ${taskInfo}`);
@@ -11582,6 +11822,10 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
11582
11822
  logger.log(`[Session ${sessionId}] Archive session requested`);
11583
11823
  archiveSession(sessionId);
11584
11824
  },
11825
+ onRegenerateSummary: async () => {
11826
+ logger.log(`[Session ${sessionId}] Manual summary regeneration requested`);
11827
+ return await regenerateSummaryNow();
11828
+ },
11585
11829
  onInboxMessage: (message) => {
11586
11830
  if (trackedSession?.stopped) return;
11587
11831
  const decision = classifyInbound(message);
@@ -11712,7 +11956,7 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
11712
11956
  if (sessionMetadata.securityContext && resolvedPath !== resolve(directory) && !resolvedPath.startsWith(resolve(directory) + "/")) {
11713
11957
  throw new Error("Path outside working directory");
11714
11958
  }
11715
- await fs.mkdir(dirname(resolvedPath), { recursive: true });
11959
+ await fs.mkdir(dirname$1(resolvedPath), { recursive: true });
11716
11960
  await fs.writeFile(resolvedPath, Buffer.from(content, "base64"));
11717
11961
  },
11718
11962
  onListDirectory: async (path) => {
@@ -11759,7 +12003,7 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
11759
12003
  if (sessionMetadata.securityContext && resolvedPath !== resolve(directory) && !resolvedPath.startsWith(resolve(directory) + "/")) {
11760
12004
  throw new Error("Path outside working directory");
11761
12005
  }
11762
- const tree = await buildTree(resolvedPath, basename(resolvedPath), 0);
12006
+ const tree = await buildTree(resolvedPath, basename$1(resolvedPath), 0);
11763
12007
  return { success: !!tree, tree };
11764
12008
  }
11765
12009
  },
@@ -12190,7 +12434,7 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
12190
12434
  if (sessionMetadata.securityContext && resolvedPath !== resolve(directory) && !resolvedPath.startsWith(resolve(directory) + "/")) {
12191
12435
  throw new Error("Path outside working directory");
12192
12436
  }
12193
- await fs.mkdir(dirname(resolvedPath), { recursive: true });
12437
+ await fs.mkdir(dirname$1(resolvedPath), { recursive: true });
12194
12438
  await fs.writeFile(resolvedPath, Buffer.from(content, "base64"));
12195
12439
  },
12196
12440
  onListDirectory: async (path) => {
@@ -12237,7 +12481,7 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
12237
12481
  if (sessionMetadata.securityContext && resolvedPath !== resolve(directory) && !resolvedPath.startsWith(resolve(directory) + "/")) {
12238
12482
  throw new Error("Path outside working directory");
12239
12483
  }
12240
- const tree = await buildTree(resolvedPath, basename(resolvedPath), 0);
12484
+ const tree = await buildTree(resolvedPath, basename$1(resolvedPath), 0);
12241
12485
  return { success: !!tree, tree };
12242
12486
  }
12243
12487
  },
@@ -12337,7 +12581,7 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
12337
12581
  try {
12338
12582
  const hasTitle = !!sessionMetadata.summary?.text?.trim() || !!sessionMetadata.customTitle?.toString().trim();
12339
12583
  if (!hasTitle) {
12340
- const base = basename(directory.replace(/[/\\]+$/, "")) || "session";
12584
+ const base = basename$1(directory.replace(/[/\\]+$/, "")) || "session";
12341
12585
  sessionMetadata = { ...sessionMetadata, summary: { text: base, updatedAt: Date.now() } };
12342
12586
  sessionService.updateMetadata(sessionMetadata);
12343
12587
  sessionService.pushMessage({ type: "summary", summary: base }, "session");
@@ -12464,7 +12708,7 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
12464
12708
  const wasInMemory = teardownTrackedSession(sessionId);
12465
12709
  const markedArchived = markSessionAsArchived(sessionId);
12466
12710
  if (loopDir && isLoopActiveForSession(loopDir, sessionId)) {
12467
- deactivateLoop(loopDir);
12711
+ deactivateLoop(loopDir, sessionId);
12468
12712
  logger.log(`Deactivated loop for archived session ${sessionId}`);
12469
12713
  }
12470
12714
  if (wasInMemory || markedArchived) {
@@ -12521,7 +12765,7 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
12521
12765
  teardownTrackedSession(sessionId);
12522
12766
  deletePersistedSession(sessionId);
12523
12767
  if (loopDir && isLoopActiveForSession(loopDir, sessionId)) {
12524
- deactivateLoop(loopDir);
12768
+ deactivateLoop(loopDir, sessionId);
12525
12769
  }
12526
12770
  logger.log(`Session ${sessionId} deleted`);
12527
12771
  return true;
@@ -12669,7 +12913,7 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
12669
12913
  const specs = loadExposedTunnels();
12670
12914
  if (specs.length === 0) return;
12671
12915
  logger.log(`[exposed-tunnels] Restoring ${specs.length} tunnel(s) from ${EXPOSED_TUNNELS_FILE}`);
12672
- const { FrpcTunnel } = await import('./frpc-C9lPF2nK.mjs');
12916
+ const { FrpcTunnel } = await import('./frpc-BmpNco2u.mjs');
12673
12917
  for (const spec of specs) {
12674
12918
  if (tunnels.has(spec.name)) continue;
12675
12919
  try {
@@ -12757,10 +13001,21 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
12757
13001
  for (const p of persistedSessions) {
12758
13002
  if (sweptDirs.has(p.directory)) continue;
12759
13003
  sweptDirs.add(p.directory);
12760
- const owner = loopOwnerSession(p.directory);
12761
- if (owner && !knownSessionIds.has(owner)) {
13004
+ try {
13005
+ for (const ent of readdirSync$1(join$1(p.directory, ".svamp"), { withFileTypes: true })) {
13006
+ if (!ent.isDirectory() || knownSessionIds.has(ent.name)) continue;
13007
+ const owner = loopOwnerSession(p.directory, ent.name);
13008
+ if (owner && !knownSessionIds.has(owner)) {
13009
+ deactivateLoop(p.directory, ent.name);
13010
+ logger.log(`[loop] Deactivated stale loop-state for ${ent.name} in ${p.directory} (owner no longer known)`);
13011
+ }
13012
+ }
13013
+ } catch {
13014
+ }
13015
+ const legacyOwner = loopOwnerSession(p.directory);
13016
+ if (legacyOwner && !knownSessionIds.has(legacyOwner)) {
12762
13017
  deactivateLoop(p.directory);
12763
- logger.log(`[loop] Deactivated stale loop-state in ${p.directory} (owner session ${owner} no longer known)`);
13018
+ logger.log(`[loop] Deactivated stale legacy loop-state in ${p.directory} (owner session ${legacyOwner} no longer known)`);
12764
13019
  }
12765
13020
  }
12766
13021
  }
@@ -13431,4 +13686,4 @@ var run = /*#__PURE__*/Object.freeze({
13431
13686
  writeStopMarker: writeStopMarker
13432
13687
  });
13433
13688
 
13434
- export { loadMachineContext as A, buildMachineInstructions as B, machineToolsForRole as C, buildMachineTools as D, resolveModel as E, normalizeAllowedUser as F, loadSecurityContextConfig as G, resolveSecurityContext as H, buildSecurityContextFromFlags as I, mergeSecurityContexts as J, buildSessionShareUrl as K, computeOutboundHop as L, buildMachineShareUrl as M, describeMisconfiguration as N, buildMachineDeps as O, generateHookSettings as P, DefaultTransport$1 as Q, RoutineStore as R, ServeAuth as S, acpBackend as T, acpAgentConfig as U, codexMcpBackend as V, GeminiTransport$1 as W, claudeAuth as X, instanceConfig as Y, api as Z, run as _, createSessionStore as a, stopDaemon as b, connectToHypha as c, daemonStatus as d, clearStopMarker as e, stopMarkerExists as f, getHyphaServerUrl$1 as g, getFrpsSubdomainHost as h, getFrpsServerPort as i, getFrpsServerAddr as j, getHyphaServerUrl as k, hasCookieToken as l, RoutineRunner as m, getSkillsServer as n, getSkillsWorkspaceName as o, parseFrontmatter as p, getSkillsCollectionName as q, registerMachineService as r, startDaemon as s, fetchWithTimeout as t, searchSkills as u, SKILLS_DIR as v, getSkillInfo as w, downloadSkillFile as x, listSkillFiles as y, READ_ONLY_TOOLS as z };
13689
+ export { api as $, READ_ONLY_TOOLS as A, loadMachineContext as B, buildMachineInstructions as C, machineToolsForRole as D, buildMachineTools as E, resolveModel as F, normalizeAllowedUser as G, loadSecurityContextConfig as H, resolveSecurityContext as I, buildSecurityContextFromFlags as J, mergeSecurityContexts as K, buildSessionShareUrl as L, computeOutboundHop as M, buildMachineShareUrl as N, describeMisconfiguration as O, buildMachineDeps as P, generateHookSettings as Q, RoutineStore as R, ServeAuth as S, projectInfo as T, DefaultTransport$1 as U, acpBackend as V, acpAgentConfig as W, codexMcpBackend as X, GeminiTransport$1 as Y, claudeAuth as Z, instanceConfig as _, createSessionStore as a, run as a0, stopDaemon as b, connectToHypha as c, daemonStatus as d, clearStopMarker as e, stopMarkerExists as f, getHyphaServerUrl$1 as g, getFrpsSubdomainHost as h, getFrpsServerPort as i, getFrpsServerAddr as j, getHyphaServerUrl as k, hasCookieToken as l, RoutineRunner as m, shortId as n, getSkillsServer as o, parseFrontmatter as p, getSkillsWorkspaceName as q, registerMachineService as r, startDaemon as s, getSkillsCollectionName as t, fetchWithTimeout as u, searchSkills as v, SKILLS_DIR as w, getSkillInfo as x, downloadSkillFile as y, listSkillFiles as z };