pi-harness-runtime 1.1.333 ā 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.
- package/README.md +2 -2
- package/RULES.md +45 -0
- package/harness/skill-sync.ts +11 -2
- package/package.json +3 -1
- package/packages/a2a-adapter/package.json +1 -1
- package/packages/agent-policy/package.json +1 -1
- package/packages/architecture-generator/package.json +1 -1
- package/packages/auth/package.json +1 -1
- package/packages/autonomous-refactor/package.json +1 -1
- package/packages/autonomous-runtime/package.json +1 -1
- package/packages/cache-strategy/package.json +1 -1
- package/packages/capability-registry/package.json +1 -1
- package/packages/checkpoint/package.json +1 -1
- package/packages/cli-plugin-sdk/package.json +1 -1
- package/packages/clipboard/package.json +1 -1
- package/packages/clipboard-plugin/package.json +1 -1
- package/packages/code-generation/package.json +1 -1
- package/packages/code-review/package.json +1 -1
- package/packages/codex-adapter/package.json +1 -1
- package/packages/codex-watcher/package.json +1 -1
- package/packages/config-capture/package.json +1 -1
- package/packages/context-compiler/package.json +1 -1
- package/packages/context-discovery/package.json +1 -1
- package/packages/context-manager/package.json +1 -1
- package/packages/cookie-sanitizer/package.json +1 -1
- package/packages/cost-optimizer/package.json +1 -1
- package/packages/dependency-analyzer/package.json +1 -1
- package/packages/django-plugin/package.json +1 -1
- package/packages/doc-generator/package.json +1 -1
- package/packages/durable-review/package.json +1 -1
- package/packages/e2e-tester/package.json +1 -1
- package/packages/evaluation-engine/package.json +1 -1
- package/packages/evaluation-runner/package.json +1 -1
- package/packages/event-bus/package.json +1 -1
- package/packages/event-store/package.json +1 -1
- package/packages/experience-replay/package.json +1 -1
- package/packages/feedback-collector/package.json +1 -1
- package/packages/file-copy-helper/package.json +1 -1
- package/packages/framework-detector/package.json +1 -1
- package/packages/framework-plugin-sdk/package.json +1 -1
- package/packages/frappe-plugin/package.json +1 -1
- package/packages/generic-web-plugin/package.json +1 -1
- package/packages/health-monitor/package.json +1 -1
- package/packages/intent-analyzer/package.json +1 -1
- package/packages/jev-judge/package.json +1 -1
- package/packages/knowledge-graph/package.json +1 -1
- package/packages/knowledge-retrieval/package.json +1 -1
- package/packages/laravel-plugin/package.json +1 -1
- package/packages/learning-engine/package.json +1 -1
- package/packages/mcp-adapter/package.json +1 -1
- package/packages/memory-engine/package.json +1 -1
- package/packages/milestone-manager/package.json +1 -1
- package/packages/model-registry/package.json +1 -1
- package/packages/nextjs-plugin/package.json +1 -1
- package/packages/notification/package.json +1 -1
- package/packages/observability/package.json +1 -1
- package/packages/okf-indexer/package.json +1 -1
- package/packages/paperclip-integration/dist/index.js +1 -1
- package/packages/paperclip-integration/dist/index.js.map +1 -1
- package/packages/paperclip-integration/package.json +1 -1
- package/packages/paperclip-integration/src/index.ts +1 -1
- package/packages/performance-optimizer/package.json +1 -1
- package/packages/privilege-broker/package.json +1 -1
- package/packages/project-analyzer/package.json +1 -1
- package/packages/project-bootstrap/package.json +1 -1
- package/packages/projection-engine/package.json +1 -1
- package/packages/prompt-compiler/package.json +1 -1
- package/packages/prompt-versioning/package.json +1 -1
- package/packages/provider-adapter-sdk/package.json +1 -1
- package/packages/provider-router/package.json +1 -1
- package/packages/provider-selector/package.json +1 -1
- package/packages/providers/package.json +1 -1
- package/packages/qdrant-skills/package.json +1 -1
- package/packages/quota-manager/package.json +1 -1
- package/packages/rate-limiter/package.json +1 -1
- package/packages/react-vite-plugin/package.json +1 -1
- package/packages/release-manager/package.json +1 -1
- package/packages/requirement-compiler/package.json +1 -1
- package/packages/runtime/package.json +1 -1
- package/packages/scheduler/package.json +1 -1
- package/packages/scheduler-adapter/package.json +1 -1
- package/packages/session/package.json +1 -1
- package/packages/session-api/package.json +1 -1
- package/packages/session-export/package.json +1 -1
- package/packages/shared-context/package.json +1 -1
- package/packages/skill-mcp-client/package.json +1 -1
- package/packages/skill-registry/package.json +1 -1
- package/packages/skills/package.json +1 -1
- package/packages/sprint-planner/package.json +1 -1
- package/packages/subscription-engine/package.json +1 -1
- package/packages/task-compiler/package.json +1 -1
- package/packages/telegram/package.json +1 -1
- package/packages/tencentdb-memory/package.json +1 -1
- package/packages/tencentdb-sync/package.json +1 -1
- package/packages/test-data-generator/package.json +1 -1
- package/packages/test-generator/package.json +1 -1
- package/packages/todo-bd-sync/package.json +1 -1
- package/packages/token-estimation/package.json +1 -1
- package/packages/token-optimizer/package.json +1 -1
- package/packages/trajectory/package.json +1 -1
- package/packages/tui/package.json +1 -1
- package/packages/types/package.json +1 -1
- package/packages/webmcp-tester/package.json +1 -1
- package/packages/wiki/package.json +1 -1
- package/packages/workflow-events/package.json +1 -1
- package/packages/workspace-scanner/package.json +1 -1
- package/packages/worktree/package.json +1 -1
- package/packages/write-review/package.json +1 -1
- package/scripts/r2-storage.ts +217 -0
- package/scripts/telegram-mobile-bot.ts +438 -0
- package/scripts/telegram-mobile-integration.ts +480 -0
- package/scripts/test-r2.ts +63 -0
- 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, "&")
|
|
220
|
+
.replace(/</g, "<")
|
|
221
|
+
.replace(/>/g, ">")
|
|
222
|
+
.replace(/"/g, """);
|
|
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(`
|
|
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);
|