pi-harness-runtime 1.1.332 → 1.1.334

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 (126) hide show
  1. package/README.md +2 -2
  2. package/RULES.md +45 -0
  3. package/harness/skill-sync.ts +11 -2
  4. package/harness/wiki-integration.ts +6 -6
  5. package/package.json +3 -1
  6. package/packages/a2a-adapter/package.json +1 -1
  7. package/packages/agent-policy/package.json +1 -1
  8. package/packages/architecture-generator/package.json +1 -1
  9. package/packages/auth/package.json +1 -1
  10. package/packages/autonomous-refactor/package.json +1 -1
  11. package/packages/autonomous-runtime/package.json +1 -1
  12. package/packages/cache-strategy/package.json +1 -1
  13. package/packages/capability-registry/package.json +1 -1
  14. package/packages/checkpoint/package.json +1 -1
  15. package/packages/cli-plugin-sdk/package.json +1 -1
  16. package/packages/clipboard/package.json +1 -1
  17. package/packages/clipboard-plugin/package.json +1 -1
  18. package/packages/code-generation/package.json +1 -1
  19. package/packages/code-review/package.json +1 -1
  20. package/packages/codex-adapter/package.json +1 -1
  21. package/packages/codex-watcher/package.json +1 -1
  22. package/packages/config-capture/package.json +1 -1
  23. package/packages/context-compiler/package.json +1 -1
  24. package/packages/context-discovery/package.json +1 -1
  25. package/packages/context-manager/package.json +1 -1
  26. package/packages/cookie-sanitizer/package.json +1 -1
  27. package/packages/cost-optimizer/package.json +1 -1
  28. package/packages/dependency-analyzer/package.json +1 -1
  29. package/packages/django-plugin/package.json +1 -1
  30. package/packages/doc-generator/package.json +1 -1
  31. package/packages/durable-review/package.json +1 -1
  32. package/packages/e2e-tester/package.json +1 -1
  33. package/packages/evaluation-engine/package.json +1 -1
  34. package/packages/evaluation-runner/package.json +1 -1
  35. package/packages/event-bus/package.json +1 -1
  36. package/packages/event-store/package.json +1 -1
  37. package/packages/experience-replay/package.json +1 -1
  38. package/packages/feedback-collector/package.json +1 -1
  39. package/packages/file-copy-helper/package.json +1 -1
  40. package/packages/framework-detector/package.json +1 -1
  41. package/packages/framework-plugin-sdk/package.json +1 -1
  42. package/packages/frappe-plugin/package.json +1 -1
  43. package/packages/generic-web-plugin/package.json +1 -1
  44. package/packages/health-monitor/package.json +1 -1
  45. package/packages/intent-analyzer/package.json +1 -1
  46. package/packages/jev-judge/package.json +1 -1
  47. package/packages/knowledge-graph/package.json +1 -1
  48. package/packages/knowledge-retrieval/package.json +1 -1
  49. package/packages/laravel-plugin/package.json +1 -1
  50. package/packages/learning-engine/package.json +1 -1
  51. package/packages/mcp-adapter/package.json +1 -1
  52. package/packages/memory-engine/package.json +1 -1
  53. package/packages/milestone-manager/package.json +1 -1
  54. package/packages/model-registry/package.json +1 -1
  55. package/packages/nextjs-plugin/package.json +1 -1
  56. package/packages/notification/package.json +1 -1
  57. package/packages/observability/package.json +1 -1
  58. package/packages/okf-indexer/package.json +1 -1
  59. package/packages/paperclip-integration/dist/index.js +1 -1
  60. package/packages/paperclip-integration/dist/index.js.map +1 -1
  61. package/packages/paperclip-integration/package.json +1 -1
  62. package/packages/paperclip-integration/src/index.ts +1 -1
  63. package/packages/performance-optimizer/package.json +1 -1
  64. package/packages/privilege-broker/package.json +1 -1
  65. package/packages/project-analyzer/package.json +1 -1
  66. package/packages/project-bootstrap/package.json +1 -1
  67. package/packages/projection-engine/package.json +1 -1
  68. package/packages/prompt-compiler/package.json +1 -1
  69. package/packages/prompt-versioning/package.json +1 -1
  70. package/packages/provider-adapter-sdk/package.json +1 -1
  71. package/packages/provider-router/package.json +1 -1
  72. package/packages/provider-selector/package.json +1 -1
  73. package/packages/providers/package.json +1 -1
  74. package/packages/qdrant-skills/package.json +1 -1
  75. package/packages/quota-manager/package.json +1 -1
  76. package/packages/rate-limiter/package.json +1 -1
  77. package/packages/react-vite-plugin/package.json +1 -1
  78. package/packages/release-manager/package.json +1 -1
  79. package/packages/requirement-compiler/package.json +1 -1
  80. package/packages/runtime/package.json +1 -1
  81. package/packages/scheduler/package.json +1 -1
  82. package/packages/scheduler-adapter/package.json +1 -1
  83. package/packages/session/package.json +1 -1
  84. package/packages/session-api/package.json +1 -1
  85. package/packages/session-export/package.json +1 -1
  86. package/packages/shared-context/package.json +1 -1
  87. package/packages/skill-mcp-client/package.json +1 -1
  88. package/packages/skill-registry/package.json +1 -1
  89. package/packages/skills/package.json +1 -1
  90. package/packages/sprint-planner/package.json +1 -1
  91. package/packages/subscription-engine/package.json +1 -1
  92. package/packages/task-compiler/package.json +1 -1
  93. package/packages/telegram/package.json +1 -1
  94. package/packages/tencentdb-memory/package.json +1 -1
  95. package/packages/tencentdb-sync/package.json +1 -1
  96. package/packages/test-data-generator/package.json +1 -1
  97. package/packages/test-generator/package.json +1 -1
  98. package/packages/todo-bd-sync/package.json +1 -1
  99. package/packages/token-estimation/package.json +1 -1
  100. package/packages/token-optimizer/package.json +1 -1
  101. package/packages/trajectory/package.json +1 -1
  102. package/packages/tui/package.json +1 -1
  103. package/packages/types/package.json +1 -1
  104. package/packages/webmcp-tester/package.json +1 -1
  105. package/packages/wiki/dist/index.js +2 -2
  106. package/packages/wiki/dist/index.js.map +1 -1
  107. package/packages/wiki/dist/storage.d.ts +4 -0
  108. package/packages/wiki/dist/storage.d.ts.map +1 -1
  109. package/packages/wiki/dist/storage.js +40 -2
  110. package/packages/wiki/dist/storage.js.map +1 -1
  111. package/packages/wiki/dist/types.d.ts +1 -0
  112. package/packages/wiki/dist/types.d.ts.map +1 -1
  113. package/packages/wiki/dist/types.js.map +1 -1
  114. package/packages/wiki/package.json +1 -1
  115. package/packages/wiki/src/index.ts +2 -2
  116. package/packages/wiki/src/storage.ts +41 -2
  117. package/packages/wiki/src/types.ts +1 -0
  118. package/packages/workflow-events/package.json +1 -1
  119. package/packages/workspace-scanner/package.json +1 -1
  120. package/packages/worktree/package.json +1 -1
  121. package/packages/write-review/package.json +1 -1
  122. package/scripts/r2-storage.ts +217 -0
  123. package/scripts/telegram-mobile-bot.ts +438 -0
  124. package/scripts/telegram-mobile-integration.ts +480 -0
  125. package/scripts/test-r2.ts +63 -0
  126. package/scripts/test-telegram-approval.ts +4 -1
@@ -0,0 +1,480 @@
1
+ /**
2
+ * Telegram Mobile Bot Integration
3
+ *
4
+ * This module provides mobile-first button interactions:
5
+ * - Job cards with one-tap actions
6
+ * - Log viewing with pagination
7
+ * - Quick status overview
8
+ *
9
+ * Import this in telegram-webhook-server.ts to enable:
10
+ * import { MobileBot } from "./telegram-mobile-integration.ts";
11
+ * const mobileBot = new MobileBot();
12
+ * mobileBot.handleCallback(cq.data);
13
+ */
14
+
15
+ import { existsSync, readFileSync, writeFileSync, appendFileSync, mkdirSync } from "node:fs";
16
+ import { join, dirname } from "node:path";
17
+ import { uploadLogsToR2, isR2Configured } from "./r2-storage.js";
18
+
19
+ // ============================================================================
20
+ // Types
21
+ // ============================================================================
22
+
23
+ export interface InlineKeyboard {
24
+ inline_keyboard: Array<Array<{ text: string; callback_data: string }>>;
25
+ }
26
+
27
+ export interface JobInfo {
28
+ id: string;
29
+ title: string;
30
+ status: "pending" | "running" | "done" | "failed";
31
+ progress?: string;
32
+ updatedAt: string;
33
+ }
34
+
35
+ export interface JobDetail {
36
+ id: string;
37
+ title: string;
38
+ description?: string;
39
+ status: string;
40
+ logs?: string[];
41
+ diff?: string;
42
+ error?: string;
43
+ }
44
+
45
+ // ============================================================================
46
+ // State Management
47
+ // ============================================================================
48
+
49
+ class JobStateStore {
50
+ private stateFile: string;
51
+
52
+ constructor(baseDir: string) {
53
+ this.stateFile = join(baseDir, "telegram-jobs.json");
54
+ }
55
+
56
+ getJobs(): JobInfo[] {
57
+ try {
58
+ if (!existsSync(this.stateFile)) return [];
59
+ const data = JSON.parse(readFileSync(this.stateFile, "utf8"));
60
+ return data.jobs || [];
61
+ } catch {
62
+ return [];
63
+ }
64
+ }
65
+
66
+ getJob(id: string): JobInfo | undefined {
67
+ return this.getJobs().find(j => j.id === id);
68
+ }
69
+
70
+ updateJob(job: JobInfo): void {
71
+ const jobs = this.getJobs();
72
+ const idx = jobs.findIndex(j => j.id === job.id);
73
+ if (idx >= 0) {
74
+ jobs[idx] = job;
75
+ } else {
76
+ jobs.push(job);
77
+ }
78
+ writeFileSync(this.stateFile, JSON.stringify({ jobs }));
79
+ }
80
+
81
+ removeJob(id: string): void {
82
+ const jobs = this.getJobs().filter(j => j.id !== id);
83
+ writeFileSync(this.stateFile, JSON.stringify({ jobs }));
84
+ }
85
+ }
86
+
87
+ // ============================================================================
88
+ // Button Kits - All Actions as Buttons, Zero Typing
89
+ // ============================================================================
90
+
91
+ export class ButtonKit {
92
+ /**
93
+ * Job card with contextual actions
94
+ */
95
+ static jobCard(job: JobInfo): InlineKeyboard {
96
+ return {
97
+ inline_keyboard: [
98
+ [
99
+ { text: "šŸ“‹ Details", callback_data: `j:details:${job.id}` },
100
+ { text: "šŸ“œ Logs", callback_data: `j:logs:${job.id}` },
101
+ ],
102
+ [
103
+ { text: "šŸ”„ Retry", callback_data: `j:retry:${job.id}` },
104
+ { text: "āŒ Cancel", callback_data: `j:cancel:${job.id}` },
105
+ ]
106
+ ]
107
+ };
108
+ }
109
+
110
+ /**
111
+ * Main navigation menu
112
+ */
113
+ static mainMenu(): InlineKeyboard {
114
+ return {
115
+ inline_keyboard: [
116
+ [
117
+ { text: "šŸ“‹ My Jobs", callback_data: "m:jobs" },
118
+ { text: "šŸ” Search", callback_data: "m:search" },
119
+ ],
120
+ [
121
+ { text: "šŸ“Š Status", callback_data: "m:status" },
122
+ { text: "āš™ļø Settings", callback_data: "m:settings" },
123
+ ]
124
+ ]
125
+ };
126
+ }
127
+
128
+ /**
129
+ * Job list with quick select
130
+ */
131
+ static jobList(jobs: JobInfo[]): InlineKeyboard {
132
+ const buttons: InlineKeyboard["inline_keyboard"] = [];
133
+
134
+ for (const job of jobs.slice(0, 8)) {
135
+ const emoji = job.status === "running" ? "šŸ”„" : job.status === "done" ? "āœ…" : job.status === "failed" ? "āŒ" : "ā³";
136
+ buttons.push([{
137
+ text: `${emoji} ${job.title.slice(0, 28)}${job.title.length > 28 ? "…" : ""}`,
138
+ callback_data: `j:select:${job.id}`
139
+ }]);
140
+ }
141
+
142
+ buttons.push([{ text: "šŸ”™ Back", callback_data: "m:menu" }]);
143
+ return { inline_keyboard: buttons };
144
+ }
145
+
146
+ /**
147
+ * Confirmation buttons
148
+ */
149
+ static confirm(action: string, targetId: string): InlineKeyboard {
150
+ return {
151
+ inline_keyboard: [
152
+ [
153
+ { text: "āœ… Confirm", callback_data: `c:yes:${action}:${targetId}` },
154
+ { text: "āŒ Cancel", callback_data: `c:no:${action}:${targetId}` },
155
+ ]
156
+ ]
157
+ };
158
+ }
159
+
160
+ /**
161
+ * Pagination for logs
162
+ */
163
+ static logPagination(jobId: string, page: number, hasMore: boolean): InlineKeyboard {
164
+ const buttons: InlineKeyboard["inline_keyboard"] = [];
165
+
166
+ if (page > 0) {
167
+ buttons.push([{ text: "ā—€ļø Prev", callback_data: `j:logs:${jobId}:${page - 1}` }]);
168
+ }
169
+
170
+ if (hasMore) {
171
+ buttons.push([{ text: "ā–¶ļø Next", callback_data: `j:logs:${jobId}:${page + 1}` }]);
172
+ }
173
+
174
+ buttons.push([{ text: "šŸ”™ Back to Job", callback_data: `j:details:${jobId}` }]);
175
+ return { inline_keyboard: buttons };
176
+ }
177
+ }
178
+
179
+ // ============================================================================
180
+ // Formatters
181
+ // ============================================================================
182
+
183
+ export function formatJobCard(job: JobInfo): string {
184
+ const emoji = job.status === "running" ? "šŸ”„" : job.status === "done" ? "āœ…" : job.status === "failed" ? "āŒ" : "ā³";
185
+ const time = new Date(job.updatedAt).toLocaleTimeString();
186
+
187
+ return `<b>${emoji} ${escapeHtml(job.title)}</b>\n` +
188
+ `Status: ${job.status}${job.progress ? ` (${job.progress})` : ""}\n` +
189
+ `Updated: ${time}`;
190
+ }
191
+
192
+ export function formatJobDetail(job: JobDetail): string {
193
+ const emoji = job.status === "running" ? "šŸ”„" : job.status === "done" ? "āœ…" : job.status === "failed" ? "āŒ" : "ā³";
194
+
195
+ let text = `<b>${emoji} ${escapeHtml(job.title)}</b>\n`;
196
+ text += `─────────────────\n`;
197
+ text += `Status: <code>${job.status}</code>\n`;
198
+
199
+ if (job.description) {
200
+ text += `\n${escapeHtml(job.description.slice(0, 500))}\n`;
201
+ }
202
+
203
+ if (job.error) {
204
+ text += `\nāš ļø <b>Error:</b>\n<code>${escapeHtml(job.error.slice(0, 300))}</code>`;
205
+ }
206
+
207
+ if (job.logs && job.logs.length > 0) {
208
+ text += `\n\nšŸ“œ <b>Recent:</b>`;
209
+ for (const log of job.logs.slice(0, 3)) {
210
+ text += `\n<code>${escapeHtml(log.slice(0, 100))}</code>`;
211
+ }
212
+ }
213
+
214
+ return text;
215
+ }
216
+
217
+ function escapeHtml(text: string): string {
218
+ return text
219
+ .replace(/&/g, "&amp;")
220
+ .replace(/</g, "&lt;")
221
+ .replace(/>/g, "&gt;")
222
+ .replace(/"/g, "&quot;");
223
+ }
224
+
225
+ // ============================================================================
226
+ // Mobile Bot - Handles All Button Clicks
227
+ // ============================================================================
228
+
229
+ export class MobileBot {
230
+ private state: JobStateStore;
231
+ private botToken: string;
232
+ private chatId: string;
233
+ private jobPipeFile: string;
234
+
235
+ constructor(baseDir: string, botToken: string, chatId: string, jobPipeFile: string) {
236
+ this.state = new JobStateStore(baseDir);
237
+ this.botToken = botToken;
238
+ this.chatId = chatId;
239
+ this.jobPipeFile = jobPipeFile;
240
+ }
241
+
242
+ /**
243
+ * Handle callback query from Telegram
244
+ * Parse and execute the requested action
245
+ */
246
+ async handleCallback(data: string): Promise<string | null> {
247
+ const parts = data.split(":");
248
+ const category = parts[0];
249
+ const action = parts[1];
250
+ const id = parts[2];
251
+ const extra = parts.slice(3);
252
+
253
+ switch (category) {
254
+ // === Job Actions ===
255
+ case "j": {
256
+ return this.handleJobAction(action, id, extra[0]);
257
+ }
258
+
259
+ // === Confirm Actions ===
260
+ case "c": {
261
+ return this.handleConfirmAction(action, id);
262
+ }
263
+
264
+ // === Menu Navigation ===
265
+ case "m": {
266
+ return this.handleMenuAction(action);
267
+ }
268
+
269
+ default:
270
+ return `Unknown action: ${data}`;
271
+ }
272
+ }
273
+
274
+ private async handleJobAction(action: string, jobId: string, page?: string): Promise<string | null> {
275
+ const job = this.state.getJob(jobId);
276
+
277
+ switch (action) {
278
+ case "select":
279
+ if (job) {
280
+ const detail = await this.getJobDetail(jobId);
281
+ await this.sendMessage(formatJobDetail(detail), ButtonKit.jobCard(job));
282
+ }
283
+ break;
284
+
285
+ case "details":
286
+ if (job) {
287
+ const detail = await this.getJobDetail(jobId);
288
+ await this.sendMessage(formatJobDetail(detail), ButtonKit.jobCard(job));
289
+ }
290
+ break;
291
+
292
+ case "logs":
293
+ const pageNum = parseInt(page || "0", 10);
294
+ await this.sendJobLogs(jobId, pageNum);
295
+ break;
296
+
297
+ case "retry":
298
+ await this.sendMessage(
299
+ `šŸ”„ Retry job <code>${jobId}</code>?`,
300
+ ButtonKit.confirm("retry", jobId)
301
+ );
302
+ break;
303
+
304
+ case "cancel":
305
+ await this.sendMessage(
306
+ `āŒ Cancel job <code>${jobId}</code>?`,
307
+ ButtonKit.confirm("cancel", jobId)
308
+ );
309
+ break;
310
+ }
311
+
312
+ return null;
313
+ }
314
+
315
+ private async handleConfirmAction(action: string, targetId: string): Promise<string | null> {
316
+ // Write to job pipe for pi-harness to process
317
+ const dir = dirname(this.jobPipeFile);
318
+ if (!existsSync(dir)) {
319
+ mkdirSync(dir, { recursive: true });
320
+ }
321
+
322
+ appendFileSync(this.jobPipeFile, JSON.stringify({
323
+ command: action,
324
+ jobId: targetId,
325
+ timestamp: new Date().toISOString(),
326
+ }) + "\n");
327
+
328
+ const messages: Record<string, string> = {
329
+ retry: `šŸ”„ Retrying job <code>${targetId}</code>...`,
330
+ cancel: `āŒ Cancelled job <code>${targetId}</code>`,
331
+ };
332
+
333
+ await this.sendMessage(messages[action] || "Action completed", ButtonKit.mainMenu());
334
+ return null;
335
+ }
336
+
337
+ private async handleMenuAction(action: string): Promise<string | null> {
338
+ switch (action) {
339
+ case "menu":
340
+ await this.sendMessage(
341
+ "šŸ  <b>pi-harness</b>\n\nSelect an action:",
342
+ ButtonKit.mainMenu()
343
+ );
344
+ break;
345
+
346
+ case "jobs":
347
+ const jobs = this.state.getJobs();
348
+ if (jobs.length === 0) {
349
+ await this.sendMessage("šŸ“‹ <b>No Jobs</b>\n\nNo active jobs.", ButtonKit.mainMenu());
350
+ } else {
351
+ await this.sendMessage(
352
+ "šŸ“‹ <b>Your Jobs</b>\n\nSelect a job:",
353
+ ButtonKit.jobList(jobs)
354
+ );
355
+ }
356
+ break;
357
+
358
+ case "status":
359
+ const latest = this.state.getJobs()[0];
360
+ if (latest) {
361
+ await this.sendMessage(formatJobCard(latest), ButtonKit.jobCard(latest));
362
+ } else {
363
+ await this.sendMessage("āœ… <b>All Clear</b>\n\nNo active jobs.", ButtonKit.mainMenu());
364
+ }
365
+ break;
366
+
367
+ case "search":
368
+ const searchJobs = this.state.getJobs();
369
+ await this.sendMessage(
370
+ "šŸ” <b>Search</b>\n\nSelect a job to view:",
371
+ ButtonKit.jobList(searchJobs)
372
+ );
373
+ break;
374
+
375
+ case "settings":
376
+ await this.sendMessage(
377
+ "āš™ļø <b>Settings</b>\n\n" +
378
+ "• Notifications: ON\n" +
379
+ "• Auto-retry: OFF\n" +
380
+ "• Log verbosity: Normal",
381
+ ButtonKit.mainMenu()
382
+ );
383
+ break;
384
+ }
385
+
386
+ return null;
387
+ }
388
+
389
+ private async sendJobLogs(jobId: string, page: number): Promise<void> {
390
+ // Get logs from storage
391
+ const logs = await this.getJobLogs(jobId);
392
+ const pageSize = 5;
393
+ const startIdx = page * pageSize;
394
+ const pageLogs = logs.slice(startIdx, startIdx + pageSize);
395
+ const hasMore = logs.length > startIdx + pageSize;
396
+
397
+ const text = `<b>šŸ“œ Logs</b>\n<code>${jobId}</code>\n─────────────────\n` +
398
+ pageLogs.map(l => `šŸ“ ${escapeHtml(l)}`).join("\n") +
399
+ `\n─────────────────\nPage ${page + 1}`;
400
+
401
+ await this.sendMessage(text, ButtonKit.logPagination(jobId, page, hasMore));
402
+ }
403
+
404
+ private async getJobDetail(jobId: string): Promise<JobDetail> {
405
+ const job = this.state.getJob(jobId);
406
+ return {
407
+ id: jobId,
408
+ title: job?.title || "Unknown Job",
409
+ status: job?.status || "unknown",
410
+ logs: await this.getJobLogs(jobId),
411
+ };
412
+ }
413
+
414
+ private async getJobLogs(_jobId: string): Promise<string[]> {
415
+ // TODO: Load from actual log storage
416
+ return [
417
+ "[10:00] Job started",
418
+ "[10:01] Running tests...",
419
+ "[10:02] Test 1: PASS",
420
+ "[10:02] Test 2: PASS",
421
+ "[10:03] All tests passed!",
422
+ ];
423
+ }
424
+
425
+ /**
426
+ * Send message to Telegram
427
+ */
428
+ private async sendMessage(text: string, keyboard?: InlineKeyboard): Promise<void> {
429
+ await fetch(`https://api.telegram.org/bot${this.botToken}/sendMessage`, {
430
+ method: "POST",
431
+ headers: { "Content-Type": "application/json" },
432
+ body: JSON.stringify({
433
+ chat_id: this.chatId,
434
+ text,
435
+ parse_mode: "HTML",
436
+ reply_markup: keyboard,
437
+ }),
438
+ });
439
+ }
440
+
441
+ /**
442
+ * Update job state from pi-harness
443
+ */
444
+ updateJobState(job: JobInfo): void {
445
+ this.state.updateJob(job);
446
+
447
+ // Send notification with quick actions
448
+ const emoji = job.status === "running" ? "šŸ”„" : job.status === "done" ? "āœ…" : job.status === "failed" ? "āŒ" : "ā³";
449
+ const text = `${emoji} <b>${escapeHtml(job.title)}</b>\n` +
450
+ `Status: ${job.status}${job.progress ? ` (${job.progress})` : ""}`;
451
+
452
+ this.sendMessage(text, ButtonKit.jobCard(job));
453
+ }
454
+ }
455
+
456
+ // ============================================================================
457
+ // Usage Example
458
+ // ============================================================================
459
+
460
+ /**
461
+ * In your telegram-webhook-server.ts, integrate like this:
462
+ *
463
+ * import { MobileBot, ButtonKit } from "./telegram-mobile-integration.ts";
464
+ *
465
+ * const mobileBot = new MobileBot(
466
+ * process.env.PIBOT_DATA_DIR || "~/.pi-harness-runtime",
467
+ * BOT_TOKEN,
468
+ * CHAT_ID,
469
+ * JOB_PIPE_FILE
470
+ * );
471
+ *
472
+ * // In callback handler:
473
+ * if (update.callback_query) {
474
+ * const result = await mobileBot.handleCallback(update.callback_query.data);
475
+ * if (result) {
476
+ * // Show error message
477
+ * await mobileBot.sendMessage(result);
478
+ * }
479
+ * }
480
+ */
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Test R2 Connection
3
+ *
4
+ * Run: bun run scripts/test-r2.ts
5
+ */
6
+
7
+ import { loadR2Config, getR2Client, uploadLogsToR2, isR2Configured } from "./r2-storage.js";
8
+
9
+ async function main() {
10
+ console.log("=".repeat(60));
11
+ console.log("R2 Connection Test");
12
+ console.log("=".repeat(60));
13
+
14
+ // Check if configured
15
+ console.log("\n1. Checking configuration...");
16
+ if (!isR2Configured()) {
17
+ console.log("āŒ R2 not configured - missing key files");
18
+ process.exit(1);
19
+ }
20
+ console.log("āœ“ R2 credentials found");
21
+
22
+ const config = loadR2Config();
23
+ console.log(` Account ID: ${config!.accountId.slice(0, 8)}...`);
24
+ console.log(` Bucket: ${config!.bucket}`);
25
+ console.log(` Endpoint: ${config!.endpoint || "(auto)"}`);
26
+
27
+ // Test connection
28
+ console.log("\n2. Testing connection...");
29
+ const client = getR2Client();
30
+ if (!client) {
31
+ console.log("āŒ Failed to create R2 client");
32
+ process.exit(1);
33
+ }
34
+ console.log("āœ“ R2 client created");
35
+
36
+ // Upload test
37
+ console.log("\n3. Testing upload...");
38
+ const testContent = `R2 Test Log
39
+ ================
40
+ Timestamp: ${new Date().toISOString()}
41
+ Test message for R2 upload verification
42
+ `;
43
+
44
+ const result = await uploadLogsToR2(testContent, "test");
45
+ if (!result) {
46
+ console.log("āŒ Upload failed");
47
+ process.exit(1);
48
+ }
49
+
50
+ console.log("āœ“ Upload successful!");
51
+ console.log(` Key: ${result.key}`);
52
+ console.log(` Presigned URL: ${result.url.slice(0, 80)}...`);
53
+ console.log(` Expires in: ${result.expiresIn / 3600} hours`);
54
+
55
+ console.log("\n" + "=".repeat(60));
56
+ console.log("āœ… R2 is working correctly!");
57
+ console.log("=".repeat(60));
58
+ }
59
+
60
+ main().catch(err => {
61
+ console.error("āŒ Test failed:", err);
62
+ process.exit(1);
63
+ });
@@ -17,6 +17,9 @@ const keysDir = path.join(process.env.HOME || "/home/frappe", ".pi-harness-runti
17
17
 
18
18
  const botToken = fs.readFileSync(path.join(keysDir, "telegram-bot-token.txt"), "utf8").trim();
19
19
  const chatId = fs.readFileSync(path.join(keysDir, "telegram-chat-id.txt"), "utf8").trim();
20
+ const webhookUrl = fs.existsSync(path.join(keysDir, "telegram-webhook-url.txt"))
21
+ ? fs.readFileSync(path.join(keysDir, "telegram-webhook-url.txt"), "utf8").trim()
22
+ : "https://yourdomain.com/api/telegram/webhook";
20
23
 
21
24
  async function sendTestApproval() {
22
25
  const message = `*Test Approval Request*\n\n` +
@@ -54,7 +57,7 @@ async function sendTestApproval() {
54
57
  console.log(`āœ… Test message sent! Message ID: ${data.result?.message_id}`);
55
58
  console.log(`\nšŸ“± Check your Telegram for the approval message.`);
56
59
  console.log(`\nWhen you click a button, the webhook at:`);
57
- console.log(` https://telegram.moo-vpn.online/api/telegram/webhook`);
60
+ console.log(` ${webhookUrl}`);
58
61
  console.log(`\nwill receive the callback and process it.`);
59
62
  } else {
60
63
  console.error(`āŒ Failed to send message:`, data);