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.
- package/README.md +93 -0
- package/dist/__tests__/need-input-flow.test.d.ts +11 -0
- package/dist/__tests__/need-input-flow.test.js +646 -0
- package/dist/agents/claude-code-adapter.d.ts +13 -0
- package/dist/agents/claude-code-adapter.js +613 -0
- package/dist/agents/factory.d.ts +2 -0
- package/dist/agents/factory.js +12 -0
- package/dist/agents/openclaw-adapter.d.ts +18 -0
- package/dist/agents/openclaw-adapter.js +217 -0
- package/dist/agents/types.d.ts +31 -0
- package/dist/agents/types.js +1 -0
- package/dist/cli/commands/connect.d.ts +8 -0
- package/dist/cli/commands/connect.js +163 -0
- package/dist/cli/commands/start.d.ts +5 -0
- package/dist/cli/commands/start.js +54 -0
- package/dist/cli/commands/status.d.ts +1 -0
- package/dist/cli/commands/status.js +21 -0
- package/dist/cli/commands/stop.d.ts +1 -0
- package/dist/cli/commands/stop.js +23 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +32 -0
- package/dist/config/store.d.ts +29 -0
- package/dist/config/store.js +94 -0
- package/dist/daemon/index.d.ts +6 -0
- package/dist/daemon/index.js +576 -0
- package/dist/daemon/pm-state.d.ts +16 -0
- package/dist/daemon/pm-state.js +37 -0
- package/dist/daemon/ws-client.d.ts +107 -0
- package/dist/daemon/ws-client.js +293 -0
- package/dist/executor/task-runner.d.ts +30 -0
- package/dist/executor/task-runner.js +638 -0
- package/dist/pm/planner.d.ts +20 -0
- package/dist/pm/planner.js +375 -0
- package/dist/system/info.d.ts +18 -0
- package/dist/system/info.js +169 -0
- package/dist/types/index.d.ts +123 -0
- package/dist/types/index.js +1 -0
- package/dist/utils/claude-cli.d.ts +35 -0
- package/dist/utils/claude-cli.js +271 -0
- package/dist/utils/execution-helpers.d.ts +32 -0
- package/dist/utils/execution-helpers.js +177 -0
- package/dist/utils/image-download.d.ts +9 -0
- package/dist/utils/image-download.js +60 -0
- package/dist/utils/message-schemas.d.ts +69 -0
- package/dist/utils/message-schemas.js +80 -0
- package/dist/utils/pm-helpers.d.ts +5 -0
- package/dist/utils/pm-helpers.js +31 -0
- package/dist/utils/skill-scanner.d.ts +13 -0
- package/dist/utils/skill-scanner.js +91 -0
- package/dist/utils/validate-path.d.ts +10 -0
- package/dist/utils/validate-path.js +18 -0
- 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,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,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,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;
|