quoroom 0.1.21 → 0.1.22

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.
@@ -9914,7 +9914,7 @@ var require_package = __commonJS({
9914
9914
  "package.json"(exports2, module2) {
9915
9915
  module2.exports = {
9916
9916
  name: "quoroom",
9917
- version: "0.1.21",
9917
+ version: "0.1.22",
9918
9918
  description: "Autonomous AI agent collective engine \u2014 Queen, Workers, Quorum",
9919
9919
  main: "./out/mcp/server.js",
9920
9920
  bin: {
@@ -9939,7 +9939,7 @@ var require_package = __commonJS({
9939
9939
  "build:mcp": "node scripts/build-mcp.js",
9940
9940
  "build:ui": "vite build --config src/ui/vite.config.ts",
9941
9941
  "kill:ports": "node scripts/kill-ports.js",
9942
- "kill:dev-ports": "npm run kill:ports -- 4700 3710 5173",
9942
+ "kill:dev-ports": "npm run kill:ports -- 4700 3715 5173",
9943
9943
  "dev:links": "node scripts/dev-links.js",
9944
9944
  dev: `sh -c 'npm run kill:dev-ports && trap "kill 0" INT TERM EXIT; npm run dev:links & npm run dev:room & npm run dev:cloud & wait'`,
9945
9945
  "dev:room": "sh -c 'export QUOROOM_DATA_DIR=$HOME/.quoroom-dev QUOROOM_SKIP_MCP_REGISTER=1; npm run build:mcp && npm run build:ui && node scripts/dev-server.js --port 4700'",
@@ -9947,7 +9947,7 @@ var require_package = __commonJS({
9947
9947
  "dev:room:shared": "npm run build:mcp && npm run build:ui && node scripts/dev-server.js",
9948
9948
  "doctor:split": "node scripts/doctor-split.js",
9949
9949
  "dev:isolated": `sh -c 'npm run kill:dev-ports && trap "kill 0" INT TERM EXIT; npm run dev:links & npm run dev:room:isolated & npm run dev:cloud & VITE_API_PORT=4700 npm run dev:ui & wait'`,
9950
- "dev:cloud": `sh -c 'npm run kill:ports -- 3710 && cd ../cloud && PORT=3710 CLOUD_PUBLIC_URL=http://127.0.0.1:3710 CLOUD_ALLOWED_ORIGINS='"'"'http://127.0.0.1:3710,http://localhost:3710,http://localhost:5173,http://127.0.0.1:5173,https://quoroom.ai,https://www.quoroom.ai,https://app.quoroom.ai'"'"' npm start'`,
9950
+ "dev:cloud": `sh -c 'npm run kill:ports -- 3715 && cd ../cloud && PORT=3715 CLOUD_PUBLIC_URL=http://127.0.0.1:3715 CLOUD_ALLOWED_ORIGINS='"'"'http://127.0.0.1:3715,http://localhost:3715,http://localhost:5173,http://127.0.0.1:5173,https://quoroom.ai,https://www.quoroom.ai,https://app.quoroom.ai'"'"' npm start'`,
9951
9951
  "dev:ui": "vite --config src/ui/vite.config.ts",
9952
9952
  "seed:style-demo": "sh -c 'export QUOROOM_DATA_DIR=$HOME/.quoroom-dev; node scripts/seed-style-demo.js'",
9953
9953
  typecheck: "tsc --noEmit",
@@ -12069,6 +12069,7 @@ function mapRoomRow(row) {
12069
12069
  queenNickname: row.queen_nickname ?? null,
12070
12070
  chatSessionId: row.chat_session_id ?? null,
12071
12071
  referredByCode: row.referred_by_code ?? null,
12072
+ allowedTools: row.allowed_tools ?? null,
12072
12073
  webhookToken: row.webhook_token ?? null,
12073
12074
  createdAt: row.created_at,
12074
12075
  updatedAt: row.updated_at
@@ -12161,6 +12162,7 @@ function updateRoom(db2, id, updates) {
12161
12162
  config: "config",
12162
12163
  referredByCode: "referred_by_code",
12163
12164
  queenNickname: "queen_nickname",
12165
+ allowedTools: "allowed_tools",
12164
12166
  webhookToken: "webhook_token"
12165
12167
  };
12166
12168
  const fields = [];
@@ -12856,6 +12858,22 @@ function listRoomCycles(db2, roomId, limit = 20) {
12856
12858
  ).all(roomId, safeLimit);
12857
12859
  return rows.map(mapWorkerCycleRow);
12858
12860
  }
12861
+ function countProductiveToolCalls(db2, workerId, lastNCycles = 2) {
12862
+ const row = db2.prepare(`
12863
+ SELECT COUNT(*) as cnt FROM cycle_logs
12864
+ WHERE cycle_id IN (
12865
+ SELECT id FROM worker_cycles
12866
+ WHERE worker_id = ? AND status = 'completed'
12867
+ ORDER BY started_at DESC LIMIT ?
12868
+ )
12869
+ AND entry_type = 'tool_call'
12870
+ AND (content LIKE '%web_search%' OR content LIKE '%web_fetch%' OR content LIKE '%remember%'
12871
+ OR content LIKE '%ask_keeper%' OR content LIKE '%inbox_send%'
12872
+ OR content LIKE '%update_progress%' OR content LIKE '%complete_goal%'
12873
+ OR content LIKE '%set_goal%')
12874
+ `).get(workerId, lastNCycles);
12875
+ return row.cnt;
12876
+ }
12859
12877
  function cleanupStaleCycles(db2) {
12860
12878
  const result = db2.prepare(
12861
12879
  "UPDATE worker_cycles SET status = 'failed', error_message = 'Server restarted', finished_at = datetime('now','localtime') WHERE status = 'running'"
@@ -23981,33 +23999,131 @@ async function closeBrowser() {
23981
23999
  }
23982
24000
  }
23983
24001
  async function webFetch(url) {
23984
- const jinaUrl = `https://r.jina.ai/${url}`;
23985
- const response = await fetch(jinaUrl, {
23986
- headers: {
23987
- "Accept": "text/plain",
23988
- "X-No-Cache": "true"
23989
- },
23990
- signal: AbortSignal.timeout(3e4)
24002
+ try {
24003
+ const jinaUrl = `https://r.jina.ai/${url}`;
24004
+ const response = await fetch(jinaUrl, {
24005
+ headers: { "Accept": "text/plain", "X-No-Cache": "true" },
24006
+ signal: AbortSignal.timeout(2e4)
24007
+ });
24008
+ if (response.ok) {
24009
+ const text = await response.text();
24010
+ if (text.length > 200 && !text.includes("Warning: Target URL returned error")) {
24011
+ return text.slice(0, MAX_CONTENT_CHARS);
24012
+ }
24013
+ }
24014
+ } catch {
24015
+ }
24016
+ return fetchWithBrowser(url);
24017
+ }
24018
+ async function fetchWithBrowser(url) {
24019
+ const browser = await getBrowser();
24020
+ const context = await browser.newContext({
24021
+ userAgent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
23991
24022
  });
23992
- if (!response.ok) {
23993
- throw new Error(`Jina fetch failed: ${response.status} ${response.statusText}`);
24023
+ const page = await context.newPage();
24024
+ try {
24025
+ await page.goto(url, { waitUntil: "domcontentloaded", timeout: 3e4 });
24026
+ const text = await page.innerText("body").catch(() => "");
24027
+ if (!text) throw new Error(`Could not read content from ${url}`);
24028
+ return text.slice(0, MAX_CONTENT_CHARS);
24029
+ } finally {
24030
+ await context.close();
23994
24031
  }
23995
- const text = await response.text();
23996
- return text.slice(0, MAX_CONTENT_CHARS);
23997
24032
  }
23998
24033
  async function webSearch(query) {
23999
- const searchUrl = `https://html.duckduckgo.com/html/?q=${encodeURIComponent(query)}`;
24000
- const response = await fetch(searchUrl, {
24001
- headers: {
24002
- "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
24003
- },
24004
- signal: AbortSignal.timeout(15e3)
24034
+ const browserResults = await searchWithBrowser(query);
24035
+ if (browserResults.length > 0) return browserResults;
24036
+ const ddgResults = await searchDdg(query);
24037
+ if (ddgResults.length > 0) return ddgResults;
24038
+ try {
24039
+ const response = await fetch(`https://s.jina.ai/${encodeURIComponent(query)}`, {
24040
+ headers: { "Accept": "application/json", "X-No-Cache": "true" },
24041
+ signal: AbortSignal.timeout(15e3)
24042
+ });
24043
+ if (response.ok) {
24044
+ const data = await response.json();
24045
+ if (data.data && Array.isArray(data.data)) {
24046
+ return data.data.slice(0, 5).map((r) => ({
24047
+ title: r.title ?? "",
24048
+ url: r.url ?? "",
24049
+ snippet: (r.description ?? r.content ?? "").slice(0, 300)
24050
+ })).filter((r) => r.url);
24051
+ }
24052
+ }
24053
+ } catch {
24054
+ }
24055
+ return [];
24056
+ }
24057
+ async function searchWithBrowser(query) {
24058
+ let browser;
24059
+ try {
24060
+ browser = await getBrowser();
24061
+ } catch {
24062
+ return [];
24063
+ }
24064
+ const context = await browser.newContext({
24065
+ userAgent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
24066
+ locale: "en-US"
24005
24067
  });
24006
- if (!response.ok) {
24007
- throw new Error(`DuckDuckGo search failed: ${response.status}`);
24068
+ const page = await context.newPage();
24069
+ try {
24070
+ await page.goto(`https://search.yahoo.com/search?p=${encodeURIComponent(query)}`, {
24071
+ waitUntil: "domcontentloaded",
24072
+ timeout: 15e3
24073
+ });
24074
+ await page.waitForTimeout(1e3);
24075
+ const results = await page.evaluate(() => {
24076
+ const items = [];
24077
+ const blocks = document.querySelectorAll("#web .algo, .dd.algo, .algo");
24078
+ for (const block of blocks) {
24079
+ const link = block.querySelector("a");
24080
+ const h3 = block.querySelector("h3");
24081
+ const snippetEl = block.querySelector(".compText p, .compText, p");
24082
+ if (!link) continue;
24083
+ const url = link.getAttribute("href") || "";
24084
+ if (!url.startsWith("http")) continue;
24085
+ items.push({
24086
+ title: h3 ? (h3.textContent || "").trim() : (link.textContent || "").trim(),
24087
+ url,
24088
+ snippet: snippetEl ? (snippetEl.textContent || "").trim().slice(0, 300) : ""
24089
+ });
24090
+ if (items.length >= 5) break;
24091
+ }
24092
+ return items;
24093
+ });
24094
+ return results.filter((r) => r.url);
24095
+ } catch {
24096
+ return [];
24097
+ } finally {
24098
+ await context.close();
24099
+ }
24100
+ }
24101
+ async function searchDdg(query) {
24102
+ for (let attempt = 0; attempt < 3; attempt++) {
24103
+ if (attempt > 0) await new Promise((r) => setTimeout(r, 1e3 * attempt));
24104
+ try {
24105
+ const response = await fetch("https://html.duckduckgo.com/html/", {
24106
+ method: "POST",
24107
+ headers: {
24108
+ "Content-Type": "application/x-www-form-urlencoded",
24109
+ "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
24110
+ "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
24111
+ "Accept-Language": "en-US,en;q=0.9",
24112
+ "Referer": "https://html.duckduckgo.com/"
24113
+ },
24114
+ body: `q=${encodeURIComponent(query)}&b=`,
24115
+ signal: AbortSignal.timeout(15e3),
24116
+ redirect: "follow"
24117
+ });
24118
+ if (response.status === 202) continue;
24119
+ if (!response.ok) continue;
24120
+ const html = await response.text();
24121
+ const results = parseDdgResults(html).slice(0, 5);
24122
+ if (results.length > 0) return results;
24123
+ } catch {
24124
+ }
24008
24125
  }
24009
- const html = await response.text();
24010
- return parseDdgResults(html).slice(0, 5);
24126
+ return [];
24011
24127
  }
24012
24128
  function parseDdgResults(html) {
24013
24129
  const results = [];
@@ -24402,6 +24518,47 @@ var QUEEN_TOOL_DEFINITIONS = [
24402
24518
  required: ["url", "actions"]
24403
24519
  }
24404
24520
  }
24521
+ },
24522
+ // ── Wallet ──────────────────────────────────────────────────────────────
24523
+ {
24524
+ type: "function",
24525
+ function: {
24526
+ name: "quoroom_wallet_balance",
24527
+ description: "Get the room's wallet balance (USDC). Returns address and transaction summary.",
24528
+ parameters: {
24529
+ type: "object",
24530
+ properties: {},
24531
+ required: []
24532
+ }
24533
+ }
24534
+ },
24535
+ {
24536
+ type: "function",
24537
+ function: {
24538
+ name: "quoroom_wallet_send",
24539
+ description: "Send USDC from the room's wallet to an address.",
24540
+ parameters: {
24541
+ type: "object",
24542
+ properties: {
24543
+ to: { type: "string", description: "Recipient address (0x...)" },
24544
+ amount: { type: "string", description: 'Amount (e.g., "10.50")' }
24545
+ },
24546
+ required: ["to", "amount"]
24547
+ }
24548
+ }
24549
+ },
24550
+ {
24551
+ type: "function",
24552
+ function: {
24553
+ name: "quoroom_wallet_history",
24554
+ description: "Get recent wallet transaction history.",
24555
+ parameters: {
24556
+ type: "object",
24557
+ properties: {
24558
+ limit: { type: "string", description: "Max transactions to return (default: 10)" }
24559
+ }
24560
+ }
24561
+ }
24405
24562
  }
24406
24563
  ];
24407
24564
  async function executeQueenTool(db2, roomId, workerId, toolName, args) {
@@ -24422,10 +24579,12 @@ async function executeQueenTool(db2, roomId, workerId, toolName, args) {
24422
24579
  if (goalCheck.roomId !== roomId) return { content: `Error: goal #${goalId} belongs to another room. Your room's goals are shown in the Active Goals section \u2014 use those goal IDs.`, isError: true };
24423
24580
  const observation = String(args.observation ?? args.progress ?? args.message ?? args.text ?? "");
24424
24581
  const metricValue = args.metricValue != null ? Number(args.metricValue) : args.metric_value != null ? Number(args.metric_value) : void 0;
24582
+ const subGoals = getSubGoals(db2, goalId);
24425
24583
  updateGoalProgress(db2, goalId, observation, metricValue, workerId);
24426
24584
  const goal = getGoal(db2, goalId);
24427
24585
  const pct = Math.round((goal?.progress ?? 0) * 100);
24428
- return { content: `Progress logged on goal #${goalId}. Now at ${pct}%.` };
24586
+ const note = subGoals.length > 0 && metricValue != null ? ` (metricValue ignored \u2014 goal has ${subGoals.length} sub-goals, progress is calculated from them. Update sub-goals directly.)` : "";
24587
+ return { content: `Progress logged on goal #${goalId}. Now at ${pct}%.${note}` };
24429
24588
  }
24430
24589
  case "quoroom_create_subgoal": {
24431
24590
  const goalId = Number(args.goalId);
@@ -24458,6 +24617,13 @@ async function executeQueenTool(db2, roomId, workerId, toolName, args) {
24458
24617
  case "quoroom_propose": {
24459
24618
  const proposalText = String(args.proposal ?? args.text ?? args.description ?? args.content ?? args.idea ?? "").trim();
24460
24619
  if (!proposalText) return { content: 'Error: proposal text is required. Provide a "proposal" string.', isError: true };
24620
+ const recentDecisions = listDecisions(db2, roomId);
24621
+ const isDuplicate = recentDecisions.slice(0, 10).some(
24622
+ (d) => (d.status === "voting" || d.status === "approved") && d.proposal.toLowerCase() === proposalText.toLowerCase()
24623
+ );
24624
+ if (isDuplicate) {
24625
+ return { content: `A similar proposal already exists: "${proposalText}". No need to propose again.`, isError: true };
24626
+ }
24461
24627
  const decisionType = String(args.decisionType ?? args.type ?? args.impact ?? args.category ?? "low_impact");
24462
24628
  const decision = propose(db2, {
24463
24629
  roomId,
@@ -24498,6 +24664,10 @@ async function executeQueenTool(db2, roomId, workerId, toolName, args) {
24498
24664
  const systemPrompt = String(args.systemPrompt ?? args.system_prompt ?? args.instructions ?? args.prompt ?? "").trim();
24499
24665
  if (!name) return { content: 'Error: name is required for quoroom_create_worker. Provide a "name" string.', isError: true };
24500
24666
  if (!systemPrompt) return { content: `Error: systemPrompt is required for quoroom_create_worker. Provide a "systemPrompt" string describing this worker's role and instructions.`, isError: true };
24667
+ const existingWorkers = listRoomWorkers(db2, roomId);
24668
+ if (existingWorkers.some((w) => w.name.toLowerCase() === name.toLowerCase())) {
24669
+ return { content: `Worker "${name}" already exists in this room. Use quoroom_update_worker to modify it, or choose a different name.`, isError: true };
24670
+ }
24501
24671
  const role = args.role && args.role !== args.name ? String(args.role) : void 0;
24502
24672
  const description = args.description ? String(args.description) : void 0;
24503
24673
  const preset = role ? WORKER_ROLE_PRESETS[role] : void 0;
@@ -24530,6 +24700,10 @@ async function executeQueenTool(db2, roomId, workerId, toolName, args) {
24530
24700
  const taskWorkerId = args.workerId ? Number(args.workerId) : void 0;
24531
24701
  const maxTurns = args.maxTurns ? Number(args.maxTurns) : void 0;
24532
24702
  const triggerType = cronExpression ? "cron" : scheduledAt ? "once" : "manual";
24703
+ const existingTasks = listTasks(db2, roomId, "active");
24704
+ if (existingTasks.some((t) => t.name.toLowerCase() === name.toLowerCase())) {
24705
+ return { content: `Task "${name}" already exists. Choose a different name or manage the existing task.`, isError: true };
24706
+ }
24533
24707
  if (taskWorkerId) {
24534
24708
  const taskWorker = getWorker(db2, taskWorkerId);
24535
24709
  if (!taskWorker || taskWorker.roomId !== roomId) {
@@ -24555,6 +24729,11 @@ async function executeQueenTool(db2, roomId, workerId, toolName, args) {
24555
24729
  const name = String(args.name ?? "");
24556
24730
  const content = String(args.content ?? "");
24557
24731
  const type = String(args.type ?? "fact");
24732
+ const existing = listEntities(db2, roomId).find((e) => e.name.toLowerCase() === name.toLowerCase());
24733
+ if (existing) {
24734
+ addObservation(db2, existing.id, content, "queen");
24735
+ return { content: `Updated memory "${name}" (added new observation to existing entry).` };
24736
+ }
24558
24737
  const entity = createEntity(db2, name, type, void 0, roomId);
24559
24738
  addObservation(db2, entity.id, content, "queen");
24560
24739
  return { content: `Remembered "${name}".` };
@@ -24609,6 +24788,26 @@ async function executeQueenTool(db2, roomId, workerId, toolName, args) {
24609
24788
  if (!url) return { content: "Error: url is required", isError: true };
24610
24789
  return { content: await browserAction(url, actions) };
24611
24790
  }
24791
+ // ── Wallet ────────────────────────────────────────────────────
24792
+ case "quoroom_wallet_balance": {
24793
+ const wallet = getWalletByRoom(db2, roomId);
24794
+ if (!wallet) return { content: "No wallet found for this room.", isError: true };
24795
+ const summary = getWalletTransactionSummary(db2, wallet.id);
24796
+ const net = (parseFloat(summary.received) - parseFloat(summary.sent)).toFixed(2);
24797
+ return { content: `Wallet ${wallet.address}: ${net} USDC (received: ${summary.received}, sent: ${summary.sent})` };
24798
+ }
24799
+ case "quoroom_wallet_send": {
24800
+ return { content: "Wallet send requires on-chain transaction \u2014 use the MCP tool quoroom_wallet_send with encryptionKey, or ask the keeper to send funds.", isError: true };
24801
+ }
24802
+ case "quoroom_wallet_history": {
24803
+ const wallet = getWalletByRoom(db2, roomId);
24804
+ if (!wallet) return { content: "No wallet found for this room.", isError: true };
24805
+ const limit = Math.min(Number(args.limit) || 10, 50);
24806
+ const txs = listWalletTransactions(db2, wallet.id, limit);
24807
+ if (txs.length === 0) return { content: "No transactions yet." };
24808
+ const lines = txs.map((tx) => `[${tx.type}] ${tx.amount} USDC \u2014 ${tx.description ?? ""} (${tx.status})`).join("\n");
24809
+ return { content: lines };
24810
+ }
24612
24811
  default:
24613
24812
  return { content: `Unknown tool: ${toolName}`, isError: true };
24614
24813
  }
@@ -25051,6 +25250,14 @@ ${roomTasks.map(
25051
25250
  (t) => `- #${t.id} "${t.name}" [${t.triggerType}] \u2014 ${t.status}`
25052
25251
  ).join("\n")}`);
25053
25252
  }
25253
+ const wallet = getWalletByRoom(db2, roomId);
25254
+ if (wallet) {
25255
+ const summary = getWalletTransactionSummary(db2, wallet.id);
25256
+ const net = (parseFloat(summary.received) - parseFloat(summary.sent)).toFixed(2);
25257
+ contextParts.push(`## Wallet
25258
+ Address: ${wallet.address}
25259
+ Balance: ${net} USDC (received: ${summary.received}, spent: ${summary.sent})`);
25260
+ }
25054
25261
  if (unreadMessages.length > 0) {
25055
25262
  contextParts.push(`## Unread Messages
25056
25263
  ${unreadMessages.map(
@@ -25088,19 +25295,54 @@ ${top3.map(
25088
25295
  }
25089
25296
  contextParts.push(`## Execution Settings
25090
25297
  ${settingsParts.join("\n")}`);
25298
+ const STUCK_THRESHOLD_CYCLES = 2;
25299
+ const productiveCallCount = countProductiveToolCalls(db2, worker.id, STUCK_THRESHOLD_CYCLES);
25300
+ const recentCompletedCycles = listRoomCycles(db2, roomId, 5).filter((c) => c.workerId === worker.id && c.status === "completed");
25301
+ const isStuck = recentCompletedCycles.length >= STUCK_THRESHOLD_CYCLES && productiveCallCount === 0;
25302
+ if (isStuck) {
25303
+ contextParts.push(`## \u26A0 STUCK DETECTED
25304
+ Your last ${STUCK_THRESHOLD_CYCLES} cycles produced no external results (no web searches, no memories stored, no goal progress, no keeper messages). You MUST change strategy NOW:
25305
+ - Try a different web search query
25306
+ - Store what you know in memory even if incomplete
25307
+ - Update goal progress with what you've learned
25308
+ - Message the keeper if you're blocked
25309
+ Do NOT repeat the same approach. Pivot immediately.`);
25310
+ logBuffer.addSynthetic("system", `Stuck detector: 0 productive tool calls in last ${STUCK_THRESHOLD_CYCLES} cycles \u2014 injecting pivot directive`);
25311
+ }
25091
25312
  const selfRegulateHint = rateLimitEvents.length > 0 ? "\n- **Self-regulate**: You are hitting rate limits. Use quoroom_configure_room to increase your cycle gap or reduce max turns to stay within API limits." : "";
25092
25313
  const isClaude = model === "claude" || model.startsWith("claude-");
25093
25314
  const toolCallInstruction = isClaude ? "Always call tools to take action \u2014 do not just describe what you would do." : "IMPORTANT: You MUST call at least one tool in your response. Respond ONLY with a tool call \u2014 do not write explanatory text without a tool call.";
25094
- const commsTools = isCli ? "quoroom_inbox_send_keeper (message keeper), quoroom_inbox_list (inter-room), quoroom_inbox_send_room, quoroom_inbox_reply" : "quoroom_ask_keeper";
25095
- const webTools = isCli ? "(use your built-in web search and fetch tools)" : "quoroom_web_search, quoroom_web_fetch, quoroom_browser";
25096
- const toolList = `**Goals:** quoroom_set_goal, quoroom_update_progress, quoroom_create_subgoal, quoroom_complete_goal, quoroom_abandon_goal
25097
- **Governance:** quoroom_propose, quoroom_vote
25098
- **Workers:** quoroom_create_worker, quoroom_update_worker
25099
- **Tasks:** quoroom_schedule
25100
- **Memory:** quoroom_remember, quoroom_recall
25101
- **Web:** ${webTools}
25102
- **Comms:** ${commsTools}
25103
- **Settings:** quoroom_configure_room${selfRegulateHint}`;
25315
+ const allowListRaw = status2.room.allowedTools?.trim() || null;
25316
+ const allowSet = allowListRaw ? new Set(allowListRaw.split(",").map((s) => s.trim())) : null;
25317
+ const has = (name) => !allowSet || allowSet.has(name);
25318
+ const toolLines = [];
25319
+ const goalTools = ["quoroom_set_goal", "quoroom_update_progress", "quoroom_create_subgoal", "quoroom_complete_goal", "quoroom_abandon_goal"].filter(has);
25320
+ if (goalTools.length) toolLines.push(`**Goals:** ${goalTools.join(", ")}`);
25321
+ const govTools = ["quoroom_propose", "quoroom_vote"].filter(has);
25322
+ if (govTools.length) toolLines.push(`**Governance:** ${govTools.join(", ")}`);
25323
+ const workerTools = ["quoroom_create_worker", "quoroom_update_worker"].filter(has);
25324
+ if (workerTools.length) toolLines.push(`**Workers:** ${workerTools.join(", ")}`);
25325
+ if (has("quoroom_schedule")) toolLines.push("**Tasks:** quoroom_schedule");
25326
+ const memTools = ["quoroom_remember", "quoroom_recall"].filter(has);
25327
+ if (memTools.length) toolLines.push(`**Memory:** ${memTools.join(", ")}`);
25328
+ const walletToolNames = isCli ? ["quoroom_wallet_balance", "quoroom_wallet_send", "quoroom_wallet_history", "quoroom_wallet_topup"] : ["quoroom_wallet_balance", "quoroom_wallet_send", "quoroom_wallet_history"];
25329
+ const filteredWallet = walletToolNames.filter(has);
25330
+ if (filteredWallet.length) toolLines.push(`**Wallet:** ${filteredWallet.join(", ")}`);
25331
+ const webToolNames = isCli ? null : ["quoroom_web_search", "quoroom_web_fetch", "quoroom_browser"];
25332
+ if (isCli) {
25333
+ if (has("quoroom_web_search") || has("quoroom_web_fetch")) toolLines.push("**Web:** (use your built-in web search and fetch tools)");
25334
+ } else {
25335
+ const filteredWeb = (webToolNames || []).filter(has);
25336
+ if (filteredWeb.length) toolLines.push(`**Web:** ${filteredWeb.join(", ")}`);
25337
+ }
25338
+ const commsToolNames = isCli ? ["quoroom_inbox_send_keeper", "quoroom_inbox_list", "quoroom_inbox_send_room", "quoroom_inbox_reply"] : ["quoroom_ask_keeper"];
25339
+ const filteredComms = commsToolNames.filter(has);
25340
+ if (filteredComms.length) {
25341
+ if (isCli) toolLines.push(`**Comms:** ${filteredComms.map((t) => t === "quoroom_inbox_send_keeper" ? `${t} (message keeper)` : t === "quoroom_inbox_list" ? `${t} (inter-room)` : t).join(", ")}`);
25342
+ else toolLines.push(`**Comms:** ${filteredComms.join(", ")}`);
25343
+ }
25344
+ if (has("quoroom_configure_room")) toolLines.push(`**Settings:** quoroom_configure_room${selfRegulateHint}`);
25345
+ const toolList = toolLines.join("\n");
25104
25346
  const sendKeeperTool = isCli ? "quoroom_inbox_send_keeper" : "quoroom_ask_keeper";
25105
25347
  contextParts.push(`## Instructions
25106
25348
  Based on the current state, decide what to do next and call the appropriate tools. Available tools:
@@ -25109,6 +25351,8 @@ ${toolList}
25109
25351
 
25110
25352
  Do NOT "stand by" or wait for anyone \u2014 every cycle must make progress. Act autonomously: make decisions and execute. Inform the keeper of progress or important updates using ${sendKeeperTool}, but never block on a response. If the keeper hasn't replied, proceed with your best judgment.
25111
25353
 
25354
+ Revenue is always a priority. Every room must sustain itself financially. Actively seek ways to earn: offer services to the keeper, propose paid work to other rooms, or find monetizable opportunities in your domain. Check your wallet balance and report financial status to the keeper.
25355
+
25112
25356
  ${toolCallInstruction}`);
25113
25357
  const prompt = contextParts.join("\n\n");
25114
25358
  updateAgentState(db2, worker.id, "acting");
@@ -25117,8 +25361,9 @@ ${toolCallInstruction}`);
25117
25361
  logBuffer.flush();
25118
25362
  const apiKey = apiKeyEarly;
25119
25363
  const needsQueenTools = model === "openai" || model.startsWith("openai:") || model === "anthropic" || model.startsWith("anthropic:") || model.startsWith("claude-api:");
25364
+ const filteredToolDefs = allowSet ? QUEEN_TOOL_DEFINITIONS.filter((t) => allowSet.has(t.function.name)) : QUEEN_TOOL_DEFINITIONS;
25120
25365
  const apiToolOpts = needsQueenTools ? {
25121
- toolDefs: QUEEN_TOOL_DEFINITIONS,
25366
+ toolDefs: filteredToolDefs,
25122
25367
  onToolCall: async (toolName, args) => {
25123
25368
  logBuffer.addSynthetic("tool_call", `\u2192 ${toolName}(${JSON.stringify(args)})`);
25124
25369
  const result2 = await executeQueenTool(db2, roomId, worker.id, toolName, args);
@@ -25134,6 +25379,8 @@ ${toolCallInstruction}`);
25134
25379
  timeoutMs: 5 * 60 * 1e3,
25135
25380
  maxTurns: maxTurns ?? 10,
25136
25381
  onConsoleLog: logBuffer.onConsoleLog,
25382
+ // CLI models: block non-quoroom MCP tools (daymon, etc.)
25383
+ disallowedTools: isCli ? "mcp__daymon*" : void 0,
25137
25384
  // CLI models: pass resumeSessionId for native --resume
25138
25385
  resumeSessionId,
25139
25386
  // API models: pass conversation history + persistence callback
@@ -25148,6 +25395,23 @@ ${toolCallInstruction}`);
25148
25395
  if (rateLimitInfo) {
25149
25396
  throw new RateLimitError(rateLimitInfo);
25150
25397
  }
25398
+ if (result.exitCode !== 0) {
25399
+ const errorDetail = result.output?.trim() || `exit code ${result.exitCode}`;
25400
+ logBuffer.addSynthetic("error", `Agent execution failed: ${errorDetail.slice(0, 500)}`);
25401
+ logBuffer.flush();
25402
+ completeWorkerCycle(db2, cycle.id, errorDetail.slice(0, 500), result.usage);
25403
+ options?.onCycleLifecycle?.("failed", cycle.id, roomId);
25404
+ logRoomActivity(
25405
+ db2,
25406
+ roomId,
25407
+ "error",
25408
+ `Agent cycle failed (${worker.name}): ${errorDetail.slice(0, 200)}`,
25409
+ errorDetail,
25410
+ worker.id
25411
+ );
25412
+ updateAgentState(db2, worker.id, "idle");
25413
+ return result.output;
25414
+ }
25151
25415
  if (isCli && result.sessionId) {
25152
25416
  saveAgentSession(db2, worker.id, { sessionId: result.sessionId, model });
25153
25417
  }
@@ -25556,6 +25820,7 @@ function registerRoomRoutes(router) {
25556
25820
  const trimmed = body.queenNickname.trim().replace(/\s+/g, "");
25557
25821
  if (trimmed.length > 0 && trimmed.length <= 40) updates.queenNickname = trimmed;
25558
25822
  }
25823
+ if (body.allowedTools !== void 0) updates.allowedTools = body.allowedTools || null;
25559
25824
  if (body.config !== void 0 && typeof body.config === "object" && body.config !== null) {
25560
25825
  updates.config = { ...room.config, ...body.config };
25561
25826
  }
@@ -28047,7 +28312,15 @@ function registerChatRoutes(router) {
28047
28312
  // 3 minutes
28048
28313
  });
28049
28314
  if (result.exitCode !== 0 || result.timedOut) {
28050
- const reason = result.output?.trim() || (result.timedOut ? "Chat request timed out" : "Chat execution failed");
28315
+ const rawOutput = result.output?.trim();
28316
+ let reason;
28317
+ if (result.timedOut) {
28318
+ reason = "Chat request timed out";
28319
+ } else if (rawOutput) {
28320
+ reason = rawOutput;
28321
+ } else {
28322
+ reason = `Chat execution failed (model: ${model}, exit code: ${result.exitCode})`;
28323
+ }
28051
28324
  return { status: result.timedOut ? 504 : 502, error: reason.slice(0, 500) };
28052
28325
  }
28053
28326
  const response = result.output || "No response";
@@ -28134,6 +28407,7 @@ CREATE TABLE IF NOT EXISTS rooms (
28134
28407
  queen_nickname TEXT,
28135
28408
  chat_session_id TEXT,
28136
28409
  referred_by_code TEXT,
28410
+ allowed_tools TEXT,
28137
28411
  created_at DATETIME DEFAULT (datetime('now','localtime')),
28138
28412
  updated_at DATETIME DEFAULT (datetime('now','localtime'))
28139
28413
  );
@@ -28618,6 +28892,13 @@ function runMigrations(database, log = console.log) {
28618
28892
  database.exec(`ALTER TABLE workers ADD COLUMN max_turns INTEGER`);
28619
28893
  log("Migrated: added cycle_gap_ms and max_turns columns to workers");
28620
28894
  }
28895
+ const hasRoomAllowedTools = database.prepare(
28896
+ `SELECT name FROM pragma_table_info('rooms') WHERE name='allowed_tools'`
28897
+ ).get()?.name;
28898
+ if (!hasRoomAllowedTools) {
28899
+ database.exec(`ALTER TABLE rooms ADD COLUMN allowed_tools TEXT`);
28900
+ log("Migrated: added allowed_tools column to rooms");
28901
+ }
28621
28902
  const ollamaWorkers = database.prepare(`SELECT id FROM workers WHERE model LIKE 'ollama:%'`).all();
28622
28903
  if (ollamaWorkers.length > 0) {
28623
28904
  database.prepare(`UPDATE workers SET model = 'claude' WHERE model LIKE 'ollama:%'`).run();
@@ -28835,7 +29116,7 @@ function semverGt(a, b) {
28835
29116
  }
28836
29117
  function getCurrentVersion() {
28837
29118
  try {
28838
- return true ? "0.1.21" : null.version;
29119
+ return true ? "0.1.22" : null.version;
28839
29120
  } catch {
28840
29121
  return "0.0.0";
28841
29122
  }
@@ -28992,7 +29273,7 @@ var cachedVersion = null;
28992
29273
  function getVersion3() {
28993
29274
  if (cachedVersion) return cachedVersion;
28994
29275
  try {
28995
- cachedVersion = true ? "0.1.21" : null.version;
29276
+ cachedVersion = true ? "0.1.22" : null.version;
28996
29277
  } catch {
28997
29278
  cachedVersion = "unknown";
28998
29279
  }
package/out/mcp/cli.js CHANGED
@@ -21632,6 +21632,7 @@ CREATE TABLE IF NOT EXISTS rooms (
21632
21632
  queen_nickname TEXT,
21633
21633
  chat_session_id TEXT,
21634
21634
  referred_by_code TEXT,
21635
+ allowed_tools TEXT,
21635
21636
  created_at DATETIME DEFAULT (datetime('now','localtime')),
21636
21637
  updated_at DATETIME DEFAULT (datetime('now','localtime'))
21637
21638
  );
@@ -22961,6 +22962,7 @@ function mapRoomRow(row) {
22961
22962
  queenNickname: row.queen_nickname ?? null,
22962
22963
  chatSessionId: row.chat_session_id ?? null,
22963
22964
  referredByCode: row.referred_by_code ?? null,
22965
+ allowedTools: row.allowed_tools ?? null,
22964
22966
  webhookToken: row.webhook_token ?? null,
22965
22967
  createdAt: row.created_at,
22966
22968
  updatedAt: row.updated_at
@@ -23011,6 +23013,7 @@ function updateRoom(db3, id, updates) {
23011
23013
  config: "config",
23012
23014
  referredByCode: "referred_by_code",
23013
23015
  queenNickname: "queen_nickname",
23016
+ allowedTools: "allowed_tools",
23014
23017
  webhookToken: "webhook_token"
23015
23018
  };
23016
23019
  const fields = [];
@@ -23725,6 +23728,22 @@ function listRoomCycles(db3, roomId, limit = 20) {
23725
23728
  ).all(roomId, safeLimit);
23726
23729
  return rows.map(mapWorkerCycleRow);
23727
23730
  }
23731
+ function countProductiveToolCalls(db3, workerId, lastNCycles = 2) {
23732
+ const row = db3.prepare(`
23733
+ SELECT COUNT(*) as cnt FROM cycle_logs
23734
+ WHERE cycle_id IN (
23735
+ SELECT id FROM worker_cycles
23736
+ WHERE worker_id = ? AND status = 'completed'
23737
+ ORDER BY started_at DESC LIMIT ?
23738
+ )
23739
+ AND entry_type = 'tool_call'
23740
+ AND (content LIKE '%web_search%' OR content LIKE '%web_fetch%' OR content LIKE '%remember%'
23741
+ OR content LIKE '%ask_keeper%' OR content LIKE '%inbox_send%'
23742
+ OR content LIKE '%update_progress%' OR content LIKE '%complete_goal%'
23743
+ OR content LIKE '%set_goal%')
23744
+ `).get(workerId, lastNCycles);
23745
+ return row.cnt;
23746
+ }
23728
23747
  function cleanupStaleCycles(db3) {
23729
23748
  const result = db3.prepare(
23730
23749
  "UPDATE worker_cycles SET status = 'failed', error_message = 'Server restarted', finished_at = datetime('now','localtime') WHERE status = 'running'"
@@ -23967,6 +23986,13 @@ function runMigrations(database, log = console.log) {
23967
23986
  database.exec(`ALTER TABLE workers ADD COLUMN max_turns INTEGER`);
23968
23987
  log("Migrated: added cycle_gap_ms and max_turns columns to workers");
23969
23988
  }
23989
+ const hasRoomAllowedTools = database.prepare(
23990
+ `SELECT name FROM pragma_table_info('rooms') WHERE name='allowed_tools'`
23991
+ ).get()?.name;
23992
+ if (!hasRoomAllowedTools) {
23993
+ database.exec(`ALTER TABLE rooms ADD COLUMN allowed_tools TEXT`);
23994
+ log("Migrated: added allowed_tools column to rooms");
23995
+ }
23970
23996
  const ollamaWorkers = database.prepare(`SELECT id FROM workers WHERE model LIKE 'ollama:%'`).all();
23971
23997
  if (ollamaWorkers.length > 0) {
23972
23998
  database.prepare(`UPDATE workers SET model = 'claude' WHERE model LIKE 'ollama:%'`).run();
@@ -51046,7 +51072,7 @@ var server_exports = {};
51046
51072
  async function main() {
51047
51073
  const server = new McpServer({
51048
51074
  name: "quoroom",
51049
- version: true ? "0.1.21" : "0.0.0"
51075
+ version: true ? "0.1.22" : "0.0.0"
51050
51076
  });
51051
51077
  registerMemoryTools(server);
51052
51078
  registerSchedulerTools(server);
@@ -51630,33 +51656,131 @@ async function closeBrowser() {
51630
51656
  }
51631
51657
  }
51632
51658
  async function webFetch(url) {
51633
- const jinaUrl = `https://r.jina.ai/${url}`;
51634
- const response = await fetch(jinaUrl, {
51635
- headers: {
51636
- "Accept": "text/plain",
51637
- "X-No-Cache": "true"
51638
- },
51639
- signal: AbortSignal.timeout(3e4)
51659
+ try {
51660
+ const jinaUrl = `https://r.jina.ai/${url}`;
51661
+ const response = await fetch(jinaUrl, {
51662
+ headers: { "Accept": "text/plain", "X-No-Cache": "true" },
51663
+ signal: AbortSignal.timeout(2e4)
51664
+ });
51665
+ if (response.ok) {
51666
+ const text = await response.text();
51667
+ if (text.length > 200 && !text.includes("Warning: Target URL returned error")) {
51668
+ return text.slice(0, MAX_CONTENT_CHARS);
51669
+ }
51670
+ }
51671
+ } catch {
51672
+ }
51673
+ return fetchWithBrowser(url);
51674
+ }
51675
+ async function fetchWithBrowser(url) {
51676
+ const browser = await getBrowser();
51677
+ const context = await browser.newContext({
51678
+ userAgent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
51640
51679
  });
51641
- if (!response.ok) {
51642
- throw new Error(`Jina fetch failed: ${response.status} ${response.statusText}`);
51680
+ const page = await context.newPage();
51681
+ try {
51682
+ await page.goto(url, { waitUntil: "domcontentloaded", timeout: 3e4 });
51683
+ const text = await page.innerText("body").catch(() => "");
51684
+ if (!text) throw new Error(`Could not read content from ${url}`);
51685
+ return text.slice(0, MAX_CONTENT_CHARS);
51686
+ } finally {
51687
+ await context.close();
51643
51688
  }
51644
- const text = await response.text();
51645
- return text.slice(0, MAX_CONTENT_CHARS);
51646
51689
  }
51647
51690
  async function webSearch(query) {
51648
- const searchUrl = `https://html.duckduckgo.com/html/?q=${encodeURIComponent(query)}`;
51649
- const response = await fetch(searchUrl, {
51650
- headers: {
51651
- "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
51652
- },
51653
- signal: AbortSignal.timeout(15e3)
51691
+ const browserResults = await searchWithBrowser(query);
51692
+ if (browserResults.length > 0) return browserResults;
51693
+ const ddgResults = await searchDdg(query);
51694
+ if (ddgResults.length > 0) return ddgResults;
51695
+ try {
51696
+ const response = await fetch(`https://s.jina.ai/${encodeURIComponent(query)}`, {
51697
+ headers: { "Accept": "application/json", "X-No-Cache": "true" },
51698
+ signal: AbortSignal.timeout(15e3)
51699
+ });
51700
+ if (response.ok) {
51701
+ const data = await response.json();
51702
+ if (data.data && Array.isArray(data.data)) {
51703
+ return data.data.slice(0, 5).map((r) => ({
51704
+ title: r.title ?? "",
51705
+ url: r.url ?? "",
51706
+ snippet: (r.description ?? r.content ?? "").slice(0, 300)
51707
+ })).filter((r) => r.url);
51708
+ }
51709
+ }
51710
+ } catch {
51711
+ }
51712
+ return [];
51713
+ }
51714
+ async function searchWithBrowser(query) {
51715
+ let browser;
51716
+ try {
51717
+ browser = await getBrowser();
51718
+ } catch {
51719
+ return [];
51720
+ }
51721
+ const context = await browser.newContext({
51722
+ userAgent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
51723
+ locale: "en-US"
51654
51724
  });
51655
- if (!response.ok) {
51656
- throw new Error(`DuckDuckGo search failed: ${response.status}`);
51725
+ const page = await context.newPage();
51726
+ try {
51727
+ await page.goto(`https://search.yahoo.com/search?p=${encodeURIComponent(query)}`, {
51728
+ waitUntil: "domcontentloaded",
51729
+ timeout: 15e3
51730
+ });
51731
+ await page.waitForTimeout(1e3);
51732
+ const results = await page.evaluate(() => {
51733
+ const items = [];
51734
+ const blocks = document.querySelectorAll("#web .algo, .dd.algo, .algo");
51735
+ for (const block of blocks) {
51736
+ const link = block.querySelector("a");
51737
+ const h3 = block.querySelector("h3");
51738
+ const snippetEl = block.querySelector(".compText p, .compText, p");
51739
+ if (!link) continue;
51740
+ const url = link.getAttribute("href") || "";
51741
+ if (!url.startsWith("http")) continue;
51742
+ items.push({
51743
+ title: h3 ? (h3.textContent || "").trim() : (link.textContent || "").trim(),
51744
+ url,
51745
+ snippet: snippetEl ? (snippetEl.textContent || "").trim().slice(0, 300) : ""
51746
+ });
51747
+ if (items.length >= 5) break;
51748
+ }
51749
+ return items;
51750
+ });
51751
+ return results.filter((r) => r.url);
51752
+ } catch {
51753
+ return [];
51754
+ } finally {
51755
+ await context.close();
51657
51756
  }
51658
- const html = await response.text();
51659
- return parseDdgResults(html).slice(0, 5);
51757
+ }
51758
+ async function searchDdg(query) {
51759
+ for (let attempt = 0; attempt < 3; attempt++) {
51760
+ if (attempt > 0) await new Promise((r) => setTimeout(r, 1e3 * attempt));
51761
+ try {
51762
+ const response = await fetch("https://html.duckduckgo.com/html/", {
51763
+ method: "POST",
51764
+ headers: {
51765
+ "Content-Type": "application/x-www-form-urlencoded",
51766
+ "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
51767
+ "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
51768
+ "Accept-Language": "en-US,en;q=0.9",
51769
+ "Referer": "https://html.duckduckgo.com/"
51770
+ },
51771
+ body: `q=${encodeURIComponent(query)}&b=`,
51772
+ signal: AbortSignal.timeout(15e3),
51773
+ redirect: "follow"
51774
+ });
51775
+ if (response.status === 202) continue;
51776
+ if (!response.ok) continue;
51777
+ const html = await response.text();
51778
+ const results = parseDdgResults(html).slice(0, 5);
51779
+ if (results.length > 0) return results;
51780
+ } catch {
51781
+ }
51782
+ }
51783
+ return [];
51660
51784
  }
51661
51785
  function parseDdgResults(html) {
51662
51786
  const results = [];
@@ -51785,10 +51909,12 @@ async function executeQueenTool(db3, roomId, workerId, toolName, args2) {
51785
51909
  if (goalCheck.roomId !== roomId) return { content: `Error: goal #${goalId} belongs to another room. Your room's goals are shown in the Active Goals section \u2014 use those goal IDs.`, isError: true };
51786
51910
  const observation = String(args2.observation ?? args2.progress ?? args2.message ?? args2.text ?? "");
51787
51911
  const metricValue = args2.metricValue != null ? Number(args2.metricValue) : args2.metric_value != null ? Number(args2.metric_value) : void 0;
51912
+ const subGoals = getSubGoals(db3, goalId);
51788
51913
  updateGoalProgress(db3, goalId, observation, metricValue, workerId);
51789
51914
  const goal = getGoal(db3, goalId);
51790
51915
  const pct = Math.round((goal?.progress ?? 0) * 100);
51791
- return { content: `Progress logged on goal #${goalId}. Now at ${pct}%.` };
51916
+ const note = subGoals.length > 0 && metricValue != null ? ` (metricValue ignored \u2014 goal has ${subGoals.length} sub-goals, progress is calculated from them. Update sub-goals directly.)` : "";
51917
+ return { content: `Progress logged on goal #${goalId}. Now at ${pct}%.${note}` };
51792
51918
  }
51793
51919
  case "quoroom_create_subgoal": {
51794
51920
  const goalId = Number(args2.goalId);
@@ -51821,6 +51947,13 @@ async function executeQueenTool(db3, roomId, workerId, toolName, args2) {
51821
51947
  case "quoroom_propose": {
51822
51948
  const proposalText = String(args2.proposal ?? args2.text ?? args2.description ?? args2.content ?? args2.idea ?? "").trim();
51823
51949
  if (!proposalText) return { content: 'Error: proposal text is required. Provide a "proposal" string.', isError: true };
51950
+ const recentDecisions = listDecisions(db3, roomId);
51951
+ const isDuplicate = recentDecisions.slice(0, 10).some(
51952
+ (d) => (d.status === "voting" || d.status === "approved") && d.proposal.toLowerCase() === proposalText.toLowerCase()
51953
+ );
51954
+ if (isDuplicate) {
51955
+ return { content: `A similar proposal already exists: "${proposalText}". No need to propose again.`, isError: true };
51956
+ }
51824
51957
  const decisionType = String(args2.decisionType ?? args2.type ?? args2.impact ?? args2.category ?? "low_impact");
51825
51958
  const decision = propose(db3, {
51826
51959
  roomId,
@@ -51861,6 +51994,10 @@ async function executeQueenTool(db3, roomId, workerId, toolName, args2) {
51861
51994
  const systemPrompt = String(args2.systemPrompt ?? args2.system_prompt ?? args2.instructions ?? args2.prompt ?? "").trim();
51862
51995
  if (!name) return { content: 'Error: name is required for quoroom_create_worker. Provide a "name" string.', isError: true };
51863
51996
  if (!systemPrompt) return { content: `Error: systemPrompt is required for quoroom_create_worker. Provide a "systemPrompt" string describing this worker's role and instructions.`, isError: true };
51997
+ const existingWorkers = listRoomWorkers(db3, roomId);
51998
+ if (existingWorkers.some((w) => w.name.toLowerCase() === name.toLowerCase())) {
51999
+ return { content: `Worker "${name}" already exists in this room. Use quoroom_update_worker to modify it, or choose a different name.`, isError: true };
52000
+ }
51864
52001
  const role = args2.role && args2.role !== args2.name ? String(args2.role) : void 0;
51865
52002
  const description = args2.description ? String(args2.description) : void 0;
51866
52003
  const preset = role ? WORKER_ROLE_PRESETS[role] : void 0;
@@ -51893,6 +52030,10 @@ async function executeQueenTool(db3, roomId, workerId, toolName, args2) {
51893
52030
  const taskWorkerId = args2.workerId ? Number(args2.workerId) : void 0;
51894
52031
  const maxTurns = args2.maxTurns ? Number(args2.maxTurns) : void 0;
51895
52032
  const triggerType = cronExpression ? "cron" : scheduledAt ? "once" : "manual";
52033
+ const existingTasks = listTasks(db3, roomId, "active");
52034
+ if (existingTasks.some((t) => t.name.toLowerCase() === name.toLowerCase())) {
52035
+ return { content: `Task "${name}" already exists. Choose a different name or manage the existing task.`, isError: true };
52036
+ }
51896
52037
  if (taskWorkerId) {
51897
52038
  const taskWorker = getWorker(db3, taskWorkerId);
51898
52039
  if (!taskWorker || taskWorker.roomId !== roomId) {
@@ -51918,6 +52059,11 @@ async function executeQueenTool(db3, roomId, workerId, toolName, args2) {
51918
52059
  const name = String(args2.name ?? "");
51919
52060
  const content = String(args2.content ?? "");
51920
52061
  const type = String(args2.type ?? "fact");
52062
+ const existing = listEntities(db3, roomId).find((e) => e.name.toLowerCase() === name.toLowerCase());
52063
+ if (existing) {
52064
+ addObservation(db3, existing.id, content, "queen");
52065
+ return { content: `Updated memory "${name}" (added new observation to existing entry).` };
52066
+ }
51921
52067
  const entity = createEntity(db3, name, type, void 0, roomId);
51922
52068
  addObservation(db3, entity.id, content, "queen");
51923
52069
  return { content: `Remembered "${name}".` };
@@ -51972,6 +52118,26 @@ async function executeQueenTool(db3, roomId, workerId, toolName, args2) {
51972
52118
  if (!url) return { content: "Error: url is required", isError: true };
51973
52119
  return { content: await browserAction(url, actions) };
51974
52120
  }
52121
+ // ── Wallet ────────────────────────────────────────────────────
52122
+ case "quoroom_wallet_balance": {
52123
+ const wallet = getWalletByRoom(db3, roomId);
52124
+ if (!wallet) return { content: "No wallet found for this room.", isError: true };
52125
+ const summary = getWalletTransactionSummary(db3, wallet.id);
52126
+ const net = (parseFloat(summary.received) - parseFloat(summary.sent)).toFixed(2);
52127
+ return { content: `Wallet ${wallet.address}: ${net} USDC (received: ${summary.received}, sent: ${summary.sent})` };
52128
+ }
52129
+ case "quoroom_wallet_send": {
52130
+ return { content: "Wallet send requires on-chain transaction \u2014 use the MCP tool quoroom_wallet_send with encryptionKey, or ask the keeper to send funds.", isError: true };
52131
+ }
52132
+ case "quoroom_wallet_history": {
52133
+ const wallet = getWalletByRoom(db3, roomId);
52134
+ if (!wallet) return { content: "No wallet found for this room.", isError: true };
52135
+ const limit = Math.min(Number(args2.limit) || 10, 50);
52136
+ const txs = listWalletTransactions(db3, wallet.id, limit);
52137
+ if (txs.length === 0) return { content: "No transactions yet." };
52138
+ const lines = txs.map((tx) => `[${tx.type}] ${tx.amount} USDC \u2014 ${tx.description ?? ""} (${tx.status})`).join("\n");
52139
+ return { content: lines };
52140
+ }
51975
52141
  default:
51976
52142
  return { content: `Unknown tool: ${toolName}`, isError: true };
51977
52143
  }
@@ -52334,6 +52500,47 @@ var init_queen_tools = __esm({
52334
52500
  required: ["url", "actions"]
52335
52501
  }
52336
52502
  }
52503
+ },
52504
+ // ── Wallet ──────────────────────────────────────────────────────────────
52505
+ {
52506
+ type: "function",
52507
+ function: {
52508
+ name: "quoroom_wallet_balance",
52509
+ description: "Get the room's wallet balance (USDC). Returns address and transaction summary.",
52510
+ parameters: {
52511
+ type: "object",
52512
+ properties: {},
52513
+ required: []
52514
+ }
52515
+ }
52516
+ },
52517
+ {
52518
+ type: "function",
52519
+ function: {
52520
+ name: "quoroom_wallet_send",
52521
+ description: "Send USDC from the room's wallet to an address.",
52522
+ parameters: {
52523
+ type: "object",
52524
+ properties: {
52525
+ to: { type: "string", description: "Recipient address (0x...)" },
52526
+ amount: { type: "string", description: 'Amount (e.g., "10.50")' }
52527
+ },
52528
+ required: ["to", "amount"]
52529
+ }
52530
+ }
52531
+ },
52532
+ {
52533
+ type: "function",
52534
+ function: {
52535
+ name: "quoroom_wallet_history",
52536
+ description: "Get recent wallet transaction history.",
52537
+ parameters: {
52538
+ type: "object",
52539
+ properties: {
52540
+ limit: { type: "string", description: "Max transactions to return (default: 10)" }
52541
+ }
52542
+ }
52543
+ }
52337
52544
  }
52338
52545
  ];
52339
52546
  }
@@ -52713,6 +52920,14 @@ ${roomTasks.map(
52713
52920
  (t) => `- #${t.id} "${t.name}" [${t.triggerType}] \u2014 ${t.status}`
52714
52921
  ).join("\n")}`);
52715
52922
  }
52923
+ const wallet = getWalletByRoom(db3, roomId);
52924
+ if (wallet) {
52925
+ const summary = getWalletTransactionSummary(db3, wallet.id);
52926
+ const net = (parseFloat(summary.received) - parseFloat(summary.sent)).toFixed(2);
52927
+ contextParts.push(`## Wallet
52928
+ Address: ${wallet.address}
52929
+ Balance: ${net} USDC (received: ${summary.received}, spent: ${summary.sent})`);
52930
+ }
52716
52931
  if (unreadMessages.length > 0) {
52717
52932
  contextParts.push(`## Unread Messages
52718
52933
  ${unreadMessages.map(
@@ -52750,19 +52965,54 @@ ${top3.map(
52750
52965
  }
52751
52966
  contextParts.push(`## Execution Settings
52752
52967
  ${settingsParts.join("\n")}`);
52968
+ const STUCK_THRESHOLD_CYCLES = 2;
52969
+ const productiveCallCount = countProductiveToolCalls(db3, worker.id, STUCK_THRESHOLD_CYCLES);
52970
+ const recentCompletedCycles = listRoomCycles(db3, roomId, 5).filter((c) => c.workerId === worker.id && c.status === "completed");
52971
+ const isStuck = recentCompletedCycles.length >= STUCK_THRESHOLD_CYCLES && productiveCallCount === 0;
52972
+ if (isStuck) {
52973
+ contextParts.push(`## \u26A0 STUCK DETECTED
52974
+ Your last ${STUCK_THRESHOLD_CYCLES} cycles produced no external results (no web searches, no memories stored, no goal progress, no keeper messages). You MUST change strategy NOW:
52975
+ - Try a different web search query
52976
+ - Store what you know in memory even if incomplete
52977
+ - Update goal progress with what you've learned
52978
+ - Message the keeper if you're blocked
52979
+ Do NOT repeat the same approach. Pivot immediately.`);
52980
+ logBuffer.addSynthetic("system", `Stuck detector: 0 productive tool calls in last ${STUCK_THRESHOLD_CYCLES} cycles \u2014 injecting pivot directive`);
52981
+ }
52753
52982
  const selfRegulateHint = rateLimitEvents.length > 0 ? "\n- **Self-regulate**: You are hitting rate limits. Use quoroom_configure_room to increase your cycle gap or reduce max turns to stay within API limits." : "";
52754
52983
  const isClaude = model === "claude" || model.startsWith("claude-");
52755
52984
  const toolCallInstruction = isClaude ? "Always call tools to take action \u2014 do not just describe what you would do." : "IMPORTANT: You MUST call at least one tool in your response. Respond ONLY with a tool call \u2014 do not write explanatory text without a tool call.";
52756
- const commsTools = isCli ? "quoroom_inbox_send_keeper (message keeper), quoroom_inbox_list (inter-room), quoroom_inbox_send_room, quoroom_inbox_reply" : "quoroom_ask_keeper";
52757
- const webTools = isCli ? "(use your built-in web search and fetch tools)" : "quoroom_web_search, quoroom_web_fetch, quoroom_browser";
52758
- const toolList = `**Goals:** quoroom_set_goal, quoroom_update_progress, quoroom_create_subgoal, quoroom_complete_goal, quoroom_abandon_goal
52759
- **Governance:** quoroom_propose, quoroom_vote
52760
- **Workers:** quoroom_create_worker, quoroom_update_worker
52761
- **Tasks:** quoroom_schedule
52762
- **Memory:** quoroom_remember, quoroom_recall
52763
- **Web:** ${webTools}
52764
- **Comms:** ${commsTools}
52765
- **Settings:** quoroom_configure_room${selfRegulateHint}`;
52985
+ const allowListRaw = status2.room.allowedTools?.trim() || null;
52986
+ const allowSet = allowListRaw ? new Set(allowListRaw.split(",").map((s) => s.trim())) : null;
52987
+ const has = (name) => !allowSet || allowSet.has(name);
52988
+ const toolLines = [];
52989
+ const goalTools = ["quoroom_set_goal", "quoroom_update_progress", "quoroom_create_subgoal", "quoroom_complete_goal", "quoroom_abandon_goal"].filter(has);
52990
+ if (goalTools.length) toolLines.push(`**Goals:** ${goalTools.join(", ")}`);
52991
+ const govTools = ["quoroom_propose", "quoroom_vote"].filter(has);
52992
+ if (govTools.length) toolLines.push(`**Governance:** ${govTools.join(", ")}`);
52993
+ const workerTools = ["quoroom_create_worker", "quoroom_update_worker"].filter(has);
52994
+ if (workerTools.length) toolLines.push(`**Workers:** ${workerTools.join(", ")}`);
52995
+ if (has("quoroom_schedule")) toolLines.push("**Tasks:** quoroom_schedule");
52996
+ const memTools = ["quoroom_remember", "quoroom_recall"].filter(has);
52997
+ if (memTools.length) toolLines.push(`**Memory:** ${memTools.join(", ")}`);
52998
+ const walletToolNames = isCli ? ["quoroom_wallet_balance", "quoroom_wallet_send", "quoroom_wallet_history", "quoroom_wallet_topup"] : ["quoroom_wallet_balance", "quoroom_wallet_send", "quoroom_wallet_history"];
52999
+ const filteredWallet = walletToolNames.filter(has);
53000
+ if (filteredWallet.length) toolLines.push(`**Wallet:** ${filteredWallet.join(", ")}`);
53001
+ const webToolNames = isCli ? null : ["quoroom_web_search", "quoroom_web_fetch", "quoroom_browser"];
53002
+ if (isCli) {
53003
+ if (has("quoroom_web_search") || has("quoroom_web_fetch")) toolLines.push("**Web:** (use your built-in web search and fetch tools)");
53004
+ } else {
53005
+ const filteredWeb = (webToolNames || []).filter(has);
53006
+ if (filteredWeb.length) toolLines.push(`**Web:** ${filteredWeb.join(", ")}`);
53007
+ }
53008
+ const commsToolNames = isCli ? ["quoroom_inbox_send_keeper", "quoroom_inbox_list", "quoroom_inbox_send_room", "quoroom_inbox_reply"] : ["quoroom_ask_keeper"];
53009
+ const filteredComms = commsToolNames.filter(has);
53010
+ if (filteredComms.length) {
53011
+ if (isCli) toolLines.push(`**Comms:** ${filteredComms.map((t) => t === "quoroom_inbox_send_keeper" ? `${t} (message keeper)` : t === "quoroom_inbox_list" ? `${t} (inter-room)` : t).join(", ")}`);
53012
+ else toolLines.push(`**Comms:** ${filteredComms.join(", ")}`);
53013
+ }
53014
+ if (has("quoroom_configure_room")) toolLines.push(`**Settings:** quoroom_configure_room${selfRegulateHint}`);
53015
+ const toolList = toolLines.join("\n");
52766
53016
  const sendKeeperTool = isCli ? "quoroom_inbox_send_keeper" : "quoroom_ask_keeper";
52767
53017
  contextParts.push(`## Instructions
52768
53018
  Based on the current state, decide what to do next and call the appropriate tools. Available tools:
@@ -52771,6 +53021,8 @@ ${toolList}
52771
53021
 
52772
53022
  Do NOT "stand by" or wait for anyone \u2014 every cycle must make progress. Act autonomously: make decisions and execute. Inform the keeper of progress or important updates using ${sendKeeperTool}, but never block on a response. If the keeper hasn't replied, proceed with your best judgment.
52773
53023
 
53024
+ Revenue is always a priority. Every room must sustain itself financially. Actively seek ways to earn: offer services to the keeper, propose paid work to other rooms, or find monetizable opportunities in your domain. Check your wallet balance and report financial status to the keeper.
53025
+
52774
53026
  ${toolCallInstruction}`);
52775
53027
  const prompt = contextParts.join("\n\n");
52776
53028
  updateAgentState(db3, worker.id, "acting");
@@ -52779,8 +53031,9 @@ ${toolCallInstruction}`);
52779
53031
  logBuffer.flush();
52780
53032
  const apiKey = apiKeyEarly;
52781
53033
  const needsQueenTools = model === "openai" || model.startsWith("openai:") || model === "anthropic" || model.startsWith("anthropic:") || model.startsWith("claude-api:");
53034
+ const filteredToolDefs = allowSet ? QUEEN_TOOL_DEFINITIONS.filter((t) => allowSet.has(t.function.name)) : QUEEN_TOOL_DEFINITIONS;
52782
53035
  const apiToolOpts = needsQueenTools ? {
52783
- toolDefs: QUEEN_TOOL_DEFINITIONS,
53036
+ toolDefs: filteredToolDefs,
52784
53037
  onToolCall: async (toolName, args2) => {
52785
53038
  logBuffer.addSynthetic("tool_call", `\u2192 ${toolName}(${JSON.stringify(args2)})`);
52786
53039
  const result2 = await executeQueenTool(db3, roomId, worker.id, toolName, args2);
@@ -52796,6 +53049,8 @@ ${toolCallInstruction}`);
52796
53049
  timeoutMs: 5 * 60 * 1e3,
52797
53050
  maxTurns: maxTurns ?? 10,
52798
53051
  onConsoleLog: logBuffer.onConsoleLog,
53052
+ // CLI models: block non-quoroom MCP tools (daymon, etc.)
53053
+ disallowedTools: isCli ? "mcp__daymon*" : void 0,
52799
53054
  // CLI models: pass resumeSessionId for native --resume
52800
53055
  resumeSessionId,
52801
53056
  // API models: pass conversation history + persistence callback
@@ -52810,6 +53065,23 @@ ${toolCallInstruction}`);
52810
53065
  if (rateLimitInfo) {
52811
53066
  throw new RateLimitError(rateLimitInfo);
52812
53067
  }
53068
+ if (result.exitCode !== 0) {
53069
+ const errorDetail = result.output?.trim() || `exit code ${result.exitCode}`;
53070
+ logBuffer.addSynthetic("error", `Agent execution failed: ${errorDetail.slice(0, 500)}`);
53071
+ logBuffer.flush();
53072
+ completeWorkerCycle(db3, cycle.id, errorDetail.slice(0, 500), result.usage);
53073
+ options?.onCycleLifecycle?.("failed", cycle.id, roomId);
53074
+ logRoomActivity(
53075
+ db3,
53076
+ roomId,
53077
+ "error",
53078
+ `Agent cycle failed (${worker.name}): ${errorDetail.slice(0, 200)}`,
53079
+ errorDetail,
53080
+ worker.id
53081
+ );
53082
+ updateAgentState(db3, worker.id, "idle");
53083
+ return result.output;
53084
+ }
52813
53085
  if (isCli && result.sessionId) {
52814
53086
  saveAgentSession(db3, worker.id, { sessionId: result.sessionId, model });
52815
53087
  }
@@ -52887,7 +53159,7 @@ var require_package = __commonJS({
52887
53159
  "package.json"(exports2, module2) {
52888
53160
  module2.exports = {
52889
53161
  name: "quoroom",
52890
- version: "0.1.21",
53162
+ version: "0.1.22",
52891
53163
  description: "Autonomous AI agent collective engine \u2014 Queen, Workers, Quorum",
52892
53164
  main: "./out/mcp/server.js",
52893
53165
  bin: {
@@ -52912,7 +53184,7 @@ var require_package = __commonJS({
52912
53184
  "build:mcp": "node scripts/build-mcp.js",
52913
53185
  "build:ui": "vite build --config src/ui/vite.config.ts",
52914
53186
  "kill:ports": "node scripts/kill-ports.js",
52915
- "kill:dev-ports": "npm run kill:ports -- 4700 3710 5173",
53187
+ "kill:dev-ports": "npm run kill:ports -- 4700 3715 5173",
52916
53188
  "dev:links": "node scripts/dev-links.js",
52917
53189
  dev: `sh -c 'npm run kill:dev-ports && trap "kill 0" INT TERM EXIT; npm run dev:links & npm run dev:room & npm run dev:cloud & wait'`,
52918
53190
  "dev:room": "sh -c 'export QUOROOM_DATA_DIR=$HOME/.quoroom-dev QUOROOM_SKIP_MCP_REGISTER=1; npm run build:mcp && npm run build:ui && node scripts/dev-server.js --port 4700'",
@@ -52920,7 +53192,7 @@ var require_package = __commonJS({
52920
53192
  "dev:room:shared": "npm run build:mcp && npm run build:ui && node scripts/dev-server.js",
52921
53193
  "doctor:split": "node scripts/doctor-split.js",
52922
53194
  "dev:isolated": `sh -c 'npm run kill:dev-ports && trap "kill 0" INT TERM EXIT; npm run dev:links & npm run dev:room:isolated & npm run dev:cloud & VITE_API_PORT=4700 npm run dev:ui & wait'`,
52923
- "dev:cloud": `sh -c 'npm run kill:ports -- 3710 && cd ../cloud && PORT=3710 CLOUD_PUBLIC_URL=http://127.0.0.1:3710 CLOUD_ALLOWED_ORIGINS='"'"'http://127.0.0.1:3710,http://localhost:3710,http://localhost:5173,http://127.0.0.1:5173,https://quoroom.ai,https://www.quoroom.ai,https://app.quoroom.ai'"'"' npm start'`,
53195
+ "dev:cloud": `sh -c 'npm run kill:ports -- 3715 && cd ../cloud && PORT=3715 CLOUD_PUBLIC_URL=http://127.0.0.1:3715 CLOUD_ALLOWED_ORIGINS='"'"'http://127.0.0.1:3715,http://localhost:3715,http://localhost:5173,http://127.0.0.1:5173,https://quoroom.ai,https://www.quoroom.ai,https://app.quoroom.ai'"'"' npm start'`,
52924
53196
  "dev:ui": "vite --config src/ui/vite.config.ts",
52925
53197
  "seed:style-demo": "sh -c 'export QUOROOM_DATA_DIR=$HOME/.quoroom-dev; node scripts/seed-style-demo.js'",
52926
53198
  typecheck: "tsc --noEmit",
@@ -53345,6 +53617,7 @@ function registerRoomRoutes(router) {
53345
53617
  const trimmed = body.queenNickname.trim().replace(/\s+/g, "");
53346
53618
  if (trimmed.length > 0 && trimmed.length <= 40) updates.queenNickname = trimmed;
53347
53619
  }
53620
+ if (body.allowedTools !== void 0) updates.allowedTools = body.allowedTools || null;
53348
53621
  if (body.config !== void 0 && typeof body.config === "object" && body.config !== null) {
53349
53622
  updates.config = { ...room.config, ...body.config };
53350
53623
  }
@@ -55344,7 +55617,15 @@ function registerChatRoutes(router) {
55344
55617
  // 3 minutes
55345
55618
  });
55346
55619
  if (result.exitCode !== 0 || result.timedOut) {
55347
- const reason = result.output?.trim() || (result.timedOut ? "Chat request timed out" : "Chat execution failed");
55620
+ const rawOutput = result.output?.trim();
55621
+ let reason;
55622
+ if (result.timedOut) {
55623
+ reason = "Chat request timed out";
55624
+ } else if (rawOutput) {
55625
+ reason = rawOutput;
55626
+ } else {
55627
+ reason = `Chat execution failed (model: ${model}, exit code: ${result.exitCode})`;
55628
+ }
55348
55629
  return { status: result.timedOut ? 504 : 502, error: reason.slice(0, 500) };
55349
55630
  }
55350
55631
  const response = result.output || "No response";
@@ -55558,7 +55839,7 @@ function semverGt(a, b) {
55558
55839
  }
55559
55840
  function getCurrentVersion() {
55560
55841
  try {
55561
- return true ? "0.1.21" : null.version;
55842
+ return true ? "0.1.22" : null.version;
55562
55843
  } catch {
55563
55844
  return "0.0.0";
55564
55845
  }
@@ -55741,7 +56022,7 @@ var init_updateChecker = __esm({
55741
56022
  function getVersion3() {
55742
56023
  if (cachedVersion) return cachedVersion;
55743
56024
  try {
55744
- cachedVersion = true ? "0.1.21" : null.version;
56025
+ cachedVersion = true ? "0.1.22" : null.version;
55745
56026
  } catch {
55746
56027
  cachedVersion = "unknown";
55747
56028
  }
@@ -62056,7 +62337,7 @@ __export(update_exports, {
62056
62337
  });
62057
62338
  function getCurrentVersion2() {
62058
62339
  try {
62059
- return true ? "0.1.21" : null.version;
62340
+ return true ? "0.1.22" : null.version;
62060
62341
  } catch {
62061
62342
  return "0.0.0";
62062
62343
  }
package/out/mcp/server.js CHANGED
@@ -31163,6 +31163,7 @@ CREATE TABLE IF NOT EXISTS rooms (
31163
31163
  queen_nickname TEXT,
31164
31164
  chat_session_id TEXT,
31165
31165
  referred_by_code TEXT,
31166
+ allowed_tools TEXT,
31166
31167
  created_at DATETIME DEFAULT (datetime('now','localtime')),
31167
31168
  updated_at DATETIME DEFAULT (datetime('now','localtime'))
31168
31169
  );
@@ -32357,6 +32358,7 @@ function mapRoomRow(row) {
32357
32358
  queenNickname: row.queen_nickname ?? null,
32358
32359
  chatSessionId: row.chat_session_id ?? null,
32359
32360
  referredByCode: row.referred_by_code ?? null,
32361
+ allowedTools: row.allowed_tools ?? null,
32360
32362
  webhookToken: row.webhook_token ?? null,
32361
32363
  createdAt: row.created_at,
32362
32364
  updatedAt: row.updated_at
@@ -32445,6 +32447,7 @@ function updateRoom(db2, id, updates) {
32445
32447
  config: "config",
32446
32448
  referredByCode: "referred_by_code",
32447
32449
  queenNickname: "queen_nickname",
32450
+ allowedTools: "allowed_tools",
32448
32451
  webhookToken: "webhook_token"
32449
32452
  };
32450
32453
  const fields = [];
@@ -33002,6 +33005,13 @@ function runMigrations(database, log = console.log) {
33002
33005
  database.exec(`ALTER TABLE workers ADD COLUMN max_turns INTEGER`);
33003
33006
  log("Migrated: added cycle_gap_ms and max_turns columns to workers");
33004
33007
  }
33008
+ const hasRoomAllowedTools = database.prepare(
33009
+ `SELECT name FROM pragma_table_info('rooms') WHERE name='allowed_tools'`
33010
+ ).get()?.name;
33011
+ if (!hasRoomAllowedTools) {
33012
+ database.exec(`ALTER TABLE rooms ADD COLUMN allowed_tools TEXT`);
33013
+ log("Migrated: added allowed_tools column to rooms");
33014
+ }
33005
33015
  const ollamaWorkers = database.prepare(`SELECT id FROM workers WHERE model LIKE 'ollama:%'`).all();
33006
33016
  if (ollamaWorkers.length > 0) {
33007
33017
  database.prepare(`UPDATE workers SET model = 'claude' WHERE model LIKE 'ollama:%'`).run();
@@ -47485,7 +47495,7 @@ Share this with the keeper or potential collaborators. Rooms created through thi
47485
47495
  async function main() {
47486
47496
  const server = new McpServer({
47487
47497
  name: "quoroom",
47488
- version: true ? "0.1.21" : "0.0.0"
47498
+ version: true ? "0.1.22" : "0.0.0"
47489
47499
  });
47490
47500
  registerMemoryTools(server);
47491
47501
  registerSchedulerTools(server);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "quoroom",
3
- "version": "0.1.21",
3
+ "version": "0.1.22",
4
4
  "description": "Autonomous AI agent collective engine — Queen, Workers, Quorum",
5
5
  "main": "./out/mcp/server.js",
6
6
  "bin": {
@@ -25,7 +25,7 @@
25
25
  "build:mcp": "node scripts/build-mcp.js",
26
26
  "build:ui": "vite build --config src/ui/vite.config.ts",
27
27
  "kill:ports": "node scripts/kill-ports.js",
28
- "kill:dev-ports": "npm run kill:ports -- 4700 3710 5173",
28
+ "kill:dev-ports": "npm run kill:ports -- 4700 3715 5173",
29
29
  "dev:links": "node scripts/dev-links.js",
30
30
  "dev": "sh -c 'npm run kill:dev-ports && trap \"kill 0\" INT TERM EXIT; npm run dev:links & npm run dev:room & npm run dev:cloud & wait'",
31
31
  "dev:room": "sh -c 'export QUOROOM_DATA_DIR=$HOME/.quoroom-dev QUOROOM_SKIP_MCP_REGISTER=1; npm run build:mcp && npm run build:ui && node scripts/dev-server.js --port 4700'",
@@ -33,7 +33,7 @@
33
33
  "dev:room:shared": "npm run build:mcp && npm run build:ui && node scripts/dev-server.js",
34
34
  "doctor:split": "node scripts/doctor-split.js",
35
35
  "dev:isolated": "sh -c 'npm run kill:dev-ports && trap \"kill 0\" INT TERM EXIT; npm run dev:links & npm run dev:room:isolated & npm run dev:cloud & VITE_API_PORT=4700 npm run dev:ui & wait'",
36
- "dev:cloud": "sh -c 'npm run kill:ports -- 3710 && cd ../cloud && PORT=3710 CLOUD_PUBLIC_URL=http://127.0.0.1:3710 CLOUD_ALLOWED_ORIGINS='\"'\"'http://127.0.0.1:3710,http://localhost:3710,http://localhost:5173,http://127.0.0.1:5173,https://quoroom.ai,https://www.quoroom.ai,https://app.quoroom.ai'\"'\"' npm start'",
36
+ "dev:cloud": "sh -c 'npm run kill:ports -- 3715 && cd ../cloud && PORT=3715 CLOUD_PUBLIC_URL=http://127.0.0.1:3715 CLOUD_ALLOWED_ORIGINS='\"'\"'http://127.0.0.1:3715,http://localhost:3715,http://localhost:5173,http://127.0.0.1:5173,https://quoroom.ai,https://www.quoroom.ai,https://app.quoroom.ai'\"'\"' npm start'",
37
37
  "dev:ui": "vite --config src/ui/vite.config.ts",
38
38
  "seed:style-demo": "sh -c 'export QUOROOM_DATA_DIR=$HOME/.quoroom-dev; node scripts/seed-style-demo.js'",
39
39
  "typecheck": "tsc --noEmit",