agentmomo 0.2.0 → 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/dist/server.js CHANGED
@@ -49915,7 +49915,7 @@ import crypto3 from "crypto";
49915
49915
  import fs2 from "fs";
49916
49916
  import path3 from "path";
49917
49917
  import { fileURLToPath } from "url";
49918
- import { exec } from "child_process";
49918
+ import { exec, spawn } from "child_process";
49919
49919
 
49920
49920
  // node_modules/ws/wrapper.mjs
49921
49921
  var import_stream = __toESM(require_stream2(), 1);
@@ -55181,6 +55181,12 @@ async function checkAndDeductCredits(userId, event) {
55181
55181
  async function getCreditBalance(userId) {
55182
55182
  const supabase2 = getSupabase();
55183
55183
  if (!supabase2) return null;
55184
+ const { error: rechargeError } = await supabase2.rpc("recharge_credits", {
55185
+ p_user_id: userId
55186
+ });
55187
+ if (rechargeError && !rechargeError.message.toLowerCase().includes("recharge_credits")) {
55188
+ console.warn("[credits] recharge_credits failed:", rechargeError.message);
55189
+ }
55184
55190
  const { data, error } = await supabase2.from("user_profiles").select("credits_remaining, credits_used_total, tier").eq("id", userId).single();
55185
55191
  if (error || !data) return null;
55186
55192
  return {
@@ -55210,6 +55216,7 @@ var WRAPPER_JS = AGENTSMOMO_DIST_DIR ? path3.join(AGENTSMOMO_DIST_DIR, "stdio-wr
55210
55216
  var HOOK_JS = AGENTSMOMO_DIST_DIR ? path3.join(AGENTSMOMO_DIST_DIR, "claude-code-hook.cjs") : path3.join(process.cwd(), "proxy", "dist", "claude-code-hook.cjs");
55211
55217
  var GEMINI_HOOK_JS = AGENTSMOMO_DIST_DIR ? path3.join(AGENTSMOMO_DIST_DIR, "gemini-cli-hook.cjs") : path3.join(process.cwd(), "proxy", "dist", "gemini-cli-hook.cjs");
55212
55218
  var CODEX_HOOK_JS = AGENTSMOMO_DIST_DIR ? path3.join(AGENTSMOMO_DIST_DIR, "codex-cli-hook.cjs") : path3.join(process.cwd(), "proxy", "dist", "codex-cli-hook.cjs");
55219
+ var QWEN_HOOK_JS = AGENTSMOMO_DIST_DIR ? path3.join(AGENTSMOMO_DIST_DIR, "qwen-cli-hook.cjs") : path3.join(process.cwd(), "proxy", "dist", "qwen-cli-hook.cjs");
55213
55220
  function projectSettingsPath(projectPath) {
55214
55221
  return path3.join(projectPath, ".claude", "settings.json");
55215
55222
  }
@@ -55342,6 +55349,10 @@ var config = {
55342
55349
  var PORT = Number(process.env.PORT ?? config.httpProxy.port);
55343
55350
  var DIST_DIR = AGENTSMOMO_DIST_DIR ?? path3.join(process.cwd(), "dist");
55344
55351
  var INDEX_HTML = path3.join(DIST_DIR, "index.html");
55352
+ var IS_PACKAGED_COMPANION = Boolean(AGENTSMOMO_DIST_DIR);
55353
+ var LOCAL_UI_ENABLED = process.env.AGENTMOMO_LOCAL_UI === "1";
55354
+ var HOSTED_APP_URL = process.env.AGENTMOMO_APP_URL?.trim() ?? "https://agentmomo.net/app";
55355
+ var SHOULD_SERVE_STATIC_UI = fs2.existsSync(INDEX_HTML) && (!IS_PACKAGED_COMPANION || LOCAL_UI_ENABLED);
55345
55356
  var FEEDBACK_WINDOW_MS = 6e4;
55346
55357
  var FEEDBACK_MAX_PER_WINDOW = 5;
55347
55358
  var feedbackIpHits = /* @__PURE__ */ new Map();
@@ -55393,6 +55404,281 @@ function allowFeedbackFromIp(ip) {
55393
55404
  return true;
55394
55405
  }
55395
55406
  var app = (0, import_express2.default)();
55407
+ function deriveOpenworldAgentPrompt(agentDefinition) {
55408
+ if (agentDefinition.mode === "prompt") {
55409
+ const prompt2 = typeof agentDefinition.prompt === "string" ? agentDefinition.prompt.trim() : "";
55410
+ if (!prompt2) {
55411
+ throw new Error("prompt is empty.");
55412
+ }
55413
+ return prompt2;
55414
+ }
55415
+ let prompt = typeof agentDefinition.goal === "string" ? agentDefinition.goal.trim() : "";
55416
+ if (!prompt) {
55417
+ throw new Error("subagent goal is empty.");
55418
+ }
55419
+ if (agentDefinition.name) {
55420
+ prompt = `You are ${agentDefinition.name}${agentDefinition.role ? `, ${agentDefinition.role}` : ""}.
55421
+
55422
+ ${prompt}`;
55423
+ }
55424
+ return prompt;
55425
+ }
55426
+ function buildSpawnTarget(tool, prompt, skipPermissions) {
55427
+ if (process.platform === "win32") {
55428
+ const safe = prompt.replace(/[\r\n]+/g, " ").replace(/"/g, '""');
55429
+ switch (tool) {
55430
+ case "claude":
55431
+ return {
55432
+ command: skipPermissions ? `claude --dangerously-skip-permissions -p "${safe}"` : `claude -p "${safe}"`,
55433
+ args: []
55434
+ };
55435
+ case "gemini":
55436
+ return {
55437
+ command: skipPermissions ? `gemini --sandbox=none -p "${safe}"` : `gemini -p "${safe}"`,
55438
+ args: []
55439
+ };
55440
+ case "codex":
55441
+ return {
55442
+ command: skipPermissions ? `codex exec --full-auto "${safe}"` : `codex exec "${safe}"`,
55443
+ args: []
55444
+ };
55445
+ case "qwen":
55446
+ return {
55447
+ command: skipPermissions ? `qwen --experimental-hooks --approval-mode=yolo -p "${safe}"` : `qwen --experimental-hooks -p "${safe}"`,
55448
+ args: []
55449
+ };
55450
+ default:
55451
+ throw new Error("Unknown tool.");
55452
+ }
55453
+ }
55454
+ switch (tool) {
55455
+ case "claude":
55456
+ return {
55457
+ command: "claude",
55458
+ args: skipPermissions ? ["--dangerously-skip-permissions", "-p", prompt] : ["-p", prompt]
55459
+ };
55460
+ case "gemini":
55461
+ return {
55462
+ command: "gemini",
55463
+ args: skipPermissions ? ["--sandbox=none", "-p", prompt] : ["-p", prompt]
55464
+ };
55465
+ case "codex":
55466
+ return {
55467
+ command: "codex",
55468
+ args: skipPermissions ? ["exec", "--full-auto", prompt] : ["exec", prompt]
55469
+ };
55470
+ case "qwen":
55471
+ return {
55472
+ command: "qwen",
55473
+ args: skipPermissions ? ["--experimental-hooks", "--approval-mode=yolo", "-p", prompt] : ["--experimental-hooks", "-p", prompt]
55474
+ };
55475
+ default:
55476
+ throw new Error("Unknown tool.");
55477
+ }
55478
+ }
55479
+ function spawnPromptDetached(tool, prompt, cwd, skipPermissions) {
55480
+ const { command, args } = buildSpawnTarget(tool, prompt, skipPermissions);
55481
+ const child = spawn(command, args, {
55482
+ cwd,
55483
+ detached: true,
55484
+ stdio: "ignore",
55485
+ shell: process.platform === "win32",
55486
+ windowsHide: true
55487
+ });
55488
+ child.on("error", (error) => {
55489
+ console.error(`[openworld/spawn-agent] ${tool} error:`, error.message);
55490
+ });
55491
+ child.unref();
55492
+ }
55493
+ function getOpenworldChatTimeoutMs(tool) {
55494
+ const sharedFromEnv = Number(process.env.AGENTMOMO_OPENWORLD_CHAT_TIMEOUT_MS);
55495
+ if (Number.isFinite(sharedFromEnv) && sharedFromEnv > 0) {
55496
+ return Math.floor(sharedFromEnv);
55497
+ }
55498
+ if (tool === "gemini") {
55499
+ const geminiFromEnv = Number(process.env.AGENTMOMO_GEMINI_CHAT_TIMEOUT_MS);
55500
+ if (Number.isFinite(geminiFromEnv) && geminiFromEnv > 0) {
55501
+ return Math.floor(geminiFromEnv);
55502
+ }
55503
+ return 3e5;
55504
+ }
55505
+ if (tool === "codex") {
55506
+ const codexFromEnv = Number(process.env.AGENTMOMO_CODEX_CHAT_TIMEOUT_MS);
55507
+ if (Number.isFinite(codexFromEnv) && codexFromEnv > 0) {
55508
+ return Math.floor(codexFromEnv);
55509
+ }
55510
+ return 42e4;
55511
+ }
55512
+ return 12e4;
55513
+ }
55514
+ function runPromptCaptured(tool, prompt, cwd, skipPermissions, openworldAgentId) {
55515
+ const { command, args } = buildSpawnTarget(tool, prompt, skipPermissions);
55516
+ const childEnv = openworldAgentId ? { ...process.env, AGENTMOMO_OPENWORLD_AGENT_ID: openworldAgentId } : void 0;
55517
+ return new Promise((resolve, reject) => {
55518
+ const child = spawn(command, args, {
55519
+ cwd,
55520
+ env: childEnv,
55521
+ shell: process.platform === "win32",
55522
+ windowsHide: true,
55523
+ stdio: ["ignore", "pipe", "pipe"]
55524
+ });
55525
+ let stdout = "";
55526
+ let stderr = "";
55527
+ let settled = false;
55528
+ const timeoutMs = getOpenworldChatTimeoutMs(
55529
+ tool
55530
+ );
55531
+ const timeoutId = setTimeout(() => {
55532
+ if (settled) {
55533
+ return;
55534
+ }
55535
+ settled = true;
55536
+ child.kill();
55537
+ reject(
55538
+ new Error(
55539
+ `${tool} did not finish before the timeout (${Math.round(timeoutMs / 1e3)}s).`
55540
+ )
55541
+ );
55542
+ }, timeoutMs);
55543
+ child.stdout.on("data", (chunk) => {
55544
+ stdout += chunk.toString();
55545
+ });
55546
+ child.stderr.on("data", (chunk) => {
55547
+ stderr += chunk.toString();
55548
+ });
55549
+ child.on("error", (error) => {
55550
+ if (settled) {
55551
+ return;
55552
+ }
55553
+ settled = true;
55554
+ clearTimeout(timeoutId);
55555
+ reject(error);
55556
+ });
55557
+ child.on("close", (code) => {
55558
+ if (settled) {
55559
+ return;
55560
+ }
55561
+ settled = true;
55562
+ clearTimeout(timeoutId);
55563
+ if (code === 0) {
55564
+ resolve({ stdout: stdout.trim(), stderr: stderr.trim() });
55565
+ return;
55566
+ }
55567
+ reject(
55568
+ new Error(
55569
+ stderr.trim() || stdout.trim() || `${tool} exited with code ${code}.`
55570
+ )
55571
+ );
55572
+ });
55573
+ });
55574
+ }
55575
+ function buildOpenworldConversationPrompt(agentDefinition, message, history) {
55576
+ const MAX_HISTORY_TURNS = 6;
55577
+ const MAX_TURN_CHARS = 1200;
55578
+ const toCompactText = (value) => {
55579
+ const compact = value.replace(/\s+/g, " ").trim();
55580
+ if (compact.length <= MAX_TURN_CHARS) {
55581
+ return compact;
55582
+ }
55583
+ return `${compact.slice(0, MAX_TURN_CHARS)}...`;
55584
+ };
55585
+ const agentContext = (() => {
55586
+ if (agentDefinition.mode === "prompt") {
55587
+ return typeof agentDefinition.prompt === "string" ? agentDefinition.prompt.trim() : "";
55588
+ }
55589
+ const goal = typeof agentDefinition.goal === "string" ? agentDefinition.goal.trim() : "";
55590
+ if (!goal) {
55591
+ return "";
55592
+ }
55593
+ if (agentDefinition.name) {
55594
+ return `You are ${agentDefinition.name}${agentDefinition.role ? `, ${agentDefinition.role}` : ""}.
55595
+
55596
+ ${goal}`;
55597
+ }
55598
+ return goal;
55599
+ })();
55600
+ const recentHistory = history.slice(-MAX_HISTORY_TURNS);
55601
+ const historyBlock = recentHistory.length ? recentHistory.map(
55602
+ (turn, index) => `${index + 1}. ${turn.role === "user" ? "User" : "Assistant"}: ${toCompactText(turn.content)}`
55603
+ ).join("\n") : "";
55604
+ const parts = [
55605
+ "You are responding inside the AgentMomo Openworld terminal.",
55606
+ agentContext ? `Agent instructions:
55607
+ ${agentContext}` : "",
55608
+ historyBlock ? `Recent conversation:
55609
+ ${historyBlock}` : "",
55610
+ `Latest user message:
55611
+ ${message}`,
55612
+ "Respond with the final answer to the latest user message. If tool work is needed, still provide a direct final response."
55613
+ ].filter(Boolean);
55614
+ return parts.join("\n\n");
55615
+ }
55616
+ function getOpenworldChatProvider(source) {
55617
+ if (typeof source !== "string") {
55618
+ return null;
55619
+ }
55620
+ if (source.startsWith("qwen")) {
55621
+ return "qwen";
55622
+ }
55623
+ if (source.startsWith("gemini")) {
55624
+ return "gemini";
55625
+ }
55626
+ if (source.startsWith("codex")) {
55627
+ return "codex";
55628
+ }
55629
+ if (source.startsWith("claude")) {
55630
+ return "claude";
55631
+ }
55632
+ return null;
55633
+ }
55634
+ async function handleOpenworldConversation(tool, req, res) {
55635
+ const {
55636
+ agentId,
55637
+ agentDefinition,
55638
+ history,
55639
+ message,
55640
+ projectPath,
55641
+ skipPermissions
55642
+ } = req.body ?? {};
55643
+ if (!agentDefinition || !agentDefinition.mode) {
55644
+ res.status(400).json({ error: "Missing agentDefinition." });
55645
+ return;
55646
+ }
55647
+ const trimmedMessage = typeof message === "string" ? message.trim() : "";
55648
+ if (!trimmedMessage) {
55649
+ res.status(400).json({ error: "Message is empty." });
55650
+ return;
55651
+ }
55652
+ const cwd = typeof projectPath === "string" && projectPath.trim() ? projectPath.trim() : process.cwd();
55653
+ const shouldSkipPermissions = skipPermissions === true;
55654
+ const safeHistory = Array.isArray(history) ? history.filter(
55655
+ (turn) => turn && (turn.role === "user" || turn.role === "assistant") && typeof turn.content === "string" && turn.content.trim().length > 0
55656
+ ) : [];
55657
+ const prompt = buildOpenworldConversationPrompt(
55658
+ agentDefinition,
55659
+ trimmedMessage,
55660
+ safeHistory
55661
+ );
55662
+ try {
55663
+ const result = await runPromptCaptured(
55664
+ tool,
55665
+ prompt,
55666
+ cwd,
55667
+ shouldSkipPermissions,
55668
+ typeof agentId === "string" && agentId ? agentId : void 0
55669
+ );
55670
+ res.json({
55671
+ ok: true,
55672
+ reply: result.stdout || result.stderr,
55673
+ cwd
55674
+ });
55675
+ } catch (error) {
55676
+ const label = tool.charAt(0).toUpperCase() + tool.slice(1);
55677
+ res.status(500).json({
55678
+ error: error instanceof Error ? error.message : `${label} chat request failed.`
55679
+ });
55680
+ }
55681
+ }
55396
55682
  app.use(
55397
55683
  import_express2.default.json({
55398
55684
  verify: (req, _res, buf) => {
@@ -55459,6 +55745,40 @@ app.post("/api/event", requireAuth, async (req, res) => {
55459
55745
  }
55460
55746
  processEvent(event);
55461
55747
  broadcast(event);
55748
+ const openworldAgentId = event.metadata?.openworldAgentId;
55749
+ const provider = getOpenworldChatProvider(event.source);
55750
+ if (typeof openworldAgentId === "string" && openworldAgentId && provider && (event.event === "call_start" || event.event === "call_end")) {
55751
+ const activityEvent = {
55752
+ agentId: openworldAgentId,
55753
+ agentName: openworldAgentId,
55754
+ source: "manual",
55755
+ event: "openworld_chat_activity",
55756
+ toolName: event.toolName,
55757
+ message: event.message,
55758
+ timestamp: event.timestamp,
55759
+ requestId: `${event.requestId}-openworld-activity`,
55760
+ metadata: {
55761
+ provider
55762
+ }
55763
+ };
55764
+ broadcast(activityEvent);
55765
+ }
55766
+ if (typeof openworldAgentId === "string" && openworldAgentId && event.source === "claude-code-teammate") {
55767
+ const teammateEvent = {
55768
+ agentId: event.agentId,
55769
+ agentName: event.agentName,
55770
+ source: "claude-code-teammate",
55771
+ event: "openworld_teammate_register",
55772
+ toolName: "",
55773
+ timestamp: event.timestamp,
55774
+ requestId: `${event.requestId}-openworld-teammate`,
55775
+ metadata: {
55776
+ ...event.metadata,
55777
+ openworldAgentId
55778
+ }
55779
+ };
55780
+ broadcast(teammateEvent);
55781
+ }
55462
55782
  if (creditResult.cost > 0 && creditResult.balance >= 0) {
55463
55783
  sendToUser(req.userId, {
55464
55784
  agentId: "__system__",
@@ -55512,6 +55832,53 @@ app.post("/api/event", requireAuth, async (req, res) => {
55512
55832
  app.get("/api/health", (_req, res) => {
55513
55833
  res.json({ status: "ok", agents: getAllAgents().length });
55514
55834
  });
55835
+ app.post("/api/openworld/spawn-agent", (req, res) => {
55836
+ const { tool, agentDefinition, projectPath, skipPermissions } = req.body ?? {};
55837
+ const allowedTools = ["claude", "codex", "gemini", "qwen"];
55838
+ if (!tool || !allowedTools.includes(String(tool))) {
55839
+ res.status(400).json({ error: "Invalid tool. Must be claude, codex, gemini, or qwen." });
55840
+ return;
55841
+ }
55842
+ if (!agentDefinition || !agentDefinition.mode) {
55843
+ res.status(400).json({ error: "Missing agentDefinition." });
55844
+ return;
55845
+ }
55846
+ let prompt;
55847
+ try {
55848
+ prompt = deriveOpenworldAgentPrompt(
55849
+ agentDefinition
55850
+ );
55851
+ } catch (error) {
55852
+ res.status(400).json({
55853
+ error: error instanceof Error ? error.message : "Invalid agent prompt."
55854
+ });
55855
+ return;
55856
+ }
55857
+ const cwd = typeof projectPath === "string" && projectPath.trim() ? projectPath.trim() : process.cwd();
55858
+ const shouldSkipPermissions = skipPermissions === true;
55859
+ try {
55860
+ spawnPromptDetached(String(tool), prompt, cwd, shouldSkipPermissions);
55861
+ } catch (error) {
55862
+ res.status(400).json({
55863
+ error: error instanceof Error ? error.message : "Unknown tool."
55864
+ });
55865
+ return;
55866
+ }
55867
+ console.log(`[openworld/spawn-agent] Spawned ${tool} in ${cwd}`);
55868
+ res.json({ ok: true, tool, cwd });
55869
+ });
55870
+ app.post("/api/openworld/claude-chat", async (req, res) => {
55871
+ await handleOpenworldConversation("claude", req, res);
55872
+ });
55873
+ app.post("/api/openworld/codex-chat", async (req, res) => {
55874
+ await handleOpenworldConversation("codex", req, res);
55875
+ });
55876
+ app.post("/api/openworld/gemini-chat", async (req, res) => {
55877
+ await handleOpenworldConversation("gemini", req, res);
55878
+ });
55879
+ app.post("/api/openworld/qwen-chat", async (req, res) => {
55880
+ await handleOpenworldConversation("qwen", req, res);
55881
+ });
55515
55882
  app.get("/.well-known/agent.json", (_req, res) => {
55516
55883
  res.json({
55517
55884
  name: "agentmomo",
@@ -56024,10 +56391,10 @@ app.post("/api/keys", requireAuth, async (req, res) => {
56024
56391
  return;
56025
56392
  }
56026
56393
  const supabase2 = getSupabase();
56027
- const name = req.body?.name || "Default";
56394
+ const name = req.body?.name || "momo";
56028
56395
  const isBootstrapRequest = req.get("x-agentmomo-key-bootstrap") === "1";
56029
- if (isBootstrapRequest && name === "Default") {
56030
- const { data: existingDefaultKeys } = await supabase2.from("api_keys").select("id, key_hash").eq("user_id", req.userId).eq("name", "Default").eq("is_active", true);
56396
+ if (isBootstrapRequest && name === "momo") {
56397
+ const { data: existingDefaultKeys } = await supabase2.from("api_keys").select("id, key_hash").eq("user_id", req.userId).eq("name", "momo").eq("is_active", true);
56031
56398
  if (existingDefaultKeys && existingDefaultKeys.length > 0) {
56032
56399
  const idsToDeactivate = existingDefaultKeys.map((k) => k.id);
56033
56400
  await supabase2.from("api_keys").update({ is_active: false }).in("id", idsToDeactivate).eq("user_id", req.userId);
@@ -56189,6 +56556,8 @@ app.post("/api/claude-code-config", (req, res) => {
56189
56556
  cfg.hooks.PostToolUse = [matcher];
56190
56557
  cfg.hooks.TeammateIdle = [matcher];
56191
56558
  cfg.hooks.TaskCompleted = [matcher];
56559
+ if (!cfg.env) cfg.env = {};
56560
+ cfg.env.CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS = "1";
56192
56561
  } else {
56193
56562
  const strip = (arr) => (arr ?? []).filter(
56194
56563
  (e) => !e.hooks?.some(
@@ -56204,6 +56573,10 @@ app.post("/api/claude-code-config", (req, res) => {
56204
56573
  if (!cfg.hooks.TeammateIdle.length) delete cfg.hooks.TeammateIdle;
56205
56574
  if (!cfg.hooks.TaskCompleted.length) delete cfg.hooks.TaskCompleted;
56206
56575
  if (!Object.keys(cfg.hooks).length) delete cfg.hooks;
56576
+ if (cfg.env) {
56577
+ delete cfg.env.CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS;
56578
+ if (!Object.keys(cfg.env).length) delete cfg.env;
56579
+ }
56207
56580
  }
56208
56581
  fs2.writeFileSync(configPath, JSON.stringify(cfg, null, 2), "utf-8");
56209
56582
  res.json({ ok: true, configPath });
@@ -56228,6 +56601,9 @@ app.post("/api/build-hook", (_req, res) => {
56228
56601
  function geminiSettingsPath(projectPath) {
56229
56602
  return path3.join(projectPath, ".gemini", "settings.json");
56230
56603
  }
56604
+ function qwenSettingsPath(projectPath) {
56605
+ return path3.join(projectPath, ".qwen", "settings.json");
56606
+ }
56231
56607
  app.get("/api/gemini-cli-config", (req, res) => {
56232
56608
  const projectPath = req.query.project || process.cwd();
56233
56609
  const configPath = geminiSettingsPath(projectPath);
@@ -56326,6 +56702,101 @@ app.post("/api/build-gemini-hook", (_req, res) => {
56326
56702
  }
56327
56703
  );
56328
56704
  });
56705
+ app.get("/api/qwen-cli-config", (req, res) => {
56706
+ const projectPath = req.query.project || process.cwd();
56707
+ const configPath = qwenSettingsPath(projectPath);
56708
+ const hookBuilt = fs2.existsSync(QWEN_HOOK_JS);
56709
+ const configExists = fs2.existsSync(configPath);
56710
+ let config2 = null;
56711
+ let hooksInstalled = false;
56712
+ if (configExists) {
56713
+ try {
56714
+ config2 = JSON.parse(fs2.readFileSync(configPath, "utf-8"));
56715
+ const preToolUse = config2?.hooks?.PreToolUse ?? [];
56716
+ hooksInstalled = preToolUse.some(
56717
+ (entry) => entry.hooks?.some(
56718
+ (hook) => String(hook.command ?? "").includes("qwen-cli-hook")
56719
+ )
56720
+ );
56721
+ } catch {
56722
+ config2 = null;
56723
+ }
56724
+ }
56725
+ res.json({
56726
+ configPath,
56727
+ projectPath,
56728
+ exists: configExists,
56729
+ config: config2,
56730
+ hooksInstalled,
56731
+ hookPath: QWEN_HOOK_JS,
56732
+ hookBuilt
56733
+ });
56734
+ });
56735
+ app.post("/api/qwen-cli-config", (req, res) => {
56736
+ const {
56737
+ action,
56738
+ projectPath: pp,
56739
+ apiKey
56740
+ } = req.body;
56741
+ const projectPath = pp || process.cwd();
56742
+ const configPath = qwenSettingsPath(projectPath);
56743
+ try {
56744
+ const configDir = path3.dirname(configPath);
56745
+ if (!fs2.existsSync(configDir)) fs2.mkdirSync(configDir, { recursive: true });
56746
+ let cfg = {};
56747
+ if (fs2.existsSync(configPath)) {
56748
+ cfg = JSON.parse(fs2.readFileSync(configPath, "utf-8"));
56749
+ }
56750
+ if (!cfg.hooks) cfg.hooks = {};
56751
+ const hookCommandParts = [`node "${QWEN_HOOK_JS.replace(/\\/g, "/")}"`];
56752
+ if (apiKey && typeof apiKey === "string" && apiKey.startsWith("amk_")) {
56753
+ hookCommandParts.push(`--api-key ${apiKey}`);
56754
+ }
56755
+ const hookEntry = {
56756
+ name: "agentmomo",
56757
+ type: "command",
56758
+ command: hookCommandParts.join(" "),
56759
+ timeout: 2e3
56760
+ };
56761
+ const matcher = { matcher: ".*", hooks: [hookEntry] };
56762
+ if (action === "install") {
56763
+ cfg.hooks.PreToolUse = [matcher];
56764
+ cfg.hooks.PostToolUse = [matcher];
56765
+ cfg.hooks.SessionStart = [matcher];
56766
+ } else {
56767
+ const strip = (arr) => (arr ?? []).filter(
56768
+ (entry) => !entry.hooks?.some(
56769
+ (hook) => String(hook.command ?? "").includes("qwen-cli-hook")
56770
+ )
56771
+ );
56772
+ cfg.hooks.PreToolUse = strip(cfg.hooks.PreToolUse ?? []);
56773
+ cfg.hooks.PostToolUse = strip(cfg.hooks.PostToolUse ?? []);
56774
+ cfg.hooks.SessionStart = strip(cfg.hooks.SessionStart ?? []);
56775
+ if (!cfg.hooks.PreToolUse.length) delete cfg.hooks.PreToolUse;
56776
+ if (!cfg.hooks.PostToolUse.length) delete cfg.hooks.PostToolUse;
56777
+ if (!cfg.hooks.SessionStart.length) delete cfg.hooks.SessionStart;
56778
+ if (!Object.keys(cfg.hooks).length) delete cfg.hooks;
56779
+ }
56780
+ fs2.writeFileSync(configPath, JSON.stringify(cfg, null, 2), "utf-8");
56781
+ res.json({ ok: true, configPath });
56782
+ } catch (err) {
56783
+ res.status(500).json({ error: err.message });
56784
+ }
56785
+ });
56786
+ app.post("/api/build-qwen-hook", (_req, res) => {
56787
+ if (AGENTSMOMO_DIST_DIR) {
56788
+ res.json({ ok: true });
56789
+ return;
56790
+ }
56791
+ exec(
56792
+ `npx esbuild proxy/qwen-cli-hook.ts --bundle --platform=node --format=cjs --outfile=proxy/dist/qwen-cli-hook.cjs`,
56793
+ { cwd: process.cwd() },
56794
+ (err, _stdout, stderr) => {
56795
+ if (err) res.status(500).json({ error: stderr || err.message });
56796
+ else res.json({ ok: true });
56797
+ }
56798
+ );
56799
+ });
56329
56800
  app.get("/api/codex-cli-config", (req, res) => {
56330
56801
  const projectPath = req.query.project || process.cwd();
56331
56802
  const helperPath = codexHelperPath(projectPath);
@@ -56439,7 +56910,7 @@ if (config.httpProxy.targets.length > 0) {
56439
56910
  const proxyRouter = createHttpProxyRouter(config.httpProxy.targets);
56440
56911
  app.use(proxyRouter);
56441
56912
  }
56442
- if (fs2.existsSync(INDEX_HTML)) {
56913
+ if (SHOULD_SERVE_STATIC_UI) {
56443
56914
  app.use(import_express2.default.static(DIST_DIR));
56444
56915
  app.get("*", (req, res, next) => {
56445
56916
  if (req.path.startsWith("/api") || req.path.startsWith("/proxy") || req.path.startsWith("/ws") || req.path.startsWith("/.well-known") || req.path.startsWith("/a2a")) {
@@ -56449,6 +56920,16 @@ if (fs2.existsSync(INDEX_HTML)) {
56449
56920
  res.sendFile(INDEX_HTML);
56450
56921
  });
56451
56922
  }
56923
+ if (IS_PACKAGED_COMPANION && !LOCAL_UI_ENABLED) {
56924
+ app.get("/", (_req, res) => {
56925
+ const lines = [
56926
+ "AgentMomo local bridge is running.",
56927
+ HOSTED_APP_URL ? `Open the hosted AgentMomo app at ${HOSTED_APP_URL} and keep this process running.` : "Open your hosted AgentMomo app and keep this process running.",
56928
+ `Health check: http://localhost:${PORT}/api/health`
56929
+ ];
56930
+ res.type("text/plain").send(lines.join("\n"));
56931
+ });
56932
+ }
56452
56933
  var server = createServer(app);
56453
56934
  var wss2 = startEventEmitter(PORT);
56454
56935
  server.on("upgrade", async (request, socket, head2) => {
@@ -56478,7 +56959,7 @@ server.listen(PORT, () => {
56478
56959
  const wsHost = `ws://localhost:${PORT}/ws`;
56479
56960
  console.log(`
56480
56961
  \u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510
56481
- \u2502 \u{1F3E2} AgentMomo Proxy Server \u2502
56962
+ \u2502 \u{1F3E2} AgentMomo Local Bridge \u2502
56482
56963
  \u251C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524
56483
56964
  \u2502 REST API: ${host}/api \u2502
56484
56965
  \u2502 WebSocket: ${wsHost} \u2502
@@ -56486,6 +56967,9 @@ server.listen(PORT, () => {
56486
56967
  \u2502 Targets: ${config.httpProxy.targets.length} configured \u2502
56487
56968
  \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518
56488
56969
  `);
56970
+ console.log(
56971
+ IS_PACKAGED_COMPANION ? LOCAL_UI_ENABLED ? `[agentmomo] Local fallback UI enabled at ${host}` : `[agentmomo] Bridge-only mode enabled. Use the hosted app${HOSTED_APP_URL ? ` at ${HOSTED_APP_URL}` : ""}.` : `[agentmomo] Hosted UI serving enabled at ${host}`
56972
+ );
56489
56973
  });
56490
56974
  export {
56491
56975
  app,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentmomo",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "3D visualization of MCP agent activity — watch your AI tools work in a virtual office",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -42,9 +42,10 @@
42
42
  "build:hook": "npx esbuild proxy/claude-code-hook.ts --bundle --platform=node --format=cjs --outfile=proxy/dist/claude-code-hook.cjs",
43
43
  "build:gemini-hook": "npx esbuild proxy/gemini-cli-hook.ts --bundle --platform=node --format=cjs --outfile=proxy/dist/gemini-cli-hook.cjs",
44
44
  "build:codex-hook": "npx esbuild proxy/codex-cli-hook.ts --bundle --platform=node --format=cjs --outfile=proxy/dist/codex-cli-hook.cjs",
45
+ "build:qwen-hook": "npx esbuild proxy/qwen-cli-hook.ts --bundle --platform=node --format=cjs --outfile=proxy/dist/qwen-cli-hook.cjs",
45
46
  "build:server": "npx esbuild proxy/server.ts --bundle --platform=node --format=esm --outfile=dist/server.js",
46
47
  "build:cli": "npx esbuild bin/cli.ts --bundle --platform=node --format=esm --banner:js=\"#!/usr/bin/env node\" --outfile=dist/cli.js",
47
- "build:npm": "npm run build && npm run build:server && npm run build:cli && npx esbuild proxy/stdio-wrapper.ts --bundle --platform=node --format=esm --outfile=dist/stdio-wrapper.js && npx esbuild proxy/claude-code-hook.ts --bundle --platform=node --format=cjs --outfile=dist/claude-code-hook.cjs && npx esbuild proxy/gemini-cli-hook.ts --bundle --platform=node --format=cjs --outfile=dist/gemini-cli-hook.cjs && npx esbuild proxy/codex-cli-hook.ts --bundle --platform=node --format=cjs --outfile=dist/codex-cli-hook.cjs",
48
+ "build:npm": "npm run build && npm run build:server && npm run build:cli && npx esbuild proxy/stdio-wrapper.ts --bundle --platform=node --format=esm --outfile=dist/stdio-wrapper.js && npx esbuild proxy/claude-code-hook.ts --bundle --platform=node --format=cjs --outfile=dist/claude-code-hook.cjs && npx esbuild proxy/gemini-cli-hook.ts --bundle --platform=node --format=cjs --outfile=dist/gemini-cli-hook.cjs && npx esbuild proxy/codex-cli-hook.ts --bundle --platform=node --format=cjs --outfile=dist/codex-cli-hook.cjs && npx esbuild proxy/qwen-cli-hook.ts --bundle --platform=node --format=cjs --outfile=dist/qwen-cli-hook.cjs",
48
49
  "prepack": "npm run build:npm",
49
50
  "preview": "vite preview"
50
51
  },
@@ -69,6 +70,7 @@
69
70
  "@react-three/fiber": "^9.0.0",
70
71
  "@react-three/postprocessing": "^3.0.4",
71
72
  "@supabase/supabase-js": "^2.98.0",
73
+ "cannon-es": "^0.20.0",
72
74
  "class-variance-authority": "^0.7.1",
73
75
  "clsx": "^2.1.1",
74
76
  "cors": "^2.8.6",
@@ -96,6 +98,7 @@
96
98
  "@vitejs/plugin-react": "^4.3.0",
97
99
  "autoprefixer": "^10.4.27",
98
100
  "concurrently": "^9.1.0",
101
+ "playwright": "^1.58.2",
99
102
  "postcss": "^8.5.8",
100
103
  "tailwindcss": "^3.4.19",
101
104
  "tsx": "^4.19.0",