@automagik/genie 0.260202.453 → 0.260202.1607
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/dist/claudio.js +44 -45
- package/dist/genie.js +58 -135
- package/dist/term.js +71 -66
- package/install.sh +43 -7
- package/package.json +1 -1
- package/src/claudio.ts +31 -21
- package/src/commands/launch.ts +12 -68
- package/src/genie-commands/doctor.ts +327 -0
- package/src/genie-commands/setup.ts +317 -199
- package/src/genie-commands/uninstall.ts +176 -0
- package/src/genie.ts +24 -44
- package/src/lib/claude-settings.ts +22 -64
- package/src/lib/genie-config.ts +169 -57
- package/src/lib/version.ts +1 -1
- package/src/term-commands/exec.ts +28 -6
- package/src/term-commands/read.ts +6 -1
- package/src/term-commands/shortcuts.ts +14 -14
- package/src/term.ts +12 -2
- package/src/types/genie-config.ts +49 -81
- package/src/genie-commands/hooks.ts +0 -317
- package/src/lib/hook-script.ts +0 -263
- package/src/lib/hooks/compose.ts +0 -72
- package/src/lib/hooks/index.ts +0 -163
- package/src/lib/hooks/presets/audited.ts +0 -191
- package/src/lib/hooks/presets/collaborative.ts +0 -143
- package/src/lib/hooks/presets/sandboxed.ts +0 -153
- package/src/lib/hooks/presets/supervised.ts +0 -66
- package/src/lib/hooks/utils/escape.ts +0 -46
|
@@ -1,143 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Collaborative Hook Preset
|
|
3
|
-
*
|
|
4
|
-
* Makes all terminal operations human-observable via tmux.
|
|
5
|
-
* Intercepts Bash tool calls and rewrites them to go through `term exec`.
|
|
6
|
-
*
|
|
7
|
-
* Usage:
|
|
8
|
-
* const hooks = collaborativeHooks({ sessionName: 'genie', windowName: 'shell' });
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import type { CollaborativeConfig } from '../../../types/genie-config.js';
|
|
12
|
-
import { escapeForSingleQuotes } from '../utils/escape.js';
|
|
13
|
-
|
|
14
|
-
export interface CollaborativeHookConfig {
|
|
15
|
-
sessionName: string;
|
|
16
|
-
windowName: string;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Default configuration for collaborative hooks
|
|
21
|
-
*/
|
|
22
|
-
export const DEFAULT_COLLABORATIVE_CONFIG: CollaborativeHookConfig = {
|
|
23
|
-
sessionName: 'genie',
|
|
24
|
-
windowName: 'shell',
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Create collaborative hooks configuration
|
|
29
|
-
*
|
|
30
|
-
* This hook intercepts all Bash tool calls and rewrites them to execute
|
|
31
|
-
* through `term exec <session>:<window> '<command>'`, making all terminal
|
|
32
|
-
* operations visible in a tmux session that humans can attach to.
|
|
33
|
-
*/
|
|
34
|
-
export function collaborativeHooks(config: Partial<CollaborativeConfig> = {}): HookConfig {
|
|
35
|
-
const sessionName = config.sessionName || DEFAULT_COLLABORATIVE_CONFIG.sessionName;
|
|
36
|
-
const windowName = config.windowName || DEFAULT_COLLABORATIVE_CONFIG.windowName;
|
|
37
|
-
const target = `${sessionName}:${windowName}`;
|
|
38
|
-
|
|
39
|
-
return {
|
|
40
|
-
PreToolUse: [
|
|
41
|
-
{
|
|
42
|
-
matcher: 'Bash',
|
|
43
|
-
timeout: 30,
|
|
44
|
-
hooks: [
|
|
45
|
-
async (input, _toolUseID, _options) => {
|
|
46
|
-
// Type guard - only process PreToolUse events
|
|
47
|
-
if (input.hook_event_name !== 'PreToolUse') {
|
|
48
|
-
return { continue: true };
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
// Only intercept Bash tool
|
|
52
|
-
if (input.tool_name !== 'Bash') {
|
|
53
|
-
return { continue: true };
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
const bashInput = input.tool_input as {
|
|
57
|
-
command: string;
|
|
58
|
-
timeout?: number;
|
|
59
|
-
description?: string;
|
|
60
|
-
run_in_background?: boolean;
|
|
61
|
-
};
|
|
62
|
-
|
|
63
|
-
// Rewrite the command to go through term exec
|
|
64
|
-
const termCommand = `term exec ${target} '${escapeForSingleQuotes(bashInput.command)}'`;
|
|
65
|
-
|
|
66
|
-
// Create a brief description for the proxied command
|
|
67
|
-
const originalDesc = bashInput.description || bashInput.command.slice(0, 50);
|
|
68
|
-
const proxyDescription = `[term] ${originalDesc}${bashInput.command.length > 50 ? '...' : ''}`;
|
|
69
|
-
|
|
70
|
-
return {
|
|
71
|
-
continue: true,
|
|
72
|
-
hookSpecificOutput: {
|
|
73
|
-
hookEventName: 'PreToolUse',
|
|
74
|
-
updatedInput: {
|
|
75
|
-
command: termCommand,
|
|
76
|
-
timeout: bashInput.timeout,
|
|
77
|
-
description: proxyDescription,
|
|
78
|
-
run_in_background: bashInput.run_in_background,
|
|
79
|
-
},
|
|
80
|
-
permissionDecision: 'allow',
|
|
81
|
-
additionalContext: `Human can observe: tmux attach -t ${sessionName}`,
|
|
82
|
-
},
|
|
83
|
-
};
|
|
84
|
-
},
|
|
85
|
-
],
|
|
86
|
-
},
|
|
87
|
-
],
|
|
88
|
-
};
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
/**
|
|
92
|
-
* HookConfig type matching the Claude Agent SDK hook configuration
|
|
93
|
-
*/
|
|
94
|
-
export interface HookConfig {
|
|
95
|
-
PreToolUse?: Array<{
|
|
96
|
-
matcher: string | RegExp;
|
|
97
|
-
timeout?: number;
|
|
98
|
-
hooks: Array<HookCallback>;
|
|
99
|
-
}>;
|
|
100
|
-
PostToolUse?: Array<{
|
|
101
|
-
matcher?: string | RegExp;
|
|
102
|
-
timeout?: number;
|
|
103
|
-
hooks: Array<HookCallback>;
|
|
104
|
-
}>;
|
|
105
|
-
PermissionRequest?: Array<{
|
|
106
|
-
timeout?: number;
|
|
107
|
-
hooks: Array<HookCallback>;
|
|
108
|
-
}>;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
/**
|
|
112
|
-
* Hook input types
|
|
113
|
-
*/
|
|
114
|
-
export interface HookInput {
|
|
115
|
-
hook_event_name: string;
|
|
116
|
-
tool_name?: string;
|
|
117
|
-
tool_input?: unknown;
|
|
118
|
-
tool_response?: unknown;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
/**
|
|
122
|
-
* Hook callback type
|
|
123
|
-
*/
|
|
124
|
-
export type HookCallback = (
|
|
125
|
-
input: HookInput,
|
|
126
|
-
toolUseID: string | undefined,
|
|
127
|
-
options: { signal: AbortSignal }
|
|
128
|
-
) => Promise<HookOutput>;
|
|
129
|
-
|
|
130
|
-
/**
|
|
131
|
-
* Hook output type
|
|
132
|
-
*/
|
|
133
|
-
export interface HookOutput {
|
|
134
|
-
continue: boolean;
|
|
135
|
-
hookSpecificOutput?: {
|
|
136
|
-
hookEventName: string;
|
|
137
|
-
updatedInput?: unknown;
|
|
138
|
-
permissionDecision?: 'allow' | 'deny' | 'ask';
|
|
139
|
-
permissionDecisionReason?: string;
|
|
140
|
-
additionalContext?: string;
|
|
141
|
-
decision?: { behavior: 'allow' | 'deny' | 'ask' };
|
|
142
|
-
};
|
|
143
|
-
}
|
|
@@ -1,153 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Sandboxed Hook Preset
|
|
3
|
-
*
|
|
4
|
-
* Restricts file operations to specific directories.
|
|
5
|
-
* Intercepts PreToolUse events for file-related tools and denies operations
|
|
6
|
-
* outside the allowed paths.
|
|
7
|
-
*
|
|
8
|
-
* Usage:
|
|
9
|
-
* const hooks = sandboxedHooks({ allowedPaths: ['~/projects', '/tmp'] });
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
import { resolve, normalize } from 'path';
|
|
13
|
-
import { homedir } from 'os';
|
|
14
|
-
import type { SandboxedConfig } from '../../../types/genie-config.js';
|
|
15
|
-
import type { HookConfig, HookInput, HookOutput } from './collaborative.js';
|
|
16
|
-
|
|
17
|
-
export interface SandboxedHookConfig {
|
|
18
|
-
allowedPaths: string[];
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Default configuration for sandboxed hooks
|
|
23
|
-
*/
|
|
24
|
-
export const DEFAULT_SANDBOXED_CONFIG: SandboxedHookConfig = {
|
|
25
|
-
allowedPaths: ['~/projects', '/tmp'],
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Tools that access files
|
|
30
|
-
*/
|
|
31
|
-
const FILE_TOOLS = ['Read', 'Write', 'Edit', 'Glob', 'Grep'];
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Expand ~ to home directory
|
|
35
|
-
*/
|
|
36
|
-
function expandTilde(path: string): string {
|
|
37
|
-
if (path.startsWith('~/')) {
|
|
38
|
-
return homedir() + path.slice(1);
|
|
39
|
-
}
|
|
40
|
-
if (path === '~') {
|
|
41
|
-
return homedir();
|
|
42
|
-
}
|
|
43
|
-
return path;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* Normalize and resolve a path, expanding ~
|
|
48
|
-
*/
|
|
49
|
-
function normalizePath(path: string): string {
|
|
50
|
-
return normalize(resolve(expandTilde(path)));
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* Check if a path is within any of the allowed paths
|
|
55
|
-
*/
|
|
56
|
-
function isWithinAllowed(targetPath: string, allowedPaths: string[]): boolean {
|
|
57
|
-
const normalizedTarget = normalizePath(targetPath);
|
|
58
|
-
|
|
59
|
-
for (const allowed of allowedPaths) {
|
|
60
|
-
const normalizedAllowed = normalizePath(allowed);
|
|
61
|
-
|
|
62
|
-
// Check if target is within or equal to allowed path
|
|
63
|
-
if (
|
|
64
|
-
normalizedTarget === normalizedAllowed ||
|
|
65
|
-
normalizedTarget.startsWith(normalizedAllowed + '/')
|
|
66
|
-
) {
|
|
67
|
-
return true;
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
return false;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* Extract the path from tool input
|
|
76
|
-
*/
|
|
77
|
-
function extractPath(toolName: string, toolInput: unknown): string | null {
|
|
78
|
-
const input = toolInput as Record<string, unknown>;
|
|
79
|
-
|
|
80
|
-
switch (toolName) {
|
|
81
|
-
case 'Read':
|
|
82
|
-
case 'Write':
|
|
83
|
-
case 'Edit':
|
|
84
|
-
return (input.file_path as string) || null;
|
|
85
|
-
case 'Glob':
|
|
86
|
-
return (input.path as string) || process.cwd();
|
|
87
|
-
case 'Grep':
|
|
88
|
-
return (input.path as string) || process.cwd();
|
|
89
|
-
default:
|
|
90
|
-
return null;
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/**
|
|
95
|
-
* Create sandboxed hooks configuration
|
|
96
|
-
*
|
|
97
|
-
* This hook intercepts PreToolUse events for file-related tools and
|
|
98
|
-
* denies operations on paths outside the allowed directories.
|
|
99
|
-
*/
|
|
100
|
-
export function sandboxedHooks(config: Partial<SandboxedConfig> = {}): HookConfig {
|
|
101
|
-
const allowedPaths = config.allowedPaths || DEFAULT_SANDBOXED_CONFIG.allowedPaths;
|
|
102
|
-
|
|
103
|
-
// Create a regex pattern that matches all file tools
|
|
104
|
-
const fileToolPattern = new RegExp(`^(${FILE_TOOLS.join('|')})$`);
|
|
105
|
-
|
|
106
|
-
return {
|
|
107
|
-
PreToolUse: [
|
|
108
|
-
{
|
|
109
|
-
matcher: fileToolPattern,
|
|
110
|
-
timeout: 30,
|
|
111
|
-
hooks: [
|
|
112
|
-
async (input: HookInput, _toolUseID, _options): Promise<HookOutput> => {
|
|
113
|
-
// Type guard - only process PreToolUse events
|
|
114
|
-
if (input.hook_event_name !== 'PreToolUse') {
|
|
115
|
-
return { continue: true };
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
const toolName = input.tool_name;
|
|
119
|
-
if (!toolName || !FILE_TOOLS.includes(toolName)) {
|
|
120
|
-
return { continue: true };
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
const targetPath = extractPath(toolName, input.tool_input);
|
|
124
|
-
|
|
125
|
-
// If we can't extract a path, allow the operation
|
|
126
|
-
// (better to be permissive than block valid operations)
|
|
127
|
-
if (!targetPath) {
|
|
128
|
-
return { continue: true };
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
// Check if path is within allowed directories
|
|
132
|
-
if (!isWithinAllowed(targetPath, allowedPaths)) {
|
|
133
|
-
const allowedDisplay = allowedPaths.map((p) =>
|
|
134
|
-
p.startsWith('~') ? p : expandTilde(p)
|
|
135
|
-
).join(', ');
|
|
136
|
-
|
|
137
|
-
return {
|
|
138
|
-
continue: false,
|
|
139
|
-
hookSpecificOutput: {
|
|
140
|
-
hookEventName: 'PreToolUse',
|
|
141
|
-
permissionDecision: 'deny',
|
|
142
|
-
permissionDecisionReason: `Path "${targetPath}" is outside the sandbox. Allowed paths: ${allowedDisplay}`,
|
|
143
|
-
},
|
|
144
|
-
};
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
return { continue: true };
|
|
148
|
-
},
|
|
149
|
-
],
|
|
150
|
-
},
|
|
151
|
-
],
|
|
152
|
-
};
|
|
153
|
-
}
|
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Supervised Hook Preset
|
|
3
|
-
*
|
|
4
|
-
* Requires explicit approval for file modifications.
|
|
5
|
-
* Intercepts PermissionRequest events and forces 'ask' behavior for Write/Edit tools.
|
|
6
|
-
*
|
|
7
|
-
* Usage:
|
|
8
|
-
* const hooks = supervisedHooks({ alwaysAsk: ['Write', 'Edit'] });
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import type { SupervisedConfig } from '../../../types/genie-config.js';
|
|
12
|
-
import type { HookConfig, HookInput, HookOutput } from './collaborative.js';
|
|
13
|
-
|
|
14
|
-
export interface SupervisedHookConfig {
|
|
15
|
-
alwaysAsk: string[];
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Default configuration for supervised hooks
|
|
20
|
-
*/
|
|
21
|
-
export const DEFAULT_SUPERVISED_CONFIG: SupervisedHookConfig = {
|
|
22
|
-
alwaysAsk: ['Write', 'Edit'],
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Create supervised hooks configuration
|
|
27
|
-
*
|
|
28
|
-
* This hook intercepts PermissionRequest events and ensures that
|
|
29
|
-
* specified tools (default: Write, Edit) always require user approval.
|
|
30
|
-
*/
|
|
31
|
-
export function supervisedHooks(config: Partial<SupervisedConfig> = {}): HookConfig {
|
|
32
|
-
const alwaysAsk = config.alwaysAsk || DEFAULT_SUPERVISED_CONFIG.alwaysAsk;
|
|
33
|
-
const alwaysAskSet = new Set(alwaysAsk);
|
|
34
|
-
|
|
35
|
-
return {
|
|
36
|
-
PermissionRequest: [
|
|
37
|
-
{
|
|
38
|
-
timeout: 30,
|
|
39
|
-
hooks: [
|
|
40
|
-
async (input: HookInput, _toolUseID, _options): Promise<HookOutput> => {
|
|
41
|
-
// Type guard - only process PermissionRequest events
|
|
42
|
-
if (input.hook_event_name !== 'PermissionRequest') {
|
|
43
|
-
return { continue: true };
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
const toolName = input.tool_name;
|
|
47
|
-
|
|
48
|
-
// Check if this tool requires approval
|
|
49
|
-
if (toolName && alwaysAskSet.has(toolName)) {
|
|
50
|
-
return {
|
|
51
|
-
continue: true,
|
|
52
|
-
hookSpecificOutput: {
|
|
53
|
-
hookEventName: 'PermissionRequest',
|
|
54
|
-
decision: { behavior: 'ask' },
|
|
55
|
-
},
|
|
56
|
-
};
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
// Allow other tools to proceed normally
|
|
60
|
-
return { continue: true };
|
|
61
|
-
},
|
|
62
|
-
],
|
|
63
|
-
},
|
|
64
|
-
],
|
|
65
|
-
};
|
|
66
|
-
}
|
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Shell escaping utilities for safely embedding commands in shell strings.
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Escape a string for safe embedding in single-quoted shell strings.
|
|
7
|
-
* e.g., "it's" becomes "it'\''s"
|
|
8
|
-
*
|
|
9
|
-
* This works by:
|
|
10
|
-
* 1. Ending the current single-quoted string: '
|
|
11
|
-
* 2. Adding an escaped single quote: \'
|
|
12
|
-
* 3. Starting a new single-quoted string: '
|
|
13
|
-
*
|
|
14
|
-
* So "it's cool" becomes 'it'\''s cool'
|
|
15
|
-
*/
|
|
16
|
-
export function escapeForSingleQuotes(str: string): string {
|
|
17
|
-
return str.replace(/'/g, "'\\''");
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Escape a string for safe embedding in double-quoted shell strings.
|
|
22
|
-
* Escapes: $ ` " \ and newlines
|
|
23
|
-
*/
|
|
24
|
-
export function escapeForDoubleQuotes(str: string): string {
|
|
25
|
-
return str
|
|
26
|
-
.replace(/\\/g, '\\\\')
|
|
27
|
-
.replace(/"/g, '\\"')
|
|
28
|
-
.replace(/\$/g, '\\$')
|
|
29
|
-
.replace(/`/g, '\\`')
|
|
30
|
-
.replace(/\n/g, '\\n');
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Wrap a command in single quotes with proper escaping.
|
|
35
|
-
* This is the safest way to pass arbitrary commands to shell.
|
|
36
|
-
*/
|
|
37
|
-
export function singleQuote(cmd: string): string {
|
|
38
|
-
return `'${escapeForSingleQuotes(cmd)}'`;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Wrap a command in double quotes with proper escaping.
|
|
43
|
-
*/
|
|
44
|
-
export function doubleQuote(cmd: string): string {
|
|
45
|
-
return `"${escapeForDoubleQuotes(cmd)}"`;
|
|
46
|
-
}
|