Skip to content

Commit 7c2c541

Browse files
committed
feat: expand onboarding wizard
1 parent f10abc8 commit 7c2c541

12 files changed

Lines changed: 1395 additions & 935 deletions

.dockerignore

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
.git
2+
.worktrees
3+
.bun-cache
4+
.bun
5+
.tmp
6+
**/.tmp
7+
.DS_Store
8+
**/.DS_Store
9+
*.png
10+
*.jpg
11+
*.jpeg
12+
*.webp
13+
*.gif
14+
*.mp4
15+
*.mov
16+
*.wav
17+
*.mp3
18+
node_modules
19+
**/node_modules
20+
.pnpm-store
21+
**/.pnpm-store
22+
.turbo
23+
**/.turbo
24+
.cache
25+
**/.cache
26+
.next
27+
**/.next
28+
coverage
29+
**/coverage
30+
*.log
31+
tmp
32+
**/tmp
33+
34+
# build artifacts
35+
dist
36+
**/dist
37+
apps/macos/.build
38+
apps/ios/build
39+
**/*.trace
40+
41+
# large app trees not needed for CLI build
42+
apps/
43+
ui/
44+
assets/
45+
Peekaboo/
46+
Swabble/
47+
Core/
48+
Users/
49+
vendor/

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
- Tests: add a Z.AI live test gate for smoke validation when keys are present.
1919
- macOS Debug: add app log verbosity and rolling file log toggle for swift-log-backed app logs.
2020
- CLI: add onboarding wizard (gateway + workspace + skills) with daemon installers and Anthropic/Minimax setup paths.
21+
- CLI: add ASCII banner header to onboarding wizard start.
2122
- Skills: allow `bun` as a node manager for skill installs.
2223
- Tests: add a Docker-based onboarding E2E harness.
2324

docs/wizard.md

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -60,22 +60,28 @@ Reset uses `trash` (never `rm`).
6060
- Auth: token | password | off
6161
- Tailscale: off | serve | funnel
6262

63-
5) **Daemon install (local only)**
63+
5) **Providers (optional)**
64+
- WhatsApp: optional `clawdis login` QR flow
65+
- Telegram: bot token (config or env)
66+
- Discord: bot token (config or env)
67+
- Signal: `signal-cli` detection + account config
68+
69+
6) **Daemon install (local only)**
6470
- macOS: LaunchAgent
6571
- Linux: systemd user unit
6672
- Windows: Scheduled Task
6773

68-
6) **Health**
74+
7) **Health**
6975
- Start/restart daemon
7076
- `clawdis health` summary
7177

72-
7) **Skills (recommended)**
78+
8) **Skills (recommended)**
7379
- Read from `buildWorkspaceSkillStatus`
7480
- Show eligible vs missing requirements
7581
- Offer installs via preferred installer
7682
- Allow skip
7783

78-
8) **Finish**
84+
9) **Finish**
7985
- Summary + next steps
8086
- Reminder: iOS/Android/macOS node apps add canvas/camera/screen/system features.
8187

@@ -97,6 +103,9 @@ Wizard writes:
97103
- `agent.model` + `models.providers` (if Minimax selected)
98104
- `skills.install.nodeManager` (npm | pnpm | bun)
99105
- `skills.entries.<key>.env` / `.apiKey` (if set in skills step)
106+
- `telegram.botToken`, `discord.token`, `signal.*` (if set in providers step)
107+
108+
WhatsApp login writes credentials to `~/.clawdis/credentials/creds.json`.
100109

101110
## Minimax M2.1 (LM Studio) config snippet
102111

scripts/e2e/Dockerfile

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,13 @@ FROM node:22-bookworm
33
RUN corepack enable
44

55
WORKDIR /app
6-
COPY . .
6+
7+
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml tsconfig.json vitest.config.ts ./
8+
COPY patches ./patches
9+
COPY src ./src
10+
COPY scripts ./scripts
11+
COPY docs ./docs
12+
COPY skills ./skills
713

814
RUN pnpm install --frozen-lockfile
915
RUN pnpm build

src/commands/onboard-auth.ts

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import fs from "node:fs/promises";
2+
import path from "node:path";
3+
4+
import type { OAuthCredentials } from "@mariozechner/pi-ai";
5+
import { discoverAuthStorage } from "@mariozechner/pi-coding-agent";
6+
7+
import { resolveClawdisAgentDir } from "../agents/agent-paths.js";
8+
import type { ClawdisConfig } from "../config/config.js";
9+
import { CONFIG_DIR } from "../utils.js";
10+
11+
export async function writeOAuthCredentials(
12+
provider: "anthropic",
13+
creds: OAuthCredentials,
14+
): Promise<void> {
15+
const dir = path.join(CONFIG_DIR, "credentials");
16+
await fs.mkdir(dir, { recursive: true, mode: 0o700 });
17+
const filePath = path.join(dir, "oauth.json");
18+
let storage: Record<string, OAuthCredentials> = {};
19+
try {
20+
const raw = await fs.readFile(filePath, "utf8");
21+
const parsed = JSON.parse(raw) as Record<string, OAuthCredentials>;
22+
if (parsed && typeof parsed === "object") storage = parsed;
23+
} catch {
24+
// ignore
25+
}
26+
storage[provider] = creds;
27+
await fs.writeFile(filePath, `${JSON.stringify(storage, null, 2)}\n`, "utf8");
28+
await fs.chmod(filePath, 0o600);
29+
}
30+
31+
export async function setAnthropicApiKey(key: string) {
32+
const agentDir = resolveClawdisAgentDir();
33+
const authStorage = discoverAuthStorage(agentDir);
34+
authStorage.set("anthropic", { type: "api_key", key });
35+
}
36+
37+
export function applyMinimaxConfig(cfg: ClawdisConfig): ClawdisConfig {
38+
const allowed = new Set(cfg.agent?.allowedModels ?? []);
39+
allowed.add("anthropic/claude-opus-4-5");
40+
allowed.add("lmstudio/minimax-m2.1-gs32");
41+
42+
const aliases = { ...(cfg.agent?.modelAliases ?? {}) };
43+
if (!aliases.Opus) aliases.Opus = "anthropic/claude-opus-4-5";
44+
if (!aliases.Minimax) aliases.Minimax = "lmstudio/minimax-m2.1-gs32";
45+
46+
const providers = { ...(cfg.models?.providers ?? {}) };
47+
if (!providers.lmstudio) {
48+
providers.lmstudio = {
49+
baseUrl: "http://127.0.0.1:1234/v1",
50+
apiKey: "lmstudio",
51+
api: "openai-responses",
52+
models: [
53+
{
54+
id: "minimax-m2.1-gs32",
55+
name: "MiniMax M2.1 GS32",
56+
reasoning: false,
57+
input: ["text"],
58+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
59+
contextWindow: 196608,
60+
maxTokens: 8192,
61+
},
62+
],
63+
};
64+
}
65+
66+
return {
67+
...cfg,
68+
agent: {
69+
...cfg.agent,
70+
model: "Minimax",
71+
allowedModels: Array.from(allowed),
72+
modelAliases: aliases,
73+
},
74+
models: {
75+
mode: cfg.models?.mode ?? "merge",
76+
providers,
77+
},
78+
};
79+
}

src/commands/onboard-helpers.ts

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
import crypto from "node:crypto";
2+
import fs from "node:fs/promises";
3+
import path from "node:path";
4+
5+
import { cancel, isCancel } from "@clack/prompts";
6+
7+
import {
8+
DEFAULT_AGENT_WORKSPACE_DIR,
9+
ensureAgentWorkspace,
10+
} from "../agents/workspace.js";
11+
import type { ClawdisConfig } from "../config/config.js";
12+
import { CONFIG_PATH_CLAWDIS } from "../config/config.js";
13+
import { resolveSessionTranscriptsDir } from "../config/sessions.js";
14+
import { runCommandWithTimeout } from "../process/exec.js";
15+
import type { RuntimeEnv } from "../runtime.js";
16+
import { CONFIG_DIR } from "../utils.js";
17+
import type { NodeManagerChoice, ResetScope } from "./onboard-types.js";
18+
19+
export function guardCancel<T>(value: T, runtime: RuntimeEnv): T {
20+
if (isCancel(value)) {
21+
cancel("Setup cancelled.");
22+
runtime.exit(0);
23+
}
24+
return value;
25+
}
26+
27+
export function summarizeExistingConfig(config: ClawdisConfig): string {
28+
const rows: string[] = [];
29+
if (config.agent?.workspace)
30+
rows.push(`workspace: ${config.agent.workspace}`);
31+
if (config.agent?.model) rows.push(`model: ${config.agent.model}`);
32+
if (config.gateway?.mode) rows.push(`gateway.mode: ${config.gateway.mode}`);
33+
if (config.gateway?.bind) rows.push(`gateway.bind: ${config.gateway.bind}`);
34+
if (config.skills?.install?.nodeManager) {
35+
rows.push(`skills.nodeManager: ${config.skills.install.nodeManager}`);
36+
}
37+
return rows.length ? rows.join("\n") : "No key settings detected.";
38+
}
39+
40+
export function randomToken(): string {
41+
return crypto.randomBytes(24).toString("hex");
42+
}
43+
44+
export async function openUrl(url: string): Promise<void> {
45+
const platform = process.platform;
46+
const command =
47+
platform === "darwin"
48+
? ["open", url]
49+
: platform === "win32"
50+
? ["cmd", "/c", "start", "", url]
51+
: ["xdg-open", url];
52+
try {
53+
await runCommandWithTimeout(command, { timeoutMs: 5_000 });
54+
} catch {
55+
// ignore; we still print the URL for manual open
56+
}
57+
}
58+
59+
export async function ensureWorkspaceAndSessions(
60+
workspaceDir: string,
61+
runtime: RuntimeEnv,
62+
) {
63+
const ws = await ensureAgentWorkspace({
64+
dir: workspaceDir,
65+
ensureBootstrapFiles: true,
66+
});
67+
runtime.log(`Workspace OK: ${ws.dir}`);
68+
const sessionsDir = resolveSessionTranscriptsDir();
69+
await fs.mkdir(sessionsDir, { recursive: true });
70+
runtime.log(`Sessions OK: ${sessionsDir}`);
71+
}
72+
73+
export function resolveNodeManagerOptions(): Array<{
74+
value: NodeManagerChoice;
75+
label: string;
76+
}> {
77+
return [
78+
{ value: "npm", label: "npm" },
79+
{ value: "pnpm", label: "pnpm" },
80+
{ value: "bun", label: "bun" },
81+
];
82+
}
83+
84+
export async function moveToTrash(
85+
pathname: string,
86+
runtime: RuntimeEnv,
87+
): Promise<void> {
88+
if (!pathname) return;
89+
try {
90+
await fs.access(pathname);
91+
} catch {
92+
return;
93+
}
94+
try {
95+
await runCommandWithTimeout(["trash", pathname], { timeoutMs: 5000 });
96+
runtime.log(`Moved to Trash: ${pathname}`);
97+
} catch {
98+
runtime.log(`Failed to move to Trash (manual delete): ${pathname}`);
99+
}
100+
}
101+
102+
export async function handleReset(
103+
scope: ResetScope,
104+
workspaceDir: string,
105+
runtime: RuntimeEnv,
106+
) {
107+
await moveToTrash(CONFIG_PATH_CLAWDIS, runtime);
108+
if (scope === "config") return;
109+
await moveToTrash(path.join(CONFIG_DIR, "credentials"), runtime);
110+
await moveToTrash(resolveSessionTranscriptsDir(), runtime);
111+
if (scope === "full") {
112+
await moveToTrash(workspaceDir, runtime);
113+
}
114+
}
115+
116+
export async function detectBinary(name: string): Promise<boolean> {
117+
const command =
118+
process.platform === "win32"
119+
? ["where", name]
120+
: ["/usr/bin/env", "sh", "-lc", `command -v ${name}`];
121+
try {
122+
const result = await runCommandWithTimeout(command, { timeoutMs: 2000 });
123+
return result.code === 0 && result.stdout.trim().length > 0;
124+
} catch {
125+
return false;
126+
}
127+
}
128+
129+
export const DEFAULT_WORKSPACE = DEFAULT_AGENT_WORKSPACE_DIR;

0 commit comments

Comments
 (0)