paperclip-plugin-telegram 0.2.1 → 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 (61) hide show
  1. package/dist/acp-bridge.d.ts +34 -0
  2. package/dist/acp-bridge.js +805 -0
  3. package/dist/acp-bridge.js.map +1 -0
  4. package/dist/adapter.d.ts +35 -0
  5. package/dist/adapter.js +75 -0
  6. package/dist/adapter.js.map +1 -0
  7. package/dist/command-registry.d.ts +3 -0
  8. package/dist/command-registry.js +273 -0
  9. package/dist/command-registry.js.map +1 -0
  10. package/dist/commands.d.ts +10 -0
  11. package/dist/commands.js +213 -0
  12. package/dist/commands.js.map +1 -0
  13. package/dist/constants.d.ts +44 -0
  14. package/dist/constants.js +48 -0
  15. package/dist/constants.js.map +1 -0
  16. package/dist/escalation.d.ts +41 -0
  17. package/dist/escalation.js +254 -0
  18. package/dist/escalation.js.map +1 -0
  19. package/dist/formatters.d.ts +13 -0
  20. package/dist/formatters.js +130 -0
  21. package/dist/formatters.js.map +1 -0
  22. package/dist/index.js +4 -0
  23. package/dist/index.js.map +1 -0
  24. package/dist/manifest.d.ts +3 -0
  25. package/dist/manifest.js +230 -0
  26. package/dist/manifest.js.map +1 -0
  27. package/dist/media-pipeline.d.ts +46 -0
  28. package/dist/media-pipeline.js +161 -0
  29. package/dist/media-pipeline.js.map +1 -0
  30. package/dist/telegram-api.d.ts +28 -0
  31. package/dist/telegram-api.js +147 -0
  32. package/dist/telegram-api.js.map +1 -0
  33. package/dist/watch-registry.d.ts +9 -0
  34. package/dist/watch-registry.js +272 -0
  35. package/dist/watch-registry.js.map +1 -0
  36. package/dist/worker.d.ts +1 -0
  37. package/dist/worker.js +548 -0
  38. package/dist/worker.js.map +1 -0
  39. package/package.json +7 -3
  40. package/src/acp-bridge.ts +0 -1273
  41. package/src/adapter.ts +0 -129
  42. package/src/command-registry.ts +0 -482
  43. package/src/commands.ts +0 -346
  44. package/src/constants.ts +0 -51
  45. package/src/escalation.ts +0 -421
  46. package/src/formatters.ts +0 -148
  47. package/src/manifest.ts +0 -246
  48. package/src/media-pipeline.ts +0 -234
  49. package/src/telegram-api.ts +0 -202
  50. package/src/watch-registry.ts +0 -369
  51. package/src/worker.ts +0 -783
  52. package/tests/acp-bridge.test.ts +0 -314
  53. package/tests/command-registry.test.ts +0 -283
  54. package/tests/commands.test.ts +0 -213
  55. package/tests/escalation.test.ts +0 -550
  56. package/tests/formatters.test.ts +0 -185
  57. package/tests/media-pipeline.test.ts +0 -324
  58. package/tests/telegram-api.test.ts +0 -108
  59. package/tests/watch-registry.test.ts +0 -404
  60. package/tsconfig.json +0 -16
  61. /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
- }