tuna-agent 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/dist/agents/claude-code-adapter.d.ts +3 -1
- package/dist/agents/claude-code-adapter.js +28 -4
- package/dist/agents/factory.d.ts +2 -1
- package/dist/agents/factory.js +2 -2
- package/dist/browser/actions/download.d.ts +16 -0
- package/dist/browser/actions/download.js +39 -0
- package/dist/browser/actions/emulation.d.ts +53 -0
- package/dist/browser/actions/emulation.js +103 -0
- package/dist/browser/actions/evaluate.d.ts +29 -0
- package/dist/browser/actions/evaluate.js +92 -0
- package/dist/browser/actions/interaction.d.ts +79 -0
- package/dist/browser/actions/interaction.js +210 -0
- package/dist/browser/actions/keyboard.d.ts +6 -0
- package/dist/browser/actions/keyboard.js +9 -0
- package/dist/browser/actions/navigation.d.ts +40 -0
- package/dist/browser/actions/navigation.js +92 -0
- package/dist/browser/actions/wait.d.ts +12 -0
- package/dist/browser/actions/wait.js +33 -0
- package/dist/browser/browser.d.ts +722 -0
- package/dist/browser/browser.js +1066 -0
- package/dist/browser/capture/activity.d.ts +22 -0
- package/dist/browser/capture/activity.js +39 -0
- package/dist/browser/capture/pdf.d.ts +6 -0
- package/dist/browser/capture/pdf.js +6 -0
- package/dist/browser/capture/response.d.ts +8 -0
- package/dist/browser/capture/response.js +28 -0
- package/dist/browser/capture/screenshot.d.ts +30 -0
- package/dist/browser/capture/screenshot.js +72 -0
- package/dist/browser/capture/trace.d.ts +13 -0
- package/dist/browser/capture/trace.js +19 -0
- package/dist/browser/chrome-launcher.d.ts +8 -0
- package/dist/browser/chrome-launcher.js +543 -0
- package/dist/browser/connection.d.ts +42 -0
- package/dist/browser/connection.js +359 -0
- package/dist/browser/index.d.ts +6 -0
- package/dist/browser/index.js +3 -0
- package/dist/browser/security.d.ts +51 -0
- package/dist/browser/security.js +357 -0
- package/dist/browser/snapshot/ai-snapshot.d.ts +12 -0
- package/dist/browser/snapshot/ai-snapshot.js +47 -0
- package/dist/browser/snapshot/aria-snapshot.d.ts +26 -0
- package/dist/browser/snapshot/aria-snapshot.js +121 -0
- package/dist/browser/snapshot/ref-map.d.ts +31 -0
- package/dist/browser/snapshot/ref-map.js +250 -0
- package/dist/browser/storage/index.d.ts +36 -0
- package/dist/browser/storage/index.js +65 -0
- package/dist/browser/types.d.ts +429 -0
- package/dist/browser/types.js +2 -0
- package/dist/cli/commands/extension.d.ts +10 -0
- package/dist/cli/commands/extension.js +86 -0
- package/dist/cli/index.js +12 -0
- package/dist/daemon/extension-handlers.d.ts +63 -0
- package/dist/daemon/extension-handlers.js +630 -0
- package/dist/daemon/index.js +173 -44
- package/dist/daemon/ws-client.d.ts +28 -8
- package/dist/daemon/ws-client.js +68 -62
- package/dist/mcp/browser-server.d.ts +11 -0
- package/dist/mcp/browser-server.js +467 -0
- package/dist/mcp/knowledge-server.d.ts +11 -0
- package/dist/mcp/knowledge-server.js +263 -0
- package/dist/mcp/setup.d.ts +20 -0
- package/dist/mcp/setup.js +94 -0
- package/dist/types/index.d.ts +2 -0
- package/dist/utils/claude-cli.d.ts +2 -0
- package/dist/utils/claude-cli.js +29 -9
- package/dist/utils/message-schemas.d.ts +4 -1
- package/dist/utils/message-schemas.js +6 -1
- package/package.json +2 -1
package/dist/daemon/index.js
CHANGED
|
@@ -2,15 +2,17 @@ import fs from 'fs';
|
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import os from 'os';
|
|
4
4
|
import { AgentWebSocketClient } from './ws-client.js';
|
|
5
|
-
import { removePid } from '../config/store.js';
|
|
6
|
-
import { validatePath } from '../utils/validate-path.js';
|
|
5
|
+
import { removePid, loadConfig, saveConfig } from '../config/store.js';
|
|
7
6
|
import { validateMessage } from '../utils/message-schemas.js';
|
|
8
7
|
import { createAgentAdapter } from '../agents/factory.js';
|
|
9
8
|
import { loadPMState, savePMState, clearPMState } from './pm-state.js';
|
|
10
9
|
import { chatWithPM } from '../pm/planner.js';
|
|
11
10
|
import { executePlanAndReport, simplifyMarkdown, waitForInput } from '../utils/execution-helpers.js';
|
|
12
11
|
import { runClaude } from '../utils/claude-cli.js';
|
|
12
|
+
import { handleGetHistory, handleRetryVideo, handleGenerateIdeas, handleGenerateScript, handleGenerateScene, handleGenerateScenes, handleRenderVideo, handleListCharacters, handleCreateCharacter, handleSaveCharacterSelection, } from './extension-handlers.js';
|
|
13
13
|
import { downloadAttachments, cleanupAttachments } from '../utils/image-download.js';
|
|
14
|
+
import { scanSkills } from '../utils/skill-scanner.js';
|
|
15
|
+
import { setupMcpConfig } from '../mcp/setup.js';
|
|
14
16
|
/**
|
|
15
17
|
* Start the agent daemon.
|
|
16
18
|
* Connects to API via WebSocket, receives tasks, delegates to agent adapter.
|
|
@@ -21,7 +23,14 @@ export async function startDaemon(config) {
|
|
|
21
23
|
type: config.agentType || 'claude-code',
|
|
22
24
|
...config.agentConfig,
|
|
23
25
|
};
|
|
24
|
-
const adapter = createAgentAdapter(agentConfig);
|
|
26
|
+
const adapter = createAgentAdapter(agentConfig, config);
|
|
27
|
+
// Setup MCP config for Knowledge server
|
|
28
|
+
try {
|
|
29
|
+
setupMcpConfig(config);
|
|
30
|
+
}
|
|
31
|
+
catch (err) {
|
|
32
|
+
console.warn(`[Daemon] MCP config setup failed (non-fatal):`, err);
|
|
33
|
+
}
|
|
25
34
|
// Health check on startup
|
|
26
35
|
const health = await adapter.checkHealth();
|
|
27
36
|
if (!health.ok) {
|
|
@@ -58,8 +67,8 @@ export async function startDaemon(config) {
|
|
|
58
67
|
switch (type) {
|
|
59
68
|
case 'connected':
|
|
60
69
|
console.log(`[Daemon] Connected as "${msg.name}" (${msg.agentId})`);
|
|
61
|
-
// Recover
|
|
62
|
-
ws.send({ action: 'recover_orphaned_tasks' });
|
|
70
|
+
// Recover orphaned tasks — pass activeTaskId so API won't fail a task we're still running
|
|
71
|
+
ws.send({ action: 'recover_orphaned_tasks', activeTaskId: currentTaskId ?? undefined });
|
|
63
72
|
break;
|
|
64
73
|
case 'task_assigned': {
|
|
65
74
|
const task = msg.task;
|
|
@@ -67,6 +76,10 @@ export async function startDaemon(config) {
|
|
|
67
76
|
console.error('[Daemon] Received task_assigned without task data');
|
|
68
77
|
break;
|
|
69
78
|
}
|
|
79
|
+
// Resolve tilde in repoPath (e.g. ~/agents/co-founder → /Users/admin/agents/co-founder)
|
|
80
|
+
if (task.repoPath?.startsWith('~/')) {
|
|
81
|
+
task.repoPath = path.join(os.homedir(), task.repoPath.slice(2));
|
|
82
|
+
}
|
|
70
83
|
if (activeTasks >= MAX_CONCURRENT) {
|
|
71
84
|
console.log(`[Daemon] Busy — rejecting task ${task.id}`);
|
|
72
85
|
ws.send({ action: 'task_rejected', taskId: task.id, reason: 'agent_busy' });
|
|
@@ -76,6 +89,15 @@ export async function startDaemon(config) {
|
|
|
76
89
|
currentTaskId = task.id;
|
|
77
90
|
currentTaskAbort = new AbortController();
|
|
78
91
|
console.log(`[Daemon] Received task: ${task.id} — ${task.description.slice(0, 80)} (attachments: ${task.attachments?.length ?? 0})`);
|
|
92
|
+
// Update MCP config with the task's agent ID so knowledge server uses correct attribution
|
|
93
|
+
if (task.agentId) {
|
|
94
|
+
try {
|
|
95
|
+
setupMcpConfig({ ...config, agentId: task.agentId });
|
|
96
|
+
}
|
|
97
|
+
catch (err) {
|
|
98
|
+
console.warn(`[Daemon] MCP config update for task failed (non-fatal):`, err);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
79
101
|
try {
|
|
80
102
|
await adapter.handleTask(task, ws, pendingInputResolvers, currentTaskAbort.signal, pendingPermissionResolvers);
|
|
81
103
|
}
|
|
@@ -117,42 +139,21 @@ export async function startDaemon(config) {
|
|
|
117
139
|
}
|
|
118
140
|
case 'command': {
|
|
119
141
|
const command = msg.command;
|
|
120
|
-
if (command === '
|
|
121
|
-
const
|
|
122
|
-
const
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
const rawFolders = msg.agent_folders || [];
|
|
136
|
-
const agentFolders = rawFolders.map(f => f.startsWith('~') ? path.join(os.homedir(), f.slice(1)) : f);
|
|
137
|
-
console.log(`[Daemon] Scanning workspace: ${wsPath}, agent folders: ${agentFolders.length}`);
|
|
138
|
-
ws.setWorkspacePath(wsPath);
|
|
139
|
-
ws.setAgentFolders(agentFolders);
|
|
140
|
-
try {
|
|
141
|
-
const entries = fs.readdirSync(wsPath, { withFileTypes: true });
|
|
142
|
-
const projects = entries
|
|
143
|
-
.filter(e => e.isDirectory() && !e.name.startsWith('.'))
|
|
144
|
-
.map(e => ({
|
|
145
|
-
name: e.name,
|
|
146
|
-
path: path.join(wsPath, e.name),
|
|
147
|
-
}));
|
|
148
|
-
ws.send({ action: 'workspace_scanned', projects });
|
|
149
|
-
console.log(`[Daemon] Found ${projects.length} projects in workspace`);
|
|
150
|
-
}
|
|
151
|
-
catch (err) {
|
|
152
|
-
const errMsg = err instanceof Error ? err.message : String(err);
|
|
153
|
-
console.error(`[Daemon] Workspace scan failed: ${errMsg}`);
|
|
154
|
-
ws.send({ action: 'workspace_scanned', projects: [] });
|
|
155
|
-
}
|
|
142
|
+
if (command === 'rescan_agent_skills') {
|
|
143
|
+
const agentId = msg.agent_id;
|
|
144
|
+
const rawFolder = msg.folder_path || '';
|
|
145
|
+
const rawWsPath = msg.workspace_path || '~/tuna-workspace';
|
|
146
|
+
const wsPath = rawWsPath.startsWith('~')
|
|
147
|
+
? path.join(os.homedir(), rawWsPath.slice(1))
|
|
148
|
+
: rawWsPath;
|
|
149
|
+
const folder = rawFolder.startsWith('~')
|
|
150
|
+
? path.join(os.homedir(), rawFolder.slice(1))
|
|
151
|
+
: rawFolder;
|
|
152
|
+
console.log(`[Daemon] Rescan skills for agent ${agentId}, folder: ${folder || '(none)'}`);
|
|
153
|
+
const folders = folder ? [folder] : [];
|
|
154
|
+
const skills = scanSkills(wsPath, folders);
|
|
155
|
+
ws.send({ action: 'agent_skills_scanned', agent_id: agentId, skills });
|
|
156
|
+
console.log(`[Daemon] Scanned ${skills.length} skill(s) for agent ${agentId}`);
|
|
156
157
|
}
|
|
157
158
|
else if (command === 'analyze_skill') {
|
|
158
159
|
const skillId = msg.skill_id;
|
|
@@ -180,12 +181,15 @@ export async function startDaemon(config) {
|
|
|
180
181
|
const analyzePrompt = `You are analyzing a Claude Code skill file (markdown prompt template). Extract the following structured information from the content below:
|
|
181
182
|
|
|
182
183
|
1. **description**: A short summary of what this skill does in under 80 characters. Be very concise — like a subtitle, not a full sentence.
|
|
183
|
-
2. **actions**: Sub-commands or modes available in this skill. Look for sections, headings, or conditional logic that indicate different actions/modes the user can invoke. Each action has
|
|
184
|
-
|
|
184
|
+
2. **actions**: Sub-commands or modes available in this skill. Look for sections, headings, or conditional logic that indicate different actions/modes the user can invoke. Each action has:
|
|
185
|
+
- "name" (short, lowercase, no spaces — use hyphens)
|
|
186
|
+
- "description" (what it does, max 100 chars)
|
|
187
|
+
- "params" (array of parameter KEY strings that are relevant to THIS specific action — only include params that this action actually uses)
|
|
188
|
+
3. **parameters**: ALL input parameters the skill accepts (the full definitions). Look for {{param_name}} placeholders, $ARGUMENTS references, or documented inputs. Each parameter has "key", "label", "type" (text/number/select/multiline), "required" (boolean), "default_value", "options" (for select type), "placeholder".
|
|
185
189
|
- IMPORTANT: If a parameter has a finite set of known values (e.g. listed in config files, enums, documented choices), use type "select" and populate the "options" array with ALL known values. Only use type "text" when the input is truly free-form.
|
|
186
190
|
|
|
187
191
|
Respond with ONLY valid JSON, no markdown, no explanation:
|
|
188
|
-
{"description":"...","actions":[{"name":"...","description":"..."}],"parameters":[{"key":"...","label":"...","type":"select","required":false,"options":["opt1","opt2"]}]}
|
|
192
|
+
{"description":"...","actions":[{"name":"...","description":"...","params":["param_key1","param_key2"]}],"parameters":[{"key":"...","label":"...","type":"select","required":false,"options":["opt1","opt2"]}]}
|
|
189
193
|
|
|
190
194
|
If no actions are found, return an empty array. If no parameters found, return an empty array.
|
|
191
195
|
|
|
@@ -287,8 +291,133 @@ ${skillContent.slice(0, 15000)}`;
|
|
|
287
291
|
}
|
|
288
292
|
break;
|
|
289
293
|
}
|
|
294
|
+
case 'extension_task': {
|
|
295
|
+
const extCode = msg.code;
|
|
296
|
+
const extTaskId = msg.taskId;
|
|
297
|
+
const extTask = msg.task;
|
|
298
|
+
const extStream = msg.stream;
|
|
299
|
+
if (!extCode || !extTaskId || !extTask) {
|
|
300
|
+
console.warn('[Daemon] extension_task missing required fields');
|
|
301
|
+
break;
|
|
302
|
+
}
|
|
303
|
+
console.log(`[Daemon] Extension task ${extTaskId}: ${extTask.substring(0, 80)}`);
|
|
304
|
+
// ── Specialized handlers (no Claude needed) ──────────────────────────
|
|
305
|
+
if (extTask === 'get_history') {
|
|
306
|
+
handleGetHistory(ws, extCode, extTaskId);
|
|
307
|
+
break;
|
|
308
|
+
}
|
|
309
|
+
if (extTask === 'generate_ideas') {
|
|
310
|
+
(async () => {
|
|
311
|
+
await handleGenerateIdeas(ws, extCode, extTaskId, msg.topic);
|
|
312
|
+
})();
|
|
313
|
+
break;
|
|
314
|
+
}
|
|
315
|
+
if (extTask === 'generate_script') {
|
|
316
|
+
(async () => {
|
|
317
|
+
await handleGenerateScript(ws, extCode, extTaskId, msg.idea, msg.topic, msg.style);
|
|
318
|
+
})();
|
|
319
|
+
break;
|
|
320
|
+
}
|
|
321
|
+
if (extTask === 'retry_video') {
|
|
322
|
+
const videoId = msg.videoId;
|
|
323
|
+
handleRetryVideo(ws, extCode, extTaskId, videoId);
|
|
324
|
+
break;
|
|
325
|
+
}
|
|
326
|
+
if (extTask === 'generate_scene') {
|
|
327
|
+
(async () => {
|
|
328
|
+
await handleGenerateScene(ws, extCode, extTaskId, msg.sceneIdx, msg.prompt, msg.aspectRatio || 'portrait');
|
|
329
|
+
})();
|
|
330
|
+
break;
|
|
331
|
+
}
|
|
332
|
+
if (extTask === 'generate_scenes') {
|
|
333
|
+
(async () => {
|
|
334
|
+
await handleGenerateScenes(ws, extCode, extTaskId, msg.scenes, msg.aspectRatio || 'portrait');
|
|
335
|
+
})();
|
|
336
|
+
break;
|
|
337
|
+
}
|
|
338
|
+
if (extTask === 'render_video') {
|
|
339
|
+
(async () => {
|
|
340
|
+
await handleRenderVideo(ws, extCode, extTaskId, {
|
|
341
|
+
scenes: msg.scenes,
|
|
342
|
+
script: msg.script,
|
|
343
|
+
title: msg.title,
|
|
344
|
+
aspectRatio: msg.aspectRatio || 'portrait',
|
|
345
|
+
voiceId: msg.voiceId || '',
|
|
346
|
+
subtitleStyle: msg.subtitleStyle || 'karaoke',
|
|
347
|
+
});
|
|
348
|
+
})();
|
|
349
|
+
break;
|
|
350
|
+
}
|
|
351
|
+
if (extTask === 'list_characters') {
|
|
352
|
+
handleListCharacters(ws, extCode, extTaskId);
|
|
353
|
+
break;
|
|
354
|
+
}
|
|
355
|
+
if (extTask === 'create_character') {
|
|
356
|
+
(async () => {
|
|
357
|
+
await handleCreateCharacter(ws, extCode, extTaskId, msg.name, msg.displayName, msg.prompt, msg.outputCount || 2, msg.orientation || 'portrait');
|
|
358
|
+
})();
|
|
359
|
+
break;
|
|
360
|
+
}
|
|
361
|
+
if (extTask === 'save_character_selection') {
|
|
362
|
+
handleSaveCharacterSelection(ws, extCode, extTaskId, msg.characterId, msg.selectedImages || []);
|
|
363
|
+
break;
|
|
364
|
+
}
|
|
365
|
+
// ── Claude-powered tasks (ideas, script, etc.) ────────────────────────
|
|
366
|
+
(async () => {
|
|
367
|
+
try {
|
|
368
|
+
const result = await runClaude({
|
|
369
|
+
prompt: extTask,
|
|
370
|
+
cwd: os.homedir(),
|
|
371
|
+
maxTurns: 10,
|
|
372
|
+
outputFormat: extStream ? 'stream-json' : 'json',
|
|
373
|
+
lightweight: true,
|
|
374
|
+
timeoutMs: 120000,
|
|
375
|
+
onStreamLine: extStream ? (data) => {
|
|
376
|
+
if (data.type === 'stream_event') {
|
|
377
|
+
const event = data.event;
|
|
378
|
+
if (event?.type === 'content_block_delta') {
|
|
379
|
+
const delta = event.delta;
|
|
380
|
+
if (delta?.type === 'text_delta' && delta.text) {
|
|
381
|
+
ws.sendExtensionStream(extCode, extTaskId, delta.text);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
} : undefined,
|
|
386
|
+
});
|
|
387
|
+
ws.sendExtensionDone(extCode, extTaskId, { script: result.result, text: result.result });
|
|
388
|
+
console.log(`[Daemon] Extension task ${extTaskId} done`);
|
|
389
|
+
}
|
|
390
|
+
catch (err) {
|
|
391
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
392
|
+
console.error(`[Daemon] Extension task ${extTaskId} error: ${errMsg}`);
|
|
393
|
+
ws.sendExtensionDone(extCode, extTaskId, { error: errMsg });
|
|
394
|
+
}
|
|
395
|
+
})();
|
|
396
|
+
break;
|
|
397
|
+
}
|
|
290
398
|
case 'pong':
|
|
291
399
|
break;
|
|
400
|
+
case 'flow_command_result': {
|
|
401
|
+
const fcId = msg.id;
|
|
402
|
+
const fcResult = msg.result;
|
|
403
|
+
if (fcId) {
|
|
404
|
+
ws.resolveFlowCommand(fcId, fcResult || {});
|
|
405
|
+
}
|
|
406
|
+
break;
|
|
407
|
+
}
|
|
408
|
+
case 'extension_unpaired': {
|
|
409
|
+
// Extension signed out — remove this code from local config so it's not restored on reconnect
|
|
410
|
+
const unpairCode = msg.code;
|
|
411
|
+
if (unpairCode) {
|
|
412
|
+
const cfg = loadConfig();
|
|
413
|
+
if (cfg?.extensionCodes) {
|
|
414
|
+
const newCodes = cfg.extensionCodes.filter(c => c !== unpairCode);
|
|
415
|
+
saveConfig({ ...cfg, extensionCodes: newCodes });
|
|
416
|
+
console.log(`[Daemon] Extension unpaired: ${unpairCode}`);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
break;
|
|
420
|
+
}
|
|
292
421
|
case 'error':
|
|
293
422
|
console.error(`[Daemon] Server error: ${msg.message}`);
|
|
294
423
|
break;
|
|
@@ -10,9 +10,8 @@ export declare class AgentWebSocketClient {
|
|
|
10
10
|
private running;
|
|
11
11
|
private onMessage;
|
|
12
12
|
private onAuthFailed?;
|
|
13
|
-
|
|
14
|
-
private
|
|
15
|
-
private _agentFolders;
|
|
13
|
+
/** Pending flow command callbacks keyed by request ID */
|
|
14
|
+
private _flowCallbacks;
|
|
16
15
|
constructor(config: AgentConfig, onMessage: MessageHandler, onAuthFailed?: (code: number, reason: string) => void);
|
|
17
16
|
connect(): void;
|
|
18
17
|
disconnect(): void;
|
|
@@ -94,14 +93,35 @@ export declare class AgentWebSocketClient {
|
|
|
94
93
|
context?: string;
|
|
95
94
|
startedAt?: string;
|
|
96
95
|
}): boolean;
|
|
96
|
+
/**
|
|
97
|
+
* Stream a text chunk back to the Chrome extension for an extension task.
|
|
98
|
+
*/
|
|
99
|
+
sendExtensionStream(code: string, taskId: string, text: string): boolean;
|
|
100
|
+
/**
|
|
101
|
+
* Signal that an extension task is complete.
|
|
102
|
+
*/
|
|
103
|
+
sendExtensionDone(code: string, taskId: string, result: Record<string, unknown>): boolean;
|
|
104
|
+
/**
|
|
105
|
+
* Send progress update for an extension task.
|
|
106
|
+
*/
|
|
107
|
+
sendExtensionProgress(code: string, taskId: string, progress: number): boolean;
|
|
108
|
+
/**
|
|
109
|
+
* Send a custom-typed event to the extension (generic forwarder via extension_event action).
|
|
110
|
+
*/
|
|
111
|
+
sendExtensionEvent(code: string, event: Record<string, unknown>): boolean;
|
|
112
|
+
/**
|
|
113
|
+
* Send a flow command to the extension and wait for the result.
|
|
114
|
+
* Returns a Promise that resolves with the command result.
|
|
115
|
+
*/
|
|
116
|
+
sendFlowCommand(code: string, command: Record<string, unknown>, timeout?: number): Promise<Record<string, unknown>>;
|
|
117
|
+
/**
|
|
118
|
+
* Resolve a pending flow command callback (called from message handler).
|
|
119
|
+
* Returns true if the callback was found and resolved.
|
|
120
|
+
*/
|
|
121
|
+
resolveFlowCommand(id: string, result: Record<string, unknown>): boolean;
|
|
97
122
|
get isConnected(): boolean;
|
|
98
123
|
private _connect;
|
|
99
124
|
private _startHeartbeat;
|
|
100
|
-
/** Set workspace path (called when scan_workspace command is received) */
|
|
101
|
-
setWorkspacePath(wsPath: string): void;
|
|
102
|
-
/** Set agent folder paths for skill scanning */
|
|
103
|
-
setAgentFolders(folders: string[]): void;
|
|
104
|
-
private _scanWorkspace;
|
|
105
125
|
private _stopHeartbeat;
|
|
106
126
|
private _scheduleReconnect;
|
|
107
127
|
}
|
package/dist/daemon/ws-client.js
CHANGED
|
@@ -1,9 +1,5 @@
|
|
|
1
|
-
import fs from 'fs';
|
|
2
|
-
import path from 'path';
|
|
3
|
-
import os from 'os';
|
|
4
1
|
import WebSocket from 'ws';
|
|
5
2
|
import { getSystemLoad, detectCapabilities } from '../system/info.js';
|
|
6
|
-
import { scanSkills } from '../utils/skill-scanner.js';
|
|
7
3
|
/** Close codes that indicate a permanent error — do NOT reconnect. */
|
|
8
4
|
const PERMANENT_CLOSE_CODES = new Set([
|
|
9
5
|
4001, // Authentication failed (invalid token)
|
|
@@ -19,9 +15,8 @@ export class AgentWebSocketClient {
|
|
|
19
15
|
running = false;
|
|
20
16
|
onMessage;
|
|
21
17
|
onAuthFailed;
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
_agentFolders = [];
|
|
18
|
+
/** Pending flow command callbacks keyed by request ID */
|
|
19
|
+
_flowCallbacks = new Map();
|
|
25
20
|
constructor(config, onMessage, onAuthFailed) {
|
|
26
21
|
this.config = config;
|
|
27
22
|
this.onMessage = onMessage;
|
|
@@ -159,6 +154,67 @@ export class AgentWebSocketClient {
|
|
|
159
154
|
timestamp: message.startedAt || new Date().toISOString(),
|
|
160
155
|
});
|
|
161
156
|
}
|
|
157
|
+
/**
|
|
158
|
+
* Stream a text chunk back to the Chrome extension for an extension task.
|
|
159
|
+
*/
|
|
160
|
+
sendExtensionStream(code, taskId, text) {
|
|
161
|
+
return this.send({ action: 'extension_task_stream', code, taskId, text });
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Signal that an extension task is complete.
|
|
165
|
+
*/
|
|
166
|
+
sendExtensionDone(code, taskId, result) {
|
|
167
|
+
return this.send({ action: 'extension_task_done', code, taskId, result });
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Send progress update for an extension task.
|
|
171
|
+
*/
|
|
172
|
+
sendExtensionProgress(code, taskId, progress) {
|
|
173
|
+
return this.send({ action: 'extension_task_progress', code, taskId, progress });
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Send a custom-typed event to the extension (generic forwarder via extension_event action).
|
|
177
|
+
*/
|
|
178
|
+
sendExtensionEvent(code, event) {
|
|
179
|
+
return this.send({ action: 'extension_event', code, event });
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Send a flow command to the extension and wait for the result.
|
|
183
|
+
* Returns a Promise that resolves with the command result.
|
|
184
|
+
*/
|
|
185
|
+
sendFlowCommand(code, command, timeout = 60000) {
|
|
186
|
+
return new Promise((resolve, reject) => {
|
|
187
|
+
const id = `fc-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
188
|
+
const timer = setTimeout(() => {
|
|
189
|
+
this._flowCallbacks.delete(id);
|
|
190
|
+
reject(new Error(`Flow command timed out after ${timeout}ms: ${command.type}`));
|
|
191
|
+
}, timeout);
|
|
192
|
+
this._flowCallbacks.set(id, { resolve, reject, timer });
|
|
193
|
+
const sent = this.sendExtensionEvent(code, {
|
|
194
|
+
type: 'flow_command',
|
|
195
|
+
id,
|
|
196
|
+
command,
|
|
197
|
+
});
|
|
198
|
+
if (!sent) {
|
|
199
|
+
clearTimeout(timer);
|
|
200
|
+
this._flowCallbacks.delete(id);
|
|
201
|
+
reject(new Error('WebSocket not connected — cannot send flow command'));
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Resolve a pending flow command callback (called from message handler).
|
|
207
|
+
* Returns true if the callback was found and resolved.
|
|
208
|
+
*/
|
|
209
|
+
resolveFlowCommand(id, result) {
|
|
210
|
+
const cb = this._flowCallbacks.get(id);
|
|
211
|
+
if (!cb)
|
|
212
|
+
return false;
|
|
213
|
+
clearTimeout(cb.timer);
|
|
214
|
+
this._flowCallbacks.delete(id);
|
|
215
|
+
cb.resolve(result);
|
|
216
|
+
return true;
|
|
217
|
+
}
|
|
162
218
|
get isConnected() {
|
|
163
219
|
return this.ws !== null && this.ws.readyState === WebSocket.OPEN;
|
|
164
220
|
}
|
|
@@ -177,6 +233,11 @@ export class AgentWebSocketClient {
|
|
|
177
233
|
this.reconnectAttempts = 0;
|
|
178
234
|
console.log('[WS] Connected to API server');
|
|
179
235
|
this._startHeartbeat();
|
|
236
|
+
// Restore extension pairings so server can send agent_connected to already-connected extensions
|
|
237
|
+
const codes = this.config.extensionCodes;
|
|
238
|
+
if (codes && codes.length > 0) {
|
|
239
|
+
this.send({ action: 'restore_extensions', codes });
|
|
240
|
+
}
|
|
180
241
|
});
|
|
181
242
|
this.ws.on('message', (raw) => {
|
|
182
243
|
try {
|
|
@@ -210,70 +271,15 @@ export class AgentWebSocketClient {
|
|
|
210
271
|
}
|
|
211
272
|
_startHeartbeat() {
|
|
212
273
|
this._stopHeartbeat();
|
|
213
|
-
this.heartbeatCount = 0;
|
|
214
|
-
// Scan workspace once on connect (delayed 3s to let connection stabilize)
|
|
215
|
-
setTimeout(() => this._scanWorkspace(), 3000);
|
|
216
274
|
this.heartbeatTimer = setInterval(() => {
|
|
217
|
-
this.heartbeatCount++;
|
|
218
275
|
const agentType = this.config.agentType || 'claude-code';
|
|
219
276
|
this.send({
|
|
220
277
|
action: 'heartbeat',
|
|
221
278
|
system_load: getSystemLoad(),
|
|
222
279
|
capabilities: detectCapabilities(agentType),
|
|
223
280
|
});
|
|
224
|
-
// Scan workspace every 20th heartbeat (~10 min)
|
|
225
|
-
if (this.heartbeatCount % 20 === 0) {
|
|
226
|
-
this._scanWorkspace();
|
|
227
|
-
}
|
|
228
281
|
}, 30000);
|
|
229
282
|
}
|
|
230
|
-
/** Set workspace path (called when scan_workspace command is received) */
|
|
231
|
-
setWorkspacePath(wsPath) {
|
|
232
|
-
this._workspacePath = wsPath;
|
|
233
|
-
}
|
|
234
|
-
/** Set agent folder paths for skill scanning */
|
|
235
|
-
setAgentFolders(folders) {
|
|
236
|
-
this._agentFolders = folders;
|
|
237
|
-
}
|
|
238
|
-
_scanWorkspace() {
|
|
239
|
-
const wsPath = this._workspacePath || path.join(os.homedir(), 'tuna-workspace');
|
|
240
|
-
try {
|
|
241
|
-
if (!fs.existsSync(wsPath))
|
|
242
|
-
return;
|
|
243
|
-
const entries = fs.readdirSync(wsPath, { withFileTypes: true });
|
|
244
|
-
const projects = entries
|
|
245
|
-
.filter(e => e.isDirectory() && !e.name.startsWith('.'))
|
|
246
|
-
.map(e => ({ name: e.name, path: path.join(wsPath, e.name) }));
|
|
247
|
-
this.send({ action: 'workspace_scanned', projects });
|
|
248
|
-
}
|
|
249
|
-
catch {
|
|
250
|
-
// Silent fail — non-critical
|
|
251
|
-
}
|
|
252
|
-
// Scan skills from .claude/commands/ directories
|
|
253
|
-
// Merge API-provided folders with local config scan_paths
|
|
254
|
-
try {
|
|
255
|
-
let folders = [...this._agentFolders];
|
|
256
|
-
if (folders.length === 0) {
|
|
257
|
-
// Fallback: read scan_paths from local config
|
|
258
|
-
const configPath = path.join(os.homedir(), '.tuna-agent', 'config.json');
|
|
259
|
-
try {
|
|
260
|
-
const cfg = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
261
|
-
if (Array.isArray(cfg.scan_paths)) {
|
|
262
|
-
folders = cfg.scan_paths;
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
catch { /* no config or parse error */ }
|
|
266
|
-
}
|
|
267
|
-
const skills = scanSkills(wsPath, folders);
|
|
268
|
-
if (skills.length > 0) {
|
|
269
|
-
this.send({ action: 'skills_scanned', skills });
|
|
270
|
-
console.log(`[WS] Scanned ${skills.length} skill(s) from ${folders.length} folder(s)`);
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
catch {
|
|
274
|
-
// Silent fail — non-critical
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
283
|
_stopHeartbeat() {
|
|
278
284
|
if (this.heartbeatTimer) {
|
|
279
285
|
clearInterval(this.heartbeatTimer);
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Browser MCP Server for Tuna Agent
|
|
4
|
+
*
|
|
5
|
+
* Stdio-based MCP server that exposes browser automation tools to Claude Code.
|
|
6
|
+
* Uses browserclaw (vendored from OpenClaw) for snapshot+ref browser control.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* node browser-server.js [--headless] [--user-data-dir /path/to/chrome/profile] [--cdp-port 9222]
|
|
10
|
+
*/
|
|
11
|
+
export {};
|