diff --git a/CHANGELOG.md b/CHANGELOG.md index fa8a57b77..2c9fb2614 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,8 @@ - macOS Debug: add app log verbosity and rolling file log toggle for swift-log-backed app logs. ### Fixes +- macOS codesign: skip hardened runtime for ad-hoc signing and avoid empty options args (#70) — thanks @petter-b +- Docs: add manual OAuth setup for remote/headless deployments (#67) — thanks @wstock - Docs/agent tools: clarify that browser `wait` should be avoided by default and used only in exceptional cases. - Browser tools: `upload` supports auto-click refs, direct `inputRef`/`element` file inputs, and emits input/change after `setFiles` so JS-heavy sites pick up attachments. - Browser tools: harden CDP readiness (HTTP + WS), retry CDP connects, and auto-restart the clawd browser when the socket handshake stalls. diff --git a/docs/wizard.md b/docs/wizard.md new file mode 100644 index 000000000..f0b076005 --- /dev/null +++ b/docs/wizard.md @@ -0,0 +1,171 @@ +--- +summary: "CLI onboarding wizard spec (gateway + workspace + skills + daemon)" +read_when: + - Designing or implementing the onboarding wizard + - Changing gateway install/setup flow +--- + +# Onboarding Wizard (CLI) + +Goal: single interactive flow to set up Clawdis Gateway + workspace + skills on a new machine. +Uses `@clack/prompts` for arrow-key selection and step UX. + +Scope: **Local gateway only**. Remote mode is **info-only** (no config writes). + +## Entry points + +- `clawdis onboard` (primary) +- `clawdis setup --wizard` (alias) + +## Non-interactive mode + +`--non-interactive` + flags to skip prompts. `--json` outputs a machine summary. + +## Preflight + +- Runtime: Node >=22 (reuse `runtime-guard`). +- Detect existing files: + - config: `~/.clawdis/clawdis.json` + - creds: `~/.clawdis/credentials/` + - sessions: `~/.clawdis/sessions/` + - workspace: `~/clawd` (or configured) +- Detect available package managers: `npm`, `pnpm`, `bun`. +- Detect optional tools: `brew`, `uv`, `go`. + +If config exists: +- Prompt: **Keep / Modify / Reset** + +Reset uses `trash` (never `rm`). + +## Flow (interactive) + +1) **Mode** + - Local (full wizard) + - Remote (info-only; no config writes) + +2) **Model/Auth (local only)** + - Anthropic OAuth (recommended) + - API key + - Minimax M2.1 (LM Studio; recommended local model) + - Skip + +3) **Workspace + config** + - Default workspace: `~/clawd` + - Writes `agent.workspace` into `~/.clawdis/clawdis.json` + - Ensures sessions dir exists + +4) **Gateway config** + - Port (default 18789) + - Bind: loopback | lan | tailnet | auto + - Auth: token | password | off + - Tailscale: off | serve | funnel + +5) **Daemon install (local only)** + - macOS: LaunchAgent + - Linux: systemd user unit + - Windows: Scheduled Task + +6) **Health** + - Start/restart daemon + - `clawdis health` summary + +7) **Skills (recommended)** + - Read from `buildWorkspaceSkillStatus` + - Show eligible vs missing requirements + - Offer installs via preferred installer + - Allow skip + +8) **Finish** + - Summary + next steps + - Reminder: iOS/Android/macOS node apps add canvas/camera/screen/system features. + +## Remote mode (info-only) + +- Explain where gateway runs. +- Show required steps on gateway host: + - `clawdis setup` + - `clawdis gateway-daemon ...` + - OAuth file: `~/.clawdis/credentials/oauth.json` + - Workspace: `~/clawd` +- No local config changes. + +## Config writes + +Wizard writes: +- `~/.clawdis/clawdis.json` + - `agent.workspace` + - `agent.model` + `models.providers` (if Minimax selected) + - `skills.install.nodeManager` (npm | pnpm | bun) + - `skills.entries..env` / `.apiKey` (if set in skills step) + +## Minimax M2.1 (LM Studio) config snippet + +```json5 +{ + agent: { + model: "Minimax", + allowedModels: [ + "anthropic/claude-opus-4-5", + "lmstudio/minimax-m2.1-gs32" + ], + modelAliases: { + Opus: "anthropic/claude-opus-4-5", + Minimax: "lmstudio/minimax-m2.1-gs32" + } + }, + models: { + mode: "merge", + providers: { + lmstudio: { + baseUrl: "http://127.0.0.1:1234/v1", + apiKey: "lmstudio", + api: "openai-responses", + models: [ + { + id: "minimax-m2.1-gs32", + name: "MiniMax M2.1 GS32", + reasoning: false, + input: ["text"], + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, + contextWindow: 196608, + maxTokens: 8192 + } + ] + } + } + } +} +``` + +## Skills install preferences + +Prompt for node manager: +- npm +- pnpm +- bun + +Writes: + +```json5 +{ + skills: { + install: { + nodeManager: "npm" // npm | pnpm | bun + } + } +} +``` + +## Reset scope (decision required) + +Options: +- A) Config only (`~/.clawdis/clawdis.json`) +- B) Config + credentials + sessions +- C) Full reset: config + credentials + sessions + workspace + +Wizard should clearly list what will be removed and use `trash`. + +## Open questions + +- Confirm “Remote = info-only” is final. +- Confirm reset scope default (A/B/C). diff --git a/package.json b/package.json index 30a30a37d..13a57461d 100644 --- a/package.json +++ b/package.json @@ -127,6 +127,11 @@ "vitest": "^4.0.16", "wireit": "^0.14.12" }, + "pnpm": { + "overrides": { + "@sinclair/typebox": "0.34.45" + } + }, "vitest": { "coverage": { "provider": "v8", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d524e4c4a..5894da5e1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4,6 +4,9 @@ settings: autoInstallPeers: true excludeLinksFromLockfile: false +overrides: + '@sinclair/typebox': 0.34.45 + patchedDependencies: '@mariozechner/pi-ai': hash: bf3e904ebaad236b8c3bb48c7d1150a1463735e783acaab6d15d6cd381b43832 @@ -29,7 +32,7 @@ importers: specifier: ^0.30.2 version: 0.30.2(ws@8.18.3)(zod@4.2.1) '@sinclair/typebox': - specifier: ^0.34.45 + specifier: 0.34.45 version: 0.34.45 '@whiskeysockets/baileys': specifier: 7.0.0-rc.9 diff --git a/scripts/codesign-mac-app.sh b/scripts/codesign-mac-app.sh index 8bb1d7eed..3d558fb65 100755 --- a/scripts/codesign-mac-app.sh +++ b/scripts/codesign-mac-app.sh @@ -83,7 +83,10 @@ case "$TIMESTAMP_MODE" in ;; esac -options_args=("--options" "runtime") +options_args=() +if [[ "$IDENTITY" != "-" ]]; then + options_args=("--options" "runtime") +fi timestamp_args=("$timestamp_arg") cat > "$ENT_TMP_BASE" <<'PLIST' @@ -157,12 +160,12 @@ xattr -cr "$APP_BUNDLE" 2>/dev/null || true sign_item() { local target="$1" local entitlements="$2" - codesign --force "${options_args[@]}" "${timestamp_args[@]}" --entitlements "$entitlements" --sign "$IDENTITY" "$target" + codesign --force ${options_args+"${options_args[@]}"} "${timestamp_args[@]}" --entitlements "$entitlements" --sign "$IDENTITY" "$target" } sign_plain_item() { local target="$1" - codesign --force "${options_args[@]}" "${timestamp_args[@]}" --sign "$IDENTITY" "$target" + codesign --force ${options_args+"${options_args[@]}"} "${timestamp_args[@]}" --sign "$IDENTITY" "$target" } # Sign main binary diff --git a/src/agents/bash-tools.ts b/src/agents/bash-tools.ts index d0ac65f47..30e43a875 100644 --- a/src/agents/bash-tools.ts +++ b/src/agents/bash-tools.ts @@ -1,7 +1,6 @@ import { type ChildProcessWithoutNullStreams, spawn } from "node:child_process"; import { randomUUID } from "node:crypto"; import type { AgentTool, AgentToolResult } from "@mariozechner/pi-ai"; -import { StringEnum } from "@mariozechner/pi-ai"; import { Type } from "@sinclair/typebox"; import { @@ -31,6 +30,18 @@ const DEFAULT_MAX_OUTPUT = clampNumber( 150_000, ); +const stringEnum = ( + values: readonly string[], + options?: Parameters[1], +) => + Type.Union( + values.map((value) => Type.Literal(value)) as [ + ReturnType, + ...ReturnType[], + ], + options, + ); + export type BashToolDefaults = { backgroundMs?: number; timeoutSec?: number; @@ -60,7 +71,7 @@ const bashSchema = Type.Object({ }), ), stdinMode: Type.Optional( - StringEnum(["pipe", "pty"] as const, { + stringEnum(["pipe", "pty"] as const, { description: "Only pipe is supported", }), ), @@ -83,7 +94,8 @@ export type BashToolDetails = export function createBashTool( defaults?: BashToolDefaults, -): AgentTool { + // biome-ignore lint/suspicious/noExplicitAny: TypeBox schema type from pi-ai uses a different module instance. +): AgentTool { const defaultBackgroundMs = clampNumber( defaults?.backgroundMs ?? readEnvInt("PI_BASH_YIELD_MS"), 20_000, @@ -329,7 +341,7 @@ export function createBashTool( export const bashTool = createBashTool(); const processSchema = Type.Object({ - action: StringEnum( + action: stringEnum( ["list", "poll", "log", "write", "kill", "clear", "remove"] as const, { description: "Process action", @@ -346,7 +358,8 @@ const processSchema = Type.Object({ export function createProcessTool( defaults?: ProcessToolDefaults, -): AgentTool { + // biome-ignore lint/suspicious/noExplicitAny: TypeBox schema type from pi-ai uses a different module instance. +): AgentTool { if (defaults?.cleanupMs !== undefined) { setJobTtlMs(defaults.cleanupMs); } diff --git a/src/agents/clawdis-tools.ts b/src/agents/clawdis-tools.ts index 0a273008e..e77a66472 100644 --- a/src/agents/clawdis-tools.ts +++ b/src/agents/clawdis-tools.ts @@ -2,7 +2,7 @@ import crypto from "node:crypto"; import fs from "node:fs/promises"; import type { AgentTool, AgentToolResult } from "@mariozechner/pi-ai"; -import { type TSchema, Type } from "@sinclair/typebox"; +import { Type } from "@sinclair/typebox"; import { browserCloseTab, browserFocusTab, @@ -45,7 +45,8 @@ import { callGateway } from "../gateway/call.js"; import { detectMime } from "../media/mime.js"; import { sanitizeToolResultImages } from "./tool-images.js"; -type AnyAgentTool = AgentTool; +// biome-ignore lint/suspicious/noExplicitAny: TypeBox schema type from pi-ai uses a different module instance. +type AnyAgentTool = AgentTool; const DEFAULT_GATEWAY_URL = "ws://127.0.0.1:18789"; diff --git a/src/agents/pi-tools.ts b/src/agents/pi-tools.ts index ffd5bb339..b7c9c9cc4 100644 --- a/src/agents/pi-tools.ts +++ b/src/agents/pi-tools.ts @@ -1,6 +1,6 @@ import type { AgentTool, AgentToolResult } from "@mariozechner/pi-ai"; import { codingTools, readTool } from "@mariozechner/pi-coding-agent"; -import { type TSchema, Type } from "@sinclair/typebox"; +import { Type } from "@sinclair/typebox"; import { detectMime } from "../media/mime.js"; import { startWebLoginWithQr, waitForWebLogin } from "../web/login-qr.js"; @@ -103,7 +103,8 @@ async function normalizeReadImageResult( return { ...result, content: nextContent }; } -type AnyAgentTool = AgentTool; +// biome-ignore lint/suspicious/noExplicitAny: TypeBox schema type from pi-ai uses a different module instance. +type AnyAgentTool = AgentTool; function extractEnumValues(schema: unknown): unknown[] | undefined { if (!schema || typeof schema !== "object") return undefined; @@ -204,7 +205,7 @@ function normalizeToolParameters(tool: AnyAgentTool): AnyAgentTool { : {}), additionalProperties: "additionalProperties" in schema ? schema.additionalProperties : true, - } as unknown as TSchema, + }, }; } diff --git a/src/commands/health.snapshot.test.ts b/src/commands/health.snapshot.test.ts index 3a7b7fb4b..4de1fa2a5 100644 --- a/src/commands/health.snapshot.test.ts +++ b/src/commands/health.snapshot.test.ts @@ -110,7 +110,6 @@ describe("getHealthSnapshot", () => { testConfig = { telegram: { tokenFile } }; testStore = {}; vi.stubEnv("TELEGRAM_BOT_TOKEN", ""); - vi.stubEnv("DISCORD_BOT_TOKEN", ""); const calls: string[] = []; vi.stubGlobal(