tuna-agent 0.1.0 → 0.1.1

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.
@@ -1,9 +1,11 @@
1
1
  import type { AgentAdapter, AgentType } from './types.js';
2
- import type { TaskAssignment, InputResponse } from '../types/index.js';
2
+ import type { AgentConfig, TaskAssignment, InputResponse } from '../types/index.js';
3
3
  import type { AgentWebSocketClient } from '../daemon/ws-client.js';
4
4
  export declare class ClaudeCodeAdapter implements AgentAdapter {
5
5
  readonly type: AgentType;
6
6
  readonly displayName = "Claude Code";
7
+ private readonly agentConfig;
8
+ constructor(config: AgentConfig);
7
9
  checkHealth(): Promise<{
8
10
  ok: boolean;
9
11
  message: string;
@@ -7,9 +7,14 @@ import { planTask, chatWithPM } from '../pm/planner.js';
7
7
  import { savePMState, clearPMState } from '../daemon/pm-state.js';
8
8
  import { simplifyMarkdown, waitForInput, sessionToPayload, executePlanAndReport, } from '../utils/execution-helpers.js';
9
9
  import { downloadAttachments, cleanupAttachments } from '../utils/image-download.js';
10
+ import { writeAgentFolderMcpConfig } from '../mcp/setup.js';
10
11
  export class ClaudeCodeAdapter {
11
12
  type = 'claude-code';
12
13
  displayName = 'Claude Code';
14
+ agentConfig;
15
+ constructor(config) {
16
+ this.agentConfig = config;
17
+ }
13
18
  async checkHealth() {
14
19
  try {
15
20
  execSync('which claude', { stdio: 'ignore' });
@@ -72,17 +77,24 @@ export class ClaudeCodeAdapter {
72
77
  const cwd = task.repoPath || defaultWorkspace;
73
78
  if (!fs.existsSync(cwd))
74
79
  fs.mkdirSync(cwd, { recursive: true });
80
+ // Write MCP config to agent folder .claude/settings.json on first round
81
+ // Claude Code reads settings.json automatically (--mcp-config flag is unreliable)
82
+ if (round === 0) {
83
+ writeAgentFolderMcpConfig(cwd, this.agentConfig);
84
+ }
85
+ // Skills/scheduled tasks use MCP tools (knowledge sync) → disable agentTeam
86
+ // Manual chat tasks may spawn sub-agents → keep agentTeam enabled
87
+ const useAgentTeam = task.source !== 'skill' && task.source !== 'scheduled';
75
88
  const result = await runClaude({
76
89
  prompt: userMessage,
77
90
  cwd,
78
- allowedTools: ['Read', 'Edit', 'Write', 'Bash', 'Glob', 'Grep'],
79
91
  outputFormat: 'stream-json',
80
92
  includePartialMessages: true,
81
- agentTeam: true,
82
93
  maxTurns: 50,
83
94
  resumeSessionId: round > 0 ? sessionId : undefined,
84
95
  signal,
85
96
  inputFiles: currentInputFiles,
97
+ agentTeam: useAgentTeam,
86
98
  ...(confirmBeforeEdit && onPermissionRequest ? {
87
99
  permissionMode: 'default',
88
100
  onPermissionRequest: (tool, detail) => onPermissionRequest('agent-team', tool, detail),
@@ -123,10 +135,22 @@ export class ClaudeCodeAdapter {
123
135
  return;
124
136
  }
125
137
  // Log non-stream events for debugging
126
- console.log(`[ClaudeCode] stream line: type=${data.type}${data.type === 'assistant' ? ' (assistant msg)' : ''}`);
127
- // Track session ID
138
+ if (data.type === 'system') {
139
+ console.log(`[ClaudeCode] system: ${JSON.stringify(data).slice(0, 300)}`);
140
+ }
141
+ else {
142
+ console.log(`[ClaudeCode] stream line: type=${data.type}${data.type === 'assistant' ? ' (assistant msg)' : ''}`);
143
+ }
144
+ // Track session ID + log MCP tools from init
128
145
  if (data.type === 'system' && data.subtype === 'init') {
129
146
  sessionId = data.session_id;
147
+ const initData = data;
148
+ // tools can be string[] or object[] depending on Claude Code version
149
+ const rawTools = initData.tools;
150
+ const toolNames = rawTools?.map(t => typeof t === 'string' ? t : String(t?.name ?? '')) ?? [];
151
+ const mcpTools = toolNames.filter(n => n.startsWith('mcp__'));
152
+ console.log(`[ClaudeCode] Init tools: total=${toolNames.length}, mcp=${mcpTools.length} (${mcpTools.join(', ') || 'none'})`);
153
+ console.log(`[ClaudeCode] All tools: ${toolNames.join(', ')}`);
130
154
  }
131
155
  // Send tool usage to activity tab
132
156
  if (data.type === 'assistant' && data.message) {
@@ -1,2 +1,3 @@
1
1
  import type { AgentAdapter, AgentConfig } from './types.js';
2
- export declare function createAgentAdapter(config: AgentConfig): AgentAdapter;
2
+ import type { AgentConfig as DaemonAgentConfig } from '../types/index.js';
3
+ export declare function createAgentAdapter(config: AgentConfig, daemonConfig?: DaemonAgentConfig): AgentAdapter;
@@ -1,9 +1,9 @@
1
1
  import { ClaudeCodeAdapter } from './claude-code-adapter.js';
2
2
  import { OpenClawAdapter } from './openclaw-adapter.js';
3
- export function createAgentAdapter(config) {
3
+ export function createAgentAdapter(config, daemonConfig) {
4
4
  switch (config.type) {
5
5
  case 'claude-code':
6
- return new ClaudeCodeAdapter();
6
+ return new ClaudeCodeAdapter(daemonConfig);
7
7
  case 'openclaw':
8
8
  return new OpenClawAdapter(config);
9
9
  default:
@@ -0,0 +1,10 @@
1
+ /**
2
+ * List all Chrome extensions paired with this machine.
3
+ * Usage: tuna-agent extension list
4
+ */
5
+ export declare function extensionList(): Promise<void>;
6
+ /**
7
+ * Pair this machine with a Chrome extension using its pairing code.
8
+ * Usage: tuna-agent extension connect TUNA-XXXX-XXXX
9
+ */
10
+ export declare function extensionConnect(code: string): Promise<void>;
@@ -0,0 +1,86 @@
1
+ import chalk from 'chalk';
2
+ import { loadConfig, saveConfig } from '../../config/store.js';
3
+ /**
4
+ * List all Chrome extensions paired with this machine.
5
+ * Usage: tuna-agent extension list
6
+ */
7
+ export async function extensionList() {
8
+ const config = loadConfig();
9
+ if (!config) {
10
+ console.error(chalk.red('Not connected. Run tuna-agent connect <CODE> first.'));
11
+ process.exit(1);
12
+ }
13
+ try {
14
+ const res = await fetch(`${config.apiUrl}/extension/list`, {
15
+ headers: { 'Authorization': `Bearer ${config.agentToken}` },
16
+ });
17
+ const body = await res.json();
18
+ if (!res.ok || !body.data) {
19
+ console.error(chalk.red(`Failed: ${body.message || 'Unknown error'}`));
20
+ process.exit(1);
21
+ }
22
+ const { extensions, total } = body.data;
23
+ console.log(chalk.cyan(`\nPaired Extensions (${total})\n`));
24
+ if (total === 0) {
25
+ console.log(chalk.gray(' No extensions paired yet.'));
26
+ console.log(`\n Run ${chalk.bold('tuna-agent extension connect <CODE>')} to pair one.`);
27
+ return;
28
+ }
29
+ for (const ext of extensions) {
30
+ const status = ext.connected ? chalk.green('● connected') : chalk.gray('○ offline');
31
+ console.log(` ${status} ${chalk.bold(ext.code)}`);
32
+ }
33
+ console.log('');
34
+ }
35
+ catch (err) {
36
+ const msg = err instanceof Error ? err.message : String(err);
37
+ console.error(chalk.red(`\nFailed: ${msg}`));
38
+ process.exit(1);
39
+ }
40
+ }
41
+ /**
42
+ * Pair this machine with a Chrome extension using its pairing code.
43
+ * Usage: tuna-agent extension connect TUNA-XXXX-XXXX
44
+ */
45
+ export async function extensionConnect(code) {
46
+ const config = loadConfig();
47
+ if (!config) {
48
+ console.error(chalk.red('Not connected. Run tuna-agent connect <CODE> first.'));
49
+ process.exit(1);
50
+ }
51
+ const normalizedCode = code.trim().toUpperCase();
52
+ if (!normalizedCode.startsWith('TUNA-')) {
53
+ console.error(chalk.red('Invalid pair code. Expected format: TUNA-XXXX-XXXX'));
54
+ process.exit(1);
55
+ }
56
+ console.log(chalk.cyan('Pairing with Chrome extension...'));
57
+ console.log(` Code: ${chalk.bold(normalizedCode)}`);
58
+ try {
59
+ const res = await fetch(`${config.apiUrl}/extension/pair`, {
60
+ method: 'POST',
61
+ headers: {
62
+ 'Content-Type': 'application/json',
63
+ 'Authorization': `Bearer ${config.agentToken}`,
64
+ },
65
+ body: JSON.stringify({ code: normalizedCode }),
66
+ });
67
+ const body = await res.json();
68
+ if (!res.ok || !body.data) {
69
+ console.error(chalk.red(`\nFailed: ${body.message || 'Unknown error'}`));
70
+ process.exit(1);
71
+ }
72
+ console.log(chalk.green('\nExtension paired successfully!'));
73
+ console.log(` Machine: ${body.data.machineName}`);
74
+ console.log('\nThe Content Creator extension should now show your agent as connected.');
75
+ // Save code locally so agent can restore the pairing after server restart
76
+ const codes = new Set(config.extensionCodes || []);
77
+ codes.add(normalizedCode);
78
+ saveConfig({ ...config, extensionCodes: [...codes] });
79
+ }
80
+ catch (err) {
81
+ const msg = err instanceof Error ? err.message : String(err);
82
+ console.error(chalk.red(`\nPairing failed: ${msg}`));
83
+ console.error(`Make sure tuna-agent daemon is configured (run tuna-agent connect first)`);
84
+ process.exit(1);
85
+ }
86
+ }
package/dist/cli/index.js CHANGED
@@ -4,6 +4,7 @@ import { connect } from './commands/connect.js';
4
4
  import { start } from './commands/start.js';
5
5
  import { stop } from './commands/stop.js';
6
6
  import { status } from './commands/status.js';
7
+ import { extensionConnect, extensionList } from './commands/extension.js';
7
8
  const program = new Command()
8
9
  .name('tuna-agent')
9
10
  .description('Tuna Agent - Run AI coding tasks on your machine')
@@ -29,4 +30,15 @@ program
29
30
  .command('status')
30
31
  .description('Show agent connection status')
31
32
  .action(status);
33
+ const extensionCmd = program
34
+ .command('extension')
35
+ .description('Manage Chrome extension pairing');
36
+ extensionCmd
37
+ .command('connect <code>')
38
+ .description('Pair this agent with the Content Creator Chrome extension')
39
+ .action(extensionConnect);
40
+ extensionCmd
41
+ .command('list')
42
+ .description('List all Chrome extensions paired with this machine')
43
+ .action(extensionList);
32
44
  program.parse();
@@ -2,8 +2,7 @@ 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';
@@ -11,6 +10,8 @@ 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';
13
12
  import { downloadAttachments, cleanupAttachments } from '../utils/image-download.js';
13
+ import { scanSkills } from '../utils/skill-scanner.js';
14
+ import { setupMcpConfig } from '../mcp/setup.js';
14
15
  /**
15
16
  * Start the agent daemon.
16
17
  * Connects to API via WebSocket, receives tasks, delegates to agent adapter.
@@ -21,7 +22,14 @@ export async function startDaemon(config) {
21
22
  type: config.agentType || 'claude-code',
22
23
  ...config.agentConfig,
23
24
  };
24
- const adapter = createAgentAdapter(agentConfig);
25
+ const adapter = createAgentAdapter(agentConfig, config);
26
+ // Setup MCP config for Knowledge server
27
+ try {
28
+ setupMcpConfig(config);
29
+ }
30
+ catch (err) {
31
+ console.warn(`[Daemon] MCP config setup failed (non-fatal):`, err);
32
+ }
25
33
  // Health check on startup
26
34
  const health = await adapter.checkHealth();
27
35
  if (!health.ok) {
@@ -58,8 +66,8 @@ export async function startDaemon(config) {
58
66
  switch (type) {
59
67
  case 'connected':
60
68
  console.log(`[Daemon] Connected as "${msg.name}" (${msg.agentId})`);
61
- // Recover any tasks orphaned by previous daemon crash/restart
62
- ws.send({ action: 'recover_orphaned_tasks' });
69
+ // Recover orphaned tasks pass activeTaskId so API won't fail a task we're still running
70
+ ws.send({ action: 'recover_orphaned_tasks', activeTaskId: currentTaskId ?? undefined });
63
71
  break;
64
72
  case 'task_assigned': {
65
73
  const task = msg.task;
@@ -76,6 +84,15 @@ export async function startDaemon(config) {
76
84
  currentTaskId = task.id;
77
85
  currentTaskAbort = new AbortController();
78
86
  console.log(`[Daemon] Received task: ${task.id} — ${task.description.slice(0, 80)} (attachments: ${task.attachments?.length ?? 0})`);
87
+ // Update MCP config with the task's agent ID so knowledge server uses correct attribution
88
+ if (task.agentId) {
89
+ try {
90
+ setupMcpConfig({ ...config, agentId: task.agentId });
91
+ }
92
+ catch (err) {
93
+ console.warn(`[Daemon] MCP config update for task failed (non-fatal):`, err);
94
+ }
95
+ }
79
96
  try {
80
97
  await adapter.handleTask(task, ws, pendingInputResolvers, currentTaskAbort.signal, pendingPermissionResolvers);
81
98
  }
@@ -117,42 +134,21 @@ export async function startDaemon(config) {
117
134
  }
118
135
  case 'command': {
119
136
  const command = msg.command;
120
- if (command === 'scan_workspace') {
121
- const rawPath = msg.workspace_path || '~/tuna-workspace';
122
- const wsPath = rawPath.startsWith('~')
123
- ? path.join(os.homedir(), rawPath.slice(1))
124
- : rawPath;
125
- try {
126
- validatePath(wsPath, os.homedir());
127
- }
128
- catch (err) {
129
- const errMsg = err instanceof Error ? err.message : String(err);
130
- console.error(`[Daemon] Workspace path validation failed: ${errMsg}`);
131
- ws.send({ action: 'workspace_scanned', projects: [] });
132
- break;
133
- }
134
- // Resolve agent folder paths (sent by API)
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
- }
137
+ if (command === 'rescan_agent_skills') {
138
+ const agentId = msg.agent_id;
139
+ const rawFolder = msg.folder_path || '';
140
+ const rawWsPath = msg.workspace_path || '~/tuna-workspace';
141
+ const wsPath = rawWsPath.startsWith('~')
142
+ ? path.join(os.homedir(), rawWsPath.slice(1))
143
+ : rawWsPath;
144
+ const folder = rawFolder.startsWith('~')
145
+ ? path.join(os.homedir(), rawFolder.slice(1))
146
+ : rawFolder;
147
+ console.log(`[Daemon] Rescan skills for agent ${agentId}, folder: ${folder || '(none)'}`);
148
+ const folders = folder ? [folder] : [];
149
+ const skills = scanSkills(wsPath, folders);
150
+ ws.send({ action: 'agent_skills_scanned', agent_id: agentId, skills });
151
+ console.log(`[Daemon] Scanned ${skills.length} skill(s) for agent ${agentId}`);
156
152
  }
157
153
  else if (command === 'analyze_skill') {
158
154
  const skillId = msg.skill_id;
@@ -180,12 +176,15 @@ export async function startDaemon(config) {
180
176
  const analyzePrompt = `You are analyzing a Claude Code skill file (markdown prompt template). Extract the following structured information from the content below:
181
177
 
182
178
  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 a "name" (short, lowercase, no spaces — use hyphens) and "description" (what it does, max 100 chars).
184
- 3. **parameters**: Input parameters the skill accepts. 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".
179
+ 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:
180
+ - "name" (short, lowercase, no spaces use hyphens)
181
+ - "description" (what it does, max 100 chars)
182
+ - "params" (array of parameter KEY strings that are relevant to THIS specific action — only include params that this action actually uses)
183
+ 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
184
  - 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
185
 
187
186
  Respond with ONLY valid JSON, no markdown, no explanation:
188
- {"description":"...","actions":[{"name":"...","description":"..."}],"parameters":[{"key":"...","label":"...","type":"select","required":false,"options":["opt1","opt2"]}]}
187
+ {"description":"...","actions":[{"name":"...","description":"...","params":["param_key1","param_key2"]}],"parameters":[{"key":"...","label":"...","type":"select","required":false,"options":["opt1","opt2"]}]}
189
188
 
190
189
  If no actions are found, return an empty array. If no parameters found, return an empty array.
191
190
 
@@ -287,8 +286,79 @@ ${skillContent.slice(0, 15000)}`;
287
286
  }
288
287
  break;
289
288
  }
289
+ case 'extension_task': {
290
+ // Task sent from Chrome extension — run Claude and stream results back
291
+ const extCode = msg.code;
292
+ const extTaskId = msg.taskId;
293
+ const extTask = msg.task;
294
+ const extStream = msg.stream;
295
+ if (!extCode || !extTaskId || !extTask) {
296
+ console.warn('[Daemon] extension_task missing required fields');
297
+ break;
298
+ }
299
+ console.log(`[Daemon] Extension task ${extTaskId}: ${extTask.substring(0, 80)}`);
300
+ // Run in background — don't block message handling
301
+ (async () => {
302
+ try {
303
+ let accumulated = '';
304
+ let progress = 0;
305
+ const result = await runClaude({
306
+ prompt: extTask,
307
+ cwd: os.homedir(),
308
+ maxTurns: 10,
309
+ outputFormat: extStream ? 'stream-json' : 'json',
310
+ lightweight: true,
311
+ timeoutMs: 120000,
312
+ onStreamLine: extStream ? (data) => {
313
+ if (data.type === 'stream_event') {
314
+ const event = data.event;
315
+ if (event?.type === 'content_block_delta') {
316
+ const delta = event.delta;
317
+ if (delta?.type === 'text_delta' && delta.text) {
318
+ const chunk = delta.text;
319
+ accumulated += chunk;
320
+ ws.send({ action: 'extension_task_stream', code: extCode, taskId: extTaskId, text: chunk });
321
+ }
322
+ }
323
+ }
324
+ } : undefined,
325
+ });
326
+ ws.send({
327
+ action: 'extension_task_done',
328
+ code: extCode,
329
+ taskId: extTaskId,
330
+ result: { script: result.result, text: result.result },
331
+ });
332
+ console.log(`[Daemon] Extension task ${extTaskId} done`);
333
+ }
334
+ catch (err) {
335
+ const errMsg = err instanceof Error ? err.message : String(err);
336
+ console.error(`[Daemon] Extension task ${extTaskId} error: ${errMsg}`);
337
+ ws.send({
338
+ action: 'extension_task_done',
339
+ code: extCode,
340
+ taskId: extTaskId,
341
+ result: { error: errMsg },
342
+ });
343
+ }
344
+ })();
345
+ break;
346
+ }
290
347
  case 'pong':
291
348
  break;
349
+ case 'extension_unpaired': {
350
+ // Extension signed out — remove this code from local config so it's not restored on reconnect
351
+ const unpairCode = msg.code;
352
+ if (unpairCode) {
353
+ const cfg = loadConfig();
354
+ if (cfg?.extensionCodes) {
355
+ const newCodes = cfg.extensionCodes.filter(c => c !== unpairCode);
356
+ saveConfig({ ...cfg, extensionCodes: newCodes });
357
+ console.log(`[Daemon] Extension unpaired: ${unpairCode}`);
358
+ }
359
+ }
360
+ break;
361
+ }
292
362
  case 'error':
293
363
  console.error(`[Daemon] Server error: ${msg.message}`);
294
364
  break;
@@ -10,9 +10,6 @@ export declare class AgentWebSocketClient {
10
10
  private running;
11
11
  private onMessage;
12
12
  private onAuthFailed?;
13
- private heartbeatCount;
14
- private _workspacePath;
15
- private _agentFolders;
16
13
  constructor(config: AgentConfig, onMessage: MessageHandler, onAuthFailed?: (code: number, reason: string) => void);
17
14
  connect(): void;
18
15
  disconnect(): void;
@@ -94,14 +91,21 @@ export declare class AgentWebSocketClient {
94
91
  context?: string;
95
92
  startedAt?: string;
96
93
  }): boolean;
94
+ /**
95
+ * Stream a text chunk back to the Chrome extension for an extension task.
96
+ */
97
+ sendExtensionStream(code: string, taskId: string, text: string): boolean;
98
+ /**
99
+ * Signal that an extension task is complete.
100
+ */
101
+ sendExtensionDone(code: string, taskId: string, result: Record<string, unknown>): boolean;
102
+ /**
103
+ * Send progress update for an extension task.
104
+ */
105
+ sendExtensionProgress(code: string, taskId: string, progress: number): boolean;
97
106
  get isConnected(): boolean;
98
107
  private _connect;
99
108
  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
109
  private _stopHeartbeat;
106
110
  private _scheduleReconnect;
107
111
  }
@@ -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,6 @@ export class AgentWebSocketClient {
19
15
  running = false;
20
16
  onMessage;
21
17
  onAuthFailed;
22
- heartbeatCount = 0;
23
- _workspacePath = null;
24
- _agentFolders = [];
25
18
  constructor(config, onMessage, onAuthFailed) {
26
19
  this.config = config;
27
20
  this.onMessage = onMessage;
@@ -159,6 +152,24 @@ export class AgentWebSocketClient {
159
152
  timestamp: message.startedAt || new Date().toISOString(),
160
153
  });
161
154
  }
155
+ /**
156
+ * Stream a text chunk back to the Chrome extension for an extension task.
157
+ */
158
+ sendExtensionStream(code, taskId, text) {
159
+ return this.send({ action: 'extension_task_stream', code, taskId, text });
160
+ }
161
+ /**
162
+ * Signal that an extension task is complete.
163
+ */
164
+ sendExtensionDone(code, taskId, result) {
165
+ return this.send({ action: 'extension_task_done', code, taskId, result });
166
+ }
167
+ /**
168
+ * Send progress update for an extension task.
169
+ */
170
+ sendExtensionProgress(code, taskId, progress) {
171
+ return this.send({ action: 'extension_task_progress', code, taskId, progress });
172
+ }
162
173
  get isConnected() {
163
174
  return this.ws !== null && this.ws.readyState === WebSocket.OPEN;
164
175
  }
@@ -177,6 +188,11 @@ export class AgentWebSocketClient {
177
188
  this.reconnectAttempts = 0;
178
189
  console.log('[WS] Connected to API server');
179
190
  this._startHeartbeat();
191
+ // Restore extension pairings so server can send agent_connected to already-connected extensions
192
+ const codes = this.config.extensionCodes;
193
+ if (codes && codes.length > 0) {
194
+ this.send({ action: 'restore_extensions', codes });
195
+ }
180
196
  });
181
197
  this.ws.on('message', (raw) => {
182
198
  try {
@@ -210,70 +226,15 @@ export class AgentWebSocketClient {
210
226
  }
211
227
  _startHeartbeat() {
212
228
  this._stopHeartbeat();
213
- this.heartbeatCount = 0;
214
- // Scan workspace once on connect (delayed 3s to let connection stabilize)
215
- setTimeout(() => this._scanWorkspace(), 3000);
216
229
  this.heartbeatTimer = setInterval(() => {
217
- this.heartbeatCount++;
218
230
  const agentType = this.config.agentType || 'claude-code';
219
231
  this.send({
220
232
  action: 'heartbeat',
221
233
  system_load: getSystemLoad(),
222
234
  capabilities: detectCapabilities(agentType),
223
235
  });
224
- // Scan workspace every 20th heartbeat (~10 min)
225
- if (this.heartbeatCount % 20 === 0) {
226
- this._scanWorkspace();
227
- }
228
236
  }, 30000);
229
237
  }
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
238
  _stopHeartbeat() {
278
239
  if (this.heartbeatTimer) {
279
240
  clearInterval(this.heartbeatTimer);
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Knowledge MCP Server for Tuna Agent
4
+ *
5
+ * Stdio-based MCP server that exposes knowledge tools to Claude Code.
6
+ * Communicates with the Tuna API using agent token auth.
7
+ *
8
+ * Usage:
9
+ * node knowledge-server.js --api-url https://api.tuna.ai --token xxx --agent-id abc123
10
+ */
11
+ export {};
@@ -0,0 +1,238 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Knowledge MCP Server for Tuna Agent
4
+ *
5
+ * Stdio-based MCP server that exposes knowledge tools to Claude Code.
6
+ * Communicates with the Tuna API using agent token auth.
7
+ *
8
+ * Usage:
9
+ * node knowledge-server.js --api-url https://api.tuna.ai --token xxx --agent-id abc123
10
+ */
11
+ function parseArgs() {
12
+ const args = process.argv.slice(2);
13
+ let apiUrl = '';
14
+ let token = '';
15
+ let agentId = '';
16
+ for (let i = 0; i < args.length; i++) {
17
+ if (args[i] === '--api-url' && args[i + 1])
18
+ apiUrl = args[++i];
19
+ else if (args[i] === '--token' && args[i + 1])
20
+ token = args[++i];
21
+ else if (args[i] === '--agent-id' && args[i + 1])
22
+ agentId = args[++i];
23
+ }
24
+ if (!apiUrl || !token || !agentId) {
25
+ process.stderr.write('Usage: knowledge-server --api-url URL --token TOKEN --agent-id ID\n');
26
+ process.exit(1);
27
+ }
28
+ return { apiUrl, token, agentId };
29
+ }
30
+ // ===== API Client =====
31
+ async function apiCall(config, method, path, body) {
32
+ const url = `${config.apiUrl}${path}`;
33
+ const headers = {
34
+ 'Authorization': `Bearer ${config.token}`,
35
+ 'Content-Type': 'application/json',
36
+ };
37
+ const options = { method, headers };
38
+ if (body)
39
+ options.body = JSON.stringify(body);
40
+ const res = await fetch(url, options);
41
+ const json = await res.json();
42
+ if (!res.ok || (json.code && json.code >= 400)) {
43
+ throw new Error(json.message || `API error: ${res.status}`);
44
+ }
45
+ return json.data;
46
+ }
47
+ function sendResponse(res) {
48
+ // Claude Code uses newline-delimited JSON (not Content-Length framing)
49
+ process.stdout.write(JSON.stringify(res) + '\n');
50
+ }
51
+ function sendResult(id, result) {
52
+ sendResponse({ jsonrpc: '2.0', id: id ?? null, result });
53
+ }
54
+ function sendError(id, code, message) {
55
+ sendResponse({ jsonrpc: '2.0', id: id ?? null, error: { code, message } });
56
+ }
57
+ // ===== Tool Definitions =====
58
+ const TOOLS = [
59
+ {
60
+ name: 'list_knowledge',
61
+ description: 'List all knowledge documents accessible to this agent. Returns document names, descriptions, and IDs.',
62
+ inputSchema: {
63
+ type: 'object',
64
+ properties: {},
65
+ },
66
+ },
67
+ {
68
+ name: 'read_knowledge',
69
+ description: 'Read the full content of a knowledge document by its ID. Returns the markdown content.',
70
+ inputSchema: {
71
+ type: 'object',
72
+ properties: {
73
+ knowledge_id: { type: 'string', description: 'The ID of the knowledge document to read' },
74
+ },
75
+ required: ['knowledge_id'],
76
+ },
77
+ },
78
+ {
79
+ name: 'create_knowledge',
80
+ description: 'Create a new knowledge document. Content should be in markdown format.',
81
+ inputSchema: {
82
+ type: 'object',
83
+ properties: {
84
+ name: { type: 'string', description: 'Name/title of the knowledge document' },
85
+ content: { type: 'string', description: 'Markdown content of the document' },
86
+ description: { type: 'string', description: 'Short description of what this document contains' },
87
+ },
88
+ required: ['name', 'content'],
89
+ },
90
+ },
91
+ {
92
+ name: 'update_knowledge',
93
+ description: 'Update an existing knowledge document. Can update name, description, and/or content.',
94
+ inputSchema: {
95
+ type: 'object',
96
+ properties: {
97
+ knowledge_id: { type: 'string', description: 'The ID of the knowledge document to update' },
98
+ name: { type: 'string', description: 'New name (optional)' },
99
+ content: { type: 'string', description: 'New markdown content (optional)' },
100
+ description: { type: 'string', description: 'New description (optional)' },
101
+ },
102
+ required: ['knowledge_id'],
103
+ },
104
+ },
105
+ ];
106
+ // ===== Request Handler =====
107
+ async function handleRequest(config, req) {
108
+ try {
109
+ switch (req.method) {
110
+ case 'initialize':
111
+ sendResult(req.id, {
112
+ protocolVersion: '2024-11-05',
113
+ capabilities: { tools: {} },
114
+ serverInfo: { name: 'tuna-knowledge', version: '1.0.0' },
115
+ });
116
+ break;
117
+ case 'notifications/initialized':
118
+ // No response needed for notifications
119
+ break;
120
+ case 'tools/list':
121
+ sendResult(req.id, { tools: TOOLS });
122
+ break;
123
+ case 'tools/call': {
124
+ const toolName = req.params?.name ?? '';
125
+ const args = req.params?.arguments ?? {};
126
+ const result = await handleToolCall(config, toolName, args);
127
+ sendResult(req.id, result);
128
+ break;
129
+ }
130
+ case 'ping':
131
+ sendResult(req.id, {});
132
+ break;
133
+ default:
134
+ if (req.id !== undefined) {
135
+ sendError(req.id, -32601, `Method not found: ${req.method}`);
136
+ }
137
+ }
138
+ }
139
+ catch (err) {
140
+ const message = err instanceof Error ? err.message : String(err);
141
+ if (req.id !== undefined) {
142
+ sendError(req.id, -32603, message);
143
+ }
144
+ }
145
+ }
146
+ async function handleToolCall(config, toolName, args) {
147
+ try {
148
+ switch (toolName) {
149
+ case 'list_knowledge': {
150
+ const data = await apiCall(config, 'GET', `/agent-knowledge?agent_id=${config.agentId}`);
151
+ const items = data.items || [];
152
+ if (items.length === 0) {
153
+ return { content: [{ type: 'text', text: 'No knowledge documents found for this agent.' }] };
154
+ }
155
+ const listing = items.map((k) => `- **${k.name}** (ID: ${k._id})\n ${k.description || 'No description'}\n Size: ${k.file_size} bytes | Updated: ${k.updated_at}`).join('\n\n');
156
+ return { content: [{ type: 'text', text: `Found ${items.length} knowledge document(s):\n\n${listing}` }] };
157
+ }
158
+ case 'read_knowledge': {
159
+ if (!args.knowledge_id) {
160
+ return { content: [{ type: 'text', text: 'Error: knowledge_id is required' }], isError: true };
161
+ }
162
+ const data = await apiCall(config, 'GET', `/agent-knowledge/${args.knowledge_id}`);
163
+ return {
164
+ content: [{
165
+ type: 'text',
166
+ text: `# ${data.name}\n\n${data.description ? `> ${data.description}\n\n` : ''}${data.content}`,
167
+ }],
168
+ };
169
+ }
170
+ case 'create_knowledge': {
171
+ if (!args.name || !args.content) {
172
+ return { content: [{ type: 'text', text: 'Error: name and content are required' }], isError: true };
173
+ }
174
+ const data = await apiCall(config, 'POST', '/agent-knowledge', {
175
+ name: args.name,
176
+ content: args.content,
177
+ description: args.description || '',
178
+ agent_id: config.agentId,
179
+ });
180
+ return { content: [{ type: 'text', text: `Knowledge "${data.name}" created (ID: ${data._id})` }] };
181
+ }
182
+ case 'update_knowledge': {
183
+ if (!args.knowledge_id) {
184
+ return { content: [{ type: 'text', text: 'Error: knowledge_id is required' }], isError: true };
185
+ }
186
+ const body = {};
187
+ if (args.name)
188
+ body.name = args.name;
189
+ if (args.content)
190
+ body.content = args.content;
191
+ if (args.description)
192
+ body.description = args.description;
193
+ const data = await apiCall(config, 'PUT', `/agent-knowledge/${args.knowledge_id}`, body);
194
+ return { content: [{ type: 'text', text: `Knowledge "${data.name}" updated (ID: ${data._id})` }] };
195
+ }
196
+ default:
197
+ return { content: [{ type: 'text', text: `Unknown tool: ${toolName}` }], isError: true };
198
+ }
199
+ }
200
+ catch (err) {
201
+ const message = err instanceof Error ? err.message : String(err);
202
+ return { content: [{ type: 'text', text: `Error: ${message}` }], isError: true };
203
+ }
204
+ }
205
+ // ===== Stdio Transport =====
206
+ function startServer(config) {
207
+ process.stderr.write(`[knowledge-mcp] Starting with agent ${config.agentId}\n`);
208
+ let buffer = '';
209
+ process.stdin.setEncoding('utf-8');
210
+ process.stdin.on('data', (chunk) => {
211
+ buffer += chunk;
212
+ // Claude Code sends newline-delimited JSON (one JSON object per line)
213
+ const lines = buffer.split('\n');
214
+ buffer = lines.pop() ?? ''; // Keep incomplete last line in buffer
215
+ for (const line of lines) {
216
+ const trimmed = line.trim();
217
+ if (!trimmed)
218
+ continue;
219
+ try {
220
+ const req = JSON.parse(trimmed);
221
+ handleRequest(config, req).catch((err) => {
222
+ process.stderr.write(`[knowledge-mcp] Error handling ${req.method}: ${err}\n`);
223
+ });
224
+ }
225
+ catch {
226
+ process.stderr.write(`[knowledge-mcp] Failed to parse JSON: ${trimmed.slice(0, 100)}\n`);
227
+ }
228
+ }
229
+ });
230
+ process.stdin.on('end', () => {
231
+ process.stderr.write('[knowledge-mcp] stdin closed, shutting down\n');
232
+ process.exit(0);
233
+ });
234
+ }
235
+ // ===== Main =====
236
+ const config = parseArgs();
237
+ startServer(config);
238
+ export {};
@@ -0,0 +1,20 @@
1
+ import type { AgentConfig } from '../types/index.js';
2
+ /**
3
+ * Generate MCP server config file for Claude Code.
4
+ * This file is auto-detected by runClaude and passed via --mcp-config.
5
+ */
6
+ export declare function setupMcpConfig(config: AgentConfig): string;
7
+ /**
8
+ * Get the MCP config path if it exists.
9
+ */
10
+ export declare function getMcpConfigPath(): string | undefined;
11
+ /**
12
+ * Write MCP server config into agent's folder .mcp.json.
13
+ * Claude Code reads .mcp.json automatically for project-level MCPs.
14
+ * Note: .claude/settings.json mcpServers is NOT reliably read by Claude Code CLI.
15
+ */
16
+ export declare function writeAgentFolderMcpConfig(agentFolderPath: string, config: AgentConfig): void;
17
+ /**
18
+ * Clean up MCP config file.
19
+ */
20
+ export declare function cleanupMcpConfig(): void;
@@ -0,0 +1,84 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { fileURLToPath } from 'url';
4
+ const __filename = fileURLToPath(import.meta.url);
5
+ const __dirname = path.dirname(__filename);
6
+ const MCP_CONFIG_DIR = path.join(process.env.HOME || '', '.tuna-agent');
7
+ const MCP_CONFIG_PATH = path.join(MCP_CONFIG_DIR, 'mcp-config.json');
8
+ /**
9
+ * Generate MCP server config file for Claude Code.
10
+ * This file is auto-detected by runClaude and passed via --mcp-config.
11
+ */
12
+ export function setupMcpConfig(config) {
13
+ const knowledgeServerPath = path.join(__dirname, 'knowledge-server.js');
14
+ const mcpConfig = {
15
+ mcpServers: {
16
+ 'tuna-knowledge': {
17
+ command: process.execPath, // full path to current node binary
18
+ args: [
19
+ knowledgeServerPath,
20
+ '--api-url', config.apiUrl,
21
+ '--token', config.agentToken,
22
+ '--agent-id', config.agentId,
23
+ ],
24
+ },
25
+ },
26
+ };
27
+ if (!fs.existsSync(MCP_CONFIG_DIR)) {
28
+ fs.mkdirSync(MCP_CONFIG_DIR, { recursive: true });
29
+ }
30
+ fs.writeFileSync(MCP_CONFIG_PATH, JSON.stringify(mcpConfig, null, 2));
31
+ console.log(`[MCP] Config written to ${MCP_CONFIG_PATH}`);
32
+ return MCP_CONFIG_PATH;
33
+ }
34
+ /**
35
+ * Get the MCP config path if it exists.
36
+ */
37
+ export function getMcpConfigPath() {
38
+ if (fs.existsSync(MCP_CONFIG_PATH)) {
39
+ return MCP_CONFIG_PATH;
40
+ }
41
+ return undefined;
42
+ }
43
+ /**
44
+ * Write MCP server config into agent's folder .mcp.json.
45
+ * Claude Code reads .mcp.json automatically for project-level MCPs.
46
+ * Note: .claude/settings.json mcpServers is NOT reliably read by Claude Code CLI.
47
+ */
48
+ export function writeAgentFolderMcpConfig(agentFolderPath, config) {
49
+ const knowledgeServerPath = path.join(__dirname, 'knowledge-server.js');
50
+ try {
51
+ const mcpJsonPath = path.join(agentFolderPath, '.mcp.json');
52
+ // Read existing .mcp.json to preserve other servers (e.g. playwright)
53
+ let existing = {};
54
+ if (fs.existsSync(mcpJsonPath)) {
55
+ existing = JSON.parse(fs.readFileSync(mcpJsonPath, 'utf-8'));
56
+ }
57
+ const existingServers = existing.mcpServers ?? {};
58
+ existing.mcpServers = {
59
+ ...existingServers,
60
+ 'tuna-knowledge': {
61
+ command: process.execPath,
62
+ args: [
63
+ knowledgeServerPath,
64
+ '--api-url', config.apiUrl,
65
+ '--token', config.agentToken,
66
+ '--agent-id', config.agentId,
67
+ ],
68
+ },
69
+ };
70
+ fs.writeFileSync(mcpJsonPath, JSON.stringify(existing, null, 2));
71
+ console.log(`[MCP] Agent folder .mcp.json written to ${mcpJsonPath}`);
72
+ }
73
+ catch (err) {
74
+ console.warn(`[MCP] Failed to write agent folder .mcp.json:`, err);
75
+ }
76
+ }
77
+ /**
78
+ * Clean up MCP config file.
79
+ */
80
+ export function cleanupMcpConfig() {
81
+ if (fs.existsSync(MCP_CONFIG_PATH)) {
82
+ fs.unlinkSync(MCP_CONFIG_PATH);
83
+ }
84
+ }
@@ -10,6 +10,7 @@ export interface AgentConfig {
10
10
  agentConfig?: {
11
11
  openclawGatewayUrl?: string;
12
12
  };
13
+ extensionCodes?: string[];
13
14
  }
14
15
  export interface AgentCapabilities {
15
16
  supports_teams: boolean;
@@ -41,6 +42,7 @@ export interface TaskAssignment {
41
42
  confirmBeforeEdit?: boolean;
42
43
  attachments?: ChatAttachment[];
43
44
  source?: 'manual' | 'skill' | 'scheduled' | 'workflow' | 'api';
45
+ agentId?: string;
44
46
  }
45
47
  export interface ConnectResponse {
46
48
  machine_id: string;
@@ -20,6 +20,8 @@ export interface ClaudeCliOptions {
20
20
  onPermissionRequest?: (tool: string, detail: string) => Promise<boolean>;
21
21
  /** Local file paths to pass as input (e.g., images) via --input-file flags */
22
22
  inputFiles?: string[];
23
+ /** Path to MCP server config JSON file (passed via --mcp-config) */
24
+ mcpConfigPath?: string;
23
25
  }
24
26
  export interface ClaudeCliResult {
25
27
  result: string;
@@ -1,5 +1,6 @@
1
1
  import { spawn, execSync } from 'child_process';
2
2
  import { StringDecoder } from 'string_decoder';
3
+ import { getMcpConfigPath } from '../mcp/setup.js';
3
4
  /** Cached absolute path to `claude` binary, resolved once at first use */
4
5
  let _claudeBinPath = null;
5
6
  function getClaudeBinPath() {
@@ -101,6 +102,11 @@ export function runClaude(options) {
101
102
  args.push('--tools', ''); // skip built-in tools
102
103
  args.push('--disable-slash-commands'); // skip skills/plugins
103
104
  }
105
+ // Auto-detect MCP config for knowledge server (skip if lightweight mode)
106
+ const mcpConfig = options.mcpConfigPath || (!options.lightweight ? getMcpConfigPath() : undefined);
107
+ if (mcpConfig) {
108
+ args.push('--mcp-config', mcpConfig);
109
+ }
104
110
  if (options.permissionMode) {
105
111
  args.push('--permission-mode', options.permissionMode);
106
112
  }
@@ -111,6 +117,7 @@ export function runClaude(options) {
111
117
  }
112
118
  }
113
119
  const useInteractiveStdin = !!options.permissionMode && !!options.onPermissionRequest;
120
+ console.log(`[claude-cli] Spawning with args: ${args.filter(a => !a.startsWith('sk-') && a.length < 200).join(' ')}`);
114
121
  const claudeBin = getClaudeBinPath();
115
122
  // Ensure PATH includes common bin dirs so shebang `#!/usr/bin/env node` resolves
116
123
  const spawnPath = [
@@ -198,6 +205,10 @@ export function runClaude(options) {
198
205
  proc.stderr.on('data', (chunk) => {
199
206
  const text = stderrDecoder.write(chunk);
200
207
  stderr += text;
208
+ // Log stderr for debugging (MCP server startup errors, etc.)
209
+ if (text.trim()) {
210
+ console.log(`[claude-cli] stderr: ${text.trim().substring(0, 300)}`);
211
+ }
201
212
  // Detect permission prompts from Claude Code on stderr
202
213
  if (useInteractiveStdin) {
203
214
  handlePermissionPrompt(text, proc, options.onPermissionRequest);
@@ -29,10 +29,13 @@ export declare const CommandMessageSchema: z.ZodObject<{
29
29
  type: z.ZodLiteral<"command">;
30
30
  command: z.ZodString;
31
31
  workspace_path: z.ZodOptional<z.ZodString>;
32
+ agent_folders: z.ZodOptional<z.ZodArray<z.ZodString>>;
33
+ agent_id: z.ZodOptional<z.ZodString>;
34
+ folder_path: z.ZodOptional<z.ZodString>;
32
35
  skill_id: z.ZodOptional<z.ZodString>;
33
36
  source_path: z.ZodOptional<z.ZodString>;
34
37
  skill_name: z.ZodOptional<z.ZodString>;
35
- }, z.core.$strip>;
38
+ }, z.core.$loose>;
36
39
  export declare const TaskCancelledSchema: z.ZodObject<{
37
40
  type: z.ZodLiteral<"task_cancelled">;
38
41
  taskId: z.ZodString;
@@ -21,11 +21,16 @@ export const CommandMessageSchema = z.object({
21
21
  type: z.literal('command'),
22
22
  command: z.string().min(1),
23
23
  workspace_path: z.string().optional(),
24
+ // For scan_workspace command
25
+ agent_folders: z.array(z.string()).optional(),
26
+ // For rescan_agent_skills command
27
+ agent_id: z.string().optional(),
28
+ folder_path: z.string().optional(),
24
29
  // For analyze_skill command
25
30
  skill_id: z.string().optional(),
26
31
  source_path: z.string().optional(),
27
32
  skill_name: z.string().optional(),
28
- });
33
+ }).passthrough();
29
34
  export const TaskCancelledSchema = z.object({
30
35
  type: z.literal('task_cancelled'),
31
36
  taskId: z.string().min(1),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tuna-agent",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Tuna Agent - Run AI coding tasks on your machine",
5
5
  "bin": {
6
6
  "tuna-agent": "dist/cli/index.js"