diff --git a/README.md b/README.md new file mode 100644 index 0000000..3608da7 --- /dev/null +++ b/README.md @@ -0,0 +1,59 @@ +# DisClaw + +DisClaw is a Discord-bot-powered multi-agent workspace manager built on Claude Code. +It turns a Discord server into a multi-agent development environment where each channel +maps to a local workspace folder with its own Claude Code agent identity. + +## Quick Start + +```bash +npm install +cp .env.example .env # add DISCORD_BOT_TOKEN and DISCORD_GUILD_ID +npm run build +npm run start +``` + +## Configuration + +Edit `disclaw.yaml` to customise global settings: + +| Key | Default | Description | +|-----|---------|-------------| +| `workspaces_root` | `~/.disclaw/workspaces` | Directory where agent workspaces are created | +| `management_channel` | `disclaw` | Discord channel name for bot management | +| `claude_command` | `claude` | Path or name of the Claude Code CLI binary | +| `claude_timeout_seconds` | `120` | Timeout for each Claude CLI invocation | + +Secrets go in `.env`: + +``` +DISCORD_BOT_TOKEN=your-token-here +DISCORD_GUILD_ID=your-guild-id # optional, speeds up slash-command registration +CLAUDE_PATH=/path/to/claude # optional, overrides claude_command +``` + +## Architecture + +See `ARCHITECTURE.md` for a detailed breakdown of the component design. + +## Development + +```bash +npm install # install dependencies +npm run build # compile TypeScript +npm test # run unit tests (Vitest) +npm run dev # build + start in one step +``` + +## Migration bestehender Workspaces + +Ab dieser Version werden Workspaces standardmaessig unter `~/.disclaw/workspaces/` angelegt +(konfigurierbar via `disclaw.yaml` -> `workspaces_root`). + +Bestehende Workspaces unter `./workspaces/` muessen manuell verschoben werden: + +```bash +mv ./workspaces/* ~/.disclaw/workspaces/ +``` + +Danach die DB-Eintraege in `data/disclaw.db` (Tabelle `workspaces`, Spalte `workspace_path`) aktualisieren. diff --git a/disclaw.yaml b/disclaw.yaml index 294129b..1458cdd 100644 --- a/disclaw.yaml +++ b/disclaw.yaml @@ -1,5 +1,6 @@ # Path where workspace folders are created -workspaces_root: "./workspaces" +# Supports tilde expansion: ~ is replaced with the user's home directory +workspaces_root: "~/.disclaw/workspaces" # Name of the management channel management_channel: "disclaw" diff --git a/tests/unit/workspace-root-resolve.test.ts b/tests/unit/workspace-root-resolve.test.ts new file mode 100644 index 0000000..233ba59 --- /dev/null +++ b/tests/unit/workspace-root-resolve.test.ts @@ -0,0 +1,63 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; +import * as os from "os"; +import * as path from "path"; +import { expandWorkspacesRoot } from "../../src/config/loader"; + +const HOME = os.homedir(); +// Use an absolute path that is portable across Linux, macOS, and Windows. +// os.tmpdir() always returns an absolute path on all platforms. +const REPO_ROOT = path.join(os.tmpdir(), "fake-disclaw-repo"); + +describe("expandWorkspacesRoot", () => { + beforeEach(() => { + vi.spyOn(console, "warn").mockImplementation(() => undefined); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + it("(a) expands leading ~ to os.homedir() and resolves to absolute path", () => { + const result = expandWorkspacesRoot("~/.disclaw/workspaces", REPO_ROOT); + expect(result).toBe(path.resolve(HOME, ".disclaw", "workspaces")); + expect(path.isAbsolute(result)).toBe(true); + }); + + it("(a) expands ~ with no trailing path to os.homedir()", () => { + const result = expandWorkspacesRoot("~", REPO_ROOT); + expect(result).toBe(path.resolve(HOME)); + }); + + it("(b) leaves an already-absolute path unchanged (no double expansion)", () => { + const absolute = path.join(HOME, ".disclaw", "workspaces"); + const result = expandWorkspacesRoot(absolute, REPO_ROOT); + expect(result).toBe(absolute); + }); + + it("(c) resolves a relative path (without ~) against repoRoot to an absolute path", () => { + const result = expandWorkspacesRoot("myworkspaces", REPO_ROOT); + expect(result).toBe(path.join(REPO_ROOT, "myworkspaces")); + expect(path.isAbsolute(result)).toBe(true); + }); + + it("(d) triggers console.warn when workspaces_root is inside the repo", () => { + expandWorkspacesRoot("workspaces", REPO_ROOT); + expect(console.warn).toHaveBeenCalledOnce(); + expect(console.warn).toHaveBeenCalledWith( + expect.stringContaining("[DisClaw] WARNING") + ); + expect(console.warn).toHaveBeenCalledWith( + expect.stringContaining("CLAUDE.md") + ); + }); + + it("(d) does NOT warn when workspaces_root is outside the repo", () => { + expandWorkspacesRoot("~/.disclaw/workspaces", REPO_ROOT); + expect(console.warn).not.toHaveBeenCalled(); + }); + + it("(d) does NOT warn when an absolute path outside the repo is given", () => { + expandWorkspacesRoot("/tmp/disclaw-workspaces", REPO_ROOT); + expect(console.warn).not.toHaveBeenCalled(); + }); +});