agentmomo 0.2.1 → 0.4.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,8 +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);
55345
55353
  var LOCAL_UI_ENABLED = process.env.AGENTMOMO_LOCAL_UI === "1";
55346
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);
55347
55356
  var FEEDBACK_WINDOW_MS = 6e4;
55348
55357
  var FEEDBACK_MAX_PER_WINDOW = 5;
55349
55358
  var feedbackIpHits = /* @__PURE__ */ new Map();
@@ -55395,6 +55404,281 @@ function allowFeedbackFromIp(ip) {
55395
55404
  return true;
55396
55405
  }
55397
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 --approval-mode=yolo -p "${safe}"` : `qwen -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 ? ["--approval-mode=yolo", "-p", prompt] : ["-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
+ }
55398
55682
  app.use(
55399
55683
  import_express2.default.json({
55400
55684
  verify: (req, _res, buf) => {
@@ -55461,6 +55745,40 @@ app.post("/api/event", requireAuth, async (req, res) => {
55461
55745
  }
55462
55746
  processEvent(event);
55463
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
+ }
55464
55782
  if (creditResult.cost > 0 && creditResult.balance >= 0) {
55465
55783
  sendToUser(req.userId, {
55466
55784
  agentId: "__system__",
@@ -55514,6 +55832,53 @@ app.post("/api/event", requireAuth, async (req, res) => {
55514
55832
  app.get("/api/health", (_req, res) => {
55515
55833
  res.json({ status: "ok", agents: getAllAgents().length });
55516
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
+ });
55517
55882
  app.get("/.well-known/agent.json", (_req, res) => {
55518
55883
  res.json({
55519
55884
  name: "agentmomo",
@@ -56026,10 +56391,10 @@ app.post("/api/keys", requireAuth, async (req, res) => {
56026
56391
  return;
56027
56392
  }
56028
56393
  const supabase2 = getSupabase();
56029
- const name = req.body?.name || "Default";
56394
+ const name = req.body?.name || "momo";
56030
56395
  const isBootstrapRequest = req.get("x-agentmomo-key-bootstrap") === "1";
56031
- if (isBootstrapRequest && name === "Default") {
56032
- 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);
56033
56398
  if (existingDefaultKeys && existingDefaultKeys.length > 0) {
56034
56399
  const idsToDeactivate = existingDefaultKeys.map((k) => k.id);
56035
56400
  await supabase2.from("api_keys").update({ is_active: false }).in("id", idsToDeactivate).eq("user_id", req.userId);
@@ -56191,6 +56556,8 @@ app.post("/api/claude-code-config", (req, res) => {
56191
56556
  cfg.hooks.PostToolUse = [matcher];
56192
56557
  cfg.hooks.TeammateIdle = [matcher];
56193
56558
  cfg.hooks.TaskCompleted = [matcher];
56559
+ if (!cfg.env) cfg.env = {};
56560
+ cfg.env.CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS = "1";
56194
56561
  } else {
56195
56562
  const strip = (arr) => (arr ?? []).filter(
56196
56563
  (e) => !e.hooks?.some(
@@ -56206,6 +56573,10 @@ app.post("/api/claude-code-config", (req, res) => {
56206
56573
  if (!cfg.hooks.TeammateIdle.length) delete cfg.hooks.TeammateIdle;
56207
56574
  if (!cfg.hooks.TaskCompleted.length) delete cfg.hooks.TaskCompleted;
56208
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
+ }
56209
56580
  }
56210
56581
  fs2.writeFileSync(configPath, JSON.stringify(cfg, null, 2), "utf-8");
56211
56582
  res.json({ ok: true, configPath });
@@ -56230,6 +56601,9 @@ app.post("/api/build-hook", (_req, res) => {
56230
56601
  function geminiSettingsPath(projectPath) {
56231
56602
  return path3.join(projectPath, ".gemini", "settings.json");
56232
56603
  }
56604
+ function qwenSettingsPath(projectPath) {
56605
+ return path3.join(projectPath, ".qwen", "settings.json");
56606
+ }
56233
56607
  app.get("/api/gemini-cli-config", (req, res) => {
56234
56608
  const projectPath = req.query.project || process.cwd();
56235
56609
  const configPath = geminiSettingsPath(projectPath);
@@ -56328,6 +56702,101 @@ app.post("/api/build-gemini-hook", (_req, res) => {
56328
56702
  }
56329
56703
  );
56330
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
+ });
56331
56800
  app.get("/api/codex-cli-config", (req, res) => {
56332
56801
  const projectPath = req.query.project || process.cwd();
56333
56802
  const helperPath = codexHelperPath(projectPath);
@@ -56441,7 +56910,7 @@ if (config.httpProxy.targets.length > 0) {
56441
56910
  const proxyRouter = createHttpProxyRouter(config.httpProxy.targets);
56442
56911
  app.use(proxyRouter);
56443
56912
  }
56444
- if (LOCAL_UI_ENABLED && fs2.existsSync(INDEX_HTML)) {
56913
+ if (SHOULD_SERVE_STATIC_UI) {
56445
56914
  app.use(import_express2.default.static(DIST_DIR));
56446
56915
  app.get("*", (req, res, next) => {
56447
56916
  if (req.path.startsWith("/api") || req.path.startsWith("/proxy") || req.path.startsWith("/ws") || req.path.startsWith("/.well-known") || req.path.startsWith("/a2a")) {
@@ -56451,7 +56920,7 @@ if (LOCAL_UI_ENABLED && fs2.existsSync(INDEX_HTML)) {
56451
56920
  res.sendFile(INDEX_HTML);
56452
56921
  });
56453
56922
  }
56454
- if (!LOCAL_UI_ENABLED) {
56923
+ if (IS_PACKAGED_COMPANION && !LOCAL_UI_ENABLED) {
56455
56924
  app.get("/", (_req, res) => {
56456
56925
  const lines = [
56457
56926
  "AgentMomo local bridge is running.",
@@ -56499,7 +56968,7 @@ server.listen(PORT, () => {
56499
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
56500
56969
  `);
56501
56970
  console.log(
56502
- 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}` : ""}.`
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}`
56503
56972
  );
56504
56973
  });
56505
56974
  export {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentmomo",
3
- "version": "0.2.1",
3
+ "version": "0.4.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",