@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.
Files changed (59) hide show
  1. package/.github/workflows/publish.yml +26 -0
  2. package/.worktrees/.metadata.json +3 -0
  3. package/README.md +532 -0
  4. package/bun.lock +101 -0
  5. package/dist/claudio.js +76 -0
  6. package/dist/genie.js +201 -0
  7. package/dist/term.js +136 -0
  8. package/install.sh +351 -0
  9. package/package.json +37 -0
  10. package/scripts/version.ts +48 -0
  11. package/src/claudio.ts +128 -0
  12. package/src/commands/launch.ts +245 -0
  13. package/src/commands/models.ts +43 -0
  14. package/src/commands/profiles.ts +95 -0
  15. package/src/commands/setup.ts +5 -0
  16. package/src/genie-commands/hooks.ts +317 -0
  17. package/src/genie-commands/install.ts +351 -0
  18. package/src/genie-commands/setup.ts +282 -0
  19. package/src/genie-commands/shortcuts.ts +62 -0
  20. package/src/genie-commands/update.ts +228 -0
  21. package/src/genie.ts +106 -0
  22. package/src/lib/api-client.ts +109 -0
  23. package/src/lib/claude-settings.ts +252 -0
  24. package/src/lib/config.ts +109 -0
  25. package/src/lib/genie-config.ts +164 -0
  26. package/src/lib/hook-manager.ts +130 -0
  27. package/src/lib/hook-script.ts +256 -0
  28. package/src/lib/hooks/compose.ts +72 -0
  29. package/src/lib/hooks/index.ts +163 -0
  30. package/src/lib/hooks/presets/audited.ts +191 -0
  31. package/src/lib/hooks/presets/collaborative.ts +143 -0
  32. package/src/lib/hooks/presets/sandboxed.ts +153 -0
  33. package/src/lib/hooks/presets/supervised.ts +66 -0
  34. package/src/lib/hooks/utils/escape.ts +46 -0
  35. package/src/lib/log-reader.ts +213 -0
  36. package/src/lib/picker.ts +62 -0
  37. package/src/lib/session-metadata.ts +58 -0
  38. package/src/lib/system-detect.ts +185 -0
  39. package/src/lib/tmux.ts +410 -0
  40. package/src/lib/version.ts +15 -0
  41. package/src/lib/wizard.ts +104 -0
  42. package/src/lib/worktree.ts +362 -0
  43. package/src/term-commands/attach.ts +23 -0
  44. package/src/term-commands/exec.ts +34 -0
  45. package/src/term-commands/hook.ts +42 -0
  46. package/src/term-commands/ls.ts +33 -0
  47. package/src/term-commands/new.ts +73 -0
  48. package/src/term-commands/pane.ts +81 -0
  49. package/src/term-commands/read.ts +70 -0
  50. package/src/term-commands/rm.ts +47 -0
  51. package/src/term-commands/send.ts +34 -0
  52. package/src/term-commands/shortcuts.ts +355 -0
  53. package/src/term-commands/split.ts +87 -0
  54. package/src/term-commands/status.ts +116 -0
  55. package/src/term-commands/window.ts +72 -0
  56. package/src/term.ts +192 -0
  57. package/src/types/config.ts +17 -0
  58. package/src/types/genie-config.ts +104 -0
  59. package/tsconfig.json +17 -0
@@ -0,0 +1,116 @@
1
+ import * as tmux from '../lib/tmux.js';
2
+
3
+ export interface StatusOptions {
4
+ command?: string;
5
+ json?: boolean;
6
+ }
7
+
8
+ interface SessionStatus {
9
+ session: string;
10
+ id: string;
11
+ attached: boolean;
12
+ windows: number;
13
+ panes: number;
14
+ state: 'idle' | 'busy';
15
+ }
16
+
17
+ interface CommandStatus {
18
+ id: string;
19
+ paneId: string;
20
+ command: string;
21
+ status: 'pending' | 'completed' | 'error';
22
+ exitCode?: number;
23
+ startTime: string;
24
+ result?: string;
25
+ }
26
+
27
+ export async function getStatus(session: string, options: StatusOptions = {}): Promise<void> {
28
+ try {
29
+ // If checking a specific command
30
+ if (options.command) {
31
+ await getCommandStatus(options.command, options.json);
32
+ return;
33
+ }
34
+
35
+ // Find session by name
36
+ const sessionObj = await tmux.findSessionByName(session);
37
+ if (!sessionObj) {
38
+ console.error(`Session "${session}" not found`);
39
+ process.exit(1);
40
+ }
41
+
42
+ // Get windows and panes
43
+ const windows = await tmux.listWindows(sessionObj.id);
44
+ let totalPanes = 0;
45
+
46
+ for (const window of windows) {
47
+ const panes = await tmux.listPanes(window.id);
48
+ totalPanes += panes.length;
49
+ }
50
+
51
+ // Determine state (simplified: idle if session exists, could be enhanced with activity detection)
52
+ const state: 'idle' | 'busy' = 'idle';
53
+
54
+ const status: SessionStatus = {
55
+ session: sessionObj.name,
56
+ id: sessionObj.id,
57
+ attached: sessionObj.attached,
58
+ windows: windows.length,
59
+ panes: totalPanes,
60
+ state,
61
+ };
62
+
63
+ if (options.json) {
64
+ console.log(JSON.stringify(status, null, 2));
65
+ return;
66
+ }
67
+
68
+ console.log(`Session: ${status.session}`);
69
+ console.log(`ID: ${status.id}`);
70
+ console.log(`Attached: ${status.attached ? 'yes' : 'no'}`);
71
+ console.log(`Windows: ${status.windows}`);
72
+ console.log(`Panes: ${status.panes}`);
73
+ console.log(`State: ${status.state}`);
74
+ } catch (error: any) {
75
+ console.error(`Error getting status: ${error.message}`);
76
+ process.exit(1);
77
+ }
78
+ }
79
+
80
+ async function getCommandStatus(commandId: string, json?: boolean): Promise<void> {
81
+ try {
82
+ const result = await tmux.checkCommandStatus(commandId);
83
+
84
+ if (!result) {
85
+ console.error(`Command "${commandId}" not found`);
86
+ process.exit(1);
87
+ }
88
+
89
+ const status: CommandStatus = {
90
+ id: result.id,
91
+ paneId: result.paneId,
92
+ command: result.command,
93
+ status: result.status,
94
+ exitCode: result.exitCode,
95
+ startTime: result.startTime.toISOString(),
96
+ result: result.result,
97
+ };
98
+
99
+ if (json) {
100
+ console.log(JSON.stringify(status, null, 2));
101
+ return;
102
+ }
103
+
104
+ console.log(`Command: ${status.id}`);
105
+ console.log(`Status: ${status.status}`);
106
+ console.log(`Exit Code: ${status.exitCode ?? 'N/A'}`);
107
+ console.log(`Start Time: ${status.startTime}`);
108
+
109
+ if (status.result) {
110
+ console.log(`\nOutput:\n${status.result}`);
111
+ }
112
+ } catch (error: any) {
113
+ console.error(`Error getting command status: ${error.message}`);
114
+ process.exit(1);
115
+ }
116
+ }
@@ -0,0 +1,72 @@
1
+ import * as tmux from '../lib/tmux.js';
2
+
3
+ export interface WindowListOptions {
4
+ json?: boolean;
5
+ }
6
+
7
+ export async function listWindows(session: string, options: WindowListOptions = {}): Promise<void> {
8
+ try {
9
+ // Find session by name first
10
+ const sessionObj = await tmux.findSessionByName(session);
11
+ if (!sessionObj) {
12
+ console.error(`Session "${session}" not found`);
13
+ process.exit(1);
14
+ }
15
+
16
+ const windows = await tmux.listWindows(sessionObj.id);
17
+
18
+ if (options.json) {
19
+ console.log(JSON.stringify(windows, null, 2));
20
+ return;
21
+ }
22
+
23
+ if (windows.length === 0) {
24
+ console.log('No windows found');
25
+ return;
26
+ }
27
+
28
+ console.log('WINDOW ID\t\tNAME\t\t\tACTIVE');
29
+ console.log('─'.repeat(60));
30
+
31
+ for (const window of windows) {
32
+ const active = window.active ? 'yes' : 'no';
33
+ console.log(`${window.id}\t\t${window.name}\t\t\t${active}`);
34
+ }
35
+ } catch (error: any) {
36
+ console.error(`Error listing windows: ${error.message}`);
37
+ process.exit(1);
38
+ }
39
+ }
40
+
41
+ export async function createWindow(session: string, name: string): Promise<void> {
42
+ try {
43
+ // Find session by name first
44
+ const sessionObj = await tmux.findSessionByName(session);
45
+ if (!sessionObj) {
46
+ console.error(`Session "${session}" not found`);
47
+ process.exit(1);
48
+ }
49
+
50
+ const window = await tmux.createWindow(sessionObj.id, name);
51
+
52
+ if (window) {
53
+ console.log(`Window created: ${window.id}`);
54
+ } else {
55
+ console.error('Failed to create window');
56
+ process.exit(1);
57
+ }
58
+ } catch (error: any) {
59
+ console.error(`Error creating window: ${error.message}`);
60
+ process.exit(1);
61
+ }
62
+ }
63
+
64
+ export async function removeWindow(windowId: string): Promise<void> {
65
+ try {
66
+ await tmux.killWindow(windowId);
67
+ console.log(`Window removed: ${windowId}`);
68
+ } catch (error: any) {
69
+ console.error(`Error removing window: ${error.message}`);
70
+ process.exit(1);
71
+ }
72
+ }
package/src/term.ts ADDED
@@ -0,0 +1,192 @@
1
+ #!/usr/bin/env bun
2
+
3
+ import { Command } from 'commander';
4
+ import * as newCmd from './term-commands/new.js';
5
+ import * as lsCmd from './term-commands/ls.js';
6
+ import * as attachCmd from './term-commands/attach.js';
7
+ import * as rmCmd from './term-commands/rm.js';
8
+ import * as readCmd from './term-commands/read.js';
9
+ import * as execCmd from './term-commands/exec.js';
10
+ import * as sendCmd from './term-commands/send.js';
11
+ import * as splitCmd from './term-commands/split.js';
12
+ import * as hookCmd from './term-commands/hook.js';
13
+ import * as windowCmd from './term-commands/window.js';
14
+ import * as paneCmd from './term-commands/pane.js';
15
+ import * as statusCmd from './term-commands/status.js';
16
+ import * as shortcutsCmd from './term-commands/shortcuts.js';
17
+
18
+ const program = new Command();
19
+
20
+ program
21
+ .name('term')
22
+ .description(`AI-friendly terminal orchestration (tmux wrapper)
23
+
24
+ Workflow: new → exec → read → rm
25
+ Full control: window new/ls/rm, pane ls/rm, split, status`)
26
+ .version('0.2.0');
27
+
28
+ // Session management
29
+ program
30
+ .command('new <name>')
31
+ .description('Create a new tmux session')
32
+ .option('-d, --workspace <path>', 'Working directory for the session')
33
+ .option('-w, --worktree', 'Create git worktree in .worktrees/<name>/')
34
+ .action(async (name: string, options: { workspace?: string; worktree?: boolean }) => {
35
+ await newCmd.createNewSession(name, options);
36
+ });
37
+
38
+ program
39
+ .command('ls')
40
+ .description('List all tmux sessions')
41
+ .option('--json', 'Output as JSON')
42
+ .action(async (options: { json?: boolean }) => {
43
+ await lsCmd.listAllSessions(options);
44
+ });
45
+
46
+ program
47
+ .command('attach <name>')
48
+ .description('Attach to a tmux session')
49
+ .action(async (name: string) => {
50
+ await attachCmd.attachToSession(name);
51
+ });
52
+
53
+ program
54
+ .command('rm <name>')
55
+ .description('Remove a tmux session')
56
+ .option('--keep-worktree', 'Keep worktree folder when removing session')
57
+ .action(async (name: string, options: { keepWorktree?: boolean }) => {
58
+ await rmCmd.removeSession(name, options);
59
+ });
60
+
61
+ // Log reading (CRITICAL for AI orchestration)
62
+ program
63
+ .command('read <session>')
64
+ .description('Read logs from a tmux session')
65
+ .option('-n, --lines <number>', 'Number of lines to read (default: 100)', '100')
66
+ .option('--from <line>', 'Start line number')
67
+ .option('--to <line>', 'End line number')
68
+ .option('--range <range>', 'Line range (e.g., 100:200)')
69
+ .option('--search <pattern>', 'Search for pattern')
70
+ .option('--grep <pattern>', 'Regex search pattern')
71
+ .option('-f, --follow', 'Follow mode (live tail)')
72
+ .option('--all', 'Export entire scrollback buffer')
73
+ .option('--reverse', 'Reverse chronological (newest first)')
74
+ .option('--json', 'Output as JSON')
75
+ .action(async (session: string, options: readCmd.ReadOptions) => {
76
+ await readCmd.readSessionLogs(session, options);
77
+ });
78
+
79
+ // Command execution
80
+ program
81
+ .command('exec <session> <command...>')
82
+ .description('Execute command in a tmux session')
83
+ .action(async (session: string, command: string[]) => {
84
+ await execCmd.executeInSession(session, command.join(' '));
85
+ });
86
+
87
+ program
88
+ .command('send <session> <keys>')
89
+ .description('Send raw keys to a tmux session')
90
+ .action(async (session: string, keys: string) => {
91
+ await sendCmd.sendKeysToSession(session, keys);
92
+ });
93
+
94
+ // Pane splitting
95
+ program
96
+ .command('split <session> [direction]')
97
+ .description('Split pane in a tmux session (h=horizontal, v=vertical)')
98
+ .option('-d, --workspace <path>', 'Working directory for the new pane')
99
+ .option('-w, --worktree <branch>', 'Create git worktree in .worktrees/<branch>/')
100
+ .action(async (session: string, direction: string | undefined, options: { workspace?: string; worktree?: string }) => {
101
+ await splitCmd.splitSessionPane(session, direction, options);
102
+ });
103
+
104
+ // Status command
105
+ program
106
+ .command('status <session>')
107
+ .description('Check session state (idle/busy, pane count)')
108
+ .option('--command <id>', 'Check specific command status')
109
+ .option('--json', 'Output as JSON')
110
+ .action(async (session: string, options: statusCmd.StatusOptions) => {
111
+ await statusCmd.getStatus(session, options);
112
+ });
113
+
114
+ // Window management
115
+ const windowProgram = program.command('window').description('Manage tmux windows');
116
+
117
+ windowProgram
118
+ .command('new <session> <name>')
119
+ .description('Create a new window in session')
120
+ .action(async (session: string, name: string) => {
121
+ await windowCmd.createWindow(session, name);
122
+ });
123
+
124
+ windowProgram
125
+ .command('ls <session>')
126
+ .description('List windows in session')
127
+ .option('--json', 'Output as JSON')
128
+ .action(async (session: string, options: { json?: boolean }) => {
129
+ await windowCmd.listWindows(session, options);
130
+ });
131
+
132
+ windowProgram
133
+ .command('rm <window-id>')
134
+ .description('Remove window by ID')
135
+ .action(async (windowId: string) => {
136
+ await windowCmd.removeWindow(windowId);
137
+ });
138
+
139
+ // Pane management
140
+ const paneProgram = program.command('pane').description('Manage tmux panes');
141
+
142
+ paneProgram
143
+ .command('ls <session>')
144
+ .description('List all panes in session')
145
+ .option('--json', 'Output as JSON')
146
+ .action(async (session: string, options: { json?: boolean }) => {
147
+ await paneCmd.listPanes(session, options);
148
+ });
149
+
150
+ paneProgram
151
+ .command('rm <pane-id>')
152
+ .description('Remove pane by ID')
153
+ .action(async (paneId: string) => {
154
+ await paneCmd.removePane(paneId);
155
+ });
156
+
157
+ // Hook management
158
+ const hookProgram = program.command('hook').description('Manage tmux hooks');
159
+
160
+ hookProgram
161
+ .command('set <event> <command>')
162
+ .description('Set a tmux hook')
163
+ .action(async (event: string, command: string) => {
164
+ await hookCmd.setHook(event, command);
165
+ });
166
+
167
+ hookProgram
168
+ .command('list')
169
+ .description('List all tmux hooks')
170
+ .action(async () => {
171
+ await hookCmd.listHooks();
172
+ });
173
+
174
+ hookProgram
175
+ .command('rm <event>')
176
+ .description('Remove a tmux hook')
177
+ .action(async (event: string) => {
178
+ await hookCmd.removeHook(event);
179
+ });
180
+
181
+ // Shortcuts command
182
+ program
183
+ .command('shortcuts')
184
+ .description('Warp-like keyboard shortcuts for tmux/Termux')
185
+ .option('--tmux', 'Output tmux.conf snippet')
186
+ .option('--termux', 'Output termux.properties snippet')
187
+ .option('--install', 'Install to config files (interactive)')
188
+ .action(async (options: shortcutsCmd.ShortcutsOptions) => {
189
+ await shortcutsCmd.handleShortcuts(options);
190
+ });
191
+
192
+ program.parse();
@@ -0,0 +1,17 @@
1
+ import { z } from 'zod';
2
+
3
+ export const ProfileSchema = z.object({
4
+ opus: z.string(),
5
+ sonnet: z.string(),
6
+ haiku: z.string(),
7
+ });
8
+
9
+ export const ConfigSchema = z.object({
10
+ apiUrl: z.string().url(),
11
+ apiKey: z.string().min(1),
12
+ defaultProfile: z.string().optional(),
13
+ profiles: z.record(ProfileSchema),
14
+ });
15
+
16
+ export type Profile = z.infer<typeof ProfileSchema>;
17
+ export type Config = z.infer<typeof ConfigSchema>;
@@ -0,0 +1,104 @@
1
+ import { z } from 'zod';
2
+
3
+ /**
4
+ * Genie Configuration Schema
5
+ *
6
+ * Stored at ~/.genie/config.json
7
+ * Manages hook presets and session configuration for the genie CLI.
8
+ */
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']),
23
+ });
24
+
25
+ // Collaborative preset configuration
26
+ export const CollaborativeConfigSchema = z.object({
27
+ sessionName: z.string().default('genie'),
28
+ windowName: z.string().default('shell'),
29
+ });
30
+
31
+ // Hook presets configuration
32
+ export const HooksConfigSchema = z.object({
33
+ enabled: z.array(z.enum(['collaborative', 'supervised', 'sandboxed', 'audited'])).default([]),
34
+ collaborative: CollaborativeConfigSchema.optional(),
35
+ supervised: SupervisedConfigSchema.optional(),
36
+ sandboxed: SandboxedConfigSchema.optional(),
37
+ audited: AuditedConfigSchema.optional(),
38
+ });
39
+
40
+ // Session configuration
41
+ export const SessionConfigSchema = z.object({
42
+ name: z.string().default('genie'),
43
+ defaultWindow: z.string().default('shell'),
44
+ });
45
+
46
+ // Full genie configuration
47
+ export const GenieConfigSchema = z.object({
48
+ hooks: HooksConfigSchema.default({}),
49
+ session: SessionConfigSchema.default({}),
50
+ });
51
+
52
+ // Inferred types
53
+ export type SandboxedConfig = z.infer<typeof SandboxedConfigSchema>;
54
+ export type AuditedConfig = z.infer<typeof AuditedConfigSchema>;
55
+ export type SupervisedConfig = z.infer<typeof SupervisedConfigSchema>;
56
+ export type CollaborativeConfig = z.infer<typeof CollaborativeConfigSchema>;
57
+ export type HooksConfig = z.infer<typeof HooksConfigSchema>;
58
+ export type SessionConfig = z.infer<typeof SessionConfigSchema>;
59
+ export type GenieConfig = z.infer<typeof GenieConfigSchema>;
60
+
61
+ // Preset names type
62
+ export type PresetName = 'collaborative' | 'supervised' | 'sandboxed' | 'audited';
63
+
64
+ // Preset description for UI
65
+ export interface PresetDescription {
66
+ name: PresetName;
67
+ title: string;
68
+ what: string;
69
+ why: string;
70
+ how: string;
71
+ recommended?: boolean;
72
+ }
73
+
74
+ export const PRESET_DESCRIPTIONS: PresetDescription[] = [
75
+ {
76
+ name: 'collaborative',
77
+ title: 'Collaborative',
78
+ what: 'All terminal commands run through tmux',
79
+ why: 'You can watch AI work in real-time',
80
+ how: 'Bash commands → term exec genie:shell',
81
+ recommended: true,
82
+ },
83
+ {
84
+ name: 'supervised',
85
+ title: 'Supervised',
86
+ what: 'File changes require your approval',
87
+ why: 'Prevents accidental overwrites',
88
+ how: 'Write/Edit tools always ask permission',
89
+ },
90
+ {
91
+ name: 'sandboxed',
92
+ title: 'Sandboxed',
93
+ what: 'Restrict file access to specific directories',
94
+ why: 'Protects sensitive areas of your system',
95
+ how: 'Operations outside sandbox are blocked',
96
+ },
97
+ {
98
+ name: 'audited',
99
+ title: 'Audited',
100
+ what: 'Log all AI tool usage to a file',
101
+ why: 'Review what the AI did after a session',
102
+ how: 'Every tool call → ~/.genie/audit.log',
103
+ },
104
+ ];
package/tsconfig.json ADDED
@@ -0,0 +1,17 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "lib": ["ES2022"],
6
+ "moduleResolution": "bundler",
7
+ "declaration": true,
8
+ "outDir": "dist",
9
+ "strict": true,
10
+ "esModuleInterop": true,
11
+ "skipLibCheck": true,
12
+ "forceConsistentCasingInFileNames": true,
13
+ "types": ["bun"]
14
+ },
15
+ "include": ["src/**/*"],
16
+ "exclude": ["node_modules", "dist"]
17
+ }