clawless 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +48 -0
- package/LICENSE +21 -0
- package/QUICKSTART-SCHEDULER.md +226 -0
- package/QUICKSTART.md +98 -0
- package/README.md +404 -0
- package/SCHEDULER.md +255 -0
- package/TESTING.md +239 -0
- package/acp/clientHelpers.ts +23 -0
- package/acp/tempAcpRunner.ts +320 -0
- package/agent-bridge.config.example.json +19 -0
- package/bin/cli.ts +255 -0
- package/dist/acp/clientHelpers.js +19 -0
- package/dist/acp/tempAcpRunner.js +263 -0
- package/dist/bin/cli.js +217 -0
- package/dist/index.js +1115 -0
- package/dist/messaging/telegramClient.js +109 -0
- package/dist/scheduler/cronScheduler.js +272 -0
- package/dist/scheduler/scheduledJobHandler.js +34 -0
- package/dist/utils/error.js +12 -0
- package/dist/utils/telegramMarkdown.js +128 -0
- package/ecosystem.config.json +23 -0
- package/index.ts +1272 -0
- package/messaging/telegramClient.ts +132 -0
- package/package.json +43 -0
- package/scheduler/cronScheduler.ts +355 -0
- package/scheduler/scheduledJobHandler.ts +55 -0
- package/scripts/callback-health.sh +6 -0
- package/scripts/callback-post-chat.sh +25 -0
- package/scripts/callback-post.sh +19 -0
- package/scripts/gemini-scheduler-examples.sh +162 -0
- package/scripts/test-scheduler.sh +78 -0
- package/tsconfig.json +23 -0
- package/utils/error.ts +12 -0
package/TESTING.md
ADDED
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
# Scheduler API Manual Testing Guide
|
|
2
|
+
|
|
3
|
+
This guide walks through manual testing of the scheduler API functionality.
|
|
4
|
+
|
|
5
|
+
## Prerequisites
|
|
6
|
+
|
|
7
|
+
1. The bridge must be running with a valid Telegram bot token
|
|
8
|
+
2. You must have sent at least one message to the bot to establish a chat binding
|
|
9
|
+
3. `curl` and `jq` must be installed for the test commands
|
|
10
|
+
|
|
11
|
+
## Test Setup
|
|
12
|
+
|
|
13
|
+
1. Start the bridge in development mode:
|
|
14
|
+
```bash
|
|
15
|
+
npm run dev
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
2. In another terminal, verify the server is running:
|
|
19
|
+
```bash
|
|
20
|
+
curl http://127.0.0.1:8788/healthz
|
|
21
|
+
# Expected: {"ok":true}
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Test Cases
|
|
25
|
+
|
|
26
|
+
### Test 1: Create a Recurring Schedule
|
|
27
|
+
|
|
28
|
+
Create a schedule that runs every minute:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
curl -X POST http://127.0.0.1:8788/api/schedule \
|
|
32
|
+
-H "Content-Type: application/json" \
|
|
33
|
+
-d '{
|
|
34
|
+
"message": "What is the current time?",
|
|
35
|
+
"description": "Test recurring - every minute",
|
|
36
|
+
"cronExpression": "* * * * *"
|
|
37
|
+
}' | jq .
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
**Expected Result:**
|
|
41
|
+
- HTTP 201 response
|
|
42
|
+
- JSON with `ok: true` and schedule details including an `id`
|
|
43
|
+
|
|
44
|
+
**Save the schedule ID** from the response for later tests.
|
|
45
|
+
|
|
46
|
+
### Test 2: List All Schedules
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
curl http://127.0.0.1:8788/api/schedule | jq .
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
**Expected Result:**
|
|
53
|
+
- HTTP 200 response
|
|
54
|
+
- JSON array containing the schedule from Test 1
|
|
55
|
+
|
|
56
|
+
### Test 3: Get a Specific Schedule
|
|
57
|
+
|
|
58
|
+
Replace `SCHEDULE_ID` with the ID from Test 1:
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
curl http://127.0.0.1:8788/api/schedule/SCHEDULE_ID | jq .
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
**Expected Result:**
|
|
65
|
+
- HTTP 200 response
|
|
66
|
+
- JSON with the schedule details
|
|
67
|
+
|
|
68
|
+
### Test 4: Wait for Schedule Execution
|
|
69
|
+
|
|
70
|
+
Wait for 60-65 seconds and observe:
|
|
71
|
+
|
|
72
|
+
1. Check the bridge logs for:
|
|
73
|
+
```
|
|
74
|
+
[timestamp] Executing scheduled job { scheduleId: '...', message: '...' }
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
2. Check your Telegram chat for a message like:
|
|
78
|
+
```
|
|
79
|
+
🔔 Scheduled task completed:
|
|
80
|
+
|
|
81
|
+
Test recurring - every minute
|
|
82
|
+
|
|
83
|
+
[Gemini's response about the current time]
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### Test 5: Create a One-Time Schedule
|
|
87
|
+
|
|
88
|
+
Create a schedule that runs in 30 seconds:
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
# Generate a timestamp 30 seconds from now
|
|
92
|
+
RUN_AT=$(date -u -d "+30 seconds" +"%Y-%m-%dT%H:%M:%SZ" 2>/dev/null || date -u -v+30S +"%Y-%m-%dT%H:%M:%SZ")
|
|
93
|
+
|
|
94
|
+
curl -X POST http://127.0.0.1:8788/api/schedule \
|
|
95
|
+
-H "Content-Type: application/json" \
|
|
96
|
+
-d "{
|
|
97
|
+
\"message\": \"This is a one-time test message\",
|
|
98
|
+
\"description\": \"Test one-time schedule\",
|
|
99
|
+
\"oneTime\": true,
|
|
100
|
+
\"runAt\": \"${RUN_AT}\"
|
|
101
|
+
}" | jq .
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
**Expected Result:**
|
|
105
|
+
- HTTP 201 response
|
|
106
|
+
- Schedule created with `oneTime: true` and `runAt` timestamp
|
|
107
|
+
|
|
108
|
+
Wait 30+ seconds and verify:
|
|
109
|
+
1. The message appears in Telegram
|
|
110
|
+
2. The schedule is automatically removed (verify with List API)
|
|
111
|
+
|
|
112
|
+
### Test 6: Test Invalid Cron Expression
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
curl -X POST http://127.0.0.1:8788/api/schedule \
|
|
116
|
+
-H "Content-Type: application/json" \
|
|
117
|
+
-d '{
|
|
118
|
+
"message": "Test",
|
|
119
|
+
"cronExpression": "invalid cron"
|
|
120
|
+
}' | jq .
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
**Expected Result:**
|
|
124
|
+
- HTTP 400 response
|
|
125
|
+
- Error message about invalid cron expression
|
|
126
|
+
|
|
127
|
+
### Test 7: Test Missing Required Fields
|
|
128
|
+
|
|
129
|
+
```bash
|
|
130
|
+
curl -X POST http://127.0.0.1:8788/api/schedule \
|
|
131
|
+
-H "Content-Type: application/json" \
|
|
132
|
+
-d '{}' | jq .
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
**Expected Result:**
|
|
136
|
+
- HTTP 400 response
|
|
137
|
+
- Error message about missing `message` field
|
|
138
|
+
|
|
139
|
+
### Test 8: Delete a Schedule
|
|
140
|
+
|
|
141
|
+
Replace `SCHEDULE_ID` with the ID from Test 1:
|
|
142
|
+
|
|
143
|
+
```bash
|
|
144
|
+
curl -X DELETE http://127.0.0.1:8788/api/schedule/SCHEDULE_ID | jq .
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
**Expected Result:**
|
|
148
|
+
- HTTP 200 response
|
|
149
|
+
- JSON with `ok: true` and message "Schedule removed"
|
|
150
|
+
|
|
151
|
+
Verify deletion by listing schedules - the deleted schedule should not appear.
|
|
152
|
+
|
|
153
|
+
### Test 9: Test with Gemini CLI Integration
|
|
154
|
+
|
|
155
|
+
Send a message to your Telegram bot:
|
|
156
|
+
|
|
157
|
+
```
|
|
158
|
+
Create a schedule to check the weather every day at 8am
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
**Expected Behavior:**
|
|
162
|
+
1. Gemini should parse your request
|
|
163
|
+
2. Call the scheduler API to create a recurring schedule
|
|
164
|
+
3. Respond with confirmation of the schedule creation
|
|
165
|
+
|
|
166
|
+
Then ask:
|
|
167
|
+
```
|
|
168
|
+
What schedules do I have?
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
**Expected Behavior:**
|
|
172
|
+
1. Gemini should query the scheduler API
|
|
173
|
+
2. List all active schedules
|
|
174
|
+
3. Show details in a human-readable format
|
|
175
|
+
|
|
176
|
+
Finally:
|
|
177
|
+
```
|
|
178
|
+
Cancel the weather check schedule
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
**Expected Behavior:**
|
|
182
|
+
1. Gemini should identify the schedule to delete
|
|
183
|
+
2. Call the DELETE endpoint
|
|
184
|
+
3. Confirm the schedule was removed
|
|
185
|
+
|
|
186
|
+
## Test with Authentication (Optional)
|
|
187
|
+
|
|
188
|
+
If `CALLBACK_AUTH_TOKEN` is set in your configuration:
|
|
189
|
+
|
|
190
|
+
```bash
|
|
191
|
+
# This should fail with 401 Unauthorized
|
|
192
|
+
curl -X POST http://127.0.0.1:8788/api/schedule \
|
|
193
|
+
-H "Content-Type: application/json" \
|
|
194
|
+
-d '{
|
|
195
|
+
"message": "Test",
|
|
196
|
+
"cronExpression": "0 9 * * *"
|
|
197
|
+
}' | jq .
|
|
198
|
+
|
|
199
|
+
# This should succeed
|
|
200
|
+
curl -X POST http://127.0.0.1:8788/api/schedule \
|
|
201
|
+
-H "Content-Type: application/json" \
|
|
202
|
+
-H "x-callback-token: YOUR_TOKEN_HERE" \
|
|
203
|
+
-d '{
|
|
204
|
+
"message": "Test",
|
|
205
|
+
"cronExpression": "0 9 * * *"
|
|
206
|
+
}' | jq .
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
## Cleanup
|
|
210
|
+
|
|
211
|
+
After testing, remove all test schedules:
|
|
212
|
+
|
|
213
|
+
```bash
|
|
214
|
+
# List all schedules
|
|
215
|
+
curl http://127.0.0.1:8788/api/schedule | jq -r '.schedules[].id' | while read id; do
|
|
216
|
+
echo "Deleting schedule: $id"
|
|
217
|
+
curl -X DELETE http://127.0.0.1:8788/api/schedule/$id
|
|
218
|
+
done
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
## Common Issues
|
|
222
|
+
|
|
223
|
+
### Schedule not executing
|
|
224
|
+
- Verify the bridge is still running
|
|
225
|
+
- Check bridge logs for errors
|
|
226
|
+
- Ensure Telegram chat binding is established (send a message to bot first)
|
|
227
|
+
|
|
228
|
+
### 404 Not Found
|
|
229
|
+
- Verify the callback server is running on port 8788
|
|
230
|
+
- Check if another process is using the port
|
|
231
|
+
|
|
232
|
+
### No response in Telegram
|
|
233
|
+
- Ensure you've sent at least one message to the bot
|
|
234
|
+
- Check if `lastIncomingChatId` is logged in the bridge output
|
|
235
|
+
- Verify Gemini CLI is properly installed and configured
|
|
236
|
+
|
|
237
|
+
### Timezone issues
|
|
238
|
+
- Set the `TZ` environment variable to your timezone
|
|
239
|
+
- Example: `TZ=America/New_York npm run dev`
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export function buildPermissionResponse(options: any[], permissionStrategy: string) {
|
|
2
|
+
if (!Array.isArray(options) || options.length === 0) {
|
|
3
|
+
return { outcome: { outcome: 'cancelled' } };
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
if (permissionStrategy === 'cancelled') {
|
|
7
|
+
return { outcome: { outcome: 'cancelled' } };
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const preferred = options.find((option: any) => option.kind === permissionStrategy);
|
|
11
|
+
const selectedOption = preferred || options[0];
|
|
12
|
+
|
|
13
|
+
return {
|
|
14
|
+
outcome: {
|
|
15
|
+
outcome: 'selected',
|
|
16
|
+
optionId: selectedOption.optionId,
|
|
17
|
+
},
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function noOpAcpFileOperation(_params: any) {
|
|
22
|
+
return {};
|
|
23
|
+
}
|
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { Readable, Writable } from 'node:stream';
|
|
3
|
+
import * as acp from '@agentclientprotocol/sdk';
|
|
4
|
+
import { buildPermissionResponse, noOpAcpFileOperation } from './clientHelpers.js';
|
|
5
|
+
import { getErrorMessage } from '../utils/error.js';
|
|
6
|
+
|
|
7
|
+
const ACP_DEBUG_STREAM = String(process.env.ACP_DEBUG_STREAM || '').toLowerCase() === 'true';
|
|
8
|
+
const GEMINI_KILL_GRACE_MS = parseInt(process.env.GEMINI_KILL_GRACE_MS || '5000', 10);
|
|
9
|
+
|
|
10
|
+
export interface TempAcpRunnerOptions {
|
|
11
|
+
scheduleId: string;
|
|
12
|
+
promptForGemini: string;
|
|
13
|
+
command: string;
|
|
14
|
+
args: string[];
|
|
15
|
+
cwd: string;
|
|
16
|
+
timeoutMs: number;
|
|
17
|
+
noOutputTimeoutMs: number;
|
|
18
|
+
permissionStrategy: string;
|
|
19
|
+
stderrTailMaxChars?: number;
|
|
20
|
+
logInfo: (message: string, details?: unknown) => void;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function runPromptWithTempAcp(options: TempAcpRunnerOptions): Promise<string> {
|
|
24
|
+
const {
|
|
25
|
+
scheduleId,
|
|
26
|
+
promptForGemini,
|
|
27
|
+
command,
|
|
28
|
+
args,
|
|
29
|
+
cwd,
|
|
30
|
+
timeoutMs,
|
|
31
|
+
noOutputTimeoutMs,
|
|
32
|
+
permissionStrategy,
|
|
33
|
+
stderrTailMaxChars = 4000,
|
|
34
|
+
logInfo,
|
|
35
|
+
} = options;
|
|
36
|
+
|
|
37
|
+
const tempProcess = spawn(command, args, {
|
|
38
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
39
|
+
cwd,
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
logInfo('Scheduler temp Gemini ACP process started', {
|
|
43
|
+
scheduleId,
|
|
44
|
+
pid: tempProcess.pid,
|
|
45
|
+
command,
|
|
46
|
+
args,
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
let tempConnection: any = null;
|
|
50
|
+
let tempSessionId: string | null = null;
|
|
51
|
+
let tempCollector: { onActivity: () => void; append: (chunk: string) => void } | null = null;
|
|
52
|
+
let tempStderrTail = '';
|
|
53
|
+
let noOutputTimeout: NodeJS.Timeout | null = null;
|
|
54
|
+
let overallTimeout: NodeJS.Timeout | null = null;
|
|
55
|
+
let cleanedUp = false;
|
|
56
|
+
|
|
57
|
+
const terminateProcessGracefully = () => {
|
|
58
|
+
return new Promise<void>((resolve) => {
|
|
59
|
+
if (!tempProcess || tempProcess.killed || tempProcess.exitCode !== null) {
|
|
60
|
+
resolve();
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
let settled = false;
|
|
65
|
+
|
|
66
|
+
const finalize = (reason: string) => {
|
|
67
|
+
if (settled) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
settled = true;
|
|
71
|
+
logInfo('Scheduler temp Gemini process termination finalized', {
|
|
72
|
+
scheduleId,
|
|
73
|
+
pid: tempProcess.pid,
|
|
74
|
+
reason,
|
|
75
|
+
});
|
|
76
|
+
resolve();
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
tempProcess.once('exit', () => finalize('exit'));
|
|
80
|
+
|
|
81
|
+
logInfo('Scheduler temp Gemini process SIGTERM', {
|
|
82
|
+
scheduleId,
|
|
83
|
+
pid: tempProcess.pid,
|
|
84
|
+
graceMs: GEMINI_KILL_GRACE_MS,
|
|
85
|
+
});
|
|
86
|
+
tempProcess.kill('SIGTERM');
|
|
87
|
+
|
|
88
|
+
setTimeout(() => {
|
|
89
|
+
if (settled || tempProcess.killed || tempProcess.exitCode !== null) {
|
|
90
|
+
finalize('already-exited');
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
logInfo('Scheduler temp Gemini process SIGKILL escalation', {
|
|
95
|
+
scheduleId,
|
|
96
|
+
pid: tempProcess.pid,
|
|
97
|
+
});
|
|
98
|
+
tempProcess.kill('SIGKILL');
|
|
99
|
+
finalize('sigkill');
|
|
100
|
+
}, Math.max(0, GEMINI_KILL_GRACE_MS));
|
|
101
|
+
});
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const appendTempStderrTail = (text: string) => {
|
|
105
|
+
tempStderrTail = `${tempStderrTail}${text}`;
|
|
106
|
+
if (tempStderrTail.length > stderrTailMaxChars) {
|
|
107
|
+
tempStderrTail = tempStderrTail.slice(-stderrTailMaxChars);
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const clearTimers = () => {
|
|
112
|
+
if (noOutputTimeout) {
|
|
113
|
+
clearTimeout(noOutputTimeout);
|
|
114
|
+
noOutputTimeout = null;
|
|
115
|
+
}
|
|
116
|
+
if (overallTimeout) {
|
|
117
|
+
clearTimeout(overallTimeout);
|
|
118
|
+
overallTimeout = null;
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
const cleanup = async () => {
|
|
123
|
+
if (cleanedUp) {
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
cleanedUp = true;
|
|
127
|
+
clearTimers();
|
|
128
|
+
|
|
129
|
+
try {
|
|
130
|
+
if (tempConnection && tempSessionId) {
|
|
131
|
+
await tempConnection.cancel({ sessionId: tempSessionId });
|
|
132
|
+
}
|
|
133
|
+
} catch (_) {
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (!tempProcess.killed && tempProcess.exitCode === null) {
|
|
137
|
+
await terminateProcessGracefully();
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
logInfo('Scheduler temp Gemini ACP process cleanup complete', {
|
|
141
|
+
scheduleId,
|
|
142
|
+
pid: tempProcess.pid,
|
|
143
|
+
});
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
tempProcess.stderr.on('data', (chunk: Buffer) => {
|
|
147
|
+
const rawText = chunk.toString();
|
|
148
|
+
appendTempStderrTail(rawText);
|
|
149
|
+
const text = rawText.trim();
|
|
150
|
+
if (text) {
|
|
151
|
+
console.error(`[gemini:scheduler:${scheduleId}] ${text}`);
|
|
152
|
+
}
|
|
153
|
+
tempCollector?.onActivity();
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
tempProcess.on('error', (error: Error) => {
|
|
157
|
+
logInfo('Scheduler temp Gemini ACP process error', {
|
|
158
|
+
scheduleId,
|
|
159
|
+
pid: tempProcess.pid,
|
|
160
|
+
error: error.message,
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
tempProcess.on('close', (code: number | null, signal: NodeJS.Signals | null) => {
|
|
165
|
+
logInfo('Scheduler temp Gemini ACP process exited', {
|
|
166
|
+
scheduleId,
|
|
167
|
+
pid: tempProcess.pid,
|
|
168
|
+
code,
|
|
169
|
+
signal,
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
try {
|
|
174
|
+
const input = Writable.toWeb(tempProcess.stdin) as unknown as WritableStream<Uint8Array>;
|
|
175
|
+
const output = Readable.toWeb(tempProcess.stdout) as unknown as ReadableStream<Uint8Array>;
|
|
176
|
+
const stream = acp.ndJsonStream(input, output);
|
|
177
|
+
|
|
178
|
+
const tempClient = {
|
|
179
|
+
async requestPermission(params: any) {
|
|
180
|
+
return buildPermissionResponse(params?.options, permissionStrategy);
|
|
181
|
+
},
|
|
182
|
+
async sessionUpdate(params: any) {
|
|
183
|
+
if (!tempCollector || params.sessionId !== tempSessionId) {
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
tempCollector.onActivity();
|
|
188
|
+
|
|
189
|
+
if (params.update?.sessionUpdate === 'agent_message_chunk' && params.update?.content?.type === 'text') {
|
|
190
|
+
const chunkText = params.update.content.text;
|
|
191
|
+
tempCollector.append(chunkText);
|
|
192
|
+
}
|
|
193
|
+
},
|
|
194
|
+
async readTextFile(_params: any) {
|
|
195
|
+
return noOpAcpFileOperation(_params);
|
|
196
|
+
},
|
|
197
|
+
async writeTextFile(_params: any) {
|
|
198
|
+
return noOpAcpFileOperation(_params);
|
|
199
|
+
},
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
tempConnection = new acp.ClientSideConnection(() => tempClient, stream);
|
|
203
|
+
|
|
204
|
+
await tempConnection.initialize({
|
|
205
|
+
protocolVersion: acp.PROTOCOL_VERSION,
|
|
206
|
+
clientCapabilities: {},
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
const session = await tempConnection.newSession({
|
|
210
|
+
cwd,
|
|
211
|
+
mcpServers: [],
|
|
212
|
+
});
|
|
213
|
+
tempSessionId = session.sessionId;
|
|
214
|
+
|
|
215
|
+
return await new Promise<string>((resolve, reject) => {
|
|
216
|
+
let response = '';
|
|
217
|
+
let settled = false;
|
|
218
|
+
const startedAt = Date.now();
|
|
219
|
+
let chunkCount = 0;
|
|
220
|
+
let firstChunkAt: number | null = null;
|
|
221
|
+
|
|
222
|
+
const settle = async (handler: () => void) => {
|
|
223
|
+
if (settled) {
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
settled = true;
|
|
227
|
+
await cleanup();
|
|
228
|
+
handler();
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
const refreshNoOutputTimer = () => {
|
|
232
|
+
if (!noOutputTimeoutMs || noOutputTimeoutMs <= 0) {
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
if (noOutputTimeout) {
|
|
236
|
+
clearTimeout(noOutputTimeout);
|
|
237
|
+
}
|
|
238
|
+
noOutputTimeout = setTimeout(async () => {
|
|
239
|
+
try {
|
|
240
|
+
if (tempConnection && tempSessionId) {
|
|
241
|
+
await tempConnection.cancel({ sessionId: tempSessionId });
|
|
242
|
+
}
|
|
243
|
+
} catch (_) {
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
await settle(() => reject(new Error(`Scheduler Gemini ACP produced no output for ${noOutputTimeoutMs}ms`)));
|
|
247
|
+
}, noOutputTimeoutMs);
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
overallTimeout = setTimeout(async () => {
|
|
251
|
+
try {
|
|
252
|
+
if (tempConnection && tempSessionId) {
|
|
253
|
+
await tempConnection.cancel({ sessionId: tempSessionId });
|
|
254
|
+
}
|
|
255
|
+
} catch (_) {
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
await settle(() => reject(new Error(`Scheduler Gemini ACP timed out after ${timeoutMs}ms`)));
|
|
259
|
+
}, timeoutMs);
|
|
260
|
+
|
|
261
|
+
tempCollector = {
|
|
262
|
+
onActivity: refreshNoOutputTimer,
|
|
263
|
+
append: (chunk: string) => {
|
|
264
|
+
refreshNoOutputTimer();
|
|
265
|
+
chunkCount += 1;
|
|
266
|
+
if (!firstChunkAt) {
|
|
267
|
+
firstChunkAt = Date.now();
|
|
268
|
+
}
|
|
269
|
+
if (ACP_DEBUG_STREAM) {
|
|
270
|
+
logInfo('Scheduler ACP chunk received', {
|
|
271
|
+
scheduleId,
|
|
272
|
+
chunkIndex: chunkCount,
|
|
273
|
+
chunkLength: chunk.length,
|
|
274
|
+
elapsedMs: Date.now() - startedAt,
|
|
275
|
+
bufferLengthBeforeAppend: response.length,
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
response += chunk;
|
|
279
|
+
},
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
refreshNoOutputTimer();
|
|
283
|
+
|
|
284
|
+
tempConnection.prompt({
|
|
285
|
+
sessionId: tempSessionId,
|
|
286
|
+
prompt: [{ type: 'text', text: promptForGemini }],
|
|
287
|
+
})
|
|
288
|
+
.then(async (result: any) => {
|
|
289
|
+
if (ACP_DEBUG_STREAM) {
|
|
290
|
+
logInfo('Scheduler ACP prompt stop reason', {
|
|
291
|
+
scheduleId,
|
|
292
|
+
stopReason: result?.stopReason || '(none)',
|
|
293
|
+
chunkCount,
|
|
294
|
+
firstChunkDelayMs: firstChunkAt ? firstChunkAt - startedAt : null,
|
|
295
|
+
elapsedMs: Date.now() - startedAt,
|
|
296
|
+
bufferedLength: response.length,
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
if (result?.stopReason === 'cancelled' && !response) {
|
|
300
|
+
await settle(() => reject(new Error('Scheduler Gemini ACP prompt was cancelled')));
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
await settle(() => resolve(response || 'No response received.'));
|
|
305
|
+
})
|
|
306
|
+
.catch(async (error: any) => {
|
|
307
|
+
await settle(() => reject(new Error(error?.message || 'Scheduler Gemini ACP prompt failed')));
|
|
308
|
+
});
|
|
309
|
+
});
|
|
310
|
+
} catch (error: any) {
|
|
311
|
+
logInfo('Scheduler temporary Gemini ACP run failed', {
|
|
312
|
+
scheduleId,
|
|
313
|
+
error: getErrorMessage(error),
|
|
314
|
+
stderrTail: tempStderrTail || '(empty)',
|
|
315
|
+
});
|
|
316
|
+
throw error;
|
|
317
|
+
} finally {
|
|
318
|
+
await cleanup();
|
|
319
|
+
}
|
|
320
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"telegramToken": "your_telegram_bot_token_here",
|
|
3
|
+
"typingIntervalMs": 4000,
|
|
4
|
+
"geminiCommand": "gemini",
|
|
5
|
+
"geminiApprovalMode": "yolo",
|
|
6
|
+
"acpPermissionStrategy": "allow_once",
|
|
7
|
+
"geminiTimeoutMs": 900000,
|
|
8
|
+
"geminiNoOutputTimeoutMs": 60000,
|
|
9
|
+
"geminiKillGraceMs": 5000,
|
|
10
|
+
"acpStreamStdout": false,
|
|
11
|
+
"acpDebugStream": false,
|
|
12
|
+
"maxResponseLength": 4000,
|
|
13
|
+
"heartbeatIntervalMs": 60000,
|
|
14
|
+
"callbackHost": "localhost",
|
|
15
|
+
"callbackPort": 8788,
|
|
16
|
+
"callbackAuthToken": "",
|
|
17
|
+
"callbackMaxBodyBytes": 65536,
|
|
18
|
+
"schedulesFilePath": "~/.clawless/schedules.json"
|
|
19
|
+
}
|