teleportation-cli 1.0.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/.claude/hooks/config-loader.mjs +93 -0
- package/.claude/hooks/heartbeat.mjs +331 -0
- package/.claude/hooks/notification.mjs +35 -0
- package/.claude/hooks/permission_request.mjs +307 -0
- package/.claude/hooks/post_tool_use.mjs +137 -0
- package/.claude/hooks/pre_tool_use.mjs +451 -0
- package/.claude/hooks/session-register.mjs +274 -0
- package/.claude/hooks/session_end.mjs +256 -0
- package/.claude/hooks/session_start.mjs +308 -0
- package/.claude/hooks/stop.mjs +277 -0
- package/.claude/hooks/user_prompt_submit.mjs +91 -0
- package/LICENSE +21 -0
- package/README.md +243 -0
- package/lib/auth/api-key.js +110 -0
- package/lib/auth/credentials.js +341 -0
- package/lib/backup/manager.js +461 -0
- package/lib/cli/daemon-commands.js +299 -0
- package/lib/cli/index.js +303 -0
- package/lib/cli/session-commands.js +294 -0
- package/lib/cli/snapshot-commands.js +223 -0
- package/lib/cli/worktree-commands.js +291 -0
- package/lib/config/manager.js +306 -0
- package/lib/daemon/lifecycle.js +336 -0
- package/lib/daemon/pid-manager.js +160 -0
- package/lib/daemon/teleportation-daemon.js +2009 -0
- package/lib/handoff/config.js +102 -0
- package/lib/handoff/example.js +152 -0
- package/lib/handoff/git-handoff.js +351 -0
- package/lib/handoff/handoff.js +277 -0
- package/lib/handoff/index.js +25 -0
- package/lib/handoff/session-state.js +238 -0
- package/lib/install/installer.js +555 -0
- package/lib/machine-coders/claude-code-adapter.js +329 -0
- package/lib/machine-coders/example.js +239 -0
- package/lib/machine-coders/gemini-cli-adapter.js +406 -0
- package/lib/machine-coders/index.js +103 -0
- package/lib/machine-coders/interface.js +168 -0
- package/lib/router/classifier.js +251 -0
- package/lib/router/example.js +92 -0
- package/lib/router/index.js +69 -0
- package/lib/router/mech-llms-client.js +277 -0
- package/lib/router/models.js +188 -0
- package/lib/router/router.js +382 -0
- package/lib/session/cleanup.js +100 -0
- package/lib/session/metadata.js +258 -0
- package/lib/session/mute-checker.js +114 -0
- package/lib/session-registry/manager.js +302 -0
- package/lib/snapshot/manager.js +390 -0
- package/lib/utils/errors.js +166 -0
- package/lib/utils/logger.js +148 -0
- package/lib/utils/retry.js +155 -0
- package/lib/worktree/manager.js +301 -0
- package/package.json +66 -0
- package/teleportation-cli.cjs +2987 -0
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gemini CLI Adapter
|
|
3
|
+
*
|
|
4
|
+
* Adapter for Gemini CLI using wrapper-based approval.
|
|
5
|
+
*
|
|
6
|
+
* NOTE: Gemini CLI Hooks v1 (Issue #9070) is proposed but not yet fully
|
|
7
|
+
* implemented in v0.19.x. The hooks in .gemini/hooks/ are ready for when
|
|
8
|
+
* native hooks become available. Until then, we use wrapper mode.
|
|
9
|
+
*
|
|
10
|
+
* Current approach (WRAPPER MODE):
|
|
11
|
+
* - Uses --output-format stream-json for real-time event parsing
|
|
12
|
+
* - Parses tool_use events from stream
|
|
13
|
+
* - Can kill process if tool is denied (tool may have started)
|
|
14
|
+
* - Approval callback invoked when tool_use event is detected
|
|
15
|
+
*
|
|
16
|
+
* Future approach (NATIVE HOOKS - when available):
|
|
17
|
+
* - Configure .gemini/settings.json with hooks.BeforeTool
|
|
18
|
+
* - True pre-execution approval like Claude Code
|
|
19
|
+
* - Hooks in .gemini/hooks/ ready to use
|
|
20
|
+
*
|
|
21
|
+
* Key characteristics:
|
|
22
|
+
* - Supports --yolo for auto-approve mode
|
|
23
|
+
* - 1M token context window (great for large repos)
|
|
24
|
+
*
|
|
25
|
+
* @see .gemini/hooks/ for hook implementations (ready for native hooks)
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import { spawn } from 'child_process';
|
|
29
|
+
import { promisify } from 'util';
|
|
30
|
+
import { exec } from 'child_process';
|
|
31
|
+
import { createInterface } from 'readline';
|
|
32
|
+
import { MachineCoder, CODER_NAMES } from './interface.js';
|
|
33
|
+
|
|
34
|
+
const execAsync = promisify(exec);
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Check if a command exists on the system
|
|
38
|
+
* @param {string} command
|
|
39
|
+
* @returns {Promise<boolean>}
|
|
40
|
+
*/
|
|
41
|
+
async function commandExists(command) {
|
|
42
|
+
try {
|
|
43
|
+
await execAsync(`which ${command}`);
|
|
44
|
+
return true;
|
|
45
|
+
} catch {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Gemini CLI Adapter
|
|
52
|
+
*/
|
|
53
|
+
export class GeminiCliAdapter extends MachineCoder {
|
|
54
|
+
name = CODER_NAMES.GEMINI_CLI;
|
|
55
|
+
displayName = 'Gemini CLI';
|
|
56
|
+
|
|
57
|
+
// Track running processes for stop()
|
|
58
|
+
#runningProcesses = new Map();
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Check if Gemini CLI is available
|
|
62
|
+
* @returns {Promise<boolean>}
|
|
63
|
+
*/
|
|
64
|
+
async isAvailable() {
|
|
65
|
+
return commandExists('gemini');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Get Gemini CLI status
|
|
70
|
+
* @returns {Promise<Object>}
|
|
71
|
+
*/
|
|
72
|
+
async getStatus() {
|
|
73
|
+
const available = await this.isAvailable();
|
|
74
|
+
|
|
75
|
+
if (!available) {
|
|
76
|
+
return { available: false };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
const { stdout } = await execAsync('gemini --version');
|
|
81
|
+
return {
|
|
82
|
+
available: true,
|
|
83
|
+
version: stdout.trim(),
|
|
84
|
+
models: [
|
|
85
|
+
'gemini-2.5-flash',
|
|
86
|
+
'gemini-2.5-flash-lite',
|
|
87
|
+
'gemini-2.5-pro',
|
|
88
|
+
'gemini-3-pro-preview',
|
|
89
|
+
],
|
|
90
|
+
};
|
|
91
|
+
} catch {
|
|
92
|
+
return { available: true };
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Execute a task with Gemini CLI
|
|
98
|
+
*
|
|
99
|
+
* Uses streaming JSON output to parse events in real-time.
|
|
100
|
+
* Can intercept tool_use events for approval.
|
|
101
|
+
*
|
|
102
|
+
* @param {Object} options
|
|
103
|
+
* @returns {Promise<Object>}
|
|
104
|
+
*/
|
|
105
|
+
async execute(options) {
|
|
106
|
+
const {
|
|
107
|
+
projectPath,
|
|
108
|
+
sessionId,
|
|
109
|
+
prompt,
|
|
110
|
+
onToolCall,
|
|
111
|
+
onProgress,
|
|
112
|
+
timeoutMs = 600000, // 10 min default
|
|
113
|
+
autoApprove = false,
|
|
114
|
+
model,
|
|
115
|
+
} = options;
|
|
116
|
+
|
|
117
|
+
const executionId = `gemini-${Date.now()}`;
|
|
118
|
+
const args = [];
|
|
119
|
+
|
|
120
|
+
// Add prompt
|
|
121
|
+
args.push('--prompt', prompt);
|
|
122
|
+
|
|
123
|
+
// Streaming JSON output for event parsing
|
|
124
|
+
args.push('--output-format', 'stream-json');
|
|
125
|
+
|
|
126
|
+
// Model selection
|
|
127
|
+
if (model) {
|
|
128
|
+
args.push('--model', model);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Auto-approve mode
|
|
132
|
+
if (autoApprove) {
|
|
133
|
+
args.push('--yolo');
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return new Promise((resolve, reject) => {
|
|
137
|
+
const startTime = Date.now();
|
|
138
|
+
const toolCalls = [];
|
|
139
|
+
let finalOutput = '';
|
|
140
|
+
let finalStats = {};
|
|
141
|
+
let geminiSessionId = null;
|
|
142
|
+
let processKilled = false; // Flag to prevent race condition after kill
|
|
143
|
+
|
|
144
|
+
const proc = spawn('gemini', args, {
|
|
145
|
+
cwd: projectPath,
|
|
146
|
+
env: {
|
|
147
|
+
...process.env,
|
|
148
|
+
// Pass session ID for potential future hook integration
|
|
149
|
+
TELEPORTATION_SESSION_ID: sessionId,
|
|
150
|
+
},
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
this.#runningProcesses.set(executionId, proc);
|
|
154
|
+
|
|
155
|
+
// Timeout handling
|
|
156
|
+
const timeout = setTimeout(() => {
|
|
157
|
+
proc.kill('SIGTERM');
|
|
158
|
+
reject(new Error(`Execution timed out after ${timeoutMs}ms`));
|
|
159
|
+
}, timeoutMs);
|
|
160
|
+
|
|
161
|
+
// Parse JSONL stream line by line
|
|
162
|
+
const rl = createInterface({
|
|
163
|
+
input: proc.stdout,
|
|
164
|
+
crlfDelay: Infinity,
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
rl.on('line', async (line) => {
|
|
168
|
+
if (!line.trim()) return;
|
|
169
|
+
if (processKilled) return; // Skip processing if process was killed
|
|
170
|
+
|
|
171
|
+
try {
|
|
172
|
+
const event = JSON.parse(line);
|
|
173
|
+
|
|
174
|
+
// Handle different event types
|
|
175
|
+
switch (event.type) {
|
|
176
|
+
case 'init':
|
|
177
|
+
geminiSessionId = event.session_id;
|
|
178
|
+
onProgress?.({
|
|
179
|
+
type: 'init',
|
|
180
|
+
timestamp: Date.now(),
|
|
181
|
+
data: {
|
|
182
|
+
sessionId: event.session_id,
|
|
183
|
+
model: event.model,
|
|
184
|
+
},
|
|
185
|
+
});
|
|
186
|
+
break;
|
|
187
|
+
|
|
188
|
+
case 'message':
|
|
189
|
+
if (event.role === 'assistant') {
|
|
190
|
+
finalOutput = event.content || finalOutput;
|
|
191
|
+
}
|
|
192
|
+
onProgress?.({
|
|
193
|
+
type: 'message',
|
|
194
|
+
timestamp: Date.now(),
|
|
195
|
+
data: {
|
|
196
|
+
role: event.role,
|
|
197
|
+
content: event.content,
|
|
198
|
+
},
|
|
199
|
+
});
|
|
200
|
+
break;
|
|
201
|
+
|
|
202
|
+
case 'tool_use':
|
|
203
|
+
const toolCall = {
|
|
204
|
+
id: event.tool_id || `tool-${toolCalls.length}`,
|
|
205
|
+
tool: event.tool_name,
|
|
206
|
+
input: event.parameters,
|
|
207
|
+
timestamp: Date.now(),
|
|
208
|
+
};
|
|
209
|
+
toolCalls.push(toolCall);
|
|
210
|
+
|
|
211
|
+
onProgress?.({
|
|
212
|
+
type: 'tool_use',
|
|
213
|
+
timestamp: Date.now(),
|
|
214
|
+
data: toolCall,
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
// Check approval if callback provided and not in auto-approve mode
|
|
218
|
+
if (onToolCall && !autoApprove) {
|
|
219
|
+
try {
|
|
220
|
+
const decision = await onToolCall(toolCall);
|
|
221
|
+
|
|
222
|
+
if (decision === 'deny') {
|
|
223
|
+
// Kill the process - tool was denied
|
|
224
|
+
processKilled = true; // Set flag to prevent race condition
|
|
225
|
+
proc.kill('SIGTERM');
|
|
226
|
+
clearTimeout(timeout);
|
|
227
|
+
this.#runningProcesses.delete(executionId);
|
|
228
|
+
|
|
229
|
+
resolve({
|
|
230
|
+
success: false,
|
|
231
|
+
output: finalOutput,
|
|
232
|
+
error: `Tool '${toolCall.tool}' was denied`,
|
|
233
|
+
toolCalls,
|
|
234
|
+
stats: {
|
|
235
|
+
durationMs: Date.now() - startTime,
|
|
236
|
+
model: finalStats.model,
|
|
237
|
+
},
|
|
238
|
+
executionId,
|
|
239
|
+
geminiSessionId,
|
|
240
|
+
});
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
} catch (err) {
|
|
244
|
+
console.error(`[gemini-adapter] Error checking tool approval: ${err.message}`);
|
|
245
|
+
// Continue on error - fail-safe to allow
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
break;
|
|
249
|
+
|
|
250
|
+
case 'tool_result':
|
|
251
|
+
onProgress?.({
|
|
252
|
+
type: 'tool_result',
|
|
253
|
+
timestamp: Date.now(),
|
|
254
|
+
data: {
|
|
255
|
+
toolId: event.tool_id,
|
|
256
|
+
status: event.status,
|
|
257
|
+
output: event.output,
|
|
258
|
+
},
|
|
259
|
+
});
|
|
260
|
+
break;
|
|
261
|
+
|
|
262
|
+
case 'error':
|
|
263
|
+
onProgress?.({
|
|
264
|
+
type: 'error',
|
|
265
|
+
timestamp: Date.now(),
|
|
266
|
+
data: {
|
|
267
|
+
message: event.message || event.error,
|
|
268
|
+
},
|
|
269
|
+
});
|
|
270
|
+
break;
|
|
271
|
+
|
|
272
|
+
case 'result':
|
|
273
|
+
// Final result with stats
|
|
274
|
+
finalStats = {
|
|
275
|
+
tokensUsed: event.stats?.total_tokens,
|
|
276
|
+
inputTokens: event.stats?.input_tokens,
|
|
277
|
+
outputTokens: event.stats?.output_tokens,
|
|
278
|
+
durationMs: event.stats?.duration_ms,
|
|
279
|
+
toolCallCount: event.stats?.tool_calls,
|
|
280
|
+
model: event.stats?.model,
|
|
281
|
+
};
|
|
282
|
+
onProgress?.({
|
|
283
|
+
type: 'done',
|
|
284
|
+
timestamp: Date.now(),
|
|
285
|
+
data: finalStats,
|
|
286
|
+
});
|
|
287
|
+
break;
|
|
288
|
+
}
|
|
289
|
+
} catch (err) {
|
|
290
|
+
// Not valid JSON - might be raw output
|
|
291
|
+
onProgress?.({
|
|
292
|
+
type: 'output',
|
|
293
|
+
timestamp: Date.now(),
|
|
294
|
+
data: { text: line },
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
let stderr = '';
|
|
300
|
+
proc.stderr.on('data', (data) => {
|
|
301
|
+
stderr += data.toString();
|
|
302
|
+
onProgress?.({
|
|
303
|
+
type: 'stderr',
|
|
304
|
+
timestamp: Date.now(),
|
|
305
|
+
data: { text: data.toString() },
|
|
306
|
+
});
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
proc.on('close', (code) => {
|
|
310
|
+
clearTimeout(timeout);
|
|
311
|
+
this.#runningProcesses.delete(executionId);
|
|
312
|
+
|
|
313
|
+
const durationMs = Date.now() - startTime;
|
|
314
|
+
|
|
315
|
+
resolve({
|
|
316
|
+
success: code === 0,
|
|
317
|
+
output: finalOutput,
|
|
318
|
+
error: code !== 0 ? stderr || `Exit code: ${code}` : undefined,
|
|
319
|
+
toolCalls,
|
|
320
|
+
stats: {
|
|
321
|
+
...finalStats,
|
|
322
|
+
durationMs: finalStats.durationMs || durationMs,
|
|
323
|
+
},
|
|
324
|
+
executionId,
|
|
325
|
+
geminiSessionId,
|
|
326
|
+
});
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
proc.on('error', (err) => {
|
|
330
|
+
clearTimeout(timeout);
|
|
331
|
+
this.#runningProcesses.delete(executionId);
|
|
332
|
+
reject(err);
|
|
333
|
+
});
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Resume a Gemini session with a new prompt
|
|
339
|
+
* @param {string} sessionId - Gemini session ID to resume
|
|
340
|
+
* @param {string} prompt - New prompt
|
|
341
|
+
* @param {Object} options - Additional options
|
|
342
|
+
* @returns {Promise<Object>}
|
|
343
|
+
*/
|
|
344
|
+
async resume(sessionId, prompt, options = {}) {
|
|
345
|
+
const {
|
|
346
|
+
projectPath = process.cwd(),
|
|
347
|
+
onToolCall,
|
|
348
|
+
onProgress,
|
|
349
|
+
timeoutMs = 600000,
|
|
350
|
+
autoApprove = false,
|
|
351
|
+
model,
|
|
352
|
+
} = options;
|
|
353
|
+
|
|
354
|
+
const executionId = `gemini-resume-${Date.now()}`;
|
|
355
|
+
const args = ['--resume', sessionId, '--prompt', prompt, '--output-format', 'stream-json'];
|
|
356
|
+
|
|
357
|
+
if (model) {
|
|
358
|
+
args.push('--model', model);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
if (autoApprove) {
|
|
362
|
+
args.push('--yolo');
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// Reuse execute logic with resume flag
|
|
366
|
+
return this.execute({
|
|
367
|
+
...options,
|
|
368
|
+
projectPath,
|
|
369
|
+
prompt: `--resume ${sessionId} ${prompt}`, // Hack: will be overridden by args
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Stop a running execution
|
|
375
|
+
* @param {string} executionId
|
|
376
|
+
* @returns {Promise<boolean>}
|
|
377
|
+
*/
|
|
378
|
+
async stop(executionId) {
|
|
379
|
+
const proc = this.#runningProcesses.get(executionId);
|
|
380
|
+
if (proc) {
|
|
381
|
+
proc.kill('SIGTERM');
|
|
382
|
+
this.#runningProcesses.delete(executionId);
|
|
383
|
+
return true;
|
|
384
|
+
}
|
|
385
|
+
return false;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Get capabilities
|
|
390
|
+
* @returns {Object}
|
|
391
|
+
*/
|
|
392
|
+
getCapabilities() {
|
|
393
|
+
return {
|
|
394
|
+
supportsResume: true,
|
|
395
|
+
supportsStreaming: true,
|
|
396
|
+
hasNativeApproval: false, // Native hooks proposed (Issue #9070) but not yet in v0.19.x
|
|
397
|
+
supportsModelSelection: true,
|
|
398
|
+
maxContextTokens: 1000000, // 1M tokens!
|
|
399
|
+
supportedTools: ['Bash', 'Read', 'Edit', 'Write', 'Glob', 'Grep', 'LS', 'WebSearch', 'WebFetch'],
|
|
400
|
+
// Hooks ready in .gemini/hooks/ for when native hooks become available
|
|
401
|
+
pendingNativeHooks: ['BeforeTool', 'AfterTool', 'SessionStart', 'SessionEnd'],
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
export default GeminiCliAdapter;
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Machine Coders Module
|
|
3
|
+
*
|
|
4
|
+
* Unified interface for different AI coding assistants:
|
|
5
|
+
* - Claude Code (via hooks)
|
|
6
|
+
* - Gemini CLI (via wrapper + streaming)
|
|
7
|
+
* - Goose CLI (via recipes)
|
|
8
|
+
*
|
|
9
|
+
* @module lib/machine-coders
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export { MachineCoder, CODER_NAMES } from './interface.js';
|
|
13
|
+
export { ClaudeCodeAdapter } from './claude-code-adapter.js';
|
|
14
|
+
export { GeminiCliAdapter } from './gemini-cli-adapter.js';
|
|
15
|
+
// export { GooseCliAdapter } from './goose-cli-adapter.js'; // TODO: implement
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Get a machine coder adapter by name
|
|
19
|
+
* @param {string} name - 'claude-code' | 'gemini-cli' | 'goose-cli'
|
|
20
|
+
* @returns {MachineCoder}
|
|
21
|
+
*/
|
|
22
|
+
export function getMachineCoder(name) {
|
|
23
|
+
switch (name) {
|
|
24
|
+
case 'claude-code':
|
|
25
|
+
const { ClaudeCodeAdapter } = require('./claude-code-adapter.js');
|
|
26
|
+
return new ClaudeCodeAdapter();
|
|
27
|
+
case 'gemini-cli':
|
|
28
|
+
const { GeminiCliAdapter } = require('./gemini-cli-adapter.js');
|
|
29
|
+
return new GeminiCliAdapter();
|
|
30
|
+
// case 'goose-cli':
|
|
31
|
+
// const { GooseCliAdapter } = require('./goose-cli-adapter.js');
|
|
32
|
+
// return new GooseCliAdapter();
|
|
33
|
+
default:
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Get all available machine coders on this system
|
|
40
|
+
* @returns {Promise<MachineCoder[]>}
|
|
41
|
+
*/
|
|
42
|
+
export async function getAvailableCoders() {
|
|
43
|
+
const { ClaudeCodeAdapter } = await import('./claude-code-adapter.js');
|
|
44
|
+
const { GeminiCliAdapter } = await import('./gemini-cli-adapter.js');
|
|
45
|
+
|
|
46
|
+
const coders = [
|
|
47
|
+
new ClaudeCodeAdapter(),
|
|
48
|
+
new GeminiCliAdapter(),
|
|
49
|
+
];
|
|
50
|
+
|
|
51
|
+
const available = [];
|
|
52
|
+
for (const coder of coders) {
|
|
53
|
+
if (await coder.isAvailable()) {
|
|
54
|
+
available.push(coder);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return available;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Get the best available machine coder for a task
|
|
63
|
+
* Priority: Claude Code > Gemini CLI > Goose CLI
|
|
64
|
+
*
|
|
65
|
+
* @param {Object} [preferences] - Optional preferences
|
|
66
|
+
* @param {string} [preferences.preferred] - Preferred coder name
|
|
67
|
+
* @param {boolean} [preferences.largeContext] - Prefer large context (Gemini)
|
|
68
|
+
* @returns {Promise<MachineCoder|null>}
|
|
69
|
+
*/
|
|
70
|
+
export async function getBestCoder(preferences = {}) {
|
|
71
|
+
const available = await getAvailableCoders();
|
|
72
|
+
|
|
73
|
+
if (available.length === 0) {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// If preferred coder is specified and available, use it
|
|
78
|
+
if (preferences.preferred) {
|
|
79
|
+
const preferred = available.find(c => c.name === preferences.preferred);
|
|
80
|
+
if (preferred) return preferred;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// If large context needed, prefer Gemini
|
|
84
|
+
if (preferences.largeContext) {
|
|
85
|
+
const gemini = available.find(c => c.name === 'gemini-cli');
|
|
86
|
+
if (gemini) return gemini;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Default priority: Claude Code first
|
|
90
|
+
const priority = ['claude-code', 'gemini-cli', 'goose-cli'];
|
|
91
|
+
for (const name of priority) {
|
|
92
|
+
const coder = available.find(c => c.name === name);
|
|
93
|
+
if (coder) return coder;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return available[0];
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export default {
|
|
100
|
+
getMachineCoder,
|
|
101
|
+
getAvailableCoders,
|
|
102
|
+
getBestCoder,
|
|
103
|
+
};
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Machine Coder Interface
|
|
3
|
+
*
|
|
4
|
+
* Defines the common interface that all machine coders must implement.
|
|
5
|
+
* This allows Teleportation to work with Claude Code, Gemini CLI, Goose CLI,
|
|
6
|
+
* and future coding assistants in a uniform way.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Known machine coder names
|
|
11
|
+
*/
|
|
12
|
+
export const CODER_NAMES = {
|
|
13
|
+
CLAUDE_CODE: 'claude-code',
|
|
14
|
+
GEMINI_CLI: 'gemini-cli',
|
|
15
|
+
GOOSE_CLI: 'goose-cli',
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Tool call event (when the coder wants to use a tool)
|
|
20
|
+
* @typedef {Object} ToolCall
|
|
21
|
+
* @property {string} id - Unique ID for this tool call
|
|
22
|
+
* @property {string} tool - Tool name (e.g., 'Bash', 'Edit', 'Read')
|
|
23
|
+
* @property {Object} input - Tool input parameters
|
|
24
|
+
* @property {number} timestamp - When the tool was called
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Progress event (streaming updates from the coder)
|
|
29
|
+
* @typedef {Object} ProgressEvent
|
|
30
|
+
* @property {string} type - Event type: 'init' | 'message' | 'tool_use' | 'tool_result' | 'error' | 'done'
|
|
31
|
+
* @property {number} timestamp - Event timestamp
|
|
32
|
+
* @property {Object} [data] - Event-specific data
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Execution options
|
|
37
|
+
* @typedef {Object} ExecuteOptions
|
|
38
|
+
* @property {string} projectPath - Path to the project directory
|
|
39
|
+
* @property {string} sessionId - Teleportation session ID
|
|
40
|
+
* @property {string} prompt - The task/prompt to execute
|
|
41
|
+
* @property {Function} [onToolCall] - Callback for tool approval: (tool: ToolCall) => Promise<'allow' | 'deny' | 'ask'>
|
|
42
|
+
* @property {Function} [onProgress] - Callback for progress events: (event: ProgressEvent) => void
|
|
43
|
+
* @property {number} [timeoutMs] - Execution timeout in milliseconds
|
|
44
|
+
* @property {boolean} [autoApprove] - Skip approval checks (dangerous)
|
|
45
|
+
* @property {string} [model] - Specific model to use (if supported)
|
|
46
|
+
*/
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Execution result
|
|
50
|
+
* @typedef {Object} ExecuteResult
|
|
51
|
+
* @property {boolean} success - Whether execution completed successfully
|
|
52
|
+
* @property {string} output - Final output/response
|
|
53
|
+
* @property {string} [error] - Error message if failed
|
|
54
|
+
* @property {ToolCall[]} toolCalls - List of tools that were called
|
|
55
|
+
* @property {Object} [stats] - Execution statistics
|
|
56
|
+
* @property {number} [stats.tokensUsed] - Total tokens used
|
|
57
|
+
* @property {number} [stats.cost] - Estimated cost in USD
|
|
58
|
+
* @property {number} [stats.durationMs] - Execution duration
|
|
59
|
+
* @property {string} [stats.model] - Model that was used
|
|
60
|
+
*/
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Coder status
|
|
64
|
+
* @typedef {Object} CoderStatus
|
|
65
|
+
* @property {boolean} available - Whether the coder is available
|
|
66
|
+
* @property {string} [version] - CLI version
|
|
67
|
+
* @property {string[]} [models] - Available models
|
|
68
|
+
* @property {Object} [config] - Current configuration
|
|
69
|
+
*/
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Base Machine Coder class
|
|
73
|
+
* All adapters should extend this class.
|
|
74
|
+
*/
|
|
75
|
+
export class MachineCoder {
|
|
76
|
+
/**
|
|
77
|
+
* Unique identifier for this coder
|
|
78
|
+
* @type {string}
|
|
79
|
+
*/
|
|
80
|
+
name = 'base';
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Human-readable display name
|
|
84
|
+
* @type {string}
|
|
85
|
+
*/
|
|
86
|
+
displayName = 'Base Machine Coder';
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Check if this coder is available on the system
|
|
90
|
+
* @returns {Promise<boolean>}
|
|
91
|
+
*/
|
|
92
|
+
async isAvailable() {
|
|
93
|
+
throw new Error('Not implemented');
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Get the current status of this coder
|
|
98
|
+
* @returns {Promise<CoderStatus>}
|
|
99
|
+
*/
|
|
100
|
+
async getStatus() {
|
|
101
|
+
return {
|
|
102
|
+
available: await this.isAvailable(),
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Execute a task/prompt
|
|
108
|
+
* @param {ExecuteOptions} options
|
|
109
|
+
* @returns {Promise<ExecuteResult>}
|
|
110
|
+
*/
|
|
111
|
+
async execute(options) {
|
|
112
|
+
throw new Error('Not implemented');
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Resume a previous session with a new prompt
|
|
117
|
+
* @param {string} sessionId - Session to resume
|
|
118
|
+
* @param {string} prompt - New prompt/task
|
|
119
|
+
* @param {ExecuteOptions} [options] - Additional options
|
|
120
|
+
* @returns {Promise<ExecuteResult>}
|
|
121
|
+
*/
|
|
122
|
+
async resume(sessionId, prompt, options = {}) {
|
|
123
|
+
// Default implementation: just execute with the prompt
|
|
124
|
+
// Subclasses can override for proper session resume
|
|
125
|
+
return this.execute({
|
|
126
|
+
...options,
|
|
127
|
+
sessionId,
|
|
128
|
+
prompt,
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Stop/kill a running execution
|
|
134
|
+
* @param {string} executionId - Execution to stop
|
|
135
|
+
* @returns {Promise<boolean>}
|
|
136
|
+
*/
|
|
137
|
+
async stop(executionId) {
|
|
138
|
+
throw new Error('Not implemented');
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Get capabilities of this coder
|
|
143
|
+
* @returns {Object}
|
|
144
|
+
*/
|
|
145
|
+
getCapabilities() {
|
|
146
|
+
return {
|
|
147
|
+
// Whether this coder supports session resume
|
|
148
|
+
supportsResume: false,
|
|
149
|
+
|
|
150
|
+
// Whether this coder supports streaming output
|
|
151
|
+
supportsStreaming: false,
|
|
152
|
+
|
|
153
|
+
// Whether this coder has native approval hooks
|
|
154
|
+
hasNativeApproval: false,
|
|
155
|
+
|
|
156
|
+
// Whether this coder supports model selection
|
|
157
|
+
supportsModelSelection: false,
|
|
158
|
+
|
|
159
|
+
// Maximum context size (tokens)
|
|
160
|
+
maxContextTokens: 0,
|
|
161
|
+
|
|
162
|
+
// Supported tools
|
|
163
|
+
supportedTools: [],
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export default MachineCoder;
|