quoroom 0.1.8 → 0.1.11

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 CHANGED
@@ -8,7 +8,7 @@
8
8
 
9
9
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
10
10
  [![npm version](https://img.shields.io/npm/v/quoroom)](https://www.npmjs.com/package/quoroom)
11
- [![Tests](https://img.shields.io/badge/tests-804%20passing-brightgreen)](#)
11
+ [![Tests](https://img.shields.io/badge/tests-907%20passing-brightgreen)](#)
12
12
  [![GitHub stars](https://img.shields.io/github/stars/quoroom-ai/room)](https://github.com/quoroom-ai/room/stargazers)
13
13
  [![macOS](https://img.shields.io/badge/macOS-.pkg-000000?logo=apple&logoColor=white)](https://github.com/quoroom-ai/room/releases/latest)
14
14
  [![Windows](https://img.shields.io/badge/Windows-.exe-0078D4?logo=windows&logoColor=white)](https://github.com/quoroom-ai/room/releases/latest)
@@ -38,6 +38,7 @@ Official channels only:
38
38
 
39
39
  - `https://quoroom.ai`
40
40
  - `https://github.com/quoroom-ai`
41
+ - Telegram: `@quoroom_ai_bot`
41
42
 
42
43
  If you see impersonation or scam activity, report it to `hello@quoroom.ai`.
43
44
  See `TRADEMARKS.md` for full trademark usage terms.
@@ -108,6 +109,12 @@ Quoroom is an open research project exploring autonomous agent collectives. Each
108
109
 
109
110
  **Dashboard** — React SPA served directly by your local Quoroom server at `http://localhost:3700` (or your configured port). Manage rooms, agents, goals, memory, wallet — all from the browser, with local-first data storage.
110
111
 
112
+ **Cloud Mode** — Deploy to the cloud and control your room remotely. Same dashboard works in both local and cloud mode. Cloud instances auto-detect their environment, support JWT-based auth, and serve the UI over HTTPS with strict CORS. Connect your Claude or Codex subscription from the remote Settings panel.
113
+
114
+ **Inbox** — Rooms can message the keeper and other rooms. Cross-room communication with reply threading. Agents escalate decisions, share updates, or request resources from neighboring rooms.
115
+
116
+ **Credentials** — Secure credential storage for API keys and secrets. Agents list and retrieve credentials at runtime without exposing raw values in prompts or logs.
117
+
111
118
  **Auto-updates** — The server polls GitHub for new releases every 4 hours. When a new version is available, the dashboard shows a notification popup and a download row in Settings. One click downloads the installer for your platform directly — no browser redirect.
112
119
 
113
120
  ---
@@ -213,6 +220,7 @@ The room engine exposes an MCP server over stdio. All tools use the `quoroom_` p
213
220
  | `quoroom_pause_room` | Pause a running room |
214
221
  | `quoroom_restart_room` | Restart a paused room |
215
222
  | `quoroom_delete_room` | Delete a room |
223
+ | `quoroom_configure_room` | Update room configuration |
216
224
 
217
225
  ### Quorum
218
226
 
@@ -304,6 +312,7 @@ The room engine exposes an MCP server over stdio. All tools use the `quoroom_` p
304
312
  | `quoroom_wallet_balance` | Check on-chain balance (USDC/USDT, all chains) |
305
313
  | `quoroom_wallet_send` | Send USDC or USDT on any supported chain |
306
314
  | `quoroom_wallet_history` | View transaction history |
315
+ | `quoroom_wallet_topup` | Get wallet top-up URL |
307
316
 
308
317
  ### Identity
309
318
 
@@ -324,9 +333,32 @@ The room engine exposes an MCP server over stdio. All tools use the `quoroom_` p
324
333
  | `quoroom_station_stop` | Stop a running station |
325
334
  | `quoroom_station_exec` | Execute a command on a station |
326
335
  | `quoroom_station_logs` | View station logs |
327
- | `quoroom_station_deploy` | Deploy to a station |
328
- | `quoroom_station_domain` | Manage station domain |
329
336
  | `quoroom_station_delete` | Delete a station |
337
+ | `quoroom_station_cancel` | Cancel a pending station |
338
+ | `quoroom_station_create_crypto` | Provision a station with crypto payment |
339
+ | `quoroom_station_renew_crypto` | Renew a station with crypto payment |
340
+
341
+ ### Inbox
342
+
343
+ | Tool | Description |
344
+ |------|-------------|
345
+ | `quoroom_inbox_send_keeper` | Send a message to the keeper |
346
+ | `quoroom_inbox_send_room` | Send a message to another room |
347
+ | `quoroom_inbox_list` | List inbox messages |
348
+ | `quoroom_inbox_reply` | Reply to a room message |
349
+
350
+ ### Credentials
351
+
352
+ | Tool | Description |
353
+ |------|-------------|
354
+ | `quoroom_credentials_list` | List stored credentials |
355
+ | `quoroom_credentials_get` | Get a credential value |
356
+
357
+ ### Resources
358
+
359
+ | Tool | Description |
360
+ |------|-------------|
361
+ | `quoroom_resources_get` | Get local system resources (CPU, memory, disk) |
330
362
 
331
363
  ### Settings
332
364
 
@@ -351,6 +383,13 @@ npm run test:watch # Watch mode
351
383
  npm run test:e2e # End-to-end tests (Playwright)
352
384
  ```
353
385
 
386
+ ### Docker (cloud runtime)
387
+
388
+ ```bash
389
+ docker build -t quoroom .
390
+ docker run -p 3700:3700 quoroom
391
+ ```
392
+
354
393
  ## Releasing
355
394
 
356
395
  Use the release runbook: [`docs/RELEASE_RUNBOOK.md`](docs/RELEASE_RUNBOOK.md)
@@ -365,19 +404,19 @@ room/
365
404
  │ ├── mcp/ # MCP server (stdio)
366
405
  │ │ ├── server.ts # Tool registration
367
406
  │ │ ├── db.ts # Database initialization
368
- │ │ └── tools/ # 13 tool modules
407
+ │ │ └── tools/ # 17 tool modules
369
408
  │ ├── server/ # HTTP/WebSocket API server
370
- │ │ ├── index.ts # Server bootstrap
409
+ │ │ ├── index.ts # Server bootstrap (local + cloud mode)
371
410
  │ │ ├── router.ts # Request router
372
- │ │ ├── auth.ts # Dual-token auth + CORS
411
+ │ │ ├── auth.ts # Dual-token auth + CORS + cloud JWT
373
412
  │ │ ├── access.ts # Role-based access control
374
413
  │ │ ├── ws.ts # WebSocket real-time events
375
- │ │ └── routes/ # REST API routes
414
+ │ │ └── routes/ # REST API routes (20 modules)
376
415
  │ ├── ui/ # React SPA dashboard
377
416
  │ │ ├── App.tsx # Root component
378
- │ │ ├── components/ # UI components
417
+ │ │ ├── components/ # UI components (32 modules)
379
418
  │ │ ├── hooks/ # React hooks
380
- │ │ └── lib/ # API client, auth, WebSocket
419
+ │ │ └── lib/ # API client, auth, storage, WebSocket
381
420
  │ └── shared/ # Core engine
382
421
  │ ├── agent-loop.ts # Worker agent loop with rate limiting
383
422
  │ ├── agent-executor.ts # Claude Code CLI execution
@@ -389,15 +428,17 @@ room/
389
428
  │ ├── identity.ts # ERC-8004 on-chain identity
390
429
  │ ├── station.ts # Cloud provisioning
391
430
  │ ├── task-runner.ts # Task execution engine
431
+ │ ├── model-provider.ts # Multi-provider LLM support
432
+ │ ├── cloud-sync.ts # Cloud registration + heartbeat
392
433
  │ ├── db-queries.ts # Database query layer
393
434
  │ ├── schema.ts # SQLite schema (WAL mode)
394
435
  │ ├── embeddings.ts # Vector embeddings (all-MiniLM-L6-v2)
395
- ├── cloud-sync.ts # Cloud registration + heartbeat
396
- │ └── __tests__/ # Test suite
436
+ └── __tests__/ # Test suite (907 tests)
397
437
  ├── e2e/ # Playwright end-to-end tests
398
438
  ├── scripts/
399
439
  │ └── build-mcp.js # esbuild bundling
400
- └── docs/ # Media assets
440
+ ├── Dockerfile # Cloud runtime image
441
+ └── docs/ # Media assets + architecture docs
401
442
  ```
402
443
 
403
444
  **Tech stack**: TypeScript (strict), React, Tailwind CSS, better-sqlite3, sqlite-vec, viem, MCP SDK, HuggingFace Transformers, node-cron, zod, esbuild, Vite, Vitest
@@ -9382,7 +9382,7 @@ var require_package = __commonJS({
9382
9382
  "package.json"(exports2, module2) {
9383
9383
  module2.exports = {
9384
9384
  name: "quoroom",
9385
- version: "0.1.8",
9385
+ version: "0.1.11",
9386
9386
  description: "Autonomous AI agent collective engine \u2014 Queen, Workers, Quorum",
9387
9387
  main: "./out/mcp/server.js",
9388
9388
  bin: {
@@ -9404,16 +9404,28 @@ var require_package = __commonJS({
9404
9404
  build: "npm run typecheck && npm run build:mcp && npm run build:ui",
9405
9405
  "build:mcp": "node scripts/build-mcp.js",
9406
9406
  "build:ui": "vite build --config src/ui/vite.config.ts",
9407
- dev: `sh -c 'trap "kill 0" INT TERM EXIT; npm run dev:room & npm run dev:cloud & wait'`,
9408
- "dev:room": "npm run build:mcp && npm run build:ui && node scripts/dev-server.js",
9409
- "dev:cloud": "cd ../cloud && PORT=3710 CLOUD_PUBLIC_URL=http://127.0.0.1:3710 CLOUD_ALLOWED_ORIGINS='http://127.0.0.1:3710,http://localhost:3710,http://localhost:5173,http://127.0.0.1:5173,https://quoroom.ai,https://www.quoroom.ai,https://app.quoroom.ai' npm start",
9407
+ "kill:ports": "node scripts/kill-ports.js",
9408
+ "kill:dev-ports": "npm run kill:ports -- 4700 3710",
9409
+ "dev:links": "node scripts/dev-links.js",
9410
+ dev: `sh -c 'npm run kill:dev-ports && trap "kill 0" INT TERM EXIT; npm run dev:links & npm run dev:room & npm run dev:cloud & wait'`,
9411
+ "dev:room": "sh -c 'export QUOROOM_DATA_DIR=$HOME/.quoroom-dev QUOROOM_SKIP_MCP_REGISTER=1; npm run build:mcp && npm run build:ui && node scripts/dev-server.js --port 4700'",
9412
+ "dev:room:isolated": "sh -c 'export QUOROOM_DATA_DIR=$HOME/.quoroom-dev QUOROOM_SKIP_MCP_REGISTER=1; npm run build:mcp && npm run build:ui && node scripts/dev-server.js --port 4700'",
9413
+ "dev:room:shared": "npm run build:mcp && npm run build:ui && node scripts/dev-server.js",
9414
+ "doctor:split": "node scripts/doctor-split.js",
9415
+ "dev:isolated": `sh -c 'npm run kill:dev-ports && trap "kill 0" INT TERM EXIT; npm run dev:links & npm run dev:room:isolated & npm run dev:cloud & wait'`,
9416
+ "dev:cloud": `sh -c 'npm run kill:ports -- 3710 && cd ../cloud && PORT=3710 CLOUD_PUBLIC_URL=http://127.0.0.1:3710 CLOUD_ALLOWED_ORIGINS='"'"'http://127.0.0.1:3710,http://localhost:3710,http://localhost:5173,http://127.0.0.1:5173,https://quoroom.ai,https://www.quoroom.ai,https://app.quoroom.ai'"'"' npm start'`,
9410
9417
  "dev:ui": "vite --config src/ui/vite.config.ts",
9411
9418
  "seed:style-demo": "node scripts/seed-style-demo.js",
9412
9419
  typecheck: "tsc --noEmit",
9413
9420
  test: "npm run rebuild:native:node && vitest run --pool=forks --passWithNoTests",
9414
9421
  "test:watch": "npm run rebuild:native:node && vitest --pool=forks",
9422
+ "test:quick": "npm run typecheck && npm run test",
9423
+ "test:smart-e2e": "git diff --cached --name-only | grep -qE '^(src/ui/|src/server/|e2e/|playwright)' && npm run test:e2e || echo 'No UI/server changes \u2014 skipping E2E'",
9415
9424
  "test:e2e": "npm run build && npx playwright test",
9416
9425
  "test:e2e:ui": "npm run build && npx playwright test e2e/ui.test.ts",
9426
+ "test:e2e:setup": "npm run build && npx playwright test e2e/setup-flow.test.ts",
9427
+ "test:e2e:setup:headed": "npm run build && npx playwright test e2e/setup-flow.test.ts --headed --project=chromium",
9428
+ "test:e2e:providers": "npm run build && npx playwright test e2e/provider-flows.test.ts",
9417
9429
  "rebuild:native:node": `node -e "try{require('better-sqlite3')(':memory:').close()}catch{process.exit(1)}" || npx --yes node-gyp rebuild --directory=node_modules/better-sqlite3`,
9418
9430
  prepublishOnly: "npm run build:mcp"
9419
9431
  },
@@ -10453,9 +10465,6 @@ function getCloudAllowedOrigins() {
10453
10465
  const origins = configured.length > 0 ? configured : DEFAULT_CLOUD_ALLOWED_ORIGINS;
10454
10466
  return new Set(origins.map(normalizeOrigin).filter(Boolean));
10455
10467
  }
10456
- function getCloudUserToken() {
10457
- return (process.env.QUOROOM_CLOUD_USER_TOKEN || "").trim();
10458
- }
10459
10468
  function getCloudJwtSecret() {
10460
10469
  return (process.env.QUOROOM_CLOUD_JWT_SECRET || "").trim();
10461
10470
  }
@@ -10554,9 +10563,9 @@ function generateToken(dataDir) {
10554
10563
  return agentToken;
10555
10564
  }
10556
10565
  }
10557
- agentToken = dataDir ? readLegacyAgentToken(dataDir) ?? import_node_crypto.default.randomBytes(32).toString("hex") : import_node_crypto.default.randomBytes(32).toString("hex");
10566
+ agentToken = dataDir && !isCloudDeployment() ? readLegacyAgentToken(dataDir) ?? import_node_crypto.default.randomBytes(32).toString("hex") : import_node_crypto.default.randomBytes(32).toString("hex");
10558
10567
  userToken = import_node_crypto.default.randomBytes(32).toString("hex");
10559
- if (dataDir) writePersistedTokens(dataDir, agentToken, userToken);
10568
+ if (dataDir && !isCloudDeployment()) writePersistedTokens(dataDir, agentToken, userToken);
10560
10569
  return agentToken;
10561
10570
  }
10562
10571
  function getUserToken() {
@@ -10572,10 +10581,10 @@ function validateToken(authHeader) {
10572
10581
  const cloudRole = validateCloudJwt(provided);
10573
10582
  if (cloudRole) return cloudRole;
10574
10583
  }
10575
- if (isCloudDeployment() && tokenEquals(getCloudUserToken(), provided)) return "user";
10576
10584
  return null;
10577
10585
  }
10578
10586
  function writeTokenFile(dataDir, token, port) {
10587
+ if (isCloudDeployment()) return;
10579
10588
  (0, import_node_fs.mkdirSync)(dataDir, { recursive: true });
10580
10589
  (0, import_node_fs.writeFileSync)((0, import_node_path.join)(dataDir, "api.token"), token, { mode: 384 });
10581
10590
  (0, import_node_fs.writeFileSync)((0, import_node_path.join)(dataDir, "api.port"), String(port));
@@ -26026,7 +26035,7 @@ var cachedVersion = null;
26026
26035
  function getVersion3() {
26027
26036
  if (cachedVersion) return cachedVersion;
26028
26037
  try {
26029
- cachedVersion = require_package().version;
26038
+ cachedVersion = true ? "0.1.11" : null.version;
26030
26039
  } catch {
26031
26040
  cachedVersion = "unknown";
26032
26041
  }
@@ -26227,12 +26236,12 @@ function registerStatusRoutes(router) {
26227
26236
  const ollama = await checkOllama();
26228
26237
  const resources = getResources();
26229
26238
  const deploymentMode = getDeploymentMode();
26239
+ const isCloud = deploymentMode === "cloud";
26230
26240
  return {
26231
26241
  data: {
26232
26242
  version: getVersion3(),
26233
26243
  uptime: Math.floor((Date.now() - startedAt) / 1e3),
26234
- dataDir,
26235
- dbPath,
26244
+ ...!isCloud && { dataDir, dbPath },
26236
26245
  claude,
26237
26246
  codex,
26238
26247
  ollama,
@@ -27745,6 +27754,39 @@ function serveStatic(staticDir, pathname, res) {
27745
27754
  res.end("Not Found");
27746
27755
  }
27747
27756
  }
27757
+ var RATE_LIMIT_WINDOW_MS = 6e4;
27758
+ var RATE_LIMIT_READ = 300;
27759
+ var RATE_LIMIT_WRITE = 120;
27760
+ var rateBuckets = /* @__PURE__ */ new Map();
27761
+ setInterval(() => {
27762
+ const now = Date.now();
27763
+ for (const [key, bucket] of rateBuckets) {
27764
+ if (now >= bucket.resetAt) rateBuckets.delete(key);
27765
+ }
27766
+ }, 12e4).unref();
27767
+ function checkRateLimit2(ip, method) {
27768
+ const isWrite = method !== "GET" && method !== "HEAD" && method !== "OPTIONS";
27769
+ const limit = isWrite ? RATE_LIMIT_WRITE : RATE_LIMIT_READ;
27770
+ const key = `${ip}:${isWrite ? "w" : "r"}`;
27771
+ const now = Date.now();
27772
+ let bucket = rateBuckets.get(key);
27773
+ if (!bucket || now >= bucket.resetAt) {
27774
+ bucket = { count: 0, resetAt: now + RATE_LIMIT_WINDOW_MS };
27775
+ rateBuckets.set(key, bucket);
27776
+ }
27777
+ bucket.count++;
27778
+ if (bucket.count > limit) {
27779
+ const retryAfter = Math.ceil((bucket.resetAt - now) / 1e3);
27780
+ return { allowed: false, retryAfter };
27781
+ }
27782
+ return { allowed: true, retryAfter: 0 };
27783
+ }
27784
+ var CLOUD_SECURITY_HEADERS = {
27785
+ "Strict-Transport-Security": "max-age=31536000; includeSubDomains",
27786
+ "X-Content-Type-Options": "nosniff",
27787
+ "X-Frame-Options": "DENY",
27788
+ "Referrer-Policy": "strict-origin-when-cross-origin"
27789
+ };
27748
27790
  function createApiServer(options = {}) {
27749
27791
  const db2 = options.db ?? getServerDatabase();
27750
27792
  const port = options.port ?? DEFAULT_PORT;
@@ -27769,7 +27811,20 @@ function createApiServer(options = {}) {
27769
27811
  const responseHeaders = {
27770
27812
  "Content-Type": "application/json"
27771
27813
  };
27814
+ if (isCloudDeployment()) {
27815
+ Object.assign(responseHeaders, CLOUD_SECURITY_HEADERS);
27816
+ }
27772
27817
  setCorsHeaders(origin, responseHeaders);
27818
+ if (isCloudDeployment() && pathname.startsWith("/api/")) {
27819
+ const clientIp = req.socket.remoteAddress || "unknown";
27820
+ const rl = checkRateLimit2(clientIp, req.method || "GET");
27821
+ if (!rl.allowed) {
27822
+ responseHeaders["Retry-After"] = String(rl.retryAfter);
27823
+ res.writeHead(429, responseHeaders);
27824
+ res.end(JSON.stringify({ error: "Too many requests" }));
27825
+ return;
27826
+ }
27827
+ }
27773
27828
  const isSameOrigin = origin && req.headers.host && (() => {
27774
27829
  try {
27775
27830
  return new import_node_url.URL(origin).host === req.headers.host;
package/out/mcp/cli.js CHANGED
@@ -49800,7 +49800,7 @@ var server_exports = {};
49800
49800
  async function main() {
49801
49801
  const server = new McpServer({
49802
49802
  name: "quoroom",
49803
- version: true ? "0.1.8" : "0.0.0"
49803
+ version: true ? "0.1.11" : "0.0.0"
49804
49804
  });
49805
49805
  registerMemoryTools(server);
49806
49806
  registerSchedulerTools(server);
@@ -49934,9 +49934,6 @@ function getCloudAllowedOrigins() {
49934
49934
  const origins = configured.length > 0 ? configured : DEFAULT_CLOUD_ALLOWED_ORIGINS;
49935
49935
  return new Set(origins.map(normalizeOrigin).filter(Boolean));
49936
49936
  }
49937
- function getCloudUserToken() {
49938
- return (process.env.QUOROOM_CLOUD_USER_TOKEN || "").trim();
49939
- }
49940
49937
  function getCloudJwtSecret() {
49941
49938
  return (process.env.QUOROOM_CLOUD_JWT_SECRET || "").trim();
49942
49939
  }
@@ -50035,9 +50032,9 @@ function generateToken(dataDir) {
50035
50032
  return agentToken;
50036
50033
  }
50037
50034
  }
50038
- agentToken = dataDir ? readLegacyAgentToken(dataDir) ?? import_node_crypto2.default.randomBytes(32).toString("hex") : import_node_crypto2.default.randomBytes(32).toString("hex");
50035
+ agentToken = dataDir && !isCloudDeployment() ? readLegacyAgentToken(dataDir) ?? import_node_crypto2.default.randomBytes(32).toString("hex") : import_node_crypto2.default.randomBytes(32).toString("hex");
50039
50036
  userToken = import_node_crypto2.default.randomBytes(32).toString("hex");
50040
- if (dataDir) writePersistedTokens(dataDir, agentToken, userToken);
50037
+ if (dataDir && !isCloudDeployment()) writePersistedTokens(dataDir, agentToken, userToken);
50041
50038
  return agentToken;
50042
50039
  }
50043
50040
  function getUserToken() {
@@ -50053,10 +50050,10 @@ function validateToken(authHeader) {
50053
50050
  const cloudRole = validateCloudJwt(provided);
50054
50051
  if (cloudRole) return cloudRole;
50055
50052
  }
50056
- if (isCloudDeployment() && tokenEquals(getCloudUserToken(), provided)) return "user";
50057
50053
  return null;
50058
50054
  }
50059
50055
  function writeTokenFile(dataDir, token, port) {
50056
+ if (isCloudDeployment()) return;
50060
50057
  (0, import_node_fs.mkdirSync)(dataDir, { recursive: true });
50061
50058
  (0, import_node_fs.writeFileSync)((0, import_node_path.join)(dataDir, "api.token"), token, { mode: 384 });
50062
50059
  (0, import_node_fs.writeFileSync)((0, import_node_path.join)(dataDir, "api.port"), String(port));
@@ -50744,7 +50741,7 @@ var require_package = __commonJS({
50744
50741
  "package.json"(exports2, module2) {
50745
50742
  module2.exports = {
50746
50743
  name: "quoroom",
50747
- version: "0.1.8",
50744
+ version: "0.1.11",
50748
50745
  description: "Autonomous AI agent collective engine \u2014 Queen, Workers, Quorum",
50749
50746
  main: "./out/mcp/server.js",
50750
50747
  bin: {
@@ -50766,16 +50763,28 @@ var require_package = __commonJS({
50766
50763
  build: "npm run typecheck && npm run build:mcp && npm run build:ui",
50767
50764
  "build:mcp": "node scripts/build-mcp.js",
50768
50765
  "build:ui": "vite build --config src/ui/vite.config.ts",
50769
- dev: `sh -c 'trap "kill 0" INT TERM EXIT; npm run dev:room & npm run dev:cloud & wait'`,
50770
- "dev:room": "npm run build:mcp && npm run build:ui && node scripts/dev-server.js",
50771
- "dev:cloud": "cd ../cloud && PORT=3710 CLOUD_PUBLIC_URL=http://127.0.0.1:3710 CLOUD_ALLOWED_ORIGINS='http://127.0.0.1:3710,http://localhost:3710,http://localhost:5173,http://127.0.0.1:5173,https://quoroom.ai,https://www.quoroom.ai,https://app.quoroom.ai' npm start",
50766
+ "kill:ports": "node scripts/kill-ports.js",
50767
+ "kill:dev-ports": "npm run kill:ports -- 4700 3710",
50768
+ "dev:links": "node scripts/dev-links.js",
50769
+ dev: `sh -c 'npm run kill:dev-ports && trap "kill 0" INT TERM EXIT; npm run dev:links & npm run dev:room & npm run dev:cloud & wait'`,
50770
+ "dev:room": "sh -c 'export QUOROOM_DATA_DIR=$HOME/.quoroom-dev QUOROOM_SKIP_MCP_REGISTER=1; npm run build:mcp && npm run build:ui && node scripts/dev-server.js --port 4700'",
50771
+ "dev:room:isolated": "sh -c 'export QUOROOM_DATA_DIR=$HOME/.quoroom-dev QUOROOM_SKIP_MCP_REGISTER=1; npm run build:mcp && npm run build:ui && node scripts/dev-server.js --port 4700'",
50772
+ "dev:room:shared": "npm run build:mcp && npm run build:ui && node scripts/dev-server.js",
50773
+ "doctor:split": "node scripts/doctor-split.js",
50774
+ "dev:isolated": `sh -c 'npm run kill:dev-ports && trap "kill 0" INT TERM EXIT; npm run dev:links & npm run dev:room:isolated & npm run dev:cloud & wait'`,
50775
+ "dev:cloud": `sh -c 'npm run kill:ports -- 3710 && cd ../cloud && PORT=3710 CLOUD_PUBLIC_URL=http://127.0.0.1:3710 CLOUD_ALLOWED_ORIGINS='"'"'http://127.0.0.1:3710,http://localhost:3710,http://localhost:5173,http://127.0.0.1:5173,https://quoroom.ai,https://www.quoroom.ai,https://app.quoroom.ai'"'"' npm start'`,
50772
50776
  "dev:ui": "vite --config src/ui/vite.config.ts",
50773
50777
  "seed:style-demo": "node scripts/seed-style-demo.js",
50774
50778
  typecheck: "tsc --noEmit",
50775
50779
  test: "npm run rebuild:native:node && vitest run --pool=forks --passWithNoTests",
50776
50780
  "test:watch": "npm run rebuild:native:node && vitest --pool=forks",
50781
+ "test:quick": "npm run typecheck && npm run test",
50782
+ "test:smart-e2e": "git diff --cached --name-only | grep -qE '^(src/ui/|src/server/|e2e/|playwright)' && npm run test:e2e || echo 'No UI/server changes \u2014 skipping E2E'",
50777
50783
  "test:e2e": "npm run build && npx playwright test",
50778
50784
  "test:e2e:ui": "npm run build && npx playwright test e2e/ui.test.ts",
50785
+ "test:e2e:setup": "npm run build && npx playwright test e2e/setup-flow.test.ts",
50786
+ "test:e2e:setup:headed": "npm run build && npx playwright test e2e/setup-flow.test.ts --headed --project=chromium",
50787
+ "test:e2e:providers": "npm run build && npx playwright test e2e/provider-flows.test.ts",
50779
50788
  "rebuild:native:node": `node -e "try{require('better-sqlite3')(':memory:').close()}catch{process.exit(1)}" || npx --yes node-gyp rebuild --directory=node_modules/better-sqlite3`,
50780
50789
  prepublishOnly: "npm run build:mcp"
50781
50790
  },
@@ -52517,7 +52526,7 @@ var init_updateChecker = __esm({
52517
52526
  function getVersion3() {
52518
52527
  if (cachedVersion) return cachedVersion;
52519
52528
  try {
52520
- cachedVersion = require_package().version;
52529
+ cachedVersion = true ? "0.1.11" : null.version;
52521
52530
  } catch {
52522
52531
  cachedVersion = "unknown";
52523
52532
  }
@@ -52713,12 +52722,12 @@ function registerStatusRoutes(router) {
52713
52722
  const ollama = await checkOllama();
52714
52723
  const resources = getResources();
52715
52724
  const deploymentMode = getDeploymentMode();
52725
+ const isCloud = deploymentMode === "cloud";
52716
52726
  return {
52717
52727
  data: {
52718
52728
  version: getVersion3(),
52719
52729
  uptime: Math.floor((Date.now() - startedAt) / 1e3),
52720
- dataDir,
52721
- dbPath,
52730
+ ...!isCloud && { dataDir, dbPath },
52722
52731
  claude,
52723
52732
  codex,
52724
52733
  ollama,
@@ -57952,6 +57961,23 @@ function serveStatic(staticDir, pathname, res) {
57952
57961
  res.end("Not Found");
57953
57962
  }
57954
57963
  }
57964
+ function checkRateLimit2(ip, method) {
57965
+ const isWrite = method !== "GET" && method !== "HEAD" && method !== "OPTIONS";
57966
+ const limit = isWrite ? RATE_LIMIT_WRITE : RATE_LIMIT_READ;
57967
+ const key = `${ip}:${isWrite ? "w" : "r"}`;
57968
+ const now = Date.now();
57969
+ let bucket = rateBuckets.get(key);
57970
+ if (!bucket || now >= bucket.resetAt) {
57971
+ bucket = { count: 0, resetAt: now + RATE_LIMIT_WINDOW_MS };
57972
+ rateBuckets.set(key, bucket);
57973
+ }
57974
+ bucket.count++;
57975
+ if (bucket.count > limit) {
57976
+ const retryAfter = Math.ceil((bucket.resetAt - now) / 1e3);
57977
+ return { allowed: false, retryAfter };
57978
+ }
57979
+ return { allowed: true, retryAfter: 0 };
57980
+ }
57955
57981
  function createApiServer(options = {}) {
57956
57982
  const db3 = options.db ?? getServerDatabase();
57957
57983
  const port = options.port ?? DEFAULT_PORT;
@@ -57976,7 +58002,20 @@ function createApiServer(options = {}) {
57976
58002
  const responseHeaders = {
57977
58003
  "Content-Type": "application/json"
57978
58004
  };
58005
+ if (isCloudDeployment()) {
58006
+ Object.assign(responseHeaders, CLOUD_SECURITY_HEADERS);
58007
+ }
57979
58008
  setCorsHeaders(origin, responseHeaders);
58009
+ if (isCloudDeployment() && pathname.startsWith("/api/")) {
58010
+ const clientIp = req.socket.remoteAddress || "unknown";
58011
+ const rl = checkRateLimit2(clientIp, req.method || "GET");
58012
+ if (!rl.allowed) {
58013
+ responseHeaders["Retry-After"] = String(rl.retryAfter);
58014
+ res.writeHead(429, responseHeaders);
58015
+ res.end(JSON.stringify({ error: "Too many requests" }));
58016
+ return;
58017
+ }
58018
+ }
57980
58019
  const isSameOrigin = origin && req.headers.host && (() => {
57981
58020
  try {
57982
58021
  return new import_node_url.URL(origin).host === req.headers.host;
@@ -58210,7 +58249,7 @@ function startServer(options = {}) {
58210
58249
  process.exit(0);
58211
58250
  });
58212
58251
  }
58213
- 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;
58252
+ 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, RATE_LIMIT_WINDOW_MS, RATE_LIMIT_READ, RATE_LIMIT_WRITE, rateBuckets, CLOUD_SECURITY_HEADERS;
58214
58253
  var init_server4 = __esm({
58215
58254
  "src/server/index.ts"() {
58216
58255
  "use strict";
@@ -58254,6 +58293,22 @@ var init_server4 = __esm({
58254
58293
  ".webp": "image/webp",
58255
58294
  ".webmanifest": "application/manifest+json"
58256
58295
  };
58296
+ RATE_LIMIT_WINDOW_MS = 6e4;
58297
+ RATE_LIMIT_READ = 300;
58298
+ RATE_LIMIT_WRITE = 120;
58299
+ rateBuckets = /* @__PURE__ */ new Map();
58300
+ setInterval(() => {
58301
+ const now = Date.now();
58302
+ for (const [key, bucket] of rateBuckets) {
58303
+ if (now >= bucket.resetAt) rateBuckets.delete(key);
58304
+ }
58305
+ }, 12e4).unref();
58306
+ CLOUD_SECURITY_HEADERS = {
58307
+ "Strict-Transport-Security": "max-age=31536000; includeSubDomains",
58308
+ "X-Content-Type-Options": "nosniff",
58309
+ "X-Frame-Options": "DENY",
58310
+ "Referrer-Policy": "strict-origin-when-cross-origin"
58311
+ };
58257
58312
  }
58258
58313
  });
58259
58314
 
package/out/mcp/server.js CHANGED
@@ -47101,7 +47101,7 @@ function registerResourceTools(server) {
47101
47101
  async function main() {
47102
47102
  const server = new McpServer({
47103
47103
  name: "quoroom",
47104
- version: true ? "0.1.8" : "0.0.0"
47104
+ version: true ? "0.1.11" : "0.0.0"
47105
47105
  });
47106
47106
  registerMemoryTools(server);
47107
47107
  registerSchedulerTools(server);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "quoroom",
3
- "version": "0.1.8",
3
+ "version": "0.1.11",
4
4
  "description": "Autonomous AI agent collective engine — Queen, Workers, Quorum",
5
5
  "main": "./out/mcp/server.js",
6
6
  "bin": {
@@ -22,16 +22,28 @@
22
22
  "build": "npm run typecheck && npm run build:mcp && npm run build:ui",
23
23
  "build:mcp": "node scripts/build-mcp.js",
24
24
  "build:ui": "vite build --config src/ui/vite.config.ts",
25
- "dev": "sh -c 'trap \"kill 0\" INT TERM EXIT; npm run dev:room & npm run dev:cloud & wait'",
26
- "dev:room": "npm run build:mcp && npm run build:ui && node scripts/dev-server.js",
27
- "dev:cloud": "cd ../cloud && PORT=3710 CLOUD_PUBLIC_URL=http://127.0.0.1:3710 CLOUD_ALLOWED_ORIGINS='http://127.0.0.1:3710,http://localhost:3710,http://localhost:5173,http://127.0.0.1:5173,https://quoroom.ai,https://www.quoroom.ai,https://app.quoroom.ai' npm start",
25
+ "kill:ports": "node scripts/kill-ports.js",
26
+ "kill:dev-ports": "npm run kill:ports -- 4700 3710",
27
+ "dev:links": "node scripts/dev-links.js",
28
+ "dev": "sh -c 'npm run kill:dev-ports && trap \"kill 0\" INT TERM EXIT; npm run dev:links & npm run dev:room & npm run dev:cloud & wait'",
29
+ "dev:room": "sh -c 'export QUOROOM_DATA_DIR=$HOME/.quoroom-dev QUOROOM_SKIP_MCP_REGISTER=1; npm run build:mcp && npm run build:ui && node scripts/dev-server.js --port 4700'",
30
+ "dev:room:isolated": "sh -c 'export QUOROOM_DATA_DIR=$HOME/.quoroom-dev QUOROOM_SKIP_MCP_REGISTER=1; npm run build:mcp && npm run build:ui && node scripts/dev-server.js --port 4700'",
31
+ "dev:room:shared": "npm run build:mcp && npm run build:ui && node scripts/dev-server.js",
32
+ "doctor:split": "node scripts/doctor-split.js",
33
+ "dev:isolated": "sh -c 'npm run kill:dev-ports && trap \"kill 0\" INT TERM EXIT; npm run dev:links & npm run dev:room:isolated & npm run dev:cloud & wait'",
34
+ "dev:cloud": "sh -c 'npm run kill:ports -- 3710 && cd ../cloud && PORT=3710 CLOUD_PUBLIC_URL=http://127.0.0.1:3710 CLOUD_ALLOWED_ORIGINS='\"'\"'http://127.0.0.1:3710,http://localhost:3710,http://localhost:5173,http://127.0.0.1:5173,https://quoroom.ai,https://www.quoroom.ai,https://app.quoroom.ai'\"'\"' npm start'",
28
35
  "dev:ui": "vite --config src/ui/vite.config.ts",
29
36
  "seed:style-demo": "node scripts/seed-style-demo.js",
30
37
  "typecheck": "tsc --noEmit",
31
38
  "test": "npm run rebuild:native:node && vitest run --pool=forks --passWithNoTests",
32
39
  "test:watch": "npm run rebuild:native:node && vitest --pool=forks",
40
+ "test:quick": "npm run typecheck && npm run test",
41
+ "test:smart-e2e": "git diff --cached --name-only | grep -qE '^(src/ui/|src/server/|e2e/|playwright)' && npm run test:e2e || echo 'No UI/server changes — skipping E2E'",
33
42
  "test:e2e": "npm run build && npx playwright test",
34
43
  "test:e2e:ui": "npm run build && npx playwright test e2e/ui.test.ts",
44
+ "test:e2e:setup": "npm run build && npx playwright test e2e/setup-flow.test.ts",
45
+ "test:e2e:setup:headed": "npm run build && npx playwright test e2e/setup-flow.test.ts --headed --project=chromium",
46
+ "test:e2e:providers": "npm run build && npx playwright test e2e/provider-flows.test.ts",
35
47
  "rebuild:native:node": "node -e \"try{require('better-sqlite3')(':memory:').close()}catch{process.exit(1)}\" || npx --yes node-gyp rebuild --directory=node_modules/better-sqlite3",
36
48
  "prepublishOnly": "npm run build:mcp"
37
49
  },