@chanlerdev/scorel 0.0.1 → 0.0.2

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.
Files changed (43) hide show
  1. package/README.md +356 -69
  2. package/dist/index.js +4237 -1759
  3. package/dist/index.js.map +4 -4
  4. package/docs/CHANGELOG.md +62 -0
  5. package/docs/ROADMAP.md +93 -9
  6. package/docs/SHIP.md +9 -3
  7. package/docs/spec/channels.md +97 -100
  8. package/docs/spec/client.md +11 -5
  9. package/docs/spec/extensions.md +115 -43
  10. package/docs/spec/ship/S0062-npm-package-and-release-workflow.md +3 -0
  11. package/docs/spec/ship/S0063-ai-release-notes.md +129 -0
  12. package/docs/spec/ship/S0064-gui-product-intent-and-boundary.md +79 -0
  13. package/docs/spec/ship/S0065-gui-electron-shell-and-embedded-host.md +73 -0
  14. package/docs/spec/ship/S0066-gui-local-project-workspace.md +79 -0
  15. package/docs/spec/ship/S0067-gui-relay-device-and-remote-project-selection.md +97 -0
  16. package/docs/spec/ship/S0068-gui-codex-app-polish-and-e2e.md +102 -0
  17. package/docs/spec/ship/S0068-gui-e2e-verification.md +50 -0
  18. package/docs/spec/ship/S0069-gui-codex-ui-refactor.md +371 -0
  19. package/docs/spec/ship/S0070-gui-streaming-and-tool-blocks.md +202 -0
  20. package/docs/spec/ship/S0071-gui-visual-fidelity-and-settings-shell.md +360 -0
  21. package/docs/spec/ship/S0072-gui-glass-sidebar-and-picker-anchoring.md +116 -0
  22. package/docs/spec/ship/S0073-provider-model-profile-contract.md +241 -0
  23. package/docs/spec/ship/S0074-gui-model-provider-settings-split.md +113 -0
  24. package/docs/spec/ship/S0075-provider-catalog-model-cards.md +93 -0
  25. package/docs/spec/ship/S0076-provider-modal-search-and-direct-key.md +70 -0
  26. package/docs/spec/ship/S0077-auxiliary-session-title-generation.md +95 -0
  27. package/docs/spec/ship/S0078-gui-provider-settings-forward-config-and-simplification.md +150 -0
  28. package/docs/spec/ship/S0079-gui-sidebar-layout-controls.md +49 -0
  29. package/docs/spec/ship/S0080-session-title-hook-and-gui-markdown-dark-code.md +58 -0
  30. package/docs/spec/ship/S0081-automatic-memory.md +117 -0
  31. package/docs/spec/ship/S0082-memory-journal-tool-and-idle-dream.md +107 -0
  32. package/docs/spec/ship/S0083-extension-manifest-and-im-channel-runtime.md +338 -0
  33. package/docs/spec/ship/S0084-built-in-telegram-im-extension.md +188 -0
  34. package/docs/spec/ship/S0085-gui-im-extension-settings.md +47 -0
  35. package/docs/spec/ship/S0086-auto-compact-and-session-memory.md +124 -0
  36. package/extensions/builtin/loopback/adapter.js +13 -0
  37. package/extensions/builtin/loopback/scorel.extension.json +7 -0
  38. package/extensions/builtin/loopback/skills/loopback/SKILL.md +7 -0
  39. package/extensions/builtin/telegram/adapter.d.ts +43 -0
  40. package/extensions/builtin/telegram/adapter.js +252 -0
  41. package/extensions/builtin/telegram/scorel.extension.json +7 -0
  42. package/extensions/builtin/telegram/skills/telegram/SKILL.md +9 -0
  43. package/package.json +6 -2
@@ -0,0 +1,13 @@
1
+ export const createAdapter = () => {
2
+ const outbox = [];
3
+ return {
4
+ async start() {},
5
+ async stop() {},
6
+ async sendMessage(_target, message) {
7
+ outbox.push(message);
8
+ },
9
+ getOutbox() {
10
+ return [...outbox];
11
+ },
12
+ };
13
+ };
@@ -0,0 +1,7 @@
1
+ {
2
+ "id": "loopback",
3
+ "kind": "im",
4
+ "displayName": "Loopback IM",
5
+ "adapter": "./adapter.js",
6
+ "skills": ["./skills"]
7
+ }
@@ -0,0 +1,7 @@
1
+ ---
2
+ description: Reply to the current loopback IM conversation with SendChannelMessage.
3
+ ---
4
+
5
+ # Loopback IM
6
+
7
+ When a message comes from the loopback IM channel, use `SendChannelMessage` to reply to the current conversation. Do not ask for raw channel ids.
@@ -0,0 +1,43 @@
1
+ export type TelegramAdapterOptions = {
2
+ token: string;
3
+ apiBaseUrl: string;
4
+ pollIntervalMs?: number;
5
+ allowedChatIds?: string[];
6
+ botUsername?: string;
7
+ };
8
+
9
+ export type TelegramIncomingMessage = {
10
+ externalConversationId: string;
11
+ text: string;
12
+ conversationType?: string;
13
+ senderDisplayName?: string;
14
+ mentionedBot?: boolean;
15
+ target?: TelegramTarget;
16
+ data?: Record<string, unknown>;
17
+ };
18
+
19
+ export type TelegramTarget = {
20
+ externalConversationId: string;
21
+ data?: Record<string, unknown>;
22
+ };
23
+
24
+ export type TelegramAdapter = {
25
+ start(ctx: {
26
+ onMessage(message: TelegramIncomingMessage): Promise<void>;
27
+ logger: {
28
+ info(message: string, data?: Record<string, unknown>): void;
29
+ error(message: string, data?: Record<string, unknown>): void;
30
+ };
31
+ }): Promise<void>;
32
+ stop(): Promise<void>;
33
+ sendMessage(target: TelegramTarget, message: { text: string }): Promise<void>;
34
+ setTyping?(target: TelegramTarget, typing: boolean): Promise<void>;
35
+ };
36
+
37
+ export function createAdapter(options?: { config?: Record<string, string | number | boolean> }): TelegramAdapter;
38
+ export function createTelegramAdapter(options: TelegramAdapterOptions): TelegramAdapter;
39
+ export function normalizeTelegramUpdate(update: unknown, options?: { botUsername?: string; allowedChatIds?: string[] }): unknown;
40
+ export function isBotMentioned(message: unknown, botUsername?: string): boolean;
41
+ export function splitTelegramText(text: string): string[];
42
+ export function parseAllowedChatIds(value: unknown): string[];
43
+ export function redactToken(value: string): string;
@@ -0,0 +1,252 @@
1
+ const DEFAULT_POLL_INTERVAL_MS = 1000;
2
+ const TELEGRAM_MESSAGE_LIMIT = 4096;
3
+
4
+ export const createAdapter = ({ config = {} } = {}) => {
5
+ const directToken = optionalStringConfig(config.apiKey ?? config.botToken, "Telegram direct API key");
6
+ const tokenEnv = stringConfig(config.botTokenEnv, "SCOREL_TELEGRAM_BOT_TOKEN");
7
+ const token = directToken ?? process.env[tokenEnv];
8
+ if (!token) {
9
+ throw new Error(`${tokenEnv} is not set`);
10
+ }
11
+ return createTelegramAdapter({
12
+ token,
13
+ apiBaseUrl: stringConfig(config.apiBaseUrl, "https://api.telegram.org"),
14
+ pollIntervalMs: numberConfig(config.pollIntervalMs, DEFAULT_POLL_INTERVAL_MS),
15
+ allowedChatIds: parseAllowedChatIds(config.allowedChatIds),
16
+ botUsername: typeof config.botUsername === "string" ? config.botUsername : undefined,
17
+ });
18
+ };
19
+
20
+ export const createTelegramAdapter = (options) => {
21
+ const state = {
22
+ running: false,
23
+ offset: undefined,
24
+ timer: undefined,
25
+ ctx: undefined,
26
+ botUsername: options.botUsername,
27
+ };
28
+
29
+ const request = async (method, body) => telegramRequest(options.apiBaseUrl, options.token, method, body);
30
+
31
+ const pollOnce = async () => {
32
+ if (!state.running || !state.ctx) {
33
+ return;
34
+ }
35
+ try {
36
+ if (!state.botUsername) {
37
+ const me = await request("getMe", {});
38
+ state.botUsername = typeof me?.username === "string" ? me.username : undefined;
39
+ }
40
+ const updates = await request("getUpdates", {
41
+ timeout: 0,
42
+ ...(state.offset !== undefined ? { offset: state.offset } : {}),
43
+ });
44
+ if (!Array.isArray(updates)) {
45
+ return;
46
+ }
47
+ for (const update of updates) {
48
+ if (typeof update?.update_id === "number") {
49
+ state.offset = update.update_id + 1;
50
+ }
51
+ const incoming = normalizeTelegramUpdate(update, {
52
+ botUsername: state.botUsername,
53
+ allowedChatIds: options.allowedChatIds ?? [],
54
+ });
55
+ if (!incoming) {
56
+ continue;
57
+ }
58
+ await state.ctx.onMessage(incoming);
59
+ }
60
+ } catch (cause) {
61
+ state.ctx.logger.error("telegram_poll_failed", { message: safeErrorMessage(cause) });
62
+ }
63
+ };
64
+
65
+ const scheduleNextPoll = () => {
66
+ if (!state.running) {
67
+ return;
68
+ }
69
+ state.timer = setTimeout(() => {
70
+ void pollOnce().finally(scheduleNextPoll);
71
+ }, options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS);
72
+ state.timer.unref?.();
73
+ };
74
+
75
+ return {
76
+ async start(ctx) {
77
+ state.ctx = ctx;
78
+ state.running = true;
79
+ await pollOnce();
80
+ scheduleNextPoll();
81
+ },
82
+ async stop() {
83
+ state.running = false;
84
+ if (state.timer) {
85
+ clearTimeout(state.timer);
86
+ state.timer = undefined;
87
+ }
88
+ },
89
+ async sendMessage(target, message) {
90
+ const chatId = target?.data?.chatId;
91
+ if (chatId === undefined || chatId === null) {
92
+ throw new Error("telegram target is missing chatId");
93
+ }
94
+ for (const text of splitTelegramText(message.text)) {
95
+ await request("sendMessage", { chat_id: chatId, text });
96
+ }
97
+ },
98
+ async setTyping(target, typing) {
99
+ if (!typing) {
100
+ return;
101
+ }
102
+ const chatId = target?.data?.chatId;
103
+ if (chatId === undefined || chatId === null) {
104
+ return;
105
+ }
106
+ await request("sendChatAction", { chat_id: chatId, action: "typing" });
107
+ },
108
+ };
109
+ };
110
+
111
+ export const normalizeTelegramUpdate = (update, options = {}) => {
112
+ const message = update?.message;
113
+ if (!message || typeof message !== "object") {
114
+ return undefined;
115
+ }
116
+ if (typeof message.text !== "string" || message.text.trim().length === 0) {
117
+ return undefined;
118
+ }
119
+ const chat = message.chat;
120
+ if (!chat || typeof chat.id !== "number") {
121
+ return undefined;
122
+ }
123
+ const allowedChatIds = options.allowedChatIds ?? [];
124
+ if (allowedChatIds.length > 0 && !allowedChatIds.includes(String(chat.id))) {
125
+ return undefined;
126
+ }
127
+ const conversationType = telegramConversationType(chat.type);
128
+ const mentionedBot = isBotMentioned(message, options.botUsername);
129
+ if ((conversationType === "group" || conversationType === "supergroup") && !mentionedBot) {
130
+ return undefined;
131
+ }
132
+ return {
133
+ externalConversationId: `telegram:${conversationType}:${chat.id}`,
134
+ text: stripBotMention(message.text, options.botUsername),
135
+ conversationType,
136
+ senderDisplayName: senderDisplayName(message.from),
137
+ mentionedBot,
138
+ target: {
139
+ externalConversationId: `telegram:${conversationType}:${chat.id}`,
140
+ data: { chatId: chat.id },
141
+ },
142
+ data: {
143
+ messageId: message.message_id,
144
+ chatType: chat.type,
145
+ },
146
+ };
147
+ };
148
+
149
+ export const isBotMentioned = (message, botUsername) => {
150
+ const text = typeof message?.text === "string" ? message.text : "";
151
+ if (botUsername && new RegExp(`(^|\\s)@${escapeRegExp(botUsername)}\\b`, "i").test(text)) {
152
+ return true;
153
+ }
154
+ const replyUsername = message?.reply_to_message?.from?.username;
155
+ return Boolean(botUsername && typeof replyUsername === "string" && replyUsername.toLowerCase() === botUsername.toLowerCase());
156
+ };
157
+
158
+ export const splitTelegramText = (text) => {
159
+ const normalized = String(text).trim();
160
+ if (normalized.length <= TELEGRAM_MESSAGE_LIMIT) {
161
+ return [normalized];
162
+ }
163
+ const chunks = [];
164
+ for (let index = 0; index < normalized.length; index += TELEGRAM_MESSAGE_LIMIT) {
165
+ chunks.push(normalized.slice(index, index + TELEGRAM_MESSAGE_LIMIT));
166
+ }
167
+ return chunks;
168
+ };
169
+
170
+ export const parseAllowedChatIds = (value) => {
171
+ if (value === undefined || value === "") {
172
+ return [];
173
+ }
174
+ if (typeof value === "number") {
175
+ return [String(value)];
176
+ }
177
+ if (typeof value !== "string") {
178
+ throw new Error("allowedChatIds must be a comma-separated string");
179
+ }
180
+ return value.split(",").map((item) => item.trim()).filter(Boolean);
181
+ };
182
+
183
+ const telegramRequest = async (apiBaseUrl, token, method, body) => {
184
+ const response = await fetch(`${apiBaseUrl.replace(/\/+$/, "")}/bot${token}/${method}`, {
185
+ method: "POST",
186
+ headers: { "content-type": "application/json" },
187
+ body: JSON.stringify(body),
188
+ });
189
+ const payload = await response.json().catch(() => undefined);
190
+ if (!response.ok || payload?.ok !== true) {
191
+ throw new Error(`telegram ${method} failed: ${payload?.description ?? response.status}`);
192
+ }
193
+ return payload.result;
194
+ };
195
+
196
+ const telegramConversationType = (type) => {
197
+ if (type === "group" || type === "supergroup" || type === "private") {
198
+ return type;
199
+ }
200
+ return "group";
201
+ };
202
+
203
+ const senderDisplayName = (from) => {
204
+ if (!from || typeof from !== "object") {
205
+ return undefined;
206
+ }
207
+ return [from.first_name, from.last_name].filter((part) => typeof part === "string" && part.trim()).join(" ") ||
208
+ (typeof from.username === "string" ? from.username : undefined);
209
+ };
210
+
211
+ const stripBotMention = (text, botUsername) => {
212
+ if (!botUsername) {
213
+ return text.trim();
214
+ }
215
+ return text.replace(new RegExp(`(^|\\s)@${escapeRegExp(botUsername)}\\b`, "i"), " ").trim();
216
+ };
217
+
218
+ const stringConfig = (value, fallback) => {
219
+ if (value === undefined || value === "") {
220
+ return fallback;
221
+ }
222
+ if (typeof value !== "string") {
223
+ throw new Error("Telegram config value must be a string");
224
+ }
225
+ return value;
226
+ };
227
+
228
+ const optionalStringConfig = (value, name) => {
229
+ if (value === undefined || value === "") {
230
+ return undefined;
231
+ }
232
+ if (typeof value !== "string") {
233
+ throw new Error(`${name} must be a string`);
234
+ }
235
+ return value;
236
+ };
237
+
238
+ const numberConfig = (value, fallback) => {
239
+ if (value === undefined) {
240
+ return fallback;
241
+ }
242
+ if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
243
+ throw new Error("Telegram numeric config value must be non-negative");
244
+ }
245
+ return value;
246
+ };
247
+
248
+ const safeErrorMessage = (cause) => cause instanceof Error ? redactToken(cause.message) : redactToken(String(cause));
249
+
250
+ export const redactToken = (value) => value.replace(/bot[0-9]+:[A-Za-z0-9_-]+/g, "bot[REDACTED]");
251
+
252
+ const escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
@@ -0,0 +1,7 @@
1
+ {
2
+ "id": "telegram",
3
+ "kind": "im",
4
+ "displayName": "Telegram",
5
+ "adapter": "./adapter.js",
6
+ "skills": ["./skills"]
7
+ }
@@ -0,0 +1,9 @@
1
+ ---
2
+ description: Reply to the current Telegram conversation through SendChannelMessage.
3
+ ---
4
+
5
+ # Telegram
6
+
7
+ When a message comes from Telegram, use `SendChannelMessage` to reply to the current chat when a response is needed.
8
+
9
+ In groups, assume the user mentioned or replied to the bot before the message reached Scorel. Keep replies concise and avoid exposing raw chat ids, user ids, bot tokens, or internal routing details.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chanlerdev/scorel",
3
- "version": "0.0.1",
3
+ "version": "0.0.2",
4
4
  "description": "Replayable, recoverable, remotely controllable AI Agent workspace.",
5
5
  "type": "module",
6
6
  "packageManager": "pnpm@11.1.2",
@@ -9,6 +9,7 @@
9
9
  },
10
10
  "files": [
11
11
  "dist",
12
+ "extensions",
12
13
  "README.md",
13
14
  "docs/SHIP.md",
14
15
  "docs/ROADMAP.md",
@@ -31,11 +32,14 @@
31
32
  "check": "pnpm typecheck && pnpm test",
32
33
  "pack:smoke": "node scripts/pack-smoke.mjs",
33
34
  "release": "node scripts/release.mjs",
35
+ "release-notes": "node scripts/release-notes.mjs",
34
36
  "typecheck": "pnpm -r typecheck",
35
37
  "test": "pnpm -r test",
36
38
  "verify:m8-relay": "node --import tsx scripts/verify-m8-relay-e2e.ts",
39
+ "verify:m9-gui": "node --import tsx scripts/verify-m9-gui-e2e.ts",
37
40
  "scorel": "node --import tsx apps/cli/src/index.ts",
38
- "dev": "node --import tsx apps/cli/src/index.ts up"
41
+ "dev": "node --import tsx apps/cli/src/index.ts up",
42
+ "gui": "pnpm --filter @scorel/app-gui dev"
39
43
  },
40
44
  "devDependencies": {
41
45
  "@types/node": "^24.10.1",