Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ const result = row * GRID_COLS + grid_col; // Works correctly
## Claude Socket Hook
- The app creates `${XDG_RUNTIME_DIR:-/tmp}/architect_notify_<pid>.sock` and sets `ARCHITECT_SESSION_ID`/`ARCHITECT_NOTIFY_SOCK` for each shell.
- Send a single JSON line to signal UI states: `{"session":N,"state":"start"|"awaiting_approval"|"done"}`. The helper `scripts/architect_notify.py` is available if needed.
- Use `architect hook install claude` (or `codex`/`gemini`) to automatically set up hooks for AI assistants.

## Done? Share
- Provide a concise summary of edits, test/build outcomes, and documentation updates.
Expand Down
23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,29 @@ just build
- Cmd+Click opening for OSC 8 hyperlinks
- AI assistant status highlights (awaiting approval / done)
- Kitty keyboard protocol support for enhanced key handling
- Built-in CLI for managing AI assistant hooks

## CLI Commands

Architect includes CLI commands for managing AI assistant hooks:

```bash
# Install hook for an AI assistant
architect hook install claude # Claude Code
architect hook install codex # OpenAI Codex
architect hook install gemini # Google Gemini CLI

# Remove an installed hook
architect hook uninstall claude

# Check installation status
architect hook status

# Show help
architect help
```

The hooks enable visual status indicators in Architect when AI assistants are waiting for approval or have completed tasks.

## Configuration

Expand Down
8 changes: 6 additions & 2 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,9 @@ Architect is a terminal multiplexer displaying interactive sessions in a grid wi

## Runtime Flow

**main.zig** owns application lifetime, window sizing, PTY/session startup, configuration persistence, and the frame loop. Each frame it:
**main.zig** first checks command-line arguments via `cli.zig`. If a CLI command is detected (e.g., `architect hook install claude`), it handles the command and exits without starting the GUI. Otherwise, it proceeds with the normal application lifecycle.

In **GUI mode**, main.zig owns application lifetime, window sizing, PTY/session startup, configuration persistence, and the frame loop. Each frame it:

1. Polls SDL events and scales coordinates to render space.
2. Builds a lightweight `UiHost` snapshot and lets `UiRoot` handle events first.
Expand Down Expand Up @@ -72,7 +74,9 @@ Architect is a terminal multiplexer displaying interactive sessions in a grid wi

```
src/
├── main.zig # Entry point, frame loop, event dispatch
├── main.zig # Entry point, frame loop, event dispatch, CLI routing
├── cli.zig # CLI argument parser (hook, help, version commands)
├── hook_manager.zig # AI assistant hook installation/uninstallation
├── c.zig # C bindings (SDL3, TTF, etc.)
├── colors.zig # Theme and color palette management (ANSI 16/256)
├── config.zig # TOML config persistence
Expand Down
29 changes: 29 additions & 0 deletions docs/development.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,35 @@ Each release includes:

Architect exposes a Unix domain socket to let external tools (Claude Code, Codex, Gemini CLI, etc.) signal UI states.

### Automated Installation

The easiest way to set up hooks is using the built-in CLI commands:

```bash
# Install hook for Claude Code
architect hook install claude

# Install hook for Codex
architect hook install codex

# Install hook for Gemini CLI
architect hook install gemini

# Check which hooks are installed
architect hook status

# Uninstall a hook
architect hook uninstall claude
```

These commands automatically:
1. Copy the notification scripts to the tool's config directory
2. Update the tool's configuration file with the appropriate hooks

### Manual Installation

If you prefer manual setup or need custom configuration, follow the instructions below.

### Socket Protocol

- Socket: `${XDG_RUNTIME_DIR:-/tmp}/architect_notify_<pid>.sock`
Expand Down
210 changes: 210 additions & 0 deletions src/cli.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
// CLI argument parser for Architect subcommands.
// Currently supports: `architect hook install|uninstall|status [tool]`
const std = @import("std");

pub const Tool = enum {
claude,
codex,
gemini,

pub fn displayName(self: Tool) []const u8 {
return switch (self) {
.claude => "Claude Code",
.codex => "Codex",
.gemini => "Gemini CLI",
};
}

pub fn configDir(self: Tool) []const u8 {
return switch (self) {
.claude => ".claude",
.codex => ".codex",
.gemini => ".gemini",
};
}

pub fn fromString(s: []const u8) ?Tool {
if (std.mem.eql(u8, s, "claude") or std.mem.eql(u8, s, "claude-code")) {
return .claude;
} else if (std.mem.eql(u8, s, "codex")) {
return .codex;
} else if (std.mem.eql(u8, s, "gemini") or std.mem.eql(u8, s, "gemini-cli")) {
return .gemini;
}
return null;
}
};

pub const HookCommand = enum {
install,
uninstall,
status,

pub fn fromString(s: []const u8) ?HookCommand {
if (std.mem.eql(u8, s, "install")) {
return .install;
} else if (std.mem.eql(u8, s, "uninstall")) {
return .uninstall;
} else if (std.mem.eql(u8, s, "status")) {
return .status;
}
return null;
}
};

pub const Command = union(enum) {
hook: struct {
action: HookCommand,
tool: ?Tool,
},
help,
version,
gui, // No CLI args, run GUI mode
};

pub const ParseError = error{
UnknownCommand,
MissingHookAction,
UnknownHookAction,
UnknownTool,
MissingToolArgument,
};

/// Parse command-line arguments and return the appropriate Command.
/// Returns `.gui` if no arguments are provided (normal GUI launch).
pub fn parse(args: []const []const u8) ParseError!Command {
// Skip program name
const cmd_args = if (args.len > 0) args[1..] else args;

if (cmd_args.len == 0) {
return .gui;
}

const first = cmd_args[0];

// Check for help flags
if (std.mem.eql(u8, first, "--help") or std.mem.eql(u8, first, "-h") or std.mem.eql(u8, first, "help")) {
return .help;
}

// Check for version flags
if (std.mem.eql(u8, first, "--version") or std.mem.eql(u8, first, "-v") or std.mem.eql(u8, first, "version")) {
return .version;
}

// Hook command
if (std.mem.eql(u8, first, "hook")) {
if (cmd_args.len < 2) {
return error.MissingHookAction;
}

const action = HookCommand.fromString(cmd_args[1]) orelse {
return error.UnknownHookAction;
};

// Status doesn't require a tool argument
if (action == .status) {
return .{ .hook = .{ .action = action, .tool = null } };
}

// Install/uninstall require a tool argument
if (cmd_args.len < 3) {
return error.MissingToolArgument;
}

const tool = Tool.fromString(cmd_args[2]) orelse {
return error.UnknownTool;
};

return .{ .hook = .{ .action = action, .tool = tool } };
}

return error.UnknownCommand;
}

pub fn printUsage(writer: anytype) !void {
try writer.writeAll(
\\Architect - A terminal multiplexer for AI-assisted development
\\
\\USAGE:
\\ architect Launch the GUI
\\ architect hook <command> Manage AI assistant hooks
\\ architect help Show this help message
\\ architect version Show version information
\\
\\HOOK COMMANDS:
\\ architect hook install <tool> Install hook for an AI tool
\\ architect hook uninstall <tool> Uninstall hook for an AI tool
\\ architect hook status Show installed hooks status
\\
\\SUPPORTED TOOLS:
\\ claude, claude-code Claude Code AI assistant
\\ codex OpenAI Codex CLI
\\ gemini, gemini-cli Google Gemini CLI
\\
\\EXAMPLES:
\\ architect hook install claude
\\ architect hook uninstall gemini
\\ architect hook status
\\
);
}

pub fn printError(err: ParseError, writer: anytype) !void {
switch (err) {
error.UnknownCommand => try writer.writeAll("Error: Unknown command. Use 'architect help' for usage.\n"),
error.MissingHookAction => try writer.writeAll("Error: Missing hook action. Use: architect hook install|uninstall|status\n"),
error.UnknownHookAction => try writer.writeAll("Error: Unknown hook action. Valid actions: install, uninstall, status\n"),
error.UnknownTool => try writer.writeAll("Error: Unknown tool. Valid tools: claude, codex, gemini\n"),
error.MissingToolArgument => try writer.writeAll("Error: Missing tool argument. Use: architect hook install <tool>\n"),
}
}

test "parse - no args returns gui" {
const result = try parse(&[_][]const u8{"architect"});
try std.testing.expectEqual(.gui, result);
}

test "parse - help flags" {
try std.testing.expectEqual(.help, try parse(&[_][]const u8{ "architect", "help" }));
try std.testing.expectEqual(.help, try parse(&[_][]const u8{ "architect", "--help" }));
try std.testing.expectEqual(.help, try parse(&[_][]const u8{ "architect", "-h" }));
}

test "parse - version flags" {
try std.testing.expectEqual(.version, try parse(&[_][]const u8{ "architect", "version" }));
try std.testing.expectEqual(.version, try parse(&[_][]const u8{ "architect", "--version" }));
try std.testing.expectEqual(.version, try parse(&[_][]const u8{ "architect", "-v" }));
}

test "parse - hook install claude" {
const result = try parse(&[_][]const u8{ "architect", "hook", "install", "claude" });
switch (result) {
.hook => |h| {
try std.testing.expectEqual(.install, h.action);
try std.testing.expectEqual(.claude, h.tool.?);
},
else => return error.TestUnexpectedResult,
}
}

test "parse - hook status" {
const result = try parse(&[_][]const u8{ "architect", "hook", "status" });
switch (result) {
.hook => |h| {
try std.testing.expectEqual(.status, h.action);
try std.testing.expect(h.tool == null);
},
else => return error.TestUnexpectedResult,
}
}

test "parse - missing hook action" {
const result = parse(&[_][]const u8{ "architect", "hook" });
try std.testing.expectError(error.MissingHookAction, result);
}

test "parse - missing tool for install" {
const result = parse(&[_][]const u8{ "architect", "hook", "install" });
try std.testing.expectError(error.MissingToolArgument, result);
}
Loading