clearclaw 0.7.0 → 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 +18 -1
- package/dist/channel/slack.js +227 -27
- package/dist/channel/telegram.d.ts +19 -0
- package/dist/channel/telegram.js +136 -30
- package/dist/config.d.ts +37 -1
- package/dist/config.js +118 -0
- package/dist/engine/acp.d.ts +9 -4
- package/dist/engine/acp.js +301 -29
- package/dist/engine/claude-code.d.ts +19 -2
- package/dist/engine/claude-code.js +121 -33
- package/dist/engine/mcp-http.d.ts +9 -0
- package/dist/engine/mcp-http.js +59 -0
- package/dist/engine/registry.d.ts +10 -7
- package/dist/engine/registry.js +99 -16
- package/dist/engine/session-transcript.d.ts +14 -0
- package/dist/engine/session-transcript.js +38 -0
- package/dist/format.d.ts +1 -1
- package/dist/format.js +42 -7
- package/dist/index.js +18 -23
- package/dist/orchestrator.d.ts +31 -4
- package/dist/orchestrator.js +822 -167
- package/dist/plan-usage.d.ts +6 -0
- package/dist/plan-usage.js +31 -0
- package/dist/prompt.d.ts +6 -2
- package/dist/prompt.js +62 -19
- package/dist/scheduler.d.ts +15 -0
- package/dist/scheduler.js +82 -0
- package/dist/types.d.ts +77 -5
- package/package.json +11 -4
- package/patches/README.md +13 -0
- package/patches/codex-acp-1.11.0.patch +38 -0
- package/prompts/SYSTEM.md +66 -16
- package/scripts/patch-codex-acp.mjs +32 -0
- package/prompts/ONBOARDING.md +0 -56
package/dist/channel/telegram.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { EventEmitter } from "node:events";
|
|
2
|
-
import { Bot, InlineKeyboard, InputFile } from "grammy";
|
|
2
|
+
import { Bot, GrammyError, InlineKeyboard, InputFile } from "grammy";
|
|
3
3
|
import mime from "mime";
|
|
4
4
|
import telegramifyMarkdown from "telegramify-markdown";
|
|
5
5
|
import log from "../logger.js";
|
|
@@ -15,6 +15,9 @@ export class TelegramChannel extends EventEmitter {
|
|
|
15
15
|
pendingCallbacks = new Map();
|
|
16
16
|
// Pending text input: chatId → resolve function (for requestText follow-ups)
|
|
17
17
|
pendingTextResolvers = new Map();
|
|
18
|
+
// Our bot's @username, captured at connect. Used to strip the `@bot` suffix
|
|
19
|
+
// Telegram appends to commands in group chats.
|
|
20
|
+
botUsername;
|
|
18
21
|
constructor(botToken, isAuthorized, onUnauthorizedDM) {
|
|
19
22
|
super();
|
|
20
23
|
this.botToken = botToken;
|
|
@@ -22,6 +25,31 @@ export class TelegramChannel extends EventEmitter {
|
|
|
22
25
|
this.onUnauthorizedDM = onUnauthorizedDM;
|
|
23
26
|
this.bot = new Bot(botToken);
|
|
24
27
|
}
|
|
28
|
+
/**
|
|
29
|
+
* Strip the `@<botUsername>` suffix Telegram appends to commands in group
|
|
30
|
+
* chats (`/context@mybot args` → `/context args`), so downstream command
|
|
31
|
+
* handling — ClearClaw's own `/new` etc. and slash-command pass-through —
|
|
32
|
+
* sees the bare `/command`. No-op for non-command text and in DMs (no suffix).
|
|
33
|
+
* Telegram usernames are `[A-Za-z0-9_]`, so they need no regex escaping.
|
|
34
|
+
*/
|
|
35
|
+
stripCommandMention(text) {
|
|
36
|
+
if (!this.botUsername || !text.startsWith("/"))
|
|
37
|
+
return text;
|
|
38
|
+
return text.replace(new RegExp(`^(/[^\\s@]+)@${this.botUsername}(?=\\s|$)`, "i"), "$1");
|
|
39
|
+
}
|
|
40
|
+
async telegramCall(call, label) {
|
|
41
|
+
try {
|
|
42
|
+
return await call();
|
|
43
|
+
}
|
|
44
|
+
catch (err) {
|
|
45
|
+
const retryAfter = telegramRetryAfterSeconds(err);
|
|
46
|
+
if (retryAfter === null)
|
|
47
|
+
throw err;
|
|
48
|
+
log.warn("[channel] Telegram rate limited during %s, retrying after %ss", label, retryAfter);
|
|
49
|
+
await sleep((retryAfter + 1) * 1000);
|
|
50
|
+
return await call();
|
|
51
|
+
}
|
|
52
|
+
}
|
|
25
53
|
async connect() {
|
|
26
54
|
this.bot.on("message:text", (ctx) => {
|
|
27
55
|
const sender = this.extractSender(ctx);
|
|
@@ -36,7 +64,7 @@ export class TelegramChannel extends EventEmitter {
|
|
|
36
64
|
}
|
|
37
65
|
this.emit("message", {
|
|
38
66
|
...sender,
|
|
39
|
-
text: ctx.message.text,
|
|
67
|
+
text: this.stripCommandMention(ctx.message.text),
|
|
40
68
|
messageId: String(ctx.message.message_id),
|
|
41
69
|
replyTo: this.extractReplyContext(ctx.message),
|
|
42
70
|
});
|
|
@@ -89,6 +117,7 @@ export class TelegramChannel extends EventEmitter {
|
|
|
89
117
|
await new Promise((resolve) => {
|
|
90
118
|
this.bot.start({
|
|
91
119
|
onStart: (botInfo) => {
|
|
120
|
+
this.botUsername = botInfo.username;
|
|
92
121
|
log.info(`[channel] Telegram bot: @${botInfo.username}`);
|
|
93
122
|
resolve();
|
|
94
123
|
},
|
|
@@ -106,6 +135,10 @@ export class TelegramChannel extends EventEmitter {
|
|
|
106
135
|
ownsId(chatId) {
|
|
107
136
|
return chatId.startsWith("tg:");
|
|
108
137
|
}
|
|
138
|
+
/** A private chat is the user's own id; a ":thread" suffix means a topic inside it. */
|
|
139
|
+
isRootDM(chatId, userId) {
|
|
140
|
+
return /^tg:[1-9]\d*$/.test(chatId) && chatId === userId;
|
|
141
|
+
}
|
|
109
142
|
async sendMessage(chatId, text, opts) {
|
|
110
143
|
const numId = this.numericId(chatId);
|
|
111
144
|
const chunks = splitMessage(text);
|
|
@@ -117,21 +150,22 @@ export class TelegramChannel extends EventEmitter {
|
|
|
117
150
|
? { reply_parameters: { message_id: Number(opts.replyToMessageId) } }
|
|
118
151
|
: {};
|
|
119
152
|
if (plain) {
|
|
120
|
-
const sent = await this.bot.api.sendMessage(numId, chunk, replyParams);
|
|
153
|
+
const sent = await this.telegramCall(() => this.bot.api.sendMessage(numId, chunk, { ...this.threadOpts(chatId), ...replyParams }), "sendMessage");
|
|
121
154
|
handles.push(String(sent.message_id));
|
|
122
155
|
}
|
|
123
156
|
else {
|
|
124
157
|
const rendered = telegramifyMarkdown(chunk, "escape");
|
|
125
158
|
try {
|
|
126
|
-
const sent = await this.bot.api.sendMessage(numId, rendered, {
|
|
159
|
+
const sent = await this.telegramCall(() => this.bot.api.sendMessage(numId, rendered, {
|
|
160
|
+
...this.threadOpts(chatId),
|
|
127
161
|
parse_mode: "MarkdownV2",
|
|
128
162
|
...replyParams,
|
|
129
|
-
});
|
|
163
|
+
}), "sendMessage");
|
|
130
164
|
handles.push(String(sent.message_id));
|
|
131
165
|
}
|
|
132
166
|
catch (err) {
|
|
133
167
|
log.warn("[channel] sendMessage failed with MarkdownV2, retrying as plain text");
|
|
134
|
-
const sent = await this.bot.api.sendMessage(numId, chunk, replyParams);
|
|
168
|
+
const sent = await this.telegramCall(() => this.bot.api.sendMessage(numId, chunk, { ...this.threadOpts(chatId), ...replyParams }), "sendMessage");
|
|
135
169
|
handles.push(String(sent.message_id));
|
|
136
170
|
}
|
|
137
171
|
}
|
|
@@ -164,16 +198,18 @@ export class TelegramChannel extends EventEmitter {
|
|
|
164
198
|
const mdv2Text = telegramifyMarkdown(text, "escape");
|
|
165
199
|
let sent;
|
|
166
200
|
try {
|
|
167
|
-
sent = await this.bot.api.sendMessage(numId, mdv2Text, {
|
|
201
|
+
sent = await this.telegramCall(() => this.bot.api.sendMessage(numId, mdv2Text, {
|
|
202
|
+
...this.threadOpts(chatId),
|
|
168
203
|
reply_markup: keyboard,
|
|
169
204
|
parse_mode: "MarkdownV2",
|
|
170
|
-
});
|
|
205
|
+
}), "sendInteractive");
|
|
171
206
|
}
|
|
172
207
|
catch {
|
|
173
208
|
log.warn("[channel] sendInteractive failed with MarkdownV2, retrying as plain text");
|
|
174
|
-
sent = await this.bot.api.sendMessage(numId, text, {
|
|
209
|
+
sent = await this.telegramCall(() => this.bot.api.sendMessage(numId, text, {
|
|
210
|
+
...this.threadOpts(chatId),
|
|
175
211
|
reply_markup: keyboard,
|
|
176
|
-
});
|
|
212
|
+
}), "sendInteractive");
|
|
177
213
|
}
|
|
178
214
|
const cleanupAll = () => {
|
|
179
215
|
for (const { cbData } of cbEntries) {
|
|
@@ -201,17 +237,18 @@ export class TelegramChannel extends EventEmitter {
|
|
|
201
237
|
feedbackKeyboard.row();
|
|
202
238
|
}
|
|
203
239
|
try {
|
|
204
|
-
await this.bot.api.editMessageReplyMarkup(numId, sent.message_id, {
|
|
240
|
+
await this.telegramCall(() => this.bot.api.editMessageReplyMarkup(numId, sent.message_id, {
|
|
205
241
|
reply_markup: feedbackKeyboard,
|
|
206
|
-
});
|
|
242
|
+
}), "editMessageReplyMarkup");
|
|
207
243
|
}
|
|
208
244
|
catch { /* best-effort */ }
|
|
209
245
|
// If the button requests text, prompt for follow-up input
|
|
210
246
|
if (pressed.requestText) {
|
|
211
|
-
await this.bot.api.sendMessage(numId, "Add your feedback:", {
|
|
247
|
+
await this.telegramCall(() => this.bot.api.sendMessage(numId, "Add your feedback:", {
|
|
248
|
+
...this.threadOpts(chatId),
|
|
212
249
|
reply_markup: { force_reply: true },
|
|
213
250
|
reply_parameters: { message_id: sent.message_id },
|
|
214
|
-
});
|
|
251
|
+
}), "sendInteractiveFollowUp");
|
|
215
252
|
const followUpText = await new Promise((resolve) => {
|
|
216
253
|
this.pendingTextResolvers.set(chatId, resolve);
|
|
217
254
|
});
|
|
@@ -222,15 +259,15 @@ export class TelegramChannel extends EventEmitter {
|
|
|
222
259
|
async editMessage(chatId, handle, text, opts) {
|
|
223
260
|
const numId = this.numericId(chatId);
|
|
224
261
|
if (opts?.format === "plain") {
|
|
225
|
-
await this.bot.api.editMessageText(numId, Number(handle), text);
|
|
262
|
+
await this.telegramCall(() => this.bot.api.editMessageText(numId, Number(handle), text), "editMessage");
|
|
226
263
|
}
|
|
227
264
|
else {
|
|
228
265
|
const rendered = telegramifyMarkdown(text, "escape");
|
|
229
266
|
try {
|
|
230
|
-
await this.bot.api.editMessageText(numId, Number(handle), rendered, { parse_mode: "MarkdownV2" });
|
|
267
|
+
await this.telegramCall(() => this.bot.api.editMessageText(numId, Number(handle), rendered, { parse_mode: "MarkdownV2" }), "editMessage");
|
|
231
268
|
}
|
|
232
269
|
catch {
|
|
233
|
-
await this.bot.api.editMessageText(numId, Number(handle), text);
|
|
270
|
+
await this.telegramCall(() => this.bot.api.editMessageText(numId, Number(handle), text), "editMessage");
|
|
234
271
|
}
|
|
235
272
|
}
|
|
236
273
|
}
|
|
@@ -240,13 +277,18 @@ export class TelegramChannel extends EventEmitter {
|
|
|
240
277
|
}
|
|
241
278
|
async pinMessage(chatId, handle) {
|
|
242
279
|
const numId = this.numericId(chatId);
|
|
243
|
-
await this.bot.api.pinChatMessage(numId, Number(handle), {
|
|
280
|
+
await this.telegramCall(() => this.bot.api.pinChatMessage(numId, Number(handle), {
|
|
244
281
|
disable_notification: true,
|
|
245
|
-
});
|
|
282
|
+
}), "pinMessage");
|
|
246
283
|
}
|
|
284
|
+
/** Unpin everything in this chat's scope: the forum topic for a topic chat, else the whole chat. */
|
|
247
285
|
async unpinAllMessages(chatId) {
|
|
248
286
|
const numId = this.numericId(chatId);
|
|
249
|
-
|
|
287
|
+
const { message_thread_id } = this.threadOpts(chatId);
|
|
288
|
+
if (message_thread_id)
|
|
289
|
+
await this.bot.api.unpinAllForumTopicMessages(numId, message_thread_id);
|
|
290
|
+
else
|
|
291
|
+
await this.bot.api.unpinAllChatMessages(numId);
|
|
250
292
|
}
|
|
251
293
|
async updateStatus(chatId, text) {
|
|
252
294
|
const existing = this.statusHandles.get(chatId);
|
|
@@ -266,7 +308,9 @@ export class TelegramChannel extends EventEmitter {
|
|
|
266
308
|
}
|
|
267
309
|
}
|
|
268
310
|
}
|
|
269
|
-
// No existing handle, or edit failed — create
|
|
311
|
+
// No existing handle, or edit failed — (re)create the pinned status. The handle lives only in
|
|
312
|
+
// memory, so after a restart the previous status is unknown; clear every pin in scope instead
|
|
313
|
+
// (topic-scoped for forum topics, so peers in other topics keep theirs).
|
|
270
314
|
try {
|
|
271
315
|
await this.unpinAllMessages(chatId);
|
|
272
316
|
}
|
|
@@ -288,10 +332,10 @@ export class TelegramChannel extends EventEmitter {
|
|
|
288
332
|
const numId = this.numericId(chatId);
|
|
289
333
|
// Send immediately, then repeat every 4s
|
|
290
334
|
await this.bot.api
|
|
291
|
-
.sendChatAction(numId, "typing")
|
|
335
|
+
.sendChatAction(numId, "typing", this.threadOpts(chatId))
|
|
292
336
|
.catch(() => { });
|
|
293
337
|
const interval = setInterval(() => {
|
|
294
|
-
this.bot.api.sendChatAction(numId, "typing").catch(() => { });
|
|
338
|
+
this.bot.api.sendChatAction(numId, "typing", this.threadOpts(chatId)).catch(() => { });
|
|
295
339
|
}, 4000);
|
|
296
340
|
this.typingIntervals.set(chatId, interval);
|
|
297
341
|
}
|
|
@@ -323,10 +367,13 @@ export class TelegramChannel extends EventEmitter {
|
|
|
323
367
|
}
|
|
324
368
|
return null;
|
|
325
369
|
}
|
|
370
|
+
const topicSuffix = ctx.message?.is_topic_message && ctx.message.message_thread_id
|
|
371
|
+
? `:${ctx.message.message_thread_id}`
|
|
372
|
+
: "";
|
|
326
373
|
return {
|
|
327
|
-
chatId: `tg:${ctx.chat.id}`,
|
|
374
|
+
chatId: `tg:${ctx.chat.id}${topicSuffix}`,
|
|
328
375
|
chatType: ctx.chat.type === "private" ? "dm" : "group",
|
|
329
|
-
user,
|
|
376
|
+
origin: { kind: "user", user },
|
|
330
377
|
};
|
|
331
378
|
}
|
|
332
379
|
/** Extract reply context from a Telegram message, if it's a reply. */
|
|
@@ -387,16 +434,16 @@ export class TelegramChannel extends EventEmitter {
|
|
|
387
434
|
const file = new InputFile(buffer, filename);
|
|
388
435
|
const caption = opts?.caption;
|
|
389
436
|
if (mimeType.startsWith("image/")) {
|
|
390
|
-
await this.bot.api.sendPhoto(id, file, { caption });
|
|
437
|
+
await this.bot.api.sendPhoto(id, file, { caption, ...this.threadOpts(chatId) });
|
|
391
438
|
}
|
|
392
439
|
else if (mimeType.startsWith("video/")) {
|
|
393
|
-
await this.bot.api.sendVideo(id, file, { caption });
|
|
440
|
+
await this.bot.api.sendVideo(id, file, { caption, ...this.threadOpts(chatId) });
|
|
394
441
|
}
|
|
395
442
|
else if (mimeType.startsWith("audio/")) {
|
|
396
|
-
await this.bot.api.sendAudio(id, file, { caption });
|
|
443
|
+
await this.bot.api.sendAudio(id, file, { caption, ...this.threadOpts(chatId) });
|
|
397
444
|
}
|
|
398
445
|
else {
|
|
399
|
-
await this.bot.api.sendDocument(id, file, { caption });
|
|
446
|
+
await this.bot.api.sendDocument(id, file, { caption, ...this.threadOpts(chatId) });
|
|
400
447
|
}
|
|
401
448
|
}
|
|
402
449
|
async reactToMessage(chatId, messageId, emoji) {
|
|
@@ -411,6 +458,45 @@ export class TelegramChannel extends EventEmitter {
|
|
|
411
458
|
log.warn("[channel] failed to set reaction on %s: %s", messageId, err instanceof Error ? err.message : String(err));
|
|
412
459
|
}
|
|
413
460
|
}
|
|
461
|
+
async createProjectChat(projectName, anchor, title) {
|
|
462
|
+
await this.validateProjectChat(projectName, anchor);
|
|
463
|
+
const topic = await this.bot.api.createForumTopic(this.numericId(anchor), title);
|
|
464
|
+
return `tg:${this.numericId(anchor)}:${topic.message_thread_id}`;
|
|
465
|
+
}
|
|
466
|
+
async closeProjectChat(chatId) {
|
|
467
|
+
const thread = chatId.split(":")[2];
|
|
468
|
+
if (!thread)
|
|
469
|
+
throw new Error("Telegram cannot close a whole chat; only topics can be archived");
|
|
470
|
+
const parentId = this.numericId(chatId);
|
|
471
|
+
if (parentId > 0) {
|
|
472
|
+
await this.bot.api.deleteForumTopic(parentId, Number(thread));
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
try {
|
|
476
|
+
await this.bot.api.closeForumTopic(parentId, Number(thread));
|
|
477
|
+
}
|
|
478
|
+
catch (err) {
|
|
479
|
+
if (err instanceof GrammyError && err.description === "Bad Request: TOPIC_NOT_MODIFIED")
|
|
480
|
+
return;
|
|
481
|
+
throw err;
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
async validateProjectChat(projectName, anchor) {
|
|
485
|
+
const parentId = this.numericId(anchor);
|
|
486
|
+
if (!Number.isFinite(parentId)) {
|
|
487
|
+
throw new Error(`Telegram project "${projectName}" must use a valid chat ID`);
|
|
488
|
+
}
|
|
489
|
+
const chat = await this.bot.api.getChat(parentId);
|
|
490
|
+
if (chat.type === "private") {
|
|
491
|
+
const botInfo = await this.bot.api.getMe();
|
|
492
|
+
if (botInfo.has_topics_enabled)
|
|
493
|
+
return;
|
|
494
|
+
throw new Error(`Telegram project "${projectName}" requires Threaded Mode for this bot. Enable it in BotFather, then retry.`);
|
|
495
|
+
}
|
|
496
|
+
if (chat.type === "supergroup" && chat.is_forum)
|
|
497
|
+
return;
|
|
498
|
+
throw new Error(`Telegram project "${projectName}" requires a forum supergroup. Enable Topics manually in Telegram, then retry.`);
|
|
499
|
+
}
|
|
414
500
|
/** Download a Telegram file by file_id, returning the raw buffer. */
|
|
415
501
|
async downloadFile(fileId) {
|
|
416
502
|
const file = await this.bot.api.getFile(fileId);
|
|
@@ -422,8 +508,14 @@ export class TelegramChannel extends EventEmitter {
|
|
|
422
508
|
throw new Error(`File download failed: ${resp.status}`);
|
|
423
509
|
return Buffer.from(await resp.arrayBuffer());
|
|
424
510
|
}
|
|
511
|
+
/** "tg:123" → 123; "tg:123:45" (forum topic) → 123 */
|
|
425
512
|
numericId(chatId) {
|
|
426
|
-
return Number(chatId.
|
|
513
|
+
return Number(chatId.split(":")[1]);
|
|
514
|
+
}
|
|
515
|
+
/** Topic part of a composite chat id, as spreadable send options. */
|
|
516
|
+
threadOpts(chatId) {
|
|
517
|
+
const thread = chatId.split(":")[2];
|
|
518
|
+
return thread ? { message_thread_id: Number(thread) } : {};
|
|
427
519
|
}
|
|
428
520
|
}
|
|
429
521
|
const MAX_MSG_LEN = 4096;
|
|
@@ -450,4 +542,18 @@ function splitMessage(text) {
|
|
|
450
542
|
chunks.push(remaining);
|
|
451
543
|
return chunks;
|
|
452
544
|
}
|
|
545
|
+
function telegramRetryAfterSeconds(err) {
|
|
546
|
+
const candidate = err;
|
|
547
|
+
const retryAfter = candidate?.parameters?.retry_after;
|
|
548
|
+
if (typeof retryAfter === "number" && Number.isFinite(retryAfter))
|
|
549
|
+
return retryAfter;
|
|
550
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
551
|
+
const match = message.match(/retry after (\d+)/i);
|
|
552
|
+
if (!match)
|
|
553
|
+
return null;
|
|
554
|
+
return Number(match[1]);
|
|
555
|
+
}
|
|
556
|
+
function sleep(ms) {
|
|
557
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
558
|
+
}
|
|
453
559
|
//# sourceMappingURL=telegram.js.map
|
package/dist/config.d.ts
CHANGED
|
@@ -24,7 +24,21 @@ export interface PendingPairing {
|
|
|
24
24
|
export interface EngineEntry {
|
|
25
25
|
name: string;
|
|
26
26
|
default?: boolean;
|
|
27
|
-
path
|
|
27
|
+
path?: string;
|
|
28
|
+
configPath?: string;
|
|
29
|
+
}
|
|
30
|
+
export interface ScheduleEntry {
|
|
31
|
+
id: string;
|
|
32
|
+
cron: string;
|
|
33
|
+
prompt: string;
|
|
34
|
+
enabled: boolean;
|
|
35
|
+
timezone?: string;
|
|
36
|
+
createdAt: number;
|
|
37
|
+
}
|
|
38
|
+
export interface Project {
|
|
39
|
+
name: string;
|
|
40
|
+
description: string;
|
|
41
|
+
main_workspace: string;
|
|
28
42
|
}
|
|
29
43
|
export declare function resolveDataDir(): string;
|
|
30
44
|
/**
|
|
@@ -61,16 +75,38 @@ export declare class Config {
|
|
|
61
75
|
hasUser(userId: string): boolean;
|
|
62
76
|
addUser(user: AuthorizedUser): void;
|
|
63
77
|
listUsers(): AuthorizedUser[];
|
|
78
|
+
/** All authorized IDs, including environment-configured and pairing-approved users. */
|
|
79
|
+
listAuthorizedUserIds(): string[];
|
|
64
80
|
/** env var IDs (in-memory) + pairing-approved users (hot-reload from disk) */
|
|
65
81
|
isAuthorized(userId: string): boolean;
|
|
66
82
|
createPairing(userId: string, userName: string, chatId: string): string;
|
|
67
83
|
consumePairing(code: string): PendingPairing | null;
|
|
68
84
|
private generateCode;
|
|
85
|
+
/** Home exists before a chat is paired. Never replace an existing home's settings. */
|
|
86
|
+
ensureHomeWorkspace(): Workspace;
|
|
87
|
+
/**
|
|
88
|
+
* ensureHomeWorkspace creates home's records at startup, before any chat is known.
|
|
89
|
+
* This gives it its destination, once: an existing binding is never replaced.
|
|
90
|
+
* Callers decide what counts as home's chat — see Channel.isRootDM.
|
|
91
|
+
*/
|
|
92
|
+
connectHomeWorkspace(chatId: string): boolean;
|
|
69
93
|
workspaceByChat(chatId: string): Workspace | undefined;
|
|
70
94
|
workspaceByName(name: string): Workspace | undefined;
|
|
95
|
+
listWorkspaces(): Workspace[];
|
|
71
96
|
upsertWorkspace(ws: Workspace): void;
|
|
97
|
+
removeWorkspace(name: string): Workspace | undefined;
|
|
98
|
+
/** Persist the session ID without changing the saved model setting. */
|
|
72
99
|
setSession(name: string, sessionId: string): void;
|
|
73
100
|
clearSession(name: string): void;
|
|
101
|
+
setModel(name: string, model: string | undefined): void;
|
|
74
102
|
setBehavior(name: string, behavior: "assistant" | "relay"): void;
|
|
103
|
+
addProject(project: Project): void;
|
|
104
|
+
listProjects(): Project[];
|
|
105
|
+
projectByName(name: string): Project | undefined;
|
|
106
|
+
removeProject(name: string): void;
|
|
107
|
+
listSchedules(): ScheduleEntry[];
|
|
108
|
+
addSchedule(entry: ScheduleEntry): void;
|
|
109
|
+
removeSchedule(id: string): void;
|
|
110
|
+
updateSchedule(id: string, update: Partial<Pick<ScheduleEntry, "enabled" | "cron" | "prompt" | "timezone">>): void;
|
|
75
111
|
approveUser(userId: string, userName: string): void;
|
|
76
112
|
}
|
package/dist/config.js
CHANGED
|
@@ -108,6 +108,8 @@ export class Config {
|
|
|
108
108
|
authorizedUsers: (raw.authorizedUsers ?? []),
|
|
109
109
|
pendingPairings: (raw.pendingPairings ?? []),
|
|
110
110
|
workspaces: (raw.workspaces ?? []),
|
|
111
|
+
schedules: (raw.schedules ?? []),
|
|
112
|
+
projects: (raw.projects ?? []),
|
|
111
113
|
};
|
|
112
114
|
}
|
|
113
115
|
write(data) {
|
|
@@ -142,6 +144,13 @@ export class Config {
|
|
|
142
144
|
listUsers() {
|
|
143
145
|
return this.read().authorizedUsers;
|
|
144
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
|
+
}
|
|
145
154
|
/** env var IDs (in-memory) + pairing-approved users (hot-reload from disk) */
|
|
146
155
|
isAuthorized(userId) {
|
|
147
156
|
return this.envUserIds.has(userId) || this.hasUser(userId);
|
|
@@ -185,12 +194,59 @@ export class Config {
|
|
|
185
194
|
.join("");
|
|
186
195
|
}
|
|
187
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
|
+
}
|
|
188
241
|
workspaceByChat(chatId) {
|
|
189
242
|
return this.read().workspaces.find((ws) => ws.chat_id === chatId);
|
|
190
243
|
}
|
|
191
244
|
workspaceByName(name) {
|
|
192
245
|
return this.read().workspaces.find((ws) => ws.name === name);
|
|
193
246
|
}
|
|
247
|
+
listWorkspaces() {
|
|
248
|
+
return this.read().workspaces;
|
|
249
|
+
}
|
|
194
250
|
upsertWorkspace(ws) {
|
|
195
251
|
const data = this.read();
|
|
196
252
|
const idx = data.workspaces.findIndex((w) => w.name === ws.name);
|
|
@@ -200,6 +256,16 @@ export class Config {
|
|
|
200
256
|
data.workspaces.push(ws);
|
|
201
257
|
this.write(data);
|
|
202
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. */
|
|
203
269
|
setSession(name, sessionId) {
|
|
204
270
|
const data = this.read();
|
|
205
271
|
const ws = data.workspaces.find((w) => w.name === name);
|
|
@@ -213,6 +279,15 @@ export class Config {
|
|
|
213
279
|
const ws = data.workspaces.find((w) => w.name === name);
|
|
214
280
|
if (ws) {
|
|
215
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;
|
|
216
291
|
this.write(data);
|
|
217
292
|
}
|
|
218
293
|
}
|
|
@@ -224,6 +299,49 @@ export class Config {
|
|
|
224
299
|
this.write(data);
|
|
225
300
|
}
|
|
226
301
|
}
|
|
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;
|
|
314
|
+
}
|
|
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
|
+
}
|
|
227
345
|
// --- Approval ---
|
|
228
346
|
approveUser(userId, userName) {
|
|
229
347
|
this.addUser({ id: userId, name: userName, approvedAt: Date.now() });
|
package/dist/engine/acp.d.ts
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import type { Engine, EngineEvent, RunTurnOpts, SessionInfo } from "../types.js";
|
|
1
|
+
import type { AcpEngineDefinition } from "./registry.js";
|
|
2
|
+
import type { Engine, EngineEvent, PlanUsageWindow, RunTurnOpts, SessionInfo, SessionHistoryOpts, SessionMessage } from "../types.js";
|
|
3
3
|
export declare class AcpEngine implements Engine {
|
|
4
4
|
readonly name: string;
|
|
5
5
|
private readonly spawnConfig;
|
|
6
|
-
constructor(name: string, spawnConfig:
|
|
6
|
+
constructor(name: string, spawnConfig: AcpEngineDefinition);
|
|
7
7
|
runTurn(opts: RunTurnOpts): AsyncIterable<EngineEvent>;
|
|
8
|
-
|
|
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;
|
|
9
12
|
}
|
|
13
|
+
/** PR #334's provider extension. Never treat standard ACP used/size as plan usage. */
|
|
14
|
+
export declare function codexPlanUsage(meta: unknown): PlanUsageWindow[];
|