notioncode 0.1.0 → 0.1.2
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 +22 -9
- package/agent-runtime-server/package-lock.json +4377 -0
- package/agent-runtime-server/package.json +36 -0
- package/agent-runtime-server/scripts/fix-node-pty.js +67 -0
- package/agent-runtime-server/server/agent-session-service.js +816 -0
- package/agent-runtime-server/server/claude-sdk.js +836 -0
- package/agent-runtime-server/server/cli.js +330 -0
- package/agent-runtime-server/server/constants/config.js +5 -0
- package/agent-runtime-server/server/cursor-cli.js +335 -0
- package/agent-runtime-server/server/database/db.js +653 -0
- package/agent-runtime-server/server/database/init.sql +99 -0
- package/agent-runtime-server/server/gemini-cli.js +460 -0
- package/agent-runtime-server/server/gemini-response-handler.js +79 -0
- package/agent-runtime-server/server/index.js +2569 -0
- package/agent-runtime-server/server/load-env.js +32 -0
- package/agent-runtime-server/server/middleware/auth.js +132 -0
- package/agent-runtime-server/server/openai-codex.js +512 -0
- package/agent-runtime-server/server/projects.js +2594 -0
- package/agent-runtime-server/server/providers/claude/adapter.js +278 -0
- package/agent-runtime-server/server/providers/codex/adapter.js +248 -0
- package/agent-runtime-server/server/providers/cursor/adapter.js +353 -0
- package/agent-runtime-server/server/providers/gemini/adapter.js +186 -0
- package/agent-runtime-server/server/providers/registry.js +44 -0
- package/agent-runtime-server/server/providers/types.js +119 -0
- package/agent-runtime-server/server/providers/utils.js +29 -0
- package/agent-runtime-server/server/routes/agent-sessions.js +238 -0
- package/agent-runtime-server/server/routes/agent.js +1244 -0
- package/agent-runtime-server/server/routes/auth.js +144 -0
- package/agent-runtime-server/server/routes/cli-auth.js +478 -0
- package/agent-runtime-server/server/routes/codex.js +329 -0
- package/agent-runtime-server/server/routes/commands.js +596 -0
- package/agent-runtime-server/server/routes/cursor.js +798 -0
- package/agent-runtime-server/server/routes/gemini.js +24 -0
- package/agent-runtime-server/server/routes/git.js +1508 -0
- package/agent-runtime-server/server/routes/mcp-utils.js +48 -0
- package/agent-runtime-server/server/routes/mcp.js +552 -0
- package/agent-runtime-server/server/routes/messages.js +61 -0
- package/agent-runtime-server/server/routes/plugins.js +307 -0
- package/agent-runtime-server/server/routes/projects.js +548 -0
- package/agent-runtime-server/server/routes/settings.js +276 -0
- package/agent-runtime-server/server/routes/taskmaster.js +1963 -0
- package/agent-runtime-server/server/routes/user.js +123 -0
- package/agent-runtime-server/server/services/notification-orchestrator.js +227 -0
- package/agent-runtime-server/server/services/vapid-keys.js +35 -0
- package/agent-runtime-server/server/sessionManager.js +226 -0
- package/agent-runtime-server/server/utils/commandParser.js +303 -0
- package/agent-runtime-server/server/utils/frontmatter.js +18 -0
- package/agent-runtime-server/server/utils/gitConfig.js +34 -0
- package/agent-runtime-server/server/utils/mcp-detector.js +198 -0
- package/agent-runtime-server/server/utils/plugin-loader.js +457 -0
- package/agent-runtime-server/server/utils/plugin-process-manager.js +184 -0
- package/agent-runtime-server/server/utils/taskmaster-websocket.js +129 -0
- package/agent-runtime-server/shared/modelConstants.js +12 -0
- package/agent-runtime-server/shared/modelConstants.test.js +34 -0
- package/agent-runtime-server/shared/networkHosts.js +22 -0
- package/agent-runtime-server/test_sdk.mjs +16 -0
- package/bin/bridges/darwin-x64/nocode-bridge +0 -0
- package/bin/{nocode-local.js → notioncode.js} +2 -8
- package/dist/assets/icon-CQtd7WEB.png +0 -0
- package/dist/assets/index-D_1ZrHDe.js +1 -0
- package/dist/assets/index-DhCWie1Z.css +1 -0
- package/dist/assets/index-DkGqIiwF.js +689 -0
- package/dist/index.html +46 -0
- package/dist/onboarding/step1_create.png +0 -0
- package/dist/onboarding/step2_capabilities.png +0 -0
- package/dist/onboarding/step2b_content_access.png +0 -0
- package/dist/onboarding/step2c_page_access.png +0 -0
- package/dist/onboarding/step3_token.png +0 -0
- package/dist/onboarding/step4_webhook.png +0 -0
- package/dist/onboarding/step6a_verify.png +0 -0
- package/dist/onboarding/step6b_copy_verify_token.png +0 -0
- package/dist/tinyfish-fish-only.png +0 -0
- package/lib/certs.js +332 -0
- package/lib/install.js +48 -4
- package/lib/start.js +346 -29
- package/package.json +10 -4
- package/src/shared/modelRegistry.d.ts +24 -0
- package/src/shared/modelRegistry.js +163 -0
|
@@ -0,0 +1,816 @@
|
|
|
1
|
+
import crypto from 'crypto';
|
|
2
|
+
import { access, readFile } from 'fs/promises';
|
|
3
|
+
import { existsSync } from 'fs';
|
|
4
|
+
import { homedir } from 'os';
|
|
5
|
+
import { join } from 'path';
|
|
6
|
+
|
|
7
|
+
import { queryClaudeSDK, abortClaudeSDKSession, isClaudeSDKSessionActive } from './claude-sdk.js';
|
|
8
|
+
import { queryCodex, abortCodexSession, isCodexSessionActive } from './openai-codex.js';
|
|
9
|
+
import { spawnCursor, abortCursorSession, isCursorSessionActive } from './cursor-cli.js';
|
|
10
|
+
import { spawnGemini, abortGeminiSession, isGeminiSessionActive } from './gemini-cli.js';
|
|
11
|
+
|
|
12
|
+
const DEFAULT_APP_DATA_DIR = '.antigravity_tools';
|
|
13
|
+
const GUI_CONFIG_FILE = 'gui_config.json';
|
|
14
|
+
const SESSION_RETENTION_MS = 2 * 60 * 60 * 1000;
|
|
15
|
+
const SESSION_PROVIDER_IDS = new Set(['claude', 'codex', 'cursor', 'gemini']);
|
|
16
|
+
const LOGICAL_PROVIDER_IDS = new Set(['openai', 'anthropic', 'gemini', 'cursor', 'claude', 'codex']);
|
|
17
|
+
const WORKER_ROLE_FALLBACK_ORDER = ['docs_product', 'active_work', 'code_surface', 'risk_debt', 'prototype'];
|
|
18
|
+
|
|
19
|
+
const localSessions = new Map();
|
|
20
|
+
|
|
21
|
+
function logAgentSession(message) {
|
|
22
|
+
console.log(`[AGENT-SESSIONS] ${message}`);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function quote(value) {
|
|
26
|
+
return JSON.stringify(value);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function readOptionalString(value) {
|
|
30
|
+
return typeof value === 'string' && value.trim().length > 0 ? value.trim() : undefined;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function getGuiConfigPath(env = process.env) {
|
|
34
|
+
const dataDir = readOptionalString(env.ABV_DATA_DIR) ?? join(homedir(), DEFAULT_APP_DATA_DIR);
|
|
35
|
+
return join(dataDir, GUI_CONFIG_FILE);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function readGuiConfig(env = process.env) {
|
|
39
|
+
const configPath = getGuiConfigPath(env);
|
|
40
|
+
try {
|
|
41
|
+
await access(configPath);
|
|
42
|
+
} catch {
|
|
43
|
+
return { configPath, raw: null };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
const content = await readFile(configPath, 'utf8');
|
|
48
|
+
return { configPath, raw: JSON.parse(content) };
|
|
49
|
+
} catch (error) {
|
|
50
|
+
logAgentSession(`gui-config parse-failed path=${quote(configPath)} message=${quote(error instanceof Error ? error.message : String(error))}`);
|
|
51
|
+
return { configPath, raw: null };
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function getRawNotionAutomationConfig(raw) {
|
|
56
|
+
if (!raw || typeof raw !== 'object') {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return raw.notion_automation ?? raw.notionAutomation ?? null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function getRawAgentProviders(raw) {
|
|
64
|
+
return raw?.agent_providers ?? raw?.agentProviders ?? [];
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function normalizeProviderChoice(value) {
|
|
68
|
+
const normalized = readOptionalString(value)?.toLowerCase();
|
|
69
|
+
return normalized && LOGICAL_PROVIDER_IDS.has(normalized) ? normalized : undefined;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function normalizeSessionProvider(value) {
|
|
73
|
+
const normalized = readOptionalString(value)?.toLowerCase();
|
|
74
|
+
return normalized && SESSION_PROVIDER_IDS.has(normalized) ? normalized : undefined;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function providerLabel(provider) {
|
|
78
|
+
switch (provider) {
|
|
79
|
+
case 'openai':
|
|
80
|
+
return 'OpenAI';
|
|
81
|
+
case 'anthropic':
|
|
82
|
+
return 'Anthropic';
|
|
83
|
+
case 'gemini':
|
|
84
|
+
return 'Gemini';
|
|
85
|
+
case 'cursor':
|
|
86
|
+
return 'Cursor';
|
|
87
|
+
case 'claude':
|
|
88
|
+
return 'Claude';
|
|
89
|
+
case 'codex':
|
|
90
|
+
return 'Codex';
|
|
91
|
+
default:
|
|
92
|
+
return provider;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function resolveSessionProvider(provider, runner) {
|
|
97
|
+
const sessionProvider = normalizeSessionProvider(provider);
|
|
98
|
+
if (sessionProvider) {
|
|
99
|
+
return sessionProvider;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const normalizedRunner = normalizeSessionProvider(runner);
|
|
103
|
+
if (normalizedRunner) {
|
|
104
|
+
return normalizedRunner;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
switch (provider) {
|
|
108
|
+
case 'anthropic':
|
|
109
|
+
return 'claude';
|
|
110
|
+
case 'openai':
|
|
111
|
+
if (runner === 'cursor') {
|
|
112
|
+
return 'cursor';
|
|
113
|
+
}
|
|
114
|
+
return 'codex';
|
|
115
|
+
case 'gemini':
|
|
116
|
+
return 'gemini';
|
|
117
|
+
default:
|
|
118
|
+
return undefined;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function normalizeConfiguredProvider(candidate, index, source) {
|
|
123
|
+
if (!candidate || typeof candidate !== 'object') {
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const logicalProvider = normalizeProviderChoice(candidate.provider);
|
|
128
|
+
const apiKey = readOptionalString(candidate.api_key ?? candidate.apiKey);
|
|
129
|
+
const sessionProvider = resolveSessionProvider(logicalProvider, readOptionalString(candidate.runner));
|
|
130
|
+
if (!apiKey || !sessionProvider) {
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return {
|
|
135
|
+
id: logicalProvider ?? sessionProvider,
|
|
136
|
+
label: providerLabel(logicalProvider ?? sessionProvider),
|
|
137
|
+
sessionProvider,
|
|
138
|
+
source,
|
|
139
|
+
configured: true,
|
|
140
|
+
available: true,
|
|
141
|
+
priority: index,
|
|
142
|
+
apiKey,
|
|
143
|
+
baseUrl: readOptionalString(candidate.base_url ?? candidate.baseUrl),
|
|
144
|
+
runner: readOptionalString(candidate.runner),
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function buildEnvFallbackProviders(env = process.env) {
|
|
149
|
+
const providers = [];
|
|
150
|
+
|
|
151
|
+
if (readOptionalString(env.OPENAI_API_KEY)) {
|
|
152
|
+
providers.push({
|
|
153
|
+
id: 'openai',
|
|
154
|
+
label: 'OpenAI',
|
|
155
|
+
sessionProvider: 'codex',
|
|
156
|
+
source: 'env',
|
|
157
|
+
configured: true,
|
|
158
|
+
available: true,
|
|
159
|
+
priority: providers.length,
|
|
160
|
+
apiKey: readOptionalString(env.OPENAI_API_KEY),
|
|
161
|
+
baseUrl: readOptionalString(env.OPENAI_BASE_URL),
|
|
162
|
+
runner: 'codex',
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (readOptionalString(env.ANTHROPIC_API_KEY)) {
|
|
167
|
+
providers.push({
|
|
168
|
+
id: 'anthropic',
|
|
169
|
+
label: 'Anthropic',
|
|
170
|
+
sessionProvider: 'claude',
|
|
171
|
+
source: 'env',
|
|
172
|
+
configured: true,
|
|
173
|
+
available: true,
|
|
174
|
+
priority: providers.length,
|
|
175
|
+
apiKey: readOptionalString(env.ANTHROPIC_API_KEY),
|
|
176
|
+
baseUrl: readOptionalString(env.ANTHROPIC_BASE_URL),
|
|
177
|
+
runner: 'claude',
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (readOptionalString(env.GEMINI_API_KEY)) {
|
|
182
|
+
providers.push({
|
|
183
|
+
id: 'gemini',
|
|
184
|
+
label: 'Gemini',
|
|
185
|
+
sessionProvider: 'gemini',
|
|
186
|
+
source: 'env',
|
|
187
|
+
configured: true,
|
|
188
|
+
available: true,
|
|
189
|
+
priority: providers.length,
|
|
190
|
+
apiKey: readOptionalString(env.GEMINI_API_KEY),
|
|
191
|
+
baseUrl: readOptionalString(env.GEMINI_BASE_URL),
|
|
192
|
+
runner: 'gemini',
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return providers;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function parseWorkerRoleDefaults(rawConfig) {
|
|
200
|
+
const rawDefaults =
|
|
201
|
+
rawConfig?.worker_role_provider_defaults ??
|
|
202
|
+
rawConfig?.workerRoleProviderDefaults ??
|
|
203
|
+
rawConfig?.worker_role_defaults ??
|
|
204
|
+
rawConfig?.workerRoleDefaults ??
|
|
205
|
+
{};
|
|
206
|
+
|
|
207
|
+
if (!rawDefaults || typeof rawDefaults !== 'object') {
|
|
208
|
+
return {};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const normalized = {};
|
|
212
|
+
for (const [role, value] of Object.entries(rawDefaults)) {
|
|
213
|
+
const provider = normalizeProviderChoice(value);
|
|
214
|
+
if (provider) {
|
|
215
|
+
normalized[role] = provider;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
return normalized;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function buildRoutingDefaults(rawNotionAutomation) {
|
|
223
|
+
const workspaceDefaultProvider =
|
|
224
|
+
normalizeProviderChoice(rawNotionAutomation?.workspace_default_provider) ??
|
|
225
|
+
normalizeProviderChoice(rawNotionAutomation?.workspaceDefaultProvider) ??
|
|
226
|
+
normalizeProviderChoice(rawNotionAutomation?.agent_provider_override) ??
|
|
227
|
+
normalizeProviderChoice(rawNotionAutomation?.agentProviderOverride);
|
|
228
|
+
|
|
229
|
+
return {
|
|
230
|
+
workspaceDefaultProvider,
|
|
231
|
+
workerRoleDefaults: parseWorkerRoleDefaults(rawNotionAutomation),
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
export async function listAvailableProviders(env = process.env) {
|
|
236
|
+
const { configPath, raw } = await readGuiConfig(env);
|
|
237
|
+
const notionAutomation = getRawNotionAutomationConfig(raw);
|
|
238
|
+
const defaults = buildRoutingDefaults(notionAutomation);
|
|
239
|
+
|
|
240
|
+
const configured = getRawAgentProviders(notionAutomation)
|
|
241
|
+
.map((candidate, index) => normalizeConfiguredProvider(candidate, index, 'gui_config'))
|
|
242
|
+
.filter(Boolean);
|
|
243
|
+
|
|
244
|
+
const providers = configured.length > 0 ? configured : buildEnvFallbackProviders(env);
|
|
245
|
+
const deduped = new Map();
|
|
246
|
+
|
|
247
|
+
for (const provider of providers) {
|
|
248
|
+
if (!deduped.has(provider.id)) {
|
|
249
|
+
deduped.set(provider.id, provider);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return {
|
|
254
|
+
source: configured.length > 0 ? 'gui_config' : 'env',
|
|
255
|
+
sourceDetail: configured.length > 0 ? configPath : 'environment',
|
|
256
|
+
providers: [...deduped.values()].map((provider, index) => ({
|
|
257
|
+
...provider,
|
|
258
|
+
priority: index,
|
|
259
|
+
isWorkspaceDefault: provider.id === defaults.workspaceDefaultProvider,
|
|
260
|
+
})),
|
|
261
|
+
defaults,
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
export class ProviderSelectionError extends Error {
|
|
266
|
+
constructor(code, message, details = {}) {
|
|
267
|
+
super(message);
|
|
268
|
+
this.name = 'ProviderSelectionError';
|
|
269
|
+
this.code = code;
|
|
270
|
+
this.details = details;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function resolveWorkerRoleCandidate(workerRole, defaults) {
|
|
275
|
+
if (!workerRole) {
|
|
276
|
+
return undefined;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const exact = defaults.workerRoleDefaults?.[workerRole];
|
|
280
|
+
if (exact) {
|
|
281
|
+
return exact;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const normalized = workerRole.toLowerCase();
|
|
285
|
+
if (defaults.workerRoleDefaults?.[normalized]) {
|
|
286
|
+
return defaults.workerRoleDefaults[normalized];
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
return undefined;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function selectProvider(options, providerState) {
|
|
293
|
+
const {
|
|
294
|
+
preferredProvider,
|
|
295
|
+
workflowDefaultProvider,
|
|
296
|
+
workerRole,
|
|
297
|
+
allowFallback = 'auto',
|
|
298
|
+
} = options;
|
|
299
|
+
|
|
300
|
+
const availableProviders = providerState.providers.filter((provider) => provider.available);
|
|
301
|
+
const availableIds = new Set(availableProviders.map((provider) => provider.id));
|
|
302
|
+
const explicitPreferred = normalizeProviderChoice(preferredProvider);
|
|
303
|
+
|
|
304
|
+
if (explicitPreferred) {
|
|
305
|
+
const selected = availableProviders.find((provider) => provider.id === explicitPreferred);
|
|
306
|
+
if (selected) {
|
|
307
|
+
return {
|
|
308
|
+
provider: selected,
|
|
309
|
+
selectionSource: 'explicit',
|
|
310
|
+
fellBack: false,
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
if (allowFallback === 'auto') {
|
|
315
|
+
return {
|
|
316
|
+
provider: availableProviders[0] ?? null,
|
|
317
|
+
selectionSource: 'explicit-fallback',
|
|
318
|
+
fellBack: true,
|
|
319
|
+
fallbackFrom: explicitPreferred,
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
throw new ProviderSelectionError('preferred_provider_unavailable', `Preferred provider "${explicitPreferred}" is not available.`, {
|
|
324
|
+
preferredProvider: explicitPreferred,
|
|
325
|
+
availableProviders,
|
|
326
|
+
canConnectAnotherProvider: true,
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const orderedCandidates = [
|
|
331
|
+
resolveWorkerRoleCandidate(workerRole, providerState.defaults),
|
|
332
|
+
normalizeProviderChoice(workflowDefaultProvider),
|
|
333
|
+
providerState.defaults.workspaceDefaultProvider,
|
|
334
|
+
].filter(Boolean);
|
|
335
|
+
|
|
336
|
+
for (const candidate of orderedCandidates) {
|
|
337
|
+
if (!availableIds.has(candidate)) {
|
|
338
|
+
continue;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
return {
|
|
342
|
+
provider: availableProviders.find((provider) => provider.id === candidate),
|
|
343
|
+
selectionSource: candidate === providerState.defaults.workspaceDefaultProvider ? 'workspace-default' : 'policy-default',
|
|
344
|
+
fellBack: false,
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
if (availableProviders.length === 0) {
|
|
349
|
+
throw new ProviderSelectionError('no_available_providers', 'No configured providers are available.', {
|
|
350
|
+
availableProviders: [],
|
|
351
|
+
canConnectAnotherProvider: true,
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
return {
|
|
356
|
+
provider: availableProviders[0],
|
|
357
|
+
selectionSource: 'first-available',
|
|
358
|
+
fellBack: false,
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function createEvent(session, type, fields = {}) {
|
|
363
|
+
const sequence = session.nextSequence++;
|
|
364
|
+
return {
|
|
365
|
+
id: crypto.randomUUID(),
|
|
366
|
+
sequence,
|
|
367
|
+
sessionId: session.id,
|
|
368
|
+
provider: session.provider,
|
|
369
|
+
sessionProvider: session.sessionProvider,
|
|
370
|
+
providerSessionId: session.providerSessionId ?? null,
|
|
371
|
+
turn: session.turn,
|
|
372
|
+
timestamp: new Date().toISOString(),
|
|
373
|
+
type,
|
|
374
|
+
...fields,
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function emitEvent(session, type, fields = {}) {
|
|
379
|
+
const lastEvent = session.events[session.events.length - 1];
|
|
380
|
+
if (
|
|
381
|
+
type === 'status' &&
|
|
382
|
+
lastEvent?.type === 'status' &&
|
|
383
|
+
lastEvent?.turn === session.turn &&
|
|
384
|
+
lastEvent?.text === fields.text
|
|
385
|
+
) {
|
|
386
|
+
return lastEvent;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
const event = createEvent(session, type, fields);
|
|
390
|
+
session.events.push(event);
|
|
391
|
+
session.updatedAt = event.timestamp;
|
|
392
|
+
|
|
393
|
+
if (session.events.length > 2000) {
|
|
394
|
+
session.events.splice(0, session.events.length - 2000);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
for (const subscriber of session.subscribers) {
|
|
398
|
+
subscriber(event);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
return event;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function ensureSession(sessionId) {
|
|
405
|
+
const session = localSessions.get(sessionId);
|
|
406
|
+
if (!session) {
|
|
407
|
+
throw new ProviderSelectionError('session_not_found', `Unknown agent session "${sessionId}".`);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
return session;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function extractTextContent(payload) {
|
|
414
|
+
if (!payload || typeof payload !== 'object') {
|
|
415
|
+
return '';
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
if (typeof payload.content === 'string') {
|
|
419
|
+
return payload.content;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
if (typeof payload.message === 'string') {
|
|
423
|
+
return payload.message;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
if (payload.message && typeof payload.message === 'object' && typeof payload.message.content === 'string') {
|
|
427
|
+
return payload.message.content;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
if (typeof payload.text === 'string') {
|
|
431
|
+
return payload.text;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
if (typeof payload.resultText === 'string') {
|
|
435
|
+
return payload.resultText;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
return '';
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
function ingestPayload(session, payload) {
|
|
442
|
+
const data =
|
|
443
|
+
typeof payload === 'string'
|
|
444
|
+
? (() => {
|
|
445
|
+
try {
|
|
446
|
+
return JSON.parse(payload);
|
|
447
|
+
} catch {
|
|
448
|
+
return { kind: 'status', text: payload };
|
|
449
|
+
}
|
|
450
|
+
})()
|
|
451
|
+
: payload;
|
|
452
|
+
|
|
453
|
+
if (!data || typeof data !== 'object') {
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
const kind = typeof data.kind === 'string' ? data.kind : typeof data.type === 'string' ? data.type : 'status';
|
|
458
|
+
|
|
459
|
+
if (kind === 'session_created') {
|
|
460
|
+
session.providerSessionId = readOptionalString(data.newSessionId) ?? readOptionalString(data.sessionId) ?? session.providerSessionId;
|
|
461
|
+
emitEvent(session, 'status', {
|
|
462
|
+
text: 'provider_session_created',
|
|
463
|
+
raw: data,
|
|
464
|
+
});
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
if (kind === 'text' || kind === 'stream_delta' || kind === 'thinking') {
|
|
469
|
+
const content = extractTextContent(data);
|
|
470
|
+
if (content) {
|
|
471
|
+
if (kind !== 'thinking') {
|
|
472
|
+
session.assistantText += content;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
emitEvent(session, 'text_delta', {
|
|
476
|
+
content,
|
|
477
|
+
channel: kind === 'thinking' ? 'thinking' : 'assistant',
|
|
478
|
+
raw: data,
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
if (kind === 'tool_use') {
|
|
485
|
+
emitEvent(session, 'tool_call', {
|
|
486
|
+
toolName: data.toolName ?? data.tool ?? 'tool',
|
|
487
|
+
toolInput: data.toolInput ?? data.arguments ?? null,
|
|
488
|
+
toolId: data.toolId ?? null,
|
|
489
|
+
raw: data,
|
|
490
|
+
});
|
|
491
|
+
return;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
if (kind === 'tool_result') {
|
|
495
|
+
emitEvent(session, 'tool_result', {
|
|
496
|
+
toolId: data.toolId ?? null,
|
|
497
|
+
content: extractTextContent(data),
|
|
498
|
+
isError: Boolean(data.isError),
|
|
499
|
+
raw: data,
|
|
500
|
+
});
|
|
501
|
+
return;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
if (kind === 'permission_request' || kind === 'permission_cancelled' || kind === 'interactive_prompt' || kind === 'task_notification') {
|
|
505
|
+
emitEvent(session, 'warning', {
|
|
506
|
+
text: extractTextContent(data) || kind,
|
|
507
|
+
raw: data,
|
|
508
|
+
});
|
|
509
|
+
return;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
if (kind === 'error') {
|
|
513
|
+
session.turnTerminal = true;
|
|
514
|
+
session.status = 'failed';
|
|
515
|
+
emitEvent(session, 'error', {
|
|
516
|
+
content: extractTextContent(data) || 'Unknown runner error',
|
|
517
|
+
raw: data,
|
|
518
|
+
});
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
if (kind === 'complete' || kind === 'stream_end') {
|
|
523
|
+
if (kind === 'complete') {
|
|
524
|
+
session.turnTerminal = true;
|
|
525
|
+
session.status = data.aborted ? 'aborted' : 'completed';
|
|
526
|
+
const resultText = extractTextContent(data) || session.assistantText.trim();
|
|
527
|
+
emitEvent(session, 'complete', {
|
|
528
|
+
aborted: Boolean(data.aborted),
|
|
529
|
+
exitCode: data.exitCode ?? null,
|
|
530
|
+
resultText,
|
|
531
|
+
raw: data,
|
|
532
|
+
});
|
|
533
|
+
}
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
emitEvent(session, 'status', {
|
|
538
|
+
text: extractTextContent(data) || kind,
|
|
539
|
+
raw: data,
|
|
540
|
+
});
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
class AgentSessionWriter {
|
|
544
|
+
constructor(session) {
|
|
545
|
+
this.session = session;
|
|
546
|
+
this.userId = session.userId ?? null;
|
|
547
|
+
this.isSSEStreamWriter = true;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
send(data) {
|
|
551
|
+
ingestPayload(this.session, data);
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
end() {
|
|
555
|
+
// Providers already emit terminal events. Nothing to do here.
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
setSessionId(sessionId) {
|
|
559
|
+
this.session.providerSessionId = sessionId;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
getSessionId() {
|
|
563
|
+
return this.session.providerSessionId ?? this.session.id;
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
function buildProviderOptions(session, overrides = {}) {
|
|
568
|
+
return {
|
|
569
|
+
sessionId: session.providerSessionId ?? undefined,
|
|
570
|
+
cwd: session.cwd,
|
|
571
|
+
projectPath: session.cwd,
|
|
572
|
+
sessionSummary: session.metadata?.summary,
|
|
573
|
+
model: overrides.model ?? session.model,
|
|
574
|
+
apiKey: session.apiKey,
|
|
575
|
+
baseUrl: session.baseUrl,
|
|
576
|
+
permissionMode: overrides.permissionMode ?? 'bypassPermissions',
|
|
577
|
+
skipPermissions: true,
|
|
578
|
+
toolsSettings: {
|
|
579
|
+
skipPermissions: true,
|
|
580
|
+
allowedTools: [],
|
|
581
|
+
disallowedTools: [],
|
|
582
|
+
},
|
|
583
|
+
};
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
async function invokeProvider(session, prompt, overrides = {}) {
|
|
587
|
+
const writer = new AgentSessionWriter(session);
|
|
588
|
+
const options = buildProviderOptions(session, overrides);
|
|
589
|
+
|
|
590
|
+
switch (session.sessionProvider) {
|
|
591
|
+
case 'claude':
|
|
592
|
+
return await queryClaudeSDK(prompt, options, writer);
|
|
593
|
+
case 'codex':
|
|
594
|
+
return await queryCodex(prompt, options, writer);
|
|
595
|
+
case 'cursor':
|
|
596
|
+
return await spawnCursor(prompt, options, writer);
|
|
597
|
+
case 'gemini':
|
|
598
|
+
return await spawnGemini(prompt, options, writer);
|
|
599
|
+
default:
|
|
600
|
+
throw new Error(`Unsupported session provider "${session.sessionProvider}".`);
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
async function runTurn(session, prompt, reason, overrides = {}) {
|
|
605
|
+
if (session.status === 'running') {
|
|
606
|
+
throw new ProviderSelectionError('session_busy', `Agent session "${session.id}" is already running.`);
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
session.turn += 1;
|
|
610
|
+
session.turnTerminal = false;
|
|
611
|
+
session.status = 'running';
|
|
612
|
+
session.currentPrompt = prompt;
|
|
613
|
+
session.assistantText = '';
|
|
614
|
+
|
|
615
|
+
emitEvent(session, 'status', {
|
|
616
|
+
text: reason,
|
|
617
|
+
canAbort: true,
|
|
618
|
+
raw: {
|
|
619
|
+
kind: 'status',
|
|
620
|
+
reason,
|
|
621
|
+
},
|
|
622
|
+
});
|
|
623
|
+
|
|
624
|
+
try {
|
|
625
|
+
await invokeProvider(session, prompt, overrides);
|
|
626
|
+
if (!session.turnTerminal) {
|
|
627
|
+
session.status = 'completed';
|
|
628
|
+
emitEvent(session, 'complete', {
|
|
629
|
+
aborted: false,
|
|
630
|
+
exitCode: 0,
|
|
631
|
+
resultText: session.assistantText.trim(),
|
|
632
|
+
});
|
|
633
|
+
}
|
|
634
|
+
} catch (error) {
|
|
635
|
+
if (!session.turnTerminal) {
|
|
636
|
+
session.status = 'failed';
|
|
637
|
+
session.turnTerminal = true;
|
|
638
|
+
emitEvent(session, 'error', {
|
|
639
|
+
content: error instanceof Error ? error.message : String(error),
|
|
640
|
+
});
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
export async function startLocalAgentSession(input, context = {}) {
|
|
646
|
+
const providerState = await listAvailableProviders();
|
|
647
|
+
const selection = selectProvider(input, providerState);
|
|
648
|
+
const provider = selection.provider;
|
|
649
|
+
if (!provider) {
|
|
650
|
+
throw new ProviderSelectionError('no_available_providers', 'No configured providers are available.', {
|
|
651
|
+
availableProviders: providerState.providers,
|
|
652
|
+
canConnectAnotherProvider: true,
|
|
653
|
+
});
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
const session = {
|
|
657
|
+
id: crypto.randomUUID(),
|
|
658
|
+
provider: provider.id,
|
|
659
|
+
sessionProvider: provider.sessionProvider,
|
|
660
|
+
cwd: input.cwd,
|
|
661
|
+
apiKey: provider.apiKey,
|
|
662
|
+
baseUrl: provider.baseUrl,
|
|
663
|
+
workerRole: input.workerRole ?? null,
|
|
664
|
+
allowFallback: input.allowFallback ?? 'auto',
|
|
665
|
+
status: 'queued',
|
|
666
|
+
createdAt: new Date().toISOString(),
|
|
667
|
+
updatedAt: new Date().toISOString(),
|
|
668
|
+
providerSessionId: null,
|
|
669
|
+
metadata: input.metadata ?? {},
|
|
670
|
+
model: readOptionalString(input.model),
|
|
671
|
+
subscribers: new Set(),
|
|
672
|
+
events: [],
|
|
673
|
+
nextSequence: 1,
|
|
674
|
+
turn: 0,
|
|
675
|
+
turnTerminal: false,
|
|
676
|
+
currentPrompt: null,
|
|
677
|
+
assistantText: '',
|
|
678
|
+
userId: context.userId ?? null,
|
|
679
|
+
routing: {
|
|
680
|
+
selectionSource: selection.selectionSource,
|
|
681
|
+
fellBack: Boolean(selection.fellBack),
|
|
682
|
+
fallbackFrom: selection.fallbackFrom ?? null,
|
|
683
|
+
availableProviders: providerState.providers,
|
|
684
|
+
defaults: providerState.defaults,
|
|
685
|
+
},
|
|
686
|
+
};
|
|
687
|
+
|
|
688
|
+
localSessions.set(session.id, session);
|
|
689
|
+
|
|
690
|
+
emitEvent(session, 'session_started', {
|
|
691
|
+
text: 'session_started',
|
|
692
|
+
raw: {
|
|
693
|
+
workerRole: session.workerRole,
|
|
694
|
+
selectionSource: selection.selectionSource,
|
|
695
|
+
fellBack: Boolean(selection.fellBack),
|
|
696
|
+
fallbackFrom: selection.fallbackFrom ?? null,
|
|
697
|
+
availableProviders: providerState.providers,
|
|
698
|
+
},
|
|
699
|
+
});
|
|
700
|
+
|
|
701
|
+
void runTurn(session, input.prompt, 'initial_prompt', {
|
|
702
|
+
model: input.model,
|
|
703
|
+
});
|
|
704
|
+
|
|
705
|
+
return {
|
|
706
|
+
sessionId: session.id,
|
|
707
|
+
provider: session.provider,
|
|
708
|
+
sessionProvider: session.sessionProvider,
|
|
709
|
+
status: session.status,
|
|
710
|
+
createdAt: session.createdAt,
|
|
711
|
+
routing: session.routing,
|
|
712
|
+
};
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
export function getLocalAgentSession(sessionId) {
|
|
716
|
+
const session = ensureSession(sessionId);
|
|
717
|
+
return {
|
|
718
|
+
sessionId: session.id,
|
|
719
|
+
provider: session.provider,
|
|
720
|
+
sessionProvider: session.sessionProvider,
|
|
721
|
+
status: session.status,
|
|
722
|
+
createdAt: session.createdAt,
|
|
723
|
+
updatedAt: session.updatedAt,
|
|
724
|
+
providerSessionId: session.providerSessionId,
|
|
725
|
+
canAbort: session.status === 'running',
|
|
726
|
+
currentPrompt: session.currentPrompt,
|
|
727
|
+
metadata: session.metadata,
|
|
728
|
+
routing: session.routing,
|
|
729
|
+
};
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
export function getLocalAgentSessionEvents(sessionId, afterSequence = 0) {
|
|
733
|
+
const session = ensureSession(sessionId);
|
|
734
|
+
return session.events.filter((event) => event.sequence > afterSequence);
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
export function subscribeToLocalAgentSession(sessionId, handler) {
|
|
738
|
+
const session = ensureSession(sessionId);
|
|
739
|
+
session.subscribers.add(handler);
|
|
740
|
+
return () => {
|
|
741
|
+
session.subscribers.delete(handler);
|
|
742
|
+
};
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
export async function sendLocalAgentSessionMessage(sessionId, input) {
|
|
746
|
+
const session = ensureSession(sessionId);
|
|
747
|
+
void runTurn(session, input.prompt, 'followup_prompt', {
|
|
748
|
+
model: input.model,
|
|
749
|
+
});
|
|
750
|
+
return getLocalAgentSession(sessionId);
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
export async function abortLocalAgentSession(sessionId) {
|
|
754
|
+
const session = ensureSession(sessionId);
|
|
755
|
+
|
|
756
|
+
session.status = 'aborted';
|
|
757
|
+
session.turnTerminal = true;
|
|
758
|
+
|
|
759
|
+
if (session.providerSessionId) {
|
|
760
|
+
if (session.sessionProvider === 'claude') {
|
|
761
|
+
await abortClaudeSDKSession(session.providerSessionId);
|
|
762
|
+
} else if (session.sessionProvider === 'codex') {
|
|
763
|
+
abortCodexSession(session.providerSessionId);
|
|
764
|
+
} else if (session.sessionProvider === 'cursor') {
|
|
765
|
+
abortCursorSession(session.providerSessionId);
|
|
766
|
+
} else if (session.sessionProvider === 'gemini') {
|
|
767
|
+
abortGeminiSession(session.providerSessionId);
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
emitEvent(session, 'complete', {
|
|
772
|
+
aborted: true,
|
|
773
|
+
exitCode: null,
|
|
774
|
+
resultText: session.assistantText.trim(),
|
|
775
|
+
});
|
|
776
|
+
|
|
777
|
+
return { ok: true };
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
export function isLocalAgentSessionActive(sessionId) {
|
|
781
|
+
const session = ensureSession(sessionId);
|
|
782
|
+
const providerSessionId = session.providerSessionId;
|
|
783
|
+
|
|
784
|
+
if (!providerSessionId) {
|
|
785
|
+
return session.status === 'running';
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
if (session.sessionProvider === 'claude') {
|
|
789
|
+
return isClaudeSDKSessionActive(providerSessionId);
|
|
790
|
+
}
|
|
791
|
+
if (session.sessionProvider === 'codex') {
|
|
792
|
+
return isCodexSessionActive(providerSessionId);
|
|
793
|
+
}
|
|
794
|
+
if (session.sessionProvider === 'cursor') {
|
|
795
|
+
return isCursorSessionActive(providerSessionId);
|
|
796
|
+
}
|
|
797
|
+
if (session.sessionProvider === 'gemini') {
|
|
798
|
+
return isGeminiSessionActive(providerSessionId);
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
return false;
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
setInterval(() => {
|
|
805
|
+
const cutoff = Date.now() - SESSION_RETENTION_MS;
|
|
806
|
+
for (const [sessionId, session] of localSessions.entries()) {
|
|
807
|
+
if (session.status === 'running') {
|
|
808
|
+
continue;
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
const updatedAt = new Date(session.updatedAt).getTime();
|
|
812
|
+
if (updatedAt < cutoff && session.subscribers.size === 0) {
|
|
813
|
+
localSessions.delete(sessionId);
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
}, 5 * 60 * 1000);
|