clearclaw 0.5.0 → 0.6.1

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.
package/dist/config.js CHANGED
@@ -1,53 +1,224 @@
1
1
  import fs from "node:fs";
2
2
  import os from "node:os";
3
3
  import path from "node:path";
4
- export function loadConfig() {
4
+ import crypto from "node:crypto";
5
+ import { fileURLToPath } from "node:url";
6
+ // --- Pairing constants ---
7
+ const CODE_LENGTH = 8;
8
+ const CODE_ALPHABET = "23456789ABCDEFGHJKLMNPQRSTUVWXYZ"; // no ambiguous 0/O/1/I/l
9
+ const EXPIRY_MS = 60 * 60 * 1000; // 1 hour
10
+ const MAX_PENDING = 3;
11
+ // --- Resolve data directory ---
12
+ export function resolveDataDir() {
5
13
  const dataDir = process.env.CLEARCLAW_HOME ?? path.join(os.homedir(), ".clearclaw");
6
14
  fs.mkdirSync(dataDir, { recursive: true });
7
15
  fs.mkdirSync(path.join(dataDir, "files"), { recursive: true });
8
- // Channel config: Slack takes priority if both SLACK_BOT_TOKEN and SLACK_APP_TOKEN are set
9
- const slackBotToken = process.env.SLACK_BOT_TOKEN;
10
- const slackAppToken = process.env.SLACK_APP_TOKEN;
11
- const telegramBotToken = process.env.TELEGRAM_BOT_TOKEN;
12
- let channel;
13
- if (slackBotToken && slackAppToken) {
14
- channel = { type: "slack", botToken: slackBotToken, appToken: slackAppToken };
15
- }
16
- else if (telegramBotToken) {
17
- channel = { type: "telegram", botToken: telegramBotToken };
18
- }
19
- else {
20
- throw new Error("Missing channel config: set TELEGRAM_BOT_TOKEN or both SLACK_BOT_TOKEN + SLACK_APP_TOKEN");
21
- }
22
- const rawIds = process.env.ALLOWED_USER_IDS ?? process.env.ALLOWED_USER_ID;
23
- if (!rawIds)
24
- throw new Error("Missing required env var: ALLOWED_USER_IDS");
25
- const allowedUserIds = new Set(rawIds.split(",").map((s) => s.trim()).filter(Boolean));
26
- const permissionMode = (process.env.PERMISSION_MODE ?? "default");
27
- const valid = [
28
- "default",
29
- "acceptEdits",
30
- "bypassPermissions",
31
- "plan",
32
- "dontAsk",
33
- ];
34
- if (!valid.includes(permissionMode)) {
35
- throw new Error(`PERMISSION_MODE must be one of: ${valid.join(", ")}`);
36
- }
37
- return {
38
- channel,
39
- allowedUserIds,
40
- permissionMode,
41
- defaultPromptPath: path.join(dataDir, "workspace", "CLAUDE.md"),
42
- workspaceStorePath: path.join(dataDir, "workspaces.json"),
43
- filesPath: path.join(dataDir, "files"),
44
- logPath: path.join(dataDir, "clearclaw.log"),
45
- };
16
+ return dataDir;
46
17
  }
47
- function requireEnv(name) {
48
- const val = process.env[name];
49
- if (!val)
50
- throw new Error(`Missing required env var: ${name}`);
51
- return val;
18
+ // --- Config class ---
19
+ /**
20
+ * Unified config: env var resolution + persistent store backed by config.json.
21
+ *
22
+ * - `new Config(dataDir)` gives store access (read/write methods)
23
+ * - `config.resolve()` adds env var settings (channel, envUserIds, permissionMode)
24
+ *
25
+ * File is re-read on every store call (hot-reload without file watching).
26
+ * Writes use 0o600 permissions (file contains bot token).
27
+ */
28
+ export class Config {
29
+ // Resolved settings — populated by resolve()
30
+ channel;
31
+ permissionMode = "default";
32
+ envUserIds = new Set();
33
+ // Derived paths
34
+ defaultPromptPath;
35
+ filesPath;
36
+ logPath;
37
+ dataDir = resolveDataDir();
38
+ filePath;
39
+ constructor() {
40
+ this.filePath = path.join(this.dataDir, "config.json");
41
+ this.defaultPromptPath = path.join(this.dataDir, "workspace", "CLAUDE.md");
42
+ this.filesPath = path.join(this.dataDir, "files");
43
+ this.logPath = path.join(this.dataDir, "clearclaw.log");
44
+ }
45
+ /** Resolve channel + env vars. Throws if no channel config found. */
46
+ resolve() {
47
+ const slackBotToken = process.env.SLACK_BOT_TOKEN;
48
+ const slackAppToken = process.env.SLACK_APP_TOKEN;
49
+ const telegramBotToken = process.env.TELEGRAM_BOT_TOKEN;
50
+ if (slackBotToken && slackAppToken) {
51
+ this.channel = { type: "slack", botToken: slackBotToken, appToken: slackAppToken };
52
+ }
53
+ else if (telegramBotToken) {
54
+ this.channel = { type: "telegram", botToken: telegramBotToken };
55
+ }
56
+ else {
57
+ const saved = this.getChannel();
58
+ if (!saved) {
59
+ throw new Error("Missing channel config: set TELEGRAM_BOT_TOKEN or SLACK_BOT_TOKEN + SLACK_APP_TOKEN, or run `clearclaw setup`");
60
+ }
61
+ this.channel = saved;
62
+ }
63
+ const rawIds = process.env.ALLOWED_USER_IDS ?? process.env.ALLOWED_USER_ID;
64
+ this.envUserIds = new Set(rawIds ? rawIds.split(",").map((s) => s.trim()).filter(Boolean) : []);
65
+ const pm = (process.env.PERMISSION_MODE ?? "default");
66
+ const valid = [
67
+ "default", "acceptEdits", "bypassPermissions", "plan", "dontAsk",
68
+ ];
69
+ if (!valid.includes(pm)) {
70
+ throw new Error(`PERMISSION_MODE must be one of: ${valid.join(", ")}`);
71
+ }
72
+ this.permissionMode = pm;
73
+ return this;
74
+ }
75
+ // --- File I/O (internal) ---
76
+ read() {
77
+ if (!fs.existsSync(this.filePath)) {
78
+ return { authorizedUsers: [], pendingPairings: [], workspaces: [] };
79
+ }
80
+ try {
81
+ const raw = JSON.parse(fs.readFileSync(this.filePath, "utf-8"));
82
+ return {
83
+ channel: raw.channel,
84
+ authorizedUsers: raw.authorizedUsers ?? [],
85
+ pendingPairings: raw.pendingPairings ?? [],
86
+ workspaces: raw.workspaces ?? [],
87
+ };
88
+ }
89
+ catch {
90
+ return { authorizedUsers: [], pendingPairings: [], workspaces: [] };
91
+ }
92
+ }
93
+ write(data) {
94
+ fs.writeFileSync(this.filePath, JSON.stringify(data, null, 2) + "\n", { mode: 0o600 });
95
+ }
96
+ // --- Channel ---
97
+ getChannel() {
98
+ return this.read().channel;
99
+ }
100
+ setChannel(config) {
101
+ const data = this.read();
102
+ data.channel = config;
103
+ this.write(data);
104
+ }
105
+ // --- Authorized users ---
106
+ hasUser(userId) {
107
+ return this.read().authorizedUsers.some((u) => u.id === userId);
108
+ }
109
+ addUser(user) {
110
+ const data = this.read();
111
+ if (data.authorizedUsers.some((u) => u.id === user.id))
112
+ return;
113
+ data.authorizedUsers.push(user);
114
+ this.write(data);
115
+ }
116
+ listUsers() {
117
+ return this.read().authorizedUsers;
118
+ }
119
+ /** env var IDs (in-memory) + pairing-approved users (hot-reload from disk) */
120
+ isAuthorized(userId) {
121
+ return this.envUserIds.has(userId) || this.hasUser(userId);
122
+ }
123
+ // --- Pairing codes ---
124
+ createPairing(userId, userName, chatId) {
125
+ const data = this.read();
126
+ const now = Date.now();
127
+ // Filter expired
128
+ data.pendingPairings = data.pendingPairings.filter((p) => p.expiresAt > now);
129
+ // Idempotent: return existing code for same user
130
+ const existing = data.pendingPairings.find((p) => p.userId === userId);
131
+ if (existing) {
132
+ this.write(data);
133
+ return existing.code;
134
+ }
135
+ // Evict oldest if at capacity
136
+ if (data.pendingPairings.length >= MAX_PENDING) {
137
+ data.pendingPairings.sort((a, b) => a.expiresAt - b.expiresAt);
138
+ data.pendingPairings = data.pendingPairings.slice(data.pendingPairings.length - MAX_PENDING + 1);
139
+ }
140
+ const code = this.generateCode();
141
+ data.pendingPairings.push({ code, userId, userName, chatId, expiresAt: now + EXPIRY_MS });
142
+ this.write(data);
143
+ return code;
144
+ }
145
+ consumePairing(code) {
146
+ const data = this.read();
147
+ const now = Date.now();
148
+ const idx = data.pendingPairings.findIndex((p) => p.code.toLowerCase() === code.toLowerCase() && p.expiresAt > now);
149
+ if (idx < 0)
150
+ return null;
151
+ const [pairing] = data.pendingPairings.splice(idx, 1);
152
+ this.write(data);
153
+ return pairing;
154
+ }
155
+ generateCode() {
156
+ const bytes = crypto.randomBytes(CODE_LENGTH);
157
+ return Array.from(bytes)
158
+ .map((b) => CODE_ALPHABET[b % CODE_ALPHABET.length])
159
+ .join("");
160
+ }
161
+ // --- Workspaces ---
162
+ workspaceByChat(chatId) {
163
+ return this.read().workspaces.find((ws) => ws.chat_id === chatId);
164
+ }
165
+ workspaceByName(name) {
166
+ return this.read().workspaces.find((ws) => ws.name === name);
167
+ }
168
+ upsertWorkspace(ws) {
169
+ const data = this.read();
170
+ const idx = data.workspaces.findIndex((w) => w.name === ws.name);
171
+ if (idx >= 0)
172
+ data.workspaces[idx] = ws;
173
+ else
174
+ data.workspaces.push(ws);
175
+ this.write(data);
176
+ }
177
+ setSession(name, sessionId) {
178
+ const data = this.read();
179
+ const ws = data.workspaces.find((w) => w.name === name);
180
+ if (ws) {
181
+ ws.current_session_id = sessionId;
182
+ this.write(data);
183
+ }
184
+ }
185
+ clearSession(name) {
186
+ const data = this.read();
187
+ const ws = data.workspaces.find((w) => w.name === name);
188
+ if (ws) {
189
+ ws.current_session_id = null;
190
+ this.write(data);
191
+ }
192
+ }
193
+ setBehavior(name, behavior) {
194
+ const data = this.read();
195
+ const ws = data.workspaces.find((w) => w.name === name);
196
+ if (ws) {
197
+ ws.behavior = behavior;
198
+ this.write(data);
199
+ }
200
+ }
201
+ // --- Approval ---
202
+ ensureDefaultWorkspace(chatId) {
203
+ if (this.workspaceByChat(chatId) || this.workspaceByName("default"))
204
+ return;
205
+ const defaultCwd = path.join(this.dataDir, "workspace");
206
+ fs.mkdirSync(defaultCwd, { recursive: true });
207
+ this.upsertWorkspace({ name: "default", cwd: defaultCwd, chat_id: chatId, current_session_id: null });
208
+ this.syncSkills();
209
+ }
210
+ /** Copy bundled skills from the repo's skills/ directory to the home workspace's .claude/skills/. */
211
+ syncSkills() {
212
+ const pkgRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
213
+ const srcSkills = path.join(pkgRoot, "skills");
214
+ if (!fs.existsSync(srcSkills))
215
+ return;
216
+ const destSkills = path.join(this.dataDir, "workspace", ".claude", "skills");
217
+ fs.cpSync(srcSkills, destSkills, { recursive: true, force: true });
218
+ }
219
+ approveUser(userId, userName, chatId) {
220
+ this.addUser({ id: userId, name: userName, approvedAt: Date.now() });
221
+ this.ensureDefaultWorkspace(chatId);
222
+ }
52
223
  }
53
224
  //# sourceMappingURL=config.js.map
@@ -36,7 +36,7 @@ export class ClaudeCodeEngine {
36
36
  toolUseId: options.toolUseID,
37
37
  });
38
38
  if (resp.decision === "allow") {
39
- return { behavior: "allow", updatedInput: input };
39
+ return { behavior: "allow", updatedInput: resp.updatedInput ?? input };
40
40
  }
41
41
  const denyMessage = resp.message
42
42
  ? `User denied this action with feedback: ${resp.message}`
@@ -56,6 +56,7 @@ export class ClaudeCodeEngine {
56
56
  const prompt = attachments?.length
57
57
  ? buildAttachmentPrompt(textPrompt, attachments)
58
58
  : textPrompt;
59
+ log.info("[turn] permissionMode=%s session=%s", permissionMode, sessionId ?? "new");
59
60
  const q = query({
60
61
  prompt,
61
62
  options: {
@@ -63,7 +64,7 @@ export class ClaudeCodeEngine {
63
64
  cwd,
64
65
  permissionMode,
65
66
  allowDangerouslySkipPermissions: permissionMode === "bypassPermissions" ? true : undefined,
66
- canUseTool,
67
+ canUseTool: permissionMode === "bypassPermissions" ? undefined : canUseTool,
67
68
  abortController,
68
69
  settingSources: ["user", "project", "local"],
69
70
  ...(mcpServers ? { mcpServers } : {}),
package/dist/format.d.ts CHANGED
@@ -22,7 +22,33 @@ export declare function formatPermissionPrompt(toolName: string, input: Record<s
22
22
  * Relative time string: "just now", "5m ago", "2h ago", "3d ago".
23
23
  */
24
24
  export declare function timeAgo(epochMs: number): string;
25
+ interface AskUserQuestionOption {
26
+ label: string;
27
+ description: string;
28
+ }
29
+ /**
30
+ * Format the first question from an AskUserQuestion tool call.
31
+ * Returns { text, options } where options are the parsed option objects.
32
+ */
33
+ export declare function formatAskUserQuestion(input: Record<string, unknown>): {
34
+ text: string;
35
+ options: AskUserQuestionOption[];
36
+ };
37
+ /**
38
+ * Truncate a button label to MAX_BTN chars with number prefix.
39
+ */
40
+ export declare function truncateButtonLabel(index: number, label: string): string;
41
+ /**
42
+ * Format an ExitPlanMode permission prompt.
43
+ * Renders plan text from the description or input fields.
44
+ */
45
+ export declare function formatExitPlanMode(input: Record<string, unknown>, description: string): string;
46
+ /**
47
+ * Format a TodoWrite task list for display.
48
+ */
49
+ export declare function formatTodoList(input: Record<string, unknown>): string;
25
50
  /**
26
51
  * Human-readable one-liner for a tool invocation (used in engine's canUseTool).
27
52
  */
28
53
  export declare function formatToolDescription(toolName: string, input: Record<string, unknown>): string;
54
+ export {};
package/dist/format.js CHANGED
@@ -117,6 +117,57 @@ export function timeAgo(epochMs) {
117
117
  const days = Math.floor(hours / 24);
118
118
  return `${days}d ago`;
119
119
  }
120
+ const MAX_BTN = 45;
121
+ /**
122
+ * Format the first question from an AskUserQuestion tool call.
123
+ * Returns { text, options } where options are the parsed option objects.
124
+ */
125
+ export function formatAskUserQuestion(input) {
126
+ const { questions } = input;
127
+ const q = questions[0];
128
+ const lines = [`❓ ${q.header}`, q.question, ""];
129
+ for (let i = 0; i < q.options.length; i++) {
130
+ const opt = q.options[i];
131
+ lines.push(`${i + 1}. ${opt.label} — ${opt.description}`);
132
+ }
133
+ return { text: lines.join("\n"), options: q.options };
134
+ }
135
+ /**
136
+ * Truncate a button label to MAX_BTN chars with number prefix.
137
+ */
138
+ export function truncateButtonLabel(index, label) {
139
+ const full = `${index + 1}. ${label}`;
140
+ if (full.length <= MAX_BTN)
141
+ return full;
142
+ return `${full.slice(0, MAX_BTN - 1)}…`;
143
+ }
144
+ /**
145
+ * Format an ExitPlanMode permission prompt.
146
+ * Renders plan text from the description or input fields.
147
+ */
148
+ export function formatExitPlanMode(input, description) {
149
+ const plan = (typeof input.plan === "string" ? input.plan : null)
150
+ || JSON.stringify(input, null, 2);
151
+ const lines = plan.split("\n");
152
+ const body = truncateLines(lines, MAX_LINES);
153
+ return `📋 Plan Review\n\n${body}`;
154
+ }
155
+ /**
156
+ * Format a TodoWrite task list for display.
157
+ */
158
+ export function formatTodoList(input) {
159
+ const todos = (input.todos ?? []);
160
+ if (todos.length === 0)
161
+ return "📋 Tasks\n(empty)";
162
+ const lines = todos.map((t) => {
163
+ switch (t.status) {
164
+ case "completed": return `✅ ${t.content}`;
165
+ case "in_progress": return `⏳ ${t.activeForm}`;
166
+ default: return `⬚ ${t.content}`;
167
+ }
168
+ });
169
+ return `📋 Tasks\n${lines.join("\n")}`;
170
+ }
120
171
  function truncateLines(lines, max) {
121
172
  if (lines.length <= max) {
122
173
  return lines.join("\n");
package/dist/index.js CHANGED
@@ -1,50 +1,152 @@
1
1
  #!/usr/bin/env node
2
- import { loadConfig } from "./config.js";
2
+ import readline from "node:readline";
3
+ import { Config } from "./config.js";
3
4
  import log, { initLogger } from "./logger.js";
4
- import { WorkspaceStore } from "./workspace-store.js";
5
5
  import { ClaudeCodeEngine } from "./engine/claude-code.js";
6
6
  import { TelegramChannel } from "./channel/telegram.js";
7
7
  import { SlackChannel } from "./channel/slack.js";
8
8
  import { Orchestrator } from "./orchestrator.js";
9
- // The Claude Agent SDK has a bug: handleControlRequest() writes to the
10
- // subprocess stdin without a .catch(), so aborting during a permission
11
- // callback causes an unhandled rejection (write to dead process).
12
- // Suppress that specific error so we can use abort for deny + /cancel.
13
- process.on("unhandledRejection", (reason) => {
14
- const msg = reason instanceof Error ? reason.message : String(reason);
15
- if (msg.includes("Operation aborted") ||
16
- msg.includes("Failed to write to process stdin") ||
17
- msg.includes("Cannot write to terminated process")) {
18
- log.warn("[sdk] suppressed write error (known SDK bug): %s", msg);
19
- return;
20
- }
21
- log.fatal({ err: reason }, "Unhandled rejection");
22
- process.exit(1);
23
- });
24
- async function main() {
25
- const config = loadConfig();
9
+ async function runDaemon() {
10
+ const config = new Config().resolve();
11
+ config.syncSkills();
26
12
  initLogger(config.logPath);
27
- const workspaceStore = new WorkspaceStore(config.workspaceStorePath);
28
- let channel;
29
- if (config.channel.type === "slack") {
30
- channel = new SlackChannel(config.channel.botToken, config.channel.appToken, config.allowedUserIds);
13
+ // The Claude Agent SDK has a bug: handleControlRequest() writes to the
14
+ // subprocess stdin without a .catch(), so aborting during a permission
15
+ // callback causes an unhandled rejection (write to dead process).
16
+ // Suppress that specific error so we can use abort for deny + /cancel.
17
+ process.on("unhandledRejection", (reason) => {
18
+ const msg = reason instanceof Error ? reason.message : String(reason);
19
+ if (msg.includes("Operation aborted") ||
20
+ msg.includes("Failed to write to process stdin") ||
21
+ msg.includes("Cannot write to terminated process")) {
22
+ log.warn("[sdk] suppressed write error (known SDK bug): %s", msg);
23
+ return;
24
+ }
25
+ log.fatal({ err: reason }, "Unhandled rejection");
26
+ process.exit(1);
27
+ });
28
+ // Daemon always uses pairing code flow (no console/readline)
29
+ const onUnauthorizedDM = (chatId, user) => {
30
+ const code = config.createPairing(user.id, user.name, chatId);
31
+ const text = [
32
+ `Your user ID: ${user.id}`,
33
+ `Pairing code: ${code}`,
34
+ "",
35
+ "Ask the server owner to run:",
36
+ ` clearclaw approve ${code}`,
37
+ "",
38
+ "Code expires in 1 hour.",
39
+ ].join("\n");
40
+ channel.sendMessage(chatId, text).catch((err) => {
41
+ log.warn({ err }, "[pairing] failed to send pairing message");
42
+ });
43
+ };
44
+ const channel = createChannel(config.channel, (userId) => config.isAuthorized(userId), onUnauthorizedDM);
45
+ await new Orchestrator({ channel, engine: new ClaudeCodeEngine(), config }).start();
46
+ }
47
+ async function runApprove(args) {
48
+ const code = args[0];
49
+ if (!code) {
50
+ console.error("Usage: clearclaw approve <code>");
51
+ process.exit(1);
31
52
  }
32
- else {
33
- channel = new TelegramChannel(config.channel.botToken, config.allowedUserIds);
53
+ const config = new Config();
54
+ const pairing = config.consumePairing(code);
55
+ if (!pairing) {
56
+ console.error("Invalid or expired pairing code.");
57
+ process.exit(1);
34
58
  }
35
- const engine = new ClaudeCodeEngine();
36
- const orchestrator = new Orchestrator({
37
- channel,
38
- engine,
39
- workspaceStore,
40
- permissionMode: config.permissionMode,
41
- defaultPromptPath: config.defaultPromptPath,
42
- filesPath: config.filesPath,
43
- });
44
- await orchestrator.start();
59
+ config.approveUser(pairing.userId, pairing.userName, pairing.chatId);
60
+ console.log(`Approved ${pairing.userName} (${pairing.userId})`);
61
+ }
62
+ const SETUP_TIMEOUT_MS = 5 * 60 * 1000;
63
+ async function runSetup() {
64
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
65
+ const ask = (q) => new Promise((resolve) => rl.question(q, resolve));
66
+ let channel = null;
67
+ try {
68
+ console.log("ClearClaw setup\n");
69
+ let channelType;
70
+ while (true) {
71
+ channelType = (await ask("Channel [telegram/slack]: ")).trim().toLowerCase();
72
+ if (channelType === "telegram" || channelType === "slack")
73
+ break;
74
+ console.log(' Please enter "telegram" or "slack".');
75
+ }
76
+ let channelConfig;
77
+ if (channelType === "slack") {
78
+ const botToken = (await ask("Slack bot token (xoxb-...): ")).trim();
79
+ const appToken = (await ask("Slack app token (xapp-...): ")).trim();
80
+ if (!botToken || !appToken)
81
+ throw new Error("Both tokens required for Slack.");
82
+ channelConfig = { type: "slack", botToken, appToken };
83
+ }
84
+ else {
85
+ const botToken = (await ask("Telegram bot token: ")).trim();
86
+ if (!botToken)
87
+ throw new Error("Bot token required.");
88
+ channelConfig = { type: "telegram", botToken };
89
+ }
90
+ const config = new Config();
91
+ config.setChannel(channelConfig);
92
+ console.log("Saved to ~/.clearclaw/config.json\n");
93
+ config.resolve();
94
+ let resolveFirstDM;
95
+ const firstDM = new Promise((r) => {
96
+ resolveFirstDM = r;
97
+ });
98
+ channel = createChannel(config.channel, (userId) => config.isAuthorized(userId), (chatId, user) => resolveFirstDM({ chatId, user }));
99
+ console.log("Connecting...");
100
+ await channel.connect();
101
+ console.log("Connected! Waiting for a DM — message the bot from your chat app.\n");
102
+ const { chatId, user } = await Promise.race([
103
+ firstDM,
104
+ new Promise((_, reject) => setTimeout(() => reject(new Error("Timed out. Run `clearclaw setup` again.")), SETUP_TIMEOUT_MS)),
105
+ ]);
106
+ console.log(`→ Message from ${user.name} (${user.id})`);
107
+ const pairingCode = config.createPairing(user.id, user.name, chatId);
108
+ await channel.sendMessage(chatId, [
109
+ `Pairing code: ${pairingCode}`,
110
+ "",
111
+ "The server owner is approving you now.",
112
+ "Code expires in 1 hour.",
113
+ ].join("\n"));
114
+ const entered = (await ask(" Enter pairing code to approve: ")).trim();
115
+ const pairing = config.consumePairing(entered);
116
+ if (!pairing) {
117
+ console.log(" Invalid code. Exiting.");
118
+ await channel.sendMessage(chatId, "Pairing code was invalid. Please try again.");
119
+ return;
120
+ }
121
+ config.approveUser(pairing.userId, pairing.userName, pairing.chatId);
122
+ console.log(` Approved ${pairing.userName} (${pairing.userId})`);
123
+ await channel.sendMessage(chatId, "You're approved! The bot is being set up — you'll be able to chat soon.");
124
+ console.log("\nSetup complete! Start the daemon:\n clearclaw daemon\n");
125
+ }
126
+ finally {
127
+ if (channel)
128
+ await channel.disconnect().catch(() => { });
129
+ rl.close();
130
+ }
131
+ }
132
+ function createChannel(ch, isAuthorized, onUnauthorizedDM) {
133
+ if (ch.type === "slack") {
134
+ return new SlackChannel(ch.botToken, ch.appToken, isAuthorized, onUnauthorizedDM);
135
+ }
136
+ return new TelegramChannel(ch.botToken, isAuthorized, onUnauthorizedDM);
137
+ }
138
+ switch (process.argv[2]) {
139
+ case "setup":
140
+ await runSetup();
141
+ break;
142
+ case "approve":
143
+ await runApprove(process.argv.slice(3));
144
+ break;
145
+ case "daemon":
146
+ await runDaemon();
147
+ break;
148
+ default:
149
+ console.log(`Usage: clearclaw <command>\n\nCommands:\n setup Interactive first-run setup\n daemon Start the relay daemon\n approve Approve a pairing code\n\nRun 'clearclaw setup' to get started.`);
150
+ process.exit(process.argv[2] ? 1 : 0);
45
151
  }
46
- main().catch((err) => {
47
- log.fatal({ err }, "Fatal");
48
- process.exit(1);
49
- });
50
152
  //# sourceMappingURL=index.js.map
@@ -1,27 +1,31 @@
1
- import type { WorkspaceStore } from "./workspace-store.js";
2
- import type { Channel, Engine, PermissionMode } from "./types.js";
1
+ import type { Config } from "./config.js";
2
+ import type { Channel, Engine } from "./types.js";
3
3
  export interface OrchestratorOpts {
4
4
  channel: Channel;
5
5
  engine: Engine;
6
- workspaceStore: WorkspaceStore;
7
- permissionMode: PermissionMode;
8
- defaultPromptPath: string;
9
- filesPath: string;
6
+ config: Config;
10
7
  }
11
8
  export declare class Orchestrator {
12
9
  private channel;
13
10
  private engine;
14
- private workspaceStore;
15
- private permissionMode;
16
- private defaultPromptPath;
17
- private filesPath;
11
+ private config;
18
12
  private chats;
13
+ private tasks;
19
14
  constructor(opts: OrchestratorOpts);
20
15
  private chat;
21
16
  start(): Promise<void>;
22
17
  stop(): Promise<void>;
23
- private handleMessage;
18
+ /** Effective behavior: tasks→assistant, workspace→explicit setting or home→assistant / project→relay. */
19
+ private effectiveBehavior;
20
+ /** Enqueue a message and drain — immediately for relay, debounced for assistant/task. */
21
+ private enqueueMessage;
22
+ private scheduleDebounce;
23
+ private processQueuedMessages;
24
+ private executeTurn;
25
+ private routeMessage;
24
26
  private routeEngineEvent;
27
+ private buildMcpTools;
28
+ private handlePermission;
25
29
  private updateStatusMessage;
26
30
  /** Read default workspace CLAUDE.md to append to non-default workspace sessions. */
27
31
  private readDefaultPrompt;