granclaw 0.0.1-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +61 -0
  3. package/bin/granclaw.js +2 -0
  4. package/dist/backend/agent/process.js +246 -0
  5. package/dist/backend/agent/runner-pi.js +993 -0
  6. package/dist/backend/agent/runner.js +334 -0
  7. package/dist/backend/agent/telegram-adapter.js +261 -0
  8. package/dist/backend/agent/telegram-http-client.js +133 -0
  9. package/dist/backend/agent-db.js +108 -0
  10. package/dist/backend/assets/stealth-extension/manifest.json +15 -0
  11. package/dist/backend/assets/stealth-extension/stealth.js +220 -0
  12. package/dist/backend/browser/session-manager.js +213 -0
  13. package/dist/backend/browser/stealth.js +140 -0
  14. package/dist/backend/browser-sessions.js +197 -0
  15. package/dist/backend/config.js +57 -0
  16. package/dist/backend/data-db.js +99 -0
  17. package/dist/backend/esm-import.js +25 -0
  18. package/dist/backend/index.js +53 -0
  19. package/dist/backend/lib/i18n-telegram.js +104 -0
  20. package/dist/backend/logs-db.js +51 -0
  21. package/dist/backend/messages-db.js +112 -0
  22. package/dist/backend/orchestrator/agent-manager.js +139 -0
  23. package/dist/backend/orchestrator/browser-live.js +533 -0
  24. package/dist/backend/orchestrator/server.js +1669 -0
  25. package/dist/backend/providers-config.js +138 -0
  26. package/dist/backend/routes/logs.js +20 -0
  27. package/dist/backend/scheduler.js +66 -0
  28. package/dist/backend/schedules-db.js +125 -0
  29. package/dist/backend/secrets-vault.js +33 -0
  30. package/dist/backend/takeover-messages.js +45 -0
  31. package/dist/backend/takeover-state.js +101 -0
  32. package/dist/backend/takeover-timeout.js +51 -0
  33. package/dist/backend/tasks-db.js +115 -0
  34. package/dist/backend/usage-scanner.js +109 -0
  35. package/dist/backend/workflows/runner.js +267 -0
  36. package/dist/backend/workflows-db.js +235 -0
  37. package/dist/backend/workspace-pool.js +189 -0
  38. package/dist/frontend/assets/index-CZcU3XNC.js +143 -0
  39. package/dist/frontend/assets/index-CkgRytfR.css +1 -0
  40. package/dist/frontend/browser-onboarding.png +0 -0
  41. package/dist/frontend/chat-history-options.html +304 -0
  42. package/dist/frontend/granclaw-logo.png +0 -0
  43. package/dist/frontend/index.html +36 -0
  44. package/dist/home.js +51 -0
  45. package/dist/index.js +159 -0
  46. package/package.json +58 -0
  47. package/templates/AGENT.onboarding.md +74 -0
  48. package/templates/SYSTEM.md +58 -0
  49. package/templates/agents.config.json +3 -0
  50. package/templates/skills/housekeeping/SKILL.md +202 -0
  51. package/templates/skills/memory/SKILL.md +109 -0
  52. package/templates/skills/schedules/SKILL.md +80 -0
  53. package/templates/skills/workflows/SKILL.md +315 -0
@@ -0,0 +1,138 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.listProviders = listProviders;
7
+ exports.getProvider = getProvider;
8
+ exports.getProviderApiKey = getProviderApiKey;
9
+ exports.saveProvider = saveProvider;
10
+ exports.removeProvider = removeProvider;
11
+ exports.clearProvider = clearProvider;
12
+ exports.getSearchApiKey = getSearchApiKey;
13
+ exports.saveSearch = saveSearch;
14
+ exports.clearSearch = clearSearch;
15
+ // packages/backend/src/providers-config.ts
16
+ const fs_1 = __importDefault(require("fs"));
17
+ const path_1 = __importDefault(require("path"));
18
+ const config_js_1 = require("./config.js");
19
+ /** Resolved on every call so PROVIDERS_CONFIG_PATH can be set per-test. */
20
+ function configPath() {
21
+ const envPath = process.env.PROVIDERS_CONFIG_PATH?.trim();
22
+ if (envPath)
23
+ return path_1.default.resolve(envPath);
24
+ return path_1.default.join(config_js_1.GRANCLAW_HOME, 'providers.config.json');
25
+ }
26
+ // ── Internal helpers ──────────────────────────────────────────────────────────
27
+ function readConfig() {
28
+ try {
29
+ return JSON.parse(fs_1.default.readFileSync(configPath(), 'utf8'));
30
+ }
31
+ catch {
32
+ return {};
33
+ }
34
+ }
35
+ /** Returns providers map, transparently migrating from legacy `active` format. */
36
+ function getProvidersMap(cfg) {
37
+ if (cfg.providers)
38
+ return { ...cfg.providers };
39
+ if (cfg.active) {
40
+ return { [cfg.active.provider]: { model: cfg.active.model, apiKey: cfg.active.apiKey } };
41
+ }
42
+ return {};
43
+ }
44
+ function writeConfig(cfg, providers) {
45
+ // Always write in the new format — drop legacy `active` field
46
+ const { active: _removed, ...rest } = cfg;
47
+ const dir = path_1.default.dirname(configPath());
48
+ fs_1.default.mkdirSync(dir, { recursive: true });
49
+ const tmp = configPath() + '.tmp';
50
+ fs_1.default.writeFileSync(tmp, JSON.stringify({ ...rest, providers }, null, 2));
51
+ fs_1.default.renameSync(tmp, configPath());
52
+ }
53
+ // ── Public API ────────────────────────────────────────────────────────────────
54
+ /** List all configured providers. Never includes apiKey. */
55
+ function listProviders() {
56
+ const cfg = readConfig();
57
+ const map = getProvidersMap(cfg);
58
+ return Object.entries(map).map(([provider, entry]) => ({ provider, model: entry.model }));
59
+ }
60
+ /**
61
+ * Returns { provider, model } for a specific provider, or the first one if no arg.
62
+ * Never returns the apiKey.
63
+ */
64
+ function getProvider(provider) {
65
+ const cfg = readConfig();
66
+ const map = getProvidersMap(cfg);
67
+ if (provider) {
68
+ const entry = map[provider];
69
+ return entry ? { provider, model: entry.model } : null;
70
+ }
71
+ const first = Object.entries(map)[0];
72
+ return first ? { provider: first[0], model: first[1].model } : null;
73
+ }
74
+ /** Server-side only — returns the raw API key for a specific provider (or first). */
75
+ function getProviderApiKey(provider) {
76
+ const cfg = readConfig();
77
+ const map = getProvidersMap(cfg);
78
+ if (provider)
79
+ return map[provider]?.apiKey ?? null;
80
+ return Object.values(map)[0]?.apiKey ?? null;
81
+ }
82
+ /** Upsert a provider entry. */
83
+ function saveProvider(provider, model, apiKey) {
84
+ const cfg = readConfig();
85
+ const map = getProvidersMap(cfg);
86
+ map[provider] = { model, apiKey };
87
+ writeConfig(cfg, map);
88
+ }
89
+ /** Remove a specific provider. No-op if not found. */
90
+ function removeProvider(provider) {
91
+ const cfg = readConfig();
92
+ const map = getProvidersMap(cfg);
93
+ delete map[provider];
94
+ writeConfig(cfg, map);
95
+ }
96
+ /** Remove all provider configs. */
97
+ function clearProvider() {
98
+ const cfg = readConfig();
99
+ writeConfig(cfg, {});
100
+ }
101
+ /** Returns the Brave Search API key, or null if not configured. */
102
+ function getSearchApiKey() {
103
+ try {
104
+ const raw = fs_1.default.readFileSync(configPath(), 'utf8');
105
+ const parsed = JSON.parse(raw);
106
+ return parsed.search?.apiKey ?? null;
107
+ }
108
+ catch {
109
+ return null;
110
+ }
111
+ }
112
+ function saveSearch(apiKey) {
113
+ let existing = {};
114
+ try {
115
+ existing = JSON.parse(fs_1.default.readFileSync(configPath(), 'utf8'));
116
+ }
117
+ catch { /* first write */ }
118
+ const dir = path_1.default.dirname(configPath());
119
+ fs_1.default.mkdirSync(dir, { recursive: true });
120
+ const tmp = configPath() + '.tmp';
121
+ fs_1.default.writeFileSync(tmp, JSON.stringify({ ...existing, search: { provider: 'brave', apiKey } }, null, 2));
122
+ fs_1.default.renameSync(tmp, configPath());
123
+ }
124
+ function clearSearch() {
125
+ let existing = {};
126
+ try {
127
+ existing = JSON.parse(fs_1.default.readFileSync(configPath(), 'utf8'));
128
+ }
129
+ catch {
130
+ return;
131
+ }
132
+ const { search: _removed, ...rest } = existing;
133
+ const dir = path_1.default.dirname(configPath());
134
+ fs_1.default.mkdirSync(dir, { recursive: true });
135
+ const tmp = configPath() + '.tmp';
136
+ fs_1.default.writeFileSync(tmp, JSON.stringify(rest, null, 2));
137
+ fs_1.default.renameSync(tmp, configPath());
138
+ }
@@ -0,0 +1,20 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const express_1 = require("express");
4
+ const logs_db_js_1 = require("../logs-db.js");
5
+ const router = (0, express_1.Router)();
6
+ // GET /logs?agentId=&type=&search=&from=<epoch_ms>&to=<epoch_ms>&limit=50&offset=0
7
+ router.get('/', (req, res) => {
8
+ const { agentId, type, search, from, to, limit = '50', offset = '0' } = req.query;
9
+ const result = (0, logs_db_js_1.queryActions)({
10
+ agentId: agentId,
11
+ type: type,
12
+ search: search,
13
+ from: from ? Number(from) : undefined,
14
+ to: to ? Number(to) : undefined,
15
+ limit: Number(limit),
16
+ offset: Number(offset),
17
+ });
18
+ res.json({ ...result, limit: Number(limit), offset: Number(offset) });
19
+ });
20
+ exports.default = router;
@@ -0,0 +1,66 @@
1
+ "use strict";
2
+ /**
3
+ * scheduler.ts
4
+ *
5
+ * Runs inside the orchestrator process. Every 60 seconds, checks all
6
+ * active schedules across all agents. When a schedule is due, enqueues
7
+ * the message into the agent's job queue.
8
+ */
9
+ var __importDefault = (this && this.__importDefault) || function (mod) {
10
+ return (mod && mod.__esModule) ? mod : { "default": mod };
11
+ };
12
+ Object.defineProperty(exports, "__esModule", { value: true });
13
+ exports.startScheduler = startScheduler;
14
+ exports.stopScheduler = stopScheduler;
15
+ const cron_parser_1 = require("cron-parser");
16
+ const path_1 = __importDefault(require("path"));
17
+ const agent_manager_js_1 = require("./orchestrator/agent-manager.js");
18
+ const schedules_db_js_1 = require("./schedules-db.js");
19
+ const agent_db_js_1 = require("./agent-db.js");
20
+ const config_js_1 = require("./config.js");
21
+ const POLL_INTERVAL_MS = 60_000;
22
+ function getNextRun(cronExpr, timezone) {
23
+ const interval = (0, cron_parser_1.parseExpression)(cronExpr, { tz: timezone });
24
+ return interval.next().getTime();
25
+ }
26
+ function tick() {
27
+ const agents = (0, agent_manager_js_1.getManagedAgents)();
28
+ for (const managed of agents) {
29
+ const agentId = managed.config.id;
30
+ let due;
31
+ try {
32
+ due = (0, schedules_db_js_1.getDueSchedules)(agentId);
33
+ }
34
+ catch {
35
+ continue; // DB not yet created for this agent
36
+ }
37
+ for (const schedule of due) {
38
+ const workspaceDir = path_1.default.resolve(config_js_1.REPO_ROOT, managed.config.workspaceDir);
39
+ (0, agent_db_js_1.enqueue)(workspaceDir, agentId, schedule.message, 'schedule');
40
+ console.log(`[scheduler] triggered "${schedule.name}" for agent "${agentId}"`);
41
+ let nextRun;
42
+ try {
43
+ nextRun = getNextRun(schedule.cron, schedule.timezone);
44
+ }
45
+ catch {
46
+ console.error(`[scheduler] invalid cron "${schedule.cron}" for schedule ${schedule.id}, pausing`);
47
+ (0, schedules_db_js_1.updateSchedule)(agentId, schedule.id, { status: 'paused' });
48
+ continue;
49
+ }
50
+ (0, schedules_db_js_1.updateSchedule)(agentId, schedule.id, { lastRun: Date.now(), nextRun });
51
+ }
52
+ }
53
+ }
54
+ let intervalHandle = null;
55
+ function startScheduler() {
56
+ if (intervalHandle)
57
+ return;
58
+ console.log(`[scheduler] started (polling every ${POLL_INTERVAL_MS / 1000}s)`);
59
+ intervalHandle = setInterval(tick, POLL_INTERVAL_MS);
60
+ }
61
+ function stopScheduler() {
62
+ if (intervalHandle) {
63
+ clearInterval(intervalHandle);
64
+ intervalHandle = null;
65
+ }
66
+ }
@@ -0,0 +1,125 @@
1
+ "use strict";
2
+ /**
3
+ * schedules-db.ts
4
+ *
5
+ * Per-agent SQLite store for cron schedules.
6
+ * Backed by <workspaceDir>/agent.sqlite (shared via workspace-pool).
7
+ */
8
+ var __importDefault = (this && this.__importDefault) || function (mod) {
9
+ return (mod && mod.__esModule) ? mod : { "default": mod };
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.listSchedules = listSchedules;
13
+ exports.getSchedule = getSchedule;
14
+ exports.createSchedule = createSchedule;
15
+ exports.updateSchedule = updateSchedule;
16
+ exports.deleteSchedule = deleteSchedule;
17
+ exports.getDueSchedules = getDueSchedules;
18
+ exports.createScheduleRun = createScheduleRun;
19
+ exports.listScheduleRuns = listScheduleRuns;
20
+ exports.closeSchedulesDb = closeSchedulesDb;
21
+ const path_1 = __importDefault(require("path"));
22
+ const crypto_1 = require("crypto");
23
+ const config_js_1 = require("./config.js");
24
+ const workspace_pool_js_1 = require("./workspace-pool.js");
25
+ // ── Internal DB accessor ──────────────────────────────────────────────────
26
+ function getDb(agentId) {
27
+ const agent = (0, config_js_1.getAgent)(agentId);
28
+ if (!agent)
29
+ throw new Error(`Agent "${agentId}" not found in config`);
30
+ return (0, workspace_pool_js_1.getWorkspaceDb)(path_1.default.resolve(config_js_1.REPO_ROOT, agent.workspaceDir));
31
+ }
32
+ // ── Row mapper ────────────────────────────────────────────────────────────
33
+ function rowToSchedule(r) {
34
+ return {
35
+ id: r.id,
36
+ agentId: r.agent_id,
37
+ name: r.name,
38
+ message: r.message,
39
+ cron: r.cron,
40
+ timezone: r.timezone,
41
+ status: r.status,
42
+ nextRun: r.next_run ?? null,
43
+ lastRun: r.last_run ?? null,
44
+ createdAt: r.created_at,
45
+ };
46
+ }
47
+ // ── ID generation ─────────────────────────────────────────────────────────
48
+ function nextScheduleId(db) {
49
+ const row = db.prepare(`SELECT COALESCE(MAX(CAST(SUBSTR(id, 5) AS INTEGER)), 0) + 1 AS next FROM schedules`).get();
50
+ return `SCH-${String(row.next).padStart(3, '0')}`;
51
+ }
52
+ // ── CRUD ──────────────────────────────────────────────────────────────────
53
+ function listSchedules(agentId) {
54
+ const db = getDb(agentId);
55
+ return db.prepare(`SELECT * FROM schedules WHERE agent_id = ? ORDER BY created_at DESC`).all(agentId).map(rowToSchedule);
56
+ }
57
+ function getSchedule(agentId, scheduleId) {
58
+ const db = getDb(agentId);
59
+ const row = db.prepare(`SELECT * FROM schedules WHERE id = ? AND agent_id = ?`).get(scheduleId, agentId);
60
+ return row ? rowToSchedule(row) : null;
61
+ }
62
+ function createSchedule(agentId, data) {
63
+ const db = getDb(agentId);
64
+ const id = nextScheduleId(db);
65
+ const now = Date.now();
66
+ db.prepare(`
67
+ INSERT INTO schedules (id, agent_id, name, message, cron, timezone, status, next_run, created_at)
68
+ VALUES (?, ?, ?, ?, ?, ?, 'active', ?, ?)
69
+ `).run(id, agentId, data.name, data.message, data.cron, data.timezone ?? 'Asia/Singapore', data.nextRun, now);
70
+ return getSchedule(agentId, id);
71
+ }
72
+ function updateSchedule(agentId, scheduleId, data) {
73
+ const db = getDb(agentId);
74
+ const existing = getSchedule(agentId, scheduleId);
75
+ if (!existing)
76
+ return null;
77
+ db.prepare(`
78
+ UPDATE schedules SET name = ?, message = ?, cron = ?, timezone = ?, status = ?, next_run = ?, last_run = ?
79
+ WHERE id = ? AND agent_id = ?
80
+ `).run(data.name ?? existing.name, data.message ?? existing.message, data.cron ?? existing.cron, data.timezone ?? existing.timezone, data.status ?? existing.status, data.nextRun ?? existing.nextRun, data.lastRun ?? existing.lastRun, scheduleId, agentId);
81
+ return getSchedule(agentId, scheduleId);
82
+ }
83
+ function deleteSchedule(agentId, scheduleId) {
84
+ const db = getDb(agentId);
85
+ const result = db.prepare(`DELETE FROM schedules WHERE id = ? AND agent_id = ?`).run(scheduleId, agentId);
86
+ return result.changes > 0;
87
+ }
88
+ function getDueSchedules(agentId) {
89
+ const db = getDb(agentId);
90
+ const now = Date.now();
91
+ return db.prepare(`SELECT * FROM schedules WHERE agent_id = ? AND status = 'active' AND next_run IS NOT NULL AND next_run <= ?`).all(agentId, now).map(rowToSchedule);
92
+ }
93
+ // ── Schedule Runs ─────────────────────────────────────────────────────────
94
+ function createScheduleRun(agentId, scheduleId) {
95
+ const db = getDb(agentId);
96
+ const id = (0, crypto_1.randomUUID)();
97
+ const channelId = `sch-${id}`;
98
+ const startedAt = Date.now();
99
+ db.prepare(`
100
+ INSERT INTO schedule_runs (id, schedule_id, agent_id, channel_id, started_at)
101
+ VALUES (?, ?, ?, ?, ?)
102
+ `).run(id, scheduleId, agentId, channelId, startedAt);
103
+ return { id, scheduleId, agentId, channelId, startedAt };
104
+ }
105
+ function listScheduleRuns(agentId, scheduleId, limit = 20) {
106
+ const db = getDb(agentId);
107
+ const rows = db.prepare(`
108
+ SELECT id, schedule_id, agent_id, channel_id, started_at
109
+ FROM schedule_runs
110
+ WHERE schedule_id = ? AND agent_id = ?
111
+ ORDER BY started_at DESC
112
+ LIMIT ?
113
+ `).all(scheduleId, agentId, limit);
114
+ return rows.map(r => ({
115
+ id: r.id, scheduleId: r.schedule_id, agentId: r.agent_id,
116
+ channelId: r.channel_id, startedAt: r.started_at,
117
+ }));
118
+ }
119
+ // ── Lifecycle ─────────────────────────────────────────────────────────────
120
+ function closeSchedulesDb(agentId) {
121
+ const agent = (0, config_js_1.getAgent)(agentId);
122
+ if (!agent)
123
+ return;
124
+ (0, workspace_pool_js_1.closeWorkspaceDb)(path_1.default.resolve(config_js_1.REPO_ROOT, agent.workspaceDir));
125
+ }
@@ -0,0 +1,33 @@
1
+ "use strict";
2
+ /**
3
+ * secrets-vault.ts
4
+ *
5
+ * Per-agent secrets store.
6
+ * Backed by data/system.sqlite (shared global DB via getDataDb()).
7
+ */
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.listSecretNames = listSecretNames;
10
+ exports.getSecrets = getSecrets;
11
+ exports.setSecret = setSecret;
12
+ exports.deleteSecret = deleteSecret;
13
+ exports.deleteAllSecrets = deleteAllSecrets;
14
+ const data_db_js_1 = require("./data-db.js");
15
+ function listSecretNames(agentId) {
16
+ return (0, data_db_js_1.getDataDb)().prepare(`SELECT name FROM secrets WHERE agent_id = ? ORDER BY created_at`).all(agentId).map((r) => r.name);
17
+ }
18
+ function getSecrets(agentId) {
19
+ const rows = (0, data_db_js_1.getDataDb)().prepare(`SELECT name, value FROM secrets WHERE agent_id = ?`).all(agentId);
20
+ return Object.fromEntries(rows.map((r) => [r.name, r.value]));
21
+ }
22
+ function setSecret(agentId, name, value) {
23
+ (0, data_db_js_1.getDataDb)().prepare(`
24
+ INSERT INTO secrets (agent_id, name, value) VALUES (?, ?, ?)
25
+ ON CONFLICT(agent_id, name) DO UPDATE SET value = excluded.value
26
+ `).run(agentId, name, value);
27
+ }
28
+ function deleteSecret(agentId, name) {
29
+ (0, data_db_js_1.getDataDb)().prepare(`DELETE FROM secrets WHERE agent_id = ? AND name = ?`).run(agentId, name);
30
+ }
31
+ function deleteAllSecrets(agentId) {
32
+ (0, data_db_js_1.getDataDb)().prepare(`DELETE FROM secrets WHERE agent_id = ?`).run(agentId);
33
+ }
@@ -0,0 +1,45 @@
1
+ "use strict";
2
+ /**
3
+ * takeover-messages.ts
4
+ *
5
+ * System messages sent back to the agent when a human browser takeover
6
+ * finishes. Two paths:
7
+ *
8
+ * 1. User clicked Completed — may include an optional note describing
9
+ * what they did. Built by `formatTakeoverResumeMessage`.
10
+ * 2. 10-minute timeout — user never clicked Done. See TAKEOVER_TIMEOUT_MESSAGE
11
+ * re-exported here for a single "where takeover messages live" home.
12
+ *
13
+ * Extracted so tests can pin the exact wire format — agents parse these
14
+ * strings (or their prompts do), so format drift is a silent regression.
15
+ */
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ exports.TAKEOVER_TIMEOUT_MESSAGE = void 0;
18
+ exports.formatTakeoverResumeMessage = formatTakeoverResumeMessage;
19
+ var takeover_timeout_js_1 = require("./takeover-timeout.js");
20
+ Object.defineProperty(exports, "TAKEOVER_TIMEOUT_MESSAGE", { enumerable: true, get: function () { return takeover_timeout_js_1.TAKEOVER_TIMEOUT_MESSAGE; } });
21
+ /** Hard cap on note length — prevents abuse from a long submission. */
22
+ const MAX_NOTE_LENGTH = 2000;
23
+ /**
24
+ * Build the system message sent to the agent when the user clicks
25
+ * "Completed" on the takeover page.
26
+ *
27
+ * The agent sees this message in its conversation stream and can decide
28
+ * what to do next. The format is:
29
+ *
30
+ * [User completed browser takeover. Note: "they typed this"]
31
+ *
32
+ * or, if no note:
33
+ *
34
+ * [User completed browser takeover with no note]
35
+ *
36
+ * The note is JSON-encoded so quote characters inside the note can't
37
+ * confuse prompt parsing.
38
+ */
39
+ function formatTakeoverResumeMessage(rawNote) {
40
+ const stringNote = typeof rawNote === 'string' ? rawNote.trim() : '';
41
+ const note = stringNote.slice(0, MAX_NOTE_LENGTH);
42
+ return note
43
+ ? `[User completed browser takeover. Note: ${JSON.stringify(note)}]`
44
+ : '[User completed browser takeover with no note]';
45
+ }
@@ -0,0 +1,101 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.TAKEOVER_TIMEOUT_MS = void 0;
4
+ exports.getTakeoverByTokenFromDb = getTakeoverByTokenFromDb;
5
+ exports.clearTakeoverFromDb = clearTakeoverFromDb;
6
+ exports.setTakeover = setTakeover;
7
+ exports.getTakeover = getTakeover;
8
+ exports.getTakeoverByToken = getTakeoverByToken;
9
+ exports.hasTakeover = hasTakeover;
10
+ exports.cancelTakeoverTimer = cancelTakeoverTimer;
11
+ exports.clearTakeover = clearTakeover;
12
+ exports.updateTakeoverTimer = updateTakeoverTimer;
13
+ const data_db_js_1 = require("./data-db.js");
14
+ exports.TAKEOVER_TIMEOUT_MS = 10 * 60 * 1000; // 10 minutes
15
+ // ── In-memory state (agent process only — handle + timer can't be serialized) ──
16
+ const byAgent = new Map();
17
+ const byToken = new Map(); // token → agentId
18
+ // ── SQLite helpers (cross-process) ─────────────────────────────────────────────
19
+ function dbInsert(entry) {
20
+ try {
21
+ (0, data_db_js_1.getDataDb)().prepare(`
22
+ INSERT OR REPLACE INTO takeovers (token, agent_id, channel_id, session_id, reason, url, requested_at)
23
+ VALUES (?, ?, ?, ?, ?, ?, ?)
24
+ `).run(entry.token, entry.agentId, entry.channelId, entry.handle.sessionId, entry.reason, entry.url ?? null, entry.requestedAt);
25
+ }
26
+ catch (err) {
27
+ console.error('[takeover-state] dbInsert failed', err);
28
+ }
29
+ }
30
+ function dbDeleteByAgent(agentId) {
31
+ try {
32
+ (0, data_db_js_1.getDataDb)().prepare(`DELETE FROM takeovers WHERE agent_id = ?`).run(agentId);
33
+ }
34
+ catch (err) {
35
+ console.error('[takeover-state] dbDeleteByAgent failed', err);
36
+ }
37
+ }
38
+ /** Look up a takeover by token from SQLite — usable from any process. */
39
+ function getTakeoverByTokenFromDb(token) {
40
+ try {
41
+ const row = (0, data_db_js_1.getDataDb)().prepare(`SELECT token, agent_id, channel_id, session_id, reason, url, requested_at FROM takeovers WHERE token = ?`).get(token);
42
+ return row ?? null;
43
+ }
44
+ catch (err) {
45
+ console.error('[takeover-state] getTakeoverByTokenFromDb failed', err);
46
+ return null;
47
+ }
48
+ }
49
+ /** Delete a takeover by agent ID from SQLite — usable from any process. */
50
+ function clearTakeoverFromDb(agentId) {
51
+ dbDeleteByAgent(agentId);
52
+ }
53
+ // ── In-memory API (agent process only) ─────────────────────────────────────────
54
+ function setTakeover(agentId, entry) {
55
+ const existing = byAgent.get(agentId);
56
+ if (existing) {
57
+ if (existing.timer)
58
+ clearTimeout(existing.timer);
59
+ byToken.delete(existing.token);
60
+ }
61
+ byAgent.set(agentId, { ...entry, timer: null });
62
+ byToken.set(entry.token, agentId);
63
+ dbInsert(entry);
64
+ }
65
+ function getTakeover(agentId) {
66
+ return byAgent.get(agentId) ?? null;
67
+ }
68
+ function getTakeoverByToken(token) {
69
+ const agentId = byToken.get(token);
70
+ if (!agentId)
71
+ return null;
72
+ return byAgent.get(agentId) ?? null;
73
+ }
74
+ function hasTakeover(agentId) {
75
+ return byAgent.has(agentId);
76
+ }
77
+ function cancelTakeoverTimer(agentId) {
78
+ const entry = byAgent.get(agentId);
79
+ if (entry?.timer) {
80
+ clearTimeout(entry.timer);
81
+ entry.timer = null;
82
+ }
83
+ }
84
+ function clearTakeover(agentId) {
85
+ const entry = byAgent.get(agentId);
86
+ if (!entry)
87
+ return;
88
+ if (entry.timer)
89
+ clearTimeout(entry.timer);
90
+ byToken.delete(entry.token);
91
+ byAgent.delete(agentId);
92
+ dbDeleteByAgent(agentId);
93
+ }
94
+ function updateTakeoverTimer(agentId, timer) {
95
+ const entry = byAgent.get(agentId);
96
+ if (entry) {
97
+ if (entry.timer)
98
+ clearTimeout(entry.timer);
99
+ entry.timer = timer;
100
+ }
101
+ }
@@ -0,0 +1,51 @@
1
+ "use strict";
2
+ /**
3
+ * takeover-timeout.ts
4
+ *
5
+ * Helper for the human-browser-takeover 10-minute timeout path.
6
+ *
7
+ * Extracted from agent/process.ts so it can be unit tested without spinning
8
+ * up an agent subprocess. The timer itself is still armed by process.ts —
9
+ * this module only owns the callback body: the set of side effects that
10
+ * should happen when the user walks away from the takeover tab.
11
+ */
12
+ Object.defineProperty(exports, "__esModule", { value: true });
13
+ exports.TAKEOVER_TIMEOUT_MESSAGE = void 0;
14
+ exports.handleTakeoverTimeout = handleTakeoverTimeout;
15
+ const takeover_state_js_1 = require("./takeover-state.js");
16
+ const session_manager_js_1 = require("./browser/session-manager.js");
17
+ const agent_db_js_1 = require("./agent-db.js");
18
+ exports.TAKEOVER_TIMEOUT_MESSAGE = '[System] The user did not take any browser action within 10 minutes. ' +
19
+ 'The browser session has been closed. Please proceed to the next step or finish gracefully.';
20
+ const defaultDeps = {
21
+ getTakeover: takeover_state_js_1.getTakeover,
22
+ clearTakeover: takeover_state_js_1.clearTakeover,
23
+ finalizeSession: session_manager_js_1.finalizeSession,
24
+ enqueue: agent_db_js_1.enqueue,
25
+ };
26
+ /**
27
+ * Run the takeover timeout side effects. Called from the `setTimeout` in
28
+ * process.ts after TAKEOVER_TIMEOUT_MS elapses.
29
+ *
30
+ * Behaviour:
31
+ * 1. If the takeover has already been resolved (user clicked Done), return
32
+ * early — no-op.
33
+ * 2. Otherwise: clear the takeover, finalize the browser session (best
34
+ * effort), and enqueue a system message telling the agent to move on.
35
+ *
36
+ * The function is idempotent: calling it twice for the same agent is safe,
37
+ * the second call finds no entry and returns early.
38
+ */
39
+ async function handleTakeoverTimeout(agentId, workspaceDir, deps = defaultDeps) {
40
+ const current = deps.getTakeover(agentId);
41
+ if (!current)
42
+ return; // already resolved by user reply
43
+ deps.clearTakeover(agentId);
44
+ try {
45
+ await deps.finalizeSession(current.handle, 'closed');
46
+ }
47
+ catch {
48
+ // best effort — recording cleanup failure shouldn't block the system message
49
+ }
50
+ deps.enqueue(workspaceDir, agentId, exports.TAKEOVER_TIMEOUT_MESSAGE, current.channelId);
51
+ }