clearclaw 0.6.1 → 0.7.0

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.
@@ -4,6 +4,7 @@ import { createSdkMcpServer, tool } from "@anthropic-ai/claude-agent-sdk";
4
4
  import { z } from "zod";
5
5
  import log from "./logger.js";
6
6
  import { saveFile } from "./files.js";
7
+ import { assemblePrompt } from "./prompt.js";
7
8
  import { formatToolStatusLine, formatToolCallSummary, formatPermissionPrompt, formatTodoList, timeAgo } from "./format.js";
8
9
  import { permissionHandlers, displayHandledTools } from "./tool-handlers.js";
9
10
  function isTask(ctx) {
@@ -17,13 +18,13 @@ const MODE_OPTIONS = [
17
18
  ];
18
19
  export class Orchestrator {
19
20
  channel;
20
- engine;
21
+ engines;
21
22
  config;
22
23
  chats = new Map();
23
24
  tasks = new Map();
24
25
  constructor(opts) {
25
26
  this.channel = opts.channel;
26
- this.engine = opts.engine;
27
+ this.engines = opts.engines;
27
28
  this.config = opts.config;
28
29
  }
29
30
  chat(chatId) {
@@ -31,13 +32,22 @@ export class Orchestrator {
31
32
  if (!s) {
32
33
  s = {
33
34
  busy: false, abort: null, permissionMode: null, stats: null, lastStatusText: null,
34
- toolCallHandle: null, todoHandle: null,
35
+ toolCallHandle: null, todoHandle: null, textHandle: null, textBuffer: "",
36
+ engineName: null,
35
37
  messageQueue: [], debounceTimer: null,
36
38
  };
37
39
  this.chats.set(chatId, s);
38
40
  }
39
41
  return s;
40
42
  }
43
+ engineFor(ws) {
44
+ const name = ws.engine ?? this.config.defaultEngine;
45
+ const engine = this.engines.get(name);
46
+ if (!engine) {
47
+ throw new Error(`Unknown engine "${name}" configured for workspace "${ws.name}". Available: ${[...this.engines.keys()].join(", ")}`);
48
+ }
49
+ return engine;
50
+ }
41
51
  async start() {
42
52
  this.channel.on("message", (msg) => {
43
53
  this.routeMessage(msg).catch((err) => {
@@ -62,7 +72,7 @@ export class Orchestrator {
62
72
  return "assistant";
63
73
  if (ctx.behavior !== undefined)
64
74
  return ctx.behavior;
65
- return ctx.cwd === path.dirname(this.config.defaultPromptPath) ? "assistant" : "relay";
75
+ return ctx.cwd === this.config.homeWorkspacePath ? "assistant" : "relay";
66
76
  }
67
77
  /** Enqueue a message and drain — immediately for relay, debounced for assistant/task. */
68
78
  enqueueMessage(msg, ctx, state) {
@@ -131,10 +141,10 @@ export class Orchestrator {
131
141
  const logPrefix = task ? "[task-turn]" : "[turn]";
132
142
  log.info("%s start session=%s msgs=%d cwd=%s", logPrefix, sessionId ?? "new", messages.length, cwd);
133
143
  await this.channel.setTyping(chatId, true);
134
- const isHomeWorkspace = ws && ws.cwd === path.dirname(this.config.defaultPromptPath);
144
+ const assembledPrompt = assemblePrompt(this.config.frameworkPromptDir, this.config.instructionsDir);
135
145
  const appendSystemPrompt = task
136
- ? task.prompt
137
- : (isHomeWorkspace ? undefined : this.readDefaultPrompt());
146
+ ? (assembledPrompt ? `${assembledPrompt}\n\n${task.prompt}` : task.prompt)
147
+ : assembledPrompt;
138
148
  const prompt = buildPrompt(messages);
139
149
  // Save attachments for workspace turns
140
150
  const allAttachments = messages.flatMap((m) => m.attachments ?? []);
@@ -152,8 +162,10 @@ export class Orchestrator {
152
162
  name: "clearclaw",
153
163
  tools: this.buildMcpTools(chatId, behavior, turnState),
154
164
  });
165
+ const engine = ws ? this.engineFor(ws) : this.engines.get(this.config.defaultEngine);
166
+ state.engineName = engine.name;
155
167
  try {
156
- for await (const event of this.engine.runTurn({
168
+ for await (const event of engine.runTurn({
157
169
  sessionId,
158
170
  cwd,
159
171
  prompt,
@@ -188,6 +200,15 @@ export class Orchestrator {
188
200
  }
189
201
  state.toolCallHandle = null;
190
202
  state.todoHandle = null;
203
+ // Final formatted edit for any pending streaming text
204
+ if (state.textHandle && state.textBuffer) {
205
+ try {
206
+ await this.channel.editMessage(chatId, state.textHandle, state.textBuffer);
207
+ }
208
+ catch { /* best effort */ }
209
+ }
210
+ state.textHandle = null;
211
+ state.textBuffer = "";
191
212
  await this.updateStatusMessage(chatId, state);
192
213
  break;
193
214
  }
@@ -283,7 +304,7 @@ export class Orchestrator {
283
304
  await this.channel.sendMessage(msg.chatId, "No workspace linked to this group.");
284
305
  return;
285
306
  }
286
- const sessions = await this.engine.listSessions(ws.cwd);
307
+ const sessions = await this.engineFor(ws).listSessions(ws.cwd);
287
308
  if (sessions.length === 0) {
288
309
  await this.channel.sendMessage(msg.chatId, "No sessions found for this workspace.");
289
310
  return;
@@ -339,13 +360,15 @@ export class Orchestrator {
339
360
  }
340
361
  if (!ws) {
341
362
  if (this.config.isAuthorized(msg.user.id)) {
363
+ const chatType = msg.chatType === "dm" ? "DM" : "group";
342
364
  const newTask = {
343
365
  sessionId: null,
344
- cwd: path.dirname(this.config.defaultPromptPath),
366
+ cwd: this.config.homeWorkspacePath,
345
367
  prompt: [
346
368
  "THIS IS A TASK SESSION — not a regular conversation.",
347
369
  "Do NOT follow the 'Every Session' startup routine. Do NOT read MEMORY.md or daily notes. Do NOT greet the user.",
348
- "Your only job: help set up a workspace for this chat. Use the onboarding skill.",
370
+ `This is a ${chatType} chat. Home workspace path: ${this.config.homeWorkspacePath}`,
371
+ "Follow the Workspace Onboarding instructions in the system prompt.",
349
372
  ].join("\n"),
350
373
  };
351
374
  this.tasks.set(msg.chatId, newTask);
@@ -376,13 +399,49 @@ export class Orchestrator {
376
399
  replyToMessageId: turnState.replyToMessageId ?? undefined,
377
400
  });
378
401
  break;
402
+ case "text_chunk": {
403
+ if (turnState.staySilent)
404
+ break;
405
+ state.textBuffer += event.text;
406
+ const plainOpts = { format: "plain" };
407
+ if (state.textHandle) {
408
+ try {
409
+ await this.channel.editMessage(chatId, state.textHandle, state.textBuffer, plainOpts);
410
+ }
411
+ catch {
412
+ const handles = await this.channel.sendMessage(chatId, state.textBuffer, {
413
+ ...plainOpts,
414
+ replyToMessageId: turnState.replyToMessageId ?? undefined,
415
+ });
416
+ state.textHandle = handles[0];
417
+ }
418
+ }
419
+ else {
420
+ const handles = await this.channel.sendMessage(chatId, state.textBuffer, {
421
+ ...plainOpts,
422
+ replyToMessageId: turnState.replyToMessageId ?? undefined,
423
+ });
424
+ state.textHandle = handles[0];
425
+ }
426
+ break;
427
+ }
379
428
  case "tool_use": {
429
+ // Flush streaming text — final edit with markdown formatting
430
+ if (state.textHandle && state.textBuffer) {
431
+ try {
432
+ await this.channel.editMessage(chatId, state.textHandle, state.textBuffer);
433
+ }
434
+ catch { /* best effort */ }
435
+ }
436
+ state.textHandle = null;
437
+ state.textBuffer = "";
380
438
  // In assistant behavior, all tool status is suppressed (no rolling status, no plan mode notifications)
381
439
  if (behavior === "assistant")
382
440
  break;
383
441
  // Relay behavior: existing display logic
384
- if (event.toolName === "TodoWrite") {
385
- const text = formatTodoList(event.input);
442
+ const { tool } = event;
443
+ if (tool.toolName === "TodoWrite") {
444
+ const text = formatTodoList(tool);
386
445
  if (state.todoHandle) {
387
446
  try {
388
447
  await this.channel.editMessage(chatId, state.todoHandle, text);
@@ -398,13 +457,13 @@ export class Orchestrator {
398
457
  }
399
458
  break;
400
459
  }
401
- if (event.toolName === "EnterPlanMode") {
460
+ if (tool.toolName === "EnterPlanMode") {
402
461
  await this.channel.sendMessage(chatId, "📋 Planning");
403
462
  break;
404
463
  }
405
- if (displayHandledTools.has(event.toolName))
464
+ if (displayHandledTools.has(tool.toolName))
406
465
  break;
407
- const line = formatToolStatusLine(event.toolName, event.input);
466
+ const line = formatToolStatusLine(tool);
408
467
  if (state.toolCallHandle) {
409
468
  try {
410
469
  await this.channel.editMessage(chatId, state.toolCallHandle, line);
@@ -470,10 +529,15 @@ export class Orchestrator {
470
529
  cwd: z.string().describe("Absolute path to the workspace directory"),
471
530
  behavior: z.enum(["assistant", "relay"]).optional()
472
531
  .describe("Workspace behavior mode"),
532
+ engine: z.string().optional()
533
+ .describe("Engine to use (e.g. 'claude-code', 'kiro'). Defaults to the server's configured default engine."),
473
534
  }, async (args) => {
474
535
  if (this.config.workspaceByName(args.name)) {
475
536
  throw new Error(`Workspace "${args.name}" already exists. Choose a different name.`);
476
537
  }
538
+ if (args.engine && !this.engines.has(args.engine)) {
539
+ throw new Error(`Unknown engine "${args.engine}". Available: ${[...this.engines.keys()].join(", ")}`);
540
+ }
477
541
  fs.mkdirSync(args.cwd, { recursive: true });
478
542
  this.config.upsertWorkspace({
479
543
  name: args.name,
@@ -481,8 +545,9 @@ export class Orchestrator {
481
545
  chat_id: chatId,
482
546
  current_session_id: null,
483
547
  behavior: args.behavior,
548
+ engine: args.engine,
484
549
  });
485
- log.info("[tool] workspace_create: %s → %s (chat %s)", args.name, args.cwd, chatId);
550
+ log.info("[tool] workspace_create: %s → %s engine=%s (chat %s)", args.name, args.cwd, args.engine ?? "default", chatId);
486
551
  return { content: [{ type: "text", text: `Workspace "${args.name}" created at ${args.cwd}, linked to this chat.` }] };
487
552
  }), tool("task_complete", "Signal that the current task is complete", {
488
553
  message: z.string().optional().describe("Summary of what was accomplished"),
@@ -512,42 +577,42 @@ export class Orchestrator {
512
577
  }
513
578
  return tools;
514
579
  }
515
- async handlePermission(req, chatId) {
580
+ async handlePermission(tool, chatId) {
516
581
  // Always auto-allow ClearClaw's own MCP tools
517
- if (req.toolName.startsWith("mcp__clearclaw__")) {
518
- log.info(`[perm] ${req.toolName} → auto-allow`);
582
+ if (tool.toolName.startsWith("mcp__clearclaw__")) {
583
+ log.info(`[perm] ${tool.toolName} → auto-allow`);
519
584
  return { decision: "allow" };
520
585
  }
521
- log.info(`[perm] ${req.toolName}`);
522
- // Custom tool handler
523
- const handler = permissionHandlers.get(req.toolName);
586
+ log.info(`[perm] ${tool.toolName}`);
587
+ // Custom tool handler (Claude Code specific tools like EnterPlanMode)
588
+ const handler = permissionHandlers.get(tool.toolName);
524
589
  if (handler) {
525
- const result = handler(req.input, req.description);
590
+ const result = handler(tool);
526
591
  if (result === null) {
527
- log.info(`[perm] ${req.toolName} → auto-allow (handler)`);
592
+ log.info(`[perm] ${tool.toolName} → auto-allow (handler)`);
528
593
  await this.channel.sendMessage(chatId, "📋 Entering plan mode");
529
594
  return { decision: "allow" };
530
595
  }
531
596
  try {
532
597
  const resp = await this.channel.sendInteractive(chatId, result.text, result.buttons);
533
- log.info(`[perm] ${req.toolName} → ${resp.value || "timeout"}${resp.text ? ` "${resp.text}"` : ""}`);
598
+ log.info(`[perm] ${tool.toolName} → ${resp.value || "timeout"}${resp.text ? ` "${resp.text}"` : ""}`);
534
599
  return result.mapResponse(resp);
535
600
  }
536
601
  catch (err) {
537
- log.warn({ err }, "[perm] failed to send prompt for %s, auto-denying", req.toolName);
602
+ log.warn({ err }, "[perm] failed to send prompt for %s, auto-denying", tool.toolName);
538
603
  return { decision: "deny", message: "Permission prompt could not be delivered to chat — denied automatically." };
539
604
  }
540
605
  }
541
606
  try {
542
- const resp = await this.channel.sendInteractive(chatId, formatPermissionPrompt(req.toolName, req.input, req.description), [
607
+ const resp = await this.channel.sendInteractive(chatId, formatPermissionPrompt(tool), [
543
608
  [{ label: "👍 Allow", value: "allow" }, { label: "👎 Deny", value: "deny" }],
544
609
  [{ label: "📝 Deny + Note", value: "deny", requestText: true }],
545
610
  ]);
546
- log.info(`[perm] ${req.toolName} → ${resp.value || "timeout"}${resp.text ? ` "${resp.text}"` : ""}`);
611
+ log.info(`[perm] ${tool.toolName} → ${resp.value || "timeout"}${resp.text ? ` "${resp.text}"` : ""}`);
547
612
  return { decision: resp.value === "allow" ? "allow" : "deny", message: resp.text };
548
613
  }
549
614
  catch (err) {
550
- log.warn({ err }, "[perm] failed to send prompt for %s, auto-denying", req.toolName);
615
+ log.warn({ err }, "[perm] failed to send prompt for %s, auto-denying", tool.toolName);
551
616
  return { decision: "deny", message: "Permission prompt could not be delivered to chat — denied automatically." };
552
617
  }
553
618
  }
@@ -559,7 +624,10 @@ export class Orchestrator {
559
624
  const pct = state.stats.contextWindow > 0
560
625
  ? Math.round((state.stats.contextUsed / state.stats.contextWindow) * 100)
561
626
  : 0;
562
- text = `🤖 ${formatModelName(state.stats.model)} ${pct}% | 🔒 ${modeLabel}`;
627
+ const displayName = state.stats.model
628
+ ? formatModelName(state.stats.model)
629
+ : state.engineName ?? "agent";
630
+ text = `🤖 ${displayName} ${pct}% | 🔒 ${modeLabel}`;
563
631
  }
564
632
  else {
565
633
  text = `🔒 ${modeLabel}`;
@@ -569,16 +637,6 @@ export class Orchestrator {
569
637
  await this.channel.updateStatus(chatId, text);
570
638
  state.lastStatusText = text;
571
639
  }
572
- /** Read default workspace CLAUDE.md to append to non-default workspace sessions. */
573
- readDefaultPrompt() {
574
- try {
575
- const content = fs.readFileSync(this.config.defaultPromptPath, "utf-8").trim();
576
- return content || undefined;
577
- }
578
- catch {
579
- return undefined;
580
- }
581
- }
582
640
  }
583
641
  /** Strip "claude-" prefix and date suffix from model ID. e.g. "claude-opus-4-6-20250514" → "opus-4-6" */
584
642
  function formatModelName(modelId) {
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Assemble the system prompt from framework and user instruction files.
3
+ * Framework content comes first, user content appended after.
4
+ * Returns undefined if no content found in either directory.
5
+ */
6
+ export declare function assemblePrompt(frameworkDir: string, instructionsDir: string): string | undefined;
package/dist/prompt.js ADDED
@@ -0,0 +1,35 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ /**
4
+ * Read all .md files from a directory, sorted alphabetically.
5
+ * Returns empty array if directory doesn't exist.
6
+ */
7
+ function readMarkdownFiles(dir) {
8
+ if (!fs.existsSync(dir))
9
+ return [];
10
+ return fs.readdirSync(dir)
11
+ .filter((f) => f.endsWith(".md"))
12
+ .sort()
13
+ .map((f) => fs.readFileSync(path.join(dir, f), "utf-8").trim())
14
+ .filter(Boolean);
15
+ }
16
+ /**
17
+ * Assemble the system prompt from framework and user instruction files.
18
+ * Framework content comes first, user content appended after.
19
+ * Returns undefined if no content found in either directory.
20
+ */
21
+ export function assemblePrompt(frameworkDir, instructionsDir) {
22
+ const framework = readMarkdownFiles(frameworkDir);
23
+ const user = readMarkdownFiles(instructionsDir);
24
+ if (framework.length === 0 && user.length === 0)
25
+ return undefined;
26
+ const parts = [];
27
+ if (framework.length > 0) {
28
+ parts.push(framework.join("\n\n---\n\n"));
29
+ }
30
+ if (user.length > 0) {
31
+ parts.push(user.join("\n\n---\n\n"));
32
+ }
33
+ return parts.join("\n\n");
34
+ }
35
+ //# sourceMappingURL=prompt.js.map
@@ -13,7 +13,7 @@ export interface ToolPromptResult {
13
13
  buttons: Button[][];
14
14
  mapResponse: (resp: ButtonResponse) => PermissionResponse;
15
15
  }
16
- export type ToolPromptHandler = (input: Record<string, unknown>, description: string) => ToolPromptResult | null;
16
+ export type ToolPromptHandler = (input: Record<string, unknown>) => ToolPromptResult | null;
17
17
  export declare const permissionHandlers: Map<string, ToolPromptHandler>;
18
18
  /** Tools that suppress the default rolling status line in routeEngineEvent. */
19
19
  export declare const displayHandledTools: Set<string>;
@@ -34,8 +34,8 @@ function handleAskUserQuestion(input) {
34
34
  },
35
35
  };
36
36
  }
37
- function handleExitPlanMode(input, description) {
38
- const text = formatExitPlanMode(input, description);
37
+ function handleExitPlanMode(input) {
38
+ const text = formatExitPlanMode(input);
39
39
  return {
40
40
  text,
41
41
  buttons: [
package/dist/types.d.ts CHANGED
@@ -7,9 +7,9 @@ export interface Channel {
7
7
  connect(): Promise<void>;
8
8
  disconnect(): Promise<void>;
9
9
  ownsId(chatId: string): boolean;
10
- sendMessage(chatId: string, text: string, opts?: SendMessageOpts): Promise<string[]>;
10
+ sendMessage(chatId: string, text: string, opts?: MessageOpts): Promise<string[]>;
11
11
  sendInteractive(chatId: string, text: string, buttons: Button[][]): Promise<ButtonResponse>;
12
- editMessage(chatId: string, handle: string, text: string): Promise<void>;
12
+ editMessage(chatId: string, handle: string, text: string, opts?: MessageOpts): Promise<void>;
13
13
  deleteMessage(chatId: string, handle: string): Promise<void>;
14
14
  pinMessage(chatId: string, handle: string): Promise<void>;
15
15
  unpinAllMessages(chatId: string): Promise<void>;
@@ -30,11 +30,14 @@ export interface ButtonResponse {
30
30
  value: string;
31
31
  text?: string;
32
32
  }
33
- export interface SendMessageOpts {
33
+ export type TextFormat = "markdown" | "plain";
34
+ export interface MessageOpts {
34
35
  /** When false, skip consuming the typing placeholder (e.g. for tool status messages). */
35
36
  consumeTyping?: boolean;
36
37
  /** Reply/thread to a specific platform message ID (Telegram reply, Slack thread). */
37
38
  replyToMessageId?: string;
39
+ /** Text format hint. Defaults to "markdown". */
40
+ format?: TextFormat;
38
41
  }
39
42
  export interface SendFileOpts {
40
43
  caption?: string;
@@ -57,25 +60,59 @@ export interface RunTurnOpts {
57
60
  prompt: string;
58
61
  attachments?: Attachment[];
59
62
  permissionMode: PermissionMode;
60
- onPermissionRequest: (req: PermissionRequest) => Promise<PermissionResponse>;
63
+ onPermissionRequest: (req: ToolCall) => Promise<PermissionResponse>;
61
64
  appendSystemPrompt?: string;
62
65
  mcpServers?: Record<string, McpServerConfig>;
63
66
  signal?: AbortSignal;
64
67
  }
65
- export interface PermissionRequest {
68
+ interface ToolCallBase {
66
69
  toolName: string;
67
- input: Record<string, unknown>;
68
- description: string;
69
- reason?: string;
70
70
  toolUseId: string;
71
71
  }
72
+ declare const ACTIONS: {
73
+ readonly edit: "edit";
74
+ readonly write: "write";
75
+ readonly execute: "execute";
76
+ readonly read: "read";
77
+ readonly search: "search";
78
+ readonly fetch: "fetch";
79
+ };
80
+ export type KnownToolCall = (ToolCallBase & {
81
+ action: typeof ACTIONS.edit;
82
+ path: string;
83
+ before: string;
84
+ after: string;
85
+ }) | (ToolCallBase & {
86
+ action: typeof ACTIONS.write;
87
+ path: string;
88
+ content: string;
89
+ }) | (ToolCallBase & {
90
+ action: typeof ACTIONS.execute;
91
+ command: string;
92
+ }) | (ToolCallBase & {
93
+ action: typeof ACTIONS.read;
94
+ paths: string[];
95
+ }) | (ToolCallBase & {
96
+ action: typeof ACTIONS.search;
97
+ pattern: string;
98
+ paths?: string[];
99
+ }) | (ToolCallBase & {
100
+ action: typeof ACTIONS.fetch;
101
+ url: string;
102
+ });
103
+ export interface UnknownToolCall extends ToolCallBase {
104
+ action: string;
105
+ [key: string]: unknown;
106
+ }
107
+ export type ToolCall = KnownToolCall | UnknownToolCall;
108
+ export declare function isKnownToolCall(tool: ToolCall): tool is KnownToolCall;
72
109
  export interface PermissionResponse {
73
110
  decision: "allow" | "deny";
74
111
  message?: string;
75
112
  updatedInput?: Record<string, unknown>;
76
113
  }
77
114
  export interface TurnStats {
78
- model: string;
115
+ model: string | null;
79
116
  contextUsed: number;
80
117
  contextWindow: number;
81
118
  toolCalls: Record<string, number>;
@@ -83,11 +120,12 @@ export interface TurnStats {
83
120
  export type EngineEvent = {
84
121
  type: "text";
85
122
  text: string;
123
+ } | {
124
+ type: "text_chunk";
125
+ text: string;
86
126
  } | {
87
127
  type: "tool_use";
88
- toolName: string;
89
- input: Record<string, unknown>;
90
- toolUseId: string;
128
+ tool: ToolCall;
91
129
  } | {
92
130
  type: "tool_result";
93
131
  toolName: string;
@@ -110,6 +148,7 @@ export interface Workspace {
110
148
  chat_id: string;
111
149
  current_session_id: string | null;
112
150
  behavior?: "assistant" | "relay";
151
+ engine?: string;
113
152
  }
114
153
  export interface UserInfo {
115
154
  id: string;
package/dist/types.js CHANGED
@@ -1,2 +1,13 @@
1
- export {};
1
+ const ACTIONS = {
2
+ edit: "edit",
3
+ write: "write",
4
+ execute: "execute",
5
+ read: "read",
6
+ search: "search",
7
+ fetch: "fetch",
8
+ };
9
+ const knownActions = new Set(Object.values(ACTIONS));
10
+ export function isKnownToolCall(tool) {
11
+ return knownActions.has(tool.action);
12
+ }
2
13
  //# sourceMappingURL=types.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clearclaw",
3
- "version": "0.6.1",
3
+ "version": "0.7.0",
4
4
  "description": "Your coding agent, from your phone — personal assistant + project workspaces on your machine, built on the CLI's permissions, config, and memory instead of replacing them.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -26,7 +26,8 @@
26
26
  },
27
27
  "files": [
28
28
  "dist/**/*.js",
29
- "dist/**/*.d.ts"
29
+ "dist/**/*.d.ts",
30
+ "prompts/**/*.md"
30
31
  ],
31
32
  "scripts": {
32
33
  "start": "tsx src/index.ts daemon",
@@ -36,6 +37,7 @@
36
37
  "check": "tsc --noEmit"
37
38
  },
38
39
  "dependencies": {
40
+ "@agentclientprotocol/sdk": "^0.16.1",
39
41
  "@anthropic-ai/claude-agent-sdk": "^0.2.89",
40
42
  "@slack/bolt": "^4.6.0",
41
43
  "diff": "^8.0.3",
@@ -0,0 +1,56 @@
1
+ # Workspace Onboarding
2
+
3
+ When a task session tells you to follow the Workspace Onboarding instructions, you're guiding an authorized user through setting up a new workspace for this chat.
4
+
5
+ The task prompt tells you the chat type: DM or group. Use this to adapt the flow.
6
+
7
+ ## Steps
8
+
9
+ ### Group chats
10
+
11
+ 1. **Ask what they want to work on.** A specific project? A git repo? Or a general-purpose assistant chat?
12
+
13
+ 2. **Find the project.** If they mention a project or repo:
14
+ - Ask for the path, or offer to look in common locations (`~/`, `~/projects/`, `~/src/`, `~/repos/`, `~/workspaces/`, `~/workplace/`)
15
+ - Use `ls` or `find` to locate git repos if they're not sure where it is
16
+
17
+ 3. **Offer a git worktree** (if it's a git repo). Explain the benefit: an isolated copy on its own branch, so the main working tree isn't disturbed. If they want one, run `git worktree add <target_path> -b <branch_name>` from the repo. Use the worktree path as the workspace cwd.
18
+
19
+ 4. **Ask about the engine.** ClearClaw supports multiple AI engines. Ask which engine they want for this workspace (e.g. `claude-code`, `kiro`). Default to `claude-code` if they don't have a preference.
20
+
21
+ 5. **Create the workspace.** Once you have a name, path, and engine:
22
+ - Call `workspace_create` with a short, descriptive name, the absolute path, and the chosen engine
23
+ - For project repos: suggest relay behavior (default)
24
+ - For general-purpose chats: suggest assistant behavior
25
+ - Then call `task_complete` to finish setup
26
+
27
+ ## Guidelines
28
+
29
+ - Be conversational. Don't dump all questions at once.
30
+ - Keep workspace names short: `clearclaw`, `myapp`, `notes` — not `my-awesome-project-workspace`.
31
+ - If the user just wants a quick assistant chat (no specific project), create a workspace pointing at the home workspace path (provided in the task prompt) with assistant behavior.
32
+ - If something goes wrong (bad path, name conflict), explain and ask them to try again.
33
+
34
+ ### DM chats (home workspace)
35
+
36
+ For DMs, this is the user's personal assistant setup. Create the home workspace, then run the "Get to Know You" bootstrap.
37
+
38
+ 1. **Create the home workspace.** Call `workspace_create` with name `default`, cwd set to the home workspace path (provided in the task prompt), and behavior `assistant`. Don't ask -- DMs are always the home workspace.
39
+
40
+ 2. **Get to know them.** Ask structured questions, one at a time:
41
+ - What should I call you?
42
+ - What timezone are you in?
43
+ - What personality/vibe do you want from me? (warm, professional, playful, direct, etc.)
44
+ - Would you like to name me, or should I suggest something?
45
+
46
+ 3. **Discover tools.** Scan PATH for common CLI tools the user might want you to use (package managers, dev tools, productivity CLIs). Present what you find: "I see you have brew, git, node installed. Any other tools you use regularly that I should know about?" Note preferred package manager and whether they want you to install tools proactively.
47
+
48
+ 4. **Open up.** Ask: "Anything else I should know about you, your work, or how you like to communicate?" Let the user go wherever they want. Don't constrain or redirect. If they share context that belongs in memory rather than instructions, capture it there without interrupting the conversation.
49
+
50
+ 5. **Write the files.** Sort everything you learned into the right place:
51
+ - Agent personality, voice, vibe -> `instructions/IDENTITY.md`
52
+ - User profile, preferences, communication style -> `instructions/USER.md`
53
+ - Tools, configurations, CLI preferences -> `instructions/TOOLS.md`
54
+ - Facts, context, decisions -> `memory/MEMORY.md`
55
+
56
+ 6. **Finish.** Call `task_complete`. The instruction files are now part of the assembled system prompt for all future sessions.