upfynai-code 2.7.5 → 2.8.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/bin/cli.js +1 -1
- package/dist/agents/claude.js +197 -0
- package/dist/agents/codex.js +48 -0
- package/dist/agents/cursor.js +48 -0
- package/dist/agents/detect.js +51 -0
- package/dist/agents/exec.js +31 -0
- package/dist/agents/files.js +105 -0
- package/dist/agents/git.js +18 -0
- package/dist/agents/index.js +87 -0
- package/dist/agents/shell.js +38 -0
- package/dist/agents/utils.js +136 -0
- package/package.json +4 -2
- package/scripts/prepublish.js +41 -0
- package/src/connect.js +61 -515
- package/src/launch.js +39 -15
package/bin/cli.js
CHANGED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claude Agent
|
|
3
|
+
* Handles: claude-query, claude-task-query
|
|
4
|
+
*
|
|
5
|
+
* These are STREAMING actions — they use ctx.stream() to send chunks
|
|
6
|
+
* and return a Promise that resolves when the process completes.
|
|
7
|
+
*
|
|
8
|
+
* Two modes:
|
|
9
|
+
* - stream-json (connect.js) — full structured streaming with session tracking
|
|
10
|
+
* - simple --print (relay-client.js) — basic stdout streaming
|
|
11
|
+
*
|
|
12
|
+
* The caller chooses the mode via ctx.streamMode ('structured' | 'simple').
|
|
13
|
+
*/
|
|
14
|
+
import { spawn } from 'child_process';
|
|
15
|
+
import os from 'os';
|
|
16
|
+
|
|
17
|
+
export default {
|
|
18
|
+
name: 'claude',
|
|
19
|
+
actions: {
|
|
20
|
+
'claude-query': async (params, ctx) => {
|
|
21
|
+
const { command, options } = params;
|
|
22
|
+
const mode = ctx.streamMode || 'structured';
|
|
23
|
+
const resolveBinary = ctx.resolveBinary || ((name) => name);
|
|
24
|
+
|
|
25
|
+
if (mode === 'simple') {
|
|
26
|
+
return runSimpleClaudeQuery(command, options, ctx, resolveBinary);
|
|
27
|
+
}
|
|
28
|
+
return runStructuredClaudeQuery(command, options, ctx, resolveBinary);
|
|
29
|
+
},
|
|
30
|
+
|
|
31
|
+
'claude-task-query': async (params, ctx) => {
|
|
32
|
+
const { command, options } = params;
|
|
33
|
+
const resolveBinary = ctx.resolveBinary || ((name) => name);
|
|
34
|
+
return runClaudeTaskQuery(command, options, ctx, resolveBinary);
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Structured streaming mode (stream-json).
|
|
41
|
+
* Parses NDJSON events, computes text deltas, tracks session ID.
|
|
42
|
+
*/
|
|
43
|
+
function runStructuredClaudeQuery(command, options, ctx, resolveBinary) {
|
|
44
|
+
return new Promise((resolve) => {
|
|
45
|
+
const args = ['-p', '--output-format', 'stream-json', '--include-partial-messages'];
|
|
46
|
+
if (options?.sessionId) args.push('--continue', options.sessionId);
|
|
47
|
+
|
|
48
|
+
const proc = spawn(resolveBinary('claude'), [...args, command || ''], {
|
|
49
|
+
shell: true,
|
|
50
|
+
cwd: options?.projectPath || os.homedir(),
|
|
51
|
+
env: process.env,
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
if (ctx.trackProcess) ctx.trackProcess(ctx.requestId, { proc, action: 'claude-query' });
|
|
55
|
+
|
|
56
|
+
let stdoutBuffer = '';
|
|
57
|
+
let capturedSessionId = null;
|
|
58
|
+
let lastTextLength = 0;
|
|
59
|
+
|
|
60
|
+
proc.stdout.on('data', (chunk) => {
|
|
61
|
+
stdoutBuffer += chunk.toString();
|
|
62
|
+
const lines = stdoutBuffer.split('\n');
|
|
63
|
+
stdoutBuffer = lines.pop();
|
|
64
|
+
|
|
65
|
+
for (const line of lines) {
|
|
66
|
+
if (!line.trim()) continue;
|
|
67
|
+
try {
|
|
68
|
+
const evt = JSON.parse(line);
|
|
69
|
+
|
|
70
|
+
if (evt.type === 'system' && evt.subtype === 'init') {
|
|
71
|
+
if (evt.session_id) capturedSessionId = evt.session_id;
|
|
72
|
+
lastTextLength = 0;
|
|
73
|
+
ctx.stream({ type: 'claude-system', sessionId: evt.session_id, model: evt.model, cwd: evt.cwd });
|
|
74
|
+
} else if (evt.type === 'assistant' && evt.message?.content?.length) {
|
|
75
|
+
const fullText = evt.message.content[0].text || '';
|
|
76
|
+
const delta = fullText.slice(lastTextLength);
|
|
77
|
+
lastTextLength = fullText.length;
|
|
78
|
+
if (delta) {
|
|
79
|
+
ctx.stream({ type: 'claude-response', content: delta });
|
|
80
|
+
}
|
|
81
|
+
} else if (evt.type === 'result') {
|
|
82
|
+
lastTextLength = 0;
|
|
83
|
+
ctx.stream({ type: 'claude-result', sessionId: capturedSessionId, subtype: evt.subtype });
|
|
84
|
+
}
|
|
85
|
+
} catch {
|
|
86
|
+
if (line.trim() && !line.startsWith('%') && !line.includes('claude query')) {
|
|
87
|
+
ctx.stream({ type: 'claude-response', content: line });
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
proc.stderr.on('data', (chunk) => {
|
|
94
|
+
ctx.stream({ type: 'claude-error', content: chunk.toString() });
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
proc.on('close', (code) => {
|
|
98
|
+
if (ctx.untrackProcess) ctx.untrackProcess(ctx.requestId);
|
|
99
|
+
resolve({ exitCode: code, sessionId: capturedSessionId });
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
proc.on('error', () => {
|
|
103
|
+
if (ctx.untrackProcess) ctx.untrackProcess(ctx.requestId);
|
|
104
|
+
resolve({ exitCode: 1, sessionId: capturedSessionId });
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Simple streaming mode (--print).
|
|
111
|
+
* Just pipes stdout/stderr chunks.
|
|
112
|
+
*/
|
|
113
|
+
function runSimpleClaudeQuery(command, options, ctx, resolveBinary) {
|
|
114
|
+
return new Promise((resolve) => {
|
|
115
|
+
const args = ['--print'];
|
|
116
|
+
if (options?.sessionId) args.push('--continue', options.sessionId);
|
|
117
|
+
|
|
118
|
+
const proc = spawn(resolveBinary('claude'), [...args, command || ''], {
|
|
119
|
+
shell: true,
|
|
120
|
+
cwd: options?.projectPath || os.homedir(),
|
|
121
|
+
env: process.env,
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
if (ctx.trackProcess) ctx.trackProcess(ctx.requestId, { proc, action: 'claude-query' });
|
|
125
|
+
|
|
126
|
+
proc.stdout.on('data', (chunk) => {
|
|
127
|
+
ctx.stream({ type: 'claude-response', content: chunk.toString() });
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
proc.stderr.on('data', (chunk) => {
|
|
131
|
+
ctx.stream({ type: 'claude-error', content: chunk.toString() });
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
proc.on('close', (code) => {
|
|
135
|
+
if (ctx.untrackProcess) ctx.untrackProcess(ctx.requestId);
|
|
136
|
+
resolve({ exitCode: code });
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
proc.on('error', () => {
|
|
140
|
+
if (ctx.untrackProcess) ctx.untrackProcess(ctx.requestId);
|
|
141
|
+
resolve({ exitCode: 1 });
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Sub-agent for read-only research tasks.
|
|
148
|
+
* Uses --allowedTools to restrict to read-only operations.
|
|
149
|
+
*/
|
|
150
|
+
function runClaudeTaskQuery(command, options, ctx, resolveBinary) {
|
|
151
|
+
return new Promise((resolve) => {
|
|
152
|
+
const args = ['-p', '--output-format', 'stream-json', '--include-partial-messages'];
|
|
153
|
+
args.push('--allowedTools', 'View,Glob,Grep,LS,Read');
|
|
154
|
+
|
|
155
|
+
const proc = spawn(resolveBinary('claude'), [...args, command || ''], {
|
|
156
|
+
shell: true,
|
|
157
|
+
cwd: options?.projectPath || os.homedir(),
|
|
158
|
+
env: process.env,
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
if (ctx.trackProcess) ctx.trackProcess(ctx.requestId, { proc, action: 'claude-task-query' });
|
|
162
|
+
let taskBuffer = '';
|
|
163
|
+
|
|
164
|
+
proc.stdout.on('data', (chunk) => {
|
|
165
|
+
taskBuffer += chunk.toString();
|
|
166
|
+
const lines = taskBuffer.split('\n');
|
|
167
|
+
taskBuffer = lines.pop();
|
|
168
|
+
for (const line of lines) {
|
|
169
|
+
if (!line.trim()) continue;
|
|
170
|
+
try {
|
|
171
|
+
const evt = JSON.parse(line);
|
|
172
|
+
if (evt.type === 'assistant' && evt.message?.content?.length) {
|
|
173
|
+
ctx.stream({ type: 'claude-response', content: evt.message.content[0].text || '' });
|
|
174
|
+
}
|
|
175
|
+
} catch {
|
|
176
|
+
if (line.trim()) {
|
|
177
|
+
ctx.stream({ type: 'claude-response', content: line });
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
proc.stderr.on('data', (chunk) => {
|
|
184
|
+
ctx.stream({ type: 'claude-error', content: chunk.toString() });
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
proc.on('close', (code) => {
|
|
188
|
+
if (ctx.untrackProcess) ctx.untrackProcess(ctx.requestId);
|
|
189
|
+
resolve({ exitCode: code });
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
proc.on('error', () => {
|
|
193
|
+
if (ctx.untrackProcess) ctx.untrackProcess(ctx.requestId);
|
|
194
|
+
resolve({ exitCode: 1 });
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Codex Agent
|
|
3
|
+
* Handles: codex-query
|
|
4
|
+
* Spawns the OpenAI Codex CLI and streams output.
|
|
5
|
+
*/
|
|
6
|
+
import { spawn } from 'child_process';
|
|
7
|
+
import os from 'os';
|
|
8
|
+
|
|
9
|
+
export default {
|
|
10
|
+
name: 'codex',
|
|
11
|
+
actions: {
|
|
12
|
+
'codex-query': async (params, ctx) => {
|
|
13
|
+
const { command, options } = params;
|
|
14
|
+
const resolveBinary = ctx.resolveBinary || ((name) => name);
|
|
15
|
+
|
|
16
|
+
return new Promise((resolve) => {
|
|
17
|
+
const args = ['--quiet'];
|
|
18
|
+
if (options?.model) args.push('--model', options.model);
|
|
19
|
+
|
|
20
|
+
const proc = spawn(resolveBinary('codex'), [...args, command || ''], {
|
|
21
|
+
shell: true,
|
|
22
|
+
cwd: options?.projectPath || options?.cwd || os.homedir(),
|
|
23
|
+
env: process.env,
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
if (ctx.trackProcess) ctx.trackProcess(ctx.requestId, { proc, action: 'codex-query' });
|
|
27
|
+
|
|
28
|
+
proc.stdout.on('data', (chunk) => {
|
|
29
|
+
ctx.stream({ type: 'codex-response', content: chunk.toString() });
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
proc.stderr.on('data', (chunk) => {
|
|
33
|
+
ctx.stream({ type: 'codex-error', content: chunk.toString() });
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
proc.on('close', (code) => {
|
|
37
|
+
if (ctx.untrackProcess) ctx.untrackProcess(ctx.requestId);
|
|
38
|
+
resolve({ exitCode: code });
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
proc.on('error', () => {
|
|
42
|
+
if (ctx.untrackProcess) ctx.untrackProcess(ctx.requestId);
|
|
43
|
+
resolve({ exitCode: 1 });
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cursor Agent
|
|
3
|
+
* Handles: cursor-query
|
|
4
|
+
* Spawns the Cursor Agent CLI and streams output.
|
|
5
|
+
*/
|
|
6
|
+
import { spawn } from 'child_process';
|
|
7
|
+
import os from 'os';
|
|
8
|
+
|
|
9
|
+
export default {
|
|
10
|
+
name: 'cursor',
|
|
11
|
+
actions: {
|
|
12
|
+
'cursor-query': async (params, ctx) => {
|
|
13
|
+
const { command, options } = params;
|
|
14
|
+
const resolveBinary = ctx.resolveBinary || ((name) => name);
|
|
15
|
+
|
|
16
|
+
return new Promise((resolve) => {
|
|
17
|
+
const args = [];
|
|
18
|
+
if (options?.model) args.push('--model', options.model);
|
|
19
|
+
|
|
20
|
+
const proc = spawn(resolveBinary('cursor-agent'), [...args, command || ''], {
|
|
21
|
+
shell: true,
|
|
22
|
+
cwd: options?.projectPath || options?.cwd || os.homedir(),
|
|
23
|
+
env: process.env,
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
if (ctx.trackProcess) ctx.trackProcess(ctx.requestId, { proc, action: 'cursor-query' });
|
|
27
|
+
|
|
28
|
+
proc.stdout.on('data', (chunk) => {
|
|
29
|
+
ctx.stream({ type: 'cursor-response', content: chunk.toString() });
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
proc.stderr.on('data', (chunk) => {
|
|
33
|
+
ctx.stream({ type: 'cursor-error', content: chunk.toString() });
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
proc.on('close', (code) => {
|
|
37
|
+
if (ctx.untrackProcess) ctx.untrackProcess(ctx.requestId);
|
|
38
|
+
resolve({ exitCode: code });
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
proc.on('error', () => {
|
|
42
|
+
if (ctx.untrackProcess) ctx.untrackProcess(ctx.requestId);
|
|
43
|
+
resolve({ exitCode: 1 });
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Detect Agent
|
|
3
|
+
* Handles: detect-agents
|
|
4
|
+
* Checks which AI CLI agents are installed on the local machine.
|
|
5
|
+
*/
|
|
6
|
+
import { execSync } from 'child_process';
|
|
7
|
+
import fs from 'fs';
|
|
8
|
+
import path from 'path';
|
|
9
|
+
|
|
10
|
+
const AGENT_DEFINITIONS = [
|
|
11
|
+
{ name: 'claude', binary: 'claude', label: 'Claude Code' },
|
|
12
|
+
{ name: 'codex', binary: 'codex', label: 'OpenAI Codex' },
|
|
13
|
+
{ name: 'cursor', binary: 'cursor-agent', label: 'Cursor Agent' },
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
export default {
|
|
17
|
+
name: 'detect',
|
|
18
|
+
actions: {
|
|
19
|
+
'detect-agents': async (params, ctx) => {
|
|
20
|
+
const isWindows = process.platform === 'win32';
|
|
21
|
+
const whichCmd = isWindows ? 'where' : 'which';
|
|
22
|
+
const localBinDir = ctx.localBinDir || null;
|
|
23
|
+
|
|
24
|
+
const agents = {};
|
|
25
|
+
for (const agent of AGENT_DEFINITIONS) {
|
|
26
|
+
try {
|
|
27
|
+
const result = execSync(`${whichCmd} ${agent.binary}`, {
|
|
28
|
+
stdio: 'pipe',
|
|
29
|
+
timeout: 5000,
|
|
30
|
+
}).toString().trim();
|
|
31
|
+
agents[agent.name] = {
|
|
32
|
+
installed: true,
|
|
33
|
+
path: result.split('\n')[0].trim(),
|
|
34
|
+
label: agent.label,
|
|
35
|
+
};
|
|
36
|
+
} catch {
|
|
37
|
+
// Check local node_modules/.bin as fallback
|
|
38
|
+
if (localBinDir) {
|
|
39
|
+
const localPath = path.join(localBinDir, agent.binary + (isWindows ? '.cmd' : ''));
|
|
40
|
+
if (fs.existsSync(localPath)) {
|
|
41
|
+
agents[agent.name] = { installed: true, path: localPath, label: agent.label };
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
agents[agent.name] = { installed: false, label: agent.label };
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return { agents };
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Exec Agent
|
|
3
|
+
* Handles: exec (arbitrary command execution)
|
|
4
|
+
*/
|
|
5
|
+
import { execSync } from 'child_process';
|
|
6
|
+
|
|
7
|
+
export default {
|
|
8
|
+
name: 'exec',
|
|
9
|
+
actions: {
|
|
10
|
+
'exec': async (params) => {
|
|
11
|
+
const { command, timeout: cmdTimeout = 60000, cwd } = params;
|
|
12
|
+
if (!command) throw new Error('No command provided');
|
|
13
|
+
|
|
14
|
+
try {
|
|
15
|
+
const output = execSync(command, {
|
|
16
|
+
encoding: 'utf8',
|
|
17
|
+
timeout: cmdTimeout,
|
|
18
|
+
cwd: cwd || process.cwd(),
|
|
19
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
20
|
+
});
|
|
21
|
+
return { output, exitCode: 0 };
|
|
22
|
+
} catch (execErr) {
|
|
23
|
+
return {
|
|
24
|
+
output: execErr.stdout || '',
|
|
25
|
+
stderr: execErr.stderr || '',
|
|
26
|
+
exitCode: execErr.status || 1,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
};
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File System Agent
|
|
3
|
+
* Handles: file-read, file-write, file-tree, browse-dirs, validate-path, create-folder
|
|
4
|
+
*/
|
|
5
|
+
import { promises as fsPromises } from 'fs';
|
|
6
|
+
import os from 'os';
|
|
7
|
+
import path from 'path';
|
|
8
|
+
import {
|
|
9
|
+
buildFileTree,
|
|
10
|
+
resolveTildePath,
|
|
11
|
+
isBlockedPath,
|
|
12
|
+
BLOCKED_READ_PATTERNS,
|
|
13
|
+
BLOCKED_WRITE_PATTERNS,
|
|
14
|
+
} from './utils.js';
|
|
15
|
+
|
|
16
|
+
export default {
|
|
17
|
+
name: 'files',
|
|
18
|
+
actions: {
|
|
19
|
+
'file-read': async (params) => {
|
|
20
|
+
let { filePath, encoding } = params;
|
|
21
|
+
if (!filePath || typeof filePath !== 'string') throw new Error('Invalid file path');
|
|
22
|
+
|
|
23
|
+
filePath = resolveTildePath(filePath);
|
|
24
|
+
const normalizedPath = path.resolve(filePath);
|
|
25
|
+
if (isBlockedPath(normalizedPath, BLOCKED_READ_PATTERNS)) throw new Error('Access denied');
|
|
26
|
+
|
|
27
|
+
const content = await fsPromises.readFile(normalizedPath, encoding === 'base64' ? null : 'utf8');
|
|
28
|
+
const result = encoding === 'base64' ? content.toString('base64') : content;
|
|
29
|
+
return { content: result };
|
|
30
|
+
},
|
|
31
|
+
|
|
32
|
+
'file-write': async (params) => {
|
|
33
|
+
let { filePath, content } = params;
|
|
34
|
+
if (!filePath || typeof filePath !== 'string') throw new Error('Invalid file path');
|
|
35
|
+
|
|
36
|
+
filePath = resolveTildePath(filePath);
|
|
37
|
+
const normalizedPath = path.resolve(filePath);
|
|
38
|
+
if (isBlockedPath(normalizedPath, BLOCKED_WRITE_PATTERNS)) throw new Error('Access denied');
|
|
39
|
+
|
|
40
|
+
const parentDir = path.dirname(normalizedPath);
|
|
41
|
+
await fsPromises.mkdir(parentDir, { recursive: true }).catch(() => {});
|
|
42
|
+
await fsPromises.writeFile(normalizedPath, content, 'utf8');
|
|
43
|
+
return { success: true };
|
|
44
|
+
},
|
|
45
|
+
|
|
46
|
+
'file-tree': async (params) => {
|
|
47
|
+
const { dirPath, depth, maxDepth } = params;
|
|
48
|
+
const treeDepth = depth || maxDepth || 3;
|
|
49
|
+
const resolvedDir = dirPath ? path.resolve(dirPath) : process.cwd();
|
|
50
|
+
const tree = await buildFileTree(resolvedDir, treeDepth, 0, {
|
|
51
|
+
maxEntries: 200,
|
|
52
|
+
includeStats: true,
|
|
53
|
+
skipDotfilesAtRoot: true,
|
|
54
|
+
});
|
|
55
|
+
return { files: tree };
|
|
56
|
+
},
|
|
57
|
+
|
|
58
|
+
'browse-dirs': async (params) => {
|
|
59
|
+
const { dirPath: browsePath } = params;
|
|
60
|
+
let targetDir = resolveTildePath(browsePath);
|
|
61
|
+
targetDir = path.resolve(targetDir);
|
|
62
|
+
|
|
63
|
+
let drives = [];
|
|
64
|
+
// On Windows, detect available drives
|
|
65
|
+
if (process.platform === 'win32') {
|
|
66
|
+
try {
|
|
67
|
+
const { execSync } = await import('child_process');
|
|
68
|
+
const wmicOut = execSync('wmic logicaldisk get name', { encoding: 'utf8', timeout: 5000 });
|
|
69
|
+
drives = wmicOut.split('\n')
|
|
70
|
+
.map(l => l.trim())
|
|
71
|
+
.filter(l => /^[A-Z]:$/.test(l))
|
|
72
|
+
.map(d => ({ name: d + '\\', path: d + '\\', type: 'drive' }));
|
|
73
|
+
} catch { /* wmic not available */ }
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const entries = await fsPromises.readdir(targetDir, { withFileTypes: true });
|
|
77
|
+
const dirs = entries
|
|
78
|
+
.filter(e => e.isDirectory() && !e.name.startsWith('.'))
|
|
79
|
+
.map(e => ({ name: e.name, path: path.join(targetDir, e.name), type: 'directory' }))
|
|
80
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
81
|
+
|
|
82
|
+
return { path: targetDir, suggestions: dirs, drives, homedir: os.homedir() };
|
|
83
|
+
},
|
|
84
|
+
|
|
85
|
+
'validate-path': async (params) => {
|
|
86
|
+
const { targetPath } = params;
|
|
87
|
+
const checkPath = resolveTildePath(targetPath);
|
|
88
|
+
const resolved = path.resolve(checkPath);
|
|
89
|
+
|
|
90
|
+
try {
|
|
91
|
+
const stats = await fsPromises.stat(resolved);
|
|
92
|
+
return { exists: true, isDirectory: stats.isDirectory(), resolvedPath: resolved };
|
|
93
|
+
} catch {
|
|
94
|
+
return { exists: false, resolvedPath: resolved };
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
|
|
98
|
+
'create-folder': async (params) => {
|
|
99
|
+
const { folderPath } = params;
|
|
100
|
+
const mkPath = path.resolve(resolveTildePath(folderPath));
|
|
101
|
+
await fsPromises.mkdir(mkPath, { recursive: true });
|
|
102
|
+
return { success: true, path: mkPath };
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Git Agent
|
|
3
|
+
* Handles: git-operation
|
|
4
|
+
*/
|
|
5
|
+
import path from 'path';
|
|
6
|
+
import { execCommand } from './utils.js';
|
|
7
|
+
|
|
8
|
+
export default {
|
|
9
|
+
name: 'git',
|
|
10
|
+
actions: {
|
|
11
|
+
'git-operation': async (params) => {
|
|
12
|
+
const { gitCommand, cwd } = params;
|
|
13
|
+
const resolvedCwd = cwd ? path.resolve(cwd) : process.cwd();
|
|
14
|
+
const result = await execCommand('git', [gitCommand], { cwd: resolvedCwd });
|
|
15
|
+
return { stdout: result };
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
};
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent Registry
|
|
3
|
+
* Central dispatch for all command actions.
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* import { executeAction, isStreamingAction } from 'upfynai-shared/agents';
|
|
7
|
+
*
|
|
8
|
+
* // Sync action (file-read, git-operation, etc.)
|
|
9
|
+
* const result = await executeAction('file-read', { filePath: '/foo' }, ctx);
|
|
10
|
+
* // result = { content: '...' }
|
|
11
|
+
*
|
|
12
|
+
* // Streaming action (claude-query, codex-query, etc.)
|
|
13
|
+
* const result = await executeAction('claude-query', params, {
|
|
14
|
+
* ...ctx,
|
|
15
|
+
* stream: (data) => ws.send(JSON.stringify({ type: 'relay-stream', requestId, data })),
|
|
16
|
+
* });
|
|
17
|
+
* // result = { exitCode: 0, sessionId: '...' }
|
|
18
|
+
*/
|
|
19
|
+
import claudeAgent from './claude.js';
|
|
20
|
+
import codexAgent from './codex.js';
|
|
21
|
+
import cursorAgent from './cursor.js';
|
|
22
|
+
import shellAgent from './shell.js';
|
|
23
|
+
import filesAgent from './files.js';
|
|
24
|
+
import gitAgent from './git.js';
|
|
25
|
+
import execAgent from './exec.js';
|
|
26
|
+
import detectAgent from './detect.js';
|
|
27
|
+
|
|
28
|
+
const agents = [claudeAgent, codexAgent, cursorAgent, shellAgent, filesAgent, gitAgent, execAgent, detectAgent];
|
|
29
|
+
|
|
30
|
+
/** Map of action name → handler function */
|
|
31
|
+
const actionMap = new Map();
|
|
32
|
+
|
|
33
|
+
/** Set of actions that use streaming (ctx.stream) instead of returning data */
|
|
34
|
+
const streamingActions = new Set(['claude-query', 'claude-task-query', 'codex-query', 'cursor-query']);
|
|
35
|
+
|
|
36
|
+
for (const agent of agents) {
|
|
37
|
+
for (const [action, handler] of Object.entries(agent.actions)) {
|
|
38
|
+
actionMap.set(action, handler);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Execute an agent action.
|
|
44
|
+
* @param {string} action - Action name (e.g., 'file-read', 'claude-query')
|
|
45
|
+
* @param {object} params - Action parameters from the command payload
|
|
46
|
+
* @param {object} ctx - Execution context:
|
|
47
|
+
* - stream(data): Send streaming chunk (required for streaming actions)
|
|
48
|
+
* - requestId: Current request ID
|
|
49
|
+
* - trackProcess(id, entry): Register process for abort support
|
|
50
|
+
* - untrackProcess(id): Remove process from tracking
|
|
51
|
+
* - getPersistentShell(cwd): Get persistent shell instance (CLI only)
|
|
52
|
+
* - resolveBinary(name): Resolve binary path (CLI only)
|
|
53
|
+
* - localBinDir: Path to local node_modules/.bin (CLI only)
|
|
54
|
+
* - streamMode: 'structured' | 'simple' (claude agent only)
|
|
55
|
+
* - log(msg): Logging function
|
|
56
|
+
* @returns {Promise<object>} Action result
|
|
57
|
+
*/
|
|
58
|
+
export async function executeAction(action, params, ctx = {}) {
|
|
59
|
+
const handler = actionMap.get(action);
|
|
60
|
+
if (!handler) throw new Error(`Unknown action: ${action}`);
|
|
61
|
+
return handler(params, ctx);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Check if an action uses streaming.
|
|
66
|
+
* Streaming actions require ctx.stream() and return { exitCode, ... } via Promise.
|
|
67
|
+
* Non-streaming actions return { data } directly.
|
|
68
|
+
*/
|
|
69
|
+
export function isStreamingAction(action) {
|
|
70
|
+
return streamingActions.has(action);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Check if an action is registered.
|
|
75
|
+
*/
|
|
76
|
+
export function hasAction(action) {
|
|
77
|
+
return actionMap.has(action);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Get all registered action names.
|
|
82
|
+
*/
|
|
83
|
+
export function getActionNames() {
|
|
84
|
+
return Array.from(actionMap.keys());
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export { agents };
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shell Agent
|
|
3
|
+
* Handles: shell-command
|
|
4
|
+
*
|
|
5
|
+
* Two execution modes:
|
|
6
|
+
* - Persistent shell (connect.js) — commands share one shell process
|
|
7
|
+
* - Simple exec (relay-client.js) — each command spawns a new process
|
|
8
|
+
*
|
|
9
|
+
* The caller provides the execution strategy via ctx.execShell or falls back to execCommand.
|
|
10
|
+
*/
|
|
11
|
+
import { execCommand, DANGEROUS_SHELL_PATTERNS } from './utils.js';
|
|
12
|
+
|
|
13
|
+
export default {
|
|
14
|
+
name: 'shell',
|
|
15
|
+
actions: {
|
|
16
|
+
'shell-command': async (params, ctx) => {
|
|
17
|
+
const { command, cwd } = params;
|
|
18
|
+
if (!command || typeof command !== 'string') throw new Error('Invalid command');
|
|
19
|
+
|
|
20
|
+
// Block dangerous shell patterns
|
|
21
|
+
const cmdLower = command.toLowerCase();
|
|
22
|
+
if (DANGEROUS_SHELL_PATTERNS.some(d => cmdLower.includes(d.toLowerCase()))) {
|
|
23
|
+
throw new Error('Command blocked for safety');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Use persistent shell if provided (connect.js), otherwise simple exec
|
|
27
|
+
if (ctx.getPersistentShell) {
|
|
28
|
+
const shell = ctx.getPersistentShell(cwd || process.cwd());
|
|
29
|
+
const result = await shell.exec(command, { timeoutMs: 60000 });
|
|
30
|
+
return { stdout: result.stdout, stderr: result.stderr, exitCode: result.exitCode, cwd: result.cwd };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Simple exec fallback
|
|
34
|
+
const result = await execCommand(command, [], { cwd: cwd || process.cwd(), timeout: 60000 });
|
|
35
|
+
return { stdout: result };
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
};
|