tuna-agent 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/README.md +93 -0
  2. package/dist/__tests__/need-input-flow.test.d.ts +11 -0
  3. package/dist/__tests__/need-input-flow.test.js +646 -0
  4. package/dist/agents/claude-code-adapter.d.ts +13 -0
  5. package/dist/agents/claude-code-adapter.js +613 -0
  6. package/dist/agents/factory.d.ts +2 -0
  7. package/dist/agents/factory.js +12 -0
  8. package/dist/agents/openclaw-adapter.d.ts +18 -0
  9. package/dist/agents/openclaw-adapter.js +217 -0
  10. package/dist/agents/types.d.ts +31 -0
  11. package/dist/agents/types.js +1 -0
  12. package/dist/cli/commands/connect.d.ts +8 -0
  13. package/dist/cli/commands/connect.js +163 -0
  14. package/dist/cli/commands/start.d.ts +5 -0
  15. package/dist/cli/commands/start.js +54 -0
  16. package/dist/cli/commands/status.d.ts +1 -0
  17. package/dist/cli/commands/status.js +21 -0
  18. package/dist/cli/commands/stop.d.ts +1 -0
  19. package/dist/cli/commands/stop.js +23 -0
  20. package/dist/cli/index.d.ts +2 -0
  21. package/dist/cli/index.js +32 -0
  22. package/dist/config/store.d.ts +29 -0
  23. package/dist/config/store.js +94 -0
  24. package/dist/daemon/index.d.ts +6 -0
  25. package/dist/daemon/index.js +576 -0
  26. package/dist/daemon/pm-state.d.ts +16 -0
  27. package/dist/daemon/pm-state.js +37 -0
  28. package/dist/daemon/ws-client.d.ts +107 -0
  29. package/dist/daemon/ws-client.js +293 -0
  30. package/dist/executor/task-runner.d.ts +30 -0
  31. package/dist/executor/task-runner.js +638 -0
  32. package/dist/pm/planner.d.ts +20 -0
  33. package/dist/pm/planner.js +375 -0
  34. package/dist/system/info.d.ts +18 -0
  35. package/dist/system/info.js +169 -0
  36. package/dist/types/index.d.ts +123 -0
  37. package/dist/types/index.js +1 -0
  38. package/dist/utils/claude-cli.d.ts +35 -0
  39. package/dist/utils/claude-cli.js +271 -0
  40. package/dist/utils/execution-helpers.d.ts +32 -0
  41. package/dist/utils/execution-helpers.js +177 -0
  42. package/dist/utils/image-download.d.ts +9 -0
  43. package/dist/utils/image-download.js +60 -0
  44. package/dist/utils/message-schemas.d.ts +69 -0
  45. package/dist/utils/message-schemas.js +80 -0
  46. package/dist/utils/pm-helpers.d.ts +5 -0
  47. package/dist/utils/pm-helpers.js +31 -0
  48. package/dist/utils/skill-scanner.d.ts +13 -0
  49. package/dist/utils/skill-scanner.js +91 -0
  50. package/dist/utils/validate-path.d.ts +10 -0
  51. package/dist/utils/validate-path.js +18 -0
  52. package/package.json +43 -0
@@ -0,0 +1,217 @@
1
+ import WebSocket from 'ws';
2
+ export class OpenClawAdapter {
3
+ type = 'openclaw';
4
+ displayName = 'OpenClaw';
5
+ gatewayUrl;
6
+ gatewayWs = null;
7
+ constructor(config) {
8
+ this.gatewayUrl = config.openclawGatewayUrl || 'ws://127.0.0.1:18789';
9
+ }
10
+ async checkHealth() {
11
+ return new Promise((resolve) => {
12
+ const timeout = setTimeout(() => {
13
+ resolve({ ok: false, message: `OpenClaw Gateway not reachable at ${this.gatewayUrl}` });
14
+ }, 5000);
15
+ try {
16
+ const ws = new WebSocket(this.gatewayUrl);
17
+ ws.on('open', () => {
18
+ clearTimeout(timeout);
19
+ ws.close();
20
+ resolve({ ok: true, message: `OpenClaw Gateway reachable at ${this.gatewayUrl}` });
21
+ });
22
+ ws.on('error', () => {
23
+ clearTimeout(timeout);
24
+ resolve({ ok: false, message: `OpenClaw Gateway not reachable at ${this.gatewayUrl}` });
25
+ });
26
+ }
27
+ catch {
28
+ clearTimeout(timeout);
29
+ resolve({ ok: false, message: `Failed to connect to OpenClaw Gateway at ${this.gatewayUrl}` });
30
+ }
31
+ });
32
+ }
33
+ async handleTask(task, ws, pendingInputResolvers, signal, _pendingPermissionResolvers) {
34
+ console.log(`[OpenClaw] Handling task ${task.id}`);
35
+ // Connect to OpenClaw Gateway
36
+ const gateway = await this.connectToGateway();
37
+ if (!gateway) {
38
+ ws.sendTaskFailed(task.id, `Cannot connect to OpenClaw Gateway at ${this.gatewayUrl}`);
39
+ return;
40
+ }
41
+ // Emit synthetic plan for UI consistency
42
+ ws.sendProgress(task.id, 'planning', { startedAt: new Date().toISOString() });
43
+ ws.sendPMMessage(task.id, {
44
+ sender: 'pm',
45
+ content: 'Forwarding task to OpenClaw...',
46
+ });
47
+ ws.sendPlanReady(task.id, {
48
+ summary: task.description,
49
+ subtasks: [{
50
+ id: 'openclaw-main',
51
+ role: 'fullstack',
52
+ description: task.description,
53
+ cwd: task.repoPath || '.',
54
+ dependencies: [],
55
+ status: 'pending',
56
+ }],
57
+ });
58
+ ws.sendProgress(task.id, 'executing', { startedAt: new Date().toISOString() });
59
+ ws.sendSubtaskStart(task.id, {
60
+ id: 'openclaw-main',
61
+ role: 'fullstack',
62
+ description: task.description,
63
+ });
64
+ // Send task to OpenClaw via Gateway WebSocket
65
+ try {
66
+ const result = await this.sendTaskToGateway(gateway, task, ws, pendingInputResolvers);
67
+ ws.sendSubtaskDone(task.id, {
68
+ subtaskId: 'openclaw-main',
69
+ status: 'done',
70
+ result: result,
71
+ });
72
+ ws.sendTaskDone(task.id, {
73
+ result: result,
74
+ sessions: [{
75
+ session_id: `openclaw-${Date.now()}`,
76
+ subtask_id: 'openclaw-main',
77
+ status: 'done',
78
+ result: result,
79
+ cost_usd: 0,
80
+ duration_ms: 0,
81
+ }],
82
+ });
83
+ ws.sendPMMessage(task.id, {
84
+ sender: 'pm',
85
+ content: 'OpenClaw completed the task.',
86
+ });
87
+ console.log(`[OpenClaw] Task ${task.id} completed`);
88
+ }
89
+ catch (err) {
90
+ const errMsg = err instanceof Error ? err.message : String(err);
91
+ ws.sendSubtaskDone(task.id, {
92
+ subtaskId: 'openclaw-main',
93
+ status: 'failed',
94
+ result: errMsg,
95
+ });
96
+ ws.sendTaskFailed(task.id, errMsg);
97
+ console.error(`[OpenClaw] Task ${task.id} failed:`, errMsg);
98
+ }
99
+ finally {
100
+ gateway.close();
101
+ }
102
+ }
103
+ async dispose() {
104
+ if (this.gatewayWs) {
105
+ this.gatewayWs.close();
106
+ this.gatewayWs = null;
107
+ }
108
+ }
109
+ connectToGateway() {
110
+ return new Promise((resolve) => {
111
+ const timeout = setTimeout(() => resolve(null), 10000);
112
+ try {
113
+ const ws = new WebSocket(this.gatewayUrl);
114
+ ws.on('open', () => {
115
+ clearTimeout(timeout);
116
+ resolve(ws);
117
+ });
118
+ ws.on('error', () => {
119
+ clearTimeout(timeout);
120
+ resolve(null);
121
+ });
122
+ }
123
+ catch {
124
+ clearTimeout(timeout);
125
+ resolve(null);
126
+ }
127
+ });
128
+ }
129
+ sendTaskToGateway(gateway, task, ws, pendingInputResolvers) {
130
+ return new Promise((resolve, reject) => {
131
+ const timeout = setTimeout(() => {
132
+ reject(new Error('OpenClaw Gateway response timeout (5 min)'));
133
+ }, 5 * 60 * 1000);
134
+ // Send task message to OpenClaw
135
+ gateway.send(JSON.stringify({
136
+ type: 'sessions_send',
137
+ message: task.description,
138
+ cwd: task.repoPath,
139
+ }));
140
+ let result = '';
141
+ gateway.on('message', (raw) => {
142
+ try {
143
+ const msg = JSON.parse(raw.toString());
144
+ const msgType = msg.type;
145
+ switch (msgType) {
146
+ case 'thinking':
147
+ case 'tool_use':
148
+ // Forward as subtask log
149
+ ws.sendProgress(task.id, 'subtask_log', {
150
+ subtaskId: 'openclaw-main',
151
+ log: { type: msgType, message: msg.content || '' },
152
+ });
153
+ break;
154
+ case 'response':
155
+ case 'result':
156
+ result = msg.content || msg.result || '';
157
+ break;
158
+ case 'complete':
159
+ case 'done':
160
+ clearTimeout(timeout);
161
+ resolve(result || msg.content || 'Task completed');
162
+ break;
163
+ case 'error':
164
+ clearTimeout(timeout);
165
+ reject(new Error(msg.message || msg.error || 'OpenClaw error'));
166
+ break;
167
+ case 'needs_input': {
168
+ const question = msg.question || 'OpenClaw needs input';
169
+ ws.sendNeedsInput(task.id, {
170
+ subtaskId: 'openclaw-main',
171
+ question,
172
+ });
173
+ ws.sendPMMessage(task.id, {
174
+ sender: 'pm',
175
+ content: `OpenClaw is asking: ${question}`,
176
+ });
177
+ // Wait for user answer and forward to gateway
178
+ const waitForAnswer = new Promise((resolveInput) => {
179
+ pendingInputResolvers.set(task.id, resolveInput);
180
+ });
181
+ waitForAnswer.then((response) => {
182
+ gateway.send(JSON.stringify({
183
+ type: 'input_response',
184
+ answer: response.text,
185
+ }));
186
+ ws.sendPMMessage(task.id, {
187
+ sender: 'user',
188
+ content: response.text,
189
+ });
190
+ });
191
+ break;
192
+ }
193
+ default:
194
+ // Log unknown messages for debugging
195
+ console.log(`[OpenClaw] Gateway message: ${msgType}`);
196
+ }
197
+ }
198
+ catch {
199
+ // Ignore parse errors
200
+ }
201
+ });
202
+ gateway.on('close', () => {
203
+ clearTimeout(timeout);
204
+ if (!result) {
205
+ reject(new Error('OpenClaw Gateway connection closed unexpectedly'));
206
+ }
207
+ else {
208
+ resolve(result);
209
+ }
210
+ });
211
+ gateway.on('error', (err) => {
212
+ clearTimeout(timeout);
213
+ reject(new Error(`OpenClaw Gateway error: ${err.message}`));
214
+ });
215
+ });
216
+ }
217
+ }
@@ -0,0 +1,31 @@
1
+ import type { TaskAssignment, InputResponse } from '../types/index.js';
2
+ import type { AgentWebSocketClient } from '../daemon/ws-client.js';
3
+ export type AgentType = 'claude-code' | 'openclaw';
4
+ export interface AgentConfig {
5
+ type: AgentType;
6
+ openclawGatewayUrl?: string;
7
+ }
8
+ /**
9
+ * AgentAdapter abstracts how a task gets executed.
10
+ * Each agent type implements this interface.
11
+ */
12
+ export interface AgentAdapter {
13
+ readonly type: AgentType;
14
+ readonly displayName: string;
15
+ /**
16
+ * Check if this agent is available/reachable on this machine.
17
+ */
18
+ checkHealth(): Promise<{
19
+ ok: boolean;
20
+ message: string;
21
+ }>;
22
+ /**
23
+ * Handle a task assignment end-to-end:
24
+ * plan → execute → report progress/results via ws.
25
+ */
26
+ handleTask(task: TaskAssignment, ws: AgentWebSocketClient, pendingInputResolvers: Map<string, (response: InputResponse) => void>, signal?: AbortSignal, pendingPermissionResolvers?: Map<string, (approved: boolean) => void>): Promise<void>;
27
+ /**
28
+ * Clean up resources on shutdown.
29
+ */
30
+ dispose(): Promise<void>;
31
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,8 @@
1
+ interface ConnectOptions {
2
+ apiUrl: string;
3
+ agentType?: string;
4
+ openclawPort?: string;
5
+ force?: boolean;
6
+ }
7
+ export declare function connect(code: string, options: ConnectOptions): Promise<void>;
8
+ export {};
@@ -0,0 +1,163 @@
1
+ import chalk from 'chalk';
2
+ import readline from 'readline';
3
+ import fs from 'fs';
4
+ import os from 'os';
5
+ import path from 'path';
6
+ import { getSystemInfo } from '../../system/info.js';
7
+ import { loadConfig, saveConfig } from '../../config/store.js';
8
+ function confirm(question) {
9
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
10
+ return new Promise((resolve) => {
11
+ rl.question(question, (answer) => {
12
+ rl.close();
13
+ resolve(answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes');
14
+ });
15
+ });
16
+ }
17
+ function prompt(question) {
18
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
19
+ return new Promise((resolve) => {
20
+ rl.question(question, (answer) => {
21
+ rl.close();
22
+ resolve(answer.trim());
23
+ });
24
+ });
25
+ }
26
+ /**
27
+ * Detect which agent types are available on this machine.
28
+ */
29
+ function detectAvailableAgents(cliTools) {
30
+ const agents = [];
31
+ if (cliTools.includes('claude')) {
32
+ agents.push({ type: 'claude-code', displayName: 'Claude Code' });
33
+ }
34
+ if (cliTools.includes('openclaw')) {
35
+ agents.push({ type: 'openclaw', displayName: 'OpenClaw' });
36
+ }
37
+ // Default: at least offer claude-code
38
+ if (agents.length === 0) {
39
+ agents.push({ type: 'claude-code', displayName: 'Claude Code' });
40
+ }
41
+ return agents;
42
+ }
43
+ export async function connect(code, options) {
44
+ const apiUrl = options.apiUrl;
45
+ // Check if already connected
46
+ const existing = loadConfig();
47
+ if (existing && !options.force) {
48
+ console.log(chalk.yellow('This machine is already connected as:'));
49
+ console.log(` Name: ${existing.name}`);
50
+ console.log(` Agent ID: ${existing.agentId}`);
51
+ console.log(` Agent: ${existing.agentType || 'claude-code'}`);
52
+ console.log(` Since: ${existing.connectedAt}`);
53
+ console.log();
54
+ const ok = await confirm('Reconnect with a new code? This will replace the current config. (y/N) ');
55
+ if (!ok) {
56
+ console.log('Aborted.');
57
+ process.exit(0);
58
+ }
59
+ console.log();
60
+ }
61
+ console.log(chalk.cyan('Tuna Agent') + ' — Connecting to Tuna...\n');
62
+ // Step 1: Detect CLI tools first (need for agent type selection)
63
+ console.log('Detecting system info...');
64
+ const preInfo = getSystemInfo();
65
+ console.log(` Machine: ${preInfo.hostname}`);
66
+ console.log(` OS: ${preInfo.os}`);
67
+ console.log(` CLIs: ${preInfo.cliTools.length > 0 ? preInfo.cliTools.join(', ') : 'none detected'}`);
68
+ // Step 2: Determine agent type
69
+ let agentType = 'claude-code';
70
+ let agentConfig = undefined;
71
+ if (options.agentType) {
72
+ // Explicit via CLI flag
73
+ if (options.agentType !== 'claude-code' && options.agentType !== 'openclaw') {
74
+ console.error(chalk.red(`\nInvalid agent type: ${options.agentType}`));
75
+ console.error('Supported types: claude-code, openclaw');
76
+ process.exit(1);
77
+ }
78
+ agentType = options.agentType;
79
+ }
80
+ else {
81
+ // Auto-detect
82
+ const detected = detectAvailableAgents(preInfo.cliTools);
83
+ if (detected.length > 1) {
84
+ console.log('\n Available agents:');
85
+ detected.forEach((a, i) => console.log(` ${i + 1}. ${a.displayName} (${a.type})`));
86
+ const choice = await prompt(`\n Select agent type [1-${detected.length}]: `);
87
+ const idx = parseInt(choice, 10) - 1;
88
+ if (idx >= 0 && idx < detected.length) {
89
+ agentType = detected[idx].type;
90
+ }
91
+ else {
92
+ agentType = detected[0].type;
93
+ }
94
+ }
95
+ else {
96
+ agentType = detected[0].type;
97
+ }
98
+ }
99
+ // OpenClaw-specific config
100
+ if (agentType === 'openclaw') {
101
+ const port = options.openclawPort || '18789';
102
+ agentConfig = {
103
+ openclawGatewayUrl: `ws://127.0.0.1:${port}`,
104
+ };
105
+ }
106
+ console.log(` Agent: ${agentType}`);
107
+ // Re-collect system info with correct agent type (for capabilities)
108
+ const systemInfo = getSystemInfo(agentType);
109
+ console.log(` Teams: ${systemInfo.capabilities.supports_teams ? 'yes' : 'no'}`);
110
+ console.log();
111
+ // Step 3: Send connection request to API
112
+ console.log(`Connecting with code ${chalk.bold(code.toUpperCase())}...`);
113
+ try {
114
+ const res = await fetch(`${apiUrl}/machine/connect`, {
115
+ method: 'POST',
116
+ headers: { 'Content-Type': 'application/json' },
117
+ body: JSON.stringify({
118
+ code: code.trim().toUpperCase(),
119
+ system_info: systemInfo,
120
+ agent_type: agentType,
121
+ }),
122
+ });
123
+ const body = await res.json();
124
+ if (!res.ok || !body.data) {
125
+ console.error(chalk.red(`\nFailed: ${body.message || 'Unknown error'}`));
126
+ console.error('Make sure the connection code is valid and not expired (5 min TTL).');
127
+ process.exit(1);
128
+ }
129
+ const { machine_id, machine_token, name } = body.data;
130
+ // Step 4: Save config
131
+ const wsUrl = apiUrl.replace(/^http/, 'ws') + '/ws/agent';
132
+ const config = {
133
+ agentId: machine_id,
134
+ agentToken: machine_token,
135
+ apiUrl,
136
+ wsUrl,
137
+ name,
138
+ connectedAt: new Date().toISOString(),
139
+ agentType,
140
+ agentConfig,
141
+ };
142
+ saveConfig(config);
143
+ // Create default workspace folder
144
+ const workspaceDir = path.join(os.homedir(), 'tuna-workspace');
145
+ if (!fs.existsSync(workspaceDir)) {
146
+ fs.mkdirSync(workspaceDir, { recursive: true });
147
+ console.log(`\nCreated workspace: ${workspaceDir}`);
148
+ }
149
+ console.log(chalk.green(`\nConnected successfully!`));
150
+ console.log(` Machine ID: ${machine_id}`);
151
+ console.log(` Name: ${name}`);
152
+ console.log(` Agent: ${agentType}`);
153
+ console.log(` Config: ~/.tuna-agent/config.json`);
154
+ console.log();
155
+ console.log(`Run ${chalk.cyan('tuna-agent start')} to start the daemon.`);
156
+ }
157
+ catch (err) {
158
+ const msg = err instanceof Error ? err.message : String(err);
159
+ console.error(chalk.red(`\nConnection failed: ${msg}`));
160
+ console.error(`Make sure the API server is running at ${apiUrl}`);
161
+ process.exit(1);
162
+ }
163
+ }
@@ -0,0 +1,5 @@
1
+ interface StartOptions {
2
+ foreground?: boolean;
3
+ }
4
+ export declare function start(options: StartOptions): Promise<void>;
5
+ export {};
@@ -0,0 +1,54 @@
1
+ import { spawn } from 'child_process';
2
+ import fs from 'fs';
3
+ import path from 'path';
4
+ import os from 'os';
5
+ import chalk from 'chalk';
6
+ import { loadConfig, isConfigured, isDaemonRunning, savePid } from '../../config/store.js';
7
+ import { startDaemon } from '../../daemon/index.js';
8
+ export async function start(options) {
9
+ if (!isConfigured()) {
10
+ console.error(chalk.red('Agent is not configured.'));
11
+ console.error(`Run ${chalk.cyan('tuna-agent connect <code>')} first.`);
12
+ process.exit(1);
13
+ }
14
+ if (!options.foreground && isDaemonRunning()) {
15
+ console.log(chalk.yellow('Agent daemon is already running.'));
16
+ console.log(`Use ${chalk.cyan('tuna-agent stop')} to stop it first.`);
17
+ return;
18
+ }
19
+ const config = loadConfig();
20
+ if (options.foreground) {
21
+ // Run in foreground
22
+ console.log(chalk.cyan('Tuna Agent') + ` — Starting in foreground mode...`);
23
+ console.log(` Agent: ${config.name} (${config.agentId})`);
24
+ console.log(` API: ${config.apiUrl}`);
25
+ console.log();
26
+ savePid(process.pid);
27
+ await startDaemon(config);
28
+ }
29
+ else {
30
+ // Daemonize: spawn detached child process
31
+ console.log(chalk.cyan('Tuna Agent') + ` — Starting daemon...`);
32
+ const logFile = path.join(os.homedir(), '.tuna-agent', 'daemon.log');
33
+ const logFd = fs.openSync(logFile, 'a');
34
+ const child = spawn(process.execPath, [process.argv[1], 'start', '--foreground'], {
35
+ detached: true,
36
+ stdio: ['ignore', logFd, logFd],
37
+ env: { ...process.env },
38
+ });
39
+ child.unref();
40
+ if (child.pid) {
41
+ savePid(child.pid);
42
+ console.log(chalk.green(`Daemon started (PID: ${child.pid})`));
43
+ console.log(` Agent: ${config.name}`);
44
+ console.log(` API: ${config.apiUrl}`);
45
+ console.log();
46
+ console.log(`Use ${chalk.cyan('tuna-agent status')} to check status.`);
47
+ console.log(`Use ${chalk.cyan('tuna-agent stop')} to stop.`);
48
+ }
49
+ else {
50
+ console.error(chalk.red('Failed to start daemon.'));
51
+ process.exit(1);
52
+ }
53
+ }
54
+ }
@@ -0,0 +1 @@
1
+ export declare function status(): Promise<void>;
@@ -0,0 +1,21 @@
1
+ import chalk from 'chalk';
2
+ import { loadConfig, isConfigured, isDaemonRunning, readPid } from '../../config/store.js';
3
+ export async function status() {
4
+ if (!isConfigured()) {
5
+ console.log(chalk.yellow('Agent is not configured.'));
6
+ console.log(`Run ${chalk.cyan('tuna-agent connect <code>')} to set up.`);
7
+ return;
8
+ }
9
+ const config = loadConfig();
10
+ const running = isDaemonRunning();
11
+ console.log(chalk.cyan('Tuna Agent Status\n'));
12
+ console.log(` Name: ${config.name}`);
13
+ console.log(` Agent ID: ${config.agentId}`);
14
+ console.log(` Agent: ${config.agentType || 'claude-code'}`);
15
+ if (config.agentType === 'openclaw' && config.agentConfig?.openclawGatewayUrl) {
16
+ console.log(` Gateway: ${config.agentConfig.openclawGatewayUrl}`);
17
+ }
18
+ console.log(` API: ${config.apiUrl}`);
19
+ console.log(` Connected: ${config.connectedAt}`);
20
+ console.log(` Daemon: ${running ? chalk.green('running') + ` (PID: ${readPid()})` : chalk.red('stopped')}`);
21
+ }
@@ -0,0 +1 @@
1
+ export declare function stop(): Promise<void>;
@@ -0,0 +1,23 @@
1
+ import chalk from 'chalk';
2
+ import { readPid, removePid, isDaemonRunning } from '../../config/store.js';
3
+ export async function stop() {
4
+ if (!isDaemonRunning()) {
5
+ console.log(chalk.yellow('Hub daemon is not running.'));
6
+ return;
7
+ }
8
+ const pid = readPid();
9
+ if (!pid) {
10
+ console.log(chalk.yellow('No PID file found.'));
11
+ return;
12
+ }
13
+ try {
14
+ process.kill(pid, 'SIGTERM');
15
+ removePid();
16
+ console.log(chalk.green(`Hub daemon stopped (PID: ${pid}).`));
17
+ }
18
+ catch (err) {
19
+ const msg = err instanceof Error ? err.message : String(err);
20
+ console.error(chalk.red(`Failed to stop daemon: ${msg}`));
21
+ removePid();
22
+ }
23
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,32 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from 'commander';
3
+ import { connect } from './commands/connect.js';
4
+ import { start } from './commands/start.js';
5
+ import { stop } from './commands/stop.js';
6
+ import { status } from './commands/status.js';
7
+ const program = new Command()
8
+ .name('tuna-agent')
9
+ .description('Tuna Agent - Run AI coding tasks on your machine')
10
+ .version('0.1.0');
11
+ program
12
+ .command('connect <code>')
13
+ .description('Connect this machine to your Tuna account')
14
+ .option('-a, --api-url <url>', 'API server URL', 'https://api.dev.tunasm.art')
15
+ .option('-t, --agent-type <type>', 'Agent type: claude-code or openclaw')
16
+ .option('--openclaw-port <port>', 'OpenClaw Gateway port', '18789')
17
+ .option('--force', 'Skip confirmation if already connected')
18
+ .action(connect);
19
+ program
20
+ .command('start')
21
+ .description('Start the agent daemon')
22
+ .option('-f, --foreground', 'Run in foreground (don\'t daemonize)')
23
+ .action(start);
24
+ program
25
+ .command('stop')
26
+ .description('Stop the agent daemon')
27
+ .action(stop);
28
+ program
29
+ .command('status')
30
+ .description('Show agent connection status')
31
+ .action(status);
32
+ program.parse();
@@ -0,0 +1,29 @@
1
+ import type { AgentConfig } from '../types/index.js';
2
+ /**
3
+ * Save agent configuration.
4
+ */
5
+ export declare function saveConfig(config: AgentConfig): void;
6
+ /**
7
+ * Load agent configuration.
8
+ */
9
+ export declare function loadConfig(): AgentConfig | null;
10
+ /**
11
+ * Check if agent is configured.
12
+ */
13
+ export declare function isConfigured(): boolean;
14
+ /**
15
+ * Save daemon PID.
16
+ */
17
+ export declare function savePid(pid: number): void;
18
+ /**
19
+ * Read daemon PID.
20
+ */
21
+ export declare function readPid(): number | null;
22
+ /**
23
+ * Remove daemon PID file.
24
+ */
25
+ export declare function removePid(): void;
26
+ /**
27
+ * Check if daemon process is running.
28
+ */
29
+ export declare function isDaemonRunning(): boolean;