agentglow 0.2.11 → 0.3.0

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 CHANGED
@@ -86,7 +86,7 @@ import dynamic from "next/dynamic";
86
86
  const AgentScene = dynamic(() => import("agentglow").then((m) => m.AgentScene), { ssr: false });
87
87
 
88
88
  export default function Live() {
89
- return <AgentScene theme="subway" source="http://localhost:8100" style={{ height: "80vh" }} />;
89
+ return <AgentScene theme="orbit" source="http://localhost:8100" style={{ height: "80vh" }} />;
90
90
  }
91
91
  ```
92
92
 
@@ -141,27 +141,14 @@ backoff on its own. Changing `scope`, `run` or `token` reconnects and clears the
141
141
  |-----------|---------|
142
142
  | `orbit` | Agents orbit a graph galaxy. Runs are rings and MCP servers are satellites. |
143
143
  | `neural` | A living brain. Agents fire as neurons and messages pulse along synapses. |
144
- | `subway` | A neon transit map. Each run is a line and each agent is a train. |
145
- | `city` | A night city. Agents rise as skyscrapers in run districts. |
146
- | `ocean` | Bioluminescent jellyfish drift on run currents over a coral graph. |
147
- | `circuit` | Agent chips sit on run buses, wired to a memory bank and MCP I/O ports. |
148
- | `tunnel` | A time warp. Runs are lanes and gates, and agents are ships. |
149
144
  | `flow` | A murmuration. Agents condense as eddies out of the current. |
150
- | `hive` | A glowing honeycomb. Agents are bees; subagents fly out as workers. |
151
- | `forest` | A moonlit forest. Agents grow as trees, subagents as saplings, LLM calls as fireflies. |
152
145
  | `constellation` | A night sky. Delegation draws constellation lines between agent stars. |
153
- | `factory` | A neon factory floor. Agents are machines; work rides conveyor belts. |
154
- | `airport` | A radar scope. Agents are flights; handoffs are flight paths. |
155
- | `mycelium`| A glowing fungal network. Agents bloom as mushrooms on spreading threads. |
156
146
  | `atom` | An atom. Agents are electrons; subagents orbit their parent. |
157
147
 
158
148
  | | | |
159
149
  |:-:|:-:|:-:|
160
- | ![neural](https://raw.githubusercontent.com/Nideesh1/agentglow/main/docs/media/neural.jpg) **neural** | ![hive](https://raw.githubusercontent.com/Nideesh1/agentglow/main/docs/media/hive.jpg) **hive** | ![constellation](https://raw.githubusercontent.com/Nideesh1/agentglow/main/docs/media/constellation.jpg) **constellation** |
161
- | ![orbit](https://raw.githubusercontent.com/Nideesh1/agentglow/main/docs/media/orbit.jpg) **orbit** | ![forest](https://raw.githubusercontent.com/Nideesh1/agentglow/main/docs/media/forest.jpg) **forest** | ![mycelium](https://raw.githubusercontent.com/Nideesh1/agentglow/main/docs/media/mycelium.jpg) **mycelium** |
162
- | ![atom](https://raw.githubusercontent.com/Nideesh1/agentglow/main/docs/media/atom.jpg) **atom** | ![airport](https://raw.githubusercontent.com/Nideesh1/agentglow/main/docs/media/airport.jpg) **airport** | ![factory](https://raw.githubusercontent.com/Nideesh1/agentglow/main/docs/media/factory.jpg) **factory** |
163
- | ![city](https://raw.githubusercontent.com/Nideesh1/agentglow/main/docs/media/city.jpg) **city** | ![ocean](https://raw.githubusercontent.com/Nideesh1/agentglow/main/docs/media/ocean.jpg) **ocean** | ![subway](https://raw.githubusercontent.com/Nideesh1/agentglow/main/docs/media/subway.jpg) **subway** |
164
- | ![circuit](https://raw.githubusercontent.com/Nideesh1/agentglow/main/docs/media/circuit.jpg) **circuit** | ![tunnel](https://raw.githubusercontent.com/Nideesh1/agentglow/main/docs/media/tunnel.jpg) **tunnel** | ![flow](https://raw.githubusercontent.com/Nideesh1/agentglow/main/docs/media/flow.jpg) **flow** |
150
+ | ![neural](https://raw.githubusercontent.com/Nideesh1/agentglow/main/docs/media/neural.jpg) **neural** | ![constellation](https://raw.githubusercontent.com/Nideesh1/agentglow/main/docs/media/constellation.jpg) **constellation** | ![orbit](https://raw.githubusercontent.com/Nideesh1/agentglow/main/docs/media/orbit.jpg) **orbit** |
151
+ | ![atom](https://raw.githubusercontent.com/Nideesh1/agentglow/main/docs/media/atom.jpg) **atom** | ![flow](https://raw.githubusercontent.com/Nideesh1/agentglow/main/docs/media/flow.jpg) **flow** | |
165
152
 
166
153
  ## Layout
167
154
 
package/cli/agentglow.mjs CHANGED
@@ -31,6 +31,9 @@ const HELP = `agentglow ${VERSION}: watch Claude Code agents in 3D
31
31
  npx agentglow stop stop the background server
32
32
  npx agentglow remove undo setup: remove the hooks and stop the server
33
33
  npx agentglow claude [-- <args>] try it without installing: one claude session with AgentGlow
34
+ npx agentglow setup --capture-prompts
35
+ also show your prompts next to Claude's replies in the agent panel
36
+ (off by default; local server only, secrets redacted)
34
37
 
35
38
 
36
39
  More: npx agentglow start [--background] [--port N] (serve = start in the foreground)
@@ -61,6 +64,7 @@ function parseArgs(argv) {
61
64
  else if (a === "--uninstall") o.uninstall = true;
62
65
  else if (a === "--remove") o.remove = true;
63
66
  else if (a === "--no-autostart") o.autostart = false;
67
+ else if (a === "--capture-prompts") o.capturePrompts = true;
64
68
  else if (a === "--background" || a === "-b") o.background = true;
65
69
  else if (a === "--quiet" || a === "-q") o.quiet = true;
66
70
  else if (a === "-h" || a === "--help") o.help = true;
@@ -141,6 +145,9 @@ async function cmdSetup(o) {
141
145
  const remote = remoteUrl();
142
146
  const port = o.port;
143
147
  const base = remote || baseUrl({ port });
148
+ // Opt-in: the local server keeps the user's prompt text (login item env + SessionStart command + started server)
149
+ if (o.capturePrompts && !remote) process.env.AGENTGLOW_CAPTURE_PROMPTS = "1";
150
+ const capturePrompts = process.env.AGENTGLOW_CAPTURE_PROMPTS === "1";
144
151
  // The AgentGlow Claude Code plugin brings the same hooks + server start; adding ours too would post every event twice.
145
152
  const plugin = pluginIdsIn(file);
146
153
  if (plugin.length) {
@@ -151,7 +158,7 @@ async function cmdSetup(o) {
151
158
  if (!remote && !plugin.length) {
152
159
  const script = installCliCopy({ srcCliDir: here, version: VERSION });
153
160
  startCommand = startHookCommand({
154
- port, version: VERSION, node: nodeForHook(), script, cacheDir: process.env.AGENTGLOW_CACHE_DIR,
161
+ port, version: VERSION, node: nodeForHook(), script, cacheDir: process.env.AGENTGLOW_CACHE_DIR, capturePrompts,
155
162
  });
156
163
  }
157
164
  const r = installSettingsFile(file, base, { startCommand, hooks: !plugin.length, extra: remote ? {} : { port } });
@@ -184,6 +191,14 @@ async function cmdSetup(o) {
184
191
  return 1;
185
192
  }
186
193
  }
194
+ if (o.capturePrompts && remote) console.log("--capture-prompts is for a local server only; ignored with AGENTGLOW_URL.");
195
+ else if (capturePrompts) {
196
+ const h = await fetchText(localBase(port) + "/live/health", 2000);
197
+ let on = false;
198
+ try { on = JSON.parse(h?.body || "{}").prompts === true; } catch { /* not ours */ }
199
+ console.log(on ? "Prompts: captured (local only)."
200
+ : `Prompts: the running server was started without capture; restart it: npx agentglow stop${portFlag(port)} && npx agentglow start --background${portFlag(port)}`);
201
+ }
187
202
  if (o.open) openBrowser(view);
188
203
  console.log(`\nDone. Just run \`claude\` as usual. View: ${view} Undo: npx agentglow remove`);
189
204
  return 0;
@@ -194,11 +209,14 @@ async function cmdSetup(o) {
194
209
  function installLoginItem(port) {
195
210
  const script = installCliCopy({ srcCliDir: here, version: VERSION });
196
211
  const node = nodeForHook() || process.execPath;
197
- const command = startHookCommand({ port, version: VERSION, node, script, cacheDir: process.env.AGENTGLOW_CACHE_DIR });
212
+ const command = startHookCommand({
213
+ port, version: VERSION, node, script, cacheDir: process.env.AGENTGLOW_CACHE_DIR,
214
+ capturePrompts: process.env.AGENTGLOW_CAPTURE_PROMPTS === "1",
215
+ });
198
216
  // macOS/Linux: the supervisor runs the server in the foreground (argv, no shell); Windows keeps the shell command
199
217
  const argv = [node, script, "start", "--port", String(port)];
200
218
  const env = { PATH: loginPath({ node }), HOME: os.homedir() };
201
- for (const k of ["AGENTGLOW_CACHE_DIR", "AGENTGLOW_API_KEY", "AGENTGLOW_INGEST_KEY", "AGENTGLOW_SECRET", "AGENTGLOW_PY_SPEC"]) {
219
+ for (const k of ["AGENTGLOW_CACHE_DIR", "AGENTGLOW_API_KEY", "AGENTGLOW_INGEST_KEY", "AGENTGLOW_SECRET", "AGENTGLOW_PY_SPEC", "AGENTGLOW_CAPTURE_PROMPTS"]) {
202
220
  if (process.env[k]) env[k] = process.env[k];
203
221
  }
204
222
  try {
@@ -331,6 +349,7 @@ async function cmdStatus(o) {
331
349
  console.log(`hooks: ${hooksLine}`);
332
350
  if (installed && plugin.length) console.log(`warning: hooks are in ${file} AND the plugin is enabled, so events post twice. Run \`npx agentglow remove\` or disable the plugin.`);
333
351
  if (hasAutostart()) console.log(`login: server starts at login and restarts on crash`);
352
+ if (health?.prompts) console.log(`prompts: captured (local only)`);
334
353
  if (health?.ok) console.log(`view: ${base}/neural`);
335
354
  return 0;
336
355
  }
@@ -2,7 +2,7 @@
2
2
  import fs from "node:fs";
3
3
  import path from "node:path";
4
4
  import { spawn, spawnSync } from "node:child_process";
5
- import { probe } from "./net.mjs";
5
+ import { fetchText, probe } from "./net.mjs";
6
6
  import { cacheDir, findRunner } from "./uv.mjs";
7
7
 
8
8
  const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
@@ -19,10 +19,21 @@ export function pySpecs(version, env = process.env) {
19
19
  return /^\d+\.\d+\.\d+$/.test(version || "") ? [`agentglow==${version}`, "agentglow"] : ["agentglow"];
20
20
  }
21
21
 
22
- export function serveArgv(runner, spec, port) {
22
+ /**
23
+ * Launch attempts in order: the pinned spec, the same pin again with uv's cached PyPI index refreshed (a version
24
+ * published after uv last fetched the index is otherwise "not found"), then the latest agentglow.
25
+ */
26
+ export function pyAttempts(version, env = process.env) {
27
+ const [first, ...rest] = pySpecs(version, env);
28
+ const pinned = /^agentglow==/.test(first);
29
+ return [{ spec: first }, ...(pinned ? [{ spec: first, refresh: true }] : []), ...rest.map((spec) => ({ spec }))];
30
+ }
31
+
32
+ export function serveArgv(runner, spec, port, { refresh = false } = {}) {
23
33
  // a local checkout (AGENTGLOW_PY_SPEC=/path/to/backend): run it editable, uvx otherwise serves a stale cached build
24
34
  const local = /^(\/|\.|~|[A-Za-z]:[\\/])/.test(spec);
25
- return [...runner.args, ...(local ? ["--with-editable", spec] : []), "--from", spec,
35
+ return [...runner.args, ...(refresh ? ["--refresh-package", "agentglow"] : []),
36
+ ...(local ? ["--with-editable", spec] : []), "--from", spec,
26
37
  "agentglow", "serve", "--host", "127.0.0.1", "--port", String(port)];
27
38
  }
28
39
 
@@ -37,6 +48,22 @@ export function readPid(port) {
37
48
  } catch { return null; }
38
49
  }
39
50
 
51
+ /** What to log when attempt i failed and attempt i + 1 (if any) comes next. */
52
+ export function retryNote(attempts, i, version) {
53
+ const { spec } = attempts[i], next = attempts[i + 1];
54
+ if (!next) return `${spec} failed to start, giving up`;
55
+ if (next.refresh) return `${spec} failed to start, refreshing uv's cached PyPI index and retrying`;
56
+ return `${spec} failed to start, falling back to the latest agentglow on PyPI` +
57
+ (version ? ` (this CLI expects ${version}, the server may be older)` : "");
58
+ }
59
+
60
+ /** After a fallback start: say which server version is running when it is not the one this CLI expects. */
61
+ async function noteVersion(port, version, log) {
62
+ let v = "";
63
+ try { v = JSON.parse((await fetchText(localBase(port) + "/live/health", 2000))?.body || "{}").version || ""; } catch { /* unknown */ }
64
+ if (v !== version) log(` WARNING: running agentglow server ${v || "?"}, expected ${version}. Restart it once ${version} is on PyPI.`);
65
+ }
66
+
40
67
  function tail(file, n = 15) {
41
68
  try { return fs.readFileSync(file, "utf8").trimEnd().split("\n").slice(-n).join("\n"); } catch { return ""; }
42
69
  }
@@ -50,9 +77,10 @@ export async function startBackground({ port, version, log = console.error, time
50
77
  "First run downloads Python + agentglow, about 30-60s ...");
51
78
  const logPath = logFile(port);
52
79
  let lastErr = "";
53
- for (const spec of pySpecs(version)) {
80
+ const attempts = pyAttempts(version);
81
+ for (const [i, { spec, refresh }] of attempts.entries()) {
54
82
  const fd = fs.openSync(logPath, "w");
55
- const child = spawn(runner.cmd, serveArgv(runner, spec, port), {
83
+ const child = spawn(runner.cmd, serveArgv(runner, spec, port, { refresh }), {
56
84
  detached: true, stdio: ["ignore", fd, fd], windowsHide: true,
57
85
  env: { ...process.env, PYTHONUNBUFFERED: "1" },
58
86
  });
@@ -65,7 +93,10 @@ export async function startBackground({ port, version, log = console.error, time
65
93
  const t0 = Date.now();
66
94
  let noted = false;
67
95
  while (Date.now() - t0 < timeoutMs) {
68
- if ((await probe(localBase(port), 1500)) === "agentglow") return { pid: child.pid, started: true };
96
+ if ((await probe(localBase(port), 1500)) === "agentglow") {
97
+ if (spec !== attempts[0].spec) await noteVersion(port, version, log);
98
+ return { pid: child.pid, started: true };
99
+ }
69
100
  if (exited !== null) break;
70
101
  if (!noted && Date.now() - t0 > 15000) { log(" still preparing the Python environment ..."); noted = true; }
71
102
  await sleep(500);
@@ -77,7 +108,7 @@ export async function startBackground({ port, version, log = console.error, time
77
108
  }
78
109
  fs.rmSync(pidFile(port), { force: true });
79
110
  lastErr = tail(logPath) || lastErr;
80
- log(` ${spec} failed to start, ${spec === "agentglow" ? "giving up" : "retrying with the latest agentglow"} ...`);
111
+ log(` ${retryNote(attempts, i, version)} ...`);
81
112
  }
82
113
  throw new Error(`AgentGlow server failed to start. Log: ${logPath}\n${lastErr}`);
83
114
  }
@@ -166,12 +197,12 @@ export function removeCliCopies() {
166
197
  /** Run the server in the foreground (stdio inherited). Resolves with its exit code. */
167
198
  export async function serveForeground({ port, version, log = console.error }) {
168
199
  const runner = await findRunner({ log });
169
- const specs = pySpecs(version);
200
+ const attempts = pyAttempts(version);
170
201
  let stopping = false;
171
- for (const [i, spec] of specs.entries()) {
202
+ for (const [i, { spec, refresh }] of attempts.entries()) {
172
203
  const t0 = Date.now();
173
204
  const code = await new Promise((resolve) => {
174
- const child = spawn(runner.cmd, serveArgv(runner, spec, port), { stdio: "inherit" });
205
+ const child = spawn(runner.cmd, serveArgv(runner, spec, port, { refresh }), { stdio: "inherit" });
175
206
  // A requested stop (Ctrl-C, `launchctl stop`, `systemctl stop`) must exit 0, or a supervisor with
176
207
  // "restart unless it exited successfully" (launchd KeepAlive/SuccessfulExit) would bring it right back.
177
208
  const fwd = (sig) => () => { stopping = true; child.kill(sig); };
@@ -184,10 +215,10 @@ export async function serveForeground({ port, version, log = console.error }) {
184
215
  resolve(stopping ? 0 : (c ?? (sig ? 130 : 1)));
185
216
  });
186
217
  });
187
- // a pinned version missing on PyPI fails fast; fall back to latest once
218
+ // a pinned version missing from uv's index fails fast: refresh the index once, then fall back to latest
188
219
  if (stopping) return 0;
189
- if (code !== 0 && i < specs.length - 1 && Date.now() - t0 < 30000) {
190
- log(`${spec} failed, retrying with the latest agentglow ...`);
220
+ if (code !== 0 && i < attempts.length - 1 && Date.now() - t0 < 30000) {
221
+ log(`${retryNote(attempts, i, version)} ...`);
191
222
  continue;
192
223
  }
193
224
  return code;
@@ -74,13 +74,13 @@ const shq = (s) => `"${String(s).replace(/(["\\$`])/g, "\\$1")}"`;
74
74
  * Shell command for the SessionStart hook that makes sure the local server runs. Prints nothing (SessionStart stdout
75
75
  * would land in Claude's context) and always exits 0. Calls the CLI copy in the cache directly (~30 ms) and falls
76
76
  * back to the pinned npm version through npx (~0.5 s when cached). Windows without Git Bash runs it in PowerShell,
77
- * so there it is npx only.
77
+ * so there it is npx only. `capturePrompts` (setup --capture-prompts) starts the server with AGENTGLOW_CAPTURE_PROMPTS=1.
78
78
  */
79
- export function startHookCommand({ port, version, node, script, cacheDir: dir, platform = process.platform }) {
79
+ export function startHookCommand({ port, version, node, script, cacheDir: dir, capturePrompts = false, platform = process.platform }) {
80
80
  const args = `${START_MARK} --port ${port}`;
81
81
  const npx = `npx -y agentglow${version ? `@${version}` : ""} ${args}`;
82
82
  if (platform === "win32" || !node || !script) return npx;
83
- const env = dir ? `AGENTGLOW_CACHE_DIR=${shq(dir)} ` : "";
83
+ const env = (capturePrompts ? "AGENTGLOW_CAPTURE_PROMPTS=1 " : "") + (dir ? `AGENTGLOW_CACHE_DIR=${shq(dir)} ` : "");
84
84
  return `${env}${shq(node)} ${shq(script)} ${args} || ${env}${npx}`;
85
85
  }
86
86