funolio-agent 1.0.53 → 1.1.65
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/dist/approval.d.ts +1 -6
- package/dist/approval.d.ts.map +1 -1
- package/dist/approval.js +2 -7
- package/dist/approval.js.map +1 -1
- package/dist/auth/credential-reader.d.ts.map +1 -1
- package/dist/auth/credential-reader.js +4 -3
- package/dist/auth/credential-reader.js.map +1 -1
- package/dist/auth/token-refresh.d.ts +8 -0
- package/dist/auth/token-refresh.d.ts.map +1 -1
- package/dist/auth/token-refresh.js +82 -52
- package/dist/auth/token-refresh.js.map +1 -1
- package/dist/auto-organizer.d.ts.map +1 -1
- package/dist/auto-organizer.js +6 -7
- package/dist/auto-organizer.js.map +1 -1
- package/dist/bench-prefix.d.ts +16 -0
- package/dist/bench-prefix.d.ts.map +1 -0
- package/dist/bench-prefix.js +25 -0
- package/dist/bench-prefix.js.map +1 -0
- package/dist/bot-manager.d.ts +5 -1
- package/dist/bot-manager.d.ts.map +1 -1
- package/dist/bot-manager.js +46 -27
- package/dist/bot-manager.js.map +1 -1
- package/dist/chat-sync.d.ts +42 -0
- package/dist/chat-sync.d.ts.map +1 -0
- package/dist/chat-sync.js +95 -0
- package/dist/chat-sync.js.map +1 -0
- package/dist/clerk-model.d.ts +7 -0
- package/dist/clerk-model.d.ts.map +1 -1
- package/dist/clerk-model.js +42 -8
- package/dist/clerk-model.js.map +1 -1
- package/dist/cli-bootstrap-history.d.ts +10 -0
- package/dist/cli-bootstrap-history.d.ts.map +1 -0
- package/dist/cli-bootstrap-history.js +112 -0
- package/dist/cli-bootstrap-history.js.map +1 -0
- package/dist/cli-models.d.ts +8 -0
- package/dist/cli-models.d.ts.map +1 -0
- package/dist/cli-models.js +91 -0
- package/dist/cli-models.js.map +1 -0
- package/dist/cli-session-epoch.d.ts +13 -3
- package/dist/cli-session-epoch.d.ts.map +1 -1
- package/dist/cli-session-epoch.js +53 -4
- package/dist/cli-session-epoch.js.map +1 -1
- package/dist/cli-session-registry.d.ts +35 -0
- package/dist/cli-session-registry.d.ts.map +1 -0
- package/dist/cli-session-registry.js +177 -0
- package/dist/cli-session-registry.js.map +1 -0
- package/dist/cli.js +62 -0
- package/dist/cli.js.map +1 -1
- package/dist/codex-app-server-manager.d.ts +189 -0
- package/dist/codex-app-server-manager.d.ts.map +1 -0
- package/dist/codex-app-server-manager.js +1468 -0
- package/dist/codex-app-server-manager.js.map +1 -0
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +8 -30
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/pool.d.ts +32 -0
- package/dist/commands/pool.d.ts.map +1 -1
- package/dist/commands/pool.js +145 -66
- package/dist/commands/pool.js.map +1 -1
- package/dist/commands/setup.d.ts +4 -1
- package/dist/commands/setup.d.ts.map +1 -1
- package/dist/commands/setup.js +9 -25
- package/dist/commands/setup.js.map +1 -1
- package/dist/commands/start.d.ts +21 -0
- package/dist/commands/start.d.ts.map +1 -1
- package/dist/commands/start.js +559 -63
- package/dist/commands/start.js.map +1 -1
- package/dist/commands/status.d.ts.map +1 -1
- package/dist/commands/status.js +5 -2
- package/dist/commands/status.js.map +1 -1
- package/dist/completion-marker.d.ts +7 -0
- package/dist/completion-marker.d.ts.map +1 -0
- package/dist/completion-marker.js +28 -0
- package/dist/completion-marker.js.map +1 -0
- package/dist/config.d.ts +7 -2
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +184 -60
- package/dist/config.js.map +1 -1
- package/dist/context-window.d.ts +37 -1
- package/dist/context-window.d.ts.map +1 -1
- package/dist/context-window.js +210 -17
- package/dist/context-window.js.map +1 -1
- package/dist/live-activity.d.ts +31 -0
- package/dist/live-activity.d.ts.map +1 -0
- package/dist/live-activity.js +36 -0
- package/dist/live-activity.js.map +1 -0
- package/dist/local-chat-execution.d.ts +114 -0
- package/dist/local-chat-execution.d.ts.map +1 -0
- package/dist/local-chat-execution.js +349 -0
- package/dist/local-chat-execution.js.map +1 -0
- package/dist/local-cli-pty-manager.d.ts +186 -0
- package/dist/local-cli-pty-manager.d.ts.map +1 -1
- package/dist/local-cli-pty-manager.js +2581 -164
- package/dist/local-cli-pty-manager.js.map +1 -1
- package/dist/local-conversation-gateway.d.ts +110 -0
- package/dist/local-conversation-gateway.d.ts.map +1 -0
- package/dist/local-conversation-gateway.js +175 -0
- package/dist/local-conversation-gateway.js.map +1 -0
- package/dist/local-data.d.ts +276 -5
- package/dist/local-data.d.ts.map +1 -1
- package/dist/local-data.js +1201 -86
- package/dist/local-data.js.map +1 -1
- package/dist/local-db.d.ts +6 -0
- package/dist/local-db.d.ts.map +1 -1
- package/dist/local-db.js +428 -2
- package/dist/local-db.js.map +1 -1
- package/dist/local-funnel.d.ts.map +1 -1
- package/dist/local-funnel.js +6 -5
- package/dist/local-funnel.js.map +1 -1
- package/dist/local-server.d.ts +55 -0
- package/dist/local-server.d.ts.map +1 -1
- package/dist/local-server.js +3281 -441
- package/dist/local-server.js.map +1 -1
- package/dist/managed-process-registry.d.ts +59 -0
- package/dist/managed-process-registry.d.ts.map +1 -0
- package/dist/managed-process-registry.js +390 -0
- package/dist/managed-process-registry.js.map +1 -0
- package/dist/mcp/claude-config-writer.d.ts +5 -5
- package/dist/mcp/claude-config-writer.d.ts.map +1 -1
- package/dist/mcp/claude-config-writer.js +19 -11
- package/dist/mcp/claude-config-writer.js.map +1 -1
- package/dist/mcp/index.d.ts +4 -2
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js.map +1 -1
- package/dist/mcp/sync-cli-config.d.ts +42 -4
- package/dist/mcp/sync-cli-config.d.ts.map +1 -1
- package/dist/mcp/sync-cli-config.js +497 -17
- package/dist/mcp/sync-cli-config.js.map +1 -1
- package/dist/message-loop.d.ts +6 -0
- package/dist/message-loop.d.ts.map +1 -1
- package/dist/message-loop.js +281 -89
- package/dist/message-loop.js.map +1 -1
- package/dist/mqtt-client.d.ts +44 -1
- package/dist/mqtt-client.d.ts.map +1 -1
- package/dist/mqtt-client.js +284 -46
- package/dist/mqtt-client.js.map +1 -1
- package/dist/mqtt-data-relay.d.ts +44 -0
- package/dist/mqtt-data-relay.d.ts.map +1 -0
- package/dist/mqtt-data-relay.js +106 -0
- package/dist/mqtt-data-relay.js.map +1 -0
- package/dist/oauth.d.ts.map +1 -1
- package/dist/oauth.js +69 -29
- package/dist/oauth.js.map +1 -1
- package/dist/orchestration/capabilities.d.ts +13 -0
- package/dist/orchestration/capabilities.d.ts.map +1 -0
- package/dist/orchestration/capabilities.js +152 -0
- package/dist/orchestration/capabilities.js.map +1 -0
- package/dist/orchestration/dispatch-executor.d.ts +83 -0
- package/dist/orchestration/dispatch-executor.d.ts.map +1 -0
- package/dist/orchestration/dispatch-executor.js +266 -0
- package/dist/orchestration/dispatch-executor.js.map +1 -0
- package/dist/orchestration/dispatch-hint.d.ts +134 -0
- package/dist/orchestration/dispatch-hint.d.ts.map +1 -0
- package/dist/orchestration/dispatch-hint.js +247 -0
- package/dist/orchestration/dispatch-hint.js.map +1 -0
- package/dist/orchestration/dispatch-runner.d.ts +106 -0
- package/dist/orchestration/dispatch-runner.d.ts.map +1 -0
- package/dist/orchestration/dispatch-runner.js +604 -0
- package/dist/orchestration/dispatch-runner.js.map +1 -0
- package/dist/orchestration/dispatch-tools.d.ts +167 -0
- package/dist/orchestration/dispatch-tools.d.ts.map +1 -0
- package/dist/orchestration/dispatch-tools.js +328 -0
- package/dist/orchestration/dispatch-tools.js.map +1 -0
- package/dist/orchestration/front-door-policy.d.ts +35 -10
- package/dist/orchestration/front-door-policy.d.ts.map +1 -1
- package/dist/orchestration/front-door-policy.js +30 -267
- package/dist/orchestration/front-door-policy.js.map +1 -1
- package/dist/orchestration/orchestrator-dispatch-prompt.d.ts +43 -0
- package/dist/orchestration/orchestrator-dispatch-prompt.d.ts.map +1 -0
- package/dist/orchestration/orchestrator-dispatch-prompt.js +267 -0
- package/dist/orchestration/orchestrator-dispatch-prompt.js.map +1 -0
- package/dist/orchestration/orchestrator-operating-prompt.d.ts +15 -0
- package/dist/orchestration/orchestrator-operating-prompt.d.ts.map +1 -1
- package/dist/orchestration/orchestrator-operating-prompt.js +206 -20
- package/dist/orchestration/orchestrator-operating-prompt.js.map +1 -1
- package/dist/orchestration/plan-import.d.ts +39 -0
- package/dist/orchestration/plan-import.d.ts.map +1 -0
- package/dist/orchestration/plan-import.js +547 -0
- package/dist/orchestration/plan-import.js.map +1 -0
- package/dist/orchestration/validation.d.ts +40 -0
- package/dist/orchestration/validation.d.ts.map +1 -0
- package/dist/orchestration/validation.js +203 -0
- package/dist/orchestration/validation.js.map +1 -0
- package/dist/orchestration/worker-operating-prompt.d.ts +2 -0
- package/dist/orchestration/worker-operating-prompt.d.ts.map +1 -1
- package/dist/orchestration/worker-operating-prompt.js +36 -46
- package/dist/orchestration/worker-operating-prompt.js.map +1 -1
- package/dist/orchestrator.d.ts +214 -33
- package/dist/orchestrator.d.ts.map +1 -1
- package/dist/orchestrator.js +2200 -1100
- package/dist/orchestrator.js.map +1 -1
- package/dist/providers/anthropic.d.ts.map +1 -1
- package/dist/providers/anthropic.js +8 -4
- package/dist/providers/anthropic.js.map +1 -1
- package/dist/providers/claude-cli-prompt.d.ts.map +1 -1
- package/dist/providers/claude-cli-prompt.js +49 -5
- package/dist/providers/claude-cli-prompt.js.map +1 -1
- package/dist/providers/claude-cli.d.ts.map +1 -1
- package/dist/providers/claude-cli.js +81 -5
- package/dist/providers/claude-cli.js.map +1 -1
- package/dist/providers/codex-cli.d.ts +10 -6
- package/dist/providers/codex-cli.d.ts.map +1 -1
- package/dist/providers/codex-cli.js +204 -26
- package/dist/providers/codex-cli.js.map +1 -1
- package/dist/providers/google.d.ts.map +1 -1
- package/dist/providers/google.js +15 -5
- package/dist/providers/google.js.map +1 -1
- package/dist/providers/index.d.ts +15 -1
- package/dist/providers/index.d.ts.map +1 -1
- package/dist/providers/index.js.map +1 -1
- package/dist/providers/openai.d.ts +1 -1
- package/dist/providers/openai.d.ts.map +1 -1
- package/dist/providers/openai.js +13 -5
- package/dist/providers/openai.js.map +1 -1
- package/dist/response-guard.js +1 -1
- package/dist/response-guard.js.map +1 -1
- package/dist/server-adapter.d.ts +8 -0
- package/dist/server-adapter.d.ts.map +1 -1
- package/dist/server-adapter.js +7 -0
- package/dist/server-adapter.js.map +1 -1
- package/dist/service-mode.d.ts +1 -1
- package/dist/service-mode.d.ts.map +1 -1
- package/dist/service-mode.js +64 -1
- package/dist/service-mode.js.map +1 -1
- package/dist/service-setup-only.d.ts +8 -0
- package/dist/service-setup-only.d.ts.map +1 -0
- package/dist/service-setup-only.js +37 -0
- package/dist/service-setup-only.js.map +1 -0
- package/dist/slash-commands.d.ts +21 -0
- package/dist/slash-commands.d.ts.map +1 -0
- package/dist/slash-commands.js +99 -0
- package/dist/slash-commands.js.map +1 -0
- package/dist/subagent/index.d.ts +4 -2
- package/dist/subagent/index.d.ts.map +1 -1
- package/dist/subagent/index.js.map +1 -1
- package/dist/summarization-pipeline.d.ts.map +1 -1
- package/dist/summarization-pipeline.js +1 -9
- package/dist/summarization-pipeline.js.map +1 -1
- package/dist/token-counter.d.ts.map +1 -1
- package/dist/token-counter.js +11 -4
- package/dist/token-counter.js.map +1 -1
- package/dist/tool-filter.d.ts.map +1 -1
- package/dist/tool-filter.js +10 -6
- package/dist/tool-filter.js.map +1 -1
- package/dist/tools/admin-tools.d.ts.map +1 -1
- package/dist/tools/admin-tools.js +20 -5
- package/dist/tools/admin-tools.js.map +1 -1
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +2 -1
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/run-command.d.ts.map +1 -1
- package/dist/tools/run-command.js +5 -1
- package/dist/tools/run-command.js.map +1 -1
- package/dist/tools/search-conversation-history.d.ts +16 -0
- package/dist/tools/search-conversation-history.d.ts.map +1 -0
- package/dist/tools/search-conversation-history.js +334 -0
- package/dist/tools/search-conversation-history.js.map +1 -0
- package/dist/tools/todo-tasks.d.ts.map +1 -1
- package/dist/tools/todo-tasks.js +77 -5
- package/dist/tools/todo-tasks.js.map +1 -1
- package/dist/usage-log.d.ts +62 -0
- package/dist/usage-log.d.ts.map +1 -0
- package/dist/usage-log.js +98 -0
- package/dist/usage-log.js.map +1 -0
- package/dist/wizard-state.d.ts +20 -0
- package/dist/wizard-state.d.ts.map +1 -1
- package/dist/wizard-state.js +90 -3
- package/dist/wizard-state.js.map +1 -1
- package/dist/wizard-support.d.ts.map +1 -1
- package/dist/wizard-support.js +27 -1
- package/dist/wizard-support.js.map +1 -1
- package/dist/workflow-engine.d.ts +44 -2
- package/dist/workflow-engine.d.ts.map +1 -1
- package/dist/workflow-engine.js +932 -111
- package/dist/workflow-engine.js.map +1 -1
- package/package.json +2 -2
package/dist/commands/start.js
CHANGED
|
@@ -36,6 +36,10 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
36
36
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
37
|
};
|
|
38
38
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.mapServerBotsToAgents = mapServerBotsToAgents;
|
|
40
|
+
exports.isServerSeedAgentId = isServerSeedAgentId;
|
|
41
|
+
exports.selectPreferredActiveAgentId = selectPreferredActiveAgentId;
|
|
42
|
+
exports.normalizeLocalModeAgents = normalizeLocalModeAgents;
|
|
39
43
|
exports.startCommand = startCommand;
|
|
40
44
|
const path = __importStar(require("path"));
|
|
41
45
|
const fs = __importStar(require("fs"));
|
|
@@ -45,14 +49,18 @@ const chalk_1 = __importDefault(require("chalk"));
|
|
|
45
49
|
const config_1 = require("../config");
|
|
46
50
|
const mqtt_client_1 = require("../mqtt-client");
|
|
47
51
|
const bot_manager_1 = require("../bot-manager");
|
|
52
|
+
const mqtt_data_relay_1 = require("../mqtt-data-relay");
|
|
48
53
|
const service_mode_1 = require("../service-mode");
|
|
49
54
|
const index_1 = require("../providers/index");
|
|
50
55
|
const sync_cli_config_1 = require("../mcp/sync-cli-config");
|
|
51
56
|
const agent_config_1 = require("../agent-config");
|
|
57
|
+
const service_setup_only_1 = require("../service-setup-only");
|
|
52
58
|
// refreshOAuthToken removed — token refresh now handled per-request in message-loop.ts
|
|
53
59
|
const local_db_1 = require("../local-db");
|
|
54
60
|
const local_server_1 = require("../local-server");
|
|
55
61
|
const data = __importStar(require("../local-data"));
|
|
62
|
+
const chat_sync_1 = require("../chat-sync");
|
|
63
|
+
const pool_1 = require("./pool");
|
|
56
64
|
const AUTH_SESSION_KEY = 'auth.session';
|
|
57
65
|
const DESKTOP_PREFS_KEY = 'desktop.preferences';
|
|
58
66
|
const WIZARD_PROFILE_KEY = 'wizard.profile';
|
|
@@ -60,6 +68,58 @@ const LOCAL_SERVER_PORT = Number(process.env.FUNOLIO_LOCAL_PORT || 18420);
|
|
|
60
68
|
const MESSAGE_ACTIVITY_RETENTION_HOURS = 24;
|
|
61
69
|
const MAINTENANCE_INTERVAL_MS = 5 * 60 * 1000;
|
|
62
70
|
const AGENT_IDLE_RESTART_MS = 30 * 60 * 1000;
|
|
71
|
+
const LAST_MAINTENANCE_RESTART_KEY = 'agent.last_maintenance_restart_at';
|
|
72
|
+
const MIN_MAINTENANCE_RESTART_INTERVAL_MS = 20 * 60 * 60 * 1000;
|
|
73
|
+
const SERVICE_LOCK_FILENAME = 'service.lock';
|
|
74
|
+
function resolveServiceLockPath() {
|
|
75
|
+
return path.join((0, config_1.getConfigDir)(), SERVICE_LOCK_FILENAME);
|
|
76
|
+
}
|
|
77
|
+
function isPidAlive(pid) {
|
|
78
|
+
if (!Number.isFinite(pid) || pid <= 0)
|
|
79
|
+
return false;
|
|
80
|
+
try {
|
|
81
|
+
process.kill(pid, 0);
|
|
82
|
+
return true;
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
function releaseServiceLock(lockPath) {
|
|
89
|
+
try {
|
|
90
|
+
if (fs.existsSync(lockPath))
|
|
91
|
+
fs.unlinkSync(lockPath);
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
// best effort
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
function acquireServiceLock(mode, userId) {
|
|
98
|
+
const lockPath = resolveServiceLockPath();
|
|
99
|
+
if (fs.existsSync(lockPath)) {
|
|
100
|
+
try {
|
|
101
|
+
const existing = JSON.parse(fs.readFileSync(lockPath, 'utf8'));
|
|
102
|
+
if (existing?.pid && isPidAlive(existing.pid)) {
|
|
103
|
+
return { lockPath, acquired: false, existingPid: existing.pid };
|
|
104
|
+
}
|
|
105
|
+
fs.unlinkSync(lockPath);
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
try {
|
|
109
|
+
fs.unlinkSync(lockPath);
|
|
110
|
+
}
|
|
111
|
+
catch { }
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
const payload = {
|
|
115
|
+
pid: process.pid,
|
|
116
|
+
createdAt: new Date().toISOString(),
|
|
117
|
+
mode,
|
|
118
|
+
userId,
|
|
119
|
+
};
|
|
120
|
+
fs.writeFileSync(lockPath, JSON.stringify(payload));
|
|
121
|
+
return { lockPath, acquired: true };
|
|
122
|
+
}
|
|
63
123
|
function cleanupExpiredMessageActivityRows() {
|
|
64
124
|
try {
|
|
65
125
|
const deleted = data.deleteExpiredMessageActivities();
|
|
@@ -78,7 +138,17 @@ function readLastInteractionAtMs() {
|
|
|
78
138
|
const parsed = Date.parse(raw);
|
|
79
139
|
return Number.isNaN(parsed) ? 0 : parsed;
|
|
80
140
|
}
|
|
81
|
-
function
|
|
141
|
+
function readLastMaintenanceRestartAtMs() {
|
|
142
|
+
const raw = data.getSetting(LAST_MAINTENANCE_RESTART_KEY);
|
|
143
|
+
if (!raw)
|
|
144
|
+
return 0;
|
|
145
|
+
const parsed = Date.parse(raw);
|
|
146
|
+
return Number.isNaN(parsed) ? 0 : parsed;
|
|
147
|
+
}
|
|
148
|
+
function markMaintenanceRestartAt(now) {
|
|
149
|
+
data.setSetting(LAST_MAINTENANCE_RESTART_KEY, now.toISOString());
|
|
150
|
+
}
|
|
151
|
+
function shouldRunNightlyRestart(now, lastInteractionAtMs, lastMaintenanceRestartAtMs, botManager, processStartAtMs) {
|
|
82
152
|
const hour = now.getHours();
|
|
83
153
|
if (hour < 1 || hour >= 4)
|
|
84
154
|
return false;
|
|
@@ -86,7 +156,282 @@ function shouldRunNightlyRestart(now, lastInteractionAtMs, botManager) {
|
|
|
86
156
|
return false;
|
|
87
157
|
if (!lastInteractionAtMs)
|
|
88
158
|
return false;
|
|
89
|
-
|
|
159
|
+
if (lastMaintenanceRestartAtMs && (now.getTime() - lastMaintenanceRestartAtMs) < MIN_MAINTENANCE_RESTART_INTERVAL_MS)
|
|
160
|
+
return false;
|
|
161
|
+
// Count idle from the most recent of (process start, last interaction). Without this
|
|
162
|
+
// floor, a stale `agent.last_interaction_at` value in local.db would make the timer
|
|
163
|
+
// fire ~5 minutes after a cold start instead of after a real idle window.
|
|
164
|
+
const idleReference = Math.max(lastInteractionAtMs, processStartAtMs);
|
|
165
|
+
return (now.getTime() - idleReference) >= AGENT_IDLE_RESTART_MS;
|
|
166
|
+
}
|
|
167
|
+
function isAgentPermissionMode(value) {
|
|
168
|
+
return value === 'autopilot' || value === 'approve-destructive' || value === 'approve-all';
|
|
169
|
+
}
|
|
170
|
+
function toManagedAgentConfigName(agent) {
|
|
171
|
+
const safeName = (agent.name || 'agent')
|
|
172
|
+
.toLowerCase()
|
|
173
|
+
.replace(/[^a-z0-9-_]+/g, '-')
|
|
174
|
+
.replace(/^-+|-+$/g, '')
|
|
175
|
+
.slice(0, 24) || 'agent';
|
|
176
|
+
const safeId = (agent.id || 'bot')
|
|
177
|
+
.toLowerCase()
|
|
178
|
+
.replace(/[^a-z0-9]/g, '')
|
|
179
|
+
.slice(0, 12) || 'bot';
|
|
180
|
+
return `managed-${safeName}-${safeId}`;
|
|
181
|
+
}
|
|
182
|
+
function mapServerBotsToAgents(serverBots, existingAgents = []) {
|
|
183
|
+
const existingById = new Map(existingAgents.map((agent) => [agent.id, agent]));
|
|
184
|
+
const mappedById = new Map(serverBots
|
|
185
|
+
.filter((bot) => !!bot?.id && !!bot?.name)
|
|
186
|
+
.map((bot) => {
|
|
187
|
+
const existing = existingById.get(bot.id);
|
|
188
|
+
return [
|
|
189
|
+
bot.id,
|
|
190
|
+
{
|
|
191
|
+
id: bot.id,
|
|
192
|
+
name: bot.name,
|
|
193
|
+
projectDir: existing?.projectDir || '.',
|
|
194
|
+
provider: bot.llmProvider || existing?.provider || bot.runtimeProvider || 'openai',
|
|
195
|
+
model: bot.llmModel || existing?.model || bot.runtimeModel || 'claude-opus-4-6',
|
|
196
|
+
runtimeProvider: bot.runtimeProvider || existing?.runtimeProvider || undefined,
|
|
197
|
+
runtimeModel: bot.runtimeModel || existing?.runtimeModel || undefined,
|
|
198
|
+
accessMode: bot.accessMode || existing?.accessMode || undefined,
|
|
199
|
+
enabledTools: Array.isArray(bot.tools) ? bot.tools : existing?.enabledTools,
|
|
200
|
+
enabledMcpTools: Array.isArray(bot.enabledMcpTools) ? bot.enabledMcpTools : existing?.enabledMcpTools,
|
|
201
|
+
permissionMode: isAgentPermissionMode(bot.permissionMode)
|
|
202
|
+
? bot.permissionMode
|
|
203
|
+
: (existing?.permissionMode || 'autopilot'),
|
|
204
|
+
systemPrompt: existing?.systemPrompt,
|
|
205
|
+
agentDescription: existing?.agentDescription,
|
|
206
|
+
createdAt: existing?.createdAt || new Date().toISOString(),
|
|
207
|
+
},
|
|
208
|
+
];
|
|
209
|
+
}));
|
|
210
|
+
const merged = [];
|
|
211
|
+
for (const existing of existingAgents) {
|
|
212
|
+
const mapped = mappedById.get(existing.id);
|
|
213
|
+
merged.push(mapped || existing);
|
|
214
|
+
if (mapped)
|
|
215
|
+
mappedById.delete(existing.id);
|
|
216
|
+
}
|
|
217
|
+
for (const mapped of mappedById.values()) {
|
|
218
|
+
merged.push(mapped);
|
|
219
|
+
}
|
|
220
|
+
return merged;
|
|
221
|
+
}
|
|
222
|
+
function isServerSeedAgentId(agentId) {
|
|
223
|
+
return typeof agentId === 'string' && /^seed-bot-/i.test(agentId);
|
|
224
|
+
}
|
|
225
|
+
function selectPreferredActiveAgentId(agents, providers, preferredId) {
|
|
226
|
+
if (!Array.isArray(agents) || agents.length === 0)
|
|
227
|
+
return null;
|
|
228
|
+
const providerIds = new Set((providers || []).map((provider) => provider.id));
|
|
229
|
+
const isRunnable = (agent) => providerIds.has(agent.runtimeProvider || agent.provider);
|
|
230
|
+
const preferred = preferredId ? agents.find((agent) => agent.id === preferredId) : undefined;
|
|
231
|
+
if (preferred && isRunnable(preferred))
|
|
232
|
+
return preferred.id;
|
|
233
|
+
const firstRunnable = agents.find(isRunnable);
|
|
234
|
+
if (firstRunnable)
|
|
235
|
+
return firstRunnable.id;
|
|
236
|
+
return preferred?.id || agents[0]?.id || null;
|
|
237
|
+
}
|
|
238
|
+
function normalizeLocalModeAgents(config) {
|
|
239
|
+
const existingAgents = Array.isArray(config.agents) ? config.agents : [];
|
|
240
|
+
const filteredAgents = existingAgents.filter((agent) => !isServerSeedAgentId(agent.id));
|
|
241
|
+
const removedCount = existingAgents.length - filteredAgents.length;
|
|
242
|
+
const preferredId = isServerSeedAgentId(config.activeAgentId) ? null : config.activeAgentId;
|
|
243
|
+
const selectedActiveAgentId = selectPreferredActiveAgentId(filteredAgents, config.providers, preferredId);
|
|
244
|
+
let changed = false;
|
|
245
|
+
if (removedCount > 0) {
|
|
246
|
+
config.agents = filteredAgents;
|
|
247
|
+
changed = true;
|
|
248
|
+
}
|
|
249
|
+
if ((selectedActiveAgentId || null) !== (config.activeAgentId || null)) {
|
|
250
|
+
config.activeAgentId = selectedActiveAgentId || undefined;
|
|
251
|
+
changed = true;
|
|
252
|
+
}
|
|
253
|
+
return { changed, removedCount };
|
|
254
|
+
}
|
|
255
|
+
function syncConfiguredAgentsToLocalConfigs(agents) {
|
|
256
|
+
if (!Array.isArray(agents) || agents.length === 0)
|
|
257
|
+
return;
|
|
258
|
+
let synced = 0;
|
|
259
|
+
for (const agent of agents) {
|
|
260
|
+
if (!agent?.id)
|
|
261
|
+
continue;
|
|
262
|
+
const configName = toManagedAgentConfigName(agent);
|
|
263
|
+
(0, agent_config_1.saveAgentConfig)(configName, {
|
|
264
|
+
name: agent.name || agent.id,
|
|
265
|
+
botId: agent.id,
|
|
266
|
+
provider: agent.provider,
|
|
267
|
+
runtimeProvider: agent.runtimeProvider,
|
|
268
|
+
model: agent.model,
|
|
269
|
+
runtimeModel: agent.runtimeModel,
|
|
270
|
+
accessMode: agent.accessMode,
|
|
271
|
+
workspace: agent.projectDir || '.',
|
|
272
|
+
permissionMode: agent.permissionMode,
|
|
273
|
+
enabledTools: agent.enabledTools,
|
|
274
|
+
enabledMcpTools: agent.enabledMcpTools,
|
|
275
|
+
systemPrompt: agent.systemPrompt || agent.agentDescription,
|
|
276
|
+
createdAt: agent.createdAt || new Date().toISOString(),
|
|
277
|
+
});
|
|
278
|
+
synced++;
|
|
279
|
+
}
|
|
280
|
+
if (synced > 0) {
|
|
281
|
+
console.log(chalk_1.default.gray(` Synced ${synced} managed agent config(s) to ~/.funolio/agents/`));
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
async function fetchMcpInstallations(apiUrl, authToken) {
|
|
285
|
+
const res = await fetch(`${apiUrl}/api/v1/bot/mcp-installations`, {
|
|
286
|
+
headers: { Authorization: `Bearer ${authToken}` },
|
|
287
|
+
});
|
|
288
|
+
if (!res.ok) {
|
|
289
|
+
const text = await res.text().catch(() => '');
|
|
290
|
+
throw new Error(`mcp-installations ${res.status}: ${text}`);
|
|
291
|
+
}
|
|
292
|
+
const data = (await res.json());
|
|
293
|
+
return Array.isArray(data.installations) ? data.installations : [];
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
296
|
+
* Sync approved MCP installations from the cloud into the local MCP manager.
|
|
297
|
+
*
|
|
298
|
+
* Flow:
|
|
299
|
+
* 1. Fetch installations from /api/v1/bot/mcp-installations
|
|
300
|
+
* 2. For each "ready" installation, ensure the MCP is launched with the
|
|
301
|
+
* current env vars (Google refresh token in particular rotates).
|
|
302
|
+
* 3. Skip installations with status "needs_reauth" or "unknown_server";
|
|
303
|
+
* log why.
|
|
304
|
+
*
|
|
305
|
+
* Idempotent: MCPManager.installAndLaunch short-circuits when a server is
|
|
306
|
+
* already running. Env-var changes trigger reloadIntegrationServers for the
|
|
307
|
+
* Google family so the subprocess restarts with fresh tokens.
|
|
308
|
+
*/
|
|
309
|
+
async function syncCloudMcpInstallations(opts) {
|
|
310
|
+
const { apiUrl, authToken, mcpManager } = opts;
|
|
311
|
+
let installations;
|
|
312
|
+
try {
|
|
313
|
+
installations = await fetchMcpInstallations(apiUrl, authToken);
|
|
314
|
+
}
|
|
315
|
+
catch (err) {
|
|
316
|
+
throw new Error(`fetch failed: ${err?.message || err}`);
|
|
317
|
+
}
|
|
318
|
+
if (installations.length === 0) {
|
|
319
|
+
if (process.env.FUNOLIO_AGENT_DEBUG === 'true') {
|
|
320
|
+
console.log(chalk_1.default.gray(' [mcp-sync] no approved MCP installations for this user'));
|
|
321
|
+
}
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
const { MCP_REGISTRY } = await Promise.resolve().then(() => __importStar(require('../mcp/registry-shared')));
|
|
325
|
+
let launched = 0;
|
|
326
|
+
let skipped = 0;
|
|
327
|
+
let googleRefreshed = false;
|
|
328
|
+
for (const inst of installations) {
|
|
329
|
+
if (inst.status !== 'ready') {
|
|
330
|
+
skipped++;
|
|
331
|
+
if (process.env.FUNOLIO_AGENT_DEBUG === 'true') {
|
|
332
|
+
console.log(chalk_1.default.gray(` [mcp-sync] skipping ${inst.serverId}: ${inst.status}${inst.error ? ` (${inst.error})` : ''}`));
|
|
333
|
+
}
|
|
334
|
+
continue;
|
|
335
|
+
}
|
|
336
|
+
const entry = MCP_REGISTRY.find((e) => e.id === inst.serverId);
|
|
337
|
+
if (!entry) {
|
|
338
|
+
skipped++;
|
|
339
|
+
continue;
|
|
340
|
+
}
|
|
341
|
+
const runningInfo = mcpManager.getServerInfo(inst.serverId);
|
|
342
|
+
if (runningInfo) {
|
|
343
|
+
// Already running. If this is a Google-family MCP we may still want
|
|
344
|
+
// to restart it so the subprocess picks up a rotated refresh token.
|
|
345
|
+
// The batch-restart below handles that.
|
|
346
|
+
continue;
|
|
347
|
+
}
|
|
348
|
+
try {
|
|
349
|
+
await mcpManager.installAndLaunch(entry, inst.envVars, inst.alwaysOn);
|
|
350
|
+
launched++;
|
|
351
|
+
}
|
|
352
|
+
catch (err) {
|
|
353
|
+
console.error(chalk_1.default.yellow(` [mcp-sync] failed to launch ${inst.serverId}: ${err?.message || err}`));
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
// If any already-running Google MCP has a changed refresh token, restart
|
|
357
|
+
// the Google family so they pick up the fresh credential. The manager's
|
|
358
|
+
// existing reloadIntegrationServers does the heavy lifting.
|
|
359
|
+
try {
|
|
360
|
+
const hasGoogleReady = installations.some((i) => i.status === 'ready' && /^google-|^gmail$/.test(i.serverId));
|
|
361
|
+
if (hasGoogleReady) {
|
|
362
|
+
const running = mcpManager.getRunningServers();
|
|
363
|
+
const googleRunning = running.some((id) => /^google-|^gmail$/.test(id));
|
|
364
|
+
if (googleRunning) {
|
|
365
|
+
await mcpManager.reloadIntegrationServers('google');
|
|
366
|
+
googleRefreshed = true;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
catch (err) {
|
|
371
|
+
console.error(chalk_1.default.yellow(` [mcp-sync] google reload failed: ${err?.message || err}`));
|
|
372
|
+
}
|
|
373
|
+
if (launched > 0 || googleRefreshed) {
|
|
374
|
+
console.log(chalk_1.default.green(` ✓ MCP sync: ${launched} launched, ${skipped} skipped${googleRefreshed ? ', google refreshed' : ''}`));
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
async function fetchPoolSubscriptions(apiUrl, authToken) {
|
|
378
|
+
const res = await fetch(`${apiUrl}/api/v1/bot/pool-subscriptions`, {
|
|
379
|
+
headers: { Authorization: `Bearer ${authToken}` },
|
|
380
|
+
});
|
|
381
|
+
if (!res.ok) {
|
|
382
|
+
const text = await res.text().catch(() => '');
|
|
383
|
+
throw new Error(`pool-subscriptions ${res.status}: ${text}`);
|
|
384
|
+
}
|
|
385
|
+
const data = (await res.json());
|
|
386
|
+
return Array.isArray(data.subscriptions) ? data.subscriptions : [];
|
|
387
|
+
}
|
|
388
|
+
async function setupPoolSubscriptions(opts) {
|
|
389
|
+
const { apiUrl, authToken, mqttClient, config } = opts;
|
|
390
|
+
let subs = [];
|
|
391
|
+
try {
|
|
392
|
+
subs = await fetchPoolSubscriptions(apiUrl, authToken);
|
|
393
|
+
}
|
|
394
|
+
catch (err) {
|
|
395
|
+
console.error(chalk_1.default.yellow(`⚠ Failed to fetch pool subscriptions: ${err?.message || err}`));
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
if (subs.length === 0) {
|
|
399
|
+
console.log(chalk_1.default.gray(' No team pool provider instances for this user.'));
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
console.log(chalk_1.default.blue(` Subscribing to ${subs.length} team-pool request topic(s)...`));
|
|
403
|
+
const publisher = {
|
|
404
|
+
publish: (topic, payload, o) => mqttClient.publish(topic, payload, { qos: (o?.qos ?? 1) }),
|
|
405
|
+
};
|
|
406
|
+
for (const sub of subs) {
|
|
407
|
+
const { teamId, instanceId, provider: providerName } = sub;
|
|
408
|
+
const providerConfig = (config.providers || []).find((p) => p.id === providerName);
|
|
409
|
+
if (!providerConfig) {
|
|
410
|
+
console.log(chalk_1.default.yellow(` ⚠ No local provider config for "${providerName}" (teamId=${teamId.slice(0, 8)}); skipping`));
|
|
411
|
+
continue;
|
|
412
|
+
}
|
|
413
|
+
const topic = `funolio/team/${teamId}/provider/${instanceId}/request/+`;
|
|
414
|
+
mqttClient.subscribeTopic(topic, async (_t, payload) => {
|
|
415
|
+
let request;
|
|
416
|
+
try {
|
|
417
|
+
request = JSON.parse(payload.toString());
|
|
418
|
+
}
|
|
419
|
+
catch {
|
|
420
|
+
console.error(chalk_1.default.red('Pool request: failed to parse payload'));
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
console.log(chalk_1.default.blue(`📨 Pool request ${String(request.requestId).slice(0, 8)}... team=${teamId.slice(0, 8)} model=${request.model}`));
|
|
424
|
+
await (0, pool_1.handlePoolRequest)({
|
|
425
|
+
teamId,
|
|
426
|
+
instanceId,
|
|
427
|
+
providerName,
|
|
428
|
+
providerConfig,
|
|
429
|
+
request,
|
|
430
|
+
publisher,
|
|
431
|
+
});
|
|
432
|
+
}, { qos: 1 });
|
|
433
|
+
console.log(chalk_1.default.gray(` • ${topic}`));
|
|
434
|
+
}
|
|
90
435
|
}
|
|
91
436
|
function parseJson(value) {
|
|
92
437
|
if (!value)
|
|
@@ -107,10 +452,25 @@ function loadDbRuntimeConfig() {
|
|
|
107
452
|
const botRows = data.listAgentProfiles();
|
|
108
453
|
const projects = data.listProjects({ includeArchived: true });
|
|
109
454
|
const firstProviderId = providerRows[0]?.provider_id;
|
|
110
|
-
const
|
|
455
|
+
const providerByConnectionId = new Map(providerRows.map((row) => [row.id, row]));
|
|
456
|
+
const providerByProviderId = new Map(providerRows.map((row) => [row.provider_id, row]));
|
|
457
|
+
const resolveBotProviderConnection = (bot) => {
|
|
458
|
+
if (bot.provider_connection_id) {
|
|
459
|
+
const direct = providerByConnectionId.get(bot.provider_connection_id);
|
|
460
|
+
if (direct)
|
|
461
|
+
return direct;
|
|
462
|
+
}
|
|
463
|
+
return providerByProviderId.get(bot.provider);
|
|
464
|
+
};
|
|
465
|
+
const isRunnableBot = (bot) => !!resolveBotProviderConnection(bot);
|
|
466
|
+
const defaultBot = botRows.find((bot) => bot.is_default === 1 && isRunnableBot(bot))
|
|
467
|
+
|| botRows.find((bot) => bot.is_active === 1 && isRunnableBot(bot))
|
|
468
|
+
|| botRows.find(isRunnableBot)
|
|
469
|
+
|| botRows.find((bot) => bot.is_default === 1)
|
|
470
|
+
|| botRows[0];
|
|
111
471
|
const providers = providerRows.map((row) => ({
|
|
112
472
|
id: row.provider_id,
|
|
113
|
-
authType: row.auth_type === 'oauth' ? 'oauth' : 'apiKey',
|
|
473
|
+
authType: row.auth_type === 'oauth' ? 'oauth' : row.auth_type === 'cli' ? 'cli' : 'apiKey',
|
|
114
474
|
apiKey: row.api_key_enc || undefined,
|
|
115
475
|
oauthToken: row.oauth_token || undefined,
|
|
116
476
|
oauthRefreshToken: row.oauth_refresh_token || undefined,
|
|
@@ -119,13 +479,16 @@ function loadDbRuntimeConfig() {
|
|
|
119
479
|
label: row.label || undefined,
|
|
120
480
|
}));
|
|
121
481
|
const agents = botRows.map((row) => {
|
|
482
|
+
const resolvedConnection = resolveBotProviderConnection(row);
|
|
483
|
+
const resolvedProviderId = resolvedConnection?.provider_id || row.provider;
|
|
484
|
+
const resolvedModel = row.model || resolvedConnection?.default_model || config_1.DEFAULT_MODELS[resolvedProviderId] || '';
|
|
122
485
|
const projectDir = projects.find((project) => project.bot_ids.includes(row.id) && project.folder)?.folder || '.';
|
|
123
486
|
return {
|
|
124
487
|
id: row.id,
|
|
125
488
|
name: row.name,
|
|
126
489
|
projectDir,
|
|
127
|
-
provider:
|
|
128
|
-
model:
|
|
490
|
+
provider: resolvedProviderId,
|
|
491
|
+
model: resolvedModel,
|
|
129
492
|
enabledTools: parseJson(row.enabled_builtin_tools_json),
|
|
130
493
|
enabledMcpTools: parseJson(row.enabled_mcp_tools_json),
|
|
131
494
|
permissionMode: row.permission_mode || 'autopilot',
|
|
@@ -140,7 +503,7 @@ function loadDbRuntimeConfig() {
|
|
|
140
503
|
return {
|
|
141
504
|
auth,
|
|
142
505
|
providers,
|
|
143
|
-
defaultProvider: defaultBot?.provider || firstProviderId,
|
|
506
|
+
defaultProvider: (defaultBot ? (resolveBotProviderConnection(defaultBot)?.provider_id || defaultBot.provider) : undefined) || firstProviderId,
|
|
144
507
|
agents,
|
|
145
508
|
activeAgentId: defaultBot?.id,
|
|
146
509
|
autostart: !!desktopPrefs?.launchOnStartup,
|
|
@@ -179,13 +542,22 @@ async function startCommand(projectDir, options) {
|
|
|
179
542
|
process.env.FUNOLIO_RUN_CONTEXT = 'windows-service';
|
|
180
543
|
}
|
|
181
544
|
const isServiceMode = options.mode === 'service' || options.mode === 'windows-service';
|
|
545
|
+
let serviceLockPath = null;
|
|
182
546
|
if (isServiceMode) {
|
|
183
547
|
(0, service_mode_1.enableServiceMode)();
|
|
548
|
+
const lock = acquireServiceLock(options.mode || 'service');
|
|
549
|
+
if (!lock.acquired) {
|
|
550
|
+
console.error(chalk_1.default.red(`✗ Another service agent is already running (pid ${lock.existingPid}).`));
|
|
551
|
+
process.exit(1);
|
|
552
|
+
}
|
|
553
|
+
serviceLockPath = lock.lockPath;
|
|
554
|
+
}
|
|
555
|
+
const config = (0, config_1.migrateConfig)((0, config_1.loadConfig)());
|
|
556
|
+
const useDbRuntime = (0, config_1.hasDbConfig)();
|
|
557
|
+
if (options.mode === 'service' && config.connectionMode === 'local') {
|
|
558
|
+
process.env.LOCAL_FIRST_ENABLED = 'true';
|
|
559
|
+
process.env.FUNOLIO_RUN_CONTEXT = process.env.FUNOLIO_RUN_CONTEXT || 'service';
|
|
184
560
|
}
|
|
185
|
-
const legacyConfig = (0, config_1.migrateConfig)((0, config_1.loadConfig)());
|
|
186
|
-
const dbConfig = loadDbRuntimeConfig();
|
|
187
|
-
const useDbRuntime = !!dbConfig;
|
|
188
|
-
const config = dbConfig || legacyConfig;
|
|
189
561
|
const authConfig = config.auth || null;
|
|
190
562
|
const hasAuth = !!authConfig;
|
|
191
563
|
const authExpired = hasAuth
|
|
@@ -193,9 +565,7 @@ async function startCommand(projectDir, options) {
|
|
|
193
565
|
: true;
|
|
194
566
|
const runSetupOnly = async (reason) => {
|
|
195
567
|
if (isServiceMode) {
|
|
196
|
-
|
|
197
|
-
(0, service_mode_1.emitEvent)('status', { status: 'setup_only', reason });
|
|
198
|
-
await new Promise(() => { });
|
|
568
|
+
await (0, service_setup_only_1.waitForServiceSetup)(reason, service_mode_1.emitEvent, options.setupOnlySignal);
|
|
199
569
|
return true;
|
|
200
570
|
}
|
|
201
571
|
return false;
|
|
@@ -204,6 +574,15 @@ async function startCommand(projectDir, options) {
|
|
|
204
574
|
let localServerStarted = false;
|
|
205
575
|
const runtimeConnectionMode = config.connectionMode === 'server' ? 'server' : 'local';
|
|
206
576
|
const runtimeServerBaseUrl = typeof config.serverBaseUrl === 'string' ? config.serverBaseUrl.trim() : '';
|
|
577
|
+
if (runtimeConnectionMode === 'local') {
|
|
578
|
+
const localNormalization = normalizeLocalModeAgents(config);
|
|
579
|
+
if (localNormalization.changed) {
|
|
580
|
+
(0, config_1.saveConfig)(config);
|
|
581
|
+
if (localNormalization.removedCount > 0) {
|
|
582
|
+
console.log(chalk_1.default.gray(` Removed ${localNormalization.removedCount} server-only bot profile(s) from local mode`));
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
}
|
|
207
586
|
process.env.FUNOLIO_CONNECTION_MODE = runtimeConnectionMode;
|
|
208
587
|
if (runtimeServerBaseUrl) {
|
|
209
588
|
process.env.FUNOLIO_SERVER_BASE_URL = runtimeServerBaseUrl;
|
|
@@ -222,6 +601,9 @@ async function startCommand(projectDir, options) {
|
|
|
222
601
|
const dbInfo = (0, local_db_1.initLocalDb)();
|
|
223
602
|
console.log(chalk_1.default.green(`✓ Local DB ready: ${dbInfo.path}`));
|
|
224
603
|
console.log(chalk_1.default.gray(` WAL mode: ${dbInfo.walMode}, tables: ${dbInfo.tableCount}, write: ${dbInfo.testWrite}, read: ${dbInfo.testRead}`));
|
|
604
|
+
if (dbInfo.claudeSessionCounterSync?.updated) {
|
|
605
|
+
console.log(chalk_1.default.gray(` Claude session counter advanced from ${dbInfo.claudeSessionCounterSync.dbValueBefore} to ${dbInfo.claudeSessionCounterSync.dbValueAfter} to match existing ~/.claude Funolio sessions`));
|
|
606
|
+
}
|
|
225
607
|
cleanupExpiredMessageActivityRows();
|
|
226
608
|
if (isServiceMode) {
|
|
227
609
|
(0, service_mode_1.emitEvent)('local_db', { status: 'ready', path: dbInfo.path, walMode: dbInfo.walMode });
|
|
@@ -308,55 +690,69 @@ async function startCommand(projectDir, options) {
|
|
|
308
690
|
process.exit(1);
|
|
309
691
|
}
|
|
310
692
|
}
|
|
311
|
-
// Re-sync
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
}
|
|
693
|
+
// Re-sync providers from server (picks up web re-auth tokens).
|
|
694
|
+
// In local mode, keep the local bot roster authoritative and do not import
|
|
695
|
+
// the server seed-bot catalog into the desktop agent list.
|
|
696
|
+
try {
|
|
697
|
+
const res = await fetch(`${config_1.FUNOLIO_API_URL}/api/v1/agent/config`, {
|
|
698
|
+
headers: { Authorization: `Bearer ${auth.token}` },
|
|
699
|
+
});
|
|
700
|
+
if (res.ok) {
|
|
701
|
+
const serverConfig = await res.json();
|
|
702
|
+
let updated = false;
|
|
703
|
+
if (serverConfig.providers && config.providers?.some(p => p.authType === 'oauth')) {
|
|
704
|
+
for (const sp of serverConfig.providers) {
|
|
705
|
+
if (sp.connectionType !== 'oauth' || !sp.access_token)
|
|
706
|
+
continue;
|
|
707
|
+
const local = config.providers.find(p => p.id === sp.id);
|
|
708
|
+
if (!local || local.authType !== 'oauth')
|
|
709
|
+
continue;
|
|
710
|
+
// Only update if server has a different (newer) token
|
|
711
|
+
if (local.oauthToken !== sp.access_token) {
|
|
712
|
+
local.oauthToken = sp.access_token;
|
|
713
|
+
local.oauthRefreshToken = sp.refresh_token;
|
|
714
|
+
local.oauthExpiresAt = sp.expires_at;
|
|
715
|
+
updated = true;
|
|
716
|
+
console.log(chalk_1.default.green(`✓ Updated ${sp.id} OAuth token from server`));
|
|
335
717
|
}
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
if (runtimeConnectionMode === 'server' && Array.isArray(serverConfig.bots) && serverConfig.bots.length > 0) {
|
|
721
|
+
const nextAgents = mapServerBotsToAgents(serverConfig.bots, config.agents || []);
|
|
722
|
+
const sameAgents = JSON.stringify(config.agents || []) === JSON.stringify(nextAgents);
|
|
723
|
+
if (!sameAgents) {
|
|
724
|
+
config.agents = nextAgents;
|
|
725
|
+
updated = true;
|
|
726
|
+
console.log(chalk_1.default.green(`✓ Synced ${nextAgents.length} bot profile(s) from server`));
|
|
727
|
+
}
|
|
728
|
+
const selectedActiveAgentId = selectPreferredActiveAgentId(nextAgents, config.providers, config.activeAgentId);
|
|
729
|
+
if (selectedActiveAgentId && selectedActiveAgentId !== config.activeAgentId) {
|
|
730
|
+
config.activeAgentId = selectedActiveAgentId;
|
|
731
|
+
updated = true;
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
if (updated) {
|
|
735
|
+
(0, config_1.saveConfig)(config);
|
|
736
|
+
// Also update agent config files on disk for OAuth providers
|
|
737
|
+
for (const p of config.providers || []) {
|
|
738
|
+
if (p.authType !== 'oauth' || !p.oauthToken)
|
|
739
|
+
continue;
|
|
740
|
+
const { listAgents, loadAgentConfig: loadAC, saveAgentConfig: saveAC } = await Promise.resolve().then(() => __importStar(require('../agent-config')));
|
|
741
|
+
for (const agentName of listAgents()) {
|
|
742
|
+
const ac = loadAC(agentName);
|
|
743
|
+
if (ac && ac.provider === p.id && ac.oauthToken) {
|
|
744
|
+
ac.oauthToken = p.oauthToken;
|
|
745
|
+
ac.oauthRefreshToken = p.oauthRefreshToken;
|
|
746
|
+
ac.oauthExpiresAt = p.oauthExpiresAt;
|
|
747
|
+
saveAC(agentName, ac);
|
|
352
748
|
}
|
|
353
749
|
}
|
|
354
750
|
}
|
|
355
751
|
}
|
|
356
752
|
}
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
}
|
|
753
|
+
}
|
|
754
|
+
catch (err) {
|
|
755
|
+
console.log(chalk_1.default.gray(` Server config re-sync skipped: ${err.message}`));
|
|
360
756
|
}
|
|
361
757
|
// Load agent-specific config if --agent was specified
|
|
362
758
|
const agentLocalConfig = options.agent ? (0, agent_config_1.loadAgentConfig)(options.agent) : null;
|
|
@@ -481,8 +877,10 @@ async function startCommand(projectDir, options) {
|
|
|
481
877
|
// Enforce strict subscription routing semantics.
|
|
482
878
|
if (effectiveAccessMode === 'openai_subscription') {
|
|
483
879
|
apiKey = undefined;
|
|
484
|
-
|
|
485
|
-
|
|
880
|
+
oauthToken = undefined;
|
|
881
|
+
// Preserve codex-cli when it was explicitly selected. The CLI manages its
|
|
882
|
+
// own auth/session state and should not be rewritten into the raw OpenAI
|
|
883
|
+
// API path during local-agent startup.
|
|
486
884
|
}
|
|
487
885
|
if (effectiveAccessMode === 'anthropic_subscription') {
|
|
488
886
|
provider = 'claude-cli';
|
|
@@ -492,10 +890,17 @@ async function startCommand(projectDir, options) {
|
|
|
492
890
|
model = 'claude-code';
|
|
493
891
|
}
|
|
494
892
|
}
|
|
495
|
-
// Auto-detect Claude Code CLI
|
|
893
|
+
// Auto-detect Claude Code CLI only when the provider was not explicitly chosen.
|
|
894
|
+
// Otherwise we can accidentally rewrite an explicit Codex/OpenAI bot selection
|
|
895
|
+
// into Claude just because no API key was passed on the outer config object.
|
|
896
|
+
const providerWasExplicit = !!(options.provider ||
|
|
897
|
+
agentLocalConfig?.runtimeProvider ||
|
|
898
|
+
activeAgentConfig?.runtimeProvider ||
|
|
899
|
+
agentLocalConfig?.provider ||
|
|
900
|
+
activeAgentConfig?.provider);
|
|
496
901
|
// OAuth tokens from claude.ai can't be used with the Anthropic API directly,
|
|
497
902
|
// so we prefer claude-cli when Claude Code is installed.
|
|
498
|
-
if (!apiKey && !oauthToken && !
|
|
903
|
+
if (!apiKey && !oauthToken && !providerWasExplicit && provider !== 'claude-cli') {
|
|
499
904
|
const claudeVersion = detectClaudeCli();
|
|
500
905
|
if (claudeVersion) {
|
|
501
906
|
console.log(chalk_1.default.green(`✓ Claude Code detected (${claudeVersion}), using your subscription`));
|
|
@@ -571,6 +976,9 @@ async function startCommand(projectDir, options) {
|
|
|
571
976
|
const dbInfo = (0, local_db_1.initLocalDb)();
|
|
572
977
|
console.log(chalk_1.default.green(`✓ Local DB ready: ${dbInfo.path}`));
|
|
573
978
|
console.log(chalk_1.default.gray(` WAL mode: ${dbInfo.walMode}, tables: ${dbInfo.tableCount}, write: ${dbInfo.testWrite}, read: ${dbInfo.testRead}`));
|
|
979
|
+
if (dbInfo.claudeSessionCounterSync?.updated) {
|
|
980
|
+
console.log(chalk_1.default.gray(` Claude session counter advanced from ${dbInfo.claudeSessionCounterSync.dbValueBefore} to ${dbInfo.claudeSessionCounterSync.dbValueAfter} to match existing ~/.claude Funolio sessions`));
|
|
981
|
+
}
|
|
574
982
|
cleanupExpiredMessageActivityRows();
|
|
575
983
|
if (isServiceMode) {
|
|
576
984
|
(0, service_mode_1.emitEvent)('local_db', { status: 'ready', path: dbInfo.path, walMode: dbInfo.walMode });
|
|
@@ -621,6 +1029,10 @@ async function startCommand(projectDir, options) {
|
|
|
621
1029
|
authToken: auth.token,
|
|
622
1030
|
refreshUrl: `${config_1.FUNOLIO_API_URL}/api/v1/agent/auth/refresh`,
|
|
623
1031
|
});
|
|
1032
|
+
(0, chat_sync_1.configureChatSyncPublisher)({
|
|
1033
|
+
userId: auth.userId,
|
|
1034
|
+
publish: (topic, payload, opts) => mqttClient.publish(topic, payload, opts),
|
|
1035
|
+
});
|
|
624
1036
|
// Create and auto-launch MCP manager for persistent tool discovery
|
|
625
1037
|
const { MCPManager } = await Promise.resolve().then(() => __importStar(require('../mcp/manager')));
|
|
626
1038
|
const mcpManager = new MCPManager();
|
|
@@ -649,6 +1061,7 @@ async function startCommand(projectDir, options) {
|
|
|
649
1061
|
});
|
|
650
1062
|
let maintenanceTimer = null;
|
|
651
1063
|
let maintenanceRestartPending = false;
|
|
1064
|
+
const processStartAtMs = Date.now();
|
|
652
1065
|
// Handle shutdown
|
|
653
1066
|
const shutdown = async () => {
|
|
654
1067
|
console.log(chalk_1.default.yellow('\nShutting down...'));
|
|
@@ -666,6 +1079,8 @@ async function startCommand(projectDir, options) {
|
|
|
666
1079
|
}).catch(() => { });
|
|
667
1080
|
await mcpManager.shutdown();
|
|
668
1081
|
await mqttClient.disconnect();
|
|
1082
|
+
if (serviceLockPath)
|
|
1083
|
+
releaseServiceLock(serviceLockPath);
|
|
669
1084
|
process.exit(0);
|
|
670
1085
|
};
|
|
671
1086
|
process.on('SIGINT', shutdown);
|
|
@@ -759,6 +1174,9 @@ async function startCommand(projectDir, options) {
|
|
|
759
1174
|
console.log(chalk_1.default.gray(' This terminal can stay open or run in background.\n'));
|
|
760
1175
|
// Start the active agent loop (driven by agents[] + activeAgentId)
|
|
761
1176
|
await botManager.startActive();
|
|
1177
|
+
// Materialize configured server bots into local agent files so explicit
|
|
1178
|
+
// bot routing can resolve botId -> loop mappings after restarts.
|
|
1179
|
+
syncConfiguredAgentsToLocalConfigs(config.agents);
|
|
762
1180
|
// Start all additional bot loops from ~/.funolio/agents/ configs
|
|
763
1181
|
await botManager.startAllBots();
|
|
764
1182
|
try {
|
|
@@ -775,9 +1193,11 @@ async function startCommand(projectDir, options) {
|
|
|
775
1193
|
return;
|
|
776
1194
|
const now = new Date();
|
|
777
1195
|
const lastInteractionAtMs = readLastInteractionAtMs();
|
|
778
|
-
|
|
1196
|
+
const lastMaintenanceRestartAtMs = readLastMaintenanceRestartAtMs();
|
|
1197
|
+
if (!shouldRunNightlyRestart(now, lastInteractionAtMs, lastMaintenanceRestartAtMs, botManager, processStartAtMs))
|
|
779
1198
|
return;
|
|
780
1199
|
maintenanceRestartPending = true;
|
|
1200
|
+
markMaintenanceRestartAt(now);
|
|
781
1201
|
const reason = `nightly maintenance restart after ${MESSAGE_ACTIVITY_RETENTION_HOURS}h activity retention window`;
|
|
782
1202
|
console.log(chalk_1.default.yellow(` Restarting agent for ${reason}`));
|
|
783
1203
|
try {
|
|
@@ -793,7 +1213,10 @@ async function startCommand(projectDir, options) {
|
|
|
793
1213
|
if (isServiceMode) {
|
|
794
1214
|
(0, service_mode_1.emitEvent)('agent_restart', { reason });
|
|
795
1215
|
}
|
|
796
|
-
|
|
1216
|
+
// Exit with EX_TEMPFAIL (75) so systemd's `Restart=on-failure` triggers a
|
|
1217
|
+
// restart. `process.exit(0)` is treated as clean and would leave the agent
|
|
1218
|
+
// dead until a human starts it (observed incident: ~2 days of downtime).
|
|
1219
|
+
setTimeout(() => process.exit(75), 1500);
|
|
797
1220
|
}, MAINTENANCE_INTERVAL_MS);
|
|
798
1221
|
if (config.agents && config.agents.length > 1) {
|
|
799
1222
|
console.log(chalk_1.default.blue(` ${config.agents.length} agent(s) configured (active: ${activeAgentConfig?.name || 'default'})`));
|
|
@@ -824,6 +1247,71 @@ async function startCommand(projectDir, options) {
|
|
|
824
1247
|
}
|
|
825
1248
|
}
|
|
826
1249
|
console.log(chalk_1.default.gray(' Waiting for commands...\n'));
|
|
1250
|
+
// Subscribe to team-pool request topics for every provider instance this user hosts.
|
|
1251
|
+
// Without this, V2 TeamBot chats routed through the team pool never reach this agent
|
|
1252
|
+
// and time out at 120s. Previously this subscription was only done by the dedicated
|
|
1253
|
+
// `funolio-agent pool provide` command, not by `start --mode service`.
|
|
1254
|
+
setupPoolSubscriptions({
|
|
1255
|
+
apiUrl: config_1.FUNOLIO_API_URL,
|
|
1256
|
+
authToken: auth.token,
|
|
1257
|
+
mqttClient,
|
|
1258
|
+
config,
|
|
1259
|
+
}).catch((err) => {
|
|
1260
|
+
console.error(chalk_1.default.yellow(`⚠ Pool subscription setup failed: ${err?.message || err}`));
|
|
1261
|
+
});
|
|
1262
|
+
// Keep liveness fresh: re-poll /api/v1/bot/pool-subscriptions every 2 minutes.
|
|
1263
|
+
// The GET side-effects a `lastSeenAt = NOW()` update on all of this user's
|
|
1264
|
+
// TeamLlmProviderInstance rows. Without this, the server's V2 completions
|
|
1265
|
+
// route fast-fails with 503 NO_RUNTIME because the staleness check
|
|
1266
|
+
// (>5 min = offline) trips — even while the agent is actively subscribed
|
|
1267
|
+
// to the pool topic.
|
|
1268
|
+
//
|
|
1269
|
+
// The re-poll also picks up any new teams the user joined after the agent
|
|
1270
|
+
// started: fetchPoolSubscriptions returns the up-to-date set, so any newly
|
|
1271
|
+
// added instance gets subscribed on the next tick.
|
|
1272
|
+
const POOL_LIVENESS_INTERVAL_MS = 2 * 60 * 1000;
|
|
1273
|
+
setInterval(() => {
|
|
1274
|
+
fetchPoolSubscriptions(config_1.FUNOLIO_API_URL, auth.token)
|
|
1275
|
+
.then(() => {
|
|
1276
|
+
// Discovery of brand-new team memberships is handled in a later
|
|
1277
|
+
// iteration; for now the GET alone is enough to keep liveness fresh.
|
|
1278
|
+
})
|
|
1279
|
+
.catch((err) => {
|
|
1280
|
+
// Non-fatal — next tick will retry. Don't spam logs.
|
|
1281
|
+
if (process.env.FUNOLIO_AGENT_DEBUG === 'true') {
|
|
1282
|
+
console.error(chalk_1.default.gray(` [pool-liveness] refresh failed: ${err?.message || err}`));
|
|
1283
|
+
}
|
|
1284
|
+
});
|
|
1285
|
+
}, POOL_LIVENESS_INTERVAL_MS).unref();
|
|
1286
|
+
// Sync approved MCP installations from the cloud. The user approves MCPs
|
|
1287
|
+
// via the web UI (stored in UserMcpServer rows server-side), but the
|
|
1288
|
+
// agent's MCP manager only auto-launches from ~/.funolio/mcp-servers.json
|
|
1289
|
+
// which is never populated on VM/remote agent installs. Without this
|
|
1290
|
+
// sync, users who approved Google Workspace/Drive/Gmail/Chat via the web
|
|
1291
|
+
// see `mcp=0` in the tool filter at startup and tool calls fail.
|
|
1292
|
+
syncCloudMcpInstallations({
|
|
1293
|
+
apiUrl: config_1.FUNOLIO_API_URL,
|
|
1294
|
+
authToken: auth.token,
|
|
1295
|
+
mcpManager,
|
|
1296
|
+
}).catch((err) => {
|
|
1297
|
+
console.error(chalk_1.default.yellow(`⚠ MCP installation sync failed: ${err?.message || err}`));
|
|
1298
|
+
});
|
|
1299
|
+
// Re-sync every 10 minutes so newly approved MCPs show up and Google
|
|
1300
|
+
// tokens get refreshed before they expire (GOOGLE_REFRESH_BUFFER_MS on
|
|
1301
|
+
// the server is 5 min; polling at 10 min is well inside the safety
|
|
1302
|
+
// margin of a typical 1-hour access token TTL).
|
|
1303
|
+
const MCP_SYNC_INTERVAL_MS = 10 * 60 * 1000;
|
|
1304
|
+
setInterval(() => {
|
|
1305
|
+
syncCloudMcpInstallations({
|
|
1306
|
+
apiUrl: config_1.FUNOLIO_API_URL,
|
|
1307
|
+
authToken: auth.token,
|
|
1308
|
+
mcpManager,
|
|
1309
|
+
}).catch((err) => {
|
|
1310
|
+
if (process.env.FUNOLIO_AGENT_DEBUG === 'true') {
|
|
1311
|
+
console.error(chalk_1.default.gray(` [mcp-sync] refresh failed: ${err?.message || err}`));
|
|
1312
|
+
}
|
|
1313
|
+
});
|
|
1314
|
+
}, MCP_SYNC_INTERVAL_MS).unref();
|
|
827
1315
|
// OAuth token refresh is handled per-request in message-loop.ts via ensureFreshToken()
|
|
828
1316
|
// No background timer needed — this matches the pi-ai/Claude Code CLI pattern of
|
|
829
1317
|
// refreshing lazily before each API call, avoiding race conditions on single-use refresh tokens
|
|
@@ -846,6 +1334,14 @@ async function startCommand(projectDir, options) {
|
|
|
846
1334
|
// Start listening for commands — BotManager routes to the right bot
|
|
847
1335
|
// Also handle MCP marketplace install/uninstall commands
|
|
848
1336
|
mqttClient.onCommand(async (message) => {
|
|
1337
|
+
// Local-DB data relay (mobile1.txt Phase 1). When the cloud needs to
|
|
1338
|
+
// serve a personal-desktop user's data via mobile/web, it publishes a
|
|
1339
|
+
// data_request command here; we forward to the local HTTP server and
|
|
1340
|
+
// reply on the results topic. Always handle this BEFORE existing
|
|
1341
|
+
// command dispatch since it's its own message type.
|
|
1342
|
+
if (await (0, mqtt_data_relay_1.handleDataRequestMessage)(message, mqttClient)) {
|
|
1343
|
+
return;
|
|
1344
|
+
}
|
|
849
1345
|
if (message.type === 'command' && message.action === 'mcp_install') {
|
|
850
1346
|
const { serverId, envVars } = message;
|
|
851
1347
|
console.log(chalk_1.default.cyan(`📦 Marketplace: installing MCP server "${serverId}"...`));
|