@automagik/genie 0.260201.2240
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/.github/workflows/publish.yml +26 -0
- package/.worktrees/.metadata.json +3 -0
- package/README.md +532 -0
- package/bun.lock +101 -0
- package/dist/claudio.js +76 -0
- package/dist/genie.js +201 -0
- package/dist/term.js +136 -0
- package/install.sh +351 -0
- package/package.json +37 -0
- package/scripts/version.ts +48 -0
- package/src/claudio.ts +128 -0
- package/src/commands/launch.ts +245 -0
- package/src/commands/models.ts +43 -0
- package/src/commands/profiles.ts +95 -0
- package/src/commands/setup.ts +5 -0
- package/src/genie-commands/hooks.ts +317 -0
- package/src/genie-commands/install.ts +351 -0
- package/src/genie-commands/setup.ts +282 -0
- package/src/genie-commands/shortcuts.ts +62 -0
- package/src/genie-commands/update.ts +228 -0
- package/src/genie.ts +106 -0
- package/src/lib/api-client.ts +109 -0
- package/src/lib/claude-settings.ts +252 -0
- package/src/lib/config.ts +109 -0
- package/src/lib/genie-config.ts +164 -0
- package/src/lib/hook-manager.ts +130 -0
- package/src/lib/hook-script.ts +256 -0
- package/src/lib/hooks/compose.ts +72 -0
- package/src/lib/hooks/index.ts +163 -0
- package/src/lib/hooks/presets/audited.ts +191 -0
- package/src/lib/hooks/presets/collaborative.ts +143 -0
- package/src/lib/hooks/presets/sandboxed.ts +153 -0
- package/src/lib/hooks/presets/supervised.ts +66 -0
- package/src/lib/hooks/utils/escape.ts +46 -0
- package/src/lib/log-reader.ts +213 -0
- package/src/lib/picker.ts +62 -0
- package/src/lib/session-metadata.ts +58 -0
- package/src/lib/system-detect.ts +185 -0
- package/src/lib/tmux.ts +410 -0
- package/src/lib/version.ts +15 -0
- package/src/lib/wizard.ts +104 -0
- package/src/lib/worktree.ts +362 -0
- package/src/term-commands/attach.ts +23 -0
- package/src/term-commands/exec.ts +34 -0
- package/src/term-commands/hook.ts +42 -0
- package/src/term-commands/ls.ts +33 -0
- package/src/term-commands/new.ts +73 -0
- package/src/term-commands/pane.ts +81 -0
- package/src/term-commands/read.ts +70 -0
- package/src/term-commands/rm.ts +47 -0
- package/src/term-commands/send.ts +34 -0
- package/src/term-commands/shortcuts.ts +355 -0
- package/src/term-commands/split.ts +87 -0
- package/src/term-commands/status.ts +116 -0
- package/src/term-commands/window.ts +72 -0
- package/src/term.ts +192 -0
- package/src/types/config.ts +17 -0
- package/src/types/genie-config.ts +104 -0
- package/tsconfig.json +17 -0
package/src/lib/tmux.ts
ADDED
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
import { exec as execCallback } from "child_process";
|
|
2
|
+
import { promisify } from "util";
|
|
3
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
4
|
+
|
|
5
|
+
const exec = promisify(execCallback);
|
|
6
|
+
|
|
7
|
+
// Basic interfaces for tmux objects
|
|
8
|
+
export interface TmuxSession {
|
|
9
|
+
id: string;
|
|
10
|
+
name: string;
|
|
11
|
+
attached: boolean;
|
|
12
|
+
windows: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface TmuxWindow {
|
|
16
|
+
id: string;
|
|
17
|
+
name: string;
|
|
18
|
+
active: boolean;
|
|
19
|
+
sessionId: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface TmuxPane {
|
|
23
|
+
id: string;
|
|
24
|
+
windowId: string;
|
|
25
|
+
active: boolean;
|
|
26
|
+
title: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
interface CommandExecution {
|
|
30
|
+
id: string;
|
|
31
|
+
paneId: string;
|
|
32
|
+
command: string;
|
|
33
|
+
status: 'pending' | 'completed' | 'error';
|
|
34
|
+
startTime: Date;
|
|
35
|
+
result?: string;
|
|
36
|
+
exitCode?: number;
|
|
37
|
+
rawMode?: boolean;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export type ShellType = 'bash' | 'zsh' | 'fish';
|
|
41
|
+
|
|
42
|
+
let shellConfig: { type: ShellType } = { type: 'bash' };
|
|
43
|
+
|
|
44
|
+
export function setShellConfig(config: { type: string }): void {
|
|
45
|
+
// Validate shell type
|
|
46
|
+
const validShells: ShellType[] = ['bash', 'zsh', 'fish'];
|
|
47
|
+
|
|
48
|
+
if (validShells.includes(config.type as ShellType)) {
|
|
49
|
+
shellConfig = { type: config.type as ShellType };
|
|
50
|
+
} else {
|
|
51
|
+
shellConfig = { type: 'bash' };
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Execute a tmux command and return the result
|
|
57
|
+
*/
|
|
58
|
+
export async function executeTmux(tmuxCommand: string): Promise<string> {
|
|
59
|
+
try {
|
|
60
|
+
const { stdout } = await exec(`tmux ${tmuxCommand}`);
|
|
61
|
+
return stdout.trim();
|
|
62
|
+
} catch (error: any) {
|
|
63
|
+
throw new Error(`Failed to execute tmux command: ${error.message}`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Check if tmux server is running
|
|
69
|
+
*/
|
|
70
|
+
export async function isTmuxRunning(): Promise<boolean> {
|
|
71
|
+
try {
|
|
72
|
+
await executeTmux("list-sessions -F '#{session_name}'");
|
|
73
|
+
return true;
|
|
74
|
+
} catch (error) {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* List all tmux sessions
|
|
81
|
+
*/
|
|
82
|
+
export async function listSessions(): Promise<TmuxSession[]> {
|
|
83
|
+
try {
|
|
84
|
+
const format = "#{session_id}:#{session_name}:#{?session_attached,1,0}:#{session_windows}";
|
|
85
|
+
const output = await executeTmux(`list-sessions -F '${format}'`);
|
|
86
|
+
|
|
87
|
+
if (!output) return [];
|
|
88
|
+
|
|
89
|
+
return output.split('\n').map(line => {
|
|
90
|
+
const [id, name, attached, windows] = line.split(':');
|
|
91
|
+
return {
|
|
92
|
+
id,
|
|
93
|
+
name,
|
|
94
|
+
attached: attached === '1',
|
|
95
|
+
windows: parseInt(windows, 10)
|
|
96
|
+
};
|
|
97
|
+
});
|
|
98
|
+
} catch (error: any) {
|
|
99
|
+
// Handle "no server running" gracefully
|
|
100
|
+
if (error.message.includes('no server running')) {
|
|
101
|
+
return [];
|
|
102
|
+
}
|
|
103
|
+
throw error;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Find a session by name
|
|
109
|
+
*/
|
|
110
|
+
export async function findSessionByName(name: string): Promise<TmuxSession | null> {
|
|
111
|
+
try {
|
|
112
|
+
const sessions = await listSessions();
|
|
113
|
+
return sessions.find(session => session.name === name) || null;
|
|
114
|
+
} catch (error) {
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* List windows in a session
|
|
121
|
+
*/
|
|
122
|
+
export async function listWindows(sessionId: string): Promise<TmuxWindow[]> {
|
|
123
|
+
try {
|
|
124
|
+
const format = "#{window_id}:#{window_name}:#{?window_active,1,0}";
|
|
125
|
+
const output = await executeTmux(`list-windows -t '${sessionId}' -F '${format}'`);
|
|
126
|
+
|
|
127
|
+
if (!output) return [];
|
|
128
|
+
|
|
129
|
+
return output.split('\n').map(line => {
|
|
130
|
+
const [id, name, active] = line.split(':');
|
|
131
|
+
return {
|
|
132
|
+
id,
|
|
133
|
+
name,
|
|
134
|
+
active: active === '1',
|
|
135
|
+
sessionId
|
|
136
|
+
};
|
|
137
|
+
});
|
|
138
|
+
} catch (error: any) {
|
|
139
|
+
// Handle session not found or no server running
|
|
140
|
+
if (error.message.includes('no server running') || error.message.includes('session not found')) {
|
|
141
|
+
return [];
|
|
142
|
+
}
|
|
143
|
+
throw error;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* List panes in a window
|
|
149
|
+
*/
|
|
150
|
+
export async function listPanes(windowId: string): Promise<TmuxPane[]> {
|
|
151
|
+
try {
|
|
152
|
+
const format = "#{pane_id}:#{pane_title}:#{?pane_active,1,0}";
|
|
153
|
+
const output = await executeTmux(`list-panes -t '${windowId}' -F '${format}'`);
|
|
154
|
+
|
|
155
|
+
if (!output) return [];
|
|
156
|
+
|
|
157
|
+
return output.split('\n').map(line => {
|
|
158
|
+
const [id, title, active] = line.split(':');
|
|
159
|
+
return {
|
|
160
|
+
id,
|
|
161
|
+
windowId,
|
|
162
|
+
title: title,
|
|
163
|
+
active: active === '1'
|
|
164
|
+
};
|
|
165
|
+
});
|
|
166
|
+
} catch (error: any) {
|
|
167
|
+
// Handle window not found or no server running
|
|
168
|
+
if (error.message.includes('no server running') || error.message.includes('window not found')) {
|
|
169
|
+
return [];
|
|
170
|
+
}
|
|
171
|
+
throw error;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Capture content from a specific pane, by default the latest 200 lines.
|
|
177
|
+
*/
|
|
178
|
+
export async function capturePaneContent(paneId: string, lines: number = 200, includeColors: boolean = false): Promise<string> {
|
|
179
|
+
try {
|
|
180
|
+
const colorFlag = includeColors ? '-e' : '';
|
|
181
|
+
return await executeTmux(`capture-pane -p ${colorFlag} -t '${paneId}' -S -${lines} -E -`);
|
|
182
|
+
} catch (error: any) {
|
|
183
|
+
// Handle pane not found or no server running
|
|
184
|
+
if (error.message.includes('no server running') || error.message.includes('pane not found')) {
|
|
185
|
+
return '';
|
|
186
|
+
}
|
|
187
|
+
throw error;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Create a new tmux session
|
|
193
|
+
*/
|
|
194
|
+
export async function createSession(name: string): Promise<TmuxSession | null> {
|
|
195
|
+
await executeTmux(`new-session -d -s "${name}" -e LC_ALL=C.UTF-8 -e LANG=C.UTF-8`);
|
|
196
|
+
return findSessionByName(name);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Create a new window in a session
|
|
201
|
+
*/
|
|
202
|
+
export async function createWindow(sessionId: string, name: string): Promise<TmuxWindow | null> {
|
|
203
|
+
const output = await executeTmux(`new-window -t '${sessionId}' -n '${name}'`);
|
|
204
|
+
const windows = await listWindows(sessionId);
|
|
205
|
+
return windows.find(window => window.name === name) || null;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Kill a tmux session by ID
|
|
210
|
+
*/
|
|
211
|
+
export async function killSession(sessionId: string): Promise<void> {
|
|
212
|
+
await executeTmux(`kill-session -t '${sessionId}'`);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Kill a tmux window by ID
|
|
217
|
+
*/
|
|
218
|
+
export async function killWindow(windowId: string): Promise<void> {
|
|
219
|
+
await executeTmux(`kill-window -t '${windowId}'`);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Kill a tmux pane by ID
|
|
224
|
+
*/
|
|
225
|
+
export async function killPane(paneId: string): Promise<void> {
|
|
226
|
+
await executeTmux(`kill-pane -t '${paneId}'`);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Split a tmux pane horizontally or vertically
|
|
231
|
+
*/
|
|
232
|
+
export async function splitPane(
|
|
233
|
+
targetPaneId: string,
|
|
234
|
+
direction: 'horizontal' | 'vertical' = 'vertical',
|
|
235
|
+
size?: number
|
|
236
|
+
): Promise<TmuxPane | null> {
|
|
237
|
+
// Build the split-window command
|
|
238
|
+
let splitCommand = 'split-window';
|
|
239
|
+
|
|
240
|
+
// Add direction flag (-h for horizontal, -v for vertical)
|
|
241
|
+
if (direction === 'horizontal') {
|
|
242
|
+
splitCommand += ' -h';
|
|
243
|
+
} else {
|
|
244
|
+
splitCommand += ' -v';
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Add target pane
|
|
248
|
+
splitCommand += ` -t '${targetPaneId}'`;
|
|
249
|
+
|
|
250
|
+
// Add size if specified (as percentage)
|
|
251
|
+
if (size !== undefined && size > 0 && size < 100) {
|
|
252
|
+
splitCommand += ` -p ${size}`;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Execute the split command
|
|
256
|
+
await executeTmux(splitCommand);
|
|
257
|
+
|
|
258
|
+
// Get the window ID from the target pane to list all panes
|
|
259
|
+
const windowInfo = await executeTmux(`display-message -p -t '${targetPaneId}' '#{window_id}'`);
|
|
260
|
+
|
|
261
|
+
// List all panes in the window to find the newly created one
|
|
262
|
+
const panes = await listPanes(windowInfo);
|
|
263
|
+
|
|
264
|
+
// The newest pane is typically the last one in the list
|
|
265
|
+
return panes.length > 0 ? panes[panes.length - 1] : null;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Map to track ongoing command executions
|
|
269
|
+
const activeCommands = new Map<string, CommandExecution>();
|
|
270
|
+
|
|
271
|
+
const startMarkerText = 'TMUX_MCP_START';
|
|
272
|
+
const endMarkerPrefix = "TMUX_MCP_DONE_";
|
|
273
|
+
|
|
274
|
+
// Execute a command in a tmux pane and track its execution
|
|
275
|
+
export async function executeCommand(paneId: string, command: string, rawMode?: boolean, noEnter?: boolean): Promise<string> {
|
|
276
|
+
// Generate unique ID for this command execution
|
|
277
|
+
const commandId = uuidv4();
|
|
278
|
+
|
|
279
|
+
let fullCommand: string;
|
|
280
|
+
if (rawMode || noEnter) {
|
|
281
|
+
fullCommand = command;
|
|
282
|
+
} else {
|
|
283
|
+
const endMarkerText = getEndMarkerText();
|
|
284
|
+
fullCommand = `echo "${startMarkerText}"; ${command}; echo "${endMarkerText}"`;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Store command in tracking map
|
|
288
|
+
activeCommands.set(commandId, {
|
|
289
|
+
id: commandId,
|
|
290
|
+
paneId,
|
|
291
|
+
command,
|
|
292
|
+
status: 'pending',
|
|
293
|
+
startTime: new Date(),
|
|
294
|
+
rawMode: rawMode || noEnter
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
// Send the command to the tmux pane
|
|
298
|
+
if (noEnter) {
|
|
299
|
+
// Check if this is a special key (e.g., Up, Down, Left, Right, Escape, Tab, etc.)
|
|
300
|
+
// Special keys in tmux are typically capitalized or have special names
|
|
301
|
+
const specialKeys = ['Up', 'Down', 'Left', 'Right', 'Escape', 'Tab', 'Enter', 'Space',
|
|
302
|
+
'BSpace', 'Delete', 'Home', 'End', 'PageUp', 'PageDown',
|
|
303
|
+
'F1', 'F2', 'F3', 'F4', 'F5', 'F6', 'F7', 'F8', 'F9', 'F10', 'F11', 'F12'];
|
|
304
|
+
|
|
305
|
+
if (specialKeys.includes(fullCommand)) {
|
|
306
|
+
// Send special key as-is
|
|
307
|
+
await executeTmux(`send-keys -t '${paneId}' ${fullCommand}`);
|
|
308
|
+
} else {
|
|
309
|
+
// For regular text, send each character individually to ensure proper processing
|
|
310
|
+
// This handles both single characters (like 'q', 'f') and strings (like 'beam')
|
|
311
|
+
for (const char of fullCommand) {
|
|
312
|
+
await executeTmux(`send-keys -t '${paneId}' '${char.replace(/'/g, "'\\''")}'`);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
} else {
|
|
316
|
+
await executeTmux(`send-keys -t '${paneId}' '${fullCommand.replace(/'/g, "'\\''")}' Enter`);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
return commandId;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
export async function checkCommandStatus(commandId: string): Promise<CommandExecution | null> {
|
|
323
|
+
const command = activeCommands.get(commandId);
|
|
324
|
+
if (!command) return null;
|
|
325
|
+
|
|
326
|
+
if (command.status !== 'pending') return command;
|
|
327
|
+
|
|
328
|
+
const content = await capturePaneContent(command.paneId, 1000);
|
|
329
|
+
|
|
330
|
+
if (command.rawMode) {
|
|
331
|
+
command.result = 'Status tracking unavailable for rawMode commands. Use capture-pane to monitor interactive apps instead.';
|
|
332
|
+
return command;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// Find the last occurrence of the markers
|
|
336
|
+
const startIndex = content.lastIndexOf(startMarkerText);
|
|
337
|
+
const endIndex = content.lastIndexOf(endMarkerPrefix);
|
|
338
|
+
|
|
339
|
+
if (startIndex === -1 || endIndex === -1 || endIndex <= startIndex) {
|
|
340
|
+
command.result = "Command output could not be captured properly";
|
|
341
|
+
return command;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// Extract exit code from the end marker line
|
|
345
|
+
const endLine = content.substring(endIndex).split('\n')[0];
|
|
346
|
+
const endMarkerRegex = new RegExp(`${endMarkerPrefix}(\\d+)`);
|
|
347
|
+
const exitCodeMatch = endLine.match(endMarkerRegex);
|
|
348
|
+
|
|
349
|
+
if (exitCodeMatch) {
|
|
350
|
+
const exitCode = parseInt(exitCodeMatch[1], 10);
|
|
351
|
+
|
|
352
|
+
command.status = exitCode === 0 ? 'completed' : 'error';
|
|
353
|
+
command.exitCode = exitCode;
|
|
354
|
+
|
|
355
|
+
// Extract output between the start and end markers
|
|
356
|
+
const outputStart = startIndex + startMarkerText.length;
|
|
357
|
+
const outputContent = content.substring(outputStart, endIndex).trim();
|
|
358
|
+
|
|
359
|
+
command.result = outputContent.substring(outputContent.indexOf('\n') + 1).trim();
|
|
360
|
+
|
|
361
|
+
// Update in map
|
|
362
|
+
activeCommands.set(commandId, command);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
return command;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// Get command by ID
|
|
369
|
+
export function getCommand(commandId: string): CommandExecution | null {
|
|
370
|
+
return activeCommands.get(commandId) || null;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// Get all active command IDs
|
|
374
|
+
export function getActiveCommandIds(): string[] {
|
|
375
|
+
return Array.from(activeCommands.keys());
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// Clean up completed commands older than a certain time
|
|
379
|
+
export function cleanupOldCommands(maxAgeMinutes: number = 60): void {
|
|
380
|
+
const now = new Date();
|
|
381
|
+
|
|
382
|
+
for (const [id, command] of activeCommands.entries()) {
|
|
383
|
+
const ageMinutes = (now.getTime() - command.startTime.getTime()) / (1000 * 60);
|
|
384
|
+
|
|
385
|
+
if (command.status !== 'pending' && ageMinutes > maxAgeMinutes) {
|
|
386
|
+
activeCommands.delete(id);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function getEndMarkerText(): string {
|
|
392
|
+
return shellConfig.type === 'fish'
|
|
393
|
+
? `${endMarkerPrefix}$status`
|
|
394
|
+
: `${endMarkerPrefix}$?`;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Get the current session ID when running inside tmux
|
|
399
|
+
*/
|
|
400
|
+
export async function getCurrentSessionId(): Promise<string> {
|
|
401
|
+
return await executeTmux(`display-message -p '#{session_id}'`);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* Rename a window
|
|
406
|
+
*/
|
|
407
|
+
export async function renameWindow(windowId: string, newName: string): Promise<void> {
|
|
408
|
+
await executeTmux(`rename-window -t '${windowId}' '${newName}'`);
|
|
409
|
+
}
|
|
410
|
+
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// Runtime version (baked in at build time)
|
|
2
|
+
export const VERSION = '0.260201.2240';
|
|
3
|
+
|
|
4
|
+
// Generate version string from current datetime
|
|
5
|
+
// Format: 0.YYMMDD.HHMM (e.g., 0.260201.1430 = Feb 1, 2026 at 14:30)
|
|
6
|
+
export function generateVersion(): string {
|
|
7
|
+
const now = new Date();
|
|
8
|
+
const yy = String(now.getFullYear()).slice(-2);
|
|
9
|
+
const mm = String(now.getMonth() + 1).padStart(2, '0');
|
|
10
|
+
const dd = String(now.getDate()).padStart(2, '0');
|
|
11
|
+
const hh = String(now.getHours()).padStart(2, '0');
|
|
12
|
+
const min = String(now.getMinutes()).padStart(2, '0');
|
|
13
|
+
|
|
14
|
+
return `0.${yy}${mm}${dd}.${hh}${min}`;
|
|
15
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { password as passwordPrompt } from '@inquirer/prompts';
|
|
2
|
+
import { testConnection, Model } from './api-client.js';
|
|
3
|
+
import { saveConfig, getDefaultApiUrl, configExists, loadConfig } from './config.js';
|
|
4
|
+
import { Config, Profile } from '../types/config.js';
|
|
5
|
+
import { pickProfileModels, promptText } from './picker.js';
|
|
6
|
+
|
|
7
|
+
export async function runSetupWizard(): Promise<void> {
|
|
8
|
+
console.log('\nš§ Claudio Setup\n');
|
|
9
|
+
|
|
10
|
+
// 1. Prompt for API URL
|
|
11
|
+
const defaultUrl = getDefaultApiUrl();
|
|
12
|
+
const apiUrl = await promptText(`API URL`, defaultUrl);
|
|
13
|
+
|
|
14
|
+
// 2. Prompt for API key (masked)
|
|
15
|
+
const apiKey = await passwordPrompt({
|
|
16
|
+
message: 'API Key:',
|
|
17
|
+
mask: '*',
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
if (!apiKey) {
|
|
21
|
+
console.error('\nā API key is required');
|
|
22
|
+
process.exit(1);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// 3. Test connection
|
|
26
|
+
process.stdout.write('\nTesting connection... ');
|
|
27
|
+
const result = await testConnection(apiUrl, apiKey);
|
|
28
|
+
|
|
29
|
+
if (!result.success) {
|
|
30
|
+
console.log('ā');
|
|
31
|
+
console.error(`\nā ${result.message}`);
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
console.log(`ā Connected (${result.modelCount} models available)`);
|
|
36
|
+
|
|
37
|
+
// 4. Create first profile
|
|
38
|
+
console.log('\nCreate your first profile:\n');
|
|
39
|
+
|
|
40
|
+
const profileName = await promptText('Profile name', 'main');
|
|
41
|
+
if (!profileName) {
|
|
42
|
+
console.error('\nā Profile name is required');
|
|
43
|
+
process.exit(1);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const models = result.models;
|
|
47
|
+
const { opus, sonnet, haiku } = await pickProfileModels(models);
|
|
48
|
+
|
|
49
|
+
const profile: Profile = { opus, sonnet, haiku };
|
|
50
|
+
const profiles: Record<string, Profile> = { [profileName]: profile };
|
|
51
|
+
|
|
52
|
+
// 5. Save config with first profile as default
|
|
53
|
+
const config: Config = {
|
|
54
|
+
apiUrl,
|
|
55
|
+
apiKey,
|
|
56
|
+
defaultProfile: profileName,
|
|
57
|
+
profiles,
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
await saveConfig(config);
|
|
61
|
+
|
|
62
|
+
console.log(`\nā Profile "${profileName}" created and set as default`);
|
|
63
|
+
console.log(`\nRun \`claudio\` to launch, or \`claudio profiles add\` to create more.`);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export async function runAddProfileWizard(): Promise<void> {
|
|
67
|
+
if (!configExists()) {
|
|
68
|
+
console.error('ā No config found. Run `claudio setup` first.');
|
|
69
|
+
process.exit(1);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const config = await loadConfig();
|
|
73
|
+
|
|
74
|
+
// Test connection to get models
|
|
75
|
+
process.stdout.write('Fetching models... ');
|
|
76
|
+
const result = await testConnection(config.apiUrl, config.apiKey);
|
|
77
|
+
|
|
78
|
+
if (!result.success) {
|
|
79
|
+
console.log('ā');
|
|
80
|
+
console.error(`\nā ${result.message}`);
|
|
81
|
+
process.exit(1);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
console.log(`ā (${result.modelCount} models)`);
|
|
85
|
+
console.log('');
|
|
86
|
+
|
|
87
|
+
const profileName = await promptText('Profile name');
|
|
88
|
+
if (!profileName) {
|
|
89
|
+
console.error('\nā Profile name is required');
|
|
90
|
+
process.exit(1);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (config.profiles[profileName]) {
|
|
94
|
+
console.error(`\nā Profile "${profileName}" already exists`);
|
|
95
|
+
process.exit(1);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const { opus, sonnet, haiku } = await pickProfileModels(result.models);
|
|
99
|
+
|
|
100
|
+
config.profiles[profileName] = { opus, sonnet, haiku };
|
|
101
|
+
await saveConfig(config);
|
|
102
|
+
|
|
103
|
+
console.log(`\nā Profile "${profileName}" created`);
|
|
104
|
+
}
|