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/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
- defaultPromptPath;
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.defaultPromptPath = path.join(this.dataDir, "workspace", "CLAUDE.md");
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
- read() {
77
- if (!fs.existsSync(this.filePath)) {
78
- return { authorizedUsers: [], pendingPairings: [], workspaces: [] };
79
- }
92
+ readRaw() {
93
+ if (!fs.existsSync(this.filePath))
94
+ return {};
80
95
  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
- };
96
+ return JSON.parse(fs.readFileSync(this.filePath, "utf-8"));
88
97
  }
89
98
  catch {
90
- return { authorizedUsers: [], pendingPairings: [], workspaces: [] };
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
- // --- 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 });
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
- approveUser(userId, userName, chatId) {
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[];