diff --git a/README.md b/README.md index 09580ed..c70e758 100644 --- a/README.md +++ b/README.md @@ -16,12 +16,26 @@ UnforcedAGI is a pattern for running a single Claude Code session as your person A user's personal instance lives in `~/UnforcedAGI/` (a separate private repo) and uses the tooling from this package. -## Install (future) +## Install ```bash bun install -g github:Unforced-Dev/unforced-agi ``` +## Usage + +```bash +unforced-agi launch # start (or attach to) your UnforcedAGI session in tmux +unforced-agi help # show help +unforced-agi version # show version +``` + +`launch` creates a tmux session named `unforcedagi` rooted at `~/UnforcedAGI`, +runs `claude` inside it, and attaches your terminal. Detach any time with +`Ctrl-b d` — the session keeps running in the background. Run `launch` again +to reattach. Requires `tmux` and `claude` on your `PATH`, and `~/UnforcedAGI` +to exist. + ## Roadmap - [ ] `unforced-agi init` — scaffold a new `~/UnforcedAGI/` directory for a user diff --git a/bin/unforced-agi.ts b/bin/unforced-agi.ts index 5503deb..f49ef42 100755 --- a/bin/unforced-agi.ts +++ b/bin/unforced-agi.ts @@ -2,27 +2,99 @@ // unforced-agi — CLI for the UnforcedAGI personal agent framework // https://github.com/Unforced-Dev/unforced-agi -const VERSION = "0.0.1"; +import { existsSync } from "node:fs"; +import { homedir } from "node:os"; +import { join } from "node:path"; -function main() { - const args = process.argv.slice(2); - const command = args[0]; +const VERSION = "0.0.2"; - if (!command || command === "--help" || command === "-h") { - console.log(`unforced-agi ${VERSION} +const SESSION_NAME = "unforcedagi"; +const AGENT_HOME = join(homedir(), "UnforcedAGI"); + +function help() { + console.log(`unforced-agi ${VERSION} A framework for running a single Claude Code session as your personal primary agent — coordinating work across projects via Agent Teams, subagents, and knowledge graph memory. Commands: + launch Start (or attach to) your UnforcedAGI session in tmux version Show version help Show this help -(More commands will be added as needed. Currently a placeholder scaffold.) - Learn more: https://github.com/Unforced-Dev/unforced-agi `); +} + +function which(cmd: string): boolean { + const r = Bun.spawnSync(["which", cmd], { stdout: "ignore", stderr: "ignore" }); + return r.exitCode === 0; +} + +function tmuxHasSession(name: string): boolean { + const r = Bun.spawnSync(["tmux", "has-session", "-t", name], { + stdout: "ignore", + stderr: "ignore", + }); + return r.exitCode === 0; +} + +async function launch() { + if (!which("tmux")) { + console.error("error: tmux is not installed or not on PATH."); + console.error("Install it (e.g. `brew install tmux`) and try again."); + process.exit(1); + } + if (!which("claude")) { + console.error("error: `claude` (Claude Code CLI) is not on PATH."); + console.error("Install Claude Code first: https://docs.claude.com/claude-code"); + process.exit(1); + } + if (!existsSync(AGENT_HOME)) { + console.error(`error: ${AGENT_HOME} does not exist.`); + console.error("UnforcedAGI expects its home directory at ~/UnforcedAGI."); + console.error("Create it (and a CLAUDE.md inside) before launching."); + process.exit(1); + } + + const alreadyRunning = tmuxHasSession(SESSION_NAME); + if (!alreadyRunning) { + const create = Bun.spawnSync( + ["tmux", "new-session", "-d", "-s", SESSION_NAME, "-c", AGENT_HOME, "claude"], + { stdout: "inherit", stderr: "inherit" }, + ); + if (create.exitCode !== 0) { + console.error("error: failed to create tmux session."); + process.exit(create.exitCode ?? 1); + } + console.log(`started UnforcedAGI in tmux session "${SESSION_NAME}" (cwd: ${AGENT_HOME})`); + } else { + console.log(`attaching to existing tmux session "${SESSION_NAME}"`); + } + + // Attach (or switch-client if we're already inside tmux). When the user + // detaches with Ctrl-b d, the session keeps running in the background. + const insideTmux = !!process.env.TMUX; + const attachCmd = insideTmux + ? ["tmux", "switch-client", "-t", SESSION_NAME] + : ["tmux", "attach-session", "-t", SESSION_NAME]; + + const attach = Bun.spawn(attachCmd, { + stdin: "inherit", + stdout: "inherit", + stderr: "inherit", + }); + const code = await attach.exited; + process.exit(code); +} + +async function main() { + const args = process.argv.slice(2); + const command = args[0]; + + if (!command || command === "help" || command === "--help" || command === "-h") { + help(); return; } @@ -31,6 +103,11 @@ Learn more: https://github.com/Unforced-Dev/unforced-agi return; } + if (command === "launch") { + await launch(); + return; + } + console.error(`Unknown command: ${command}`); console.error(`Run 'unforced-agi help' for usage.`); process.exit(1); diff --git a/package.json b/package.json index 4af64aa..b083e5d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "unforced-agi", - "version": "0.0.1", + "version": "0.0.2", "description": "CLI tooling for the UnforcedAGI personal agent framework", "license": "MIT", "type": "module",