@pat-lewczuk/cezar 0.1.0

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 (45) hide show
  1. package/README.md +90 -0
  2. package/dist/core/agent-runner.d.ts +95 -0
  3. package/dist/core/agent-runner.js +8 -0
  4. package/dist/core/agent-runner.js.map +1 -0
  5. package/dist/core/backend-detect.d.ts +14 -0
  6. package/dist/core/backend-detect.js +78 -0
  7. package/dist/core/backend-detect.js.map +1 -0
  8. package/dist/core/claude-cli-runner.d.ts +68 -0
  9. package/dist/core/claude-cli-runner.js +424 -0
  10. package/dist/core/claude-cli-runner.js.map +1 -0
  11. package/dist/core/usage.d.ts +11 -0
  12. package/dist/core/usage.js +15 -0
  13. package/dist/core/usage.js.map +1 -0
  14. package/dist/index.d.ts +2 -0
  15. package/dist/index.js +294 -0
  16. package/dist/index.js.map +1 -0
  17. package/dist/runs/store.d.ts +218 -0
  18. package/dist/runs/store.js +286 -0
  19. package/dist/runs/store.js.map +1 -0
  20. package/dist/server/git.d.ts +21 -0
  21. package/dist/server/git.js +54 -0
  22. package/dist/server/git.js.map +1 -0
  23. package/dist/server/open-in-terminal.d.ts +11 -0
  24. package/dist/server/open-in-terminal.js +84 -0
  25. package/dist/server/open-in-terminal.js.map +1 -0
  26. package/dist/server/server.d.ts +12 -0
  27. package/dist/server/server.js +243 -0
  28. package/dist/server/server.js.map +1 -0
  29. package/dist/skills.d.ts +31 -0
  30. package/dist/skills.js +117 -0
  31. package/dist/skills.js.map +1 -0
  32. package/dist/workflows/load.d.ts +15 -0
  33. package/dist/workflows/load.js +58 -0
  34. package/dist/workflows/load.js.map +1 -0
  35. package/dist/workflows/run.d.ts +52 -0
  36. package/dist/workflows/run.js +452 -0
  37. package/dist/workflows/run.js.map +1 -0
  38. package/dist/workflows/types.d.ts +202 -0
  39. package/dist/workflows/types.js +56 -0
  40. package/dist/workflows/types.js.map +1 -0
  41. package/package.json +44 -0
  42. package/scripts/mock-claude.mjs +106 -0
  43. package/web/app.js +634 -0
  44. package/web/index.html +48 -0
  45. package/web/style.css +317 -0
package/README.md ADDED
@@ -0,0 +1,90 @@
1
+ # cezar ⚡
2
+
3
+ Local cockpit for running and tracking AI agent tasks in your repo. Single
4
+ user, zero config, one command:
5
+
6
+ ```bash
7
+ cd your-repo
8
+ npx cezar
9
+ ```
10
+
11
+ That's it. If you have the `claude` CLI logged in (Pro/Max subscription) and
12
+ `gh` authenticated, there is nothing to configure — cez opens a browser
13
+ cockpit at `http://localhost:4321` where you type a task, pick a workflow and
14
+ watch the agent work live (steps, tool calls, tokens), with a repo view
15
+ (status / diff / log) right next to it.
16
+
17
+ No accounts, no database, no cloud. State lives in `.ai/cezar/` inside your repo.
18
+
19
+ ## What it does
20
+
21
+ - **Runs** — every agent task is a run: queued → running → done/failed, with a
22
+ live event log (agent text, tool calls, tool results), token usage per step
23
+ and cancel/delete controls. Failed run? Take the session over interactively:
24
+ `claude --resume <session-id>` (shown in the run header).
25
+ - **Workflows** — a YAML file: agent steps + shell checks, with bounded
26
+ retry loops (`onFail: retry`). The built-in `quick-task` workflow (one agent
27
+ step) works with zero setup.
28
+ - **Skills** — Markdown files in `.ai/skills/` or `.ai/cezar/skills/`; a workflow
29
+ step references one via `skill: <name>` and its body becomes the agent's
30
+ extra system prompt.
31
+ - **Repo view** — branch, working-tree status, diff vs HEAD, recent commits.
32
+
33
+ ## Commands
34
+
35
+ ```bash
36
+ npx cezar # start the cockpit for the current repo
37
+ npx cezar run "task" # headless run in the terminal (CI-friendly)
38
+ npx cezar init # scaffold .ai/cezar/ with an example workflow + skill
39
+ ```
40
+
41
+ ## Workflow format
42
+
43
+ `.ai/cezar/workflows/fix-and-verify.yaml`:
44
+
45
+ ```yaml
46
+ name: fix-and-verify
47
+ description: Implement, then verify; retry with failing output on red.
48
+ steps:
49
+ - id: implement
50
+ name: Implement
51
+ prompt: "{{task}}"
52
+ skill: project-conventions # optional, from .ai/skills or .ai/cezar/skills
53
+ # model: opus # optional per-step override
54
+ # allowedTools: [Read, Edit, Write, Grep, Glob, Bash]
55
+ - id: verify
56
+ name: Verify
57
+ command: "npm test"
58
+ onFail:
59
+ retry: implement # loop back, at most:
60
+ max: 2
61
+ ```
62
+
63
+ `{{task}}` is replaced with the task you typed. When a check fails and loops
64
+ back, the failing output is appended to the retried agent's prompt.
65
+
66
+ ## How it runs agents
67
+
68
+ cez shells out to your locally installed, logged-in `claude` CLI in headless
69
+ stream-json mode — your subscription, no API key. Tool access is
70
+ default-deny via `--allowedTools`; edits are auto-accepted
71
+ (`--permission-mode acceptEdits`) inside the repo directory.
72
+
73
+ Useful env vars:
74
+
75
+ - `CEZ_DRY_RUN=1` — use the bundled mock instead of the real CLI (demo/dev).
76
+ - `CEZ_CLAUDE_BIN=/path/to/claude` — override the binary.
77
+
78
+ ## Development
79
+
80
+ ```bash
81
+ npm install
82
+ npm run dev # tsx src/index.ts
83
+ npm run build # tsc → dist/
84
+ ```
85
+
86
+ ---
87
+
88
+ cez is the radically-simple, single-user sibling of
89
+ [cezar](https://github.com/...) — same core ideas (agent runner, skills,
90
+ declarative workflows, run cockpit), none of the SaaS.
@@ -0,0 +1,95 @@
1
+ /**
2
+ * The backend-agnostic seam for running one agent task. Adapted from
3
+ * @cezar/core's `agents/agent-runner.ts`, trimmed for single-user local use:
4
+ * no token-budget circuit breaker, no zod response schemas — one run is one
5
+ * `claude` CLI invocation streaming normalized events.
6
+ */
7
+ export type AgentBackend = 'claude-cli';
8
+ export interface AgentRunSpec {
9
+ /** Appended to the CLI's default system prompt (`--append-system-prompt`). */
10
+ systemPrompt?: string;
11
+ userPrompt: string;
12
+ /** The directory the agent runs in — also the only writable root. */
13
+ cwd: string;
14
+ /** Tool allowlist; the CLI is default-deny for anything not listed. */
15
+ allowedTools?: string[];
16
+ /** When `Bash` is allowed, restrict it to commands starting with one of these. */
17
+ bashAllowlist?: string[];
18
+ /** Extra directories the agent may read/write besides `cwd`. */
19
+ additionalDirectories?: string[];
20
+ model?: string;
21
+ /** Wall-clock kill switch for the run (ms). */
22
+ timeoutMs?: number;
23
+ /**
24
+ * Stable session id (UUID) so the user can take over interactively later:
25
+ * `cd <repo> && claude --resume <sessionId>`.
26
+ */
27
+ sessionId?: string;
28
+ /**
29
+ * Spawn `claude --resume <sessionId>` instead of starting a fresh session —
30
+ * picks up the on-disk conversation (used by "Continue" after a run ends).
31
+ */
32
+ resume?: boolean;
33
+ }
34
+ /** One content block of a user message — mirrors the Anthropic wire format
35
+ * so it can be written to the claude CLI's stdin verbatim. */
36
+ export type ContentBlock = {
37
+ type: 'text';
38
+ text: string;
39
+ } | {
40
+ type: 'image';
41
+ source: {
42
+ type: 'base64';
43
+ media_type: string;
44
+ data: string;
45
+ };
46
+ };
47
+ /** Normalized event stream — the GUI renders these, the store persists them. */
48
+ export type AgentEvent = {
49
+ type: 'text';
50
+ text: string;
51
+ } | {
52
+ type: 'tool-call';
53
+ id: string;
54
+ tool: string;
55
+ input: unknown;
56
+ } | {
57
+ type: 'tool-result';
58
+ toolCallId: string;
59
+ result: string;
60
+ isError: boolean;
61
+ } | {
62
+ type: 'token-usage';
63
+ tokensUsed: number;
64
+ } | {
65
+ type: 'cost';
66
+ usd: number;
67
+ } | {
68
+ type: 'turn-end';
69
+ } | {
70
+ type: 'note';
71
+ message: string;
72
+ } | {
73
+ type: 'done';
74
+ } | {
75
+ type: 'error';
76
+ message: string;
77
+ };
78
+ export interface AgentToolCallRecord {
79
+ id: string;
80
+ name: string;
81
+ input: unknown;
82
+ }
83
+ export interface AgentRunResult {
84
+ /** Concatenated assistant text across the run, trimmed. */
85
+ text: string;
86
+ toolCalls: AgentToolCallRecord[];
87
+ /** Cost-weighted token usage; 0 when the backend surfaced no telemetry. */
88
+ tokensUsed: number;
89
+ sessionId?: string;
90
+ }
91
+ export interface AgentRunner {
92
+ readonly backend: AgentBackend;
93
+ run(spec: AgentRunSpec, onEvent?: (event: AgentEvent) => void): Promise<AgentRunResult>;
94
+ interrupt(): Promise<void>;
95
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * The backend-agnostic seam for running one agent task. Adapted from
3
+ * @cezar/core's `agents/agent-runner.ts`, trimmed for single-user local use:
4
+ * no token-budget circuit breaker, no zod response schemas — one run is one
5
+ * `claude` CLI invocation streaming normalized events.
6
+ */
7
+ export {};
8
+ //# sourceMappingURL=agent-runner.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"agent-runner.js","sourceRoot":"","sources":["../../src/core/agent-runner.ts"],"names":[],"mappings":"AAAA;;;;;GAKG"}
@@ -0,0 +1,14 @@
1
+ export interface BackendCheck {
2
+ name: 'claude' | 'gh' | 'git';
3
+ available: boolean;
4
+ version?: string;
5
+ hint?: string;
6
+ }
7
+ /**
8
+ * Probe the host for everything cez leans on: the `claude` CLI (the agent
9
+ * backend), `gh` (GitHub auth for PR creation) and `git`. Nothing is required
10
+ * except `claude` — the GUI degrades gracefully and shows the hints instead.
11
+ */
12
+ export declare function detectEnvironment(): Promise<BackendCheck[]>;
13
+ /** The host's GitHub token: logged-in `gh` first, `GITHUB_TOKEN` fallback. */
14
+ export declare function readHostGithubToken(): Promise<string | null>;
@@ -0,0 +1,78 @@
1
+ import { execFile } from 'node:child_process';
2
+ import { promisify } from 'node:util';
3
+ const exec = promisify(execFile);
4
+ /**
5
+ * Probe the host for everything cez leans on: the `claude` CLI (the agent
6
+ * backend), `gh` (GitHub auth for PR creation) and `git`. Nothing is required
7
+ * except `claude` — the GUI degrades gracefully and shows the hints instead.
8
+ */
9
+ export async function detectEnvironment() {
10
+ return Promise.all([probeClaude(), probeGh(), probeGit()]);
11
+ }
12
+ async function probeClaude() {
13
+ if (process.env.CEZ_DRY_RUN === '1') {
14
+ return { name: 'claude', available: true, version: 'mock (CEZ_DRY_RUN=1)' };
15
+ }
16
+ try {
17
+ const { stdout } = await exec('claude', ['--version'], { timeout: 10_000 });
18
+ const version = stdout.trim();
19
+ // A generic `claude` binary (a shell wrapper, an unrelated tool) can shadow
20
+ // the real CLI on $PATH — reject anything that doesn't match the banner.
21
+ if (!/^\d+\.\d+|^claude(\s+code)?\s+version\s+\d+\.\d+/i.test(version)) {
22
+ return {
23
+ name: 'claude',
24
+ available: false,
25
+ hint: `\`claude\` is on PATH but doesn't look like Claude Code (got: ${version.slice(0, 80)})`,
26
+ };
27
+ }
28
+ return {
29
+ name: 'claude',
30
+ available: true,
31
+ version,
32
+ hint: 'if not authenticated, run `claude` once and log in',
33
+ };
34
+ }
35
+ catch {
36
+ return {
37
+ name: 'claude',
38
+ available: false,
39
+ hint: 'install Claude Code (npm i -g @anthropic-ai/claude-code) and log in',
40
+ };
41
+ }
42
+ }
43
+ async function probeGh() {
44
+ try {
45
+ const { stdout } = await exec('gh', ['auth', 'token'], { timeout: 10_000 });
46
+ return { name: 'gh', available: stdout.trim().length > 0, version: 'authenticated' };
47
+ }
48
+ catch {
49
+ return {
50
+ name: 'gh',
51
+ available: false,
52
+ hint: 'install the GitHub CLI and run `gh auth login` (only needed for PR creation)',
53
+ };
54
+ }
55
+ }
56
+ async function probeGit() {
57
+ try {
58
+ const { stdout } = await exec('git', ['--version'], { timeout: 10_000 });
59
+ return { name: 'git', available: true, version: stdout.trim() };
60
+ }
61
+ catch {
62
+ return { name: 'git', available: false, hint: 'install git' };
63
+ }
64
+ }
65
+ /** The host's GitHub token: logged-in `gh` first, `GITHUB_TOKEN` fallback. */
66
+ export async function readHostGithubToken() {
67
+ try {
68
+ const { stdout } = await exec('gh', ['auth', 'token'], { timeout: 10_000 });
69
+ const token = stdout.trim();
70
+ if (token)
71
+ return token;
72
+ }
73
+ catch {
74
+ // fall through to the env var
75
+ }
76
+ return process.env.GITHUB_TOKEN?.trim() || null;
77
+ }
78
+ //# sourceMappingURL=backend-detect.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"backend-detect.js","sourceRoot":"","sources":["../../src/core/backend-detect.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,68 @@
1
+ import type { AgentEvent, AgentRunResult, AgentRunSpec, AgentRunner, ContentBlock } from './agent-runner.js';
2
+ /** Default wall-clock cap for a single run before SIGTERM → SIGKILL.
3
+ * Interactive sessions pass `timeoutMs: 0` to disable it entirely. */
4
+ export declare const DEFAULT_RUN_TIMEOUT_MS: number;
5
+ /** Grace period between SIGTERM and SIGKILL when a timeout fires. */
6
+ export declare const KILL_GRACE_MS = 10000;
7
+ /** After `end()` closes stdin: claude in stream-json mode can ignore EOF and
8
+ * hang (janitor-confirmed CLI bug) — escalate SIGTERM, then SIGKILL. */
9
+ export declare const EOF_TERM_GRACE_MS = 8000;
10
+ export declare const EOF_KILL_GRACE_MS = 4000;
11
+ /** Reopen window after a turn ends before an auto-ended session closes stdin. */
12
+ export declare const AUTO_END_DELAY_MS = 250;
13
+ export interface ClaudeCliRunnerOptions {
14
+ /** Override the binary name/path; defaults to `claude` on PATH. */
15
+ bin?: string;
16
+ /** Wall-clock timeout for a run (ms); per-spec `timeoutMs` still wins. */
17
+ timeoutMs?: number;
18
+ }
19
+ export interface SessionOptions {
20
+ /** Close the session shortly after the first turn ends (single-turn
21
+ * behavior, used for non-interactive workflow steps). Interactive
22
+ * sessions omit this and control `end()` themselves. */
23
+ autoEndAfterFirstTurn?: boolean;
24
+ }
25
+ /**
26
+ * A live agent session over one spawned claude process. The process stays
27
+ * alive between turns (no `-p`) and reads further user messages from stdin —
28
+ * that's what makes mid-task follow-ups and pasted screenshots possible.
29
+ */
30
+ export interface AgentSession {
31
+ /** Resolves when the claude process exits — the session is fully over. */
32
+ result: Promise<AgentRunResult>;
33
+ /** Write a user message into the live session. False when stdin is closed. */
34
+ sendMessage(content: ContentBlock[]): boolean;
35
+ /** Graceful close: EOF on stdin, then the SIGTERM→SIGKILL watchdog. */
36
+ end(): void;
37
+ /** Hard stop (used by cancel). */
38
+ interrupt(): void;
39
+ /** True while the session still accepts messages. */
40
+ readonly open: boolean;
41
+ }
42
+ /**
43
+ * `AgentRunner` over the Claude Code CLI in headless stream-json mode. Auth =
44
+ * the host's logged-in Pro/Max subscription (no API key needed). Sandboxing is
45
+ * `--allowedTools` (default-deny) + running inside the repo `cwd`; `Bash` is
46
+ * narrowed to `Bash(<prefix>:*)` patterns when `bashAllowlist` is set.
47
+ *
48
+ * Session mechanics (multi-turn stdin, EOF watchdog, reopen window) follow
49
+ * github-janitor's `claudeRunner.ts`; the original single-turn adaptation
50
+ * came from @cezar/core's `ClaudeCodeCliRunner`.
51
+ */
52
+ export declare class ClaudeCliRunner implements AgentRunner {
53
+ readonly backend: "claude-cli";
54
+ private readonly bin;
55
+ private readonly timeoutMs;
56
+ private lastSession;
57
+ constructor(opts?: ClaudeCliRunnerOptions);
58
+ /** One-shot run: start a session and auto-end it after the first turn. */
59
+ run(spec: AgentRunSpec, onEvent?: (event: AgentEvent) => void): Promise<AgentRunResult>;
60
+ interrupt(): Promise<void>;
61
+ startSession(spec: AgentRunSpec, onEvent?: (event: AgentEvent) => void, opts?: SessionOptions): AgentSession;
62
+ }
63
+ /**
64
+ * Map `allowedTools` onto claude's `--allowedTools` syntax. `Bash` with a
65
+ * `bashAllowlist` becomes one `Bash(<prefix>:*)` entry per allowed prefix;
66
+ * `Bash` with no allowlist stays plain `Bash`.
67
+ */
68
+ export declare function buildAllowedTools(allowedTools: string[], bashAllowlist?: string[]): string[];