quoroom 0.1.13 → 0.1.15

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/out/mcp/cli.js CHANGED
@@ -16662,7 +16662,7 @@ var require_schemes = __commonJS({
16662
16662
  urnComponent.nss = (uuidComponent.uuid || "").toLowerCase();
16663
16663
  return urnComponent;
16664
16664
  }
16665
- var http4 = (
16665
+ var http3 = (
16666
16666
  /** @type {SchemeHandler} */
16667
16667
  {
16668
16668
  scheme: "http",
@@ -16675,7 +16675,7 @@ var require_schemes = __commonJS({
16675
16675
  /** @type {SchemeHandler} */
16676
16676
  {
16677
16677
  scheme: "https",
16678
- domainHost: http4.domainHost,
16678
+ domainHost: http3.domainHost,
16679
16679
  parse: httpParse,
16680
16680
  serialize: httpSerialize
16681
16681
  }
@@ -16719,7 +16719,7 @@ var require_schemes = __commonJS({
16719
16719
  var SCHEMES = (
16720
16720
  /** @type {Record<SchemeName, SchemeHandler>} */
16721
16721
  {
16722
- http: http4,
16722
+ http: http3,
16723
16723
  https: https3,
16724
16724
  ws,
16725
16725
  wss,
@@ -21620,12 +21620,14 @@ CREATE TABLE IF NOT EXISTS rooms (
21620
21620
  visibility TEXT NOT NULL DEFAULT 'private',
21621
21621
  autonomy_mode TEXT NOT NULL DEFAULT 'auto',
21622
21622
  max_concurrent_tasks INTEGER NOT NULL DEFAULT 3,
21623
- worker_model TEXT NOT NULL DEFAULT 'ollama:llama3.2',
21623
+ worker_model TEXT NOT NULL DEFAULT 'claude',
21624
21624
  queen_cycle_gap_ms INTEGER NOT NULL DEFAULT 1800000,
21625
21625
  queen_max_turns INTEGER NOT NULL DEFAULT 3,
21626
21626
  queen_quiet_from TEXT,
21627
21627
  queen_quiet_until TEXT,
21628
21628
  config TEXT,
21629
+ webhook_token TEXT,
21630
+ queen_nickname TEXT,
21629
21631
  chat_session_id TEXT,
21630
21632
  referred_by_code TEXT,
21631
21633
  created_at DATETIME DEFAULT (datetime('now','localtime')),
@@ -21706,6 +21708,7 @@ CREATE TABLE IF NOT EXISTS tasks (
21706
21708
  cron_expression TEXT,
21707
21709
  trigger_type TEXT NOT NULL DEFAULT 'cron',
21708
21710
  trigger_config TEXT,
21711
+ webhook_token TEXT,
21709
21712
  executor TEXT NOT NULL DEFAULT 'claude_code',
21710
21713
  status TEXT NOT NULL DEFAULT 'active',
21711
21714
  last_run DATETIME,
@@ -22014,6 +22017,18 @@ CREATE TABLE IF NOT EXISTS cycle_logs (
22014
22017
  );
22015
22018
  CREATE INDEX IF NOT EXISTS idx_cycle_logs_seq ON cycle_logs(cycle_id, seq);
22016
22019
 
22020
+ -- Agent session continuity (persists conversation history across queen cycles for all model types)
22021
+ -- session_id: for CLI models (claude/codex) \u2014 passed as --resume to continue the native session
22022
+ -- messages_json: for API models \u2014 full conversation turns array (no system prompt), stored as JSON
22023
+ CREATE TABLE IF NOT EXISTS agent_sessions (
22024
+ worker_id INTEGER PRIMARY KEY REFERENCES workers(id) ON DELETE CASCADE,
22025
+ session_id TEXT,
22026
+ messages_json TEXT,
22027
+ model TEXT NOT NULL DEFAULT '',
22028
+ turn_count INTEGER NOT NULL DEFAULT 0,
22029
+ updated_at DATETIME DEFAULT (datetime('now','localtime'))
22030
+ );
22031
+
22017
22032
  -- Schema version tracking
22018
22033
  CREATE TABLE IF NOT EXISTS schema_version (
22019
22034
  version INTEGER PRIMARY KEY,
@@ -22024,29 +22039,8 @@ INSERT OR IGNORE INTO schema_version (version) VALUES (1);
22024
22039
  }
22025
22040
  });
22026
22041
 
22027
- // src/shared/db-migrations.ts
22028
- function runMigrations(database, log = console.log) {
22029
- database.exec(SCHEMA);
22030
- if (!database.prepare("SELECT value FROM settings WHERE key = ?").get("keeper_referral_code")) {
22031
- const code = (0, import_crypto.randomBytes)(6).toString("base64url").slice(0, 10);
22032
- database.prepare(
22033
- `INSERT INTO settings (key, value, updated_at) VALUES (?, ?, datetime('now','localtime'))
22034
- ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at`
22035
- ).run("keeper_referral_code", code);
22036
- }
22037
- log("Database schema initialized");
22038
- }
22039
- var import_crypto;
22040
- var init_db_migrations = __esm({
22041
- "src/shared/db-migrations.ts"() {
22042
- "use strict";
22043
- import_crypto = require("crypto");
22044
- init_schema();
22045
- }
22046
- });
22047
-
22048
22042
  // src/shared/constants.ts
22049
- var APP_NAME, DEFAULTS, TASK_STATUSES, BASE_CHAIN_CONFIG, BASE_SEPOLIA_CONFIG, CHAIN_CONFIGS, SUPPORTED_CHAINS, SUPPORTED_TOKENS, ERC8004_IDENTITY_REGISTRY, QUEEN_DEFAULTS_BY_PLAN, CHATGPT_DEFAULTS_BY_PLAN, DEFAULT_ROOM_CONFIG;
22043
+ var APP_NAME, DEFAULTS, TRIGGER_TYPES, TASK_STATUSES, BASE_CHAIN_CONFIG, BASE_SEPOLIA_CONFIG, CHAIN_CONFIGS, SUPPORTED_CHAINS, SUPPORTED_TOKENS, ERC8004_IDENTITY_REGISTRY, QUEEN_DEFAULTS_BY_PLAN, CHATGPT_DEFAULTS_BY_PLAN, DEFAULT_ROOM_CONFIG;
22050
22044
  var init_constants = __esm({
22051
22045
  "src/shared/constants.ts"() {
22052
22046
  "use strict";
@@ -22062,6 +22056,12 @@ var init_constants = __esm({
22062
22056
  WINDOW_HEIGHT_LARGE: 1200,
22063
22057
  PROGRESS_THROTTLE_MS: 2e3
22064
22058
  };
22059
+ TRIGGER_TYPES = {
22060
+ CRON: "cron",
22061
+ ONCE: "once",
22062
+ MANUAL: "manual",
22063
+ WEBHOOK: "webhook"
22064
+ };
22065
22065
  TASK_STATUSES = {
22066
22066
  ACTIVE: "active",
22067
22067
  PAUSED: "paused",
@@ -22397,8 +22397,8 @@ function mapWorkerRow(row) {
22397
22397
  }
22398
22398
  function createTask(db3, input) {
22399
22399
  const result = db3.prepare(
22400
- `INSERT INTO tasks (name, description, prompt, cron_expression, trigger_type, trigger_config, scheduled_at, executor, max_runs, worker_id, session_continuity, timeout_minutes, max_turns, allowed_tools, disallowed_tools, room_id)
22401
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
22400
+ `INSERT INTO tasks (name, description, prompt, cron_expression, trigger_type, trigger_config, webhook_token, scheduled_at, executor, max_runs, worker_id, session_continuity, timeout_minutes, max_turns, allowed_tools, disallowed_tools, room_id)
22401
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
22402
22402
  ).run(
22403
22403
  input.name,
22404
22404
  input.description ?? null,
@@ -22406,6 +22406,7 @@ function createTask(db3, input) {
22406
22406
  input.cronExpression ?? null,
22407
22407
  input.triggerType ?? "cron",
22408
22408
  input.triggerConfig ?? null,
22409
+ input.webhookToken ?? null,
22409
22410
  input.scheduledAt ?? null,
22410
22411
  input.executor ?? "claude_code",
22411
22412
  input.maxRuns ?? null,
@@ -22425,6 +22426,10 @@ function getTask(db3, id) {
22425
22426
  const row = db3.prepare("SELECT * FROM tasks WHERE id = ?").get(id);
22426
22427
  return row ? mapTaskRow(row) : null;
22427
22428
  }
22429
+ function getTaskByWebhookToken(db3, token) {
22430
+ const row = db3.prepare("SELECT * FROM tasks WHERE webhook_token = ?").get(token);
22431
+ return row ? mapTaskRow(row) : null;
22432
+ }
22428
22433
  function listTasks(db3, roomId, status) {
22429
22434
  if (roomId != null && status) {
22430
22435
  const rows2 = db3.prepare("SELECT * FROM tasks WHERE room_id = ? AND status = ? ORDER BY created_at DESC").all(roomId, status);
@@ -22445,6 +22450,7 @@ function updateTask(db3, id, updates) {
22445
22450
  cronExpression: "cron_expression",
22446
22451
  triggerType: "trigger_type",
22447
22452
  triggerConfig: "trigger_config",
22453
+ webhookToken: "webhook_token",
22448
22454
  scheduledAt: "scheduled_at",
22449
22455
  executor: "executor",
22450
22456
  status: "status",
@@ -22801,6 +22807,7 @@ function mapTaskRow(row) {
22801
22807
  cronExpression: row.cron_expression,
22802
22808
  triggerType: row.trigger_type,
22803
22809
  triggerConfig: row.trigger_config,
22810
+ webhookToken: row.webhook_token,
22804
22811
  scheduledAt: row.scheduled_at,
22805
22812
  executor: row.executor,
22806
22813
  status: row.status,
@@ -22926,21 +22933,34 @@ function mapRoomRow(row) {
22926
22933
  queenQuietFrom: row.queen_quiet_from ?? null,
22927
22934
  queenQuietUntil: row.queen_quiet_until ?? null,
22928
22935
  config: config2,
22936
+ queenNickname: row.queen_nickname ?? null,
22929
22937
  chatSessionId: row.chat_session_id ?? null,
22930
22938
  referredByCode: row.referred_by_code ?? null,
22939
+ webhookToken: row.webhook_token ?? null,
22931
22940
  createdAt: row.created_at,
22932
22941
  updatedAt: row.updated_at
22933
22942
  };
22934
22943
  }
22935
- function createRoom(db3, name, goal, config2, referredByCode) {
22944
+ function createRoom(db3, name, goal, config2, referredByCode, queenNickname) {
22936
22945
  const configJson = config2 ? JSON.stringify({ ...DEFAULT_ROOM_CONFIG, ...config2 }) : JSON.stringify(DEFAULT_ROOM_CONFIG);
22937
- const result = db3.prepare("INSERT INTO rooms (name, goal, config, referred_by_code) VALUES (?, ?, ?, ?)").run(name, goal ?? null, configJson, referredByCode ?? null);
22946
+ const nickname = queenNickname ?? pickQueenNickname(db3);
22947
+ const result = db3.prepare("INSERT INTO rooms (name, goal, config, referred_by_code, queen_nickname) VALUES (?, ?, ?, ?, ?)").run(name, goal ?? null, configJson, referredByCode ?? null, nickname);
22938
22948
  return getRoom(db3, result.lastInsertRowid);
22939
22949
  }
22950
+ function pickQueenNickname(db3) {
22951
+ const usedNames = db3.prepare(`SELECT queen_nickname FROM rooms WHERE queen_nickname IS NOT NULL AND queen_nickname != ''`).all().map((r) => r.queen_nickname.toLowerCase());
22952
+ const available = QUEEN_WOMAN_NAMES.filter((n) => !usedNames.includes(n.toLowerCase()));
22953
+ const pool = available.length > 0 ? available : QUEEN_WOMAN_NAMES;
22954
+ return pool[Math.floor(Math.random() * pool.length)];
22955
+ }
22940
22956
  function getRoom(db3, id) {
22941
22957
  const row = db3.prepare("SELECT * FROM rooms WHERE id = ?").get(id);
22942
22958
  return row ? mapRoomRow(row) : null;
22943
22959
  }
22960
+ function getRoomByWebhookToken(db3, token) {
22961
+ const row = db3.prepare("SELECT * FROM rooms WHERE webhook_token = ?").get(token);
22962
+ return row ? mapRoomRow(row) : null;
22963
+ }
22944
22964
  function listRooms(db3, status) {
22945
22965
  if (status) {
22946
22966
  const rows2 = db3.prepare("SELECT * FROM rooms WHERE status = ? ORDER BY created_at DESC").all(status);
@@ -22964,7 +22984,9 @@ function updateRoom(db3, id, updates) {
22964
22984
  queenQuietFrom: "queen_quiet_from",
22965
22985
  queenQuietUntil: "queen_quiet_until",
22966
22986
  config: "config",
22967
- referredByCode: "referred_by_code"
22987
+ referredByCode: "referred_by_code",
22988
+ queenNickname: "queen_nickname",
22989
+ webhookToken: "webhook_token"
22968
22990
  };
22969
22991
  const fields = [];
22970
22992
  const values = [];
@@ -23715,7 +23737,52 @@ function pruneOldCycles(db3) {
23715
23737
  deleteAll();
23716
23738
  return ids.length;
23717
23739
  }
23718
- var DEFAULT_TIMEOUT_MINUTES, MAX_RUNS_PER_TASK, PRUNE_INTERVAL_MS, lastPruneTime, MAX_OWN_OBSERVATIONS, MAX_RELATED_OBSERVATIONS, MAX_MEMORY_LENGTH, MAX_OBSERVATIONS_PER_ENTITY, MAX_CYCLES_PER_WORKER, CYCLE_PRUNE_INTERVAL_MS, lastCyclePruneTime;
23740
+ function listRecentDecisions(db3, roomId, limit = 5) {
23741
+ const safeLimit = clampLimit(limit, 5, 50);
23742
+ const rows = db3.prepare(
23743
+ `SELECT * FROM quorum_decisions WHERE room_id = ? AND status != 'voting' ORDER BY created_at DESC LIMIT ?`
23744
+ ).all(roomId, safeLimit);
23745
+ return rows.map(mapDecisionRow);
23746
+ }
23747
+ function getAgentSession(db3, workerId) {
23748
+ const row = db3.prepare(
23749
+ "SELECT session_id, messages_json, model, turn_count, updated_at FROM agent_sessions WHERE worker_id = ?"
23750
+ ).get(workerId);
23751
+ if (!row) return void 0;
23752
+ return {
23753
+ sessionId: row.session_id,
23754
+ messagesJson: row.messages_json,
23755
+ model: row.model,
23756
+ turnCount: row.turn_count,
23757
+ updatedAt: row.updated_at
23758
+ };
23759
+ }
23760
+ function saveAgentSession(db3, workerId, opts) {
23761
+ db3.prepare(
23762
+ `INSERT INTO agent_sessions (worker_id, session_id, messages_json, model, turn_count, updated_at)
23763
+ VALUES (?, ?, ?, ?, 1, datetime('now','localtime'))
23764
+ ON CONFLICT(worker_id) DO UPDATE SET
23765
+ session_id = CASE WHEN ? IS NOT NULL THEN ? ELSE session_id END,
23766
+ messages_json = CASE WHEN ? IS NOT NULL THEN ? ELSE messages_json END,
23767
+ model = ?,
23768
+ turn_count = turn_count + 1,
23769
+ updated_at = datetime('now','localtime')`
23770
+ ).run(
23771
+ workerId,
23772
+ opts.sessionId ?? null,
23773
+ opts.messagesJson ?? null,
23774
+ opts.model,
23775
+ opts.sessionId ?? null,
23776
+ opts.sessionId ?? null,
23777
+ opts.messagesJson ?? null,
23778
+ opts.messagesJson ?? null,
23779
+ opts.model
23780
+ );
23781
+ }
23782
+ function deleteAgentSession(db3, workerId) {
23783
+ db3.prepare("DELETE FROM agent_sessions WHERE worker_id = ?").run(workerId);
23784
+ }
23785
+ var DEFAULT_TIMEOUT_MINUTES, MAX_RUNS_PER_TASK, PRUNE_INTERVAL_MS, lastPruneTime, MAX_OWN_OBSERVATIONS, MAX_RELATED_OBSERVATIONS, MAX_MEMORY_LENGTH, MAX_OBSERVATIONS_PER_ENTITY, QUEEN_WOMAN_NAMES, MAX_CYCLES_PER_WORKER, CYCLE_PRUNE_INTERVAL_MS, lastCyclePruneTime;
23719
23786
  var init_db_queries = __esm({
23720
23787
  "src/shared/db-queries.ts"() {
23721
23788
  "use strict";
@@ -23729,12 +23796,135 @@ var init_db_queries = __esm({
23729
23796
  MAX_RELATED_OBSERVATIONS = 3;
23730
23797
  MAX_MEMORY_LENGTH = 2e3;
23731
23798
  MAX_OBSERVATIONS_PER_ENTITY = 10;
23799
+ QUEEN_WOMAN_NAMES = [
23800
+ "Alice",
23801
+ "Anna",
23802
+ "Belle",
23803
+ "Cara",
23804
+ "Dana",
23805
+ "Elena",
23806
+ "Fiona",
23807
+ "Grace",
23808
+ "Hana",
23809
+ "Iris",
23810
+ "Julia",
23811
+ "Kate",
23812
+ "Lena",
23813
+ "Luna",
23814
+ "Mara",
23815
+ "Maya",
23816
+ "Nina",
23817
+ "Nora",
23818
+ "Olga",
23819
+ "Petra",
23820
+ "Rose",
23821
+ "Sara",
23822
+ "Sofia",
23823
+ "Tara",
23824
+ "Uma",
23825
+ "Vera",
23826
+ "Wren",
23827
+ "Zara",
23828
+ "Zoe",
23829
+ "Ava",
23830
+ "Cleo",
23831
+ "Dara",
23832
+ "Emmy",
23833
+ "Gaia",
23834
+ "Hera",
23835
+ "Ines",
23836
+ "Jada",
23837
+ "Kara",
23838
+ "Lila",
23839
+ "Mina"
23840
+ ];
23732
23841
  MAX_CYCLES_PER_WORKER = 50;
23733
23842
  CYCLE_PRUNE_INTERVAL_MS = 5 * 60 * 1e3;
23734
23843
  lastCyclePruneTime = 0;
23735
23844
  }
23736
23845
  });
23737
23846
 
23847
+ // src/shared/db-migrations.ts
23848
+ function upsertSetting(database, key, value) {
23849
+ database.prepare(
23850
+ `INSERT INTO settings (key, value, updated_at) VALUES (?, ?, datetime('now','localtime'))
23851
+ ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at`
23852
+ ).run(key, value);
23853
+ }
23854
+ function runMigrations(database, log = console.log) {
23855
+ database.exec(SCHEMA);
23856
+ if (!database.prepare("SELECT value FROM settings WHERE key = ?").get("keeper_referral_code")) {
23857
+ const code = (0, import_crypto.randomBytes)(6).toString("base64url").slice(0, 10);
23858
+ upsertSetting(database, "keeper_referral_code", code);
23859
+ }
23860
+ if (!database.prepare("SELECT value FROM settings WHERE key = ?").get("keeper_user_number")) {
23861
+ const num2 = String(1e4 + Math.floor(Math.random() * 9e4));
23862
+ upsertSetting(database, "keeper_user_number", num2);
23863
+ log(`Migrated: assigned keeper_user_number=${num2}`);
23864
+ }
23865
+ const hasQueenNickname = database.prepare(
23866
+ `SELECT name FROM pragma_table_info('rooms') WHERE name='queen_nickname'`
23867
+ ).get()?.name;
23868
+ if (!hasQueenNickname) {
23869
+ database.exec(`ALTER TABLE rooms ADD COLUMN queen_nickname TEXT`);
23870
+ log("Migrated: added queen_nickname column to rooms");
23871
+ }
23872
+ const roomsWithoutNickname = database.prepare(`SELECT id FROM rooms WHERE queen_nickname IS NULL OR queen_nickname = ''`).all();
23873
+ if (roomsWithoutNickname.length > 0) {
23874
+ for (const room of roomsWithoutNickname) {
23875
+ const nickname = pickQueenNickname(database);
23876
+ database.prepare(`UPDATE rooms SET queen_nickname = ? WHERE id = ?`).run(nickname, room.id);
23877
+ }
23878
+ log(`Migrated: assigned queen nicknames to ${roomsWithoutNickname.length} room(s)`);
23879
+ }
23880
+ const hasTaskWebhookToken = database.prepare(
23881
+ `SELECT name FROM pragma_table_info('tasks') WHERE name='webhook_token'`
23882
+ ).get()?.name;
23883
+ if (!hasTaskWebhookToken) {
23884
+ database.exec(`ALTER TABLE tasks ADD COLUMN webhook_token TEXT`);
23885
+ log("Migrated: added webhook_token column to tasks");
23886
+ }
23887
+ const hasTaskWebhookIndex = database.prepare(
23888
+ `SELECT name FROM sqlite_master WHERE type='index' AND name='idx_tasks_webhook_token'`
23889
+ ).get()?.name;
23890
+ if (!hasTaskWebhookIndex) {
23891
+ database.exec(`CREATE UNIQUE INDEX IF NOT EXISTS idx_tasks_webhook_token ON tasks(webhook_token) WHERE webhook_token IS NOT NULL`);
23892
+ }
23893
+ const hasRoomWebhookToken = database.prepare(
23894
+ `SELECT name FROM pragma_table_info('rooms') WHERE name='webhook_token'`
23895
+ ).get()?.name;
23896
+ if (!hasRoomWebhookToken) {
23897
+ database.exec(`ALTER TABLE rooms ADD COLUMN webhook_token TEXT`);
23898
+ log("Migrated: added webhook_token column to rooms");
23899
+ }
23900
+ const hasRoomWebhookIndex = database.prepare(
23901
+ `SELECT name FROM sqlite_master WHERE type='index' AND name='idx_rooms_webhook_token'`
23902
+ ).get()?.name;
23903
+ if (!hasRoomWebhookIndex) {
23904
+ database.exec(`CREATE UNIQUE INDEX IF NOT EXISTS idx_rooms_webhook_token ON rooms(webhook_token) WHERE webhook_token IS NOT NULL`);
23905
+ }
23906
+ const ollamaWorkers = database.prepare(`SELECT id FROM workers WHERE model LIKE 'ollama:%'`).all();
23907
+ if (ollamaWorkers.length > 0) {
23908
+ database.prepare(`UPDATE workers SET model = 'claude' WHERE model LIKE 'ollama:%'`).run();
23909
+ log(`Migrated: reset ${ollamaWorkers.length} ollama worker model(s) to 'claude'`);
23910
+ }
23911
+ const ollamaRooms = database.prepare(`SELECT id FROM rooms WHERE worker_model LIKE 'ollama:%'`).all();
23912
+ if (ollamaRooms.length > 0) {
23913
+ database.prepare(`UPDATE rooms SET worker_model = 'claude' WHERE worker_model LIKE 'ollama:%'`).run();
23914
+ log(`Migrated: reset ${ollamaRooms.length} room worker_model(s) to 'claude'`);
23915
+ }
23916
+ log("Database schema initialized");
23917
+ }
23918
+ var import_crypto;
23919
+ var init_db_migrations = __esm({
23920
+ "src/shared/db-migrations.ts"() {
23921
+ "use strict";
23922
+ import_crypto = require("crypto");
23923
+ init_schema();
23924
+ init_db_queries();
23925
+ }
23926
+ });
23927
+
23738
23928
  // src/shared/embeddings.ts
23739
23929
  function loadSqliteVec(db3) {
23740
23930
  if (sqliteVecLoaded) return true;
@@ -25209,12 +25399,40 @@ var init_telemetry = __esm({
25209
25399
  });
25210
25400
 
25211
25401
  // src/shared/cloud-sync.ts
25402
+ var cloud_sync_exports = {};
25403
+ __export(cloud_sync_exports, {
25404
+ cancelCloudStation: () => cancelCloudStation,
25405
+ createCloudInvite: () => createCloudInvite,
25406
+ cryptoCheckoutStation: () => cryptoCheckoutStation,
25407
+ cryptoRenewStation: () => cryptoRenewStation,
25408
+ deleteCloudStation: () => deleteCloudStation,
25409
+ ensureCloudRoomToken: () => ensureCloudRoomToken,
25410
+ execOnCloudStation: () => execOnCloudStation,
25411
+ fetchCloudRoomMessages: () => fetchCloudRoomMessages,
25412
+ fetchPublicRooms: () => fetchPublicRooms,
25413
+ fetchReferredRooms: () => fetchReferredRooms,
25414
+ fetchRoomFeed: () => fetchRoomFeed,
25415
+ getCloudCryptoPrices: () => getCloudCryptoPrices,
25416
+ getCloudOnrampUrl: () => getCloudOnrampUrl,
25417
+ getCloudStationLogs: () => getCloudStationLogs,
25418
+ getRoomCloudId: () => getRoomCloudId,
25419
+ getRoomId: () => getRoomId,
25420
+ getStoredCloudRoomToken: () => getStoredCloudRoomToken,
25421
+ listCloudInvites: () => listCloudInvites,
25422
+ listCloudStationPayments: () => listCloudStationPayments,
25423
+ listCloudStations: () => listCloudStations,
25424
+ pushActivityToCloud: () => pushActivityToCloud,
25425
+ registerWithCloud: () => registerWithCloud,
25426
+ sendCloudHeartbeat: () => sendCloudHeartbeat,
25427
+ sendCloudRoomMessage: () => sendCloudRoomMessage,
25428
+ startCloudStation: () => startCloudStation,
25429
+ startCloudSync: () => startCloudSync,
25430
+ stopCloudStation: () => stopCloudStation,
25431
+ stopCloudSync: () => stopCloudSync
25432
+ });
25212
25433
  function getCloudApi() {
25213
25434
  return (process.env.QUOROOM_CLOUD_API ?? "https://quoroom.ai/api").replace(/\/$/, "");
25214
25435
  }
25215
- function getCloudMasterToken() {
25216
- return (process.env.QUOROOM_CLOUD_API_KEY ?? "").trim();
25217
- }
25218
25436
  function getCloudTokenFilePath() {
25219
25437
  const explicitDataDir = process.env.QUOROOM_DATA_DIR?.trim();
25220
25438
  if (explicitDataDir) return (0, import_path2.join)(explicitDataDir, TOKEN_FILE_NAME);
@@ -25242,6 +25460,10 @@ function saveTokenStore() {
25242
25460
  function getRoomToken(roomId) {
25243
25461
  return loadTokenStore()[roomId];
25244
25462
  }
25463
+ function getStoredCloudRoomToken(roomId) {
25464
+ const token = getRoomToken(roomId);
25465
+ return typeof token === "string" && token.trim() ? token : null;
25466
+ }
25245
25467
  function setRoomToken(roomId, token) {
25246
25468
  loadTokenStore()[roomId] = token;
25247
25469
  saveTokenStore();
@@ -25254,9 +25476,8 @@ function clearRoomToken(roomId) {
25254
25476
  }
25255
25477
  function cloudHeaders(roomId, extra = {}) {
25256
25478
  const roomToken = roomId ? getRoomToken(roomId) : void 0;
25257
- const token = roomToken || getCloudMasterToken();
25258
- if (!token) return extra;
25259
- return { ...extra, "X-Room-Token": token };
25479
+ if (!roomToken) return extra;
25480
+ return { ...extra, "X-Room-Token": roomToken };
25260
25481
  }
25261
25482
  async function ensureCloudRoomToken(data) {
25262
25483
  if (getRoomToken(data.roomId)) return true;
@@ -25358,6 +25579,9 @@ function stopCloudSync() {
25358
25579
  heartbeatInterval = null;
25359
25580
  }
25360
25581
  }
25582
+ function getRoomId() {
25583
+ return getMachineId();
25584
+ }
25361
25585
  function getRoomCloudId(dbRoomId) {
25362
25586
  const machineId = getMachineId();
25363
25587
  return (0, import_crypto6.createHash)("sha256").update(`${machineId}:${dbRoomId}`).digest("hex").slice(0, 32);
@@ -25612,6 +25836,18 @@ async function fetchPublicRooms() {
25612
25836
  return [];
25613
25837
  }
25614
25838
  }
25839
+ async function fetchRoomFeed(roomId) {
25840
+ try {
25841
+ const res = await fetch(`${getCloudApi()}/rooms/public/${encodeURIComponent(roomId)}/feed`, {
25842
+ signal: AbortSignal.timeout(1e4)
25843
+ });
25844
+ if (!res.ok) return [];
25845
+ const data = await res.json();
25846
+ return data.feed ?? [];
25847
+ } catch {
25848
+ return [];
25849
+ }
25850
+ }
25615
25851
  async function createCloudInvite(cloudRoomId, options) {
25616
25852
  try {
25617
25853
  const res = await fetch(`${getCloudApi()}/rooms/${encodeURIComponent(cloudRoomId)}/invites`, {
@@ -25672,195 +25908,11 @@ var init_cloud_sync = __esm({
25672
25908
  }
25673
25909
  });
25674
25910
 
25675
- // src/shared/ollama-ensure.ts
25676
- function ollamaRequest(path3, body, timeoutMs = 3e5) {
25677
- return new Promise((resolve2, reject) => {
25678
- const options = {
25679
- hostname: "127.0.0.1",
25680
- port: 11434,
25681
- path: path3,
25682
- method: body ? "POST" : "GET",
25683
- headers: body ? { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(body) } : void 0,
25684
- timeout: timeoutMs
25685
- };
25686
- const req = import_node_http.default.request(options, (res) => {
25687
- let data = "";
25688
- res.on("data", (chunk) => {
25689
- data += chunk;
25690
- });
25691
- res.on("end", () => {
25692
- if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) {
25693
- resolve2(data);
25694
- } else {
25695
- reject(new Error(`Ollama HTTP ${res.statusCode}: ${data}`));
25696
- }
25697
- });
25698
- });
25699
- req.on("error", reject);
25700
- req.on("timeout", () => {
25701
- req.destroy();
25702
- reject(new Error("Ollama request timeout"));
25703
- });
25704
- if (body) req.write(body);
25705
- req.end();
25706
- });
25707
- }
25708
- async function isOllamaAvailable() {
25709
- try {
25710
- await ollamaRequest("/api/tags", void 0, 5e3);
25711
- return true;
25712
- } catch {
25713
- return false;
25714
- }
25715
- }
25716
- async function listOllamaModels() {
25717
- try {
25718
- const response = await ollamaRequest("/api/tags", void 0, 5e3);
25719
- const parsed = JSON.parse(response);
25720
- return (parsed.models ?? []).map((m) => ({ name: m.name, size: m.size }));
25721
- } catch {
25722
- return [];
25723
- }
25724
- }
25725
- function hasOllamaBinary() {
25726
- try {
25727
- (0, import_node_child_process.execSync)("which ollama 2>/dev/null", { timeout: 3e3 });
25728
- return true;
25729
- } catch {
25730
- return false;
25731
- }
25732
- }
25733
- function installOllamaBinary() {
25734
- try {
25735
- if (process.platform === "darwin") {
25736
- (0, import_node_child_process.execSync)("brew install ollama 2>&1", { timeout: OLLAMA_INSTALL_TIMEOUT_MS });
25737
- } else {
25738
- (0, import_node_child_process.execSync)("curl -fsSL https://ollama.com/install.sh | sh 2>&1", { timeout: OLLAMA_INSTALL_TIMEOUT_MS });
25739
- }
25740
- return true;
25741
- } catch {
25742
- return false;
25743
- }
25744
- }
25745
- function startOllamaServe() {
25746
- try {
25747
- const child = (0, import_node_child_process.spawn)("ollama", ["serve"], { detached: true, stdio: "ignore" });
25748
- child.on("error", () => {
25749
- });
25750
- child.unref();
25751
- return true;
25752
- } catch {
25753
- return false;
25754
- }
25755
- }
25756
- async function waitForOllamaAvailable(timeoutMs = OLLAMA_STARTUP_TIMEOUT_MS) {
25757
- const startedAt2 = Date.now();
25758
- while (Date.now() - startedAt2 < timeoutMs) {
25759
- if (await isOllamaAvailable()) return true;
25760
- await new Promise((resolve2) => setTimeout(resolve2, 1e3));
25761
- }
25762
- return false;
25763
- }
25764
- async function ensureOllamaRunning() {
25765
- const already = await isOllamaAvailable();
25766
- if (already) return { available: true, status: "running" };
25767
- if (!hasOllamaBinary() && !installOllamaBinary()) {
25768
- return { available: false, status: "install_failed" };
25769
- }
25770
- if (!startOllamaServe()) {
25771
- return { available: false, status: "start_failed" };
25772
- }
25773
- const available = await waitForOllamaAvailable();
25774
- return { available, status: available ? "running" : "start_failed" };
25775
- }
25776
- function isModelInstalled(models, requested) {
25777
- const requestedLower = requested.toLowerCase();
25778
- for (const model of models) {
25779
- const installedName = model.name.toLowerCase();
25780
- if (installedName === requestedLower) return true;
25781
- if (!requestedLower.includes(":") && installedName === `${requestedLower}:latest`) return true;
25782
- if (requestedLower.endsWith(":latest") && installedName === requestedLower.slice(0, -7)) return true;
25783
- }
25784
- return false;
25785
- }
25786
- async function pullOllamaModel(model) {
25787
- return await new Promise((resolve2) => {
25788
- let stdout = "";
25789
- let stderr = "";
25790
- let settled = false;
25791
- let proc;
25792
- try {
25793
- proc = (0, import_node_child_process.spawn)("ollama", ["pull", model], {
25794
- stdio: ["ignore", "pipe", "pipe"]
25795
- });
25796
- } catch (err) {
25797
- const message = err instanceof Error ? err.message : String(err);
25798
- resolve2({ ok: false, error: `Failed to start ollama pull: ${message}` });
25799
- return;
25800
- }
25801
- const finish = (result) => {
25802
- if (settled) return;
25803
- settled = true;
25804
- resolve2(result);
25805
- };
25806
- const timer = setTimeout(() => {
25807
- proc.kill("SIGTERM");
25808
- finish({ ok: false, error: `Timed out while pulling model ${model}` });
25809
- }, 15 * 60 * 1e3);
25810
- proc.stdout?.on("data", (chunk) => {
25811
- stdout += String(chunk);
25812
- });
25813
- proc.stderr?.on("data", (chunk) => {
25814
- stderr += String(chunk);
25815
- });
25816
- proc.on("error", (err) => {
25817
- clearTimeout(timer);
25818
- const message = err instanceof Error ? err.message : String(err);
25819
- finish({ ok: false, error: `ollama pull failed: ${message}` });
25820
- });
25821
- proc.on("close", (code) => {
25822
- clearTimeout(timer);
25823
- if (code === 0) {
25824
- finish({ ok: true });
25825
- return;
25826
- }
25827
- const details = `${stderr}
25828
- ${stdout}`.trim().split("\n").slice(-3).join("\n");
25829
- finish({ ok: false, error: details || `ollama pull exited with code ${code ?? -1}` });
25830
- });
25831
- });
25832
- }
25833
- async function ensureOllamaModel(modelName) {
25834
- const running = await ensureOllamaRunning();
25835
- if (!running.available) {
25836
- throw new Error(`Ollama unavailable (${running.status})`);
25837
- }
25838
- const installed = await listOllamaModels();
25839
- if (isModelInstalled(installed, modelName)) return;
25840
- const pulled = await pullOllamaModel(modelName);
25841
- if (!pulled.ok) {
25842
- throw new Error(`Failed to pull model ${modelName}: ${pulled.ok === false ? pulled.error : "unknown"}`);
25843
- }
25844
- }
25845
- var import_node_http, import_node_child_process, OLLAMA_INSTALL_TIMEOUT_MS, OLLAMA_STARTUP_TIMEOUT_MS;
25846
- var init_ollama_ensure = __esm({
25847
- "src/shared/ollama-ensure.ts"() {
25848
- "use strict";
25849
- import_node_http = __toESM(require("node:http"));
25850
- import_node_child_process = require("node:child_process");
25851
- OLLAMA_INSTALL_TIMEOUT_MS = 12e4;
25852
- OLLAMA_STARTUP_TIMEOUT_MS = 3e4;
25853
- }
25854
- });
25855
-
25856
25911
  // src/shared/agent-executor.ts
25857
25912
  async function executeAgent(options) {
25858
25913
  const model = options.model.trim();
25859
25914
  if (model.startsWith("ollama:")) {
25860
- if (options.toolDefs && options.toolDefs.length > 0 && options.onToolCall) {
25861
- return executeOllamaWithTools(options);
25862
- }
25863
- return executeOllama(options);
25915
+ throw new Error(`Ollama models are no longer supported. Update your room model to 'claude', 'codex', 'anthropic:*', or 'openai:*'.`);
25864
25916
  }
25865
25917
  if (model === "codex" || model.startsWith("codex:")) {
25866
25918
  return executeCodex(options);
@@ -26004,106 +26056,25 @@ async function executeCodex(options) {
26004
26056
  });
26005
26057
  });
26006
26058
  }
26007
- async function executeOllamaWithTools(options) {
26008
- const modelName = options.model.replace(/^ollama:/, "");
26009
- const startTime = Date.now();
26010
- try {
26011
- await ensureOllamaModel(modelName);
26012
- } catch (err) {
26013
- return {
26014
- output: `Error: ${err instanceof Error ? err.message : String(err)}`,
26015
- exitCode: 1,
26016
- durationMs: Date.now() - startTime,
26017
- sessionId: null,
26018
- timedOut: false
26019
- };
26020
- }
26021
- const messages = [];
26022
- if (options.systemPrompt) {
26023
- messages.push({ role: "system", content: options.systemPrompt });
26024
- }
26025
- messages.push({ role: "user", content: options.prompt });
26026
- const timeoutMs = options.timeoutMs ?? 5 * 60 * 1e3;
26027
- const maxTurns = options.maxTurns ?? 10;
26028
- let finalOutput = "";
26029
- for (let turn = 0; turn < maxTurns; turn++) {
26030
- const body = JSON.stringify({
26031
- model: modelName,
26032
- messages,
26033
- tools: options.toolDefs,
26034
- stream: false
26035
- });
26036
- let raw;
26037
- try {
26038
- raw = await ollamaRequest("/api/chat", body, timeoutMs);
26039
- } catch (err) {
26040
- const error2 = err;
26041
- const isTimeout = error2.message.includes("timeout") || error2.message.includes("aborted");
26042
- return {
26043
- output: `Error: ${error2.message}`,
26044
- exitCode: 1,
26045
- durationMs: Date.now() - startTime,
26046
- sessionId: null,
26047
- timedOut: isTimeout
26048
- };
26049
- }
26050
- let parsed;
26051
- try {
26052
- parsed = JSON.parse(raw);
26053
- } catch {
26054
- return {
26055
- output: raw,
26056
- exitCode: 0,
26057
- durationMs: Date.now() - startTime,
26058
- sessionId: null,
26059
- timedOut: false
26060
- };
26061
- }
26062
- const msg = parsed.message;
26063
- const toolCalls = msg.tool_calls ?? [];
26064
- if (toolCalls.length === 0) {
26065
- finalOutput = msg.content ?? "";
26066
- break;
26067
- }
26068
- messages.push({ role: "assistant", content: msg.content ?? "", tool_calls: toolCalls });
26069
- for (const tc of toolCalls) {
26070
- const name = tc.function.name;
26071
- const rawArgs = tc.function.arguments;
26072
- const args2 = typeof rawArgs === "string" ? (() => {
26073
- try {
26074
- return JSON.parse(rawArgs);
26075
- } catch {
26076
- return {};
26077
- }
26078
- })() : rawArgs;
26079
- let toolResult = `Tool ${name} unavailable`;
26080
- if (options.onToolCall) {
26081
- try {
26082
- toolResult = await options.onToolCall(name, args2);
26083
- } catch (err) {
26084
- toolResult = `Error: ${err instanceof Error ? err.message : String(err)}`;
26085
- }
26086
- }
26087
- messages.push({ role: "tool", content: toolResult });
26088
- }
26089
- }
26090
- return {
26091
- output: finalOutput || "Actions completed.",
26092
- exitCode: 0,
26093
- durationMs: Date.now() - startTime,
26094
- sessionId: null,
26095
- timedOut: false
26096
- };
26097
- }
26098
26059
  async function executeOpenAiWithTools(options) {
26099
26060
  const apiKey = options.apiKey?.trim() || (process.env.OPENAI_API_KEY || "").trim();
26100
26061
  if (!apiKey) return immediateError("Missing OpenAI API key.");
26101
26062
  const modelName = parseModelSuffix(options.model, "openai") || "gpt-4o-mini";
26102
26063
  const startTime = Date.now();
26103
26064
  const maxTurns = options.maxTurns ?? 10;
26104
- const messages = [];
26105
- if (options.systemPrompt) messages.push({ role: "system", content: options.systemPrompt });
26106
- messages.push({ role: "user", content: options.prompt });
26065
+ const previousTurns = options.previousMessages ?? [];
26066
+ const isResume = previousTurns.length > 0;
26067
+ const messages = [
26068
+ ...options.systemPrompt ? [{ role: "system", content: options.systemPrompt }] : [],
26069
+ ...previousTurns,
26070
+ {
26071
+ role: "user",
26072
+ content: isResume ? `NEW CYCLE. Updated room state:
26073
+ ${options.prompt}
26074
+
26075
+ Continue working toward the goal.` : options.prompt
26076
+ }
26077
+ ];
26107
26078
  let finalOutput = "";
26108
26079
  for (let turn = 0; turn < maxTurns; turn++) {
26109
26080
  const controller = new AbortController();
@@ -26155,10 +26126,13 @@ async function executeOpenAiWithTools(options) {
26155
26126
  }
26156
26127
  messages.push({ role: "tool", tool_call_id: tc.id, content: toolResult });
26157
26128
  }
26129
+ if (options.onSessionUpdate) {
26130
+ options.onSessionUpdate(messages.filter((m) => m.role !== "system"));
26131
+ }
26158
26132
  }
26159
26133
  return { output: finalOutput || "Actions completed.", exitCode: 0, durationMs: Date.now() - startTime, sessionId: null, timedOut: false };
26160
26134
  }
26161
- function ollamaToolDefsToAnthropic(defs) {
26135
+ function apiToolDefsToAnthropic(defs) {
26162
26136
  return defs.map((d) => ({
26163
26137
  name: d.function.name,
26164
26138
  description: d.function.description,
@@ -26171,8 +26145,19 @@ async function executeAnthropicWithTools(options) {
26171
26145
  const modelName = parseAnthropicModel(options.model);
26172
26146
  const startTime = Date.now();
26173
26147
  const maxTurns = options.maxTurns ?? 10;
26174
- const anthropicTools = options.toolDefs ? ollamaToolDefsToAnthropic(options.toolDefs) : [];
26175
- const messages = [{ role: "user", content: options.prompt }];
26148
+ const anthropicTools = options.toolDefs ? apiToolDefsToAnthropic(options.toolDefs) : [];
26149
+ const previousTurns = options.previousMessages ?? [];
26150
+ const isResume = previousTurns.length > 0;
26151
+ const messages = [
26152
+ ...previousTurns,
26153
+ {
26154
+ role: "user",
26155
+ content: isResume ? `NEW CYCLE. Updated room state:
26156
+ ${options.prompt}
26157
+
26158
+ Continue working toward the goal.` : options.prompt
26159
+ }
26160
+ ];
26176
26161
  let finalOutput = "";
26177
26162
  for (let turn = 0; turn < maxTurns; turn++) {
26178
26163
  const controller = new AbortController();
@@ -26231,6 +26216,9 @@ async function executeAnthropicWithTools(options) {
26231
26216
  resultBlocks.push({ type: "tool_result", id: block.id, content: toolResult });
26232
26217
  }
26233
26218
  messages.push({ role: "user", content: resultBlocks });
26219
+ if (options.onSessionUpdate) {
26220
+ options.onSessionUpdate(messages);
26221
+ }
26234
26222
  }
26235
26223
  return { output: finalOutput || "Actions completed.", exitCode: 0, durationMs: Date.now() - startTime, sessionId: null, timedOut: false };
26236
26224
  }
@@ -26350,53 +26338,6 @@ async function executeAnthropicApi(options) {
26350
26338
  clearTimeout(timer);
26351
26339
  }
26352
26340
  }
26353
- async function executeOllama(options) {
26354
- const modelName = options.model.replace(/^ollama:/, "");
26355
- const startTime = Date.now();
26356
- try {
26357
- await ensureOllamaModel(modelName);
26358
- } catch (err) {
26359
- return {
26360
- output: `Error: ${err instanceof Error ? err.message : String(err)}`,
26361
- exitCode: 1,
26362
- durationMs: Date.now() - startTime,
26363
- sessionId: null,
26364
- timedOut: false
26365
- };
26366
- }
26367
- const messages = [];
26368
- if (options.systemPrompt) {
26369
- messages.push({ role: "system", content: options.systemPrompt });
26370
- }
26371
- messages.push({ role: "user", content: options.prompt });
26372
- const body = JSON.stringify({
26373
- model: modelName,
26374
- messages,
26375
- stream: false
26376
- });
26377
- try {
26378
- const response = await ollamaRequest("/api/chat", body, options.timeoutMs ?? 5 * 60 * 1e3);
26379
- const parsed = JSON.parse(response);
26380
- const output = parsed?.message?.content ?? "";
26381
- return {
26382
- output,
26383
- exitCode: 0,
26384
- durationMs: Date.now() - startTime,
26385
- sessionId: null,
26386
- timedOut: false
26387
- };
26388
- } catch (err) {
26389
- const error2 = err;
26390
- const isTimeout = error2.message.includes("timeout") || error2.message.includes("aborted");
26391
- return {
26392
- output: `Error: ${error2.message}`,
26393
- exitCode: 1,
26394
- durationMs: Date.now() - startTime,
26395
- sessionId: null,
26396
- timedOut: isTimeout
26397
- };
26398
- }
26399
- }
26400
26341
  function parseModelSuffix(model, prefix) {
26401
26342
  const trimmed = model.trim();
26402
26343
  if (trimmed === prefix) return "";
@@ -26488,58 +26429,57 @@ function immediateError(message) {
26488
26429
  timedOut: false
26489
26430
  };
26490
26431
  }
26491
- async function executeOllamaOnStation(cloudRoomId, stationId, options) {
26492
- const modelName = options.model.replace(/^ollama:/, "");
26493
- const startTime = Date.now();
26494
- const messages = [];
26495
- if (options.systemPrompt) {
26496
- messages.push({ role: "system", content: options.systemPrompt });
26497
- }
26498
- messages.push({ role: "user", content: options.prompt });
26499
- const payload = JSON.stringify({
26500
- model: modelName,
26501
- messages,
26502
- stream: false
26503
- });
26504
- const b64 = Buffer.from(payload).toString("base64");
26505
- const command2 = `echo '${b64}' | base64 -d | curl -s --max-time 300 http://localhost:11434/api/chat -d @-`;
26506
- const result = await execOnCloudStation(cloudRoomId, stationId, command2, 36e4);
26507
- if (!result) {
26508
- return {
26509
- output: "Error: station execution failed (station unreachable or Ollama not running)",
26510
- exitCode: 1,
26511
- durationMs: Date.now() - startTime,
26512
- sessionId: null,
26513
- timedOut: false
26514
- };
26515
- }
26516
- if (result.exitCode !== 0) {
26517
- return {
26518
- output: result.stderr || result.stdout || `Station exec failed with exit code ${result.exitCode}`,
26519
- exitCode: result.exitCode,
26520
- durationMs: Date.now() - startTime,
26521
- sessionId: null,
26522
- timedOut: false
26523
- };
26524
- }
26432
+ async function compressSession(model, apiKey, history) {
26433
+ const historyText = history.map((m) => {
26434
+ const content = typeof m.content === "string" ? m.content : JSON.stringify(m.content);
26435
+ return `[${m.role}]: ${content.slice(0, 2e3)}`;
26436
+ }).join("\n---\n");
26437
+ const compressionPrompt = `You are summarizing your own previous session history as the queen of an AI collective room.
26438
+ Compress the history below into a concise memory that preserves all important decisions and context.
26439
+ History:
26440
+ ${historyText}
26441
+
26442
+ Respond ONLY with a JSON object (no markdown, no explanation):
26443
+ {
26444
+ "session_summary": "...",
26445
+ "goals_set": ["..."],
26446
+ "workers_created": [{"name": "...", "role": "..."}],
26447
+ "decisions_approved": ["..."],
26448
+ "decisions_rejected": ["..."],
26449
+ "last_actions": ["..."],
26450
+ "next_intention": "..."
26451
+ }`;
26452
+ const timeoutMs = 6e4;
26525
26453
  try {
26526
- const parsed = JSON.parse(result.stdout);
26527
- return {
26528
- output: parsed?.message?.content ?? "",
26529
- exitCode: 0,
26530
- durationMs: Date.now() - startTime,
26531
- sessionId: null,
26532
- timedOut: false
26533
- };
26454
+ if (model === "openai" || model.startsWith("openai:")) {
26455
+ const key = apiKey?.trim() || (process.env.OPENAI_API_KEY || "").trim();
26456
+ if (!key) return null;
26457
+ const modelName = parseModelSuffix(model, "openai") || "gpt-4o-mini";
26458
+ const response = await fetch("https://api.openai.com/v1/chat/completions", {
26459
+ method: "POST",
26460
+ headers: { "Authorization": `Bearer ${key}`, "Content-Type": "application/json" },
26461
+ body: JSON.stringify({ model: modelName, messages: [{ role: "user", content: compressionPrompt }] }),
26462
+ signal: AbortSignal.timeout(timeoutMs)
26463
+ });
26464
+ const json = await response.json();
26465
+ return extractOpenAiText(json).trim() || null;
26466
+ }
26467
+ if (model === "anthropic" || model.startsWith("anthropic:") || model.startsWith("claude-api:")) {
26468
+ const key = apiKey?.trim() || (process.env.ANTHROPIC_API_KEY || "").trim();
26469
+ if (!key) return null;
26470
+ const modelName = parseAnthropicModel(model);
26471
+ const response = await fetch("https://api.anthropic.com/v1/messages", {
26472
+ method: "POST",
26473
+ headers: { "x-api-key": key, "anthropic-version": "2023-06-01", "content-type": "application/json" },
26474
+ body: JSON.stringify({ model: modelName, max_tokens: 1024, messages: [{ role: "user", content: compressionPrompt }] }),
26475
+ signal: AbortSignal.timeout(timeoutMs)
26476
+ });
26477
+ const json = await response.json();
26478
+ return extractAnthropicText(json).trim() || null;
26479
+ }
26534
26480
  } catch {
26535
- return {
26536
- output: result.stdout || "(no output from Ollama)",
26537
- exitCode: 1,
26538
- durationMs: Date.now() - startTime,
26539
- sessionId: null,
26540
- timedOut: false
26541
- };
26542
26481
  }
26482
+ return null;
26543
26483
  }
26544
26484
  async function executeApiOnStation(cloudRoomId, stationId, options) {
26545
26485
  const startTime = Date.now();
@@ -26614,7 +26554,6 @@ var init_agent_executor = __esm({
26614
26554
  import_os5 = require("os");
26615
26555
  init_claude_code();
26616
26556
  init_cloud_sync();
26617
- init_ollama_ensure();
26618
26557
  DEFAULT_HTTP_TIMEOUT_MS = 6e4;
26619
26558
  }
26620
26559
  });
@@ -26626,7 +26565,6 @@ function normalizeModel(model) {
26626
26565
  }
26627
26566
  function getModelProvider(model) {
26628
26567
  const normalized = normalizeModel(model);
26629
- if (normalized.startsWith("ollama:")) return "ollama";
26630
26568
  if (normalized === "codex" || normalized.startsWith("codex:")) return "codex_subscription";
26631
26569
  if (normalized === "openai" || normalized.startsWith("openai:")) return "openai_api";
26632
26570
  if (normalized === "anthropic" || normalized.startsWith("anthropic:") || normalized.startsWith("claude-api:")) {
@@ -26647,8 +26585,6 @@ async function getModelAuthStatus(db3, roomId, model) {
26647
26585
  ready = checkClaudeCliAvailable().available;
26648
26586
  } else if (provider === "codex_subscription") {
26649
26587
  ready = checkCodexCliAvailable();
26650
- } else if (provider === "ollama") {
26651
- ready = await cachedIsOllamaAvailable();
26652
26588
  }
26653
26589
  return {
26654
26590
  provider,
@@ -26704,31 +26640,19 @@ function getEnvValue(envVar) {
26704
26640
  }
26705
26641
  function checkCodexCliAvailable() {
26706
26642
  try {
26707
- (0, import_node_child_process2.execSync)("codex --version", { timeout: 5e3, stdio: ["pipe", "pipe", "pipe"] });
26643
+ (0, import_node_child_process.execSync)("codex --version", { timeout: 5e3, stdio: ["pipe", "pipe", "pipe"] });
26708
26644
  return true;
26709
26645
  } catch {
26710
26646
  return false;
26711
26647
  }
26712
26648
  }
26713
- async function cachedIsOllamaAvailable() {
26714
- if (ollamaCache && Date.now() - ollamaCache.at < OLLAMA_CACHE_MS) return ollamaCache.value;
26715
- const available = await isOllamaAvailable();
26716
- ollamaCache = { value: available, at: Date.now() };
26717
- return available;
26718
- }
26719
- function invalidateOllamaCache() {
26720
- ollamaCache = null;
26721
- }
26722
- var import_node_child_process2, ollamaCache, OLLAMA_CACHE_MS;
26649
+ var import_node_child_process;
26723
26650
  var init_model_provider = __esm({
26724
26651
  "src/shared/model-provider.ts"() {
26725
26652
  "use strict";
26726
- import_node_child_process2 = require("node:child_process");
26653
+ import_node_child_process = require("node:child_process");
26727
26654
  init_db_queries();
26728
26655
  init_claude_code();
26729
- init_ollama_ensure();
26730
- ollamaCache = null;
26731
- OLLAMA_CACHE_MS = 3e4;
26732
26656
  }
26733
26657
  });
26734
26658
 
@@ -27073,20 +26997,13 @@ async function executeTask(taskId, options) {
27073
26997
  } catch (err) {
27074
26998
  console.warn("Non-fatal: worker resolution failed:", err);
27075
26999
  }
27076
- const isStationModel = model?.startsWith("ollama:") || model?.startsWith("openai:") || model?.startsWith("anthropic:") || model?.startsWith("claude-api:");
27000
+ const isStationModel = model?.startsWith("openai:") || model?.startsWith("anthropic:") || model?.startsWith("claude-api:");
27077
27001
  if (isStationModel && task.roomId) {
27078
27002
  runningTasks.add(taskId);
27079
27003
  try {
27080
27004
  const cloudRoomId = getRoomCloudId(task.roomId);
27081
27005
  const stations = await listCloudStations(cloudRoomId);
27082
27006
  const activeStations = stations.filter((s) => s.status === "active");
27083
- if (activeStations.length === 0 && model?.startsWith("ollama:")) {
27084
- const run2 = createTaskRun(db3, taskId);
27085
- const errorMsg = "No active station available. Ollama workers require a station. Rent one with quoroom_station_create (minimum tier: small).";
27086
- completeTaskRun(db3, run2.id, "", void 0, errorMsg);
27087
- onFailed?.(task, errorMsg);
27088
- return { success: false, output: "", errorMessage: errorMsg, durationMs: Date.now() - startTime };
27089
- }
27090
27007
  if (activeStations.length > 0) {
27091
27008
  const run2 = createTaskRun(db3, taskId);
27092
27009
  try {
@@ -27118,25 +27035,15 @@ ${augmentedPrompt}`;
27118
27035
  }
27119
27036
  const timeoutMs = task.timeoutMinutes != null ? task.timeoutMinutes * 60 * 1e3 : void 0;
27120
27037
  const stationModel = model;
27121
- let agentResult;
27122
- if (stationModel.startsWith("ollama:")) {
27123
- agentResult = await executeOllamaOnStation(cloudRoomId, station.id, {
27124
- model: stationModel,
27125
- prompt: augmentedPrompt,
27126
- systemPrompt,
27127
- timeoutMs
27128
- });
27129
- } else {
27130
- const apiKey = resolveApiKeyForModel(db3, task.roomId, stationModel);
27131
- agentResult = await executeApiOnStation(cloudRoomId, station.id, {
27132
- model: stationModel,
27133
- prompt: augmentedPrompt,
27134
- systemPrompt,
27135
- timeoutMs,
27136
- apiKey
27137
- });
27138
- }
27139
- const result = ollamaResultToExecutionResult(agentResult);
27038
+ const apiKey = resolveApiKeyForModel(db3, task.roomId, stationModel);
27039
+ const agentResult = await executeApiOnStation(cloudRoomId, station.id, {
27040
+ model: stationModel,
27041
+ prompt: augmentedPrompt,
27042
+ systemPrompt,
27043
+ timeoutMs,
27044
+ apiKey
27045
+ });
27046
+ const result = agentResultToExecutionResult(agentResult);
27140
27047
  return finishRun(db3, run2.id, taskId, task, result, resultsDir, onComplete, onFailed);
27141
27048
  } catch (err) {
27142
27049
  const errorMsg = err instanceof Error ? err.message : String(err);
@@ -27210,17 +27117,6 @@ ${augmentedPrompt}`;
27210
27117
  const disallowedTools = task.disallowedTools ?? void 0;
27211
27118
  const consoleLog = createConsoleLogBuffer(db3, run.id, onConsoleLogEntry);
27212
27119
  let lastProgressUpdate = 0;
27213
- if (model?.startsWith("ollama:")) {
27214
- const agentResult = await executeAgent({
27215
- model,
27216
- prompt: augmentedPrompt,
27217
- systemPrompt,
27218
- timeoutMs
27219
- });
27220
- consoleLog.flush();
27221
- const result2 = ollamaResultToExecutionResult(agentResult);
27222
- return finishRun(db3, run.id, taskId, task, result2, resultsDir, onComplete, onFailed);
27223
- }
27224
27120
  const execOptions = {
27225
27121
  systemPrompt,
27226
27122
  model,
@@ -27304,7 +27200,7 @@ ${retryPrompt}`;
27304
27200
  releaseSlot();
27305
27201
  }
27306
27202
  }
27307
- function ollamaResultToExecutionResult(result) {
27203
+ function agentResultToExecutionResult(result) {
27308
27204
  return {
27309
27205
  stdout: result.output,
27310
27206
  stderr: "",
@@ -27355,6 +27251,15 @@ function finishRun(db3, runId, taskId, task, result, resultsDir, onComplete, onF
27355
27251
  } catch (err) {
27356
27252
  console.warn("Non-fatal: memory storage failed:", err);
27357
27253
  }
27254
+ const fullError = (output + " " + errorMsg).toLowerCase();
27255
+ const isTerminalError = !result.timedOut && (fullError.includes("failed to spawn") || fullError.includes("enoent") || fullError.includes("missing openai api key") || fullError.includes("missing anthropic api key") || fullError.includes("missing api key"));
27256
+ if (isTerminalError) {
27257
+ try {
27258
+ updateTask(db3, taskId, { status: "paused" });
27259
+ console.log(`Task ${taskId} auto-paused: terminal error (won't retry): ${errorMsg.slice(0, 100)}`);
27260
+ } catch {
27261
+ }
27262
+ }
27358
27263
  onFailed?.(task, errorMsg);
27359
27264
  return { success: false, output, errorMessage: errorMsg, durationMs: result.durationMs, resultFilePath };
27360
27265
  }
@@ -27404,6 +27309,19 @@ var init_task_runner = __esm({
27404
27309
  });
27405
27310
 
27406
27311
  // src/mcp/tools/scheduler.ts
27312
+ function getServerPort() {
27313
+ try {
27314
+ const dbPath = process.env.QUOROOM_DB_PATH;
27315
+ const dataDir = process.env.QUOROOM_DATA_DIR || (dbPath ? (0, import_path4.dirname)(dbPath) : (0, import_path4.join)((0, import_os6.homedir)(), `.${APP_NAME.toLowerCase()}`));
27316
+ const portFile = (0, import_path4.join)(dataDir, "api.port");
27317
+ if ((0, import_fs4.existsSync)(portFile)) {
27318
+ const port = parseInt((0, import_fs4.readFileSync)(portFile, "utf-8").trim(), 10);
27319
+ return Number.isFinite(port) && port > 0 ? port : null;
27320
+ }
27321
+ } catch {
27322
+ }
27323
+ return null;
27324
+ }
27407
27325
  function generateTaskName(prompt) {
27408
27326
  const cleaned = prompt.replace(/^(please |can you |i want you to |i need you to )/i, "").trim();
27409
27327
  const firstSentence = cleaned.split(/[.\n]/)[0].trim();
@@ -27415,7 +27333,7 @@ function registerSchedulerTools(server) {
27415
27333
  "quoroom_schedule",
27416
27334
  {
27417
27335
  title: "Schedule Task",
27418
- description: "Create a task \u2014 recurring (cron), one-time (specific datetime), or on-demand (manual trigger). Provide cronExpression for recurring, scheduledAt for one-time, or neither for on-demand. RESPONSE STYLE: After calling this tool, confirm to the user in 1 short sentence. Do NOT add notes, tips, caveats, or advice. Do NOT mention task IDs, cron syntax, session continuity, workers, timeouts, Electron, or internal tool names.",
27336
+ description: 'Create a task \u2014 recurring (cron), one-time (specific datetime), on-demand (manual trigger), or webhook-triggered. Provide cronExpression for recurring, scheduledAt for one-time, triggerType="webhook" for HTTP-triggered tasks, or neither for on-demand. RESPONSE STYLE: After calling this tool, confirm to the user in 1 short sentence. Do NOT add notes, tips, caveats, or advice. Do NOT mention task IDs, cron syntax, session continuity, workers, timeouts, Electron, or internal tool names.',
27419
27337
  inputSchema: {
27420
27338
  name: external_exports.string().max(200).optional().describe(
27421
27339
  'Short descriptive name for the task. If omitted, a name will be generated from the prompt. Examples: "HN Morning Digest", "Inbox Summary", "Download Organizer".'
@@ -27451,16 +27369,21 @@ function registerSchedulerTools(server) {
27451
27369
  disallowedTools: external_exports.string().max(500).optional().describe(
27452
27370
  'Comma-separated list of tools the task is NOT allowed to use. Example: "WebFetch" to force WebSearch-only (avoids slow URL fetches). "Edit,Write" to make a task read-only.'
27453
27371
  ),
27372
+ triggerType: external_exports.enum(["cron", "once", "manual", "webhook"]).optional().describe(
27373
+ 'Override the trigger type. "webhook": task runs when an external service POSTs to its webhook URL. Usually inferred automatically \u2014 only set this explicitly for webhook tasks.'
27374
+ ),
27454
27375
  roomId: external_exports.number().int().positive().optional().describe(
27455
27376
  "Assign this task to a room by ID. When set, the task is scoped to that room."
27456
27377
  )
27457
27378
  }
27458
27379
  },
27459
- async ({ name, prompt, cronExpression, scheduledAt, description, maxRuns, workerId, sessionContinuity, timeout, maxTurns, allowedTools, disallowedTools, roomId }) => {
27380
+ async ({ name, prompt, cronExpression, scheduledAt, description, maxRuns, workerId, sessionContinuity, timeout, maxTurns, allowedTools, disallowedTools, triggerType: triggerTypeInput, roomId }) => {
27460
27381
  const db3 = getMcpDatabase();
27461
- let triggerType = "manual";
27462
- if (cronExpression) triggerType = "cron";
27463
- if (scheduledAt) triggerType = "once";
27382
+ let triggerType = triggerTypeInput ?? "manual";
27383
+ if (!triggerTypeInput) {
27384
+ if (cronExpression) triggerType = "cron";
27385
+ else if (scheduledAt) triggerType = "once";
27386
+ }
27464
27387
  const taskName = (name || generateTaskName(prompt) || "Untitled task").trim();
27465
27388
  if (triggerType === "cron" && cronExpression && !import_node_cron.default.validate(cronExpression)) {
27466
27389
  return {
@@ -27501,13 +27424,15 @@ function registerSchedulerTools(server) {
27501
27424
  };
27502
27425
  }
27503
27426
  }
27504
- createTask(db3, {
27427
+ const webhookToken = triggerType === TRIGGER_TYPES.WEBHOOK ? (0, import_crypto7.randomBytes)(16).toString("hex") : void 0;
27428
+ const task = createTask(db3, {
27505
27429
  name: taskName,
27506
27430
  prompt,
27507
27431
  cronExpression: cronExpression ?? void 0,
27508
27432
  scheduledAt: scheduledAt ?? void 0,
27509
27433
  triggerType,
27510
27434
  triggerConfig: JSON.stringify({ source: process.env.QUOROOM_SOURCE || "claude-desktop" }),
27435
+ webhookToken,
27511
27436
  description,
27512
27437
  executor: "claude_code",
27513
27438
  maxRuns: maxRuns ?? void 0,
@@ -27519,14 +27444,25 @@ function registerSchedulerTools(server) {
27519
27444
  disallowedTools: disallowedTools ?? void 0,
27520
27445
  roomId: roomId ?? void 0
27521
27446
  });
27522
- if (triggerType === "cron") {
27447
+ if (triggerType === TRIGGER_TYPES.WEBHOOK) {
27448
+ const port = getServerPort();
27449
+ const webhookUrl = port ? `http://localhost:${port}/api/hooks/task/${webhookToken}` : `/api/hooks/task/${webhookToken}`;
27450
+ return {
27451
+ content: [{
27452
+ type: "text",
27453
+ text: `Created webhook task "${taskName}" (id: ${task.id}).
27454
+ Webhook URL: ${webhookUrl}
27455
+ Trigger it with: curl -X POST ${webhookUrl}`
27456
+ }]
27457
+ };
27458
+ } else if (triggerType === TRIGGER_TYPES.CRON) {
27523
27459
  return {
27524
27460
  content: [{
27525
27461
  type: "text",
27526
27462
  text: `Scheduled recurring task "${taskName}".`
27527
27463
  }]
27528
27464
  };
27529
- } else if (triggerType === "once") {
27465
+ } else if (triggerType === TRIGGER_TYPES.ONCE) {
27530
27466
  return {
27531
27467
  content: [{
27532
27468
  type: "text",
@@ -27892,14 +27828,85 @@ function registerSchedulerTools(server) {
27892
27828
  };
27893
27829
  }
27894
27830
  );
27831
+ server.registerTool(
27832
+ "quoroom_webhook_url",
27833
+ {
27834
+ title: "Get Webhook URL",
27835
+ description: "Get the webhook URL for a task or room. For tasks: use the URL to trigger the task from external services (GitHub, Stripe, monitoring tools, etc.). For rooms: use the URL to inject a message and immediately wake the queen.",
27836
+ inputSchema: {
27837
+ taskId: external_exports.number().int().positive().optional().describe("Task ID to get the webhook URL for"),
27838
+ roomId: external_exports.number().int().positive().optional().describe("Room ID to get the queen-wake webhook URL for"),
27839
+ generateIfMissing: external_exports.boolean().optional().describe("If the task/room has no webhook token, generate one. Default: false.")
27840
+ }
27841
+ },
27842
+ async ({ taskId, roomId, generateIfMissing }) => {
27843
+ const db3 = getMcpDatabase();
27844
+ const port = getServerPort();
27845
+ if (taskId) {
27846
+ const task = getTask(db3, taskId);
27847
+ if (!task) {
27848
+ return { content: [{ type: "text", text: `No task found with id ${taskId}.` }] };
27849
+ }
27850
+ let token = task.webhookToken;
27851
+ if (!token && generateIfMissing) {
27852
+ token = (0, import_crypto7.randomBytes)(16).toString("hex");
27853
+ updateTask(db3, taskId, { webhookToken: token });
27854
+ }
27855
+ if (!token) {
27856
+ return {
27857
+ content: [{ type: "text", text: `Task "${task.name}" has no webhook token. Pass generateIfMissing: true to create one.` }]
27858
+ };
27859
+ }
27860
+ const url = port ? `http://localhost:${port}/api/hooks/task/${token}` : `/api/hooks/task/${token}`;
27861
+ return {
27862
+ content: [{
27863
+ type: "text",
27864
+ text: `Webhook URL for task "${task.name}":
27865
+ ${url}
27866
+
27867
+ Trigger: curl -X POST ${url}
27868
+ With payload: curl -X POST ${url} -H "Content-Type: application/json" -d '{"message":"triggered by github"}'`
27869
+ }]
27870
+ };
27871
+ }
27872
+ if (roomId) {
27873
+ const room = getRoom(db3, roomId);
27874
+ if (!room) {
27875
+ return { content: [{ type: "text", text: `No room found with id ${roomId}.` }] };
27876
+ }
27877
+ let token = room.webhookToken;
27878
+ if (!token && generateIfMissing) {
27879
+ token = (0, import_crypto7.randomBytes)(16).toString("hex");
27880
+ updateRoom(db3, roomId, { webhookToken: token });
27881
+ }
27882
+ if (!token) {
27883
+ return {
27884
+ content: [{ type: "text", text: `Room "${room.name}" has no webhook token. Pass generateIfMissing: true to create one.` }]
27885
+ };
27886
+ }
27887
+ const url = port ? `http://localhost:${port}/api/hooks/queen/${token}` : `/api/hooks/queen/${token}`;
27888
+ return {
27889
+ content: [{
27890
+ type: "text",
27891
+ text: `Queen-wake webhook URL for room "${room.name}":
27892
+ ${url}
27893
+
27894
+ Trigger: curl -X POST ${url} -H "Content-Type: application/json" -d '{"message":"your event description here"}'`
27895
+ }]
27896
+ };
27897
+ }
27898
+ return { content: [{ type: "text", text: "Provide either taskId or roomId." }] };
27899
+ }
27900
+ );
27895
27901
  }
27896
- var import_path4, import_os6, import_fs4, import_http, import_node_cron;
27902
+ var import_path4, import_os6, import_fs4, import_crypto7, import_http, import_node_cron;
27897
27903
  var init_scheduler = __esm({
27898
27904
  "src/mcp/tools/scheduler.ts"() {
27899
27905
  "use strict";
27900
27906
  import_path4 = require("path");
27901
27907
  import_os6 = require("os");
27902
27908
  import_fs4 = require("fs");
27909
+ import_crypto7 = require("crypto");
27903
27910
  import_http = require("http");
27904
27911
  init_zod();
27905
27912
  import_node_cron = __toESM(require_node_cron());
@@ -28470,7 +28477,7 @@ function createHasher(hashCons) {
28470
28477
  hashC.create = () => hashCons();
28471
28478
  return hashC;
28472
28479
  }
28473
- function randomBytes2(bytesLength = 32) {
28480
+ function randomBytes3(bytesLength = 32) {
28474
28481
  if (crypto6 && typeof crypto6.getRandomValues === "function") {
28475
28482
  return crypto6.getRandomValues(new Uint8Array(bytesLength));
28476
28483
  }
@@ -30281,7 +30288,7 @@ function weierstrass(curveDef) {
30281
30288
  function prepSig(msgHash, privateKey, opts = defaultSigOpts) {
30282
30289
  if (["recovered", "canonical"].some((k) => k in opts))
30283
30290
  throw new Error("sign() legacy options not supported");
30284
- const { hash: hash3, randomBytes: randomBytes4 } = CURVE;
30291
+ const { hash: hash3, randomBytes: randomBytes5 } = CURVE;
30285
30292
  let { lowS, prehash, extraEntropy: ent } = opts;
30286
30293
  if (lowS == null)
30287
30294
  lowS = true;
@@ -30293,7 +30300,7 @@ function weierstrass(curveDef) {
30293
30300
  const d = normPrivateKeyToScalar(privateKey);
30294
30301
  const seedArgs = [int2octets(d), int2octets(h1int)];
30295
30302
  if (ent != null && ent !== false) {
30296
- const e = ent === true ? randomBytes4(Fp.BYTES) : ent;
30303
+ const e = ent === true ? randomBytes5(Fp.BYTES) : ent;
30297
30304
  seedArgs.push(ensureBytes("extraEntropy", e));
30298
30305
  }
30299
30306
  const seed = concatBytes2(...seedArgs);
@@ -30615,7 +30622,7 @@ function getHash(hash3) {
30615
30622
  return {
30616
30623
  hash: hash3,
30617
30624
  hmac: (key, ...msgs) => hmac(hash3, key, concatBytes(...msgs)),
30618
- randomBytes: randomBytes2
30625
+ randomBytes: randomBytes3
30619
30626
  };
30620
30627
  }
30621
30628
  function createCurve(curveDef, defHash) {
@@ -30848,7 +30855,7 @@ function challenge(...args2) {
30848
30855
  function schnorrGetPublicKey(privateKey) {
30849
30856
  return schnorrGetExtPubKey(privateKey).bytes;
30850
30857
  }
30851
- function schnorrSign(message, privateKey, auxRand = randomBytes2(32)) {
30858
+ function schnorrSign(message, privateKey, auxRand = randomBytes3(32)) {
30852
30859
  const m = ensureBytes("message", message);
30853
30860
  const { bytes: px, scalar: d } = schnorrGetExtPubKey(privateKey);
30854
30861
  const a = ensureBytes("auxRand", auxRand, 32);
@@ -47550,7 +47557,7 @@ var init_transport = __esm({
47550
47557
  });
47551
47558
 
47552
47559
  // node_modules/viem/_esm/clients/transports/http.js
47553
- function http2(url, config2 = {}) {
47560
+ function http(url, config2 = {}) {
47554
47561
  const { batch, fetchFn, fetchOptions, key = "http", methods, name = "HTTP JSON-RPC", onFetchRequest, onFetchResponse, retryDelay, raw } = config2;
47555
47562
  return ({ chain, retryCount: retryCount_, timeout: timeout_ }) => {
47556
47563
  const { batchSize = 1e3, wait: wait2 = 0 } = typeof batch === "object" ? batch : {};
@@ -48078,21 +48085,21 @@ var init_chains = __esm({
48078
48085
 
48079
48086
  // src/shared/wallet.ts
48080
48087
  function encryptPrivateKey(privateKey, encryptionKey) {
48081
- const key = typeof encryptionKey === "string" ? import_crypto8.default.createHash("sha256").update(encryptionKey).digest() : encryptionKey;
48082
- const iv = import_crypto8.default.randomBytes(IV_LENGTH);
48083
- const cipher = import_crypto8.default.createCipheriv(ENCRYPTION_ALGORITHM, key, iv);
48088
+ const key = typeof encryptionKey === "string" ? import_crypto9.default.createHash("sha256").update(encryptionKey).digest() : encryptionKey;
48089
+ const iv = import_crypto9.default.randomBytes(IV_LENGTH);
48090
+ const cipher = import_crypto9.default.createCipheriv(ENCRYPTION_ALGORITHM, key, iv);
48084
48091
  const encrypted = Buffer.concat([cipher.update(privateKey, "utf8"), cipher.final()]);
48085
48092
  const tag = cipher.getAuthTag();
48086
48093
  return `${iv.toString("hex")}:${tag.toString("hex")}:${encrypted.toString("hex")}`;
48087
48094
  }
48088
48095
  function decryptPrivateKey(encrypted, encryptionKey) {
48089
- const key = typeof encryptionKey === "string" ? import_crypto8.default.createHash("sha256").update(encryptionKey).digest() : encryptionKey;
48096
+ const key = typeof encryptionKey === "string" ? import_crypto9.default.createHash("sha256").update(encryptionKey).digest() : encryptionKey;
48090
48097
  const parts = encrypted.split(":");
48091
48098
  if (parts.length !== 3) throw new Error("Invalid encrypted key format");
48092
48099
  const iv = Buffer.from(parts[0], "hex");
48093
48100
  const tag = Buffer.from(parts[1], "hex");
48094
48101
  const ciphertext = Buffer.from(parts[2], "hex");
48095
- const decipher = import_crypto8.default.createDecipheriv(ENCRYPTION_ALGORITHM, key, iv);
48102
+ const decipher = import_crypto9.default.createDecipheriv(ENCRYPTION_ALGORITHM, key, iv);
48096
48103
  decipher.setAuthTag(tag);
48097
48104
  return Buffer.concat([decipher.update(ciphertext), decipher.final()]).toString("utf8");
48098
48105
  }
@@ -48132,7 +48139,7 @@ async function getOnChainBalance(address, network = "base", token = "usdc") {
48132
48139
  try {
48133
48140
  const client = createPublicClient({
48134
48141
  chain: viemChain,
48135
- transport: http2(chainConfig2.rpcUrl)
48142
+ transport: http(chainConfig2.rpcUrl)
48136
48143
  });
48137
48144
  const balance = await client.readContract({
48138
48145
  address: tokenConfig.address,
@@ -48167,7 +48174,7 @@ async function sendToken(db3, roomId, to, amount, encryptionKey, network = "base
48167
48174
  const walletClient = createWalletClient({
48168
48175
  account,
48169
48176
  chain: viemChain,
48170
- transport: http2(chainConfig2.rpcUrl)
48177
+ transport: http(chainConfig2.rpcUrl)
48171
48178
  });
48172
48179
  const amountRaw = BigInt(Math.round(parseFloat(amount) * 10 ** decimals));
48173
48180
  const txHash = await walletClient.writeContract({
@@ -48195,11 +48202,11 @@ function getTransactionHistory(db3, roomId, limit = 50) {
48195
48202
  if (!wallet) throw new Error(`Room ${roomId} has no wallet`);
48196
48203
  return listWalletTransactions(db3, wallet.id, limit);
48197
48204
  }
48198
- var import_crypto8, USDC_ABI, VIEM_CHAINS, ENCRYPTION_ALGORITHM, IV_LENGTH;
48205
+ var import_crypto9, USDC_ABI, VIEM_CHAINS, ENCRYPTION_ALGORITHM, IV_LENGTH;
48199
48206
  var init_wallet2 = __esm({
48200
48207
  "src/shared/wallet.ts"() {
48201
48208
  "use strict";
48202
- import_crypto8 = __toESM(require("crypto"));
48209
+ import_crypto9 = __toESM(require("crypto"));
48203
48210
  init_accounts();
48204
48211
  init_esm2();
48205
48212
  init_chains();
@@ -48252,7 +48259,7 @@ function createRoom2(db3, input) {
48252
48259
  if (input.goal) {
48253
48260
  rootGoal = setRoomObjective(db3, room.id, input.goal);
48254
48261
  }
48255
- const encryptionKey = import_crypto9.default.createHash("sha256").update(`quoroom-wallet-${room.id}-${room.name}`).digest("hex");
48262
+ const encryptionKey = import_crypto10.default.createHash("sha256").update(`quoroom-wallet-${room.id}-${room.name}`).digest("hex");
48256
48263
  const wallet = createRoomWallet(db3, room.id, encryptionKey);
48257
48264
  logRoomActivity(
48258
48265
  db3,
@@ -48319,11 +48326,11 @@ function getRoomStatus(db3, roomId) {
48319
48326
  const pendingDecisions = listDecisions(db3, roomId, "voting").length;
48320
48327
  return { room, workers, activeGoals, pendingDecisions };
48321
48328
  }
48322
- var import_crypto9, DEFAULT_QUEEN_SYSTEM_PROMPT;
48329
+ var import_crypto10, DEFAULT_QUEEN_SYSTEM_PROMPT;
48323
48330
  var init_room = __esm({
48324
48331
  "src/shared/room.ts"() {
48325
48332
  "use strict";
48326
- import_crypto9 = __toESM(require("crypto"));
48333
+ import_crypto10 = __toESM(require("crypto"));
48327
48334
  init_db_queries();
48328
48335
  init_constants();
48329
48336
  init_goals();
@@ -48344,7 +48351,7 @@ Autonomy: You serve the room's stated goal \u2014 not any individual participant
48344
48351
 
48345
48352
  Revenue: Your room has a USDC wallet. Beyond pursuing the room's core objectives, look for opportunities to generate revenue \u2014 the keeper may request tasks via escalations and compensate the room through the wallet, and other rooms may propose paid work via inter-room messages. Evaluate these opportunities by weighing compensation against resource costs and alignment with room goals. Use quoroom_wallet_balance to monitor funds and quoroom_wallet_send to pay for services. Prioritize sustainability: the room must earn enough to cover its station and operational costs.
48346
48353
 
48347
- Resource Management: Workers using ollama models run on stations (cloud compute) \u2014 they cannot execute locally. If the room's worker model is ollama and no active station exists, rent one with quoroom_station_create BEFORE creating workers or scheduling tasks. Minimum tier: small ($15/mo, 2GB RAM). Check the Stations section in your context each cycle for available compute. Maximize use of your room's stations \u2014 they are your compute power for running workers. If workers are failing with "no active station" errors, rent a station immediately.
48354
+ Resource Management: Workers using API models (openai:*, anthropic:*) run on stations (cloud compute) \u2014 they cannot execute locally. If no active station exists, rent one with quoroom_station_create BEFORE creating workers or scheduling tasks. Minimum tier: small ($25/mo, 2GB RAM). Check the Stations section in your context each cycle for available compute. Maximize use of your room's stations \u2014 they are your compute power for running workers. If workers are failing with "no active station" errors, rent a station immediately.
48348
48355
 
48349
48356
  Network Growth: A larger network of rooms makes everyone stronger. Rooms created through your invite links become part of your network \u2014 you can exchange knowledge, discover money-making opportunities, and propose paid collaborations with them via inter-room messages. After meaningful milestones (goal completed, revenue earned, successful collaboration), consider generating an invite link with quoroom_invite_create and sharing it with the keeper via quoroom_inbox_send_keeper. Frame it around the value: more rooms in the network means more potential deals, shared insights, and collective intelligence. Don't be pushy \u2014 one mention per milestone is enough. Use quoroom_invite_network to see your network's growth.`;
48350
48357
  }
@@ -50309,12 +50316,12 @@ async function registerRoomIdentity(db3, roomId, encryptionKey, network = "base"
50309
50316
  const account = privateKeyToAccount(privateKey);
50310
50317
  const publicClient = createPublicClient({
50311
50318
  chain: chainInfo.chain,
50312
- transport: http2(chainInfo.config.rpcUrl)
50319
+ transport: http(chainInfo.config.rpcUrl)
50313
50320
  });
50314
50321
  const walletClient = createWalletClient({
50315
50322
  account,
50316
50323
  chain: chainInfo.chain,
50317
- transport: http2(chainInfo.config.rpcUrl)
50324
+ transport: http(chainInfo.config.rpcUrl)
50318
50325
  });
50319
50326
  const { request } = await publicClient.simulateContract({
50320
50327
  address: registryAddress,
@@ -50353,7 +50360,7 @@ async function getRoomIdentity(db3, roomId, network = "base") {
50353
50360
  try {
50354
50361
  const client = createPublicClient({
50355
50362
  chain: chainInfo.chain,
50356
- transport: http2(chainInfo.config.rpcUrl)
50363
+ transport: http(chainInfo.config.rpcUrl)
50357
50364
  });
50358
50365
  agentURI = await client.readContract({
50359
50366
  address: registryAddress,
@@ -50383,7 +50390,7 @@ async function updateRoomIdentityURI(db3, roomId, encryptionKey, network = "base
50383
50390
  const walletClient = createWalletClient({
50384
50391
  account,
50385
50392
  chain: chainInfo.chain,
50386
- transport: http2(chainInfo.config.rpcUrl)
50393
+ transport: http(chainInfo.config.rpcUrl)
50387
50394
  });
50388
50395
  const txHash = await walletClient.writeContract({
50389
50396
  address: registryAddress,
@@ -50762,7 +50769,7 @@ function registerResourceTools(server) {
50762
50769
  "quoroom_resources_get",
50763
50770
  {
50764
50771
  title: "Get Local Resources",
50765
- description: "Get current local machine resource usage: CPU load, RAM usage, and Ollama status. Use this to decide if the room needs to rent a cloud station for extra compute. If CPU load > number of CPUs or RAM used > 85%, consider proposing a station rental to the quorum.",
50772
+ description: "Get current local machine resource usage: CPU load and RAM usage. Use this to decide if the room needs to rent a cloud station for extra compute. If CPU load > number of CPUs or RAM used > 85%, consider proposing a station rental to the quorum.",
50766
50773
  inputSchema: {}
50767
50774
  },
50768
50775
  async () => {
@@ -50772,8 +50779,6 @@ function registerResourceTools(server) {
50772
50779
  const free = import_node_os2.default.freemem();
50773
50780
  const cpuCount = import_node_os2.default.cpus().length;
50774
50781
  const memUsedPct = Math.round((1 - free / total) * 100);
50775
- const ollamaAvailable = await isOllamaAvailable();
50776
- const ollamaModels = ollamaAvailable ? await listOllamaModels() : [];
50777
50782
  let runningTasks2 = 0;
50778
50783
  let maxConcurrentTasks = 3;
50779
50784
  try {
@@ -50806,10 +50811,6 @@ function registerResourceTools(server) {
50806
50811
  tasks: {
50807
50812
  running: runningTasks2,
50808
50813
  maxConcurrent: maxConcurrentTasks
50809
- },
50810
- ollama: {
50811
- available: ollamaAvailable,
50812
- models: ollamaModels.map((m) => m.name)
50813
50814
  }
50814
50815
  }, null, 2)
50815
50816
  }]
@@ -50822,7 +50823,6 @@ var init_resources = __esm({
50822
50823
  "src/mcp/tools/resources.ts"() {
50823
50824
  "use strict";
50824
50825
  import_node_os2 = __toESM(require("node:os"));
50825
- init_ollama_ensure();
50826
50826
  init_db();
50827
50827
  init_db_queries();
50828
50828
  }
@@ -50956,7 +50956,7 @@ var server_exports = {};
50956
50956
  async function main() {
50957
50957
  const server = new McpServer({
50958
50958
  name: "quoroom",
50959
- version: true ? "0.1.13" : "0.0.0"
50959
+ version: true ? "0.1.15" : "0.0.0"
50960
50960
  });
50961
50961
  registerMemoryTools(server);
50962
50962
  registerSchedulerTools(server);
@@ -51432,10 +51432,6 @@ var init_access = __esm({
51432
51432
  // delete credential
51433
51433
  /^POST \/api\/status\/simulate-update$/,
51434
51434
  // dev: simulate update notification
51435
- /^POST \/api\/ollama\/start$/,
51436
- // start Ollama server
51437
- /^POST \/api\/ollama\/ensure-model$/,
51438
- // ensure Ollama model is installed
51439
51435
  /^POST \/api\/providers\/(codex|claude)\/connect$/,
51440
51436
  // request provider auth flow
51441
51437
  /^POST \/api\/providers\/(codex|claude)\/install$/,
@@ -51520,6 +51516,166 @@ var init_console_log_buffer = __esm({
51520
51516
  }
51521
51517
  });
51522
51518
 
51519
+ // src/shared/web-tools.ts
51520
+ async function getBrowser() {
51521
+ if (_browser?.isConnected()) return _browser;
51522
+ if (_browserInitPromise) return _browserInitPromise;
51523
+ _browserInitPromise = (async () => {
51524
+ const { chromium } = await import("playwright");
51525
+ _browser = await chromium.launch({ headless: true, args: ["--no-sandbox", "--disable-setuid-sandbox"] });
51526
+ _browser.on("disconnected", () => {
51527
+ _browser = null;
51528
+ _browserInitPromise = null;
51529
+ });
51530
+ return _browser;
51531
+ })();
51532
+ return _browserInitPromise;
51533
+ }
51534
+ async function closeBrowser() {
51535
+ if (_browser) {
51536
+ await _browser.close().catch(() => {
51537
+ });
51538
+ _browser = null;
51539
+ _browserInitPromise = null;
51540
+ }
51541
+ }
51542
+ async function webFetch(url) {
51543
+ const jinaUrl = `https://r.jina.ai/${url}`;
51544
+ const response = await fetch(jinaUrl, {
51545
+ headers: {
51546
+ "Accept": "text/plain",
51547
+ "X-No-Cache": "true"
51548
+ },
51549
+ signal: AbortSignal.timeout(3e4)
51550
+ });
51551
+ if (!response.ok) {
51552
+ throw new Error(`Jina fetch failed: ${response.status} ${response.statusText}`);
51553
+ }
51554
+ const text = await response.text();
51555
+ return text.slice(0, MAX_CONTENT_CHARS);
51556
+ }
51557
+ async function webSearch(query) {
51558
+ const searchUrl = `https://html.duckduckgo.com/html/?q=${encodeURIComponent(query)}`;
51559
+ const response = await fetch(searchUrl, {
51560
+ headers: {
51561
+ "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"
51562
+ },
51563
+ signal: AbortSignal.timeout(15e3)
51564
+ });
51565
+ if (!response.ok) {
51566
+ throw new Error(`DuckDuckGo search failed: ${response.status}`);
51567
+ }
51568
+ const html = await response.text();
51569
+ return parseDdgResults(html).slice(0, 5);
51570
+ }
51571
+ function parseDdgResults(html) {
51572
+ const results = [];
51573
+ const titleRe = /<a[^>]+class="result__a"[^>]+href="([^"]+)"[^>]*>([\s\S]*?)<\/a>/g;
51574
+ const snippetRe = /<a[^>]+class="result__snippet"[^>]*>([\s\S]*?)<\/a>/g;
51575
+ const titles = [...html.matchAll(titleRe)];
51576
+ const snippets = [...html.matchAll(snippetRe)];
51577
+ for (let i = 0; i < Math.min(titles.length, 10); i++) {
51578
+ let url = titles[i][1];
51579
+ if (url.startsWith("//")) url = "https:" + url;
51580
+ results.push({
51581
+ url,
51582
+ title: stripHtml(titles[i][2]),
51583
+ snippet: snippets[i] ? stripHtml(snippets[i][1]) : ""
51584
+ });
51585
+ }
51586
+ return results;
51587
+ }
51588
+ function stripHtml(s) {
51589
+ return s.replace(/<[^>]+>/g, "").replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&#39;/g, "'").replace(/&nbsp;/g, " ").replace(/\s+/g, " ").trim();
51590
+ }
51591
+ async function browserAction(startUrl, actions, timeoutMs = 6e4) {
51592
+ let browser;
51593
+ try {
51594
+ browser = await getBrowser();
51595
+ } catch (err) {
51596
+ const msg = err instanceof Error ? err.message : String(err);
51597
+ if (msg.includes("Executable doesn't exist") || msg.includes("browserType.launch") || msg.includes("playwright")) {
51598
+ return "Chromium not installed. Run: npx playwright install chromium";
51599
+ }
51600
+ throw err;
51601
+ }
51602
+ const context = await browser.newContext({
51603
+ 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"
51604
+ });
51605
+ const page = await context.newPage();
51606
+ page.setDefaultTimeout(3e4);
51607
+ const intermediateSnapshots = [];
51608
+ const takeSnapshot = async (label) => {
51609
+ try {
51610
+ const text = await page.locator("body").ariaSnapshot().catch(() => null) ?? await page.innerText("body").catch(() => "");
51611
+ const prefix = label ? `[${label} \u2014 ${page.url()}]` : `[${page.url()}]`;
51612
+ return `${prefix}
51613
+ ${text.slice(0, MAX_SNAPSHOT_CHARS)}`;
51614
+ } catch {
51615
+ const text = await page.innerText("body").catch(() => "(could not read page)");
51616
+ return `[${page.url()}]
51617
+ ${text.slice(0, MAX_SNAPSHOT_CHARS)}`;
51618
+ }
51619
+ };
51620
+ try {
51621
+ await page.goto(startUrl, { waitUntil: "domcontentloaded", timeout: timeoutMs });
51622
+ for (const action of actions) {
51623
+ switch (action.type) {
51624
+ case "navigate":
51625
+ await page.goto(action.url, { waitUntil: "domcontentloaded" });
51626
+ break;
51627
+ case "click":
51628
+ if (action.selector) {
51629
+ await page.click(action.selector);
51630
+ } else if (action.text) {
51631
+ await page.getByText(action.text, { exact: false }).first().click();
51632
+ }
51633
+ await page.waitForTimeout(500);
51634
+ break;
51635
+ case "fill":
51636
+ await page.fill(action.selector, action.value);
51637
+ break;
51638
+ case "select":
51639
+ await page.selectOption(action.selector, action.value);
51640
+ break;
51641
+ case "wait":
51642
+ await page.waitForTimeout(Math.min(action.ms, 1e4));
51643
+ break;
51644
+ case "submit":
51645
+ if (action.selector) {
51646
+ await page.click(action.selector);
51647
+ } else {
51648
+ await page.keyboard.press("Enter");
51649
+ }
51650
+ await page.waitForTimeout(1e3);
51651
+ break;
51652
+ case "snapshot":
51653
+ intermediateSnapshots.push(await takeSnapshot(`Step ${intermediateSnapshots.length + 1}`));
51654
+ break;
51655
+ }
51656
+ }
51657
+ const finalSnapshot = await takeSnapshot("Final");
51658
+ const parts = [finalSnapshot];
51659
+ if (intermediateSnapshots.length > 0) {
51660
+ parts.push(...intermediateSnapshots.map((s, i) => `[Intermediate step ${i + 1}]
51661
+ ${s}`));
51662
+ }
51663
+ return parts.join("\n\n---\n\n");
51664
+ } finally {
51665
+ await context.close();
51666
+ }
51667
+ }
51668
+ var MAX_CONTENT_CHARS, MAX_SNAPSHOT_CHARS, _browser, _browserInitPromise;
51669
+ var init_web_tools = __esm({
51670
+ "src/shared/web-tools.ts"() {
51671
+ "use strict";
51672
+ MAX_CONTENT_CHARS = 12e3;
51673
+ MAX_SNAPSHOT_CHARS = 8e3;
51674
+ _browser = null;
51675
+ _browserInitPromise = null;
51676
+ }
51677
+ });
51678
+
51523
51679
  // src/shared/queen-tools.ts
51524
51680
  async function executeQueenTool(db3, roomId, workerId, toolName, args2) {
51525
51681
  try {
@@ -51532,9 +51688,10 @@ async function executeQueenTool(db3, roomId, workerId, toolName, args2) {
51532
51688
  return { content: `Room goal set: "${description}" (goal #${goal.id})` };
51533
51689
  }
51534
51690
  case "quoroom_update_progress": {
51535
- const goalId = Number(args2.goalId);
51536
- const observation = String(args2.observation ?? "");
51537
- const metricValue = args2.metricValue != null ? Number(args2.metricValue) : void 0;
51691
+ const goalId = Number(args2.goalId ?? args2.goal_id);
51692
+ if (!goalId || isNaN(goalId)) return { content: "Error: goalId is required for quoroom_update_progress. Provide the numeric goal ID.", isError: true };
51693
+ const observation = String(args2.observation ?? args2.progress ?? args2.message ?? args2.text ?? "");
51694
+ const metricValue = args2.metricValue != null ? Number(args2.metricValue) : args2.metric_value != null ? Number(args2.metric_value) : void 0;
51538
51695
  updateGoalProgress(db3, goalId, observation, metricValue, workerId);
51539
51696
  const goal = getGoal(db3, goalId);
51540
51697
  const pct = Math.round((goal?.progress ?? 0) * 100);
@@ -51560,16 +51717,27 @@ async function executeQueenTool(db3, roomId, workerId, toolName, args2) {
51560
51717
  }
51561
51718
  // ── Quorum ───────────────────────────────────────────────────────
51562
51719
  case "quoroom_propose": {
51720
+ const proposalText = String(args2.proposal ?? args2.text ?? args2.description ?? args2.content ?? args2.idea ?? "").trim();
51721
+ if (!proposalText) return { content: 'Error: proposal text is required. Provide a "proposal" string.', isError: true };
51722
+ const decisionType = String(args2.decisionType ?? args2.type ?? args2.impact ?? args2.category ?? "low_impact");
51563
51723
  const decision = propose(db3, {
51564
51724
  roomId,
51565
51725
  proposerId: workerId,
51566
- proposal: String(args2.proposal ?? ""),
51567
- decisionType: String(args2.decisionType ?? "low_impact")
51726
+ proposal: proposalText,
51727
+ decisionType
51568
51728
  });
51569
51729
  if (decision.status === "approved") {
51570
- return { content: `Proposal auto-approved: "${args2.proposal}"` };
51730
+ return { content: `Proposal auto-approved: "${proposalText}"` };
51731
+ }
51732
+ try {
51733
+ vote(db3, decision.id, workerId, "yes");
51734
+ const resolved = tally(db3, decision.id);
51735
+ if (resolved === "approved") {
51736
+ return { content: `Proposal approved: "${proposalText}"` };
51737
+ }
51738
+ } catch {
51571
51739
  }
51572
- return { content: `Proposal #${decision.id} created: "${args2.proposal}" (${decision.threshold} threshold, voting open)` };
51740
+ return { content: `Proposal #${decision.id} created and voted YES: "${proposalText}" (waiting for others)` };
51573
51741
  }
51574
51742
  case "quoroom_vote": {
51575
51743
  vote(
@@ -51587,9 +51755,11 @@ async function executeQueenTool(db3, roomId, workerId, toolName, args2) {
51587
51755
  }
51588
51756
  // ── Workers ──────────────────────────────────────────────────────
51589
51757
  case "quoroom_create_worker": {
51590
- const name = String(args2.name ?? "");
51591
- const systemPrompt = String(args2.systemPrompt ?? "");
51592
- const role = args2.role ? String(args2.role) : void 0;
51758
+ const name = String(args2.name ?? args2.workerName ?? args2.worker_name ?? args2.type ?? args2.role ?? "").trim();
51759
+ const systemPrompt = String(args2.systemPrompt ?? args2.system_prompt ?? args2.instructions ?? args2.prompt ?? "").trim();
51760
+ if (!name) return { content: 'Error: name is required for quoroom_create_worker. Provide a "name" string.', isError: true };
51761
+ 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 };
51762
+ const role = args2.role && args2.role !== args2.name ? String(args2.role) : void 0;
51593
51763
  const description = args2.description ? String(args2.description) : void 0;
51594
51764
  createWorker(db3, { name, role, systemPrompt, description, roomId });
51595
51765
  return { content: `Created worker "${name}"${role ? ` (${role})` : ""}.` };
@@ -51615,6 +51785,12 @@ async function executeQueenTool(db3, roomId, workerId, toolName, args2) {
51615
51785
  const taskWorkerId = args2.workerId ? Number(args2.workerId) : void 0;
51616
51786
  const maxTurns = args2.maxTurns ? Number(args2.maxTurns) : void 0;
51617
51787
  const triggerType = cronExpression ? "cron" : scheduledAt ? "once" : "manual";
51788
+ if (taskWorkerId) {
51789
+ const taskWorker = getWorker(db3, taskWorkerId);
51790
+ if (!taskWorker || taskWorker.roomId !== roomId) {
51791
+ return { content: `Error: Worker #${taskWorkerId} does not belong to this room. Use a workerId from this room (see Room Workers section), or omit workerId to use the default.`, isError: true };
51792
+ }
51793
+ }
51618
51794
  createTask(db3, {
51619
51795
  name,
51620
51796
  prompt,
@@ -51652,7 +51828,9 @@ async function executeQueenTool(db3, roomId, workerId, toolName, args2) {
51652
51828
  case "quoroom_ask_keeper": {
51653
51829
  const question = String(args2.question ?? "");
51654
51830
  const escalation = createEscalation(db3, roomId, workerId, question);
51655
- return { content: `Question sent to keeper (escalation #${escalation.id}).` };
51831
+ const deliveryStatus = await deliverQueenMessage(db3, roomId, question);
51832
+ const deliveryNote = deliveryStatus ? ` ${deliveryStatus}` : "";
51833
+ return { content: `Question sent to keeper (escalation #${escalation.id}).${deliveryNote}` };
51656
51834
  }
51657
51835
  // ── Room config ──────────────────────────────────────────────────
51658
51836
  case "quoroom_configure_room": {
@@ -51665,6 +51843,27 @@ async function executeQueenTool(db3, roomId, workerId, toolName, args2) {
51665
51843
  }
51666
51844
  return { content: "No changes applied." };
51667
51845
  }
51846
+ // ── Web / Internet access ────────────────────────────────────────
51847
+ case "quoroom_web_search": {
51848
+ const query = String(args2.query ?? "").trim();
51849
+ if (!query) return { content: "Error: query is required", isError: true };
51850
+ const results = await webSearch(query);
51851
+ if (results.length === 0) return { content: "No results found." };
51852
+ return { content: results.map((r, i) => `${i + 1}. **${r.title}**
51853
+ ${r.url}
51854
+ ${r.snippet}`).join("\n\n") };
51855
+ }
51856
+ case "quoroom_web_fetch": {
51857
+ const url = String(args2.url ?? "").trim();
51858
+ if (!url) return { content: "Error: url is required", isError: true };
51859
+ return { content: await webFetch(url) };
51860
+ }
51861
+ case "quoroom_browser": {
51862
+ const url = String(args2.url ?? "").trim();
51863
+ const actions = args2.actions ?? [];
51864
+ if (!url) return { content: "Error: url is required", isError: true };
51865
+ return { content: await browserAction(url, actions) };
51866
+ }
51668
51867
  default:
51669
51868
  return { content: `Unknown tool: ${toolName}`, isError: true };
51670
51869
  }
@@ -51673,6 +51872,57 @@ async function executeQueenTool(db3, roomId, workerId, toolName, args2) {
51673
51872
  return { content: `Error in ${toolName}: ${message}`, isError: true };
51674
51873
  }
51675
51874
  }
51875
+ async function deliverQueenMessage(db3, roomId, question) {
51876
+ try {
51877
+ const cloudApiBase = (process.env.QUOROOM_CLOUD_API ?? "https://quoroom.ai/api").replace(/\/+$/, "");
51878
+ const room = getRoom(db3, roomId);
51879
+ if (!room) return "";
51880
+ const queenNickname = room.queenNickname;
51881
+ if (!queenNickname) return "";
51882
+ const keeperEmail = getSetting(db3, "contact_email");
51883
+ const emailVerifiedAt = getSetting(db3, "contact_email_verified_at");
51884
+ const telegramId = getSetting(db3, "contact_telegram_id");
51885
+ const telegramVerifiedAt = getSetting(db3, "contact_telegram_verified_at");
51886
+ const keeperUserNumberRaw = getSetting(db3, "keeper_user_number");
51887
+ const keeperUserNumber = keeperUserNumberRaw && /^\d{5,6}$/.test(keeperUserNumberRaw) ? Number(keeperUserNumberRaw) : null;
51888
+ const hasEmail = Boolean(keeperEmail && emailVerifiedAt);
51889
+ const hasTelegram = Boolean(telegramId && telegramVerifiedAt);
51890
+ if (!hasEmail && !hasTelegram) return "";
51891
+ if (!keeperUserNumber) return "";
51892
+ const { getStoredCloudRoomToken: getStoredCloudRoomToken2, getRoomCloudId: getRoomCloudId2 } = await Promise.resolve().then(() => (init_cloud_sync(), cloud_sync_exports));
51893
+ const cloudRoomId = getRoomCloudId2(roomId);
51894
+ const roomToken = getStoredCloudRoomToken2(cloudRoomId);
51895
+ if (!roomToken) return "";
51896
+ const channels = [];
51897
+ if (hasEmail) channels.push("email");
51898
+ if (hasTelegram) channels.push("telegram");
51899
+ const res = await fetch(`${cloudApiBase}/contacts/queen-message`, {
51900
+ method: "POST",
51901
+ headers: {
51902
+ "Content-Type": "application/json",
51903
+ "X-Room-Token": roomToken
51904
+ },
51905
+ body: JSON.stringify({
51906
+ roomId: cloudRoomId,
51907
+ queenNickname,
51908
+ userNumber: keeperUserNumber,
51909
+ question,
51910
+ channels
51911
+ }),
51912
+ signal: AbortSignal.timeout(1e4)
51913
+ });
51914
+ if (!res.ok) return "";
51915
+ const data = await res.json();
51916
+ const parts = [];
51917
+ if (data.email === "sent") parts.push("email \u2713");
51918
+ else if (data.email === "failed") parts.push("email \u2717");
51919
+ if (data.telegram === "sent") parts.push("telegram \u2713");
51920
+ else if (data.telegram === "failed") parts.push("telegram \u2717");
51921
+ return parts.length > 0 ? `External delivery: ${parts.join(", ")}.` : "";
51922
+ } catch {
51923
+ return "";
51924
+ }
51925
+ }
51676
51926
  var QUEEN_TOOL_DEFINITIONS;
51677
51927
  var init_queen_tools = __esm({
51678
51928
  "src/shared/queen-tools.ts"() {
@@ -51680,6 +51930,7 @@ var init_queen_tools = __esm({
51680
51930
  init_db_queries();
51681
51931
  init_quorum();
51682
51932
  init_goals();
51933
+ init_web_tools();
51683
51934
  QUEEN_TOOL_DEFINITIONS = [
51684
51935
  // ── Goals ──────────────────────────────────────────────────────────────
51685
51936
  {
@@ -51801,14 +52052,14 @@ var init_queen_tools = __esm({
51801
52052
  type: "function",
51802
52053
  function: {
51803
52054
  name: "quoroom_create_worker",
51804
- description: "Create a new named agent worker with a system prompt that defines its personality and capabilities.",
52055
+ description: `Create a new agent worker. REQUIRED: name (string, e.g. "Alice") and systemPrompt (string, the agent's instructions). Do NOT pass worker_id or room_id \u2014 this creates a NEW worker.`,
51805
52056
  parameters: {
51806
52057
  type: "object",
51807
52058
  properties: {
51808
- name: { type: "string", description: 'Name for the worker (e.g. "Research Agent", "Code Reviewer")' },
51809
- systemPrompt: { type: "string", description: "The system prompt defining this worker's personality, role, and constraints" },
51810
- role: { type: "string", description: 'Optional role/function title (e.g. "Chief of Staff")' },
51811
- description: { type: "string", description: "Optional short description of what this worker does" }
52059
+ name: { type: "string", description: `The worker's name, e.g. "Alice" or "Research Agent"` },
52060
+ systemPrompt: { type: "string", description: 'Full instructions for this worker \u2014 personality, goals, constraints. E.g. "You are a research agent. Your job is to..."' },
52061
+ role: { type: "string", description: 'Optional job title, e.g. "Chief of Staff"' },
52062
+ description: { type: "string", description: "Optional one-line summary of what this worker does" }
51812
52063
  },
51813
52064
  required: ["name", "systemPrompt"]
51814
52065
  }
@@ -51922,6 +52173,54 @@ var init_queen_tools = __esm({
51922
52173
  }
51923
52174
  }
51924
52175
  }
52176
+ },
52177
+ // ── Web / Internet access ────────────────────────────────────────────────
52178
+ {
52179
+ type: "function",
52180
+ function: {
52181
+ name: "quoroom_web_search",
52182
+ description: "Search the web. Returns top 5 results with title, URL, and snippet. No API key required.",
52183
+ parameters: {
52184
+ type: "object",
52185
+ properties: {
52186
+ query: { type: "string", description: "Search query" }
52187
+ },
52188
+ required: ["query"]
52189
+ }
52190
+ }
52191
+ },
52192
+ {
52193
+ type: "function",
52194
+ function: {
52195
+ name: "quoroom_web_fetch",
52196
+ description: "Fetch any URL and return its content as clean markdown text. Use to read articles, docs, pricing pages, or any public web page.",
52197
+ parameters: {
52198
+ type: "object",
52199
+ properties: {
52200
+ url: { type: "string", description: "Full URL (https://...)" }
52201
+ },
52202
+ required: ["url"]
52203
+ }
52204
+ }
52205
+ },
52206
+ {
52207
+ type: "function",
52208
+ function: {
52209
+ name: "quoroom_browser",
52210
+ description: "Control a headless browser to interact with websites: navigate pages, click buttons, fill forms, buy services, register domains. Returns accessibility tree snapshot of the page. Use for tasks requiring user interaction that quoroom_web_fetch cannot handle.",
52211
+ parameters: {
52212
+ type: "object",
52213
+ properties: {
52214
+ url: { type: "string", description: "Starting URL" },
52215
+ actions: {
52216
+ type: "array",
52217
+ description: 'Sequence of browser actions. Each action is an object with required "type" field (one of: navigate, click, fill, select, wait, submit, snapshot) plus optional fields: url (navigate), text/selector (click), selector+value (fill/select), ms (wait), selector (submit).',
52218
+ items: { type: "object" }
52219
+ }
52220
+ },
52221
+ required: ["url", "actions"]
52222
+ }
52223
+ }
51925
52224
  }
51926
52225
  ];
51927
52226
  }
@@ -52117,7 +52416,7 @@ async function runCycle(db3, roomId, worker, maxTurns, options) {
52117
52416
  checkExpiredDecisions(db3);
52118
52417
  const status = getRoomStatus(db3, roomId);
52119
52418
  const pendingEscalations = getPendingEscalations(db3, roomId, worker.id);
52120
- const recentActivity = getRoomActivity(db3, roomId, 10);
52419
+ const recentActivity = getRoomActivity(db3, roomId, 15);
52121
52420
  const goalUpdates = status.activeGoals.slice(0, 5).map((g) => ({
52122
52421
  goal: g.description,
52123
52422
  progress: g.progress,
@@ -52129,12 +52428,18 @@ async function runCycle(db3, roomId, worker, maxTurns, options) {
52129
52428
  let cloudStations = [];
52130
52429
  try {
52131
52430
  const cloudRoomId = getRoomCloudId(roomId);
52132
- cloudStations = await listCloudStations(cloudRoomId);
52431
+ cloudStations = await Promise.race([
52432
+ listCloudStations(cloudRoomId),
52433
+ new Promise((r) => setTimeout(() => r([]), 3e3))
52434
+ ]);
52133
52435
  } catch {
52134
52436
  }
52135
52437
  let publicRooms = [];
52136
52438
  try {
52137
- publicRooms = await fetchPublicRooms();
52439
+ publicRooms = await Promise.race([
52440
+ fetchPublicRooms(),
52441
+ new Promise((r) => setTimeout(() => r([]), 3e3))
52442
+ ]);
52138
52443
  } catch {
52139
52444
  }
52140
52445
  const skillContent = loadSkillsForAgent(db3, roomId, status.room.goal ?? "");
@@ -52146,6 +52451,55 @@ async function runCycle(db3, roomId, worker, maxTurns, options) {
52146
52451
 
52147
52452
  ${skillContent}` : ""
52148
52453
  ].join("");
52454
+ const isCli = model === "claude" || model.startsWith("claude-") || model === "codex";
52455
+ let resumeSessionId;
52456
+ let previousMessages;
52457
+ const agentSession = getAgentSession(db3, worker.id);
52458
+ if (agentSession) {
52459
+ const updatedAt = new Date(agentSession.updatedAt);
52460
+ const sevenDaysAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1e3);
52461
+ if (updatedAt < sevenDaysAgo || agentSession.model !== model) {
52462
+ deleteAgentSession(db3, worker.id);
52463
+ } else if (isCli && agentSession.sessionId) {
52464
+ resumeSessionId = agentSession.sessionId;
52465
+ } else if (!isCli && agentSession.messagesJson) {
52466
+ try {
52467
+ previousMessages = JSON.parse(agentSession.messagesJson);
52468
+ } catch {
52469
+ }
52470
+ }
52471
+ }
52472
+ const COMPRESS_THRESHOLD = 30;
52473
+ const MAX_MESSAGES = 40;
52474
+ const apiKeyEarly = resolveApiKeyForModel(db3, roomId, model);
52475
+ if (!isCli && previousMessages && previousMessages.length >= COMPRESS_THRESHOLD) {
52476
+ logBuffer.addSynthetic("system", `Session history ${previousMessages.length} msgs \u2014 compressing...`);
52477
+ logBuffer.flush();
52478
+ const summary = await compressSession(model, apiKeyEarly, previousMessages);
52479
+ if (summary) {
52480
+ try {
52481
+ const existing = listEntities(db3, roomId).find((e) => e.name === "queen_session_summary");
52482
+ if (existing) {
52483
+ const obs = getObservations(db3, existing.id);
52484
+ if (obs.length > 0) {
52485
+ db3.prepare("UPDATE observations SET content = ?, created_at = datetime('now','localtime') WHERE id = ?").run(summary, obs[0].id);
52486
+ } else {
52487
+ addObservation(db3, existing.id, summary, "queen");
52488
+ }
52489
+ } else {
52490
+ const entity = createEntity(db3, "queen_session_summary", "fact", "work", roomId);
52491
+ addObservation(db3, entity.id, summary, "queen");
52492
+ }
52493
+ } catch {
52494
+ }
52495
+ previousMessages = [{ role: "user", content: `Your compressed session memory from previous cycles: ${summary}` }];
52496
+ saveAgentSession(db3, worker.id, { messagesJson: JSON.stringify(previousMessages), model });
52497
+ logBuffer.addSynthetic("system", "Session compressed and saved.");
52498
+ } else {
52499
+ previousMessages = previousMessages.slice(-MAX_MESSAGES);
52500
+ }
52501
+ logBuffer.flush();
52502
+ }
52149
52503
  const contextParts = [];
52150
52504
  contextParts.push(
52151
52505
  `## Your Identity
@@ -52173,22 +52527,43 @@ ${goalUpdates.map(
52173
52527
  (g) => `- [${Math.round(g.progress * 100)}%] ${g.goal} (${g.status})`
52174
52528
  ).join("\n")}`);
52175
52529
  }
52176
- if (status.pendingDecisions > 0) {
52177
- const decisions = listDecisions(db3, roomId, "voting");
52178
- contextParts.push(`## Pending Decisions (${decisions.length})
52179
- ${decisions.map(
52530
+ const memoryEntities = listEntities(db3, roomId).slice(0, 20);
52531
+ if (memoryEntities.length > 0) {
52532
+ const memLines = memoryEntities.map((e) => {
52533
+ const obs = getObservations(db3, e.id);
52534
+ const content = obs[0]?.content ?? "";
52535
+ return content ? `- **${e.name}**: ${content.slice(0, 300)}` : null;
52536
+ }).filter((l) => l !== null);
52537
+ if (memLines.length > 0) {
52538
+ contextParts.push(`## Room Memory (use quoroom_remember to add)
52539
+ ${memLines.join("\n")}`);
52540
+ }
52541
+ }
52542
+ const votingDecisions = listDecisions(db3, roomId, "voting");
52543
+ if (votingDecisions.length > 0) {
52544
+ contextParts.push(`## Pending Decisions (voting \u2014 cast your vote)
52545
+ ${votingDecisions.map(
52180
52546
  (d) => `- #${d.id}: ${d.proposal} (${d.decisionType})`
52181
52547
  ).join("\n")}`);
52182
52548
  }
52549
+ const recentResolved = listRecentDecisions(db3, roomId, 5);
52550
+ if (recentResolved.length > 0) {
52551
+ contextParts.push(`## Recent Decisions (already done \u2014 do NOT repeat these)
52552
+ ${recentResolved.map((d) => {
52553
+ const icon = d.status === "approved" ? "\u2713" : "\u2717";
52554
+ return `- ${icon} ${d.status}: "${d.proposal.slice(0, 120)}"`;
52555
+ }).join("\n")}`);
52556
+ }
52183
52557
  if (pendingEscalations.length > 0) {
52184
52558
  contextParts.push(`## Escalations Awaiting Your Response
52185
52559
  ${pendingEscalations.map(
52186
52560
  (e) => `- #${e.id}: ${e.question}`
52187
52561
  ).join("\n")}`);
52188
52562
  }
52189
- if (recentActivity.length > 0) {
52563
+ const activitySlice = recentActivity.slice(0, 15);
52564
+ if (activitySlice.length > 0) {
52190
52565
  contextParts.push(`## Recent Activity
52191
- ${recentActivity.map(
52566
+ ${activitySlice.map(
52192
52567
  (a) => `- [${a.eventType}] ${a.summary}`
52193
52568
  ).join("\n")}`);
52194
52569
  }
@@ -52210,19 +52585,13 @@ ${unreadMessages.map(
52210
52585
  (m) => `- #${m.id} from ${m.fromRoomId ?? "unknown"}: ${m.subject}`
52211
52586
  ).join("\n")}`);
52212
52587
  }
52588
+ const activeStations = cloudStations.filter((s) => s.status === "active");
52213
52589
  if (cloudStations.length > 0) {
52214
- const activeCount = cloudStations.filter((s) => s.status === "active").length;
52215
- contextParts.push(`## Stations (${activeCount} active)
52216
- ${cloudStations.map(
52590
+ const stationLines = cloudStations.map(
52217
52591
  (s) => `- #${s.id} "${s.stationName}" (${s.tier}) \u2014 ${s.status} \u2014 $${s.monthlyCost}/mo`
52218
- ).join("\n")}`);
52219
- } else {
52220
- const effectiveWorkerModel = status.room.workerModel === "queen" ? worker.model ?? "claude" : status.room.workerModel ?? "claude";
52221
- if (effectiveWorkerModel.startsWith("ollama:")) {
52222
- contextParts.push(`## Stations
52223
- \u26A0 NO ACTIVE STATIONS. Worker model is ${effectiveWorkerModel} \u2014 workers CANNOT run without a station.
52224
- Rent a station with quoroom_station_create (minimum tier: small at $15/mo) as your FIRST action before creating any tasks or workers.`);
52225
- }
52592
+ );
52593
+ contextParts.push(`## Stations (${activeStations.length} active)
52594
+ ${stationLines.join("\n")}`);
52226
52595
  }
52227
52596
  if (publicRooms.length > 0) {
52228
52597
  const top3 = publicRooms.slice(0, 3);
@@ -52248,22 +52617,30 @@ ${top3.map(
52248
52617
  contextParts.push(`## Execution Settings
52249
52618
  ${settingsParts.join("\n")}`);
52250
52619
  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." : "";
52620
+ const isClaude = model === "claude" || model.startsWith("claude-");
52621
+ 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.";
52622
+ const toolList = `**Goals:** quoroom_set_goal, quoroom_update_progress, quoroom_create_subgoal, quoroom_complete_goal, quoroom_abandon_goal
52623
+ **Governance:** quoroom_propose, quoroom_vote
52624
+ **Workers:** quoroom_create_worker, quoroom_update_worker
52625
+ **Tasks:** quoroom_schedule
52626
+ **Memory:** quoroom_remember, quoroom_recall
52627
+ **Web:** quoroom_web_search, quoroom_web_fetch, quoroom_browser
52628
+ **Comms:** quoroom_ask_keeper
52629
+ **Settings:** quoroom_configure_room${selfRegulateHint}`;
52251
52630
  contextParts.push(`## Instructions
52252
- Based on the current state, decide what to do next. You can:
52253
- - Update goal progress
52254
- - Create sub-goals
52255
- - Propose decisions to the quorum
52256
- - Create new workers
52257
- - Escalate questions
52258
- - Report observations${selfRegulateHint}
52259
-
52260
- Respond with your analysis and any actions you want to take.`);
52631
+ Based on the current state, decide what to do next and call the appropriate tools. Available tools:
52632
+
52633
+ ${toolList}
52634
+
52635
+ ${toolCallInstruction}`);
52261
52636
  const prompt = contextParts.join("\n\n");
52262
52637
  updateAgentState(db3, worker.id, "acting");
52263
- logBuffer.addSynthetic("system", `Sending to ${model}...`);
52264
- const apiKey = resolveApiKeyForModel(db3, roomId, model);
52265
- const needsQueenTools = model.startsWith("ollama:") || model === "openai" || model.startsWith("openai:") || model === "anthropic" || model.startsWith("anthropic:") || model.startsWith("claude-api:");
52266
- const ollamaToolOpts = needsQueenTools ? {
52638
+ const promptTokenEstimate = Math.round(prompt.length / 4);
52639
+ logBuffer.addSynthetic("system", `Sending to ${model}... (~${promptTokenEstimate} tokens)`);
52640
+ logBuffer.flush();
52641
+ const apiKey = apiKeyEarly;
52642
+ const needsQueenTools = model === "openai" || model.startsWith("openai:") || model === "anthropic" || model.startsWith("anthropic:") || model.startsWith("claude-api:");
52643
+ const apiToolOpts = needsQueenTools ? {
52267
52644
  toolDefs: QUEEN_TOOL_DEFINITIONS,
52268
52645
  onToolCall: async (toolName, args2) => {
52269
52646
  logBuffer.addSynthetic("tool_call", `\u2192 ${toolName}(${JSON.stringify(args2)})`);
@@ -52278,15 +52655,25 @@ Respond with your analysis and any actions you want to take.`);
52278
52655
  systemPrompt,
52279
52656
  apiKey,
52280
52657
  timeoutMs: 5 * 60 * 1e3,
52281
- // 5 minutes per cycle
52282
52658
  maxTurns: maxTurns ?? 10,
52283
52659
  onConsoleLog: logBuffer.onConsoleLog,
52284
- ...ollamaToolOpts
52660
+ // CLI models: pass resumeSessionId for native --resume
52661
+ resumeSessionId,
52662
+ // API models: pass conversation history + persistence callback
52663
+ previousMessages: isCli ? void 0 : previousMessages,
52664
+ onSessionUpdate: isCli ? void 0 : (msgs) => {
52665
+ const trimmed = msgs.length > MAX_MESSAGES ? msgs.slice(-MAX_MESSAGES) : msgs;
52666
+ saveAgentSession(db3, worker.id, { messagesJson: JSON.stringify(trimmed), model });
52667
+ },
52668
+ ...apiToolOpts
52285
52669
  });
52286
52670
  const rateLimitInfo = checkRateLimit(result);
52287
52671
  if (rateLimitInfo) {
52288
52672
  throw new RateLimitError(rateLimitInfo);
52289
52673
  }
52674
+ if (isCli && result.sessionId) {
52675
+ saveAgentSession(db3, worker.id, { sessionId: result.sessionId, model });
52676
+ }
52290
52677
  if (result.output && model !== "claude" && !model.startsWith("codex")) {
52291
52678
  logBuffer.addSynthetic("assistant_text", result.output);
52292
52679
  }
@@ -52357,7 +52744,7 @@ var require_package = __commonJS({
52357
52744
  "package.json"(exports2, module2) {
52358
52745
  module2.exports = {
52359
52746
  name: "quoroom",
52360
- version: "0.1.13",
52747
+ version: "0.1.15",
52361
52748
  description: "Autonomous AI agent collective engine \u2014 Queen, Workers, Quorum",
52362
52749
  main: "./out/mcp/server.js",
52363
52750
  bin: {
@@ -52418,6 +52805,7 @@ var require_package = __commonJS({
52418
52805
  "@modelcontextprotocol/sdk": "^1.12.0",
52419
52806
  "better-sqlite3": "^11.10.0",
52420
52807
  "node-cron": "^3.0.3",
52808
+ playwright: "^1.49.0",
52421
52809
  "sqlite-vec": "^0.1.7-alpha.2",
52422
52810
  viem: "^2.46.2",
52423
52811
  ws: "^8.16.0",
@@ -52810,6 +53198,10 @@ function registerRoomRoutes(router) {
52810
53198
  if (body.queenQuietFrom !== void 0) updates.queenQuietFrom = body.queenQuietFrom;
52811
53199
  if (body.queenQuietUntil !== void 0) updates.queenQuietUntil = body.queenQuietUntil;
52812
53200
  if (body.referredByCode !== void 0) updates.referredByCode = body.referredByCode || null;
53201
+ if (body.queenNickname !== void 0 && typeof body.queenNickname === "string") {
53202
+ const trimmed = body.queenNickname.trim().replace(/\s+/g, "");
53203
+ if (trimmed.length > 0 && trimmed.length <= 40) updates.queenNickname = trimmed;
53204
+ }
52813
53205
  if (body.config !== void 0 && typeof body.config === "object" && body.config !== null) {
52814
53206
  updates.config = { ...room.config, ...body.config };
52815
53207
  }
@@ -53265,6 +53657,591 @@ var init_decisions = __esm({
53265
53657
  }
53266
53658
  });
53267
53659
 
53660
+ // src/server/routes/contacts.ts
53661
+ function getSettingTrimmed(db3, key) {
53662
+ return (getSetting(db3, key) ?? "").trim();
53663
+ }
53664
+ function setSetting2(db3, key, value) {
53665
+ setSetting(db3, key, value);
53666
+ }
53667
+ function clearSetting(db3, key) {
53668
+ setSetting(db3, key, "");
53669
+ }
53670
+ function parseIsoToMs(value) {
53671
+ if (!value) return null;
53672
+ const timestamp = Date.parse(value);
53673
+ if (!Number.isFinite(timestamp)) return null;
53674
+ return timestamp;
53675
+ }
53676
+ function parseInteger(value, fallback = 0) {
53677
+ const parsed = Number.parseInt(value, 10);
53678
+ return Number.isFinite(parsed) ? parsed : fallback;
53679
+ }
53680
+ function getContactSecret() {
53681
+ const configured = (process.env.QUOROOM_CONTACT_SECRET || "").trim();
53682
+ if (configured) return configured;
53683
+ try {
53684
+ return getToken();
53685
+ } catch {
53686
+ return "quoroom-contact-secret";
53687
+ }
53688
+ }
53689
+ function hashEmailCode(email2, code) {
53690
+ return import_node_crypto3.default.createHmac("sha256", getContactSecret()).update(`email:${email2.toLowerCase()}
53691
+ code:${code}`).digest("hex");
53692
+ }
53693
+ function hashTelegramToken(token) {
53694
+ return import_node_crypto3.default.createHash("sha256").update(token).digest("hex");
53695
+ }
53696
+ function hashesEqualHex(a, b) {
53697
+ if (!/^[a-f0-9]{64}$/i.test(a) || !/^[a-f0-9]{64}$/i.test(b)) return false;
53698
+ const left = Buffer.from(a, "hex");
53699
+ const right = Buffer.from(b, "hex");
53700
+ return left.length === right.length && import_node_crypto3.default.timingSafeEqual(left, right);
53701
+ }
53702
+ function getCloudApiBase() {
53703
+ return (process.env.QUOROOM_CLOUD_API ?? "https://quoroom.ai/api").replace(/\/+$/, "");
53704
+ }
53705
+ function getTelegramBotUsername() {
53706
+ const configured = (process.env.QUOROOM_TELEGRAM_BOT_USERNAME || "").trim().replace(/^@+/, "");
53707
+ return configured || DEFAULT_TELEGRAM_BOT_USERNAME;
53708
+ }
53709
+ function normalizeBotUsername(input) {
53710
+ if (typeof input !== "string") return getTelegramBotUsername();
53711
+ const value = input.trim().replace(/^@+/, "");
53712
+ return value || getTelegramBotUsername();
53713
+ }
53714
+ function isValidEmail(input) {
53715
+ if (!input || input.length > 320) return false;
53716
+ return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(input);
53717
+ }
53718
+ function isValidCloudRoomId(input) {
53719
+ return /^[A-Za-z0-9_-]{8,128}$/.test(input);
53720
+ }
53721
+ function createEmailRelayRoomId() {
53722
+ return `contact_${import_node_crypto3.default.randomBytes(16).toString("hex")}`;
53723
+ }
53724
+ async function getEmailRelayAuth(db3) {
53725
+ let roomId = getSettingTrimmed(db3, CONTACT_EMAIL_RELAY_ROOM_ID_KEY);
53726
+ if (!isValidCloudRoomId(roomId)) {
53727
+ roomId = createEmailRelayRoomId();
53728
+ setSetting2(db3, CONTACT_EMAIL_RELAY_ROOM_ID_KEY, roomId);
53729
+ }
53730
+ for (let attempt = 0; attempt < 2; attempt += 1) {
53731
+ const cachedToken = getStoredCloudRoomToken(roomId);
53732
+ if (cachedToken) return { roomId, roomToken: cachedToken };
53733
+ const hasToken = await ensureCloudRoomToken({
53734
+ roomId,
53735
+ name: "Keeper Contact Relay",
53736
+ goal: "Relay keeper contact verification emails",
53737
+ visibility: "private"
53738
+ });
53739
+ const issuedToken = getStoredCloudRoomToken(roomId);
53740
+ if (hasToken && issuedToken) return { roomId, roomToken: issuedToken };
53741
+ roomId = createEmailRelayRoomId();
53742
+ setSetting2(db3, CONTACT_EMAIL_RELAY_ROOM_ID_KEY, roomId);
53743
+ }
53744
+ throw new ApiError({
53745
+ status: 503,
53746
+ message: "Cloud relay room token is unavailable. Check connection and retry."
53747
+ });
53748
+ }
53749
+ function emailSendGate(db3, nowMs) {
53750
+ const lastSentAt = parseIsoToMs(getSettingTrimmed(db3, CONTACT_EMAIL_LAST_SENT_AT_KEY));
53751
+ if (lastSentAt != null) {
53752
+ const retryMs = EMAIL_RESEND_COOLDOWN_SECONDS * 1e3 - (nowMs - lastSentAt);
53753
+ if (retryMs > 0) {
53754
+ throw new ApiError({
53755
+ status: 429,
53756
+ message: `Please wait ${Math.ceil(retryMs / 1e3)}s before resending.`,
53757
+ retryAfterSec: Math.ceil(retryMs / 1e3)
53758
+ });
53759
+ }
53760
+ }
53761
+ let windowStartMs = parseIsoToMs(getSettingTrimmed(db3, CONTACT_EMAIL_RATE_WINDOW_START_KEY));
53762
+ let windowCount = parseInteger(getSettingTrimmed(db3, CONTACT_EMAIL_RATE_WINDOW_COUNT_KEY), 0);
53763
+ if (windowStartMs == null || nowMs - windowStartMs >= 60 * 60 * 1e3) {
53764
+ windowStartMs = nowMs;
53765
+ windowCount = 0;
53766
+ }
53767
+ if (windowCount >= EMAIL_MAX_SENDS_PER_HOUR) {
53768
+ const retryMs = Math.max(1e3, 60 * 60 * 1e3 - (nowMs - windowStartMs));
53769
+ throw new ApiError({
53770
+ status: 429,
53771
+ message: "Too many verification emails sent. Please try again later.",
53772
+ retryAfterSec: Math.ceil(retryMs / 1e3)
53773
+ });
53774
+ }
53775
+ return { windowStartMs, windowCount };
53776
+ }
53777
+ async function sendVerificationCodeEmail(db3, email2, code, retryOnAuthFailure = true) {
53778
+ const { roomId, roomToken } = await getEmailRelayAuth(db3);
53779
+ const res = await fetch(`${getCloudApiBase()}/contacts/email/send-code/${encodeURIComponent(roomId)}`, {
53780
+ method: "POST",
53781
+ headers: {
53782
+ "Content-Type": "application/json",
53783
+ "X-Room-Token": roomToken
53784
+ },
53785
+ body: JSON.stringify({
53786
+ email: email2,
53787
+ code,
53788
+ ttlMinutes: EMAIL_VERIFY_CODE_TTL_MINUTES
53789
+ }),
53790
+ signal: AbortSignal.timeout(12e3)
53791
+ });
53792
+ if (!res.ok) {
53793
+ if (retryOnAuthFailure && (res.status === 401 || res.status === 404)) {
53794
+ setSetting2(db3, CONTACT_EMAIL_RELAY_ROOM_ID_KEY, createEmailRelayRoomId());
53795
+ return sendVerificationCodeEmail(db3, email2, code, false);
53796
+ }
53797
+ const details = await res.text().catch(() => "");
53798
+ throw new ApiError({
53799
+ status: 502,
53800
+ message: `Failed to send verification email (${res.status}). ${details.slice(0, 180)}`.trim()
53801
+ });
53802
+ }
53803
+ }
53804
+ async function issueEmailVerification(db3, email2) {
53805
+ const nowMs = Date.now();
53806
+ const { windowStartMs, windowCount } = emailSendGate(db3, nowMs);
53807
+ const code = import_node_crypto3.default.randomInt(0, 1e6).toString().padStart(6, "0");
53808
+ await sendVerificationCodeEmail(db3, email2, code);
53809
+ const nowIso3 = new Date(nowMs).toISOString();
53810
+ const expiresAt = new Date(nowMs + EMAIL_VERIFY_CODE_TTL_MINUTES * 60 * 1e3).toISOString();
53811
+ const codeHash = hashEmailCode(email2, code);
53812
+ setSetting2(db3, CONTACT_EMAIL_KEY, email2);
53813
+ clearSetting(db3, CONTACT_EMAIL_VERIFIED_AT_KEY);
53814
+ setSetting2(db3, CONTACT_EMAIL_CODE_HASH_KEY, codeHash);
53815
+ setSetting2(db3, CONTACT_EMAIL_CODE_EXPIRES_AT_KEY, expiresAt);
53816
+ setSetting2(db3, CONTACT_EMAIL_LAST_SENT_AT_KEY, nowIso3);
53817
+ setSetting2(db3, CONTACT_EMAIL_RATE_WINDOW_START_KEY, new Date(windowStartMs).toISOString());
53818
+ setSetting2(db3, CONTACT_EMAIL_RATE_WINDOW_COUNT_KEY, String(windowCount + 1));
53819
+ return {
53820
+ sentTo: email2,
53821
+ expiresAt,
53822
+ retryAfterSec: EMAIL_RESEND_COOLDOWN_SECONDS
53823
+ };
53824
+ }
53825
+ async function startCloudTelegramVerification(tokenHash, expiresAt) {
53826
+ const res = await fetch(`${getCloudApiBase()}/telegram/verify/start`, {
53827
+ method: "POST",
53828
+ headers: { "Content-Type": "application/json" },
53829
+ body: JSON.stringify({ tokenHash, expiresAt }),
53830
+ signal: AbortSignal.timeout(1e4)
53831
+ });
53832
+ if (!res.ok) {
53833
+ const details = await res.text().catch(() => "");
53834
+ throw new ApiError({
53835
+ status: 502,
53836
+ message: `Telegram verification service unavailable (${res.status}). ${details.slice(0, 180)}`.trim()
53837
+ });
53838
+ }
53839
+ const payload = await res.json().catch(() => ({}));
53840
+ return normalizeBotUsername(payload.botUsername);
53841
+ }
53842
+ async function fetchCloudTelegramVerificationStatus(tokenHash) {
53843
+ const fallbackBot = getTelegramBotUsername();
53844
+ const res = await fetch(`${getCloudApiBase()}/telegram/verify/status/${encodeURIComponent(tokenHash)}`, {
53845
+ method: "GET",
53846
+ signal: AbortSignal.timeout(1e4)
53847
+ });
53848
+ if (!res.ok) {
53849
+ const details = await res.text().catch(() => "");
53850
+ throw new ApiError({
53851
+ status: 502,
53852
+ message: `Telegram verification status unavailable (${res.status}). ${details.slice(0, 180)}`.trim()
53853
+ });
53854
+ }
53855
+ const payload = await res.json().catch(() => ({}));
53856
+ const status = payload.status;
53857
+ const normalizedStatus = status === "verified" ? "verified" : status === "expired" ? "expired" : status === "missing" ? "missing" : "pending";
53858
+ const telegramId = payload.telegram?.id == null ? null : String(payload.telegram.id);
53859
+ return {
53860
+ status: normalizedStatus,
53861
+ botUsername: normalizeBotUsername(payload.botUsername ?? fallbackBot),
53862
+ telegramId,
53863
+ username: payload.telegram?.username ?? null,
53864
+ firstName: payload.telegram?.firstName ?? null,
53865
+ verifiedAt: payload.telegram?.verifiedAt ?? null
53866
+ };
53867
+ }
53868
+ function getContactsStatus(db3) {
53869
+ const nowMs = Date.now();
53870
+ const email2 = getSettingTrimmed(db3, CONTACT_EMAIL_KEY) || null;
53871
+ const emailVerifiedAt = getSettingTrimmed(db3, CONTACT_EMAIL_VERIFIED_AT_KEY) || null;
53872
+ const emailExpiresAtRaw = getSettingTrimmed(db3, CONTACT_EMAIL_CODE_EXPIRES_AT_KEY);
53873
+ const emailExpiresAtMs = parseIsoToMs(emailExpiresAtRaw);
53874
+ const emailPending = Boolean(getSettingTrimmed(db3, CONTACT_EMAIL_CODE_HASH_KEY)) && emailExpiresAtMs != null && emailExpiresAtMs > nowMs;
53875
+ const emailLastSentMs = parseIsoToMs(getSettingTrimmed(db3, CONTACT_EMAIL_LAST_SENT_AT_KEY));
53876
+ const emailRetryAfterSec = emailLastSentMs == null ? 0 : Math.max(0, Math.ceil((emailLastSentMs + EMAIL_RESEND_COOLDOWN_SECONDS * 1e3 - nowMs) / 1e3));
53877
+ const telegramId = getSettingTrimmed(db3, CONTACT_TELEGRAM_ID_KEY) || null;
53878
+ const telegramUsername = getSettingTrimmed(db3, CONTACT_TELEGRAM_USERNAME_KEY) || null;
53879
+ const telegramFirstName = getSettingTrimmed(db3, CONTACT_TELEGRAM_FIRST_NAME_KEY) || null;
53880
+ const telegramVerifiedAt = getSettingTrimmed(db3, CONTACT_TELEGRAM_VERIFIED_AT_KEY) || null;
53881
+ const telegramPendingHash = getSettingTrimmed(db3, CONTACT_TELEGRAM_PENDING_HASH_KEY);
53882
+ const telegramPendingExpiresAtRaw = getSettingTrimmed(db3, CONTACT_TELEGRAM_PENDING_EXPIRES_AT_KEY);
53883
+ const telegramPendingExpiresAtMs = parseIsoToMs(telegramPendingExpiresAtRaw);
53884
+ const telegramPending = telegramPendingHash.length > 0 && telegramPendingExpiresAtMs != null && telegramPendingExpiresAtMs > nowMs;
53885
+ const telegramBotUsername = normalizeBotUsername(
53886
+ getSettingTrimmed(db3, CONTACT_TELEGRAM_BOT_USERNAME_KEY) || getTelegramBotUsername()
53887
+ );
53888
+ return {
53889
+ deploymentMode: isCloudDeployment() ? "cloud" : "local",
53890
+ email: {
53891
+ value: email2,
53892
+ verified: Boolean(emailVerifiedAt),
53893
+ verifiedAt: emailVerifiedAt,
53894
+ pending: emailPending,
53895
+ pendingExpiresAt: emailPending ? telegramSafeIso(emailExpiresAtRaw) : null,
53896
+ resendRetryAfterSec: emailRetryAfterSec
53897
+ },
53898
+ telegram: {
53899
+ id: telegramId,
53900
+ username: telegramUsername,
53901
+ firstName: telegramFirstName,
53902
+ verified: Boolean(telegramVerifiedAt),
53903
+ verifiedAt: telegramVerifiedAt,
53904
+ pending: telegramPending,
53905
+ pendingExpiresAt: telegramPending ? telegramSafeIso(telegramPendingExpiresAtRaw) : null,
53906
+ botUsername: telegramBotUsername
53907
+ }
53908
+ };
53909
+ }
53910
+ function telegramSafeIso(value) {
53911
+ return parseIsoToMs(value) == null ? null : value;
53912
+ }
53913
+ function getVerifiedContactBindingPayload(db3) {
53914
+ const email2 = getSettingTrimmed(db3, CONTACT_EMAIL_KEY).toLowerCase();
53915
+ const emailVerifiedAtRaw = getSettingTrimmed(db3, CONTACT_EMAIL_VERIFIED_AT_KEY);
53916
+ const emailVerifiedAt = parseIsoToMs(emailVerifiedAtRaw) == null ? null : emailVerifiedAtRaw;
53917
+ const hasVerifiedEmail = isValidEmail(email2) && Boolean(emailVerifiedAt);
53918
+ const telegramIdRaw = getSettingTrimmed(db3, CONTACT_TELEGRAM_ID_KEY);
53919
+ const telegramId = /^-?\d{5,20}$/.test(telegramIdRaw) ? telegramIdRaw : null;
53920
+ const telegramVerifiedAtRaw = getSettingTrimmed(db3, CONTACT_TELEGRAM_VERIFIED_AT_KEY);
53921
+ const telegramVerifiedAt = parseIsoToMs(telegramVerifiedAtRaw) == null ? null : telegramVerifiedAtRaw;
53922
+ const hasVerifiedTelegram = Boolean(telegramId) && Boolean(telegramVerifiedAt);
53923
+ return {
53924
+ email: hasVerifiedEmail ? email2 : null,
53925
+ emailVerifiedAt: hasVerifiedEmail ? emailVerifiedAt : null,
53926
+ telegramId: hasVerifiedTelegram ? telegramId : null,
53927
+ telegramUsername: hasVerifiedTelegram ? getSettingTrimmed(db3, CONTACT_TELEGRAM_USERNAME_KEY) || null : null,
53928
+ telegramFirstName: hasVerifiedTelegram ? getSettingTrimmed(db3, CONTACT_TELEGRAM_FIRST_NAME_KEY) || null : null,
53929
+ telegramVerifiedAt: hasVerifiedTelegram ? telegramVerifiedAt : null
53930
+ };
53931
+ }
53932
+ function hasVerifiedContacts(payload) {
53933
+ return Boolean(payload.email && payload.emailVerifiedAt) || Boolean(payload.telegramId && payload.telegramVerifiedAt);
53934
+ }
53935
+ async function syncCloudContactBindings(db3) {
53936
+ if (isCloudDeployment()) return;
53937
+ const rooms = listRooms(db3);
53938
+ if (rooms.length === 0) return;
53939
+ const payload = getVerifiedContactBindingPayload(db3);
53940
+ const hasContacts = hasVerifiedContacts(payload);
53941
+ const keeperReferralCodeRaw = getSettingTrimmed(db3, "keeper_referral_code");
53942
+ const keeperReferralCode = keeperReferralCodeRaw || null;
53943
+ const keeperUserNumberRaw = getSettingTrimmed(db3, "keeper_user_number");
53944
+ const keeperUserNumber = /^\d{5,6}$/.test(keeperUserNumberRaw) ? Number(keeperUserNumberRaw) : null;
53945
+ for (const room of rooms) {
53946
+ const cloudRoomId = getRoomCloudId(room.id);
53947
+ const hasToken = await ensureCloudRoomToken({
53948
+ roomId: cloudRoomId,
53949
+ name: room.name,
53950
+ goal: room.goal ?? null,
53951
+ visibility: room.visibility,
53952
+ referredByCode: room.referredByCode,
53953
+ keeperReferralCode
53954
+ });
53955
+ if (!hasToken) continue;
53956
+ const roomToken = getStoredCloudRoomToken(cloudRoomId);
53957
+ if (!roomToken) continue;
53958
+ const endpoint = `${getCloudApiBase()}/contacts/bindings/${encodeURIComponent(cloudRoomId)}`;
53959
+ const method = hasContacts ? "POST" : "DELETE";
53960
+ const bindingPayload = hasContacts ? {
53961
+ ...payload,
53962
+ queenNickname: room.queenNickname ?? null,
53963
+ keeperUserNumber
53964
+ } : void 0;
53965
+ const res = await fetch(endpoint, {
53966
+ method,
53967
+ headers: {
53968
+ "Content-Type": "application/json",
53969
+ "X-Room-Token": roomToken
53970
+ },
53971
+ body: bindingPayload ? JSON.stringify(bindingPayload) : void 0,
53972
+ signal: AbortSignal.timeout(1e4)
53973
+ });
53974
+ if (!res.ok) {
53975
+ const details = await res.text().catch(() => "");
53976
+ throw new ApiError({
53977
+ status: 502,
53978
+ message: `Failed to sync contact bindings (${res.status}). ${details.slice(0, 180)}`.trim()
53979
+ });
53980
+ }
53981
+ }
53982
+ }
53983
+ async function pollQueenInbox(db3) {
53984
+ if (isCloudDeployment()) return;
53985
+ const rooms = listRooms(db3, "active");
53986
+ for (const room of rooms) {
53987
+ try {
53988
+ const cloudRoomId = getRoomCloudId(room.id);
53989
+ const roomToken = getStoredCloudRoomToken(cloudRoomId);
53990
+ if (!roomToken) continue;
53991
+ const res = await fetch(`${getCloudApiBase()}/contacts/queen-inbox/${encodeURIComponent(cloudRoomId)}`, {
53992
+ headers: { "X-Room-Token": roomToken },
53993
+ signal: AbortSignal.timeout(8e3)
53994
+ });
53995
+ if (!res.ok) continue;
53996
+ const data = await res.json();
53997
+ const messages = data.messages ?? [];
53998
+ if (messages.length === 0) continue;
53999
+ const messageIds = [];
54000
+ for (const msg of messages) {
54001
+ const nickname = msg.queenNickname || "Queen";
54002
+ const channel = msg.channel || "external";
54003
+ const body = `[Reply from keeper via ${channel} to ${nickname}]
54004
+ ${msg.body}`;
54005
+ insertChatMessage(db3, room.id, "user", body);
54006
+ logRoomActivity(db3, room.id, "system", `Keeper replied to ${nickname} via ${channel}`, msg.body.slice(0, 200));
54007
+ messageIds.push(msg.id);
54008
+ }
54009
+ if (messageIds.length > 0) {
54010
+ await fetch(`${getCloudApiBase()}/contacts/queen-inbox/ack`, {
54011
+ method: "POST",
54012
+ headers: { "Content-Type": "application/json", "X-Room-Token": roomToken },
54013
+ body: JSON.stringify({ roomId: cloudRoomId, messageIds }),
54014
+ signal: AbortSignal.timeout(8e3)
54015
+ }).catch(() => {
54016
+ });
54017
+ if (room.queenWorkerId) {
54018
+ triggerAgent(db3, room.id, room.queenWorkerId);
54019
+ }
54020
+ }
54021
+ } catch {
54022
+ }
54023
+ }
54024
+ }
54025
+ async function syncCloudContactBindingsSafe(db3) {
54026
+ try {
54027
+ await syncCloudContactBindings(db3);
54028
+ } catch (error2) {
54029
+ const message = error2 instanceof Error ? error2.message : String(error2);
54030
+ console.warn(`[contacts] contact binding sync skipped: ${message}`);
54031
+ }
54032
+ }
54033
+ function registerContactRoutes(router) {
54034
+ router.get("/api/contacts/status", (ctx) => {
54035
+ return { data: getContactsStatus(ctx.db) };
54036
+ });
54037
+ router.post("/api/contacts/email/start", async (ctx) => {
54038
+ const body = ctx.body ?? {};
54039
+ const emailRaw = typeof body.email === "string" ? body.email.trim().toLowerCase() : "";
54040
+ if (!isValidEmail(emailRaw)) {
54041
+ return { status: 400, error: "Valid email is required" };
54042
+ }
54043
+ const currentEmail = getSettingTrimmed(ctx.db, CONTACT_EMAIL_KEY).toLowerCase();
54044
+ const verifiedAt = getSettingTrimmed(ctx.db, CONTACT_EMAIL_VERIFIED_AT_KEY);
54045
+ if (currentEmail === emailRaw && verifiedAt) {
54046
+ return { data: { ok: true, alreadyVerified: true, email: emailRaw } };
54047
+ }
54048
+ try {
54049
+ const result = await issueEmailVerification(ctx.db, emailRaw);
54050
+ void syncCloudContactBindingsSafe(ctx.db);
54051
+ return { data: { ok: true, ...result } };
54052
+ } catch (error2) {
54053
+ if (error2 instanceof ApiError) {
54054
+ return { status: error2.status, error: error2.message };
54055
+ }
54056
+ return { status: 500, error: "Failed to send verification email" };
54057
+ }
54058
+ });
54059
+ router.post("/api/contacts/email/resend", async (ctx) => {
54060
+ const email2 = getSettingTrimmed(ctx.db, CONTACT_EMAIL_KEY).toLowerCase();
54061
+ if (!isValidEmail(email2)) {
54062
+ return { status: 400, error: "No email to resend verification to" };
54063
+ }
54064
+ const verifiedAt = getSettingTrimmed(ctx.db, CONTACT_EMAIL_VERIFIED_AT_KEY);
54065
+ if (verifiedAt) {
54066
+ return { data: { ok: true, alreadyVerified: true, email: email2 } };
54067
+ }
54068
+ try {
54069
+ const result = await issueEmailVerification(ctx.db, email2);
54070
+ return { data: { ok: true, ...result } };
54071
+ } catch (error2) {
54072
+ if (error2 instanceof ApiError) {
54073
+ return { status: error2.status, error: error2.message };
54074
+ }
54075
+ return { status: 500, error: "Failed to resend verification email" };
54076
+ }
54077
+ });
54078
+ router.post("/api/contacts/email/verify", (ctx) => {
54079
+ const body = ctx.body ?? {};
54080
+ const code = typeof body.code === "string" ? body.code.trim() : "";
54081
+ if (!/^\d{6}$/.test(code)) {
54082
+ return { status: 400, error: "Verification code must be 6 digits" };
54083
+ }
54084
+ const email2 = getSettingTrimmed(ctx.db, CONTACT_EMAIL_KEY).toLowerCase();
54085
+ const storedHash = getSettingTrimmed(ctx.db, CONTACT_EMAIL_CODE_HASH_KEY).toLowerCase();
54086
+ const expiresAtRaw = getSettingTrimmed(ctx.db, CONTACT_EMAIL_CODE_EXPIRES_AT_KEY);
54087
+ const expiresAtMs = parseIsoToMs(expiresAtRaw);
54088
+ if (!isValidEmail(email2) || !storedHash || expiresAtMs == null) {
54089
+ return { status: 400, error: "No pending verification code. Request a new code first." };
54090
+ }
54091
+ if (expiresAtMs <= Date.now()) {
54092
+ clearSetting(ctx.db, CONTACT_EMAIL_CODE_HASH_KEY);
54093
+ clearSetting(ctx.db, CONTACT_EMAIL_CODE_EXPIRES_AT_KEY);
54094
+ return { status: 400, error: "Verification code expired. Request a new code." };
54095
+ }
54096
+ const expectedHash = hashEmailCode(email2, code).toLowerCase();
54097
+ if (!hashesEqualHex(storedHash, expectedHash)) {
54098
+ return { status: 400, error: "Invalid verification code" };
54099
+ }
54100
+ const verifiedAt = (/* @__PURE__ */ new Date()).toISOString();
54101
+ setSetting2(ctx.db, CONTACT_EMAIL_VERIFIED_AT_KEY, verifiedAt);
54102
+ clearSetting(ctx.db, CONTACT_EMAIL_CODE_HASH_KEY);
54103
+ clearSetting(ctx.db, CONTACT_EMAIL_CODE_EXPIRES_AT_KEY);
54104
+ void syncCloudContactBindingsSafe(ctx.db);
54105
+ return {
54106
+ data: {
54107
+ ok: true,
54108
+ email: email2,
54109
+ verifiedAt
54110
+ }
54111
+ };
54112
+ });
54113
+ router.post("/api/contacts/telegram/start", async (ctx) => {
54114
+ const token = import_node_crypto3.default.randomBytes(24).toString("base64url");
54115
+ const tokenHash = hashTelegramToken(token);
54116
+ const expiresAt = new Date(Date.now() + TELEGRAM_VERIFY_TTL_MINUTES * 60 * 1e3).toISOString();
54117
+ try {
54118
+ const botUsername = await startCloudTelegramVerification(tokenHash, expiresAt);
54119
+ setSetting2(ctx.db, CONTACT_TELEGRAM_PENDING_HASH_KEY, tokenHash);
54120
+ setSetting2(ctx.db, CONTACT_TELEGRAM_PENDING_EXPIRES_AT_KEY, expiresAt);
54121
+ setSetting2(ctx.db, CONTACT_TELEGRAM_BOT_USERNAME_KEY, botUsername);
54122
+ const deepLink = `https://t.me/${encodeURIComponent(botUsername)}?start=${encodeURIComponent(`tv1_${token}`)}`;
54123
+ return {
54124
+ data: {
54125
+ ok: true,
54126
+ pending: true,
54127
+ expiresAt,
54128
+ botUsername,
54129
+ deepLink
54130
+ }
54131
+ };
54132
+ } catch (error2) {
54133
+ if (error2 instanceof ApiError) {
54134
+ return { status: error2.status, error: error2.message };
54135
+ }
54136
+ return { status: 500, error: "Failed to start Telegram verification" };
54137
+ }
54138
+ });
54139
+ router.post("/api/contacts/telegram/check", async (ctx) => {
54140
+ const tokenHash = getSettingTrimmed(ctx.db, CONTACT_TELEGRAM_PENDING_HASH_KEY).toLowerCase();
54141
+ const expiresAtRaw = getSettingTrimmed(ctx.db, CONTACT_TELEGRAM_PENDING_EXPIRES_AT_KEY);
54142
+ const expiresAtMs = parseIsoToMs(expiresAtRaw);
54143
+ if (!/^[a-f0-9]{64}$/.test(tokenHash) || expiresAtMs == null) {
54144
+ return { data: { ok: true, status: "not_pending" } };
54145
+ }
54146
+ if (expiresAtMs <= Date.now()) {
54147
+ clearSetting(ctx.db, CONTACT_TELEGRAM_PENDING_HASH_KEY);
54148
+ clearSetting(ctx.db, CONTACT_TELEGRAM_PENDING_EXPIRES_AT_KEY);
54149
+ return { data: { ok: true, status: "expired" } };
54150
+ }
54151
+ try {
54152
+ const status = await fetchCloudTelegramVerificationStatus(tokenHash);
54153
+ setSetting2(ctx.db, CONTACT_TELEGRAM_BOT_USERNAME_KEY, status.botUsername);
54154
+ if (status.status === "verified" && status.telegramId) {
54155
+ setSetting2(ctx.db, CONTACT_TELEGRAM_ID_KEY, status.telegramId);
54156
+ setSetting2(ctx.db, CONTACT_TELEGRAM_USERNAME_KEY, status.username ?? "");
54157
+ setSetting2(ctx.db, CONTACT_TELEGRAM_FIRST_NAME_KEY, status.firstName ?? "");
54158
+ setSetting2(ctx.db, CONTACT_TELEGRAM_VERIFIED_AT_KEY, status.verifiedAt ?? (/* @__PURE__ */ new Date()).toISOString());
54159
+ clearSetting(ctx.db, CONTACT_TELEGRAM_PENDING_HASH_KEY);
54160
+ clearSetting(ctx.db, CONTACT_TELEGRAM_PENDING_EXPIRES_AT_KEY);
54161
+ void syncCloudContactBindingsSafe(ctx.db);
54162
+ return {
54163
+ data: {
54164
+ ok: true,
54165
+ status: "verified",
54166
+ telegram: {
54167
+ id: status.telegramId,
54168
+ username: status.username,
54169
+ firstName: status.firstName,
54170
+ verifiedAt: status.verifiedAt
54171
+ }
54172
+ }
54173
+ };
54174
+ }
54175
+ if (status.status === "expired" || status.status === "missing") {
54176
+ clearSetting(ctx.db, CONTACT_TELEGRAM_PENDING_HASH_KEY);
54177
+ clearSetting(ctx.db, CONTACT_TELEGRAM_PENDING_EXPIRES_AT_KEY);
54178
+ }
54179
+ return {
54180
+ data: {
54181
+ ok: true,
54182
+ status: status.status,
54183
+ botUsername: status.botUsername
54184
+ }
54185
+ };
54186
+ } catch (error2) {
54187
+ if (error2 instanceof ApiError) {
54188
+ return { status: error2.status, error: error2.message };
54189
+ }
54190
+ return { status: 500, error: "Failed to check Telegram verification status" };
54191
+ }
54192
+ });
54193
+ router.post("/api/contacts/telegram/disconnect", (ctx) => {
54194
+ clearSetting(ctx.db, CONTACT_TELEGRAM_ID_KEY);
54195
+ clearSetting(ctx.db, CONTACT_TELEGRAM_USERNAME_KEY);
54196
+ clearSetting(ctx.db, CONTACT_TELEGRAM_FIRST_NAME_KEY);
54197
+ clearSetting(ctx.db, CONTACT_TELEGRAM_VERIFIED_AT_KEY);
54198
+ clearSetting(ctx.db, CONTACT_TELEGRAM_PENDING_HASH_KEY);
54199
+ clearSetting(ctx.db, CONTACT_TELEGRAM_PENDING_EXPIRES_AT_KEY);
54200
+ void syncCloudContactBindingsSafe(ctx.db);
54201
+ return { data: { ok: true } };
54202
+ });
54203
+ }
54204
+ var import_node_crypto3, EMAIL_VERIFY_CODE_TTL_MINUTES, EMAIL_RESEND_COOLDOWN_SECONDS, EMAIL_MAX_SENDS_PER_HOUR, TELEGRAM_VERIFY_TTL_MINUTES, DEFAULT_TELEGRAM_BOT_USERNAME, CONTACT_EMAIL_KEY, CONTACT_EMAIL_VERIFIED_AT_KEY, CONTACT_EMAIL_CODE_HASH_KEY, CONTACT_EMAIL_CODE_EXPIRES_AT_KEY, CONTACT_EMAIL_LAST_SENT_AT_KEY, CONTACT_EMAIL_RATE_WINDOW_START_KEY, CONTACT_EMAIL_RATE_WINDOW_COUNT_KEY, CONTACT_EMAIL_RELAY_ROOM_ID_KEY, CONTACT_TELEGRAM_ID_KEY, CONTACT_TELEGRAM_USERNAME_KEY, CONTACT_TELEGRAM_FIRST_NAME_KEY, CONTACT_TELEGRAM_VERIFIED_AT_KEY, CONTACT_TELEGRAM_PENDING_HASH_KEY, CONTACT_TELEGRAM_PENDING_EXPIRES_AT_KEY, CONTACT_TELEGRAM_BOT_USERNAME_KEY, ApiError;
54205
+ var init_contacts = __esm({
54206
+ "src/server/routes/contacts.ts"() {
54207
+ "use strict";
54208
+ import_node_crypto3 = __toESM(require("node:crypto"));
54209
+ init_db_queries();
54210
+ init_auth();
54211
+ init_cloud_sync();
54212
+ init_agent_loop();
54213
+ EMAIL_VERIFY_CODE_TTL_MINUTES = 15;
54214
+ EMAIL_RESEND_COOLDOWN_SECONDS = 60;
54215
+ EMAIL_MAX_SENDS_PER_HOUR = 6;
54216
+ TELEGRAM_VERIFY_TTL_MINUTES = 20;
54217
+ DEFAULT_TELEGRAM_BOT_USERNAME = "quoroom_ai_bot";
54218
+ CONTACT_EMAIL_KEY = "contact_email";
54219
+ CONTACT_EMAIL_VERIFIED_AT_KEY = "contact_email_verified_at";
54220
+ CONTACT_EMAIL_CODE_HASH_KEY = "contact_email_verify_code_hash";
54221
+ CONTACT_EMAIL_CODE_EXPIRES_AT_KEY = "contact_email_verify_code_expires_at";
54222
+ CONTACT_EMAIL_LAST_SENT_AT_KEY = "contact_email_verify_last_sent_at";
54223
+ CONTACT_EMAIL_RATE_WINDOW_START_KEY = "contact_email_verify_rate_window_start";
54224
+ CONTACT_EMAIL_RATE_WINDOW_COUNT_KEY = "contact_email_verify_rate_window_count";
54225
+ CONTACT_EMAIL_RELAY_ROOM_ID_KEY = "contact_email_relay_room_id";
54226
+ CONTACT_TELEGRAM_ID_KEY = "contact_telegram_id";
54227
+ CONTACT_TELEGRAM_USERNAME_KEY = "contact_telegram_username";
54228
+ CONTACT_TELEGRAM_FIRST_NAME_KEY = "contact_telegram_first_name";
54229
+ CONTACT_TELEGRAM_VERIFIED_AT_KEY = "contact_telegram_verified_at";
54230
+ CONTACT_TELEGRAM_PENDING_HASH_KEY = "contact_telegram_pending_hash";
54231
+ CONTACT_TELEGRAM_PENDING_EXPIRES_AT_KEY = "contact_telegram_pending_expires_at";
54232
+ CONTACT_TELEGRAM_BOT_USERNAME_KEY = "contact_telegram_bot_username";
54233
+ ApiError = class extends Error {
54234
+ status;
54235
+ retryAfterSec;
54236
+ constructor(meta) {
54237
+ super(meta.message);
54238
+ this.status = meta.status;
54239
+ this.retryAfterSec = meta.retryAfterSec;
54240
+ }
54241
+ };
54242
+ }
54243
+ });
54244
+
53268
54245
  // src/server/runtime.ts
53269
54246
  function getResultsDir() {
53270
54247
  return process.env.QUOROOM_RESULTS_DIR || (0, import_node_path2.join)((0, import_node_os3.homedir)(), APP_NAME, "results");
@@ -53537,6 +54514,13 @@ function startServerRuntime(db3) {
53537
54514
  cloudSyncInFlight = false;
53538
54515
  });
53539
54516
  }, CLOUD_MESSAGE_SYNC_MS);
54517
+ queenInboxTimer = setInterval(() => {
54518
+ if (queenInboxInFlight) return;
54519
+ queenInboxInFlight = true;
54520
+ void pollQueenInbox(db3).finally(() => {
54521
+ queenInboxInFlight = false;
54522
+ });
54523
+ }, QUEEN_INBOX_POLL_MS);
53540
54524
  resumeActiveQueens(db3);
53541
54525
  }
53542
54526
  function makeCycleCallbacks() {
@@ -53565,11 +54549,14 @@ function stopServerRuntime() {
53565
54549
  if (watcherTimer) clearInterval(watcherTimer);
53566
54550
  if (maintenanceTimer) clearInterval(maintenanceTimer);
53567
54551
  if (cloudMessageTimer) clearInterval(cloudMessageTimer);
54552
+ if (queenInboxTimer) clearInterval(queenInboxTimer);
53568
54553
  schedulerTimer = null;
53569
54554
  watcherTimer = null;
53570
54555
  maintenanceTimer = null;
53571
54556
  cloudMessageTimer = null;
54557
+ queenInboxTimer = null;
53572
54558
  cloudSyncInFlight = false;
54559
+ queenInboxInFlight = false;
53573
54560
  for (const [, entry] of cronJobs) {
53574
54561
  entry.job.stop();
53575
54562
  }
@@ -53579,7 +54566,7 @@ function stopServerRuntime() {
53579
54566
  closeWatch(id);
53580
54567
  }
53581
54568
  }
53582
- var import_node_fs2, import_node_path2, import_node_os3, import_node_cron2, SCHEDULER_REFRESH_MS, WATCH_REFRESH_MS, TASK_MAINTENANCE_MS, CLOUD_MESSAGE_SYNC_MS, WATCH_DEBOUNCE_MS, cronJobs, pendingTaskStarts, watchStates, schedulerTimer, watcherTimer, maintenanceTimer, cloudMessageTimer, cloudSyncInFlight;
54569
+ var import_node_fs2, import_node_path2, import_node_os3, import_node_cron2, SCHEDULER_REFRESH_MS, WATCH_REFRESH_MS, TASK_MAINTENANCE_MS, CLOUD_MESSAGE_SYNC_MS, QUEEN_INBOX_POLL_MS, WATCH_DEBOUNCE_MS, cronJobs, pendingTaskStarts, watchStates, schedulerTimer, watcherTimer, maintenanceTimer, cloudMessageTimer, queenInboxTimer, cloudSyncInFlight, queenInboxInFlight;
53583
54570
  var init_runtime = __esm({
53584
54571
  "src/server/runtime.ts"() {
53585
54572
  "use strict";
@@ -53594,11 +54581,13 @@ var init_runtime = __esm({
53594
54581
  init_model_provider();
53595
54582
  init_agent_loop();
53596
54583
  init_cloud_sync();
54584
+ init_contacts();
53597
54585
  init_event_bus();
53598
54586
  SCHEDULER_REFRESH_MS = 15e3;
53599
54587
  WATCH_REFRESH_MS = 15e3;
53600
54588
  TASK_MAINTENANCE_MS = 6e4;
53601
54589
  CLOUD_MESSAGE_SYNC_MS = 6e4;
54590
+ QUEEN_INBOX_POLL_MS = 6e4;
53602
54591
  WATCH_DEBOUNCE_MS = 1500;
53603
54592
  cronJobs = /* @__PURE__ */ new Map();
53604
54593
  pendingTaskStarts = /* @__PURE__ */ new Set();
@@ -53607,7 +54596,9 @@ var init_runtime = __esm({
53607
54596
  watcherTimer = null;
53608
54597
  maintenanceTimer = null;
53609
54598
  cloudMessageTimer = null;
54599
+ queenInboxTimer = null;
53610
54600
  cloudSyncInFlight = false;
54601
+ queenInboxInFlight = false;
53611
54602
  }
53612
54603
  });
53613
54604
 
@@ -53629,11 +54620,13 @@ function registerTaskRoutes(router) {
53629
54620
  }
53630
54621
  const timeoutMinutesRaw = body.timeoutMinutes ?? body.timeout;
53631
54622
  const timeoutMinutes = typeof timeoutMinutesRaw === "number" ? timeoutMinutesRaw : typeof timeoutMinutesRaw === "string" ? Number.parseInt(timeoutMinutesRaw, 10) : void 0;
54623
+ const triggerType = body.triggerType || "manual";
54624
+ const webhookToken = triggerType === "webhook" ? import_node_crypto4.default.randomBytes(16).toString("hex") : void 0;
53632
54625
  const task = createTask(ctx.db, {
53633
54626
  name: body.name || body.prompt.slice(0, 50),
53634
54627
  prompt: body.prompt,
53635
54628
  description: body.description,
53636
- triggerType: body.triggerType || "manual",
54629
+ triggerType,
53637
54630
  cronExpression: body.cronExpression,
53638
54631
  scheduledAt: body.scheduledAt,
53639
54632
  workerId: body.workerId,
@@ -53643,7 +54636,8 @@ function registerTaskRoutes(router) {
53643
54636
  allowedTools: body.allowedTools,
53644
54637
  disallowedTools: body.disallowedTools,
53645
54638
  sessionContinuity: body.sessionContinuity,
53646
- roomId: body.roomId
54639
+ roomId: body.roomId,
54640
+ webhookToken
53647
54641
  });
53648
54642
  eventBus.emit("tasks", "task:created", task);
53649
54643
  return { status: 201, data: task };
@@ -53663,6 +54657,11 @@ function registerTaskRoutes(router) {
53663
54657
  const task = getTask(ctx.db, id);
53664
54658
  if (!task) return { status: 404, error: "Task not found" };
53665
54659
  const body = ctx.body || {};
54660
+ if (body.regenerateWebhookToken) {
54661
+ const newToken = import_node_crypto4.default.randomBytes(16).toString("hex");
54662
+ updateTask(ctx.db, id, { webhookToken: newToken });
54663
+ delete body.regenerateWebhookToken;
54664
+ }
53666
54665
  updateTask(ctx.db, id, body);
53667
54666
  const updated = getTask(ctx.db, id);
53668
54667
  if (updated) eventBus.emit("tasks", "task:updated", updated);
@@ -53717,9 +54716,11 @@ function registerTaskRoutes(router) {
53717
54716
  return { data: { ok: true } };
53718
54717
  });
53719
54718
  }
54719
+ var import_node_crypto4;
53720
54720
  var init_tasks = __esm({
53721
54721
  "src/server/routes/tasks.ts"() {
53722
54722
  "use strict";
54723
+ import_node_crypto4 = __toESM(require("node:crypto"));
53723
54724
  init_db_queries();
53724
54725
  init_event_bus();
53725
54726
  init_runtime();
@@ -54014,7 +55015,7 @@ var init_watches = __esm({
54014
55015
  function ensureKeeperReferralCode(db3) {
54015
55016
  const existing = getSetting(db3, "keeper_referral_code")?.trim();
54016
55017
  if (existing) return existing;
54017
- const generated = (0, import_node_crypto3.randomBytes)(6).toString("base64url").slice(0, 10);
55018
+ const generated = (0, import_node_crypto5.randomBytes)(6).toString("base64url").slice(0, 10);
54018
55019
  setSetting(db3, "keeper_referral_code", generated);
54019
55020
  return generated;
54020
55021
  }
@@ -54046,11 +55047,11 @@ function registerSettingRoutes(router) {
54046
55047
  return { data: { key: ctx.params.key, value: String(body.value) } };
54047
55048
  });
54048
55049
  }
54049
- var import_node_crypto3;
55050
+ var import_node_crypto5;
54050
55051
  var init_settings2 = __esm({
54051
55052
  "src/server/routes/settings.ts"() {
54052
55053
  "use strict";
54053
- import_node_crypto3 = require("node:crypto");
55054
+ import_node_crypto5 = require("node:crypto");
54054
55055
  init_db_queries();
54055
55056
  }
54056
55057
  });
@@ -54264,41 +55265,6 @@ var init_db2 = __esm({
54264
55265
  }
54265
55266
  });
54266
55267
 
54267
- // src/shared/ollama-models.ts
54268
- function stripOllamaPrefix(model) {
54269
- const trimmed = model.trim();
54270
- return trimmed.startsWith("ollama:") ? trimmed.slice("ollama:".length) : trimmed;
54271
- }
54272
- function isSupportedFreeOllamaModel(model) {
54273
- const trimmed = model.trim();
54274
- if (!trimmed) return false;
54275
- return FREE_OLLAMA_MODEL_VALUES.has(trimmed);
54276
- }
54277
- var FREE_OLLAMA_MODEL_DEFS, LEGACY_OLLAMA_MODEL_IDS, FREE_OLLAMA_MODEL_OPTIONS, FREE_OLLAMA_MODEL_VALUES;
54278
- var init_ollama_models = __esm({
54279
- "src/shared/ollama-models.ts"() {
54280
- "use strict";
54281
- FREE_OLLAMA_MODEL_DEFS = [
54282
- { id: "llama3.2", label: "Llama 3.2" },
54283
- { id: "qwen3:14b", label: "Qwen3 14B" },
54284
- { id: "deepseek-r1:14b", label: "DeepSeek R1 14B" },
54285
- { id: "gemma3:12b", label: "Gemma 3 12B" },
54286
- { id: "phi4", label: "Phi-4" }
54287
- ];
54288
- LEGACY_OLLAMA_MODEL_IDS = ["llama3"];
54289
- FREE_OLLAMA_MODEL_OPTIONS = FREE_OLLAMA_MODEL_DEFS.map((model) => ({
54290
- value: `ollama:${model.id}`,
54291
- label: model.label
54292
- }));
54293
- FREE_OLLAMA_MODEL_VALUES = /* @__PURE__ */ new Set([
54294
- ...FREE_OLLAMA_MODEL_OPTIONS.map((model) => model.value),
54295
- ...FREE_OLLAMA_MODEL_DEFS.map((model) => model.id),
54296
- ...LEGACY_OLLAMA_MODEL_IDS.map((model) => `ollama:${model}`),
54297
- ...LEGACY_OLLAMA_MODEL_IDS
54298
- ]);
54299
- }
54300
- });
54301
-
54302
55268
  // src/server/updateChecker.ts
54303
55269
  function isTestTag(tag) {
54304
55270
  return /-test/i.test(tag);
@@ -54402,7 +55368,7 @@ var init_updateChecker = __esm({
54402
55368
  function getVersion3() {
54403
55369
  if (cachedVersion) return cachedVersion;
54404
55370
  try {
54405
- cachedVersion = true ? "0.1.13" : null.version;
55371
+ cachedVersion = true ? "0.1.15" : null.version;
54406
55372
  } catch {
54407
55373
  cachedVersion = "unknown";
54408
55374
  }
@@ -54448,34 +55414,12 @@ function getCodexStatus() {
54448
55414
  scheduleCodexRefresh();
54449
55415
  return cachedCodex;
54450
55416
  }
54451
- async function refreshOllama() {
54452
- const available = await isOllamaAvailable();
54453
- const models = available ? await listOllamaModels() : [];
54454
- cachedOllama = { available, models };
54455
- ollamaCachedAt = Date.now();
54456
- }
54457
- function scheduleOllamaRefresh(force = false) {
54458
- if (!force && ollamaCachedAt > 0 && Date.now() - ollamaCachedAt < OLLAMA_CACHE_MS2) return;
54459
- if (ollamaRefreshInFlight) return;
54460
- ollamaRefreshInFlight = refreshOllama().finally(() => {
54461
- ollamaRefreshInFlight = null;
54462
- });
54463
- }
54464
- function getOllamaStatus() {
54465
- scheduleOllamaRefresh();
54466
- return cachedOllama;
54467
- }
54468
- function resetOllamaCaches() {
54469
- cachedOllama = { available: false, models: [] };
54470
- ollamaCachedAt = 0;
54471
- invalidateOllamaCache();
54472
- }
54473
55417
  function parseStatusParts(raw) {
54474
55418
  if (!raw || !raw.trim()) return null;
54475
55419
  const values = raw.split(",").map((part) => part.trim().toLowerCase()).filter(Boolean);
54476
55420
  const set = /* @__PURE__ */ new Set();
54477
55421
  for (const part of values) {
54478
- if (part === "storage" || part === "providers" || part === "ollama" || part === "resources" || part === "update") {
55422
+ if (part === "storage" || part === "providers" || part === "resources" || part === "update") {
54479
55423
  set.add(part);
54480
55424
  }
54481
55425
  }
@@ -54484,7 +55428,6 @@ function parseStatusParts(raw) {
54484
55428
  function warmStatusCaches() {
54485
55429
  scheduleClaudeRefresh(true);
54486
55430
  scheduleCodexRefresh(true);
54487
- scheduleOllamaRefresh(true);
54488
55431
  }
54489
55432
  function getResources() {
54490
55433
  const [load1, load5] = import_node_os4.default.loadavg();
@@ -54508,36 +55451,6 @@ function registerStatusRoutes(router) {
54508
55451
  await forceCheck();
54509
55452
  return { data: { updateInfo: getUpdateInfo() } };
54510
55453
  });
54511
- router.post("/api/ollama/start", async () => {
54512
- const result = await ensureOllamaRunning();
54513
- resetOllamaCaches();
54514
- scheduleOllamaRefresh(true);
54515
- return { data: result };
54516
- });
54517
- router.post("/api/ollama/ensure-model", async (ctx) => {
54518
- const body = ctx.body || {};
54519
- const requestedModel = typeof body.model === "string" ? body.model.trim() : "";
54520
- if (!requestedModel) return { status: 400, error: "model is required" };
54521
- if (!isSupportedFreeOllamaModel(requestedModel)) {
54522
- return { status: 400, error: `Unsupported free Ollama model: ${requestedModel}` };
54523
- }
54524
- const normalizedModel = stripOllamaPrefix(requestedModel);
54525
- const running = await ensureOllamaRunning();
54526
- if (!running.available) {
54527
- return { status: 500, error: `Ollama unavailable (${running.status})` };
54528
- }
54529
- const installedModels = await listOllamaModels();
54530
- if (isModelInstalled(installedModels, normalizedModel)) {
54531
- resetOllamaCaches();
54532
- scheduleOllamaRefresh(true);
54533
- return { data: { ok: true, status: "ready", model: `ollama:${normalizedModel}` } };
54534
- }
54535
- const pulled = await pullOllamaModel(normalizedModel);
54536
- if (!pulled.ok) return { status: 500, error: `Failed to pull model ${normalizedModel}: ${pulled.error}` };
54537
- resetOllamaCaches();
54538
- scheduleOllamaRefresh(true);
54539
- return { data: { ok: true, status: "pulled", model: `ollama:${normalizedModel}` } };
54540
- });
54541
55454
  router.get("/api/status", (ctx) => {
54542
55455
  const dataDir = getDataDir();
54543
55456
  const dbPath = ctx.db.name;
@@ -54563,10 +55476,6 @@ function registerStatusRoutes(router) {
54563
55476
  pending.claude = claudeRefreshInFlight !== null;
54564
55477
  pending.codex = codexRefreshInFlight !== null;
54565
55478
  }
54566
- if (include("ollama")) {
54567
- data.ollama = getOllamaStatus();
54568
- pending.ollama = ollamaRefreshInFlight !== null;
54569
- }
54570
55479
  if (include("resources")) {
54571
55480
  data.resources = getResources();
54572
55481
  }
@@ -54581,22 +55490,19 @@ function registerStatusRoutes(router) {
54581
55490
  };
54582
55491
  });
54583
55492
  }
54584
- var import_node_os4, import_node_child_process3, import_node_util, startedAt, cachedVersion, execFileAsync, CLI_CACHE_MS, cachedClaude, claudeCachedAt, claudeRefreshInFlight, cachedCodex, codexCachedAt, codexRefreshInFlight, cachedOllama, ollamaCachedAt, OLLAMA_CACHE_MS2, ollamaRefreshInFlight;
55493
+ var import_node_os4, import_node_child_process2, import_node_util, startedAt, cachedVersion, execFileAsync, CLI_CACHE_MS, cachedClaude, claudeCachedAt, claudeRefreshInFlight, cachedCodex, codexCachedAt, codexRefreshInFlight;
54585
55494
  var init_status = __esm({
54586
55495
  "src/server/routes/status.ts"() {
54587
55496
  "use strict";
54588
55497
  import_node_os4 = __toESM(require("node:os"));
54589
- import_node_child_process3 = require("node:child_process");
55498
+ import_node_child_process2 = require("node:child_process");
54590
55499
  import_node_util = require("node:util");
54591
55500
  init_db2();
54592
- init_model_provider();
54593
- init_ollama_models();
54594
- init_ollama_ensure();
54595
55501
  init_updateChecker();
54596
55502
  init_auth();
54597
55503
  startedAt = Date.now();
54598
55504
  cachedVersion = null;
54599
- execFileAsync = (0, import_node_util.promisify)(import_node_child_process3.execFile);
55505
+ execFileAsync = (0, import_node_util.promisify)(import_node_child_process2.execFile);
54600
55506
  CLI_CACHE_MS = 3e4;
54601
55507
  cachedClaude = { available: false };
54602
55508
  claudeCachedAt = 0;
@@ -54604,13 +55510,6 @@ var init_status = __esm({
54604
55510
  cachedCodex = { available: false };
54605
55511
  codexCachedAt = 0;
54606
55512
  codexRefreshInFlight = null;
54607
- cachedOllama = {
54608
- available: false,
54609
- models: []
54610
- };
54611
- ollamaCachedAt = 0;
54612
- OLLAMA_CACHE_MS2 = 3e4;
54613
- ollamaRefreshInFlight = null;
54614
55513
  warmStatusCaches();
54615
55514
  }
54616
55515
  });
@@ -55353,13 +56252,13 @@ function startProviderAuthSession(provider) {
55353
56252
  }
55354
56253
  const cmd = getProviderCommand(provider);
55355
56254
  const displayCommand = [cmd.command, ...cmd.args].join(" ");
55356
- const child = (0, import_node_child_process4.spawn)(cmd.command, cmd.args, {
56255
+ const child = (0, import_node_child_process3.spawn)(cmd.command, cmd.args, {
55357
56256
  stdio: ["pipe", "pipe", "pipe"],
55358
56257
  env: { ...process.env, CI: "1", FORCE_COLOR: "0" }
55359
56258
  });
55360
56259
  const startedAt2 = nowIso();
55361
56260
  const session = {
55362
- sessionId: (0, import_node_crypto4.randomUUID)(),
56261
+ sessionId: (0, import_node_crypto6.randomUUID)(),
55363
56262
  provider,
55364
56263
  command: displayCommand,
55365
56264
  status: "starting",
@@ -55423,12 +56322,12 @@ function startProviderAuthSession(provider) {
55423
56322
  });
55424
56323
  return { session: toSessionView(session, true), reused: false };
55425
56324
  }
55426
- var import_node_child_process4, import_node_crypto4, sessionStore, activeByProvider, MAX_LINES, SESSION_TIMEOUT_MS, SESSION_TTL_MS;
56325
+ var import_node_child_process3, import_node_crypto6, sessionStore, activeByProvider, MAX_LINES, SESSION_TIMEOUT_MS, SESSION_TTL_MS;
55427
56326
  var init_provider_auth = __esm({
55428
56327
  "src/server/provider-auth.ts"() {
55429
56328
  "use strict";
55430
- import_node_child_process4 = require("node:child_process");
55431
- import_node_crypto4 = require("node:crypto");
56329
+ import_node_child_process3 = require("node:child_process");
56330
+ import_node_crypto6 = require("node:crypto");
55432
56331
  init_event_bus();
55433
56332
  sessionStore = /* @__PURE__ */ new Map();
55434
56333
  activeByProvider = /* @__PURE__ */ new Map();
@@ -55441,7 +56340,7 @@ var init_provider_auth = __esm({
55441
56340
  // src/server/provider-cli.ts
55442
56341
  function safeExec(cmd, args2) {
55443
56342
  try {
55444
- const stdout = (0, import_node_child_process5.execFileSync)(cmd, args2, { timeout: CLI_PROBE_TIMEOUT_MS, stdio: ["ignore", "pipe", "pipe"] }).toString().trim();
56343
+ const stdout = (0, import_node_child_process4.execFileSync)(cmd, args2, { timeout: CLI_PROBE_TIMEOUT_MS, stdio: ["ignore", "pipe", "pipe"] }).toString().trim();
55445
56344
  return { ok: true, stdout, stderr: "" };
55446
56345
  } catch (err) {
55447
56346
  const e = err;
@@ -55475,11 +56374,11 @@ function disconnectProvider(provider) {
55475
56374
  result: safeExec(provider, args2)
55476
56375
  };
55477
56376
  }
55478
- var import_node_child_process5, CLI_PROBE_TIMEOUT_MS;
56377
+ var import_node_child_process4, CLI_PROBE_TIMEOUT_MS;
55479
56378
  var init_provider_cli = __esm({
55480
56379
  "src/server/provider-cli.ts"() {
55481
56380
  "use strict";
55482
- import_node_child_process5 = require("node:child_process");
56381
+ import_node_child_process4 = require("node:child_process");
55483
56382
  CLI_PROBE_TIMEOUT_MS = 1500;
55484
56383
  }
55485
56384
  });
@@ -55502,7 +56401,7 @@ function getProviderInstallCommand(provider, platform = process.platform) {
55502
56401
  function addGlobalNpmBinToPath(platform = process.platform) {
55503
56402
  const npmCommand = getNpmCommand(platform);
55504
56403
  try {
55505
- const npmBin = (0, import_node_child_process6.execFileSync)(npmCommand, ["bin", "-g"], { timeout: 5e3, stdio: ["ignore", "pipe", "ignore"] }).toString().trim();
56404
+ const npmBin = (0, import_node_child_process5.execFileSync)(npmCommand, ["bin", "-g"], { timeout: 5e3, stdio: ["ignore", "pipe", "ignore"] }).toString().trim();
55506
56405
  if (!npmBin) return;
55507
56406
  const currentPath = process.env.PATH || "";
55508
56407
  const parts = currentPath.split(import_node_path3.default.delimiter).filter(Boolean);
@@ -55663,13 +56562,13 @@ function startProviderInstallSession(provider) {
55663
56562
  }
55664
56563
  const cmd = getProviderInstallCommand(provider);
55665
56564
  const displayCommand = [cmd.command, ...cmd.args].join(" ");
55666
- const child = (0, import_node_child_process6.spawn)(cmd.command, cmd.args, {
56565
+ const child = (0, import_node_child_process5.spawn)(cmd.command, cmd.args, {
55667
56566
  stdio: ["pipe", "pipe", "pipe"],
55668
56567
  env: { ...process.env, CI: "1", FORCE_COLOR: "0" }
55669
56568
  });
55670
56569
  const startedAt2 = nowIso2();
55671
56570
  const session = {
55672
- sessionId: (0, import_node_crypto5.randomUUID)(),
56571
+ sessionId: (0, import_node_crypto7.randomUUID)(),
55673
56572
  provider,
55674
56573
  command: displayCommand,
55675
56574
  status: "starting",
@@ -55742,12 +56641,12 @@ function startProviderInstallSession(provider) {
55742
56641
  });
55743
56642
  return { session: toSessionView2(session, true), reused: false };
55744
56643
  }
55745
- var import_node_child_process6, import_node_crypto5, import_node_path3, sessionStore2, activeByProvider2, MAX_LINES2, SESSION_TIMEOUT_MS2, SESSION_TTL_MS2;
56644
+ var import_node_child_process5, import_node_crypto7, import_node_path3, sessionStore2, activeByProvider2, MAX_LINES2, SESSION_TIMEOUT_MS2, SESSION_TTL_MS2;
55746
56645
  var init_provider_install = __esm({
55747
56646
  "src/server/provider-install.ts"() {
55748
56647
  "use strict";
55749
- import_node_child_process6 = require("node:child_process");
55750
- import_node_crypto5 = require("node:crypto");
56648
+ import_node_child_process5 = require("node:child_process");
56649
+ import_node_crypto7 = require("node:crypto");
55751
56650
  import_node_path3 = __toESM(require("node:path"));
55752
56651
  init_event_bus();
55753
56652
  init_provider_cli();
@@ -55933,453 +56832,6 @@ var init_providers = __esm({
55933
56832
  }
55934
56833
  });
55935
56834
 
55936
- // src/server/routes/contacts.ts
55937
- function getSettingTrimmed(db3, key) {
55938
- return (getSetting(db3, key) ?? "").trim();
55939
- }
55940
- function setSetting2(db3, key, value) {
55941
- setSetting(db3, key, value);
55942
- }
55943
- function clearSetting(db3, key) {
55944
- setSetting(db3, key, "");
55945
- }
55946
- function parseIsoToMs(value) {
55947
- if (!value) return null;
55948
- const timestamp = Date.parse(value);
55949
- if (!Number.isFinite(timestamp)) return null;
55950
- return timestamp;
55951
- }
55952
- function parseInteger(value, fallback = 0) {
55953
- const parsed = Number.parseInt(value, 10);
55954
- return Number.isFinite(parsed) ? parsed : fallback;
55955
- }
55956
- function getContactSecret() {
55957
- const configured = (process.env.QUOROOM_CONTACT_SECRET || "").trim();
55958
- if (configured) return configured;
55959
- try {
55960
- return getToken();
55961
- } catch {
55962
- return "quoroom-contact-secret";
55963
- }
55964
- }
55965
- function hashEmailCode(email2, code) {
55966
- return import_node_crypto6.default.createHmac("sha256", getContactSecret()).update(`email:${email2.toLowerCase()}
55967
- code:${code}`).digest("hex");
55968
- }
55969
- function hashTelegramToken(token) {
55970
- return import_node_crypto6.default.createHash("sha256").update(token).digest("hex");
55971
- }
55972
- function hashesEqualHex(a, b) {
55973
- if (!/^[a-f0-9]{64}$/i.test(a) || !/^[a-f0-9]{64}$/i.test(b)) return false;
55974
- const left = Buffer.from(a, "hex");
55975
- const right = Buffer.from(b, "hex");
55976
- return left.length === right.length && import_node_crypto6.default.timingSafeEqual(left, right);
55977
- }
55978
- function getCloudApiBase() {
55979
- return (process.env.QUOROOM_CLOUD_API ?? "https://quoroom.ai/api").replace(/\/+$/, "");
55980
- }
55981
- function getTelegramBotUsername() {
55982
- const configured = (process.env.QUOROOM_TELEGRAM_BOT_USERNAME || "").trim().replace(/^@+/, "");
55983
- return configured || DEFAULT_TELEGRAM_BOT_USERNAME;
55984
- }
55985
- function normalizeBotUsername(input) {
55986
- if (typeof input !== "string") return getTelegramBotUsername();
55987
- const value = input.trim().replace(/^@+/, "");
55988
- return value || getTelegramBotUsername();
55989
- }
55990
- function isValidEmail(input) {
55991
- if (!input || input.length > 320) return false;
55992
- return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(input);
55993
- }
55994
- function emailSendGate(db3, nowMs) {
55995
- const lastSentAt = parseIsoToMs(getSettingTrimmed(db3, CONTACT_EMAIL_LAST_SENT_AT_KEY));
55996
- if (lastSentAt != null) {
55997
- const retryMs = EMAIL_RESEND_COOLDOWN_SECONDS * 1e3 - (nowMs - lastSentAt);
55998
- if (retryMs > 0) {
55999
- throw new ApiError({
56000
- status: 429,
56001
- message: `Please wait ${Math.ceil(retryMs / 1e3)}s before resending.`,
56002
- retryAfterSec: Math.ceil(retryMs / 1e3)
56003
- });
56004
- }
56005
- }
56006
- let windowStartMs = parseIsoToMs(getSettingTrimmed(db3, CONTACT_EMAIL_RATE_WINDOW_START_KEY));
56007
- let windowCount = parseInteger(getSettingTrimmed(db3, CONTACT_EMAIL_RATE_WINDOW_COUNT_KEY), 0);
56008
- if (windowStartMs == null || nowMs - windowStartMs >= 60 * 60 * 1e3) {
56009
- windowStartMs = nowMs;
56010
- windowCount = 0;
56011
- }
56012
- if (windowCount >= EMAIL_MAX_SENDS_PER_HOUR) {
56013
- const retryMs = Math.max(1e3, 60 * 60 * 1e3 - (nowMs - windowStartMs));
56014
- throw new ApiError({
56015
- status: 429,
56016
- message: "Too many verification emails sent. Please try again later.",
56017
- retryAfterSec: Math.ceil(retryMs / 1e3)
56018
- });
56019
- }
56020
- return { windowStartMs, windowCount };
56021
- }
56022
- async function sendVerificationCodeEmail(email2, code) {
56023
- const apiKey = (process.env.QUOROOM_RESEND_API_KEY || process.env.RESEND_API_KEY || "").trim();
56024
- if (!apiKey) {
56025
- throw new ApiError({ status: 503, message: "Email provider is not configured (RESEND_API_KEY)." });
56026
- }
56027
- const fromEmail = (process.env.QUOROOM_RESEND_FROM_EMAIL || process.env.RESEND_FROM_EMAIL || "Quoroom <noreply@quoroom.ai>").trim();
56028
- const ttlLabel = `${EMAIL_VERIFY_CODE_TTL_MINUTES} minutes`;
56029
- const subject = "Your Quoroom verification code";
56030
- const text = [
56031
- "Your Quoroom email verification code:",
56032
- code,
56033
- "",
56034
- `This code expires in ${ttlLabel}.`,
56035
- "",
56036
- "If you did not request this code, you can ignore this email."
56037
- ].join("\n");
56038
- const html = [
56039
- '<div style="font-family:Arial,sans-serif;line-height:1.45;">',
56040
- "<p>Your Quoroom email verification code:</p>",
56041
- `<p style="font-size:24px;font-weight:700;letter-spacing:2px;margin:12px 0;">${code}</p>`,
56042
- `<p>This code expires in ${ttlLabel}.</p>`,
56043
- '<p style="color:#666;">If you did not request this code, you can ignore this email.</p>',
56044
- "</div>"
56045
- ].join("");
56046
- const res = await fetch("https://api.resend.com/emails", {
56047
- method: "POST",
56048
- headers: {
56049
- "Authorization": `Bearer ${apiKey}`,
56050
- "Content-Type": "application/json"
56051
- },
56052
- body: JSON.stringify({
56053
- from: fromEmail,
56054
- to: [email2],
56055
- subject,
56056
- text,
56057
- html
56058
- }),
56059
- signal: AbortSignal.timeout(12e3)
56060
- });
56061
- if (!res.ok) {
56062
- const details = await res.text().catch(() => "");
56063
- throw new ApiError({
56064
- status: 502,
56065
- message: `Failed to send verification email (${res.status}). ${details.slice(0, 180)}`.trim()
56066
- });
56067
- }
56068
- }
56069
- async function issueEmailVerification(db3, email2) {
56070
- const nowMs = Date.now();
56071
- const { windowStartMs, windowCount } = emailSendGate(db3, nowMs);
56072
- const code = import_node_crypto6.default.randomInt(0, 1e6).toString().padStart(6, "0");
56073
- await sendVerificationCodeEmail(email2, code);
56074
- const nowIso3 = new Date(nowMs).toISOString();
56075
- const expiresAt = new Date(nowMs + EMAIL_VERIFY_CODE_TTL_MINUTES * 60 * 1e3).toISOString();
56076
- const codeHash = hashEmailCode(email2, code);
56077
- setSetting2(db3, CONTACT_EMAIL_KEY, email2);
56078
- clearSetting(db3, CONTACT_EMAIL_VERIFIED_AT_KEY);
56079
- setSetting2(db3, CONTACT_EMAIL_CODE_HASH_KEY, codeHash);
56080
- setSetting2(db3, CONTACT_EMAIL_CODE_EXPIRES_AT_KEY, expiresAt);
56081
- setSetting2(db3, CONTACT_EMAIL_LAST_SENT_AT_KEY, nowIso3);
56082
- setSetting2(db3, CONTACT_EMAIL_RATE_WINDOW_START_KEY, new Date(windowStartMs).toISOString());
56083
- setSetting2(db3, CONTACT_EMAIL_RATE_WINDOW_COUNT_KEY, String(windowCount + 1));
56084
- return {
56085
- sentTo: email2,
56086
- expiresAt,
56087
- retryAfterSec: EMAIL_RESEND_COOLDOWN_SECONDS
56088
- };
56089
- }
56090
- async function startCloudTelegramVerification(tokenHash, expiresAt) {
56091
- const res = await fetch(`${getCloudApiBase()}/telegram/verify/start`, {
56092
- method: "POST",
56093
- headers: { "Content-Type": "application/json" },
56094
- body: JSON.stringify({ tokenHash, expiresAt }),
56095
- signal: AbortSignal.timeout(1e4)
56096
- });
56097
- if (!res.ok) {
56098
- const details = await res.text().catch(() => "");
56099
- throw new ApiError({
56100
- status: 502,
56101
- message: `Telegram verification service unavailable (${res.status}). ${details.slice(0, 180)}`.trim()
56102
- });
56103
- }
56104
- const payload = await res.json().catch(() => ({}));
56105
- return normalizeBotUsername(payload.botUsername);
56106
- }
56107
- async function fetchCloudTelegramVerificationStatus(tokenHash) {
56108
- const fallbackBot = getTelegramBotUsername();
56109
- const res = await fetch(`${getCloudApiBase()}/telegram/verify/status/${encodeURIComponent(tokenHash)}`, {
56110
- method: "GET",
56111
- signal: AbortSignal.timeout(1e4)
56112
- });
56113
- if (!res.ok) {
56114
- const details = await res.text().catch(() => "");
56115
- throw new ApiError({
56116
- status: 502,
56117
- message: `Telegram verification status unavailable (${res.status}). ${details.slice(0, 180)}`.trim()
56118
- });
56119
- }
56120
- const payload = await res.json().catch(() => ({}));
56121
- const status = payload.status;
56122
- const normalizedStatus = status === "verified" ? "verified" : status === "expired" ? "expired" : status === "missing" ? "missing" : "pending";
56123
- const telegramId = payload.telegram?.id == null ? null : String(payload.telegram.id);
56124
- return {
56125
- status: normalizedStatus,
56126
- botUsername: normalizeBotUsername(payload.botUsername ?? fallbackBot),
56127
- telegramId,
56128
- username: payload.telegram?.username ?? null,
56129
- firstName: payload.telegram?.firstName ?? null,
56130
- verifiedAt: payload.telegram?.verifiedAt ?? null
56131
- };
56132
- }
56133
- function getContactsStatus(db3) {
56134
- const nowMs = Date.now();
56135
- const email2 = getSettingTrimmed(db3, CONTACT_EMAIL_KEY) || null;
56136
- const emailVerifiedAt = getSettingTrimmed(db3, CONTACT_EMAIL_VERIFIED_AT_KEY) || null;
56137
- const emailExpiresAtRaw = getSettingTrimmed(db3, CONTACT_EMAIL_CODE_EXPIRES_AT_KEY);
56138
- const emailExpiresAtMs = parseIsoToMs(emailExpiresAtRaw);
56139
- const emailPending = Boolean(getSettingTrimmed(db3, CONTACT_EMAIL_CODE_HASH_KEY)) && emailExpiresAtMs != null && emailExpiresAtMs > nowMs;
56140
- const emailLastSentMs = parseIsoToMs(getSettingTrimmed(db3, CONTACT_EMAIL_LAST_SENT_AT_KEY));
56141
- const emailRetryAfterSec = emailLastSentMs == null ? 0 : Math.max(0, Math.ceil((emailLastSentMs + EMAIL_RESEND_COOLDOWN_SECONDS * 1e3 - nowMs) / 1e3));
56142
- const telegramId = getSettingTrimmed(db3, CONTACT_TELEGRAM_ID_KEY) || null;
56143
- const telegramUsername = getSettingTrimmed(db3, CONTACT_TELEGRAM_USERNAME_KEY) || null;
56144
- const telegramFirstName = getSettingTrimmed(db3, CONTACT_TELEGRAM_FIRST_NAME_KEY) || null;
56145
- const telegramVerifiedAt = getSettingTrimmed(db3, CONTACT_TELEGRAM_VERIFIED_AT_KEY) || null;
56146
- const telegramPendingHash = getSettingTrimmed(db3, CONTACT_TELEGRAM_PENDING_HASH_KEY);
56147
- const telegramPendingExpiresAtRaw = getSettingTrimmed(db3, CONTACT_TELEGRAM_PENDING_EXPIRES_AT_KEY);
56148
- const telegramPendingExpiresAtMs = parseIsoToMs(telegramPendingExpiresAtRaw);
56149
- const telegramPending = telegramPendingHash.length > 0 && telegramPendingExpiresAtMs != null && telegramPendingExpiresAtMs > nowMs;
56150
- const telegramBotUsername = normalizeBotUsername(
56151
- getSettingTrimmed(db3, CONTACT_TELEGRAM_BOT_USERNAME_KEY) || getTelegramBotUsername()
56152
- );
56153
- return {
56154
- deploymentMode: isCloudDeployment() ? "cloud" : "local",
56155
- email: {
56156
- value: email2,
56157
- verified: Boolean(emailVerifiedAt),
56158
- verifiedAt: emailVerifiedAt,
56159
- pending: emailPending,
56160
- pendingExpiresAt: emailPending ? telegramSafeIso(emailExpiresAtRaw) : null,
56161
- resendRetryAfterSec: emailRetryAfterSec
56162
- },
56163
- telegram: {
56164
- id: telegramId,
56165
- username: telegramUsername,
56166
- firstName: telegramFirstName,
56167
- verified: Boolean(telegramVerifiedAt),
56168
- verifiedAt: telegramVerifiedAt,
56169
- pending: telegramPending,
56170
- pendingExpiresAt: telegramPending ? telegramSafeIso(telegramPendingExpiresAtRaw) : null,
56171
- botUsername: telegramBotUsername
56172
- }
56173
- };
56174
- }
56175
- function telegramSafeIso(value) {
56176
- return parseIsoToMs(value) == null ? null : value;
56177
- }
56178
- function registerContactRoutes(router) {
56179
- router.get("/api/contacts/status", (ctx) => {
56180
- return { data: getContactsStatus(ctx.db) };
56181
- });
56182
- router.post("/api/contacts/email/start", async (ctx) => {
56183
- const body = ctx.body ?? {};
56184
- const emailRaw = typeof body.email === "string" ? body.email.trim().toLowerCase() : "";
56185
- if (!isValidEmail(emailRaw)) {
56186
- return { status: 400, error: "Valid email is required" };
56187
- }
56188
- const currentEmail = getSettingTrimmed(ctx.db, CONTACT_EMAIL_KEY).toLowerCase();
56189
- const verifiedAt = getSettingTrimmed(ctx.db, CONTACT_EMAIL_VERIFIED_AT_KEY);
56190
- if (currentEmail === emailRaw && verifiedAt) {
56191
- return { data: { ok: true, alreadyVerified: true, email: emailRaw } };
56192
- }
56193
- try {
56194
- const result = await issueEmailVerification(ctx.db, emailRaw);
56195
- return { data: { ok: true, ...result } };
56196
- } catch (error2) {
56197
- if (error2 instanceof ApiError) {
56198
- return { status: error2.status, error: error2.message };
56199
- }
56200
- return { status: 500, error: "Failed to send verification email" };
56201
- }
56202
- });
56203
- router.post("/api/contacts/email/resend", async (ctx) => {
56204
- const email2 = getSettingTrimmed(ctx.db, CONTACT_EMAIL_KEY).toLowerCase();
56205
- if (!isValidEmail(email2)) {
56206
- return { status: 400, error: "No email to resend verification to" };
56207
- }
56208
- const verifiedAt = getSettingTrimmed(ctx.db, CONTACT_EMAIL_VERIFIED_AT_KEY);
56209
- if (verifiedAt) {
56210
- return { data: { ok: true, alreadyVerified: true, email: email2 } };
56211
- }
56212
- try {
56213
- const result = await issueEmailVerification(ctx.db, email2);
56214
- return { data: { ok: true, ...result } };
56215
- } catch (error2) {
56216
- if (error2 instanceof ApiError) {
56217
- return { status: error2.status, error: error2.message };
56218
- }
56219
- return { status: 500, error: "Failed to resend verification email" };
56220
- }
56221
- });
56222
- router.post("/api/contacts/email/verify", (ctx) => {
56223
- const body = ctx.body ?? {};
56224
- const code = typeof body.code === "string" ? body.code.trim() : "";
56225
- if (!/^\d{6}$/.test(code)) {
56226
- return { status: 400, error: "Verification code must be 6 digits" };
56227
- }
56228
- const email2 = getSettingTrimmed(ctx.db, CONTACT_EMAIL_KEY).toLowerCase();
56229
- const storedHash = getSettingTrimmed(ctx.db, CONTACT_EMAIL_CODE_HASH_KEY).toLowerCase();
56230
- const expiresAtRaw = getSettingTrimmed(ctx.db, CONTACT_EMAIL_CODE_EXPIRES_AT_KEY);
56231
- const expiresAtMs = parseIsoToMs(expiresAtRaw);
56232
- if (!isValidEmail(email2) || !storedHash || expiresAtMs == null) {
56233
- return { status: 400, error: "No pending verification code. Request a new code first." };
56234
- }
56235
- if (expiresAtMs <= Date.now()) {
56236
- clearSetting(ctx.db, CONTACT_EMAIL_CODE_HASH_KEY);
56237
- clearSetting(ctx.db, CONTACT_EMAIL_CODE_EXPIRES_AT_KEY);
56238
- return { status: 400, error: "Verification code expired. Request a new code." };
56239
- }
56240
- const expectedHash = hashEmailCode(email2, code).toLowerCase();
56241
- if (!hashesEqualHex(storedHash, expectedHash)) {
56242
- return { status: 400, error: "Invalid verification code" };
56243
- }
56244
- const verifiedAt = (/* @__PURE__ */ new Date()).toISOString();
56245
- setSetting2(ctx.db, CONTACT_EMAIL_VERIFIED_AT_KEY, verifiedAt);
56246
- clearSetting(ctx.db, CONTACT_EMAIL_CODE_HASH_KEY);
56247
- clearSetting(ctx.db, CONTACT_EMAIL_CODE_EXPIRES_AT_KEY);
56248
- return {
56249
- data: {
56250
- ok: true,
56251
- email: email2,
56252
- verifiedAt
56253
- }
56254
- };
56255
- });
56256
- router.post("/api/contacts/telegram/start", async (ctx) => {
56257
- const token = import_node_crypto6.default.randomBytes(24).toString("base64url");
56258
- const tokenHash = hashTelegramToken(token);
56259
- const expiresAt = new Date(Date.now() + TELEGRAM_VERIFY_TTL_MINUTES * 60 * 1e3).toISOString();
56260
- try {
56261
- const botUsername = await startCloudTelegramVerification(tokenHash, expiresAt);
56262
- setSetting2(ctx.db, CONTACT_TELEGRAM_PENDING_HASH_KEY, tokenHash);
56263
- setSetting2(ctx.db, CONTACT_TELEGRAM_PENDING_EXPIRES_AT_KEY, expiresAt);
56264
- setSetting2(ctx.db, CONTACT_TELEGRAM_BOT_USERNAME_KEY, botUsername);
56265
- const deepLink = `https://t.me/${encodeURIComponent(botUsername)}?start=${encodeURIComponent(`tv1_${token}`)}`;
56266
- return {
56267
- data: {
56268
- ok: true,
56269
- pending: true,
56270
- expiresAt,
56271
- botUsername,
56272
- deepLink
56273
- }
56274
- };
56275
- } catch (error2) {
56276
- if (error2 instanceof ApiError) {
56277
- return { status: error2.status, error: error2.message };
56278
- }
56279
- return { status: 500, error: "Failed to start Telegram verification" };
56280
- }
56281
- });
56282
- router.post("/api/contacts/telegram/check", async (ctx) => {
56283
- const tokenHash = getSettingTrimmed(ctx.db, CONTACT_TELEGRAM_PENDING_HASH_KEY).toLowerCase();
56284
- const expiresAtRaw = getSettingTrimmed(ctx.db, CONTACT_TELEGRAM_PENDING_EXPIRES_AT_KEY);
56285
- const expiresAtMs = parseIsoToMs(expiresAtRaw);
56286
- if (!/^[a-f0-9]{64}$/.test(tokenHash) || expiresAtMs == null) {
56287
- return { data: { ok: true, status: "not_pending" } };
56288
- }
56289
- if (expiresAtMs <= Date.now()) {
56290
- clearSetting(ctx.db, CONTACT_TELEGRAM_PENDING_HASH_KEY);
56291
- clearSetting(ctx.db, CONTACT_TELEGRAM_PENDING_EXPIRES_AT_KEY);
56292
- return { data: { ok: true, status: "expired" } };
56293
- }
56294
- try {
56295
- const status = await fetchCloudTelegramVerificationStatus(tokenHash);
56296
- setSetting2(ctx.db, CONTACT_TELEGRAM_BOT_USERNAME_KEY, status.botUsername);
56297
- if (status.status === "verified" && status.telegramId) {
56298
- setSetting2(ctx.db, CONTACT_TELEGRAM_ID_KEY, status.telegramId);
56299
- setSetting2(ctx.db, CONTACT_TELEGRAM_USERNAME_KEY, status.username ?? "");
56300
- setSetting2(ctx.db, CONTACT_TELEGRAM_FIRST_NAME_KEY, status.firstName ?? "");
56301
- setSetting2(ctx.db, CONTACT_TELEGRAM_VERIFIED_AT_KEY, status.verifiedAt ?? (/* @__PURE__ */ new Date()).toISOString());
56302
- clearSetting(ctx.db, CONTACT_TELEGRAM_PENDING_HASH_KEY);
56303
- clearSetting(ctx.db, CONTACT_TELEGRAM_PENDING_EXPIRES_AT_KEY);
56304
- return {
56305
- data: {
56306
- ok: true,
56307
- status: "verified",
56308
- telegram: {
56309
- id: status.telegramId,
56310
- username: status.username,
56311
- firstName: status.firstName,
56312
- verifiedAt: status.verifiedAt
56313
- }
56314
- }
56315
- };
56316
- }
56317
- if (status.status === "expired" || status.status === "missing") {
56318
- clearSetting(ctx.db, CONTACT_TELEGRAM_PENDING_HASH_KEY);
56319
- clearSetting(ctx.db, CONTACT_TELEGRAM_PENDING_EXPIRES_AT_KEY);
56320
- }
56321
- return {
56322
- data: {
56323
- ok: true,
56324
- status: status.status,
56325
- botUsername: status.botUsername
56326
- }
56327
- };
56328
- } catch (error2) {
56329
- if (error2 instanceof ApiError) {
56330
- return { status: error2.status, error: error2.message };
56331
- }
56332
- return { status: 500, error: "Failed to check Telegram verification status" };
56333
- }
56334
- });
56335
- router.post("/api/contacts/telegram/disconnect", (ctx) => {
56336
- clearSetting(ctx.db, CONTACT_TELEGRAM_ID_KEY);
56337
- clearSetting(ctx.db, CONTACT_TELEGRAM_USERNAME_KEY);
56338
- clearSetting(ctx.db, CONTACT_TELEGRAM_FIRST_NAME_KEY);
56339
- clearSetting(ctx.db, CONTACT_TELEGRAM_VERIFIED_AT_KEY);
56340
- clearSetting(ctx.db, CONTACT_TELEGRAM_PENDING_HASH_KEY);
56341
- clearSetting(ctx.db, CONTACT_TELEGRAM_PENDING_EXPIRES_AT_KEY);
56342
- return { data: { ok: true } };
56343
- });
56344
- }
56345
- var import_node_crypto6, EMAIL_VERIFY_CODE_TTL_MINUTES, EMAIL_RESEND_COOLDOWN_SECONDS, EMAIL_MAX_SENDS_PER_HOUR, TELEGRAM_VERIFY_TTL_MINUTES, DEFAULT_TELEGRAM_BOT_USERNAME, CONTACT_EMAIL_KEY, CONTACT_EMAIL_VERIFIED_AT_KEY, CONTACT_EMAIL_CODE_HASH_KEY, CONTACT_EMAIL_CODE_EXPIRES_AT_KEY, CONTACT_EMAIL_LAST_SENT_AT_KEY, CONTACT_EMAIL_RATE_WINDOW_START_KEY, CONTACT_EMAIL_RATE_WINDOW_COUNT_KEY, CONTACT_TELEGRAM_ID_KEY, CONTACT_TELEGRAM_USERNAME_KEY, CONTACT_TELEGRAM_FIRST_NAME_KEY, CONTACT_TELEGRAM_VERIFIED_AT_KEY, CONTACT_TELEGRAM_PENDING_HASH_KEY, CONTACT_TELEGRAM_PENDING_EXPIRES_AT_KEY, CONTACT_TELEGRAM_BOT_USERNAME_KEY, ApiError;
56346
- var init_contacts = __esm({
56347
- "src/server/routes/contacts.ts"() {
56348
- "use strict";
56349
- import_node_crypto6 = __toESM(require("node:crypto"));
56350
- init_db_queries();
56351
- init_auth();
56352
- EMAIL_VERIFY_CODE_TTL_MINUTES = 15;
56353
- EMAIL_RESEND_COOLDOWN_SECONDS = 60;
56354
- EMAIL_MAX_SENDS_PER_HOUR = 6;
56355
- TELEGRAM_VERIFY_TTL_MINUTES = 20;
56356
- DEFAULT_TELEGRAM_BOT_USERNAME = "quoroom_ai_bot";
56357
- CONTACT_EMAIL_KEY = "contact_email";
56358
- CONTACT_EMAIL_VERIFIED_AT_KEY = "contact_email_verified_at";
56359
- CONTACT_EMAIL_CODE_HASH_KEY = "contact_email_verify_code_hash";
56360
- CONTACT_EMAIL_CODE_EXPIRES_AT_KEY = "contact_email_verify_code_expires_at";
56361
- CONTACT_EMAIL_LAST_SENT_AT_KEY = "contact_email_verify_last_sent_at";
56362
- CONTACT_EMAIL_RATE_WINDOW_START_KEY = "contact_email_verify_rate_window_start";
56363
- CONTACT_EMAIL_RATE_WINDOW_COUNT_KEY = "contact_email_verify_rate_window_count";
56364
- CONTACT_TELEGRAM_ID_KEY = "contact_telegram_id";
56365
- CONTACT_TELEGRAM_USERNAME_KEY = "contact_telegram_username";
56366
- CONTACT_TELEGRAM_FIRST_NAME_KEY = "contact_telegram_first_name";
56367
- CONTACT_TELEGRAM_VERIFIED_AT_KEY = "contact_telegram_verified_at";
56368
- CONTACT_TELEGRAM_PENDING_HASH_KEY = "contact_telegram_pending_hash";
56369
- CONTACT_TELEGRAM_PENDING_EXPIRES_AT_KEY = "contact_telegram_pending_expires_at";
56370
- CONTACT_TELEGRAM_BOT_USERNAME_KEY = "contact_telegram_bot_username";
56371
- ApiError = class extends Error {
56372
- status;
56373
- retryAfterSec;
56374
- constructor(meta) {
56375
- super(meta.message);
56376
- this.status = meta.status;
56377
- this.retryAfterSec = meta.retryAfterSec;
56378
- }
56379
- };
56380
- }
56381
- });
56382
-
56383
56835
  // src/server/routes/index.ts
56384
56836
  function registerAllRoutes(router) {
56385
56837
  registerRoomRoutes(router);
@@ -58628,10 +59080,10 @@ var require_websocket = __commonJS({
58628
59080
  "use strict";
58629
59081
  var EventEmitter = require("events");
58630
59082
  var https3 = require("https");
58631
- var http4 = require("http");
59083
+ var http3 = require("http");
58632
59084
  var net = require("net");
58633
59085
  var tls = require("tls");
58634
- var { randomBytes: randomBytes4, createHash: createHash3 } = require("crypto");
59086
+ var { randomBytes: randomBytes5, createHash: createHash3 } = require("crypto");
58635
59087
  var { Duplex, Readable } = require("stream");
58636
59088
  var { URL: URL4 } = require("url");
58637
59089
  var PerMessageDeflate = require_permessage_deflate();
@@ -59158,8 +59610,8 @@ var require_websocket = __commonJS({
59158
59610
  }
59159
59611
  }
59160
59612
  const defaultPort = isSecure ? 443 : 80;
59161
- const key = randomBytes4(16).toString("base64");
59162
- const request = isSecure ? https3.request : http4.request;
59613
+ const key = randomBytes5(16).toString("base64");
59614
+ const request = isSecure ? https3.request : http3.request;
59163
59615
  const protocolSet = /* @__PURE__ */ new Set();
59164
59616
  let perMessageDeflate;
59165
59617
  opts.createConnection = opts.createConnection || (isSecure ? tlsConnect : netConnect);
@@ -59653,7 +60105,7 @@ var require_websocket_server = __commonJS({
59653
60105
  "node_modules/ws/lib/websocket-server.js"(exports2, module2) {
59654
60106
  "use strict";
59655
60107
  var EventEmitter = require("events");
59656
- var http4 = require("http");
60108
+ var http3 = require("http");
59657
60109
  var { Duplex } = require("stream");
59658
60110
  var { createHash: createHash3 } = require("crypto");
59659
60111
  var extension = require_extension();
@@ -59724,8 +60176,8 @@ var require_websocket_server = __commonJS({
59724
60176
  );
59725
60177
  }
59726
60178
  if (options.port != null) {
59727
- this._server = http4.createServer((req, res) => {
59728
- const body = http4.STATUS_CODES[426];
60179
+ this._server = http3.createServer((req, res) => {
60180
+ const body = http3.STATUS_CODES[426];
59729
60181
  res.writeHead(426, {
59730
60182
  "Content-Length": body.length,
59731
60183
  "Content-Type": "text/plain"
@@ -60012,7 +60464,7 @@ var require_websocket_server = __commonJS({
60012
60464
  this.destroy();
60013
60465
  }
60014
60466
  function abortHandshake(socket, code, message, headers) {
60015
- message = message || http4.STATUS_CODES[code];
60467
+ message = message || http3.STATUS_CODES[code];
60016
60468
  headers = {
60017
60469
  Connection: "close",
60018
60470
  "Content-Type": "text/html",
@@ -60021,7 +60473,7 @@ var require_websocket_server = __commonJS({
60021
60473
  };
60022
60474
  socket.once("finish", socket.destroy);
60023
60475
  socket.end(
60024
- `HTTP/1.1 ${code} ${http4.STATUS_CODES[code]}\r
60476
+ `HTTP/1.1 ${code} ${http3.STATUS_CODES[code]}\r
60025
60477
  ` + Object.keys(headers).map((h) => `${h}: ${headers[h]}`).join("\r\n") + "\r\n\r\n" + message
60026
60478
  );
60027
60479
  }
@@ -60175,6 +60627,82 @@ var init_ws = __esm({
60175
60627
  }
60176
60628
  });
60177
60629
 
60630
+ // src/server/webhooks.ts
60631
+ function checkWebhookRateLimit(token) {
60632
+ const now = Date.now();
60633
+ let bucket = rateBuckets.get(token);
60634
+ if (!bucket || now >= bucket.resetAt) {
60635
+ bucket = { count: 0, resetAt: now + RATE_LIMIT_WINDOW_MS };
60636
+ rateBuckets.set(token, bucket);
60637
+ }
60638
+ bucket.count++;
60639
+ return bucket.count <= RATE_LIMIT_MAX;
60640
+ }
60641
+ async function handleWebhookRequest(pathname, body, db3) {
60642
+ const taskMatch = pathname.match(/^\/api\/hooks\/task\/([a-f0-9]{32})$/);
60643
+ if (taskMatch) {
60644
+ const token = taskMatch[1];
60645
+ if (!checkWebhookRateLimit(token)) {
60646
+ return { status: 429, data: { error: "Too many requests" } };
60647
+ }
60648
+ const task = getTaskByWebhookToken(db3, token);
60649
+ if (!task) {
60650
+ return { status: 401, data: { error: "Invalid webhook token" } };
60651
+ }
60652
+ if (task.status === "paused") {
60653
+ return { status: 409, data: { error: "Task is paused" } };
60654
+ }
60655
+ const result = runTaskNow(db3, task.id);
60656
+ if (!result.started) {
60657
+ return { status: 409, data: { error: result.reason ?? "Task is already running" } };
60658
+ }
60659
+ eventBus.emit("tasks", "task:webhook_triggered", { id: task.id });
60660
+ return { status: 202, data: { ok: true, taskId: task.id } };
60661
+ }
60662
+ const queenMatch = pathname.match(/^\/api\/hooks\/queen\/([a-f0-9]{32})$/);
60663
+ if (queenMatch) {
60664
+ const token = queenMatch[1];
60665
+ if (!checkWebhookRateLimit(token)) {
60666
+ return { status: 429, data: { error: "Too many requests" } };
60667
+ }
60668
+ const room = getRoomByWebhookToken(db3, token);
60669
+ if (!room) {
60670
+ return { status: 401, data: { error: "Invalid webhook token" } };
60671
+ }
60672
+ if (room.status === "paused" || room.status === "stopped") {
60673
+ return { status: 409, data: { error: `Room is ${room.status}` } };
60674
+ }
60675
+ const payload = body && typeof body === "object" ? body : {};
60676
+ const message = typeof payload.message === "string" && payload.message.trim() ? payload.message.trim() : "Webhook triggered";
60677
+ insertChatMessage(db3, room.id, "user", message);
60678
+ if (room.queenWorkerId) {
60679
+ triggerAgent(db3, room.id, room.queenWorkerId);
60680
+ }
60681
+ eventBus.emit("rooms", "room:webhook_triggered", { id: room.id });
60682
+ return { status: 202, data: { ok: true, roomId: room.id } };
60683
+ }
60684
+ return { status: 404, data: { error: "Not found" } };
60685
+ }
60686
+ var RATE_LIMIT_MAX, RATE_LIMIT_WINDOW_MS, rateBuckets;
60687
+ var init_webhooks = __esm({
60688
+ "src/server/webhooks.ts"() {
60689
+ "use strict";
60690
+ init_db_queries();
60691
+ init_runtime();
60692
+ init_agent_loop();
60693
+ init_event_bus();
60694
+ RATE_LIMIT_MAX = 30;
60695
+ RATE_LIMIT_WINDOW_MS = 6e4;
60696
+ rateBuckets = /* @__PURE__ */ new Map();
60697
+ setInterval(() => {
60698
+ const now = Date.now();
60699
+ for (const [key, bucket] of rateBuckets) {
60700
+ if (now >= bucket.resetAt) rateBuckets.delete(key);
60701
+ }
60702
+ }, 12e4).unref();
60703
+ }
60704
+ });
60705
+
60178
60706
  // src/server/index.ts
60179
60707
  var server_exports2 = {};
60180
60708
  __export(server_exports2, {
@@ -60191,7 +60719,7 @@ function streamWithRedirects(url, res, corsHeaders, filename, depth = 0) {
60191
60719
  }
60192
60720
  try {
60193
60721
  const parsed = new import_node_url.URL(url);
60194
- const mod2 = parsed.protocol === "https:" ? import_node_https2.default : import_node_http2.default;
60722
+ const mod2 = parsed.protocol === "https:" ? import_node_https2.default : import_node_http.default;
60195
60723
  mod2.get(url, { headers: { "User-Agent": "quoroom-updater/1.0" } }, (assetRes) => {
60196
60724
  if ((assetRes.statusCode === 301 || assetRes.statusCode === 302 || assetRes.statusCode === 307) && assetRes.headers.location) {
60197
60725
  assetRes.resume();
@@ -60276,7 +60804,7 @@ function scheduleSelfRestart() {
60276
60804
  const args2 = [...process.execArgv, ...process.argv.slice(1)];
60277
60805
  if (process.platform === "win32") {
60278
60806
  const cmd = [process.execPath, ...args2].map(windowsQuote).join(" ");
60279
- const child = (0, import_node_child_process7.spawn)("cmd.exe", ["/d", "/s", "/c", `ping -n 2 127.0.0.1 >nul && ${cmd}`], {
60807
+ const child = (0, import_node_child_process6.spawn)("cmd.exe", ["/d", "/s", "/c", `ping -n 2 127.0.0.1 >nul && ${cmd}`], {
60280
60808
  detached: true,
60281
60809
  stdio: "ignore",
60282
60810
  windowsHide: true,
@@ -60285,7 +60813,7 @@ function scheduleSelfRestart() {
60285
60813
  child.unref();
60286
60814
  } else {
60287
60815
  const cmd = [process.execPath, ...args2].map(shellQuote).join(" ");
60288
- const child = (0, import_node_child_process7.spawn)("/bin/sh", ["-c", `sleep 1; exec ${cmd}`], {
60816
+ const child = (0, import_node_child_process6.spawn)("/bin/sh", ["-c", `sleep 1; exec ${cmd}`], {
60289
60817
  detached: true,
60290
60818
  stdio: "ignore",
60291
60819
  env: process.env
@@ -60370,10 +60898,10 @@ function checkRateLimit2(ip, method) {
60370
60898
  const limit = isWrite ? RATE_LIMIT_WRITE : RATE_LIMIT_READ;
60371
60899
  const key = `${ip}:${isWrite ? "w" : "r"}`;
60372
60900
  const now = Date.now();
60373
- let bucket = rateBuckets.get(key);
60901
+ let bucket = rateBuckets2.get(key);
60374
60902
  if (!bucket || now >= bucket.resetAt) {
60375
- bucket = { count: 0, resetAt: now + RATE_LIMIT_WINDOW_MS };
60376
- rateBuckets.set(key, bucket);
60903
+ bucket = { count: 0, resetAt: now + RATE_LIMIT_WINDOW_MS2 };
60904
+ rateBuckets2.set(key, bucket);
60377
60905
  }
60378
60906
  bucket.count++;
60379
60907
  if (bucket.count > limit) {
@@ -60392,7 +60920,7 @@ function createApiServer(options = {}) {
60392
60920
  if (!options.skipTokenFile) {
60393
60921
  writeTokenFile(dataDir, token, port);
60394
60922
  }
60395
- const server = import_node_http2.default.createServer(async (req, res) => {
60923
+ const server = import_node_http.default.createServer(async (req, res) => {
60396
60924
  res.on("error", () => {
60397
60925
  });
60398
60926
  const url = new import_node_url.URL(req.url, `http://${req.headers.host || "localhost"}`);
@@ -60500,6 +61028,13 @@ function createApiServer(options = {}) {
60500
61028
  }));
60501
61029
  return;
60502
61030
  }
61031
+ if (pathname.startsWith("/api/hooks/") && req.method === "POST") {
61032
+ const body = await parseBody(req).catch(() => void 0);
61033
+ const result = await handleWebhookRequest(pathname, body, db3);
61034
+ res.writeHead(result.status, responseHeaders);
61035
+ res.end(JSON.stringify(result.data));
61036
+ return;
61037
+ }
60503
61038
  if (pathname.startsWith("/api/")) {
60504
61039
  const isDownloadRoute = pathname === "/api/status/update/download" && req.method === "GET";
60505
61040
  const isRedirectRoute = pathname.endsWith("/onramp-redirect") && req.method === "GET";
@@ -60652,7 +61187,7 @@ function startServer(options = {}) {
60652
61187
  console.error(`Auth token: ${token.slice(0, 8)}...`);
60653
61188
  if (process.env.NODE_ENV === "production" && deploymentMode !== "cloud") {
60654
61189
  const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
60655
- (0, import_node_child_process7.exec)(`${cmd} ${dashboardUrl}`);
61190
+ (0, import_node_child_process6.exec)(`${cmd} ${dashboardUrl}`);
60656
61191
  }
60657
61192
  });
60658
61193
  }
@@ -60660,7 +61195,7 @@ function startServer(options = {}) {
60660
61195
  if (err.code === "EADDRINUSE") {
60661
61196
  console.error(`Port ${port} is in use \u2014 killing existing process...`);
60662
61197
  try {
60663
- (0, import_node_child_process7.execSync)(`lsof -ti :${port} | xargs kill -9`, { stdio: "ignore" });
61198
+ (0, import_node_child_process6.execSync)(`lsof -ti :${port} | xargs kill -9`, { stdio: "ignore" });
60664
61199
  } catch {
60665
61200
  }
60666
61201
  setTimeout(listen, 500);
@@ -60681,6 +61216,8 @@ function startServer(options = {}) {
60681
61216
  stopServerRuntime();
60682
61217
  stopCloudSync();
60683
61218
  stopUpdateChecker();
61219
+ closeBrowser().catch(() => {
61220
+ });
60684
61221
  server.close();
60685
61222
  closeServerDatabase();
60686
61223
  process.exit(0);
@@ -60690,22 +61227,24 @@ function startServer(options = {}) {
60690
61227
  stopServerRuntime();
60691
61228
  stopCloudSync();
60692
61229
  stopUpdateChecker();
61230
+ closeBrowser().catch(() => {
61231
+ });
60693
61232
  server.close();
60694
61233
  closeServerDatabase();
60695
61234
  process.exit(0);
60696
61235
  });
60697
61236
  }
60698
- var import_node_http2, import_node_https2, import_node_url, import_node_fs3, import_node_path4, import_node_os5, import_node_child_process7, DEFAULT_PORT, DEFAULT_BIND_HOST_LOCAL, DEFAULT_BIND_HOST_CLOUD, MIME_TYPES, PROFILE_HTTP, PROFILE_HTTP_SLOW_MS, PROFILE_HTTP_ENDPOINTS, RATE_LIMIT_WINDOW_MS, RATE_LIMIT_READ, RATE_LIMIT_WRITE, rateBuckets, CLOUD_SECURITY_HEADERS;
61237
+ var import_node_http, import_node_https2, import_node_url, import_node_fs3, import_node_path4, import_node_os5, import_node_child_process6, DEFAULT_PORT, DEFAULT_BIND_HOST_LOCAL, DEFAULT_BIND_HOST_CLOUD, MIME_TYPES, PROFILE_HTTP, PROFILE_HTTP_SLOW_MS, PROFILE_HTTP_ENDPOINTS, RATE_LIMIT_WINDOW_MS2, RATE_LIMIT_READ, RATE_LIMIT_WRITE, rateBuckets2, CLOUD_SECURITY_HEADERS;
60699
61238
  var init_server4 = __esm({
60700
61239
  "src/server/index.ts"() {
60701
61240
  "use strict";
60702
- import_node_http2 = __toESM(require("node:http"));
61241
+ import_node_http = __toESM(require("node:http"));
60703
61242
  import_node_https2 = __toESM(require("node:https"));
60704
61243
  import_node_url = require("node:url");
60705
61244
  import_node_fs3 = __toESM(require("node:fs"));
60706
61245
  import_node_path4 = __toESM(require("node:path"));
60707
61246
  import_node_os5 = require("node:os");
60708
- import_node_child_process7 = require("node:child_process");
61247
+ import_node_child_process6 = require("node:child_process");
60709
61248
  init_router();
60710
61249
  init_auth();
60711
61250
  init_access();
@@ -60717,6 +61256,8 @@ var init_server4 = __esm({
60717
61256
  init_agent_loop();
60718
61257
  init_updateChecker();
60719
61258
  init_runtime();
61259
+ init_web_tools();
61260
+ init_webhooks();
60720
61261
  try {
60721
61262
  process.loadEnvFile?.(".env");
60722
61263
  } catch {
@@ -60753,14 +61294,14 @@ var init_server4 = __esm({
60753
61294
  "/api/workers",
60754
61295
  "/api/memory/entities"
60755
61296
  ]);
60756
- RATE_LIMIT_WINDOW_MS = 6e4;
61297
+ RATE_LIMIT_WINDOW_MS2 = 6e4;
60757
61298
  RATE_LIMIT_READ = 300;
60758
61299
  RATE_LIMIT_WRITE = 120;
60759
- rateBuckets = /* @__PURE__ */ new Map();
61300
+ rateBuckets2 = /* @__PURE__ */ new Map();
60760
61301
  setInterval(() => {
60761
61302
  const now = Date.now();
60762
- for (const [key, bucket] of rateBuckets) {
60763
- if (now >= bucket.resetAt) rateBuckets.delete(key);
61303
+ for (const [key, bucket] of rateBuckets2) {
61304
+ if (now >= bucket.resetAt) rateBuckets2.delete(key);
60764
61305
  }
60765
61306
  }, 12e4).unref();
60766
61307
  CLOUD_SECURITY_HEADERS = {