pi-harness-runtime 1.1.341 → 1.1.344

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 (129) hide show
  1. package/README.md +127 -0
  2. package/harness/notification-center.ts +363 -0
  3. package/index.ts +111 -15
  4. package/package.json +1 -1
  5. package/packages/a2a-adapter/package.json +1 -1
  6. package/packages/agent-policy/package.json +1 -1
  7. package/packages/architecture-generator/package.json +1 -1
  8. package/packages/auth/package.json +1 -1
  9. package/packages/autonomous-refactor/package.json +1 -1
  10. package/packages/autonomous-runtime/package.json +1 -1
  11. package/packages/cache-strategy/package.json +1 -1
  12. package/packages/capability-registry/package.json +1 -1
  13. package/packages/checkpoint/package.json +1 -1
  14. package/packages/cli-plugin-sdk/package.json +1 -1
  15. package/packages/clipboard/package.json +1 -1
  16. package/packages/clipboard-plugin/package.json +1 -1
  17. package/packages/code-generation/package.json +1 -1
  18. package/packages/code-review/package.json +1 -1
  19. package/packages/codex-adapter/package.json +1 -1
  20. package/packages/codex-watcher/package.json +1 -1
  21. package/packages/config-capture/package.json +1 -1
  22. package/packages/connection-runtime/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/contracts/package.json +1 -1
  27. package/packages/cookie-sanitizer/package.json +1 -1
  28. package/packages/cost-optimizer/package.json +1 -1
  29. package/packages/dependency-analyzer/package.json +1 -1
  30. package/packages/django-plugin/package.json +1 -1
  31. package/packages/doc-generator/package.json +1 -1
  32. package/packages/durable-review/package.json +1 -1
  33. package/packages/e2e-tester/package.json +1 -1
  34. package/packages/evaluation-engine/package.json +1 -1
  35. package/packages/evaluation-runner/package.json +1 -1
  36. package/packages/event-bus/package.json +1 -1
  37. package/packages/event-sink/package.json +1 -1
  38. package/packages/event-store/package.json +1 -1
  39. package/packages/experience-replay/package.json +1 -1
  40. package/packages/feedback-collector/package.json +1 -1
  41. package/packages/file-copy-helper/package.json +1 -1
  42. package/packages/framework-detector/package.json +1 -1
  43. package/packages/framework-plugin-sdk/package.json +1 -1
  44. package/packages/frappe-plugin/package.json +1 -1
  45. package/packages/generic-web-plugin/package.json +1 -1
  46. package/packages/health-monitor/package.json +1 -1
  47. package/packages/intent-analyzer/package.json +1 -1
  48. package/packages/jev-judge/package.json +1 -1
  49. package/packages/knowledge-graph/package.json +1 -1
  50. package/packages/knowledge-retrieval/package.json +1 -1
  51. package/packages/laravel-plugin/package.json +1 -1
  52. package/packages/learning-engine/package.json +1 -1
  53. package/packages/mcp-adapter/package.json +1 -1
  54. package/packages/memory-engine/package.json +1 -1
  55. package/packages/milestone-manager/package.json +1 -1
  56. package/packages/model-registry/package.json +1 -1
  57. package/packages/nextjs-plugin/package.json +1 -1
  58. package/packages/notification/dist/notification-center.d.ts +18 -1
  59. package/packages/notification/dist/notification-center.d.ts.map +1 -1
  60. package/packages/notification/dist/notification-center.js +49 -0
  61. package/packages/notification/dist/notification-center.js.map +1 -1
  62. package/packages/notification/dist/telegram-webhook-handler.d.ts +4 -0
  63. package/packages/notification/dist/telegram-webhook-handler.d.ts.map +1 -1
  64. package/packages/notification/dist/telegram-webhook-handler.js +4 -0
  65. package/packages/notification/dist/telegram-webhook-handler.js.map +1 -1
  66. package/packages/notification/package.json +1 -1
  67. package/packages/notification/src/notification-center.ts +69 -13
  68. package/packages/notification/src/telegram-webhook-handler.ts +4 -0
  69. package/packages/observability/package.json +1 -1
  70. package/packages/okf-indexer/package.json +1 -1
  71. package/packages/paperclip-integration/package.json +1 -1
  72. package/packages/performance-optimizer/package.json +1 -1
  73. package/packages/permission-mode/package.json +1 -1
  74. package/packages/privilege-broker/package.json +1 -1
  75. package/packages/project-analyzer/package.json +1 -1
  76. package/packages/project-bootstrap/package.json +1 -1
  77. package/packages/projection-engine/package.json +1 -1
  78. package/packages/prompt-compiler/package.json +1 -1
  79. package/packages/prompt-versioning/package.json +1 -1
  80. package/packages/provider-adapter-sdk/package.json +1 -1
  81. package/packages/provider-router/package.json +1 -1
  82. package/packages/provider-selector/package.json +1 -1
  83. package/packages/providers/package.json +1 -1
  84. package/packages/qdrant-skills/package.json +1 -1
  85. package/packages/quota-manager/package.json +1 -1
  86. package/packages/rate-limiter/package.json +1 -1
  87. package/packages/react-vite-plugin/package.json +1 -1
  88. package/packages/relay/package.json +1 -1
  89. package/packages/release-manager/package.json +1 -1
  90. package/packages/requirement-compiler/package.json +1 -1
  91. package/packages/runtime/package.json +1 -1
  92. package/packages/scheduler/package.json +1 -1
  93. package/packages/scheduler-adapter/package.json +1 -1
  94. package/packages/session/package.json +1 -1
  95. package/packages/session-api/package.json +1 -1
  96. package/packages/session-export/package.json +1 -1
  97. package/packages/shared-context/package.json +1 -1
  98. package/packages/skill-mcp-client/package.json +1 -1
  99. package/packages/skill-registry/package.json +1 -1
  100. package/packages/skills/package.json +1 -1
  101. package/packages/sprint-planner/package.json +1 -1
  102. package/packages/subscription-engine/package.json +1 -1
  103. package/packages/task-compiler/package.json +1 -1
  104. package/packages/telegram/package.json +1 -1
  105. package/packages/tencentdb-memory/package.json +1 -1
  106. package/packages/tencentdb-sync/package.json +1 -1
  107. package/packages/test-data-generator/package.json +1 -1
  108. package/packages/test-generator/package.json +1 -1
  109. package/packages/todo-bd-sync/package.json +1 -1
  110. package/packages/token-estimation/package.json +1 -1
  111. package/packages/token-optimizer/package.json +1 -1
  112. package/packages/trajectory/package.json +1 -1
  113. package/packages/tui/package.json +1 -1
  114. package/packages/types/package.json +1 -1
  115. package/packages/voice-api/dist/line.d.ts +2 -1
  116. package/packages/voice-api/dist/line.js +9 -6
  117. package/packages/voice-api/dist/telegram.js +8 -1
  118. package/packages/voice-api/dist/transcriber.d.ts +12 -41
  119. package/packages/voice-api/dist/transcriber.js +88 -108
  120. package/packages/voice-api/package.json +1 -1
  121. package/packages/voice-api/src/line.ts +11 -8
  122. package/packages/voice-api/src/telegram.ts +9 -1
  123. package/packages/voice-api/src/transcriber.ts +98 -122
  124. package/packages/webmcp-tester/package.json +1 -1
  125. package/packages/wiki/package.json +1 -1
  126. package/packages/workflow-events/package.json +1 -1
  127. package/packages/workspace-scanner/package.json +1 -1
  128. package/packages/worktree/package.json +1 -1
  129. package/packages/write-review/package.json +1 -1
package/README.md CHANGED
@@ -25,6 +25,15 @@ Give the runtime a requirement. It keeps working, survives interruption, and ret
25
25
  pi install npm:pi-harness-runtime
26
26
  ```
27
27
 
28
+ ### Recommended Extensions
29
+
30
+ This runtime works best with these extensions (auto-installed by pi-harness-runtime):
31
+
32
+ ```bash
33
+ pi install npm:pi-mcp-adapter # MCP protocol adapter for external tools
34
+ pi install npm:@juicesharp/rpiv-todo # Task tracking with bd sync
35
+ ```
36
+
28
37
  Requires Pi v0.37.3+.
29
38
 
30
39
  ### Headless Server Setup (herdr clipboard bridge)
@@ -679,6 +688,9 @@ Your local machine clipboard
679
688
 
680
689
  Get notified on your phone when tasks complete! LINE notifications via the LINE Messaging API.
681
690
 
691
+ > **Self-Hosting Required**: LINE webhooks require your own server. This is NOT a hosted service.
692
+ > You must **clone this repository** and deploy your own webhook server. See [LINE Voice/Webhook Setup](#line-voicewebhook-setup) below.
693
+
682
694
  #### Step-by-Step Setup
683
695
 
684
696
  **1. Create a LINE Messaging API channel**
@@ -754,6 +766,118 @@ Task: Implement login flow
754
766
  Job: feature-auth
755
767
  ```
756
768
 
769
+ ### LINE Voice/Webhook Setup (Voice Commands)
770
+
771
+ > **Self-Hosting Required**: LINE voice messages require your own server with HTTPS.
772
+ > This is NOT a hosted service. You must **clone this repository** and deploy your own webhook server.
773
+
774
+ Voice messages from LINE are transcribed via OpenAI Whisper API and executed as commands.
775
+
776
+ #### Requirements
777
+
778
+ - **Own server** (VPS, cloud VM) with public IP
779
+ - **Domain** with DNS A record pointing to your server
780
+ - **SSL certificate** (Let's Encrypt via certbot)
781
+ - **Clone this repository** on your server
782
+
783
+ #### Setup Steps
784
+
785
+ **1. Clone and Build**
786
+
787
+ ```bash
788
+ # Clone on your server
789
+ git clone https://github.com/ManotLuijiu/pi-harness-runtime.git
790
+ cd pi-harness-runtime
791
+ bun install
792
+ bun run build
793
+ ```
794
+
795
+ **2. Configure DNS**
796
+
797
+ Create an A record: `line-api.your-domain.com` → `YOUR_SERVER_IP`
798
+
799
+ **3. Setup Nginx + SSL**
800
+
801
+ ```bash
802
+ # Install certbot
803
+ sudo apt install certbot python3-certbot-nginx
804
+
805
+ # Create nginx config
806
+ sudo tee /etc/nginx/sites-available/line-api.your-domain.com > /dev/null << 'NGINX'
807
+ server {
808
+ listen 80;
809
+ server_name line-api.your-domain.com;
810
+ location / {
811
+ proxy_pass http://127.0.0.1:3900;
812
+ proxy_http_version 1.1;
813
+ proxy_set_header Host $host;
814
+ }
815
+ }
816
+ NGINX
817
+
818
+ # Enable and get SSL
819
+ sudo ln -sf /etc/nginx/sites-available/line-api.your-domain.com /etc/nginx/sites-enabled/
820
+ sudo certbot --nginx -d line-api.your-domain.com --noninteractive --agree-tos -m admin@your-domain.com --redirect
821
+ ```
822
+
823
+ **4. Configure LINE Webhook**
824
+
825
+ ```bash
826
+ # Set credentials
827
+ mkdir -p ~/.pi-harness-runtime/keys
828
+
829
+ # Get from LINE Developers Console > Messaging API > Basic settings
830
+ cp YOUR_ACCESS_TOKEN ~/.pi-harness-runtime/keys/line-access-token.txt
831
+ cp YOUR_CHANNEL_SECRET ~/.pi-harness-runtime/keys/line-channel-secret.txt
832
+
833
+ # Get from LINE Developers Console > Messaging API > Webhook URL
834
+ # Set to: https://line-api.your-domain.com/webhook/line
835
+ ```
836
+
837
+ **5. Create Webhook Server**
838
+
839
+ ```bash
840
+ # Create startup script
841
+ mkdir -p ~/line-webhook
842
+ cp pi-harness-runtime/packages/voice-api/dist/*.js ~/line-webhook/
843
+
844
+ # Run server
845
+ bun run ~/line-webhook/server.ts
846
+ ```
847
+
848
+ **6. Set Webhook URL in LINE Console**
849
+
850
+ 1. Go to LINE Developers Console → Messaging API → Messaging settings
851
+ 2. Set Webhook URL: `https://line-api.your-domain.com/webhook/line`
852
+ 3. Click Verify
853
+
854
+ #### Systemd Service (Auto-Restart)
855
+
856
+ ```bash
857
+ sudo tee /etc/systemd/system/line-webhook.service > /dev/null << 'SERVICE'
858
+ [Unit]
859
+ Description=LINE Webhook Server for pi-harness
860
+ After=network.target
861
+
862
+ [Service]
863
+ Type=simple
864
+ User=YOUR_USER
865
+ WorkingDirectory=/home/YOUR_USER/line-webhook
866
+ ExecStart=/home/YOUR_USER/.bun/bin/bun run /home/YOUR_USER/line-webhook/server.ts
867
+ Restart=always
868
+ RestartSec=5
869
+ StandardOutput=journal
870
+ StandardError=journal
871
+
872
+ [Install]
873
+ WantedBy=multi-user.target
874
+ SERVICE
875
+
876
+ sudo systemctl daemon-reload
877
+ sudo systemctl enable line-webhook.service
878
+ sudo systemctl start line-webhook.service
879
+ ```
880
+
757
881
  ### Other Providers
758
882
 
759
883
  No setup needed. The runtime reads TUI messages and captures quota-exhaustion signals.
@@ -780,6 +904,9 @@ footer updates immediately and the data persists in `mirror.json`.
780
904
 
781
905
  Enable interactive Yes/No buttons in Telegram notifications for human approval.
782
906
 
907
+ > **Self-Hosting Required**: Telegram webhooks require your own server with HTTPS.
908
+ > This is NOT a hosted service. You must **clone this repository** and deploy your own webhook server.
909
+
783
910
  ### Architecture (Per-User Setup)
784
911
 
785
912
  ```
@@ -0,0 +1,363 @@
1
+ /**
2
+ * Multi-Channel Notification Center
3
+ *
4
+ * Unified notification system supporting Telegram and LINE.
5
+ * Users can configure which channels to use and toggle them via inline buttons.
6
+ */
7
+
8
+ import * as https from "https";
9
+
10
+ export interface NotificationConfig {
11
+ telegram?: {
12
+ botToken: string;
13
+ chatId: string;
14
+ enabled: boolean;
15
+ };
16
+ line?: {
17
+ channelAccessToken: string;
18
+ userId: string;
19
+ enabled: boolean;
20
+ };
21
+ }
22
+
23
+ export interface NotificationEvent {
24
+ type: string;
25
+ emoji: string;
26
+ title: string;
27
+ message: string;
28
+ metadata?: Record<string, unknown>;
29
+ }
30
+
31
+ export interface NotificationButtons {
32
+ label: string;
33
+ action: string;
34
+ style?: "primary" | "secondary" | "danger";
35
+ }
36
+
37
+ const DEFAULT_CONFIG_FILE = `${process.env.HOME || "/home/frappe"}/.pi-harness-runtime/notification-config.json`;
38
+
39
+ /**
40
+ * Load notification configuration
41
+ */
42
+ function loadConfig(): NotificationConfig {
43
+ try {
44
+ const { existsSync, readFileSync } = require("fs");
45
+ if (existsSync(DEFAULT_CONFIG_FILE)) {
46
+ return JSON.parse(readFileSync(DEFAULT_CONFIG_FILE, "utf-8"));
47
+ }
48
+ } catch (e) {
49
+ console.error("Failed to save notification config:", e);
50
+ }
51
+ return {};
52
+ }
53
+
54
+ /**
55
+ * Save notification configuration
56
+ */
57
+ function saveConfig(config: NotificationConfig): void {
58
+ try {
59
+ const { writeFileSync, mkdirSync, existsSync } = require("fs");
60
+ const dir = DEFAULT_CONFIG_FILE.substring(0, DEFAULT_CONFIG_FILE.lastIndexOf("/"));
61
+ if (!existsSync(dir)) {
62
+ mkdirSync(dir, { recursive: true });
63
+ }
64
+ writeFileSync(DEFAULT_CONFIG_FILE, JSON.stringify(config, null, 2), "utf-8");
65
+ } catch (e) {
66
+ console.error("Failed to save notification config:", e);
67
+ }
68
+ }
69
+
70
+ /**
71
+ * Multi-Channel Notification Center
72
+ */
73
+ export class NotificationCenter {
74
+ private config: NotificationConfig;
75
+
76
+ constructor(config?: Partial<NotificationConfig>) {
77
+ this.config = loadConfig();
78
+ if (config?.telegram) {
79
+ this.config.telegram = { ...this.config.telegram, ...config.telegram };
80
+ }
81
+ if (config?.line) {
82
+ this.config.line = { ...this.config.line, ...config.line };
83
+ }
84
+ }
85
+
86
+ /**
87
+ * Update configuration
88
+ */
89
+ updateConfig(updates: Partial<NotificationConfig>): void {
90
+ this.config = { ...this.config, ...updates };
91
+ if (updates.telegram) {
92
+ this.config.telegram = { ...this.config.telegram!, ...updates.telegram };
93
+ }
94
+ if (updates.line) {
95
+ this.config.line = { ...this.config.line!, ...updates.line };
96
+ }
97
+ saveConfig(this.config);
98
+ }
99
+
100
+ /**
101
+ * Get current configuration
102
+ */
103
+ getConfig(): NotificationConfig {
104
+ return { ...this.config };
105
+ }
106
+
107
+ /**
108
+ * Send notification to all enabled channels
109
+ */
110
+ async notify(event: NotificationEvent): Promise<void> {
111
+ const { telegram, line } = this.config;
112
+ const promises: Promise<void>[] = [];
113
+
114
+ if (telegram?.enabled && telegram.botToken && telegram.chatId) {
115
+ promises.push(this.sendTelegram(event, telegram.botToken, telegram.chatId));
116
+ }
117
+
118
+ if (line?.enabled && line.channelAccessToken && line.userId) {
119
+ promises.push(this.sendLINE(event, line.channelAccessToken, line.userId));
120
+ }
121
+
122
+ await Promise.allSettled(promises);
123
+ }
124
+
125
+ /**
126
+ * Send notification with action buttons
127
+ */
128
+ async notifyWithButtons(
129
+ event: NotificationEvent,
130
+ buttons: NotificationButtons[]
131
+ ): Promise<void> {
132
+ const { telegram, line } = this.config;
133
+
134
+ if (telegram?.enabled && telegram.botToken && telegram.chatId) {
135
+ await this.sendTelegramWithButtons(event, buttons, telegram.botToken, telegram.chatId);
136
+ }
137
+
138
+ if (line?.enabled && line.channelAccessToken && line.userId) {
139
+ await this.sendLINEWithButtons(event, buttons, line.channelAccessToken, line.userId);
140
+ }
141
+ }
142
+
143
+ /**
144
+ * Send to Telegram
145
+ */
146
+ private async sendTelegram(
147
+ event: NotificationEvent,
148
+ botToken: string,
149
+ chatId: string
150
+ ): Promise<void> {
151
+ const text = `${event.emoji} ${event.title}\n\n${event.message}`;
152
+
153
+ await this.telegramRequest(botToken, "sendMessage", {
154
+ chat_id: chatId,
155
+ text,
156
+ parse_mode: "Markdown",
157
+ });
158
+ }
159
+
160
+ /**
161
+ * Send to Telegram with inline buttons
162
+ */
163
+ private async sendTelegramWithButtons(
164
+ event: NotificationEvent,
165
+ buttons: NotificationButtons[],
166
+ botToken: string,
167
+ chatId: string
168
+ ): Promise<void> {
169
+ const text = `${event.emoji} ${event.title}\n\n${event.message}`;
170
+ const inlineButtons = buttons.map((btn) => ({
171
+ text: btn.label,
172
+ callback_data: btn.action,
173
+ }));
174
+
175
+ await this.telegramRequest(botToken, "sendMessage", {
176
+ chat_id: chatId,
177
+ text,
178
+ parse_mode: "Markdown",
179
+ reply_markup: {
180
+ inline_keyboard: [inlineButtons],
181
+ },
182
+ });
183
+ }
184
+
185
+ /**
186
+ * Telegram API request
187
+ */
188
+ private telegramRequest(botToken: string, method: string, body: Record<string, unknown>): Promise<void> {
189
+ return new Promise((resolve, _reject) => {
190
+ const data = JSON.stringify(body);
191
+ const options = {
192
+ hostname: "api.telegram.org",
193
+ path: `/bot${botToken}/${method}`,
194
+ method: "POST",
195
+ headers: {
196
+ "Content-Type": "application/json",
197
+ "Content-Length": Buffer.byteLength(data),
198
+ },
199
+ };
200
+
201
+ const req = https.request(options, (res) => {
202
+ let response = "";
203
+ res.on("data", (chunk) => (response += chunk));
204
+ res.on("end", () => {
205
+ try {
206
+ const parsed = JSON.parse(response);
207
+ if (!parsed.ok) {
208
+ console.error(`Telegram API error: ${parsed.description}`);
209
+ }
210
+ } catch (e) {
211
+ console.error(`JSON parse error: ${e}`);
212
+ }
213
+ resolve();
214
+ });
215
+ });
216
+
217
+ req.on("error", (err: Error) => {
218
+ console.error(`Telegram API request error: ${err.message}`);
219
+ resolve();
220
+ });
221
+ req.write(data);
222
+ req.end();
223
+ });
224
+ }
225
+
226
+ /**
227
+ * Send to LINE
228
+ */
229
+ private async sendLINE(
230
+ event: NotificationEvent,
231
+ accessToken: string,
232
+ userId: string
233
+ ): Promise<void> {
234
+ const text = `${event.emoji} ${event.title}\n\n${event.message}`;
235
+
236
+ await this.lineRequest(accessToken, "push", {
237
+ to: userId,
238
+ messages: [{ type: "text", text }],
239
+ });
240
+ }
241
+
242
+ /**
243
+ * Send to LINE with quick reply buttons
244
+ */
245
+ private async sendLINEWithButtons(
246
+ event: NotificationEvent,
247
+ buttons: NotificationButtons[],
248
+ accessToken: string,
249
+ userId: string
250
+ ): Promise<void> {
251
+ const text = `${event.emoji} ${event.title}\n\n${event.message}`;
252
+
253
+ const actions = buttons.map((btn) => ({
254
+ type: "message" as const,
255
+ label: btn.label,
256
+ text: btn.action,
257
+ }));
258
+
259
+ await this.lineRequest(accessToken, "push", {
260
+ to: userId,
261
+ messages: [
262
+ { type: "text", text },
263
+ {
264
+ type: "text",
265
+ text: "Quick actions:",
266
+ quickReply: {
267
+ items: actions.map((action) => ({
268
+ type: "action",
269
+ action,
270
+ })),
271
+ },
272
+ },
273
+ ],
274
+ });
275
+ }
276
+
277
+ /**
278
+ * LINE API request
279
+ */
280
+ private lineRequest(accessToken: string, method: string, body: Record<string, unknown>): Promise<void> {
281
+ return new Promise((resolve, reject) => {
282
+ const data = JSON.stringify(body);
283
+ const options = {
284
+ hostname: "api.line.me",
285
+ path: `/v2/bot/message/${method}`,
286
+ method: "POST",
287
+ headers: {
288
+ "Content-Type": "application/json",
289
+ "Authorization": `Bearer ${accessToken}`,
290
+ "Content-Length": Buffer.byteLength(data),
291
+ },
292
+ };
293
+
294
+ const req = https.request(options, (res) => {
295
+ let response = "";
296
+ res.on("data", (chunk) => (response += chunk));
297
+ res.on("end", () => {
298
+ if (res.statusCode !== 200) {
299
+ console.error(`LINE API error: ${res.statusCode} ${response}`);
300
+ }
301
+ resolve();
302
+ });
303
+ });
304
+
305
+ req.on("error", reject);
306
+ req.write(data);
307
+ req.end();
308
+ });
309
+ }
310
+
311
+ /**
312
+ * Toggle Telegram notifications
313
+ */
314
+ toggleTelegram(enabled: boolean): void {
315
+ if (!this.config.telegram) {
316
+ this.config.telegram = { botToken: "", chatId: "", enabled };
317
+ } else {
318
+ this.config.telegram.enabled = enabled;
319
+ }
320
+ saveConfig(this.config);
321
+ }
322
+
323
+ /**
324
+ * Toggle LINE notifications
325
+ */
326
+ toggleLINE(enabled: boolean): void {
327
+ if (!this.config.line) {
328
+ this.config.line = { channelAccessToken: "", userId: "", enabled };
329
+ } else {
330
+ this.config.line.enabled = enabled;
331
+ }
332
+ saveConfig(this.config);
333
+ }
334
+
335
+ /**
336
+ * Check if Telegram is enabled
337
+ */
338
+ isTelegramEnabled(): boolean {
339
+ return this.config.telegram?.enabled ?? false;
340
+ }
341
+
342
+ /**
343
+ * Check if LINE is enabled
344
+ */
345
+ isLINEEnabled(): boolean {
346
+ return this.config.line?.enabled ?? false;
347
+ }
348
+ }
349
+
350
+ // Export singleton
351
+ let center: NotificationCenter | null = null;
352
+
353
+ export function getNotificationCenter(): NotificationCenter {
354
+ if (!center) {
355
+ center = new NotificationCenter();
356
+ }
357
+ return center;
358
+ }
359
+
360
+ export function initNotificationCenter(config?: Partial<NotificationConfig>): NotificationCenter {
361
+ center = new NotificationCenter(config);
362
+ return center;
363
+ }
package/index.ts CHANGED
@@ -1078,16 +1078,17 @@ async function advanceCodexTask(
1078
1078
  async function initTelegram(pi: ExtensionAPI): Promise<void> {
1079
1079
  addHarnessMessage("Telegram", "initializing", "wait");
1080
1080
  try {
1081
- const {
1082
- NotificationCenter,
1083
- createConversationBridge,
1084
- } = await import("./packages/notification/dist/index.js");
1081
+ const notifModule = await import("./packages/notification/dist/index.js");
1082
+ const NotificationCenter = notifModule.NotificationCenter;
1083
+ const createConversationBridge = notifModule.createConversationBridge;
1085
1084
  const { readFileSync, existsSync } = await import("node:fs");
1086
1085
  const home = process.env.HOME || process.env.USERPROFILE || "/home/frappe";
1087
1086
  const keysDir = `${home}/.pi-harness-runtime/keys`;
1088
1087
 
1089
1088
  const botTokenPath = `${keysDir}/telegram-bot-token.txt`;
1090
1089
  const chatIdPath = `${keysDir}/telegram-chat-id.txt`;
1090
+ const lineTokenPath = `${keysDir}/line-access-token.txt`;
1091
+ const lineUserIdPath = `${keysDir}/line-user-id.txt`;
1091
1092
 
1092
1093
  if (!existsSync(botTokenPath) || !existsSync(chatIdPath)) {
1093
1094
  logStartup("[pi-harness] Telegram (@PiHarnessRuntimeBot) not configured:");
@@ -1107,6 +1108,11 @@ async function initTelegram(pi: ExtensionAPI): Promise<void> {
1107
1108
  const botToken = readFileSync(botTokenPath, "utf8").trim();
1108
1109
  const chatId = readFileSync(chatIdPath, "utf8").trim();
1109
1110
 
1111
+ // Load LINE credentials
1112
+ const hasLineCredentials = existsSync(lineTokenPath) && existsSync(lineUserIdPath);
1113
+ const lineAccessToken = hasLineCredentials ? readFileSync(lineTokenPath, "utf8").trim() : null;
1114
+ const lineUserId = hasLineCredentials ? readFileSync(lineUserIdPath, "utf8").trim() : null;
1115
+
1110
1116
  if (!botToken || !chatId) {
1111
1117
  logStartup("[pi-harness] Telegram: bot token or chat ID is empty — skipping");
1112
1118
  addHarnessMessage("Telegram", "bot token or chat ID is empty; notifications unavailable", "warn", [
@@ -1115,16 +1121,35 @@ async function initTelegram(pi: ExtensionAPI): Promise<void> {
1115
1121
  return;
1116
1122
  }
1117
1123
 
1124
+ const channels: Array<{
1125
+ id: string;
1126
+ type: string;
1127
+ enabled: boolean;
1128
+ config: Record<string, unknown>;
1129
+ }> = [
1130
+ {
1131
+ id: "telegram",
1132
+ type: "telegram",
1133
+ enabled: true,
1134
+ config: { botToken, chatId },
1135
+ },
1136
+ ];
1137
+
1138
+ // Add LINE channel if credentials exist
1139
+ if (hasLineCredentials && lineAccessToken && lineUserId) {
1140
+ channels.push({
1141
+ id: "line",
1142
+ type: "line" as const,
1143
+ enabled: true,
1144
+ config: { channelAccessToken: lineAccessToken, userId: lineUserId },
1145
+ });
1146
+ addHarnessMessage("LINE", "configured and enabled", "ok");
1147
+ }
1148
+
1149
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1118
1150
  const center = new NotificationCenter({
1119
1151
  enabled: true,
1120
- channels: [
1121
- {
1122
- id: "telegram",
1123
- type: "telegram",
1124
- enabled: true,
1125
- config: { botToken, chatId },
1126
- },
1127
- ],
1152
+ channels: channels as any,
1128
1153
  });
1129
1154
 
1130
1155
  await center.initialize();
@@ -1187,9 +1212,22 @@ async function initTelegram(pi: ExtensionAPI): Promise<void> {
1187
1212
  (sharedState.todoContinuationController as any)?.explicitPause?.();
1188
1213
  break;
1189
1214
  default:
1190
- logToFile("[TelegramCallback]", `Unknown action: ${parsed.action}`);
1191
- }
1192
- });
1215
+ // Check for channel toggle actions
1216
+ if (parsed.action === "toggle_telegram") {
1217
+ const telegramEnabled = (center as any).isChannelEnabled?.("telegram") ?? true;
1218
+ (center as any).setChannelEnabled?.("telegram", !telegramEnabled);
1219
+ logToFile("[TelegramCallback]", `Telegram ${!telegramEnabled ? "enabled" : "disabled"}`);
1220
+ await acknowledge(`Telegram notifications ${!telegramEnabled ? "enabled" : "disabled"}`);
1221
+ } else if (parsed.action === "toggle_line") {
1222
+ const lineEnabled = (center as any).isChannelEnabled?.("line") ?? true;
1223
+ (center as any).setChannelEnabled?.("line", !lineEnabled);
1224
+ logToFile("[TelegramCallback]", `LINE ${!lineEnabled ? "enabled" : "disabled"}`);
1225
+ await acknowledge(`LINE notifications ${!lineEnabled ? "enabled" : "disabled"}`);
1226
+ } else {
1227
+ logToFile("[TelegramCallback]", `Unknown action: ${parsed.action}`);
1228
+ }
1229
+ }
1230
+ });
1193
1231
 
1194
1232
  // Register message handler to forward Telegram messages to Pi
1195
1233
  center.setMessageHandler(async (text: string, _chatId: string) => {
@@ -2396,6 +2434,61 @@ function initJobPipeWatcher(pi: ExtensionAPI): void {
2396
2434
  logToFile("[JobPipe]", "Watching for Telegram approval commands via API (polling every 60s)");
2397
2435
  }
2398
2436
 
2437
+ // --- LINE Voice Queue Watcher: Listen for LINE voice commands ---------
2438
+ // Watches the LINE voice queue file and injects transcribed prompts into pi.
2439
+ //
2440
+ // Flow:
2441
+ // User sends voice message in LINE
2442
+ // → LINE sends webhook to our VPS
2443
+ // → VPS transcribes via Whisper API
2444
+ // → VPS writes command to line-voice-queue.jsonl
2445
+ // → pi-harness polls this file
2446
+ // → pi-harness injects prompt via steer
2447
+ function initLineVoiceQueue(pi: ExtensionAPI): void {
2448
+ const homedir = process.env.HOME || process.env.USERPROFILE || "/home/frappe";
2449
+ const queueFile = join(homedir, ".pi-harness-runtime", "line-voice-queue.jsonl");
2450
+
2451
+ const processLineVoiceQueue = async () => {
2452
+ try {
2453
+ const { existsSync, readFileSync, writeFileSync } = await import("node:fs");
2454
+
2455
+ if (!existsSync(queueFile)) {
2456
+ return;
2457
+ }
2458
+
2459
+ const content = readFileSync(queueFile, "utf-8");
2460
+ const lines = content.split("\n").filter((l) => l.trim());
2461
+
2462
+ if (lines.length === 0) {
2463
+ return;
2464
+ }
2465
+
2466
+ // Clear queue
2467
+ writeFileSync(queueFile, "", "utf-8");
2468
+
2469
+ for (const line of lines) {
2470
+ try {
2471
+ const cmd = JSON.parse(line) as { type: string; text: string; userId: string; timestamp: string };
2472
+
2473
+ if (cmd.type === "voice-prompt" && cmd.text) {
2474
+ logToFile("[LineVoice]", `Injecting voice prompt: "${cmd.text}"`);
2475
+ pi.sendUserMessage(cmd.text, { deliverAs: "steer" });
2476
+ logToFile("[LineVoice]", "Voice prompt injected via steer");
2477
+ }
2478
+ } catch {
2479
+ // Skip malformed lines
2480
+ }
2481
+ }
2482
+ } catch {
2483
+ // Ignore errors
2484
+ }
2485
+ };
2486
+
2487
+ // Poll every 5 seconds for new voice commands (voice is more time-sensitive than approvals)
2488
+ setInterval(processLineVoiceQueue, 5_000);
2489
+ logToFile("[LineVoice]", `Watching LINE voice queue: ${queueFile} (polling every 5s)`);
2490
+ }
2491
+
2399
2492
  export default async function (pi: ExtensionAPI) {
2400
2493
  const tracker = new UsageTracker();
2401
2494
  const mirrorStore = new MirrorStore();
@@ -2442,6 +2535,9 @@ export default async function (pi: ExtensionAPI) {
2442
2535
  // Collect promise so we can wait for initialization before showing startup notification
2443
2536
  asyncInitPromises.push(initTelegram(pi));
2444
2537
 
2538
+ // --- line-voice-queue: Watch for LINE voice commands ----
2539
+ void initLineVoiceQueue(pi);
2540
+
2445
2541
  // --- codex-watcher: Auto-trigger Pi when Codex finishes a plan ---
2446
2542
  // Flow: detect plan -> settle timer -> file stability check -> cwd match -> trigger
2447
2543
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-harness-runtime",
3
- "version": "1.1.341",
3
+ "version": "1.1.344",
4
4
  "description": "[BETA] Codex-style /usage status + autonomous coding harness for pi. Not production ready — expect breaking changes.",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi/a2a-adapter",
3
- "version": "1.1.341",
3
+ "version": "1.1.344",
4
4
  "type": "module",
5
5
  "description": "Google Agent-to-Agent (A2A) protocol adapter for pi-harness",
6
6
  "main": "dist/index.js",