@automagik/genie 0.260202.530 → 0.260202.1833

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/dist/claudio.js +44 -45
  2. package/dist/genie.js +58 -135
  3. package/dist/term.js +134 -67
  4. package/install.sh +43 -7
  5. package/package.json +1 -1
  6. package/src/claudio.ts +31 -21
  7. package/src/commands/launch.ts +12 -68
  8. package/src/genie-commands/doctor.ts +327 -0
  9. package/src/genie-commands/setup.ts +317 -199
  10. package/src/genie-commands/uninstall.ts +176 -0
  11. package/src/genie.ts +24 -44
  12. package/src/lib/claude-settings.ts +22 -64
  13. package/src/lib/genie-config.ts +169 -57
  14. package/src/lib/orchestrator/completion.ts +392 -0
  15. package/src/lib/orchestrator/event-monitor.ts +442 -0
  16. package/src/lib/orchestrator/index.ts +12 -0
  17. package/src/lib/orchestrator/patterns.ts +277 -0
  18. package/src/lib/orchestrator/state-detector.ts +339 -0
  19. package/src/lib/version.ts +1 -1
  20. package/src/lib/worker-registry.ts +229 -0
  21. package/src/term-commands/close.ts +221 -0
  22. package/src/term-commands/exec.ts +28 -6
  23. package/src/term-commands/kill.ts +143 -0
  24. package/src/term-commands/orchestrate.ts +844 -0
  25. package/src/term-commands/read.ts +6 -1
  26. package/src/term-commands/shortcuts.ts +14 -14
  27. package/src/term-commands/work.ts +415 -0
  28. package/src/term-commands/workers.ts +264 -0
  29. package/src/term.ts +201 -3
  30. package/src/types/genie-config.ts +49 -81
  31. package/src/genie-commands/hooks.ts +0 -317
  32. package/src/lib/hook-script.ts +0 -263
  33. package/src/lib/hooks/compose.ts +0 -72
  34. package/src/lib/hooks/index.ts +0 -163
  35. package/src/lib/hooks/presets/audited.ts +0 -191
  36. package/src/lib/hooks/presets/collaborative.ts +0 -143
  37. package/src/lib/hooks/presets/sandboxed.ts +0 -153
  38. package/src/lib/hooks/presets/supervised.ts +0 -66
  39. package/src/lib/hooks/utils/escape.ts +0 -46
@@ -0,0 +1,264 @@
1
+ /**
2
+ * Workers command - Show worker status
3
+ *
4
+ * Usage:
5
+ * term workers - List all workers and their states
6
+ * term workers --json - Output as JSON
7
+ * term workers --watch - Live updates (TODO: Phase 1.5)
8
+ */
9
+
10
+ import { $ } from 'bun';
11
+ import * as tmux from '../lib/tmux.js';
12
+ import * as registry from '../lib/worker-registry.js';
13
+ import { detectState, stripAnsi } from '../lib/orchestrator/index.js';
14
+
15
+ // ============================================================================
16
+ // Types
17
+ // ============================================================================
18
+
19
+ export interface WorkersOptions {
20
+ json?: boolean;
21
+ watch?: boolean;
22
+ }
23
+
24
+ interface WorkerDisplay {
25
+ name: string;
26
+ pane: string;
27
+ task: string;
28
+ state: string;
29
+ time: string;
30
+ alive: boolean;
31
+ }
32
+
33
+ // ============================================================================
34
+ // Helper Functions
35
+ // ============================================================================
36
+
37
+ /**
38
+ * Check if a pane still exists
39
+ */
40
+ async function isPaneAlive(paneId: string): Promise<boolean> {
41
+ try {
42
+ await tmux.capturePaneContent(paneId, 1);
43
+ return true;
44
+ } catch {
45
+ return false;
46
+ }
47
+ }
48
+
49
+ /**
50
+ * Get current state from pane output
51
+ */
52
+ async function getCurrentState(paneId: string): Promise<string> {
53
+ try {
54
+ const output = await tmux.capturePaneContent(paneId, 30);
55
+ const state = detectState(output);
56
+
57
+ // Map to display format
58
+ switch (state.type) {
59
+ case 'working':
60
+ case 'tool_use':
61
+ return 'working';
62
+ case 'idle':
63
+ return 'idle';
64
+ case 'permission':
65
+ return '⚠️ perm';
66
+ case 'question':
67
+ return '⚠️ question';
68
+ case 'error':
69
+ return '❌ error';
70
+ case 'complete':
71
+ return '✅ done';
72
+ default:
73
+ return state.type;
74
+ }
75
+ } catch {
76
+ return 'unknown';
77
+ }
78
+ }
79
+
80
+ /**
81
+ * Get beads queue status
82
+ */
83
+ async function getQueueStatus(): Promise<{ ready: string[]; blocked: string[] }> {
84
+ const ready: string[] = [];
85
+ const blocked: string[] = [];
86
+
87
+ try {
88
+ // Get ready issues
89
+ const readyResult = await $`bd ready --json`.quiet();
90
+ const readyOutput = readyResult.stdout.toString().trim();
91
+ if (readyOutput) {
92
+ try {
93
+ const issues = JSON.parse(readyOutput);
94
+ for (const issue of issues) {
95
+ ready.push(`${issue.id}`);
96
+ }
97
+ } catch {
98
+ // Parse line-based format
99
+ const lines = readyOutput.split('\n').filter(l => l.trim());
100
+ for (const line of lines) {
101
+ const match = line.match(/^(bd-\d+)/);
102
+ if (match) ready.push(match[1]);
103
+ }
104
+ }
105
+ }
106
+ } catch {
107
+ // Ignore bd errors
108
+ }
109
+
110
+ try {
111
+ // Get blocked issues
112
+ const listResult = await $`bd list --json`.quiet();
113
+ const listOutput = listResult.stdout.toString().trim();
114
+ if (listOutput) {
115
+ try {
116
+ const issues = JSON.parse(listOutput);
117
+ for (const issue of issues) {
118
+ if (issue.blockedBy && issue.blockedBy.length > 0) {
119
+ blocked.push(`${issue.id} (blocked by ${issue.blockedBy.join(', ')})`);
120
+ }
121
+ }
122
+ } catch {
123
+ // Ignore parse errors
124
+ }
125
+ }
126
+ } catch {
127
+ // Ignore bd errors
128
+ }
129
+
130
+ return { ready, blocked };
131
+ }
132
+
133
+ /**
134
+ * Format time elapsed
135
+ */
136
+ function formatElapsed(startedAt: string): string {
137
+ const startTime = new Date(startedAt).getTime();
138
+ const ms = Date.now() - startTime;
139
+ const minutes = Math.floor(ms / 60000);
140
+ const hours = Math.floor(minutes / 60);
141
+
142
+ if (hours > 0) {
143
+ return `${hours}h ${minutes % 60}m`;
144
+ } else if (minutes > 0) {
145
+ return `${minutes}m`;
146
+ }
147
+ return '<1m';
148
+ }
149
+
150
+ // ============================================================================
151
+ // Main Command
152
+ // ============================================================================
153
+
154
+ export async function workersCommand(options: WorkersOptions = {}): Promise<void> {
155
+ try {
156
+ const workers = await registry.list();
157
+
158
+ // Gather display data for each worker
159
+ const displayData: WorkerDisplay[] = [];
160
+
161
+ for (const worker of workers) {
162
+ const alive = await isPaneAlive(worker.paneId);
163
+ let currentState = worker.state;
164
+
165
+ if (alive) {
166
+ // Get live state from pane
167
+ currentState = await getCurrentState(worker.paneId);
168
+
169
+ // Update registry if state differs
170
+ const mappedState = mapDisplayStateToRegistry(currentState);
171
+ if (mappedState && mappedState !== worker.state) {
172
+ await registry.updateState(worker.id, mappedState);
173
+ }
174
+ } else {
175
+ currentState = '💀 dead';
176
+ }
177
+
178
+ displayData.push({
179
+ name: worker.id,
180
+ pane: worker.paneId,
181
+ task: worker.taskTitle
182
+ ? `"${worker.taskTitle.substring(0, 25)}${worker.taskTitle.length > 25 ? '...' : ''}"`
183
+ : worker.taskId,
184
+ state: currentState,
185
+ time: formatElapsed(worker.startedAt),
186
+ alive,
187
+ });
188
+ }
189
+
190
+ // Get queue status
191
+ const queue = await getQueueStatus();
192
+
193
+ // Filter out dead workers from ready count
194
+ const activeTaskIds = workers.filter(w => displayData.find(d => d.name === w.id && d.alive)).map(w => w.taskId);
195
+ const actuallyReady = queue.ready.filter(id => !activeTaskIds.includes(id));
196
+
197
+ if (options.json) {
198
+ console.log(JSON.stringify({
199
+ workers: displayData,
200
+ queue: {
201
+ ready: actuallyReady,
202
+ blocked: queue.blocked,
203
+ },
204
+ }, null, 2));
205
+ return;
206
+ }
207
+
208
+ // Display workers table
209
+ console.log('┌─────────────────────────────────────────────────────────────────┐');
210
+ console.log('│ WORKERS │');
211
+ console.log('├──────────┬──────────┬───────────────────────────┬──────────┬────┤');
212
+ console.log('│ Name │ Pane │ Task │ State │Time│');
213
+ console.log('├──────────┼──────────┼───────────────────────────┼──────────┼────┤');
214
+
215
+ if (displayData.length === 0) {
216
+ console.log('│ (no workers) │');
217
+ } else {
218
+ for (const w of displayData) {
219
+ const name = w.name.padEnd(8).substring(0, 8);
220
+ const pane = w.pane.padEnd(8).substring(0, 8);
221
+ const task = w.task.padEnd(25).substring(0, 25);
222
+ const state = w.state.padEnd(8).substring(0, 8);
223
+ const time = w.time.padStart(4).substring(0, 4);
224
+ console.log(`│ ${name} │ ${pane} │ ${task} │ ${state} │${time}│`);
225
+ }
226
+ }
227
+
228
+ console.log('└──────────┴──────────┴───────────────────────────┴──────────┴────┘');
229
+
230
+ // Display queue
231
+ if (queue.blocked.length > 0) {
232
+ console.log(`\nBlocked: ${queue.blocked.slice(0, 5).join(', ')}${queue.blocked.length > 5 ? '...' : ''}`);
233
+ }
234
+
235
+ if (actuallyReady.length > 0) {
236
+ console.log(`Ready: ${actuallyReady.slice(0, 5).join(', ')}${actuallyReady.length > 5 ? '...' : ''}`);
237
+ } else if (displayData.length > 0) {
238
+ console.log(`\nReady: (none - all assigned or blocked)`);
239
+ }
240
+
241
+ // Cleanup dead workers (optional - could prompt)
242
+ const deadWorkers = displayData.filter(w => !w.alive);
243
+ if (deadWorkers.length > 0) {
244
+ console.log(`\n⚠️ ${deadWorkers.length} dead worker(s) detected. Run \`term kill <name>\` to clean up.`);
245
+ }
246
+
247
+ } catch (error: any) {
248
+ console.error(`❌ Error: ${error.message}`);
249
+ process.exit(1);
250
+ }
251
+ }
252
+
253
+ /**
254
+ * Map display state to registry state
255
+ */
256
+ function mapDisplayStateToRegistry(displayState: string): registry.WorkerState | null {
257
+ if (displayState === 'working') return 'working';
258
+ if (displayState === 'idle') return 'idle';
259
+ if (displayState === '⚠️ perm') return 'permission';
260
+ if (displayState === '⚠️ question') return 'question';
261
+ if (displayState === '❌ error') return 'error';
262
+ if (displayState === '✅ done') return 'done';
263
+ return null;
264
+ }
package/src/term.ts CHANGED
@@ -15,6 +15,11 @@ import * as windowCmd from './term-commands/window.js';
15
15
  import * as paneCmd from './term-commands/pane.js';
16
16
  import * as statusCmd from './term-commands/status.js';
17
17
  import * as shortcutsCmd from './term-commands/shortcuts.js';
18
+ import * as orchestrateCmd from './term-commands/orchestrate.js';
19
+ import * as workCmd from './term-commands/work.js';
20
+ import * as workersCmd from './term-commands/workers.js';
21
+ import * as closeCmd from './term-commands/close.js';
22
+ import * as killCmd from './term-commands/kill.js';
18
23
 
19
24
  const program = new Command();
20
25
 
@@ -22,8 +27,20 @@ program
22
27
  .name('term')
23
28
  .description(`AI-friendly terminal orchestration (tmux wrapper)
24
29
 
30
+ Collaborative Usage:
31
+ AI runs: term exec genie:shell '<command>'
32
+ Human watches: tmux attach -t genie
33
+ AI reads: term read genie
34
+
25
35
  Workflow: new → exec → read → rm
26
- Full control: window new/ls/rm, pane ls/rm, split, status`)
36
+ Full control: window new/ls/rm, pane ls/rm, split, status
37
+
38
+ Worker Orchestration:
39
+ term work <bd-id> - Spawn worker bound to beads issue
40
+ term work next - Work on next ready issue
41
+ term workers - List all workers and states
42
+ term close <bd-id> - Close issue, cleanup worker
43
+ term kill <worker> - Force kill a stuck worker`)
27
44
  .version(VERSION);
28
45
 
29
46
  // Session management
@@ -81,8 +98,13 @@ program
81
98
  program
82
99
  .command('exec <session> <command...>')
83
100
  .description('Execute command in a tmux session')
84
- .action(async (session: string, command: string[]) => {
85
- await execCmd.executeInSession(session, command.join(' '));
101
+ .option('-q, --quiet', 'Suppress stdout output')
102
+ .option('-t, --timeout <ms>', 'Timeout in milliseconds (default: 120000)')
103
+ .action(async (session: string, command: string[], options: { quiet?: boolean; timeout?: string }) => {
104
+ await execCmd.executeInSession(session, command.join(' '), {
105
+ quiet: options.quiet,
106
+ timeout: options.timeout ? parseInt(options.timeout, 10) : undefined,
107
+ });
86
108
  });
87
109
 
88
110
  program
@@ -190,4 +212,180 @@ program
190
212
  await shortcutsCmd.handleShortcuts(options);
191
213
  });
192
214
 
215
+ // Worker management commands (beads + Claude orchestration)
216
+ program
217
+ .command('work <target>')
218
+ .description('Spawn worker bound to beads issue (target: bd-id, "next", or "wish")')
219
+ .option('--no-worktree', 'Use shared repo instead of worktree')
220
+ .option('-s, --session <name>', 'Target tmux session')
221
+ .option('--no-focus', 'Don\'t focus the worker pane')
222
+ .option('-p, --prompt <message>', 'Custom initial prompt')
223
+ .action(async (target: string, options: workCmd.WorkOptions) => {
224
+ await workCmd.workCommand(target, options);
225
+ });
226
+
227
+ program
228
+ .command('workers')
229
+ .description('List all workers and their states')
230
+ .option('--json', 'Output as JSON')
231
+ .option('-w, --watch', 'Live updates (coming soon)')
232
+ .action(async (options: workersCmd.WorkersOptions) => {
233
+ if (options.watch) {
234
+ console.log('ℹ️ --watch mode coming in Phase 1.5');
235
+ }
236
+ await workersCmd.workersCommand(options);
237
+ });
238
+
239
+ program
240
+ .command('close <task-id>')
241
+ .description('Close beads issue and cleanup worker')
242
+ .option('--no-sync', 'Skip bd sync')
243
+ .option('--keep-worktree', 'Don\'t remove the worktree')
244
+ .option('--merge', 'Merge worktree changes to main branch')
245
+ .option('-y, --yes', 'Skip confirmation')
246
+ .action(async (taskId: string, options: closeCmd.CloseOptions) => {
247
+ await closeCmd.closeCommand(taskId, options);
248
+ });
249
+
250
+ program
251
+ .command('kill <worker>')
252
+ .description('Force kill a worker')
253
+ .option('-y, --yes', 'Skip confirmation')
254
+ .option('--keep-worktree', 'Don\'t remove the worktree')
255
+ .action(async (worker: string, options: killCmd.KillOptions) => {
256
+ await killCmd.killCommand(worker, options);
257
+ });
258
+
259
+ // Approve command (shortcut to orc approve for worker use)
260
+ program
261
+ .command('approve <worker>')
262
+ .description('Approve pending permission request for a worker')
263
+ .option('--deny', 'Deny instead of approve')
264
+ .action(async (worker: string, options: { deny?: boolean }) => {
265
+ // Find worker to get pane
266
+ const registry = await import('./lib/worker-registry.js');
267
+ let workerInfo = await registry.get(worker);
268
+ if (!workerInfo) {
269
+ workerInfo = await registry.findByTask(worker);
270
+ }
271
+ if (!workerInfo) {
272
+ console.error(`❌ Worker "${worker}" not found. Run \`term workers\` to see workers.`);
273
+ process.exit(1);
274
+ }
275
+ await orchestrateCmd.approvePermission(workerInfo.session, {
276
+ pane: workerInfo.paneId,
277
+ deny: options.deny,
278
+ });
279
+ });
280
+
281
+ // Answer command (shortcut to orc answer for worker use)
282
+ program
283
+ .command('answer <worker> <choice>')
284
+ .description('Answer a question for a worker (use "text:..." for text input)')
285
+ .action(async (worker: string, choice: string) => {
286
+ const registry = await import('./lib/worker-registry.js');
287
+ let workerInfo = await registry.get(worker);
288
+ if (!workerInfo) {
289
+ workerInfo = await registry.findByTask(worker);
290
+ }
291
+ if (!workerInfo) {
292
+ console.error(`❌ Worker "${worker}" not found. Run \`term workers\` to see workers.`);
293
+ process.exit(1);
294
+ }
295
+ await orchestrateCmd.answerQuestion(workerInfo.session, choice, {
296
+ pane: workerInfo.paneId,
297
+ });
298
+ });
299
+
300
+ // Orchestration commands (Claude Code automation)
301
+ const orcProgram = program.command('orc').description('Orchestrate Claude Code sessions');
302
+
303
+ orcProgram
304
+ .command('start <session>')
305
+ .description('Start Claude Code in a session with optional monitoring')
306
+ .option('-p, --pane <id>', 'Target specific pane ID (e.g., %16)')
307
+ .option('-m, --monitor', 'Enable real-time event monitoring')
308
+ .option('-c, --command <cmd>', 'Command to run instead of claude')
309
+ .option('--json', 'Output events as JSON')
310
+ .action(async (session: string, options: orchestrateCmd.StartOptions) => {
311
+ await orchestrateCmd.startSession(session, options);
312
+ });
313
+
314
+ orcProgram
315
+ .command('send <session> <message>')
316
+ .description('Send message to Claude and track completion')
317
+ .option('--pane <id>', 'Target specific pane ID (e.g., %16)')
318
+ .option('--method <name>', 'Completion detection method')
319
+ .option('-t, --timeout <ms>', 'Timeout in milliseconds')
320
+ .option('--no-wait', 'Send without waiting for completion')
321
+ .option('--json', 'Output as JSON')
322
+ .action(async (session: string, message: string, options: orchestrateCmd.SendOptions) => {
323
+ await orchestrateCmd.sendMessage(session, message, options);
324
+ });
325
+
326
+ orcProgram
327
+ .command('status <session>')
328
+ .description('Show current Claude state and details')
329
+ .option('--pane <id>', 'Target specific pane ID (e.g., %16)')
330
+ .option('--json', 'Output as JSON')
331
+ .action(async (session: string, options: orchestrateCmd.StatusOptions) => {
332
+ await orchestrateCmd.showStatus(session, options);
333
+ });
334
+
335
+ orcProgram
336
+ .command('watch <session>')
337
+ .description('Watch session events in real-time')
338
+ .option('--pane <id>', 'Target specific pane ID (e.g., %16)')
339
+ .option('--json', 'Output events as JSON')
340
+ .option('-p, --poll <ms>', 'Poll interval in milliseconds')
341
+ .action(async (session: string, options: orchestrateCmd.WatchOptions) => {
342
+ await orchestrateCmd.watchSession(session, options);
343
+ });
344
+
345
+ orcProgram
346
+ .command('approve <session>')
347
+ .description('Approve pending permission request')
348
+ .option('-p, --pane <id>', 'Specific pane ID to target')
349
+ .option('--auto', 'Auto-approve all future permissions (dangerous!)')
350
+ .option('--deny', 'Deny instead of approve')
351
+ .action(async (session: string, options: orchestrateCmd.ApproveOptions) => {
352
+ await orchestrateCmd.approvePermission(session, options);
353
+ });
354
+
355
+ orcProgram
356
+ .command('answer <session> <choice>')
357
+ .description('Answer a question with the given choice (use "text:..." to send feedback)')
358
+ .option('-p, --pane <id>', 'Specific pane ID to target')
359
+ .action(async (session: string, choice: string, options: { pane?: string }) => {
360
+ await orchestrateCmd.answerQuestion(session, choice, options);
361
+ });
362
+
363
+ orcProgram
364
+ .command('experiment <method>')
365
+ .description('Test a completion detection method')
366
+ .option('-n, --runs <number>', 'Number of test runs')
367
+ .option('--task <command>', 'Test command to run')
368
+ .option('--json', 'Output as JSON')
369
+ .action(async (method: string, options: orchestrateCmd.ExperimentOptions) => {
370
+ await orchestrateCmd.runExperiment(method, options);
371
+ });
372
+
373
+ orcProgram
374
+ .command('methods')
375
+ .description('List available completion detection methods')
376
+ .action(async () => {
377
+ await orchestrateCmd.listMethods();
378
+ });
379
+
380
+ orcProgram
381
+ .command('run <session> <message>')
382
+ .description('Send task and auto-approve until idle (fire-and-forget)')
383
+ .option('-p, --pane <id>', 'Target specific pane ID (e.g., %16)')
384
+ .option('-a, --auto-approve', 'Auto-approve permissions and plans')
385
+ .option('-t, --timeout <ms>', 'Timeout in milliseconds (default: 300000)')
386
+ .option('--json', 'Output final state as JSON')
387
+ .action(async (session: string, message: string, options: orchestrateCmd.RunOptions) => {
388
+ await orchestrateCmd.runTask(session, message, options);
389
+ });
390
+
193
391
  program.parse();
@@ -1,112 +1,80 @@
1
1
  import { z } from 'zod';
2
2
 
3
3
  /**
4
- * Genie Configuration Schema
4
+ * Genie Configuration Schema v2
5
5
  *
6
6
  * Stored at ~/.genie/config.json
7
- * Manages hook presets and session configuration for the genie CLI.
7
+ * Manages session configuration, terminal defaults, and shortcuts for the genie CLI.
8
8
  */
9
9
 
10
- // Sandboxed preset configuration
11
- export const SandboxedConfigSchema = z.object({
12
- allowedPaths: z.array(z.string()).default(['~/projects', '/tmp']),
13
- });
14
-
15
- // Audited preset configuration
16
- export const AuditedConfigSchema = z.object({
17
- logPath: z.string().default('~/.genie/audit.log'),
18
- });
19
-
20
- // Supervised preset configuration
21
- export const SupervisedConfigSchema = z.object({
22
- alwaysAsk: z.array(z.string()).default(['Write', 'Edit']),
10
+ // Session configuration
11
+ export const SessionConfigSchema = z.object({
12
+ name: z.string().default('genie'),
13
+ defaultWindow: z.string().default('shell'),
14
+ autoCreate: z.boolean().default(true),
23
15
  });
24
16
 
25
- // Collaborative preset configuration
26
- export const CollaborativeConfigSchema = z.object({
27
- sessionName: z.string().default('genie'),
28
- windowName: z.string().default('shell'),
17
+ // Terminal configuration
18
+ export const TerminalConfigSchema = z.object({
19
+ execTimeout: z.number().default(120000),
20
+ readLines: z.number().default(100),
21
+ worktreeBase: z.string().default('.worktrees'),
29
22
  });
30
23
 
31
24
  // Logging configuration
32
25
  export const LoggingConfigSchema = z.object({
33
26
  tmuxDebug: z.boolean().default(false),
27
+ verbose: z.boolean().default(false),
34
28
  });
35
29
 
36
- // Hook presets configuration
37
- export const HooksConfigSchema = z.object({
38
- enabled: z.array(z.enum(['collaborative', 'supervised', 'sandboxed', 'audited'])).default([]),
39
- collaborative: CollaborativeConfigSchema.optional(),
40
- supervised: SupervisedConfigSchema.optional(),
41
- sandboxed: SandboxedConfigSchema.optional(),
42
- audited: AuditedConfigSchema.optional(),
30
+ // Shell configuration
31
+ export const ShellConfigSchema = z.object({
32
+ preference: z.enum(['auto', 'zsh', 'bash', 'fish']).default('auto'),
43
33
  });
44
34
 
45
- // Session configuration
46
- export const SessionConfigSchema = z.object({
47
- name: z.string().default('genie'),
48
- defaultWindow: z.string().default('shell'),
35
+ // Shortcuts configuration
36
+ export const ShortcutsConfigSchema = z.object({
37
+ tmuxInstalled: z.boolean().default(false),
38
+ shellInstalled: z.boolean().default(false),
39
+ });
40
+
41
+ // Claudio integration configuration
42
+ export const ClaudioConfigSchema = z.object({
43
+ enabled: z.boolean().default(false),
49
44
  });
50
45
 
51
46
  // Full genie configuration
52
47
  export const GenieConfigSchema = z.object({
53
- hooks: HooksConfigSchema.default({}),
48
+ version: z.number().default(2),
54
49
  session: SessionConfigSchema.default({}),
50
+ terminal: TerminalConfigSchema.default({}),
55
51
  logging: LoggingConfigSchema.default({}),
52
+ shell: ShellConfigSchema.default({}),
53
+ shortcuts: ShortcutsConfigSchema.default({}),
54
+ claudio: ClaudioConfigSchema.optional(),
55
+ installMethod: z.enum(['source', 'npm', 'bun']).optional(),
56
+ setupComplete: z.boolean().default(false),
57
+ lastSetupAt: z.string().optional(),
58
+ });
59
+
60
+ // Legacy v1 config schema (for migration)
61
+ export const GenieConfigV1Schema = z.object({
62
+ session: z.object({
63
+ name: z.string().default('genie'),
64
+ defaultWindow: z.string().default('shell'),
65
+ }).default({}),
66
+ logging: z.object({
67
+ tmuxDebug: z.boolean().default(false),
68
+ }).default({}),
56
69
  installMethod: z.enum(['source', 'npm', 'bun']).optional(),
57
70
  });
58
71
 
59
72
  // Inferred types
60
- export type SandboxedConfig = z.infer<typeof SandboxedConfigSchema>;
61
- export type AuditedConfig = z.infer<typeof AuditedConfigSchema>;
62
- export type SupervisedConfig = z.infer<typeof SupervisedConfigSchema>;
63
- export type CollaborativeConfig = z.infer<typeof CollaborativeConfigSchema>;
64
- export type HooksConfig = z.infer<typeof HooksConfigSchema>;
65
73
  export type SessionConfig = z.infer<typeof SessionConfigSchema>;
74
+ export type TerminalConfig = z.infer<typeof TerminalConfigSchema>;
66
75
  export type LoggingConfig = z.infer<typeof LoggingConfigSchema>;
76
+ export type ShellConfig = z.infer<typeof ShellConfigSchema>;
77
+ export type ShortcutsConfig = z.infer<typeof ShortcutsConfigSchema>;
78
+ export type ClaudioConfig = z.infer<typeof ClaudioConfigSchema>;
67
79
  export type GenieConfig = z.infer<typeof GenieConfigSchema>;
68
-
69
- // Preset names type
70
- export type PresetName = 'collaborative' | 'supervised' | 'sandboxed' | 'audited';
71
-
72
- // Preset description for UI
73
- export interface PresetDescription {
74
- name: PresetName;
75
- title: string;
76
- what: string;
77
- why: string;
78
- how: string;
79
- recommended?: boolean;
80
- }
81
-
82
- export const PRESET_DESCRIPTIONS: PresetDescription[] = [
83
- {
84
- name: 'collaborative',
85
- title: 'Collaborative',
86
- what: 'All terminal commands run through tmux',
87
- why: 'You can watch AI work in real-time',
88
- how: 'Bash commands → term exec genie:shell',
89
- recommended: true,
90
- },
91
- {
92
- name: 'supervised',
93
- title: 'Supervised',
94
- what: 'File changes require your approval',
95
- why: 'Prevents accidental overwrites',
96
- how: 'Write/Edit tools always ask permission',
97
- },
98
- {
99
- name: 'sandboxed',
100
- title: 'Sandboxed',
101
- what: 'Restrict file access to specific directories',
102
- why: 'Protects sensitive areas of your system',
103
- how: 'Operations outside sandbox are blocked',
104
- },
105
- {
106
- name: 'audited',
107
- title: 'Audited',
108
- what: 'Log all AI tool usage to a file',
109
- why: 'Review what the AI did after a session',
110
- how: 'Every tool call → ~/.genie/audit.log',
111
- },
112
- ];
80
+ export type GenieConfigV1 = z.infer<typeof GenieConfigV1Schema>;