Add initial project files and configurations for clawd_code
This commit is contained in:
@@ -0,0 +1,115 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'hook_types.dart';
|
||||
|
||||
/// Context passed to hooks for inspection/modification
|
||||
class HookContext {
|
||||
/// The kind of hook being executed
|
||||
final HookKind kind;
|
||||
|
||||
/// Name of the target (tool, command, etc)
|
||||
final String? targetName;
|
||||
|
||||
/// tool/command input as JSON
|
||||
final Map<String, dynamic>? input;
|
||||
|
||||
/// Tool output (for post-tool hooks)
|
||||
final dynamic output;
|
||||
|
||||
/// Exit code from command
|
||||
final int? exitCode;
|
||||
|
||||
/// Environment variables available to hook
|
||||
final Map<String, String> environment;
|
||||
|
||||
/// Additional context about the operation
|
||||
final Map<String, dynamic> metadata;
|
||||
|
||||
HookContext({
|
||||
required this.kind,
|
||||
this.targetName,
|
||||
this.input,
|
||||
this.output,
|
||||
this.exitCode,
|
||||
Map<String, String>? environment,
|
||||
Map<String, dynamic>? metadata,
|
||||
}) : environment = environment ?? {},
|
||||
metadata = metadata ?? {};
|
||||
|
||||
/// Convenient method to get display name
|
||||
String get kindName => kind.displayName;
|
||||
|
||||
/// Convert context to JSON for passing to shell commands
|
||||
String toJsonString() {
|
||||
return jsonEncode({
|
||||
'hook_kind': kindName,
|
||||
if (targetName != null) 'target': targetName,
|
||||
if (input != null) 'input': input,
|
||||
if (output != null) 'output': output,
|
||||
if (exitCode != null) 'exit_code': exitCode,
|
||||
...metadata,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Result returned from executing a hook
|
||||
class HookResult {
|
||||
/// Whether the hook completed successfully
|
||||
final bool success;
|
||||
|
||||
/// Output from the hook
|
||||
final String? stdout;
|
||||
final String? stderr;
|
||||
|
||||
/// Exit code (for command hooks)
|
||||
final int? exitCode;
|
||||
|
||||
/// Whether hook wants to continue or block (if applicable)
|
||||
final bool? shouldContinue;
|
||||
|
||||
/// Custom message to display
|
||||
final String? message;
|
||||
|
||||
/// Hook-specific output for processing
|
||||
final Map<String, dynamic>? hookOutput;
|
||||
|
||||
HookResult({
|
||||
required this.success,
|
||||
this.stdout,
|
||||
this.stderr,
|
||||
this.exitCode,
|
||||
this.shouldContinue,
|
||||
this.message,
|
||||
this.hookOutput,
|
||||
});
|
||||
|
||||
/// Parse hook output JSON (for structured responses)
|
||||
static HookResult fromJson(
|
||||
String jsonStr, {
|
||||
required int exitCode,
|
||||
required String stdout,
|
||||
required String stderr,
|
||||
}) {
|
||||
try {
|
||||
final parsed = jsonDecode(jsonStr) as Map<String, dynamic>;
|
||||
return HookResult(
|
||||
success: exitCode == 0,
|
||||
stdout: stdout,
|
||||
stderr: stderr,
|
||||
exitCode: exitCode,
|
||||
shouldContinue: parsed['continue'] as bool? ?? true,
|
||||
message: parsed['message'] as String?,
|
||||
hookOutput: parsed,
|
||||
);
|
||||
} catch (e) {
|
||||
return HookResult(
|
||||
success: exitCode == 0,
|
||||
stdout: stdout,
|
||||
stderr: stderr,
|
||||
exitCode: exitCode,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String get displayOutput => stdout ?? stderr ?? '';
|
||||
}
|
||||
@@ -0,0 +1,260 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'hook_types.dart';
|
||||
|
||||
/// Loads hook configuration from ~/.claude/hooks.yaml or hooks.json
|
||||
class HookLoader {
|
||||
/// Load hooks from config files
|
||||
static Future<List<HookSpec>> loadHooks() async {
|
||||
final homeDir = Platform.environment['HOME'];
|
||||
if (homeDir == null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
final claudeDir = Directory('$homeDir/.claude');
|
||||
final hooks = <HookSpec>[];
|
||||
|
||||
// Try JSON first
|
||||
final jsonFile = File('${claudeDir.path}/hooks.json');
|
||||
if (jsonFile.existsSync()) {
|
||||
try {
|
||||
final content = await jsonFile.readAsString();
|
||||
final data = jsonDecode(content) as Map<String, dynamic>;
|
||||
hooks.addAll(_parseHooksFromConfig(data));
|
||||
return hooks;
|
||||
} catch (e) {
|
||||
print('error loading hooks.json: $e');
|
||||
}
|
||||
}
|
||||
|
||||
// Try YAML (basic parsing without external deps)
|
||||
final yamlFile = File('${claudeDir.path}/hooks.yaml');
|
||||
if (yamlFile.existsSync()) {
|
||||
try {
|
||||
final content = await yamlFile.readAsString();
|
||||
// Basic YAML-to-JSON conversion for hooks config
|
||||
hooks.addAll(_parseHooksFromYaml(content));
|
||||
return hooks;
|
||||
} catch (e) {
|
||||
print('error loading hooks.yaml: $e');
|
||||
}
|
||||
}
|
||||
|
||||
return hooks;
|
||||
}
|
||||
|
||||
/// Parse hooks from JSON config
|
||||
static List<HookSpec> _parseHooksFromConfig(Map<String, dynamic> config) {
|
||||
final hooks = <HookSpec>[];
|
||||
|
||||
config.forEach((eventName, matchers) {
|
||||
final kind = _hookKindFromString(eventName);
|
||||
if (kind == null) return;
|
||||
|
||||
if (matchers is! List) return;
|
||||
|
||||
for (final matcher in matchers) {
|
||||
if (matcher is! Map<String, dynamic>) continue;
|
||||
|
||||
final matcherString = matcher['matcher'] as String?;
|
||||
final hooksList = matcher['hooks'] as List?;
|
||||
|
||||
if (hooksList == null) continue;
|
||||
|
||||
for (final hookData in hooksList) {
|
||||
if (hookData is! Map<String, dynamic>) continue;
|
||||
|
||||
try {
|
||||
final hook = _parseHookCommand(hookData);
|
||||
if (hook != null) {
|
||||
hooks.add(HookSpec(
|
||||
kind: kind,
|
||||
command: hook,
|
||||
target: matcherString,
|
||||
));
|
||||
}
|
||||
} catch (e) {
|
||||
print('error parsing hook: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return hooks;
|
||||
}
|
||||
|
||||
/// Parse hooks from YAML content (basic parser)
|
||||
static List<HookSpec> _parseHooksFromYaml(String content) {
|
||||
// this is a simplistic parser for basic hook YAML
|
||||
// for complex YAML, users should use hooks.json instead
|
||||
final hooks = <HookSpec>[];
|
||||
|
||||
// quick and dirty YAML parsing
|
||||
// look for pattern: EventName: and then " - matcher:" blocks
|
||||
final lines = content.split('\n');
|
||||
HookKind? currentKind;
|
||||
String? currentMatcher;
|
||||
|
||||
for (int i = 0; i < lines.length; i++) {
|
||||
final line = lines[i];
|
||||
final trimmed = line.trim();
|
||||
|
||||
// Check for hook event (no leading spaces)
|
||||
if (!line.startsWith(' ')) {
|
||||
final kindMatch = trimmed.split(':')[0];
|
||||
currentKind = _hookKindFromString(kindMatch);
|
||||
currentMatcher = null;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (currentKind == null) continue;
|
||||
|
||||
// Check for matcher
|
||||
if (trimmed.startsWith('- matcher:')) {
|
||||
currentMatcher = trimmed.substring(10).trim();
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check for hooks array
|
||||
if (trimmed.startsWith('hooks:')) {
|
||||
// next non-empty indented lines are hooks
|
||||
i++;
|
||||
while (i < lines.length) {
|
||||
final hookLine = lines[i];
|
||||
if (!hookLine.startsWith(' ')) break;
|
||||
|
||||
final hookTrimmed = hookLine.trim();
|
||||
if (hookTrimmed.startsWith('- type:')) {
|
||||
// parse individual hook
|
||||
final hookBlock = <String>[];
|
||||
hookBlock.add(hookTrimmed);
|
||||
|
||||
// collect remaining lines for this hook
|
||||
i++;
|
||||
while (i < lines.length) {
|
||||
final nextLine = lines[i];
|
||||
if (nextLine.trim().isEmpty) {
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
if (!nextLine.startsWith(' ')) break;
|
||||
hookBlock.add(nextLine.trim());
|
||||
i++;
|
||||
}
|
||||
i--; // back up one since the outer loop will increment
|
||||
|
||||
try {
|
||||
final hook = _parseHookFromYamlBlock(hookBlock);
|
||||
if (hook != null) {
|
||||
hooks.add(HookSpec(
|
||||
kind: currentKind,
|
||||
command: hook,
|
||||
target: currentMatcher,
|
||||
));
|
||||
}
|
||||
} catch (e) {
|
||||
print('error parsing YAML hook: $e');
|
||||
}
|
||||
} else {
|
||||
i++;
|
||||
}
|
||||
}
|
||||
i--;
|
||||
}
|
||||
}
|
||||
|
||||
return hooks;
|
||||
}
|
||||
|
||||
/// Parse single hook from YAML block lines
|
||||
static HookCommand? _parseHookFromYamlBlock(List<String> lines) {
|
||||
final map = <String, dynamic>{};
|
||||
|
||||
for (final line in lines) {
|
||||
final colonIndex = line.indexOf(':');
|
||||
if (colonIndex == -1) continue;
|
||||
|
||||
final key = line.substring(0, colonIndex).trim();
|
||||
final value = line.substring(colonIndex + 1).trim();
|
||||
|
||||
// handle basic value types
|
||||
if (value == 'true' || value == 'false') {
|
||||
map[key] = value == 'true';
|
||||
} else if (value.startsWith('[') && value.endsWith(']')) {
|
||||
// simple array parsing
|
||||
final items = value
|
||||
.substring(1, value.length - 1)
|
||||
.split(',')
|
||||
.map((s) => s.trim())
|
||||
.toList();
|
||||
map[key] = items;
|
||||
} else {
|
||||
// remove quotes if present
|
||||
String stringValue = value;
|
||||
if ((stringValue.startsWith('"') && stringValue.endsWith('"')) ||
|
||||
(stringValue.startsWith("'") && stringValue.endsWith("'"))) {
|
||||
stringValue = stringValue.substring(1, stringValue.length - 1);
|
||||
}
|
||||
// try to parse as number
|
||||
final numValue = num.tryParse(stringValue);
|
||||
map[key] = numValue ?? stringValue;
|
||||
}
|
||||
}
|
||||
|
||||
return _parseHookCommand(map);
|
||||
}
|
||||
|
||||
/// Parse a single hook command from a map
|
||||
static HookCommand? _parseHookCommand(Map<String, dynamic> data) {
|
||||
final type = data['type'] as String?;
|
||||
|
||||
switch (type) {
|
||||
case 'command':
|
||||
return BashCommandHook.fromMap(data);
|
||||
case 'prompt':
|
||||
return PromptHook.fromMap(data);
|
||||
case 'http':
|
||||
return HttpHook.fromMap(data);
|
||||
case 'agent':
|
||||
return AgentHook.fromMap(data);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert hook kind string to enum
|
||||
static HookKind? _hookKindFromString(String name) {
|
||||
const mapping = {
|
||||
'PreToolUse': HookKind.preToolUse,
|
||||
'PostToolUse': HookKind.postToolUse,
|
||||
'PostToolUseFailure': HookKind.postToolUseFailure,
|
||||
'PermissionDenied': HookKind.permissionDenied,
|
||||
'Notification': HookKind.notification,
|
||||
'UserPromptSubmit': HookKind.userPromptSubmit,
|
||||
'SessionStart': HookKind.sessionStart,
|
||||
'SessionEnd': HookKind.sessionEnd,
|
||||
'Stop': HookKind.stop,
|
||||
'StopFailure': HookKind.stopFailure,
|
||||
'SubagentStart': HookKind.subagentStart,
|
||||
'SubagentStop': HookKind.subagentStop,
|
||||
'PreCompact': HookKind.preCompact,
|
||||
'PostCompact': HookKind.postCompact,
|
||||
'PermissionRequest': HookKind.permissionRequest,
|
||||
'Setup': HookKind.setup,
|
||||
'TeammateIdle': HookKind.teammateIdle,
|
||||
'TaskCreated': HookKind.taskCreated,
|
||||
'TaskCompleted': HookKind.taskCompleted,
|
||||
'Elicitation': HookKind.elicitation,
|
||||
'ElicitationResult': HookKind.elicitationResult,
|
||||
'ConfigChange': HookKind.configChange,
|
||||
'InstructionsLoaded': HookKind.instructionsLoaded,
|
||||
'WorktreeCreate': HookKind.worktreeCreate,
|
||||
'WorktreeRemove': HookKind.worktreeRemove,
|
||||
'CwdChanged': HookKind.cwdChanged,
|
||||
'FileChanged': HookKind.fileChanged,
|
||||
};
|
||||
|
||||
return mapping[name];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,220 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'hook_context.dart';
|
||||
import 'hook_types.dart';
|
||||
|
||||
/// Executes hooks based on kind and optional target filtering
|
||||
class HookRunner {
|
||||
static const _defaultShell = 'bash';
|
||||
static const _defaultTimeoutSecs = 10 * 60;
|
||||
|
||||
final List<HookSpec> hooks;
|
||||
|
||||
HookRunner({required this.hooks});
|
||||
|
||||
/// Run hooks matching the given kind and optional target
|
||||
Future<List<HookResult>> runHooksForKind(
|
||||
HookKind kind, {
|
||||
String? targetName,
|
||||
Map<String, dynamic>? input,
|
||||
dynamic output,
|
||||
int? exitCode,
|
||||
Map<String, String>? environment,
|
||||
Map<String, dynamic>? metadata,
|
||||
}) async {
|
||||
final results = <HookResult>[];
|
||||
|
||||
// Filter hooks by kind and optional target
|
||||
final matchingHooks = hooks.where((h) {
|
||||
if (h.kind != kind) return false;
|
||||
// If target is required, filter by target
|
||||
if (targetName != null && h.target != null) {
|
||||
return h.target == targetName;
|
||||
}
|
||||
return true;
|
||||
}).toList();
|
||||
|
||||
for (final hookSpec in matchingHooks) {
|
||||
// Evaluate condition if present
|
||||
if (hookSpec.command.ifCondition != null) {
|
||||
if (!_evaluateCondition(hookSpec.command.ifCondition!)) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
final context = HookContext(
|
||||
kind: kind,
|
||||
targetName: targetName,
|
||||
input: input,
|
||||
output: output,
|
||||
exitCode: exitCode,
|
||||
environment: environment,
|
||||
metadata: metadata,
|
||||
);
|
||||
|
||||
try {
|
||||
final result = await _executeHook(hookSpec, context);
|
||||
results.add(result);
|
||||
|
||||
// If hook wants to stop, break
|
||||
if (result.hookOutput != null) {
|
||||
final shouldContinue = result.hookOutput!['continue'] as bool? ?? true;
|
||||
if (!shouldContinue) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
print('error executing hook: $e');
|
||||
results.add(HookResult(
|
||||
success: false,
|
||||
stderr: 'error: $e',
|
||||
exitCode: 1,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
/// Execute a single hook
|
||||
Future<HookResult> _executeHook(HookSpec spec, HookContext context) async {
|
||||
final command = spec.command;
|
||||
|
||||
if (command is BashCommandHook) {
|
||||
return _executeBashHook(command, context);
|
||||
} else if (command is HttpHook) {
|
||||
return _executeHttpHook(command, context);
|
||||
} else if (command is PromptHook) {
|
||||
// prompt hooks not implemented yet in CLI version
|
||||
return HookResult(success: true);
|
||||
} else if (command is AgentHook) {
|
||||
// agent hooks not implemented yet in CLI version
|
||||
return HookResult(success: true);
|
||||
}
|
||||
|
||||
return HookResult(success: false, stderr: 'unsupported hook type');
|
||||
}
|
||||
|
||||
/// Execute a bash command hook
|
||||
Future<HookResult> _executeBashHook(
|
||||
BashCommandHook hook,
|
||||
HookContext context,
|
||||
) async {
|
||||
final shell = hook.shell ?? _defaultShell;
|
||||
final timeout =
|
||||
Duration(seconds: (hook.timeout?.toInt() ?? _defaultTimeoutSecs));
|
||||
|
||||
// prepare environment
|
||||
final env = Map<String, String>.from(Platform.environment);
|
||||
env.addAll(context.environment);
|
||||
|
||||
// Pass context as JSON via environment
|
||||
env['HOOK_INPUT'] = context.toJsonString();
|
||||
|
||||
try {
|
||||
final process = await Process.start(
|
||||
shell,
|
||||
['-c', hook.command],
|
||||
environment: env,
|
||||
workingDirectory: Directory.current.path,
|
||||
);
|
||||
|
||||
// Capture output
|
||||
final stdout = StringBuffer();
|
||||
final stderr = StringBuffer();
|
||||
|
||||
process.stdout.transform(utf8.decoder).listen((data) {
|
||||
stdout.write(data);
|
||||
});
|
||||
|
||||
process.stderr.transform(utf8.decoder).listen((data) {
|
||||
stderr.write(data);
|
||||
});
|
||||
|
||||
// Wait for process with timeout
|
||||
final exitCode = await process.exitCode.timeout(
|
||||
timeout,
|
||||
onTimeout: () {
|
||||
process.kill();
|
||||
return 124; // timeout exit code
|
||||
},
|
||||
);
|
||||
|
||||
final stdoutStr = stdout.toString();
|
||||
final stderrStr = stderr.toString();
|
||||
|
||||
return HookResult.fromJson(
|
||||
stdoutStr,
|
||||
exitCode: exitCode,
|
||||
stdout: stdoutStr,
|
||||
stderr: stderrStr,
|
||||
);
|
||||
} catch (e) {
|
||||
return HookResult(
|
||||
success: false,
|
||||
stderr: 'failed to execute hook: $e',
|
||||
exitCode: 1,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Execute an HTTP hook
|
||||
Future<HookResult> _executeHttpHook(
|
||||
HttpHook hook,
|
||||
HookContext context,
|
||||
) async {
|
||||
try {
|
||||
final client = HttpClient();
|
||||
final timeout = Duration(
|
||||
seconds: (hook.timeout?.toInt() ?? _defaultTimeoutSecs),
|
||||
);
|
||||
|
||||
final request = await client.postUrl(Uri.parse(hook.url)).timeout(timeout);
|
||||
|
||||
// add headers
|
||||
if (hook.headers != null) {
|
||||
hook.headers!.forEach((key, value) {
|
||||
// interpolate env vars if allowed
|
||||
String finalValue = value;
|
||||
if (hook.allowedEnvVars != null) {
|
||||
for (final varName in hook.allowedEnvVars!) {
|
||||
final pattern = RegExp(r'\$\{?$varName\}?');
|
||||
final envValue = Platform.environment[varName] ?? '';
|
||||
finalValue = finalValue.replaceAll(pattern, envValue);
|
||||
}
|
||||
}
|
||||
request.headers.set(key, finalValue);
|
||||
});
|
||||
}
|
||||
|
||||
request.headers.set('Content-Type', 'application/json');
|
||||
|
||||
// send context as JSON body
|
||||
request.write(context.toJsonString());
|
||||
|
||||
final response = await request.close().timeout(timeout);
|
||||
final responseBody = await response.transform(utf8.decoder).join();
|
||||
|
||||
return HookResult(
|
||||
success: response.statusCode == 200,
|
||||
stdout: responseBody,
|
||||
exitCode: response.statusCode,
|
||||
);
|
||||
} catch (e) {
|
||||
return HookResult(
|
||||
success: false,
|
||||
stderr: 'HTTP hook failed: $e',
|
||||
exitCode: 1,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Evaluate condition expression (simple placeholder implementation)
|
||||
/// Full implementation would parse permission rule syntax
|
||||
bool _evaluateCondition(String condition) {
|
||||
// basic placeholder: if condition exists and is non-empty, evaluate to true
|
||||
// full implementation should parse pattern like "Bash(git *)" or "Read(*.ts)"
|
||||
return condition.isNotEmpty;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,261 @@
|
||||
enum HookKind {
|
||||
preToolUse,
|
||||
postToolUse,
|
||||
postToolUseFailure,
|
||||
permissionDenied,
|
||||
notification,
|
||||
userPromptSubmit,
|
||||
sessionStart,
|
||||
sessionEnd,
|
||||
stop,
|
||||
stopFailure,
|
||||
subagentStart,
|
||||
subagentStop,
|
||||
preCompact,
|
||||
postCompact,
|
||||
permissionRequest,
|
||||
setup,
|
||||
teammateIdle,
|
||||
taskCreated,
|
||||
taskCompleted,
|
||||
elicitation,
|
||||
elicitationResult,
|
||||
configChange,
|
||||
instructionsLoaded,
|
||||
worktreeCreate,
|
||||
worktreeRemove,
|
||||
cwdChanged,
|
||||
fileChanged,
|
||||
}
|
||||
|
||||
extension HookKindExt on HookKind {
|
||||
String get displayName {
|
||||
const names = {
|
||||
HookKind.preToolUse: 'PreToolUse',
|
||||
HookKind.postToolUse: 'PostToolUse',
|
||||
HookKind.postToolUseFailure: 'PostToolUseFailure',
|
||||
HookKind.permissionDenied: 'PermissionDenied',
|
||||
HookKind.notification: 'Notification',
|
||||
HookKind.userPromptSubmit: 'UserPromptSubmit',
|
||||
HookKind.sessionStart: 'SessionStart',
|
||||
HookKind.sessionEnd: 'SessionEnd',
|
||||
HookKind.stop: 'Stop',
|
||||
HookKind.stopFailure: 'StopFailure',
|
||||
HookKind.subagentStart: 'SubagentStart',
|
||||
HookKind.subagentStop: 'SubagentStop',
|
||||
HookKind.preCompact: 'PreCompact',
|
||||
HookKind.postCompact: 'PostCompact',
|
||||
HookKind.permissionRequest: 'PermissionRequest',
|
||||
HookKind.setup: 'Setup',
|
||||
HookKind.teammateIdle: 'TeammateIdle',
|
||||
HookKind.taskCreated: 'TaskCreated',
|
||||
HookKind.taskCompleted: 'TaskCompleted',
|
||||
HookKind.elicitation: 'Elicitation',
|
||||
HookKind.elicitationResult: 'ElicitationResult',
|
||||
HookKind.configChange: 'ConfigChange',
|
||||
HookKind.instructionsLoaded: 'InstructionsLoaded',
|
||||
HookKind.worktreeCreate: 'WorktreeCreate',
|
||||
HookKind.worktreeRemove: 'WorktreeRemove',
|
||||
HookKind.cwdChanged: 'CwdChanged',
|
||||
HookKind.fileChanged: 'FileChanged',
|
||||
};
|
||||
return names[this] ?? 'Unknown';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// hook command types
|
||||
sealed class HookCommand {
|
||||
final String? ifCondition;
|
||||
final double? timeout;
|
||||
final String? statusMessage;
|
||||
final bool? once;
|
||||
|
||||
HookCommand({
|
||||
this.ifCondition,
|
||||
this.timeout,
|
||||
this.statusMessage,
|
||||
this.once,
|
||||
});
|
||||
}
|
||||
|
||||
class BashCommandHook extends HookCommand {
|
||||
final String command;
|
||||
final String? shell;
|
||||
final bool? async;
|
||||
final bool? asyncRewake;
|
||||
|
||||
BashCommandHook({
|
||||
required this.command,
|
||||
this.shell,
|
||||
this.async,
|
||||
this.asyncRewake,
|
||||
super.ifCondition,
|
||||
super.timeout,
|
||||
super.statusMessage,
|
||||
super.once,
|
||||
});
|
||||
|
||||
factory BashCommandHook.fromMap(Map<String, dynamic> map) {
|
||||
return BashCommandHook(
|
||||
command: map['command'] as String,
|
||||
shell: map['shell'] as String?,
|
||||
async: map['async'] as bool?,
|
||||
asyncRewake: map['asyncRewake'] as bool?,
|
||||
ifCondition: map['if'] as String?,
|
||||
timeout: (map['timeout'] as num?)?.toDouble(),
|
||||
statusMessage: map['statusMessage'] as String?,
|
||||
once: map['once'] as bool?,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toMap() => {
|
||||
'type': 'command',
|
||||
'command': command,
|
||||
if (shell != null) 'shell': shell,
|
||||
if (async != null) 'async': async,
|
||||
if (asyncRewake != null) 'asyncRewake': asyncRewake,
|
||||
if (ifCondition != null) 'if': ifCondition,
|
||||
if (timeout != null) 'timeout': timeout,
|
||||
if (statusMessage != null) 'statusMessage': statusMessage,
|
||||
if (once != null) 'once': once,
|
||||
};
|
||||
}
|
||||
|
||||
class PromptHook extends HookCommand {
|
||||
final String prompt;
|
||||
final String? model;
|
||||
|
||||
PromptHook({
|
||||
required this.prompt,
|
||||
this.model,
|
||||
super.ifCondition,
|
||||
super.timeout,
|
||||
super.statusMessage,
|
||||
super.once,
|
||||
});
|
||||
|
||||
factory PromptHook.fromMap(Map<String, dynamic> map) {
|
||||
return PromptHook(
|
||||
prompt: map['prompt'] as String,
|
||||
model: map['model'] as String?,
|
||||
ifCondition: map['if'] as String?,
|
||||
timeout: (map['timeout'] as num?)?.toDouble(),
|
||||
statusMessage: map['statusMessage'] as String?,
|
||||
once: map['once'] as bool?,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toMap() => {
|
||||
'type': 'prompt',
|
||||
'prompt': prompt,
|
||||
if (model != null) 'model': model,
|
||||
if (ifCondition != null) 'if': ifCondition,
|
||||
if (timeout != null) 'timeout': timeout,
|
||||
if (statusMessage != null) 'statusMessage': statusMessage,
|
||||
if (once != null) 'once': once,
|
||||
};
|
||||
}
|
||||
|
||||
class HttpHook extends HookCommand {
|
||||
final String url;
|
||||
final Map<String, String>? headers;
|
||||
final List<String>? allowedEnvVars;
|
||||
|
||||
HttpHook({
|
||||
required this.url,
|
||||
this.headers,
|
||||
this.allowedEnvVars,
|
||||
super.ifCondition,
|
||||
super.timeout,
|
||||
super.statusMessage,
|
||||
super.once,
|
||||
});
|
||||
|
||||
factory HttpHook.fromMap(Map<String, dynamic> map) {
|
||||
return HttpHook(
|
||||
url: map['url'] as String,
|
||||
headers: (map['headers'] as Map<String, dynamic>?)?.cast<String, String>(),
|
||||
allowedEnvVars: (map['allowedEnvVars'] as List<dynamic>?)
|
||||
?.map((e) => e as String)
|
||||
.toList(),
|
||||
ifCondition: map['if'] as String?,
|
||||
timeout: (map['timeout'] as num?)?.toDouble(),
|
||||
statusMessage: map['statusMessage'] as String?,
|
||||
once: map['once'] as bool?,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toMap() => {
|
||||
'type': 'http',
|
||||
'url': url,
|
||||
if (headers != null) 'headers': headers,
|
||||
if (allowedEnvVars != null) 'allowedEnvVars': allowedEnvVars,
|
||||
if (ifCondition != null) 'if': ifCondition,
|
||||
if (timeout != null) 'timeout': timeout,
|
||||
if (statusMessage != null) 'statusMessage': statusMessage,
|
||||
if (once != null) 'once': once,
|
||||
};
|
||||
}
|
||||
|
||||
class AgentHook extends HookCommand {
|
||||
final String prompt;
|
||||
final String? model;
|
||||
|
||||
AgentHook({
|
||||
required this.prompt,
|
||||
this.model,
|
||||
super.ifCondition,
|
||||
super.timeout,
|
||||
super.statusMessage,
|
||||
super.once,
|
||||
});
|
||||
|
||||
factory AgentHook.fromMap(Map<String, dynamic> map) {
|
||||
return AgentHook(
|
||||
prompt: map['prompt'] as String,
|
||||
model: map['model'] as String?,
|
||||
ifCondition: map['if'] as String?,
|
||||
timeout: (map['timeout'] as num?)?.toDouble(),
|
||||
statusMessage: map['statusMessage'] as String?,
|
||||
once: map['once'] as bool?,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toMap() => {
|
||||
'type': 'agent',
|
||||
'prompt': prompt,
|
||||
if (model != null) 'model': model,
|
||||
if (ifCondition != null) 'if': ifCondition,
|
||||
if (timeout != null) 'timeout': timeout,
|
||||
if (statusMessage != null) 'statusMessage': statusMessage,
|
||||
if (once != null) 'once': once,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
// hook spec represents a parsed hook configuration
|
||||
class HookSpec {
|
||||
final HookKind kind;
|
||||
final HookCommand command;
|
||||
final String? target; // for hooks with matchers (tool_name, notification_type, etc)
|
||||
|
||||
HookSpec({
|
||||
required this.kind,
|
||||
required this.command,
|
||||
this.target,
|
||||
});
|
||||
|
||||
String getDisplayText() {
|
||||
if (command is BashCommandHook) {
|
||||
return (command as BashCommandHook).command;
|
||||
} else if (command is PromptHook) {
|
||||
return (command as PromptHook).prompt;
|
||||
} else if (command is AgentHook) {
|
||||
return (command as AgentHook).prompt;
|
||||
} else if (command is HttpHook) {
|
||||
return (command as HttpHook).url;
|
||||
}
|
||||
return command.statusMessage ?? 'unknown';
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user