clearclaw 0.6.1 → 0.8.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/README.md +53 -7
- package/dist/channel/slack.d.ts +21 -4
- package/dist/channel/slack.js +270 -44
- package/dist/channel/telegram.d.ts +22 -3
- package/dist/channel/telegram.js +159 -35
- package/dist/config.d.ts +49 -5
- package/dist/config.js +159 -33
- package/dist/engine/acp.d.ts +14 -0
- package/dist/engine/acp.js +519 -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 +20 -1
- package/dist/engine/claude-code.js +174 -44
- package/dist/engine/mcp-http.d.ts +9 -0
- package/dist/engine/mcp-http.js +59 -0
- package/dist/engine/registry.d.ts +19 -0
- package/dist/engine/registry.js +115 -0
- package/dist/engine/session-transcript.d.ts +14 -0
- package/dist/engine/session-transcript.js +38 -0
- package/dist/format.d.ts +8 -10
- package/dist/format.js +106 -82
- package/dist/index.js +33 -9
- package/dist/orchestrator.d.ts +34 -8
- package/dist/orchestrator.js +866 -153
- package/dist/plan-usage.d.ts +6 -0
- package/dist/plan-usage.js +31 -0
- package/dist/prompt.d.ts +10 -0
- package/dist/prompt.js +78 -0
- package/dist/scheduler.d.ts +15 -0
- package/dist/scheduler.js +82 -0
- package/dist/tool-handlers.d.ts +1 -1
- package/dist/tool-handlers.js +2 -2
- package/dist/types.d.ts +128 -17
- package/dist/types.js +12 -1
- package/package.json +13 -4
- package/patches/README.md +13 -0
- package/patches/codex-acp-1.11.0.patch +38 -0
- package/prompts/SYSTEM.md +214 -0
- package/scripts/patch-codex-acp.mjs +32 -0
package/dist/format.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Formatting utilities for Telegram display.
|
|
3
3
|
*
|
|
4
|
-
* Uses the `diff` library for
|
|
4
|
+
* Uses the `diff` library for interleaved diffs in Edit permission prompts.
|
|
5
5
|
*/
|
|
6
|
-
import {
|
|
6
|
+
import { diffLines } from "diff";
|
|
7
|
+
import { isKnownToolCall } from "./types.js";
|
|
7
8
|
const MAX_LINES = 200;
|
|
8
9
|
const MAX_STATUS_LEN = 60;
|
|
9
10
|
/**
|
|
@@ -24,82 +25,82 @@ export function formatToolCallSummary(toolCalls) {
|
|
|
24
25
|
* Short one-liner for the rolling tool message. Includes key detail
|
|
25
26
|
* but truncated to stay compact.
|
|
26
27
|
*/
|
|
27
|
-
export function formatToolStatusLine(
|
|
28
|
-
const full = `🔧 ${formatToolStatus(
|
|
28
|
+
export function formatToolStatusLine(tool) {
|
|
29
|
+
const full = `🔧 ${formatToolStatus(tool)}`;
|
|
29
30
|
if (full.length <= MAX_STATUS_LEN)
|
|
30
31
|
return full;
|
|
31
32
|
return `${full.slice(0, MAX_STATUS_LEN - 1)}…`;
|
|
32
33
|
}
|
|
33
34
|
/**
|
|
34
|
-
* Format a permission prompt for display.
|
|
35
|
-
*
|
|
35
|
+
* Format a permission prompt for display.
|
|
36
|
+
* Switches on action type: edit shows diff, write shows preview,
|
|
37
|
+
* execute shows command, others show available detail.
|
|
36
38
|
*/
|
|
37
|
-
export function formatPermissionPrompt(
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
const start = lines.findIndex((l) => l.startsWith("---"));
|
|
43
|
-
const diffLines = start >= 0 ? lines.slice(start) : lines;
|
|
44
|
-
const body = truncateLines(diffLines, MAX_LINES);
|
|
45
|
-
return `🔐 Allow Edit? ${edit.file_path}\n\`\`\`diff\n${body}\n\`\`\``;
|
|
46
|
-
}
|
|
47
|
-
if (toolName === "Write") {
|
|
48
|
-
const write = input;
|
|
49
|
-
const lines = write.content.split("\n");
|
|
50
|
-
const body = truncateLines(lines, MAX_LINES);
|
|
51
|
-
return `🔐 Allow Write? ${write.file_path} (${lines.length} lines)\n\`\`\`\n${body}\n\`\`\``;
|
|
52
|
-
}
|
|
53
|
-
// Non-Edit/Write tools: emoji header + detail in code block
|
|
54
|
-
const detail = formatToolDetail(toolName, input);
|
|
55
|
-
const body = truncateLines(detail.split("\n"), MAX_LINES);
|
|
56
|
-
return `🔐 Allow ${toolName}?\n\`\`\`\n${body}\n\`\`\``;
|
|
39
|
+
export function formatPermissionPrompt(tool) {
|
|
40
|
+
const detail = formatToolDetail(tool);
|
|
41
|
+
if (!detail)
|
|
42
|
+
return `🔐 Allow ${tool.toolName}?`;
|
|
43
|
+
return `🔐 Allow ${tool.toolName}?\n${detail}`;
|
|
57
44
|
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
return
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
return
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
45
|
+
/** Multi-line detail for permission prompts (diff, content preview, command, etc.). */
|
|
46
|
+
function formatToolDetail(tool) {
|
|
47
|
+
if (!isKnownToolCall(tool))
|
|
48
|
+
return formatUnknownDetail(tool);
|
|
49
|
+
switch (tool.action) {
|
|
50
|
+
case "edit": {
|
|
51
|
+
if (!tool.before && !tool.after)
|
|
52
|
+
return tool.path;
|
|
53
|
+
const lines = formatInterleavedDiff(tool.before, tool.after);
|
|
54
|
+
return `${tool.path}\n\`\`\`\n${truncateLines(lines, MAX_LINES)}\n\`\`\``;
|
|
55
|
+
}
|
|
56
|
+
case "write": {
|
|
57
|
+
const lines = tool.content.split("\n");
|
|
58
|
+
return `${tool.path} (${lines.length} lines)\n\`\`\`\n${truncateLines(lines, MAX_LINES)}\n\`\`\``;
|
|
59
|
+
}
|
|
60
|
+
case "execute":
|
|
61
|
+
return `\`\`\`\n${truncateLines(tool.command.split("\n"), MAX_LINES)}\n\`\`\``;
|
|
62
|
+
case "read":
|
|
63
|
+
return `\`\`\`\n${tool.paths.join("\n")}\n\`\`\``;
|
|
64
|
+
case "search":
|
|
65
|
+
return `\`\`\`\n${tool.pattern}\n\`\`\``;
|
|
66
|
+
case "fetch":
|
|
67
|
+
return `\`\`\`\n${tool.url}\n\`\`\``;
|
|
80
68
|
}
|
|
81
69
|
}
|
|
82
|
-
/**
|
|
83
|
-
function
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
case "Read":
|
|
88
|
-
case "Edit":
|
|
89
|
-
case "Write":
|
|
90
|
-
return String(input.file_path ?? "");
|
|
91
|
-
case "Glob":
|
|
92
|
-
case "Grep":
|
|
93
|
-
return String(input.pattern ?? "");
|
|
94
|
-
case "WebSearch":
|
|
95
|
-
return String(input.query ?? "");
|
|
96
|
-
case "WebFetch":
|
|
97
|
-
return String(input.url ?? "");
|
|
98
|
-
case "Agent":
|
|
99
|
-
return String(input.description ?? "");
|
|
100
|
-
default:
|
|
101
|
-
return JSON.stringify(input, null, 2);
|
|
70
|
+
/** One-liner summary for tool status display. */
|
|
71
|
+
function formatToolStatus(tool) {
|
|
72
|
+
if (!isKnownToolCall(tool)) {
|
|
73
|
+
const summary = formatUnknownSummary(tool);
|
|
74
|
+
return summary ? `${tool.toolName}: ${summary}` : tool.toolName;
|
|
102
75
|
}
|
|
76
|
+
switch (tool.action) {
|
|
77
|
+
case "edit":
|
|
78
|
+
case "write":
|
|
79
|
+
return `${tool.toolName}: ${tool.path}`;
|
|
80
|
+
case "execute":
|
|
81
|
+
return `${tool.toolName}: ${tool.command}`;
|
|
82
|
+
case "read":
|
|
83
|
+
return `${tool.toolName}: ${tool.paths.join(", ")}`;
|
|
84
|
+
case "search":
|
|
85
|
+
return `${tool.toolName}: ${tool.pattern}`;
|
|
86
|
+
case "fetch":
|
|
87
|
+
return `${tool.toolName}: ${tool.url}`;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
/** Extract displayable info from an unknown tool call's generic fields. */
|
|
91
|
+
function formatUnknownSummary(tool) {
|
|
92
|
+
const parts = [];
|
|
93
|
+
if ("paths" in tool && Array.isArray(tool.paths))
|
|
94
|
+
parts.push(...tool.paths);
|
|
95
|
+
if ("detail" in tool && tool.detail)
|
|
96
|
+
parts.push(String(tool.detail));
|
|
97
|
+
return parts.length > 0 ? parts.join(", ") : null;
|
|
98
|
+
}
|
|
99
|
+
function formatUnknownDetail(tool) {
|
|
100
|
+
const summary = formatUnknownSummary(tool);
|
|
101
|
+
if (!summary)
|
|
102
|
+
return null;
|
|
103
|
+
return `\`\`\`\n${truncateLines(summary.split("\n"), MAX_LINES)}\n\`\`\``;
|
|
103
104
|
}
|
|
104
105
|
/**
|
|
105
106
|
* Relative time string: "just now", "5m ago", "2h ago", "3d ago".
|
|
@@ -145,7 +146,7 @@ export function truncateButtonLabel(index, label) {
|
|
|
145
146
|
* Format an ExitPlanMode permission prompt.
|
|
146
147
|
* Renders plan text from the description or input fields.
|
|
147
148
|
*/
|
|
148
|
-
export function formatExitPlanMode(input
|
|
149
|
+
export function formatExitPlanMode(input) {
|
|
149
150
|
const plan = (typeof input.plan === "string" ? input.plan : null)
|
|
150
151
|
|| JSON.stringify(input, null, 2);
|
|
151
152
|
const lines = plan.split("\n");
|
|
@@ -176,19 +177,42 @@ function truncateLines(lines, max) {
|
|
|
176
177
|
const remaining = lines.length - max;
|
|
177
178
|
return `${shown}\n... (${remaining} more lines)`;
|
|
178
179
|
}
|
|
179
|
-
/**
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
180
|
+
/** Build an interleaved diff with clear separator between markers and content. */
|
|
181
|
+
function formatInterleavedDiff(before, after) {
|
|
182
|
+
const changes = diffLines(before, after);
|
|
183
|
+
const lines = [];
|
|
184
|
+
const splitValue = (value) => value.replace(/\n$/, "").split("\n");
|
|
185
|
+
for (let i = 0; i < changes.length; i++) {
|
|
186
|
+
const c = changes[i];
|
|
187
|
+
if (c.removed) {
|
|
188
|
+
const next = changes[i + 1];
|
|
189
|
+
if (next?.added) {
|
|
190
|
+
// Interleave paired removals and additions line by line
|
|
191
|
+
const remLines = splitValue(c.value);
|
|
192
|
+
const addLines = splitValue(next.value);
|
|
193
|
+
const max = Math.max(remLines.length, addLines.length);
|
|
194
|
+
for (let j = 0; j < max; j++) {
|
|
195
|
+
if (j < remLines.length)
|
|
196
|
+
lines.push(`- │ ${remLines[j]}`);
|
|
197
|
+
if (j < addLines.length)
|
|
198
|
+
lines.push(`+ │ ${addLines[j]}`);
|
|
199
|
+
}
|
|
200
|
+
i++; // skip the paired added chunk
|
|
201
|
+
}
|
|
202
|
+
else {
|
|
203
|
+
for (const l of splitValue(c.value))
|
|
204
|
+
lines.push(`- │ ${l}`);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
else if (c.added) {
|
|
208
|
+
for (const l of splitValue(c.value))
|
|
209
|
+
lines.push(`+ │ ${l}`);
|
|
210
|
+
}
|
|
211
|
+
else {
|
|
212
|
+
for (const l of splitValue(c.value))
|
|
213
|
+
lines.push(`· │ ${l}`);
|
|
214
|
+
}
|
|
192
215
|
}
|
|
216
|
+
return lines;
|
|
193
217
|
}
|
|
194
218
|
//# sourceMappingURL=format.js.map
|
package/dist/index.js
CHANGED
|
@@ -2,13 +2,12 @@
|
|
|
2
2
|
import readline from "node:readline";
|
|
3
3
|
import { Config } from "./config.js";
|
|
4
4
|
import log, { initLogger } from "./logger.js";
|
|
5
|
-
import {
|
|
5
|
+
import { createEngineMap, ENGINE_NAMES, resolveEnginePath } from "./engine/registry.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
9
|
async function runDaemon() {
|
|
10
10
|
const config = new Config().resolve();
|
|
11
|
-
config.syncSkills();
|
|
12
11
|
initLogger(config.logPath);
|
|
13
12
|
// The Claude Agent SDK has a bug: handleControlRequest() writes to the
|
|
14
13
|
// subprocess stdin without a .catch(), so aborting during a permission
|
|
@@ -41,8 +40,8 @@ async function runDaemon() {
|
|
|
41
40
|
log.warn({ err }, "[pairing] failed to send pairing message");
|
|
42
41
|
});
|
|
43
42
|
};
|
|
44
|
-
const channel = createChannel(config.channel, (userId) => config.isAuthorized(userId), onUnauthorizedDM);
|
|
45
|
-
await new Orchestrator({ channel,
|
|
43
|
+
const channel = createChannel(config.channel, (userId) => config.isAuthorized(userId), onUnauthorizedDM, () => config.listAuthorizedUserIds());
|
|
44
|
+
await new Orchestrator({ channel, engines: createEngineMap(config.engines), config }).start();
|
|
46
45
|
}
|
|
47
46
|
async function runApprove(args) {
|
|
48
47
|
const code = args[0];
|
|
@@ -56,7 +55,9 @@ async function runApprove(args) {
|
|
|
56
55
|
console.error("Invalid or expired pairing code.");
|
|
57
56
|
process.exit(1);
|
|
58
57
|
}
|
|
59
|
-
config.approveUser(pairing.userId, pairing.userName
|
|
58
|
+
config.approveUser(pairing.userId, pairing.userName);
|
|
59
|
+
// Pairings only originate from a root DM, so this chat is home's destination.
|
|
60
|
+
config.connectHomeWorkspace(pairing.chatId);
|
|
60
61
|
console.log(`Approved ${pairing.userName} (${pairing.userId})`);
|
|
61
62
|
}
|
|
62
63
|
const SETUP_TIMEOUT_MS = 5 * 60 * 1000;
|
|
@@ -87,15 +88,36 @@ async function runSetup() {
|
|
|
87
88
|
throw new Error("Bot token required.");
|
|
88
89
|
channelConfig = { type: "telegram", botToken };
|
|
89
90
|
}
|
|
91
|
+
let engineName;
|
|
92
|
+
let resolvedPath;
|
|
93
|
+
const engineList = ENGINE_NAMES.join("/");
|
|
94
|
+
while (true) {
|
|
95
|
+
engineName = (await ask(`Default engine [${engineList}]: `)).trim().toLowerCase();
|
|
96
|
+
if (!ENGINE_NAMES.includes(engineName)) {
|
|
97
|
+
console.log(` Please enter one of: ${engineList}`);
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
try {
|
|
101
|
+
resolvedPath = resolveEnginePath(engineName);
|
|
102
|
+
break;
|
|
103
|
+
}
|
|
104
|
+
catch (err) {
|
|
105
|
+
console.log(` ${err.message}`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
90
108
|
const config = new Config();
|
|
91
109
|
config.setChannel(channelConfig);
|
|
110
|
+
config.setEngines([
|
|
111
|
+
{ name: engineName, default: true, path: resolvedPath },
|
|
112
|
+
]);
|
|
92
113
|
console.log("Saved to ~/.clearclaw/config.json\n");
|
|
93
114
|
config.resolve();
|
|
115
|
+
config.ensureHomeWorkspace();
|
|
94
116
|
let resolveFirstDM;
|
|
95
117
|
const firstDM = new Promise((r) => {
|
|
96
118
|
resolveFirstDM = r;
|
|
97
119
|
});
|
|
98
|
-
channel = createChannel(config.channel, (userId) => config.isAuthorized(userId), (chatId, user) => resolveFirstDM({ chatId, user }));
|
|
120
|
+
channel = createChannel(config.channel, (userId) => config.isAuthorized(userId), (chatId, user) => resolveFirstDM({ chatId, user }), () => config.listAuthorizedUserIds());
|
|
99
121
|
console.log("Connecting...");
|
|
100
122
|
await channel.connect();
|
|
101
123
|
console.log("Connected! Waiting for a DM — message the bot from your chat app.\n");
|
|
@@ -118,7 +140,9 @@ async function runSetup() {
|
|
|
118
140
|
await channel.sendMessage(chatId, "Pairing code was invalid. Please try again.");
|
|
119
141
|
return;
|
|
120
142
|
}
|
|
121
|
-
config.approveUser(pairing.userId, pairing.userName
|
|
143
|
+
config.approveUser(pairing.userId, pairing.userName);
|
|
144
|
+
// Pairings only originate from a root DM, so this chat is home's destination.
|
|
145
|
+
config.connectHomeWorkspace(pairing.chatId);
|
|
122
146
|
console.log(` Approved ${pairing.userName} (${pairing.userId})`);
|
|
123
147
|
await channel.sendMessage(chatId, "You're approved! The bot is being set up — you'll be able to chat soon.");
|
|
124
148
|
console.log("\nSetup complete! Start the daemon:\n clearclaw daemon\n");
|
|
@@ -129,9 +153,9 @@ async function runSetup() {
|
|
|
129
153
|
rl.close();
|
|
130
154
|
}
|
|
131
155
|
}
|
|
132
|
-
function createChannel(ch, isAuthorized, onUnauthorizedDM) {
|
|
156
|
+
function createChannel(ch, isAuthorized, onUnauthorizedDM, authorizedUserIds) {
|
|
133
157
|
if (ch.type === "slack") {
|
|
134
|
-
return new SlackChannel(ch.botToken, ch.appToken, isAuthorized, onUnauthorizedDM);
|
|
158
|
+
return new SlackChannel(ch.botToken, ch.appToken, isAuthorized, onUnauthorizedDM, authorizedUserIds);
|
|
135
159
|
}
|
|
136
160
|
return new TelegramChannel(ch.botToken, isAuthorized, onUnauthorizedDM);
|
|
137
161
|
}
|
package/dist/orchestrator.d.ts
CHANGED
|
@@ -1,32 +1,58 @@
|
|
|
1
1
|
import type { Config } from "./config.js";
|
|
2
|
-
import type { Channel, Engine } from "./types.js";
|
|
2
|
+
import type { Channel, Engine, InboundMessage, MessageOrigin } from "./types.js";
|
|
3
3
|
export interface OrchestratorOpts {
|
|
4
4
|
channel: Channel;
|
|
5
|
-
|
|
5
|
+
engines: Map<string, Engine>;
|
|
6
6
|
config: Config;
|
|
7
7
|
}
|
|
8
8
|
export declare class Orchestrator {
|
|
9
9
|
private channel;
|
|
10
|
-
private
|
|
10
|
+
private engines;
|
|
11
11
|
private config;
|
|
12
12
|
private chats;
|
|
13
|
-
private
|
|
13
|
+
private creatingWorkspaces;
|
|
14
|
+
private scheduler;
|
|
15
|
+
private planUsage;
|
|
14
16
|
constructor(opts: OrchestratorOpts);
|
|
15
17
|
private chat;
|
|
18
|
+
private engineFor;
|
|
16
19
|
start(): Promise<void>;
|
|
17
20
|
stop(): Promise<void>;
|
|
18
|
-
/**
|
|
21
|
+
/** Deliver a synthetic message to a named workspace and trigger its turn. */
|
|
22
|
+
deliverToWorkspace(workspaceName: string, origin: MessageOrigin, text: string): boolean;
|
|
23
|
+
/**
|
|
24
|
+
* Everything creation can reject without touching the platform. Runs before the
|
|
25
|
+
* confirmation, so the user is never asked to approve a doomed spawn, and again
|
|
26
|
+
* inside creation, where it is the authoritative check.
|
|
27
|
+
*/
|
|
28
|
+
private validateNewWorkspace;
|
|
29
|
+
/** Shared creation path for standalone projects and peers, with optional manual binding. */
|
|
30
|
+
private createWorkspace;
|
|
31
|
+
/** Grouping is best-effort: it never fails a lifecycle operation, but it is never silent either. */
|
|
32
|
+
private groupChat;
|
|
33
|
+
private deliverInitialBrief;
|
|
34
|
+
/** An unbound chat can connect without starting a temporary agent or sharing home's session. */
|
|
35
|
+
private connectWorkspace;
|
|
36
|
+
private peerRuntime;
|
|
37
|
+
/** Effective behavior: Explicit setting or home→assistant / project→relay. */
|
|
19
38
|
private effectiveBehavior;
|
|
20
|
-
/**
|
|
39
|
+
/** Permission mode a chat runs in until /mode picks one. */
|
|
40
|
+
private baseMode;
|
|
41
|
+
/** Enqueue a message and drain — immediately for relay, debounced for assistant. */
|
|
21
42
|
private enqueueMessage;
|
|
22
43
|
private scheduleDebounce;
|
|
23
44
|
private processQueuedMessages;
|
|
24
45
|
private executeTurn;
|
|
46
|
+
private persistSessionId;
|
|
47
|
+
/** Native selection works even when the workspace's engine cannot run. */
|
|
48
|
+
private selectEngine;
|
|
25
49
|
private routeMessage;
|
|
26
50
|
private routeEngineEvent;
|
|
51
|
+
private routeTextChunk;
|
|
52
|
+
private publishTextSegment;
|
|
53
|
+
private flushTextSegment;
|
|
27
54
|
private buildMcpTools;
|
|
28
55
|
private handlePermission;
|
|
29
56
|
private updateStatusMessage;
|
|
30
|
-
/** Read default workspace CLAUDE.md to append to non-default workspace sessions. */
|
|
31
|
-
private readDefaultPrompt;
|
|
32
57
|
}
|
|
58
|
+
export declare function buildPrompt(messages: InboundMessage[]): string;
|