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/README.md +15 -10
- package/out/mcp/api-server.js +15341 -14815
- package/out/mcp/cli.js +1711 -1170
- package/out/mcp/server.js +318 -923
- package/package.json +2 -1
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
|
|
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:
|
|
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:
|
|
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 '
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
25258
|
-
|
|
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
|
-
|
|
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
|
|
26105
|
-
|
|
26106
|
-
messages
|
|
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
|
|
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 ?
|
|
26175
|
-
const
|
|
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
|
|
26492
|
-
const
|
|
26493
|
-
|
|
26494
|
-
|
|
26495
|
-
|
|
26496
|
-
|
|
26497
|
-
|
|
26498
|
-
|
|
26499
|
-
|
|
26500
|
-
|
|
26501
|
-
|
|
26502
|
-
|
|
26503
|
-
|
|
26504
|
-
|
|
26505
|
-
|
|
26506
|
-
|
|
26507
|
-
|
|
26508
|
-
|
|
26509
|
-
|
|
26510
|
-
|
|
26511
|
-
|
|
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
|
-
|
|
26527
|
-
|
|
26528
|
-
|
|
26529
|
-
|
|
26530
|
-
|
|
26531
|
-
|
|
26532
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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("
|
|
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
|
-
|
|
27122
|
-
|
|
27123
|
-
|
|
27124
|
-
|
|
27125
|
-
|
|
27126
|
-
|
|
27127
|
-
|
|
27128
|
-
|
|
27129
|
-
|
|
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
|
|
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:
|
|
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 (
|
|
27463
|
-
|
|
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
|
-
|
|
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 ===
|
|
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 ===
|
|
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
|
|
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:
|
|
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 ?
|
|
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:
|
|
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 =
|
|
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
|
|
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" ?
|
|
48082
|
-
const iv =
|
|
48083
|
-
const cipher =
|
|
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" ?
|
|
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 =
|
|
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:
|
|
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:
|
|
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
|
|
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
|
-
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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
|
|
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:
|
|
50319
|
+
transport: http(chainInfo.config.rpcUrl)
|
|
50313
50320
|
});
|
|
50314
50321
|
const walletClient = createWalletClient({
|
|
50315
50322
|
account,
|
|
50316
50323
|
chain: chainInfo.chain,
|
|
50317
|
-
transport:
|
|
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:
|
|
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:
|
|
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
|
|
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.
|
|
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(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').replace(/'/g, "'").replace(/ /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
|
-
|
|
51537
|
-
const
|
|
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:
|
|
51567
|
-
decisionType
|
|
51726
|
+
proposal: proposalText,
|
|
51727
|
+
decisionType
|
|
51568
51728
|
});
|
|
51569
51729
|
if (decision.status === "approved") {
|
|
51570
|
-
return { content: `Proposal auto-approved: "${
|
|
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: "${
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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: '
|
|
51809
|
-
systemPrompt: { type: "string", description:
|
|
51810
|
-
role: { type: "string", description: 'Optional
|
|
51811
|
-
description: { type: "string", description: "Optional
|
|
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,
|
|
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
|
|
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
|
|
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
|
-
|
|
52177
|
-
|
|
52178
|
-
|
|
52179
|
-
|
|
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
|
-
|
|
52563
|
+
const activitySlice = recentActivity.slice(0, 15);
|
|
52564
|
+
if (activitySlice.length > 0) {
|
|
52190
52565
|
contextParts.push(`## Recent Activity
|
|
52191
|
-
${
|
|
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
|
|
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
|
-
)
|
|
52219
|
-
|
|
52220
|
-
|
|
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.
|
|
52253
|
-
|
|
52254
|
-
|
|
52255
|
-
|
|
52256
|
-
|
|
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
|
-
|
|
52264
|
-
|
|
52265
|
-
|
|
52266
|
-
const
|
|
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
|
-
|
|
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.
|
|
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
|
|
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,
|
|
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
|
|
55050
|
+
var import_node_crypto5;
|
|
54050
55051
|
var init_settings2 = __esm({
|
|
54051
55052
|
"src/server/routes/settings.ts"() {
|
|
54052
55053
|
"use strict";
|
|
54053
|
-
|
|
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.
|
|
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 === "
|
|
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,
|
|
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
|
-
|
|
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)(
|
|
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,
|
|
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,
|
|
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
|
|
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
|
-
|
|
55431
|
-
|
|
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,
|
|
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
|
|
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
|
-
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
|
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
|
-
|
|
55750
|
-
|
|
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
|
|
59083
|
+
var http3 = require("http");
|
|
58632
59084
|
var net = require("net");
|
|
58633
59085
|
var tls = require("tls");
|
|
58634
|
-
var { randomBytes:
|
|
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 =
|
|
59162
|
-
const request = isSecure ? https3.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
|
|
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 =
|
|
59728
|
-
const body =
|
|
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 ||
|
|
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} ${
|
|
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 :
|
|
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,
|
|
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,
|
|
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 =
|
|
60901
|
+
let bucket = rateBuckets2.get(key);
|
|
60374
60902
|
if (!bucket || now >= bucket.resetAt) {
|
|
60375
|
-
bucket = { count: 0, resetAt: now +
|
|
60376
|
-
|
|
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 =
|
|
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,
|
|
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,
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
61297
|
+
RATE_LIMIT_WINDOW_MS2 = 6e4;
|
|
60757
61298
|
RATE_LIMIT_READ = 300;
|
|
60758
61299
|
RATE_LIMIT_WRITE = 120;
|
|
60759
|
-
|
|
61300
|
+
rateBuckets2 = /* @__PURE__ */ new Map();
|
|
60760
61301
|
setInterval(() => {
|
|
60761
61302
|
const now = Date.now();
|
|
60762
|
-
for (const [key, bucket] of
|
|
60763
|
-
if (now >= bucket.resetAt)
|
|
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 = {
|