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.
- package/README.md +127 -0
- package/harness/notification-center.ts +363 -0
- package/index.ts +111 -15
- package/package.json +1 -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/connection-runtime/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/contracts/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-sink/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/dist/notification-center.d.ts +18 -1
- package/packages/notification/dist/notification-center.d.ts.map +1 -1
- package/packages/notification/dist/notification-center.js +49 -0
- package/packages/notification/dist/notification-center.js.map +1 -1
- package/packages/notification/dist/telegram-webhook-handler.d.ts +4 -0
- package/packages/notification/dist/telegram-webhook-handler.d.ts.map +1 -1
- package/packages/notification/dist/telegram-webhook-handler.js +4 -0
- package/packages/notification/dist/telegram-webhook-handler.js.map +1 -1
- package/packages/notification/package.json +1 -1
- package/packages/notification/src/notification-center.ts +69 -13
- package/packages/notification/src/telegram-webhook-handler.ts +4 -0
- package/packages/observability/package.json +1 -1
- package/packages/okf-indexer/package.json +1 -1
- package/packages/paperclip-integration/package.json +1 -1
- package/packages/performance-optimizer/package.json +1 -1
- package/packages/permission-mode/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/relay/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/voice-api/dist/line.d.ts +2 -1
- package/packages/voice-api/dist/line.js +9 -6
- package/packages/voice-api/dist/telegram.js +8 -1
- package/packages/voice-api/dist/transcriber.d.ts +12 -41
- package/packages/voice-api/dist/transcriber.js +88 -108
- package/packages/voice-api/package.json +1 -1
- package/packages/voice-api/src/line.ts +11 -8
- package/packages/voice-api/src/telegram.ts +9 -1
- package/packages/voice-api/src/transcriber.ts +98 -122
- 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/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
|
-
|
|
1083
|
-
|
|
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
|
-
|
|
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