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/config.js
CHANGED
|
@@ -28,19 +28,25 @@ export function resolveDataDir() {
|
|
|
28
28
|
export class Config {
|
|
29
29
|
// Resolved settings — populated by resolve()
|
|
30
30
|
channel;
|
|
31
|
+
engines;
|
|
32
|
+
defaultEngine; // derived: name of the engine with default: true
|
|
31
33
|
permissionMode = "default";
|
|
32
34
|
envUserIds = new Set();
|
|
33
35
|
// Derived paths
|
|
34
|
-
|
|
36
|
+
homeWorkspacePath;
|
|
35
37
|
filesPath;
|
|
36
38
|
logPath;
|
|
39
|
+
instructionsDir;
|
|
40
|
+
frameworkPromptDir;
|
|
37
41
|
dataDir = resolveDataDir();
|
|
38
42
|
filePath;
|
|
39
43
|
constructor() {
|
|
40
44
|
this.filePath = path.join(this.dataDir, "config.json");
|
|
41
|
-
this.
|
|
45
|
+
this.homeWorkspacePath = path.join(this.dataDir, "workspace");
|
|
42
46
|
this.filesPath = path.join(this.dataDir, "files");
|
|
43
47
|
this.logPath = path.join(this.dataDir, "clearclaw.log");
|
|
48
|
+
this.instructionsDir = path.join(this.dataDir, "workspace", "instructions");
|
|
49
|
+
this.frameworkPromptDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "prompts");
|
|
44
50
|
}
|
|
45
51
|
/** Resolve channel + env vars. Throws if no channel config found. */
|
|
46
52
|
resolve() {
|
|
@@ -70,26 +76,42 @@ export class Config {
|
|
|
70
76
|
throw new Error(`PERMISSION_MODE must be one of: ${valid.join(", ")}`);
|
|
71
77
|
}
|
|
72
78
|
this.permissionMode = pm;
|
|
79
|
+
const data = this.read();
|
|
80
|
+
if (!data.engines?.length) {
|
|
81
|
+
throw new Error("No engines configured. Run `clearclaw setup`.");
|
|
82
|
+
}
|
|
83
|
+
this.engines = Object.fromEntries(data.engines.map((e) => [e.name, e]));
|
|
84
|
+
const defaultEntry = data.engines.find((e) => e.default);
|
|
85
|
+
if (!defaultEntry) {
|
|
86
|
+
throw new Error("No default engine configured. Run `clearclaw setup`.");
|
|
87
|
+
}
|
|
88
|
+
this.defaultEngine = defaultEntry.name;
|
|
73
89
|
return this;
|
|
74
90
|
}
|
|
75
91
|
// --- File I/O (internal) ---
|
|
76
|
-
|
|
77
|
-
if (!fs.existsSync(this.filePath))
|
|
78
|
-
return {
|
|
79
|
-
}
|
|
92
|
+
readRaw() {
|
|
93
|
+
if (!fs.existsSync(this.filePath))
|
|
94
|
+
return {};
|
|
80
95
|
try {
|
|
81
|
-
|
|
82
|
-
return {
|
|
83
|
-
channel: raw.channel,
|
|
84
|
-
authorizedUsers: raw.authorizedUsers ?? [],
|
|
85
|
-
pendingPairings: raw.pendingPairings ?? [],
|
|
86
|
-
workspaces: raw.workspaces ?? [],
|
|
87
|
-
};
|
|
96
|
+
return JSON.parse(fs.readFileSync(this.filePath, "utf-8"));
|
|
88
97
|
}
|
|
89
98
|
catch {
|
|
90
|
-
return {
|
|
99
|
+
return {};
|
|
91
100
|
}
|
|
92
101
|
}
|
|
102
|
+
read() {
|
|
103
|
+
const raw = this.readRaw();
|
|
104
|
+
return {
|
|
105
|
+
...raw,
|
|
106
|
+
channel: raw.channel,
|
|
107
|
+
engines: raw.engines,
|
|
108
|
+
authorizedUsers: (raw.authorizedUsers ?? []),
|
|
109
|
+
pendingPairings: (raw.pendingPairings ?? []),
|
|
110
|
+
workspaces: (raw.workspaces ?? []),
|
|
111
|
+
schedules: (raw.schedules ?? []),
|
|
112
|
+
projects: (raw.projects ?? []),
|
|
113
|
+
};
|
|
114
|
+
}
|
|
93
115
|
write(data) {
|
|
94
116
|
fs.writeFileSync(this.filePath, JSON.stringify(data, null, 2) + "\n", { mode: 0o600 });
|
|
95
117
|
}
|
|
@@ -102,6 +124,12 @@ export class Config {
|
|
|
102
124
|
data.channel = config;
|
|
103
125
|
this.write(data);
|
|
104
126
|
}
|
|
127
|
+
// --- Engines ---
|
|
128
|
+
setEngines(engines) {
|
|
129
|
+
const data = this.read();
|
|
130
|
+
data.engines = engines;
|
|
131
|
+
this.write(data);
|
|
132
|
+
}
|
|
105
133
|
// --- Authorized users ---
|
|
106
134
|
hasUser(userId) {
|
|
107
135
|
return this.read().authorizedUsers.some((u) => u.id === userId);
|
|
@@ -116,6 +144,13 @@ export class Config {
|
|
|
116
144
|
listUsers() {
|
|
117
145
|
return this.read().authorizedUsers;
|
|
118
146
|
}
|
|
147
|
+
/** All authorized IDs, including environment-configured and pairing-approved users. */
|
|
148
|
+
listAuthorizedUserIds() {
|
|
149
|
+
return [...new Set([
|
|
150
|
+
...this.envUserIds,
|
|
151
|
+
...this.listUsers().map((user) => user.id),
|
|
152
|
+
])];
|
|
153
|
+
}
|
|
119
154
|
/** env var IDs (in-memory) + pairing-approved users (hot-reload from disk) */
|
|
120
155
|
isAuthorized(userId) {
|
|
121
156
|
return this.envUserIds.has(userId) || this.hasUser(userId);
|
|
@@ -159,12 +194,59 @@ export class Config {
|
|
|
159
194
|
.join("");
|
|
160
195
|
}
|
|
161
196
|
// --- Workspaces ---
|
|
197
|
+
/** Home exists before a chat is paired. Never replace an existing home's settings. */
|
|
198
|
+
ensureHomeWorkspace() {
|
|
199
|
+
fs.mkdirSync(this.homeWorkspacePath, { recursive: true });
|
|
200
|
+
const data = this.read();
|
|
201
|
+
let home = data.workspaces.find((ws) => ws.name === "default");
|
|
202
|
+
let changed = false;
|
|
203
|
+
if (!home) {
|
|
204
|
+
home = { name: "default", cwd: this.homeWorkspacePath, chat_id: null,
|
|
205
|
+
current_session_id: null, behavior: "assistant", description: "Personal assistant", project: "home" };
|
|
206
|
+
// Avoid adopting an unrelated project with a reserved home name.
|
|
207
|
+
if (data.projects.some((project) => project.name === "home" && project.main_workspace !== "default")) {
|
|
208
|
+
throw new Error('Project "home" already has another main workspace; rename it before creating home.');
|
|
209
|
+
}
|
|
210
|
+
data.workspaces.push(home);
|
|
211
|
+
changed = true;
|
|
212
|
+
}
|
|
213
|
+
if (!home.project) {
|
|
214
|
+
const existing = data.projects.find((project) => project.main_workspace === home.name);
|
|
215
|
+
home.project = existing?.name ?? "home";
|
|
216
|
+
if (data.projects.some((project) => project.name === home.project && project.main_workspace !== home.name)) {
|
|
217
|
+
throw new Error(`Project "${home.project}" already has another main workspace.`);
|
|
218
|
+
}
|
|
219
|
+
changed = true;
|
|
220
|
+
}
|
|
221
|
+
if (!data.projects.some((project) => project.name === home.project)) {
|
|
222
|
+
data.projects.push({ name: home.project, description: home.description ?? "Personal assistant", main_workspace: home.name });
|
|
223
|
+
changed = true;
|
|
224
|
+
}
|
|
225
|
+
if (changed)
|
|
226
|
+
this.write(data);
|
|
227
|
+
return home;
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* ensureHomeWorkspace creates home's records at startup, before any chat is known.
|
|
231
|
+
* This gives it its destination, once: an existing binding is never replaced.
|
|
232
|
+
* Callers decide what counts as home's chat — see Channel.isRootDM.
|
|
233
|
+
*/
|
|
234
|
+
connectHomeWorkspace(chatId) {
|
|
235
|
+
const home = this.ensureHomeWorkspace();
|
|
236
|
+
if (home.chat_id || this.workspaceByChat(chatId))
|
|
237
|
+
return false;
|
|
238
|
+
this.upsertWorkspace({ ...home, chat_id: chatId });
|
|
239
|
+
return true;
|
|
240
|
+
}
|
|
162
241
|
workspaceByChat(chatId) {
|
|
163
242
|
return this.read().workspaces.find((ws) => ws.chat_id === chatId);
|
|
164
243
|
}
|
|
165
244
|
workspaceByName(name) {
|
|
166
245
|
return this.read().workspaces.find((ws) => ws.name === name);
|
|
167
246
|
}
|
|
247
|
+
listWorkspaces() {
|
|
248
|
+
return this.read().workspaces;
|
|
249
|
+
}
|
|
168
250
|
upsertWorkspace(ws) {
|
|
169
251
|
const data = this.read();
|
|
170
252
|
const idx = data.workspaces.findIndex((w) => w.name === ws.name);
|
|
@@ -174,6 +256,16 @@ export class Config {
|
|
|
174
256
|
data.workspaces.push(ws);
|
|
175
257
|
this.write(data);
|
|
176
258
|
}
|
|
259
|
+
removeWorkspace(name) {
|
|
260
|
+
const data = this.read();
|
|
261
|
+
const idx = data.workspaces.findIndex((w) => w.name === name);
|
|
262
|
+
if (idx < 0)
|
|
263
|
+
return undefined;
|
|
264
|
+
const [removed] = data.workspaces.splice(idx, 1);
|
|
265
|
+
this.write(data);
|
|
266
|
+
return removed;
|
|
267
|
+
}
|
|
268
|
+
/** Persist the session ID without changing the saved model setting. */
|
|
177
269
|
setSession(name, sessionId) {
|
|
178
270
|
const data = this.read();
|
|
179
271
|
const ws = data.workspaces.find((w) => w.name === name);
|
|
@@ -187,6 +279,15 @@ export class Config {
|
|
|
187
279
|
const ws = data.workspaces.find((w) => w.name === name);
|
|
188
280
|
if (ws) {
|
|
189
281
|
ws.current_session_id = null;
|
|
282
|
+
delete ws.engine_handoff;
|
|
283
|
+
this.write(data);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
setModel(name, model) {
|
|
287
|
+
const data = this.read();
|
|
288
|
+
const ws = data.workspaces.find((w) => w.name === name);
|
|
289
|
+
if (ws) {
|
|
290
|
+
ws.model = model;
|
|
190
291
|
this.write(data);
|
|
191
292
|
}
|
|
192
293
|
}
|
|
@@ -198,27 +299,52 @@ export class Config {
|
|
|
198
299
|
this.write(data);
|
|
199
300
|
}
|
|
200
301
|
}
|
|
201
|
-
// ---
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
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 });
|
|
302
|
+
// --- Projects ---
|
|
303
|
+
addProject(project) {
|
|
304
|
+
const data = this.read();
|
|
305
|
+
const idx = data.projects.findIndex((p) => p.name === project.name);
|
|
306
|
+
if (idx >= 0)
|
|
307
|
+
data.projects[idx] = project;
|
|
308
|
+
else
|
|
309
|
+
data.projects.push(project);
|
|
310
|
+
this.write(data);
|
|
311
|
+
}
|
|
312
|
+
listProjects() {
|
|
313
|
+
return this.read().projects;
|
|
218
314
|
}
|
|
219
|
-
|
|
315
|
+
projectByName(name) {
|
|
316
|
+
return this.read().projects.find((p) => p.name === name);
|
|
317
|
+
}
|
|
318
|
+
removeProject(name) {
|
|
319
|
+
const data = this.read();
|
|
320
|
+
data.projects = data.projects.filter((p) => p.name !== name);
|
|
321
|
+
this.write(data);
|
|
322
|
+
}
|
|
323
|
+
// --- Schedules ---
|
|
324
|
+
listSchedules() {
|
|
325
|
+
return this.read().schedules;
|
|
326
|
+
}
|
|
327
|
+
addSchedule(entry) {
|
|
328
|
+
const data = this.read();
|
|
329
|
+
data.schedules.push(entry);
|
|
330
|
+
this.write(data);
|
|
331
|
+
}
|
|
332
|
+
removeSchedule(id) {
|
|
333
|
+
const data = this.read();
|
|
334
|
+
data.schedules = data.schedules.filter((s) => s.id !== id);
|
|
335
|
+
this.write(data);
|
|
336
|
+
}
|
|
337
|
+
updateSchedule(id, update) {
|
|
338
|
+
const data = this.read();
|
|
339
|
+
const entry = data.schedules.find((s) => s.id === id);
|
|
340
|
+
if (entry) {
|
|
341
|
+
Object.assign(entry, update);
|
|
342
|
+
this.write(data);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
// --- Approval ---
|
|
346
|
+
approveUser(userId, userName) {
|
|
220
347
|
this.addUser({ id: userId, name: userName, approvedAt: Date.now() });
|
|
221
|
-
this.ensureDefaultWorkspace(chatId);
|
|
222
348
|
}
|
|
223
349
|
}
|
|
224
350
|
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { AcpEngineDefinition } from "./registry.js";
|
|
2
|
+
import type { Engine, EngineEvent, PlanUsageWindow, RunTurnOpts, SessionInfo, SessionHistoryOpts, SessionMessage } from "../types.js";
|
|
3
|
+
export declare class AcpEngine implements Engine {
|
|
4
|
+
readonly name: string;
|
|
5
|
+
private readonly spawnConfig;
|
|
6
|
+
constructor(name: string, spawnConfig: AcpEngineDefinition);
|
|
7
|
+
runTurn(opts: RunTurnOpts): AsyncIterable<EngineEvent>;
|
|
8
|
+
getSessionMessages(opts: SessionHistoryOpts): Promise<SessionMessage[]>;
|
|
9
|
+
listSessions(cwd: string): Promise<SessionInfo[]>;
|
|
10
|
+
/** Short-lived, read-only ACP queries share one deadline and always release the child. */
|
|
11
|
+
private readSessionData;
|
|
12
|
+
}
|
|
13
|
+
/** PR #334's provider extension. Never treat standard ACP used/size as plan usage. */
|
|
14
|
+
export declare function codexPlanUsage(meta: unknown): PlanUsageWindow[];
|