paperclip-plugin-telegram 0.2.0 → 0.2.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 (62) hide show
  1. package/README.md +68 -4
  2. package/dist/acp-bridge.d.ts +34 -0
  3. package/dist/acp-bridge.js +805 -0
  4. package/dist/acp-bridge.js.map +1 -0
  5. package/dist/adapter.d.ts +35 -0
  6. package/dist/adapter.js +75 -0
  7. package/dist/adapter.js.map +1 -0
  8. package/dist/command-registry.d.ts +3 -0
  9. package/dist/command-registry.js +273 -0
  10. package/dist/command-registry.js.map +1 -0
  11. package/dist/commands.d.ts +10 -0
  12. package/dist/commands.js +213 -0
  13. package/dist/commands.js.map +1 -0
  14. package/dist/constants.d.ts +44 -0
  15. package/dist/constants.js +48 -0
  16. package/dist/constants.js.map +1 -0
  17. package/dist/escalation.d.ts +41 -0
  18. package/dist/escalation.js +254 -0
  19. package/dist/escalation.js.map +1 -0
  20. package/dist/formatters.d.ts +13 -0
  21. package/dist/formatters.js +130 -0
  22. package/dist/formatters.js.map +1 -0
  23. package/dist/index.js +4 -0
  24. package/dist/index.js.map +1 -0
  25. package/dist/manifest.d.ts +3 -0
  26. package/dist/manifest.js +230 -0
  27. package/dist/manifest.js.map +1 -0
  28. package/dist/media-pipeline.d.ts +46 -0
  29. package/dist/media-pipeline.js +161 -0
  30. package/dist/media-pipeline.js.map +1 -0
  31. package/dist/telegram-api.d.ts +28 -0
  32. package/dist/telegram-api.js +147 -0
  33. package/dist/telegram-api.js.map +1 -0
  34. package/dist/watch-registry.d.ts +9 -0
  35. package/dist/watch-registry.js +272 -0
  36. package/dist/watch-registry.js.map +1 -0
  37. package/dist/worker.d.ts +1 -0
  38. package/dist/worker.js +548 -0
  39. package/dist/worker.js.map +1 -0
  40. package/package.json +7 -3
  41. package/src/acp-bridge.ts +0 -1273
  42. package/src/adapter.ts +0 -129
  43. package/src/command-registry.ts +0 -482
  44. package/src/commands.ts +0 -346
  45. package/src/constants.ts +0 -51
  46. package/src/escalation.ts +0 -421
  47. package/src/formatters.ts +0 -148
  48. package/src/manifest.ts +0 -246
  49. package/src/media-pipeline.ts +0 -234
  50. package/src/telegram-api.ts +0 -202
  51. package/src/watch-registry.ts +0 -369
  52. package/src/worker.ts +0 -783
  53. package/tests/acp-bridge.test.ts +0 -314
  54. package/tests/command-registry.test.ts +0 -283
  55. package/tests/commands.test.ts +0 -213
  56. package/tests/escalation.test.ts +0 -550
  57. package/tests/formatters.test.ts +0 -185
  58. package/tests/media-pipeline.test.ts +0 -324
  59. package/tests/telegram-api.test.ts +0 -108
  60. package/tests/watch-registry.test.ts +0 -404
  61. package/tsconfig.json +0 -16
  62. /package/{src/index.ts → dist/index.d.ts} +0 -0
package/src/adapter.ts DELETED
@@ -1,129 +0,0 @@
1
- import type { PluginContext } from "@paperclipai/plugin-sdk";
2
- import {
3
- sendMessage,
4
- editMessage as editTelegramMessage,
5
- escapeMarkdownV2,
6
- } from "./telegram-api.js";
7
- import type { SendMessageOptions } from "./telegram-api.js";
8
-
9
- export type MessageRef = {
10
- chatId: string;
11
- threadId: string;
12
- messageId: string;
13
- };
14
-
15
- export type ActionButton = {
16
- label: string;
17
- callbackData: string;
18
- };
19
-
20
- export type SendOpts = {
21
- replyTo?: string;
22
- silent?: boolean;
23
- };
24
-
25
- export interface PlatformAdapter {
26
- platformId: string;
27
- sendText(chatId: string, threadId: string | undefined, text: string, opts?: SendOpts): Promise<MessageRef>;
28
- sendButtons(chatId: string, threadId: string | undefined, text: string, buttons: ActionButton[]): Promise<MessageRef>;
29
- editMessage(ref: MessageRef, text: string, buttons?: ActionButton[]): Promise<void>;
30
- formatAgentLabel(agentName: string): string;
31
- formatMention(userId: string): string;
32
- formatCodeBlock(code: string, lang?: string): string;
33
- }
34
-
35
- export class TelegramAdapter implements PlatformAdapter {
36
- platformId = "telegram" as const;
37
-
38
- constructor(
39
- private ctx: PluginContext,
40
- private botToken: string,
41
- ) {}
42
-
43
- async sendText(
44
- chatId: string,
45
- threadId: string | undefined,
46
- text: string,
47
- opts?: SendOpts,
48
- ): Promise<MessageRef> {
49
- const options: SendMessageOptions = {
50
- parseMode: "MarkdownV2",
51
- };
52
- if (threadId) options.messageThreadId = Number(threadId);
53
- if (opts?.replyTo) options.replyToMessageId = Number(opts.replyTo);
54
- if (opts?.silent) options.disableNotification = true;
55
-
56
- const messageId = await sendMessage(this.ctx, this.botToken, chatId, text, options);
57
- return {
58
- chatId,
59
- threadId: threadId || "",
60
- messageId: String(messageId ?? ""),
61
- };
62
- }
63
-
64
- async sendButtons(
65
- chatId: string,
66
- threadId: string | undefined,
67
- text: string,
68
- buttons: ActionButton[],
69
- ): Promise<MessageRef> {
70
- const keyboard = [];
71
- for (let i = 0; i < buttons.length; i += 2) {
72
- const row = buttons.slice(i, i + 2).map((b) => ({
73
- text: b.label,
74
- callback_data: b.callbackData,
75
- }));
76
- keyboard.push(row);
77
- }
78
-
79
- const options: SendMessageOptions = {
80
- parseMode: "MarkdownV2",
81
- inlineKeyboard: keyboard,
82
- };
83
- if (threadId) options.messageThreadId = Number(threadId);
84
-
85
- const messageId = await sendMessage(this.ctx, this.botToken, chatId, text, options);
86
- return {
87
- chatId,
88
- threadId: threadId || "",
89
- messageId: String(messageId ?? ""),
90
- };
91
- }
92
-
93
- async editMessage(ref: MessageRef, text: string, buttons?: ActionButton[]): Promise<void> {
94
- const keyboard = buttons
95
- ? (() => {
96
- const rows = [];
97
- for (let i = 0; i < buttons.length; i += 2) {
98
- const row = buttons.slice(i, i + 2).map((b) => ({
99
- text: b.label,
100
- callback_data: b.callbackData,
101
- }));
102
- rows.push(row);
103
- }
104
- return rows;
105
- })()
106
- : undefined;
107
-
108
- await editTelegramMessage(
109
- this.ctx,
110
- this.botToken,
111
- ref.chatId,
112
- Number(ref.messageId),
113
- text,
114
- { parseMode: "MarkdownV2", inlineKeyboard: keyboard },
115
- );
116
- }
117
-
118
- formatAgentLabel(agentName: string): string {
119
- return `*\\[${escapeMarkdownV2(agentName)}\\]*`;
120
- }
121
-
122
- formatMention(userId: string): string {
123
- return `@${escapeMarkdownV2(userId)}`;
124
- }
125
-
126
- formatCodeBlock(code: string, lang?: string): string {
127
- return lang ? `\`\`\`${lang}\n${code}\n\`\`\`` : `\`\`\`\n${code}\n\`\`\``;
128
- }
129
- }
@@ -1,482 +0,0 @@
1
- import type { PluginContext } from "@paperclipai/plugin-sdk";
2
- import { sendMessage, escapeMarkdownV2, sendChatAction } from "./telegram-api.js";
3
- import { METRIC_NAMES } from "./constants.js";
4
-
5
- // --- Types ---
6
-
7
- type WorkflowStepBase = {
8
- id: string;
9
- name?: string;
10
- };
11
-
12
- type FetchIssueStep = WorkflowStepBase & {
13
- type: "fetch_issue";
14
- issueId: string; // supports {{arg1}} template
15
- };
16
-
17
- type InvokeAgentStep = WorkflowStepBase & {
18
- type: "invoke_agent";
19
- agentId: string;
20
- prompt: string; // supports {{prev.result}}, {{arg1}} etc.
21
- };
22
-
23
- type HttpRequestStep = WorkflowStepBase & {
24
- type: "http_request";
25
- url: string;
26
- method: "GET" | "POST" | "PUT" | "DELETE";
27
- headers?: Record<string, string>;
28
- body?: string;
29
- };
30
-
31
- type SendMessageStep = WorkflowStepBase & {
32
- type: "send_message";
33
- text: string;
34
- };
35
-
36
- type CreateIssueStep = WorkflowStepBase & {
37
- type: "create_issue";
38
- title: string;
39
- description?: string;
40
- projectId?: string;
41
- };
42
-
43
- type WaitApprovalStep = WorkflowStepBase & {
44
- type: "wait_approval";
45
- prompt: string;
46
- timeoutMs?: number;
47
- };
48
-
49
- type SetStateStep = WorkflowStepBase & {
50
- type: "set_state";
51
- key: string;
52
- value: string;
53
- };
54
-
55
- type WorkflowStep =
56
- | FetchIssueStep
57
- | InvokeAgentStep
58
- | HttpRequestStep
59
- | SendMessageStep
60
- | CreateIssueStep
61
- | WaitApprovalStep
62
- | SetStateStep;
63
-
64
- type CustomCommand = {
65
- name: string;
66
- description: string;
67
- steps: WorkflowStep[];
68
- createdBy: string;
69
- createdAt: string;
70
- };
71
-
72
- type StepResult = {
73
- stepId: string;
74
- result: string;
75
- data?: unknown;
76
- };
77
-
78
- // --- Built-in commands ---
79
-
80
- const BUILTIN_COMMANDS = new Set([
81
- "status", "issues", "agents", "approve", "help",
82
- "connect", "connect-topic", "acp", "commands",
83
- ]);
84
-
85
- // --- Command registry ---
86
-
87
- export async function handleCommandsCommand(
88
- ctx: PluginContext,
89
- token: string,
90
- chatId: string,
91
- args: string,
92
- messageThreadId?: number,
93
- companyId?: string,
94
- ): Promise<void> {
95
- const parts = args.trim().split(/\s+/);
96
- const subcommand = parts[0]?.toLowerCase() ?? "";
97
-
98
- switch (subcommand) {
99
- case "list":
100
- await listCommands(ctx, token, chatId, messageThreadId, companyId);
101
- break;
102
- case "import":
103
- await importCommand(ctx, token, chatId, parts.slice(1).join(" "), messageThreadId, companyId);
104
- break;
105
- case "delete":
106
- await deleteCommand(ctx, token, chatId, parts[1] ?? "", messageThreadId, companyId);
107
- break;
108
- case "run":
109
- await runCommand(ctx, token, chatId, parts[1] ?? "", parts.slice(2), messageThreadId, companyId);
110
- break;
111
- default:
112
- await sendMessage(ctx, token, chatId, [
113
- escapeMarkdownV2("\ud83d\udee0\ufe0f") + " *Custom Commands*",
114
- "",
115
- `/commands list \\- ${escapeMarkdownV2("Show all custom commands")}`,
116
- `/commands import <json> \\- ${escapeMarkdownV2("Import a workflow command")}`,
117
- `/commands delete <name> \\- ${escapeMarkdownV2("Remove a custom command")}`,
118
- `/commands run <name> [args] \\- ${escapeMarkdownV2("Execute a custom command")}`,
119
- ].join("\n"), { parseMode: "MarkdownV2", messageThreadId });
120
- }
121
- }
122
-
123
- // Check if a command is custom and run it, returns true if handled
124
- export async function tryCustomCommand(
125
- ctx: PluginContext,
126
- token: string,
127
- chatId: string,
128
- command: string,
129
- argsStr: string,
130
- messageThreadId?: number,
131
- companyId?: string,
132
- ): Promise<boolean> {
133
- if (BUILTIN_COMMANDS.has(command)) return false;
134
-
135
- const resolvedCompanyId = companyId ?? chatId;
136
- const commands = await getCommandRegistry(ctx, resolvedCompanyId);
137
- const cmd = commands.find((c) => c.name === command);
138
-
139
- if (!cmd) return false;
140
-
141
- const args = argsStr.trim().split(/\s+/).filter(Boolean);
142
- await executeWorkflow(ctx, token, chatId, cmd, args, messageThreadId, resolvedCompanyId);
143
- return true;
144
- }
145
-
146
- async function listCommands(
147
- ctx: PluginContext,
148
- token: string,
149
- chatId: string,
150
- messageThreadId?: number,
151
- companyId?: string,
152
- ): Promise<void> {
153
- const resolvedCompanyId = companyId ?? chatId;
154
- const commands = await getCommandRegistry(ctx, resolvedCompanyId);
155
-
156
- if (commands.length === 0) {
157
- await sendMessage(ctx, token, chatId, "No custom commands registered. Use /commands import to add one.", { messageThreadId });
158
- return;
159
- }
160
-
161
- const lines = [
162
- escapeMarkdownV2("\ud83d\udee0\ufe0f") + " *Custom Commands*",
163
- "",
164
- ];
165
-
166
- for (const cmd of commands) {
167
- lines.push(`/${escapeMarkdownV2(cmd.name)} \\- ${escapeMarkdownV2(cmd.description)}`);
168
- lines.push(` Steps: ${escapeMarkdownV2(String(cmd.steps.length))} \\| Created: ${escapeMarkdownV2(cmd.createdAt.split("T")[0] ?? cmd.createdAt)}`);
169
- }
170
-
171
- await sendMessage(ctx, token, chatId, lines.join("\n"), {
172
- parseMode: "MarkdownV2",
173
- messageThreadId,
174
- });
175
- }
176
-
177
- async function importCommand(
178
- ctx: PluginContext,
179
- token: string,
180
- chatId: string,
181
- jsonStr: string,
182
- messageThreadId?: number,
183
- companyId?: string,
184
- ): Promise<void> {
185
- if (!jsonStr.trim()) {
186
- await sendMessage(ctx, token, chatId, "Usage: /commands import <json-definition>", { messageThreadId });
187
- return;
188
- }
189
-
190
- let definition: { name: string; description: string; steps: WorkflowStep[] };
191
- try {
192
- definition = JSON.parse(jsonStr);
193
- } catch {
194
- await sendMessage(ctx, token, chatId, "Invalid JSON. Please provide a valid command definition.", { messageThreadId });
195
- return;
196
- }
197
-
198
- if (!definition.name || !definition.steps || !Array.isArray(definition.steps)) {
199
- await sendMessage(ctx, token, chatId, "Command definition must have 'name' and 'steps' fields.", { messageThreadId });
200
- return;
201
- }
202
-
203
- if (BUILTIN_COMMANDS.has(definition.name)) {
204
- await sendMessage(ctx, token, chatId, `Cannot override built-in command: /${definition.name}`, { messageThreadId });
205
- return;
206
- }
207
-
208
- // Validate steps
209
- for (const step of definition.steps) {
210
- if (!step.type || !step.id) {
211
- await sendMessage(ctx, token, chatId, "Each step must have 'type' and 'id' fields.", { messageThreadId });
212
- return;
213
- }
214
- const validTypes = ["fetch_issue", "invoke_agent", "http_request", "send_message", "create_issue", "wait_approval", "set_state"];
215
- if (!validTypes.includes(step.type)) {
216
- await sendMessage(ctx, token, chatId, `Invalid step type: ${step.type}. Valid: ${validTypes.join(", ")}`, { messageThreadId });
217
- return;
218
- }
219
- }
220
-
221
- const resolvedCompanyId = companyId ?? chatId;
222
- const commands = await getCommandRegistry(ctx, resolvedCompanyId);
223
-
224
- // Replace existing or add new
225
- const existingIdx = commands.findIndex((c) => c.name === definition.name);
226
- const newCmd: CustomCommand = {
227
- name: definition.name,
228
- description: definition.description ?? "No description",
229
- steps: definition.steps,
230
- createdBy: `telegram:${chatId}`,
231
- createdAt: new Date().toISOString(),
232
- };
233
-
234
- if (existingIdx >= 0) {
235
- commands[existingIdx] = newCmd;
236
- } else {
237
- commands.push(newCmd);
238
- }
239
-
240
- await saveCommandRegistry(ctx, resolvedCompanyId, commands);
241
-
242
- await sendMessage(
243
- ctx,
244
- token,
245
- chatId,
246
- `${escapeMarkdownV2("\u2705")} Command /${escapeMarkdownV2(definition.name)} ${existingIdx >= 0 ? "updated" : "imported"} \\(${escapeMarkdownV2(String(definition.steps.length))} steps\\)`,
247
- { parseMode: "MarkdownV2", messageThreadId },
248
- );
249
- }
250
-
251
- async function deleteCommand(
252
- ctx: PluginContext,
253
- token: string,
254
- chatId: string,
255
- name: string,
256
- messageThreadId?: number,
257
- companyId?: string,
258
- ): Promise<void> {
259
- if (!name.trim()) {
260
- await sendMessage(ctx, token, chatId, "Usage: /commands delete <name>", { messageThreadId });
261
- return;
262
- }
263
-
264
- const resolvedCompanyId = companyId ?? chatId;
265
- const commands = await getCommandRegistry(ctx, resolvedCompanyId);
266
- const filtered = commands.filter((c) => c.name !== name);
267
-
268
- if (filtered.length === commands.length) {
269
- await sendMessage(ctx, token, chatId, `Command /${name} not found.`, { messageThreadId });
270
- return;
271
- }
272
-
273
- await saveCommandRegistry(ctx, resolvedCompanyId, filtered);
274
-
275
- await sendMessage(
276
- ctx,
277
- token,
278
- chatId,
279
- `${escapeMarkdownV2("\ud83d\uddd1\ufe0f")} Command /${escapeMarkdownV2(name)} deleted.`,
280
- { parseMode: "MarkdownV2", messageThreadId },
281
- );
282
- }
283
-
284
- async function runCommand(
285
- ctx: PluginContext,
286
- token: string,
287
- chatId: string,
288
- name: string,
289
- args: string[],
290
- messageThreadId?: number,
291
- companyId?: string,
292
- ): Promise<void> {
293
- const resolvedCompanyId = companyId ?? chatId;
294
- const commands = await getCommandRegistry(ctx, resolvedCompanyId);
295
- const cmd = commands.find((c) => c.name === name);
296
-
297
- if (!cmd) {
298
- await sendMessage(ctx, token, chatId, `Command /${name} not found.`, { messageThreadId });
299
- return;
300
- }
301
-
302
- await executeWorkflow(ctx, token, chatId, cmd, args, messageThreadId, resolvedCompanyId);
303
- }
304
-
305
- // --- Workflow executor ---
306
-
307
- async function executeWorkflow(
308
- ctx: PluginContext,
309
- token: string,
310
- chatId: string,
311
- cmd: CustomCommand,
312
- args: string[],
313
- messageThreadId: number | undefined,
314
- companyId: string,
315
- ): Promise<void> {
316
- await sendChatAction(ctx, token, chatId);
317
- await ctx.metrics.write(METRIC_NAMES.commandsExecuted, 1);
318
-
319
- const results: StepResult[] = [];
320
-
321
- for (const step of cmd.steps) {
322
- try {
323
- const result = await executeStep(ctx, token, chatId, step, args, results, messageThreadId, companyId);
324
- results.push({ stepId: step.id, result: result ?? "" });
325
- } catch (err) {
326
- ctx.logger.error("Workflow step failed", { command: cmd.name, stepId: step.id, error: String(err) });
327
- await sendMessage(
328
- ctx,
329
- token,
330
- chatId,
331
- `Step "${step.name ?? step.id}" failed: ${String(err)}`,
332
- { messageThreadId },
333
- );
334
- return; // Stop execution on failure
335
- }
336
- }
337
-
338
- ctx.logger.info("Workflow completed", { command: cmd.name, steps: results.length });
339
- }
340
-
341
- async function executeStep(
342
- ctx: PluginContext,
343
- token: string,
344
- chatId: string,
345
- step: WorkflowStep,
346
- args: string[],
347
- prevResults: StepResult[],
348
- messageThreadId: number | undefined,
349
- companyId: string,
350
- ): Promise<string | null> {
351
- const interpolate = (template: string): string => {
352
- let result = template;
353
- // Replace {{arg0}}, {{arg1}}, etc.
354
- for (let i = 0; i < args.length; i++) {
355
- result = result.replace(new RegExp(`\\{\\{arg${i}\\}\\}`, "g"), args[i]!);
356
- }
357
- result = result.replace(/\{\{args\}\}/g, args.join(" "));
358
- // Replace {{prev.result}}, {{step_id.result}}
359
- if (prevResults.length > 0) {
360
- const lastResult = prevResults[prevResults.length - 1]!;
361
- result = result.replace(/\{\{prev\.result\}\}/g, lastResult.result);
362
- }
363
- for (const prev of prevResults) {
364
- result = result.replace(new RegExp(`\\{\\{${prev.stepId}\\.result\\}\\}`, "g"), prev.result);
365
- }
366
- return result;
367
- };
368
-
369
- switch (step.type) {
370
- case "fetch_issue": {
371
- const issueId = interpolate(step.issueId);
372
- const issue = await ctx.issues.get(issueId, companyId);
373
- if (!issue) return JSON.stringify({ error: "Issue not found", issueId });
374
- return JSON.stringify({ id: issue.id, title: issue.title, status: issue.status });
375
- }
376
-
377
- case "invoke_agent": {
378
- const prompt = interpolate(step.prompt);
379
- const { runId } = await ctx.agents.invoke(step.agentId, companyId, {
380
- prompt,
381
- reason: `custom_command:${step.id}`,
382
- });
383
- return runId;
384
- }
385
-
386
- case "http_request": {
387
- const url = interpolate(step.url);
388
- const body = step.body ? interpolate(step.body) : undefined;
389
- const res = await ctx.http.fetch(url, {
390
- method: step.method,
391
- headers: step.headers ? Object.fromEntries(
392
- Object.entries(step.headers).map(([k, v]) => [k, interpolate(v)]),
393
- ) : undefined,
394
- body,
395
- });
396
- const data = await res.text();
397
- return data;
398
- }
399
-
400
- case "send_message": {
401
- const text = interpolate(step.text);
402
- await sendMessage(ctx, token, chatId, text, { messageThreadId });
403
- return "sent";
404
- }
405
-
406
- case "create_issue": {
407
- const title = interpolate(step.title);
408
- const description = step.description ? interpolate(step.description) : undefined;
409
- const res = await ctx.http.fetch(
410
- `${await resolveBaseUrl(ctx)}/api/issues`,
411
- {
412
- method: "POST",
413
- headers: { "Content-Type": "application/json" },
414
- body: JSON.stringify({
415
- companyId,
416
- title,
417
- description,
418
- projectId: step.projectId,
419
- }),
420
- },
421
- );
422
- const data = (await res.json()) as { id: string };
423
- return data.id;
424
- }
425
-
426
- case "wait_approval": {
427
- const prompt = interpolate(step.prompt);
428
- const approvalId = `cmd_approval_${Date.now()}`;
429
- await sendMessage(ctx, token, chatId, prompt, {
430
- messageThreadId,
431
- inlineKeyboard: [
432
- [
433
- { text: "Approve", callback_data: `cmd_approve_${approvalId}` },
434
- { text: "Reject", callback_data: `cmd_reject_${approvalId}` },
435
- ],
436
- ],
437
- });
438
- // Store approval state - workflow will be continued by callback handler
439
- await ctx.state.set(
440
- { scopeKind: "instance", stateKey: `cmd_approval_${approvalId}` },
441
- { status: "pending", createdAt: Date.now() },
442
- );
443
- return "awaiting_approval";
444
- }
445
-
446
- case "set_state": {
447
- const key = interpolate(step.key);
448
- const value = interpolate(step.value);
449
- await ctx.state.set(
450
- { scopeKind: "company", scopeId: companyId, stateKey: key },
451
- value,
452
- );
453
- return value;
454
- }
455
-
456
- default:
457
- return null;
458
- }
459
- }
460
-
461
- // --- State helpers ---
462
-
463
- async function getCommandRegistry(ctx: PluginContext, companyId: string): Promise<CustomCommand[]> {
464
- const commands = await ctx.state.get({
465
- scopeKind: "company",
466
- scopeId: companyId,
467
- stateKey: `commands_${companyId}`,
468
- }) as CustomCommand[] | null;
469
- return commands ?? [];
470
- }
471
-
472
- async function saveCommandRegistry(ctx: PluginContext, companyId: string, commands: CustomCommand[]): Promise<void> {
473
- await ctx.state.set(
474
- { scopeKind: "company", scopeId: companyId, stateKey: `commands_${companyId}` },
475
- commands,
476
- );
477
- }
478
-
479
- async function resolveBaseUrl(ctx: PluginContext): Promise<string> {
480
- const config = await ctx.config.get() as { paperclipBaseUrl?: string };
481
- return config.paperclipBaseUrl ?? "http://localhost:3100";
482
- }