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.
- package/dist/channel/slack.d.ts +3 -3
- package/dist/channel/slack.js +45 -19
- package/dist/channel/telegram.d.ts +3 -3
- package/dist/channel/telegram.js +30 -12
- package/dist/config.d.ts +13 -5
- package/dist/config.js +41 -33
- package/dist/engine/acp.d.ts +9 -0
- package/dist/engine/acp.js +247 -0
- package/dist/engine/async-queue.d.ts +14 -0
- package/dist/engine/async-queue.js +45 -0
- package/dist/engine/claude-code.d.ts +2 -0
- package/dist/engine/claude-code.js +54 -12
- package/dist/engine/registry.d.ts +16 -0
- package/dist/engine/registry.js +32 -0
- package/dist/format.d.ts +7 -9
- package/dist/format.js +71 -82
- package/dist/index.js +34 -5
- package/dist/orchestrator.d.ts +3 -4
- package/dist/orchestrator.js +99 -41
- package/dist/prompt.d.ts +6 -0
- package/dist/prompt.js +35 -0
- package/dist/tool-handlers.d.ts +1 -1
- package/dist/tool-handlers.js +2 -2
- package/dist/types.d.ts +51 -12
- package/dist/types.js +12 -1
- package/package.json +4 -2
- package/prompts/ONBOARDING.md +56 -0
- package/prompts/SYSTEM.md +164 -0
package/dist/orchestrator.js
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
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 ===
|
|
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
|
|
144
|
+
const assembledPrompt = assemblePrompt(this.config.frameworkPromptDir, this.config.instructionsDir);
|
|
135
145
|
const appendSystemPrompt = task
|
|
136
|
-
? task.prompt
|
|
137
|
-
:
|
|
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
|
|
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.
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
385
|
-
|
|
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 (
|
|
460
|
+
if (tool.toolName === "EnterPlanMode") {
|
|
402
461
|
await this.channel.sendMessage(chatId, "📋 Planning");
|
|
403
462
|
break;
|
|
404
463
|
}
|
|
405
|
-
if (displayHandledTools.has(
|
|
464
|
+
if (displayHandledTools.has(tool.toolName))
|
|
406
465
|
break;
|
|
407
|
-
const line = formatToolStatusLine(
|
|
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(
|
|
580
|
+
async handlePermission(tool, chatId) {
|
|
516
581
|
// Always auto-allow ClearClaw's own MCP tools
|
|
517
|
-
if (
|
|
518
|
-
log.info(`[perm] ${
|
|
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] ${
|
|
522
|
-
// Custom tool handler
|
|
523
|
-
const handler = permissionHandlers.get(
|
|
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(
|
|
590
|
+
const result = handler(tool);
|
|
526
591
|
if (result === null) {
|
|
527
|
-
log.info(`[perm] ${
|
|
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] ${
|
|
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",
|
|
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(
|
|
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] ${
|
|
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",
|
|
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
|
-
|
|
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) {
|
package/dist/prompt.d.ts
ADDED
|
@@ -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
|
package/dist/tool-handlers.d.ts
CHANGED
|
@@ -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
|
|
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>;
|
package/dist/tool-handlers.js
CHANGED
|
@@ -34,8 +34,8 @@ function handleAskUserQuestion(input) {
|
|
|
34
34
|
},
|
|
35
35
|
};
|
|
36
36
|
}
|
|
37
|
-
function handleExitPlanMode(input
|
|
38
|
-
const text = formatExitPlanMode(input
|
|
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?:
|
|
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
|
|
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:
|
|
63
|
+
onPermissionRequest: (req: ToolCall) => Promise<PermissionResponse>;
|
|
61
64
|
appendSystemPrompt?: string;
|
|
62
65
|
mcpServers?: Record<string, McpServerConfig>;
|
|
63
66
|
signal?: AbortSignal;
|
|
64
67
|
}
|
|
65
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|