runsignal 0.1.1 → 0.1.3
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/LICENSE +22 -22
- package/README.md +42 -25
- package/bin/runsignal.js +244 -33
- package/package.json +48 -48
- package/src/api-client.js +107 -107
package/LICENSE
CHANGED
|
@@ -1,22 +1,22 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2026 RunSignal
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
|
13
|
-
copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE.
|
|
22
|
-
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 RunSignal
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
22
|
+
|
package/README.md
CHANGED
|
@@ -1,37 +1,54 @@
|
|
|
1
|
-
# RunSignal CLI
|
|
2
|
-
|
|
3
|
-
RunSignal CLI lets you initialize a machine, wrap agent commands, and inspect event decisions.
|
|
4
|
-
|
|
5
|
-
## Install
|
|
1
|
+
# RunSignal CLI
|
|
2
|
+
|
|
3
|
+
RunSignal CLI lets you initialize a machine, wrap agent commands, and inspect event decisions.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm i -g runsignal
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Or run directly:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npx runsignal --help
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Commands
|
|
18
|
+
|
|
19
|
+
- `runsignal login`
|
|
20
|
+
- `runsignal init`
|
|
21
|
+
- `runsignal wrap -- <command>`
|
|
22
|
+
- `runsignal send --message "..."`
|
|
23
|
+
- `runsignal status <eventId>`
|
|
24
|
+
- `runsignal logout`
|
|
25
|
+
- `runsignal hooks install --shell powershell`
|
|
26
|
+
- `runsignal hooks doctor --shell powershell`
|
|
27
|
+
- `runsignal hooks uninstall --shell powershell`
|
|
28
|
+
|
|
29
|
+
## Quickstart
|
|
6
30
|
|
|
7
31
|
```bash
|
|
8
|
-
|
|
32
|
+
npx runsignal@latest login
|
|
33
|
+
npx runsignal@latest init
|
|
34
|
+
npx runsignal@latest wrap -- claude "refactor auth flow"
|
|
9
35
|
```
|
|
10
36
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
```bash
|
|
14
|
-
npx runsignal --help
|
|
15
|
-
```
|
|
37
|
+
## Environment
|
|
16
38
|
|
|
17
|
-
|
|
39
|
+
- `RUNSIGNAL_APP_URL` to override app URL (default: `https://runsignal.dev`)
|
|
18
40
|
|
|
19
|
-
|
|
20
|
-
- `runsignal init --provider telegram|slack`
|
|
21
|
-
- `runsignal wrap -- <command>`
|
|
22
|
-
- `runsignal send --message "..."`
|
|
23
|
-
- `runsignal status <eventId>`
|
|
24
|
-
- `runsignal logout`
|
|
41
|
+
## Automatic Wrapping
|
|
25
42
|
|
|
26
|
-
|
|
43
|
+
To keep using `claude ...` / `codex ...` normally while routing through RunSignal:
|
|
27
44
|
|
|
28
45
|
```bash
|
|
29
|
-
runsignal
|
|
30
|
-
runsignal init --provider telegram
|
|
31
|
-
runsignal wrap -- claude "refactor auth flow"
|
|
46
|
+
runsignal hooks install --shell powershell
|
|
32
47
|
```
|
|
33
48
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
- `RUNSIGNAL_APP_URL` to override app URL (default: `http://localhost:3000`)
|
|
49
|
+
Then restart terminal and verify:
|
|
37
50
|
|
|
51
|
+
```bash
|
|
52
|
+
runsignal hooks doctor --shell powershell
|
|
53
|
+
```
|
|
54
|
+
|
package/bin/runsignal.js
CHANGED
|
@@ -5,8 +5,6 @@ import {spawn, execSync} from 'node:child_process';
|
|
|
5
5
|
import {promises as fs} from 'node:fs';
|
|
6
6
|
import os from 'node:os';
|
|
7
7
|
import path from 'node:path';
|
|
8
|
-
import readline from 'node:readline/promises';
|
|
9
|
-
import {stdin as input, stdout as output} from 'node:process';
|
|
10
8
|
import {
|
|
11
9
|
getEventAnswer,
|
|
12
10
|
getEventDecision,
|
|
@@ -15,7 +13,7 @@ import {
|
|
|
15
13
|
sendEvent
|
|
16
14
|
} from '../src/api-client.js';
|
|
17
15
|
|
|
18
|
-
const DEFAULT_APP_URL = process.env.RUNSIGNAL_APP_URL || '
|
|
16
|
+
const DEFAULT_APP_URL = process.env.RUNSIGNAL_APP_URL || 'https://runsignal.dev';
|
|
19
17
|
const CONFIG_DIR = path.join(os.homedir(), '.runsignal');
|
|
20
18
|
const CONFIG_PATH = path.join(CONFIG_DIR, 'config.json');
|
|
21
19
|
|
|
@@ -42,6 +40,8 @@ function toUserMessage(errorCode) {
|
|
|
42
40
|
if (code.includes('message_required')) return 'Message is required. Use --message "..."';
|
|
43
41
|
if (code.includes('event_id_required')) return 'Event id is required. Usage: runsignal status <eventId>';
|
|
44
42
|
if (code.includes('init_failed')) return 'Initialization failed. Check token/provider config and try again.';
|
|
43
|
+
if (code.includes('hooks_shell_not_supported'))
|
|
44
|
+
return 'Hooks shell not supported. Use --shell powershell|bash|zsh|all.';
|
|
45
45
|
return code || 'Unknown error';
|
|
46
46
|
}
|
|
47
47
|
|
|
@@ -100,13 +100,159 @@ function sleep(ms) {
|
|
|
100
100
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
-
|
|
104
|
-
|
|
103
|
+
const HOOK_START = '# >>> RunSignal hooks start >>>';
|
|
104
|
+
const HOOK_END = '# <<< RunSignal hooks end <<<';
|
|
105
|
+
|
|
106
|
+
function stripHookBlock(content) {
|
|
107
|
+
const start = content.indexOf(HOOK_START);
|
|
108
|
+
const end = content.indexOf(HOOK_END);
|
|
109
|
+
if (start === -1 || end === -1 || end < start) {
|
|
110
|
+
return content;
|
|
111
|
+
}
|
|
112
|
+
const before = content.slice(0, start).trimEnd();
|
|
113
|
+
const after = content.slice(end + HOOK_END.length).trimStart();
|
|
114
|
+
if (!before && !after) return '';
|
|
115
|
+
if (!before) return `${after}\n`;
|
|
116
|
+
if (!after) return `${before}\n`;
|
|
117
|
+
return `${before}\n\n${after}\n`;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function buildHookBlock(shellKind) {
|
|
121
|
+
if (shellKind === 'powershell') {
|
|
122
|
+
return `${HOOK_START}
|
|
123
|
+
function claude { runsignal wrap -- claude @args }
|
|
124
|
+
function codex { runsignal wrap -- codex @args }
|
|
125
|
+
function cursor { runsignal wrap -- cursor @args }
|
|
126
|
+
function gemini { runsignal wrap -- gemini @args }
|
|
127
|
+
function aider { runsignal wrap -- aider @args }
|
|
128
|
+
${HOOK_END}
|
|
129
|
+
`;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return `${HOOK_START}
|
|
133
|
+
claude() { runsignal wrap -- claude "$@"; }
|
|
134
|
+
codex() { runsignal wrap -- codex "$@"; }
|
|
135
|
+
cursor() { runsignal wrap -- cursor "$@"; }
|
|
136
|
+
gemini() { runsignal wrap -- gemini "$@"; }
|
|
137
|
+
aider() { runsignal wrap -- aider "$@"; }
|
|
138
|
+
${HOOK_END}
|
|
139
|
+
`;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function resolveHookTargets(shellOption) {
|
|
143
|
+
const shell = String(shellOption || 'auto').toLowerCase();
|
|
144
|
+
const home = os.homedir();
|
|
145
|
+
|
|
146
|
+
if (process.platform === 'win32') {
|
|
147
|
+
const powershellTargets = [
|
|
148
|
+
{
|
|
149
|
+
path: path.join(home, 'Documents', 'PowerShell', 'Microsoft.PowerShell_profile.ps1'),
|
|
150
|
+
kind: 'powershell'
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
path: path.join(home, 'Documents', 'WindowsPowerShell', 'Microsoft.PowerShell_profile.ps1'),
|
|
154
|
+
kind: 'powershell'
|
|
155
|
+
}
|
|
156
|
+
];
|
|
157
|
+
if (shell === 'auto' || shell === 'powershell' || shell === 'all') {
|
|
158
|
+
return powershellTargets;
|
|
159
|
+
}
|
|
160
|
+
return [];
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (shell === 'zsh') return [{path: path.join(home, '.zshrc'), kind: 'posix'}];
|
|
164
|
+
if (shell === 'bash') return [{path: path.join(home, '.bashrc'), kind: 'posix'}];
|
|
165
|
+
if (shell === 'all') {
|
|
166
|
+
return [
|
|
167
|
+
{path: path.join(home, '.bashrc'), kind: 'posix'},
|
|
168
|
+
{path: path.join(home, '.zshrc'), kind: 'posix'}
|
|
169
|
+
];
|
|
170
|
+
}
|
|
171
|
+
if (shell === 'auto') {
|
|
172
|
+
const shellEnv = String(process.env.SHELL || '').toLowerCase();
|
|
173
|
+
if (shellEnv.includes('zsh')) return [{path: path.join(home, '.zshrc'), kind: 'posix'}];
|
|
174
|
+
return [{path: path.join(home, '.bashrc'), kind: 'posix'}];
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return [];
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
async function readTextFile(filePath) {
|
|
105
181
|
try {
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
182
|
+
return await fs.readFile(filePath, 'utf8');
|
|
183
|
+
} catch {
|
|
184
|
+
return '';
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
async function runHooksInstall(options) {
|
|
189
|
+
const targets = resolveHookTargets(options.shell);
|
|
190
|
+
if (!targets.length) {
|
|
191
|
+
throw new Error('hooks_shell_not_supported');
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
for (const target of targets) {
|
|
195
|
+
const existing = await readTextFile(target.path);
|
|
196
|
+
const cleaned = stripHookBlock(existing).trimEnd();
|
|
197
|
+
const block = buildHookBlock(target.kind);
|
|
198
|
+
const next = cleaned ? `${cleaned}\n\n${block}` : block;
|
|
199
|
+
await fs.mkdir(path.dirname(target.path), {recursive: true});
|
|
200
|
+
await fs.writeFile(target.path, next, 'utf8');
|
|
201
|
+
console.log(`Hooks installed: ${target.path}`);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
console.log('Restart your terminal to apply hooks.');
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
async function runHooksUninstall(options) {
|
|
208
|
+
const targets = resolveHookTargets(options.shell);
|
|
209
|
+
if (!targets.length) {
|
|
210
|
+
throw new Error('hooks_shell_not_supported');
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
for (const target of targets) {
|
|
214
|
+
const existing = await readTextFile(target.path);
|
|
215
|
+
if (!existing) {
|
|
216
|
+
console.log(`Hooks not present: ${target.path}`);
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
const next = stripHookBlock(existing);
|
|
220
|
+
if (next === existing) {
|
|
221
|
+
console.log(`Hooks not present: ${target.path}`);
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
await fs.writeFile(target.path, next, 'utf8');
|
|
225
|
+
console.log(`Hooks removed: ${target.path}`);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
async function runHooksDoctor(options) {
|
|
230
|
+
const targets = resolveHookTargets(options.shell);
|
|
231
|
+
if (!targets.length) {
|
|
232
|
+
throw new Error('hooks_shell_not_supported');
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
let healthy = true;
|
|
236
|
+
for (const target of targets) {
|
|
237
|
+
const existing = await readTextFile(target.path);
|
|
238
|
+
const hasHooks = existing.includes(HOOK_START) && existing.includes(HOOK_END);
|
|
239
|
+
if (!hasHooks) {
|
|
240
|
+
healthy = false;
|
|
241
|
+
}
|
|
242
|
+
console.log(
|
|
243
|
+
JSON.stringify(
|
|
244
|
+
{
|
|
245
|
+
path: target.path,
|
|
246
|
+
installed: hasHooks
|
|
247
|
+
},
|
|
248
|
+
null,
|
|
249
|
+
2
|
|
250
|
+
)
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (!healthy) {
|
|
255
|
+
process.exitCode = 1;
|
|
110
256
|
}
|
|
111
257
|
}
|
|
112
258
|
|
|
@@ -183,6 +329,18 @@ function extractInterestingLine(chunk) {
|
|
|
183
329
|
return null;
|
|
184
330
|
}
|
|
185
331
|
|
|
332
|
+
function quoteForShell(arg) {
|
|
333
|
+
const value = String(arg ?? '');
|
|
334
|
+
if (value.length === 0) {
|
|
335
|
+
return '""';
|
|
336
|
+
}
|
|
337
|
+
if (!/[\s"]/g.test(value)) {
|
|
338
|
+
return value;
|
|
339
|
+
}
|
|
340
|
+
const escaped = value.replace(/(["\\$`])/g, '\\$1');
|
|
341
|
+
return `"${escaped}"`;
|
|
342
|
+
}
|
|
343
|
+
|
|
186
344
|
async function runLogin(options) {
|
|
187
345
|
const config = await readConfig();
|
|
188
346
|
const baseUrl = String(options.appUrl || config.app_url || DEFAULT_APP_URL).replace(/\/$/, '');
|
|
@@ -239,10 +397,20 @@ async function runInit(options) {
|
|
|
239
397
|
throw new Error('not_logged_in');
|
|
240
398
|
}
|
|
241
399
|
|
|
400
|
+
const hasSlackWebhook = typeof options.webhookUrl === 'string' && options.webhookUrl.length > 0;
|
|
401
|
+
const hasTelegramConfig =
|
|
402
|
+
typeof options.botToken === 'string' ||
|
|
403
|
+
typeof options.chatId === 'string' ||
|
|
404
|
+
typeof config.provider === 'string';
|
|
242
405
|
let provider = String(options.provider || '').toLowerCase();
|
|
243
406
|
if (provider !== 'telegram' && provider !== 'slack') {
|
|
244
|
-
|
|
245
|
-
|
|
407
|
+
if (hasSlackWebhook) {
|
|
408
|
+
provider = 'slack';
|
|
409
|
+
} else if (hasTelegramConfig) {
|
|
410
|
+
provider = 'telegram';
|
|
411
|
+
} else {
|
|
412
|
+
provider = 'telegram';
|
|
413
|
+
}
|
|
246
414
|
}
|
|
247
415
|
|
|
248
416
|
if (provider !== 'telegram' && provider !== 'slack') {
|
|
@@ -253,21 +421,25 @@ async function runInit(options) {
|
|
|
253
421
|
const providerConfig = {};
|
|
254
422
|
|
|
255
423
|
if (provider === 'telegram') {
|
|
256
|
-
const botToken = String(options.botToken ||
|
|
257
|
-
const chatId = String(options.chatId ||
|
|
258
|
-
if (
|
|
424
|
+
const botToken = String(options.botToken || '').trim();
|
|
425
|
+
const chatId = String(options.chatId || '').trim();
|
|
426
|
+
if ((botToken && !chatId) || (!botToken && chatId)) {
|
|
259
427
|
throw new Error('telegram_config_invalid');
|
|
260
428
|
}
|
|
261
|
-
|
|
262
|
-
|
|
429
|
+
if (botToken && chatId) {
|
|
430
|
+
providerConfig.bot_token = botToken;
|
|
431
|
+
providerConfig.chat_id = chatId;
|
|
432
|
+
}
|
|
263
433
|
}
|
|
264
434
|
|
|
265
435
|
if (provider === 'slack') {
|
|
266
|
-
const webhookUrl = String(options.webhookUrl ||
|
|
267
|
-
if (
|
|
268
|
-
|
|
436
|
+
const webhookUrl = String(options.webhookUrl || '').trim();
|
|
437
|
+
if (webhookUrl) {
|
|
438
|
+
if (!webhookUrl.startsWith('https://')) {
|
|
439
|
+
throw new Error('slack_webhook_invalid');
|
|
440
|
+
}
|
|
441
|
+
providerConfig.webhook_url = webhookUrl;
|
|
269
442
|
}
|
|
270
|
-
providerConfig.webhook_url = webhookUrl;
|
|
271
443
|
}
|
|
272
444
|
|
|
273
445
|
const register = await apiFetch(baseUrl, '/api/cli/machines/register', {
|
|
@@ -323,9 +495,14 @@ async function runWrap(commandParts) {
|
|
|
323
495
|
agent_label: agentLabel
|
|
324
496
|
};
|
|
325
497
|
|
|
326
|
-
const commandString =
|
|
498
|
+
const commandString =
|
|
499
|
+
commandParts.length === 1
|
|
500
|
+
? String(commandParts[0])
|
|
501
|
+
: commandParts.map((part) => quoteForShell(part)).join(' ');
|
|
502
|
+
const interactivePassthrough =
|
|
503
|
+
commandParts.length === 1 && !/[\s"'`]/.test(String(commandParts[0]));
|
|
327
504
|
const child = spawn(commandString, {
|
|
328
|
-
stdio: ['inherit', 'pipe', 'pipe'],
|
|
505
|
+
stdio: interactivePassthrough ? 'inherit' : ['inherit', 'pipe', 'pipe'],
|
|
329
506
|
shell: true,
|
|
330
507
|
env: process.env
|
|
331
508
|
});
|
|
@@ -333,6 +510,12 @@ async function runWrap(commandParts) {
|
|
|
333
510
|
let sawApproval = false;
|
|
334
511
|
let sawQuestion = false;
|
|
335
512
|
let lastInterestingLine = '';
|
|
513
|
+
const pendingTasks = new Set();
|
|
514
|
+
|
|
515
|
+
const queueTask = (promise) => {
|
|
516
|
+
pendingTasks.add(promise);
|
|
517
|
+
promise.finally(() => pendingTasks.delete(promise));
|
|
518
|
+
};
|
|
336
519
|
|
|
337
520
|
const handleChunk = async (chunkRaw) => {
|
|
338
521
|
const chunk = chunkRaw.toString('utf8');
|
|
@@ -410,18 +593,21 @@ async function runWrap(commandParts) {
|
|
|
410
593
|
}
|
|
411
594
|
};
|
|
412
595
|
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
596
|
+
if (!interactivePassthrough) {
|
|
597
|
+
child.stdout.on('data', (chunk) => {
|
|
598
|
+
process.stdout.write(chunk);
|
|
599
|
+
queueTask(handleChunk(chunk));
|
|
600
|
+
});
|
|
601
|
+
child.stderr.on('data', (chunk) => {
|
|
602
|
+
process.stderr.write(chunk);
|
|
603
|
+
queueTask(handleChunk(chunk));
|
|
604
|
+
});
|
|
605
|
+
}
|
|
421
606
|
|
|
422
607
|
const exitCode = await new Promise((resolve) => {
|
|
423
608
|
child.on('close', (code) => resolve(code ?? 1));
|
|
424
609
|
});
|
|
610
|
+
await Promise.allSettled(Array.from(pendingTasks));
|
|
425
611
|
|
|
426
612
|
const completionMessage =
|
|
427
613
|
exitCode === 0
|
|
@@ -447,7 +633,9 @@ async function runWrap(commandParts) {
|
|
|
447
633
|
// no-op
|
|
448
634
|
}
|
|
449
635
|
|
|
450
|
-
process.
|
|
636
|
+
// Avoid hard process exit on Windows: let Node close handles cleanly.
|
|
637
|
+
process.exitCode = exitCode;
|
|
638
|
+
return;
|
|
451
639
|
}
|
|
452
640
|
|
|
453
641
|
async function runTest() {
|
|
@@ -607,7 +795,7 @@ async function runLogout() {
|
|
|
607
795
|
|
|
608
796
|
const program = new Command();
|
|
609
797
|
|
|
610
|
-
program.name('runsignal').description('RunSignal CLI').version('0.1.
|
|
798
|
+
program.name('runsignal').description('RunSignal CLI').version('0.1.3');
|
|
611
799
|
|
|
612
800
|
program
|
|
613
801
|
.command('login')
|
|
@@ -618,7 +806,7 @@ program
|
|
|
618
806
|
|
|
619
807
|
program
|
|
620
808
|
.command('init')
|
|
621
|
-
.option('--provider <provider>', 'telegram or slack')
|
|
809
|
+
.option('--provider <provider>', 'telegram or slack (optional)')
|
|
622
810
|
.option('--app-url <url>', 'RunSignal app base URL')
|
|
623
811
|
.option('--machine-name <name>', 'Machine name override')
|
|
624
812
|
.option('--bot-token <token>', 'Telegram bot token')
|
|
@@ -664,6 +852,29 @@ program.command('logout').action(async () => {
|
|
|
664
852
|
await runLogout();
|
|
665
853
|
});
|
|
666
854
|
|
|
855
|
+
const hooks = program.command('hooks').description('Manage shell hooks for automatic wrapping');
|
|
856
|
+
|
|
857
|
+
hooks
|
|
858
|
+
.command('install')
|
|
859
|
+
.option('--shell <shell>', 'auto|powershell|bash|zsh|all', 'auto')
|
|
860
|
+
.action(async (options) => {
|
|
861
|
+
await runHooksInstall(options);
|
|
862
|
+
});
|
|
863
|
+
|
|
864
|
+
hooks
|
|
865
|
+
.command('uninstall')
|
|
866
|
+
.option('--shell <shell>', 'auto|powershell|bash|zsh|all', 'auto')
|
|
867
|
+
.action(async (options) => {
|
|
868
|
+
await runHooksUninstall(options);
|
|
869
|
+
});
|
|
870
|
+
|
|
871
|
+
hooks
|
|
872
|
+
.command('doctor')
|
|
873
|
+
.option('--shell <shell>', 'auto|powershell|bash|zsh|all', 'auto')
|
|
874
|
+
.action(async (options) => {
|
|
875
|
+
await runHooksDoctor(options);
|
|
876
|
+
});
|
|
877
|
+
|
|
667
878
|
program.parseAsync(process.argv).catch((error) => {
|
|
668
879
|
const message = error instanceof Error ? error.message : String(error);
|
|
669
880
|
console.error(`RunSignal error: ${toUserMessage(message)}`);
|
package/package.json
CHANGED
|
@@ -1,48 +1,48 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "runsignal",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "RunSignal CLI for wrapping AI agent runs and handling approvals",
|
|
5
|
-
"type": "module",
|
|
6
|
-
"license": "MIT",
|
|
7
|
-
"homepage": "https://runsignal.dev",
|
|
8
|
-
"repository": {
|
|
9
|
-
"type": "git",
|
|
10
|
-
"url": "git+https://github.com/gdamien33/runsignal.git",
|
|
11
|
-
"directory": "apps/cli"
|
|
12
|
-
},
|
|
13
|
-
"bugs": {
|
|
14
|
-
"url": "https://github.com/gdamien33/runsignal/issues"
|
|
15
|
-
},
|
|
16
|
-
"keywords": [
|
|
17
|
-
"runsignal",
|
|
18
|
-
"cli",
|
|
19
|
-
"ai",
|
|
20
|
-
"agent",
|
|
21
|
-
"devtools",
|
|
22
|
-
"monitoring"
|
|
23
|
-
],
|
|
24
|
-
"engines": {
|
|
25
|
-
"node": ">=18.18.0"
|
|
26
|
-
},
|
|
27
|
-
"publishConfig": {
|
|
28
|
-
"access": "public"
|
|
29
|
-
},
|
|
30
|
-
"files": [
|
|
31
|
-
"bin",
|
|
32
|
-
"src",
|
|
33
|
-
"README.md",
|
|
34
|
-
"LICENSE"
|
|
35
|
-
],
|
|
36
|
-
"bin": {
|
|
37
|
-
"runsignal": "bin/runsignal.js"
|
|
38
|
-
},
|
|
39
|
-
"scripts": {
|
|
40
|
-
"start": "node bin/runsignal.js --help",
|
|
41
|
-
"test:smoke": "node test/e2e-smoke.mjs",
|
|
42
|
-
"pack:check": "npm pack --dry-run",
|
|
43
|
-
"prepublishOnly": "npm run test:smoke"
|
|
44
|
-
},
|
|
45
|
-
"dependencies": {
|
|
46
|
-
"commander": "^14.0.1"
|
|
47
|
-
}
|
|
48
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "runsignal",
|
|
3
|
+
"version": "0.1.3",
|
|
4
|
+
"description": "RunSignal CLI for wrapping AI agent runs and handling approvals",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"homepage": "https://runsignal.dev",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/gdamien33/runsignal.git",
|
|
11
|
+
"directory": "apps/cli"
|
|
12
|
+
},
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/gdamien33/runsignal/issues"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"runsignal",
|
|
18
|
+
"cli",
|
|
19
|
+
"ai",
|
|
20
|
+
"agent",
|
|
21
|
+
"devtools",
|
|
22
|
+
"monitoring"
|
|
23
|
+
],
|
|
24
|
+
"engines": {
|
|
25
|
+
"node": ">=18.18.0"
|
|
26
|
+
},
|
|
27
|
+
"publishConfig": {
|
|
28
|
+
"access": "public"
|
|
29
|
+
},
|
|
30
|
+
"files": [
|
|
31
|
+
"bin",
|
|
32
|
+
"src",
|
|
33
|
+
"README.md",
|
|
34
|
+
"LICENSE"
|
|
35
|
+
],
|
|
36
|
+
"bin": {
|
|
37
|
+
"runsignal": "bin/runsignal.js"
|
|
38
|
+
},
|
|
39
|
+
"scripts": {
|
|
40
|
+
"start": "node bin/runsignal.js --help",
|
|
41
|
+
"test:smoke": "node test/e2e-smoke.mjs",
|
|
42
|
+
"pack:check": "npm pack --dry-run",
|
|
43
|
+
"prepublishOnly": "npm run test:smoke"
|
|
44
|
+
},
|
|
45
|
+
"dependencies": {
|
|
46
|
+
"commander": "^14.0.1"
|
|
47
|
+
}
|
|
48
|
+
}
|
package/src/api-client.js
CHANGED
|
@@ -1,107 +1,107 @@
|
|
|
1
|
-
function normalizeEventResponse(payload) {
|
|
2
|
-
if (!payload || typeof payload !== 'object') return { eventId: null, decision: 'pending' };
|
|
3
|
-
|
|
4
|
-
if (payload.data && typeof payload.data === 'object') {
|
|
5
|
-
return {
|
|
6
|
-
eventId: payload.data.event_id || null,
|
|
7
|
-
decision: payload.data.decision || 'pending'
|
|
8
|
-
};
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
return {
|
|
12
|
-
eventId: payload.event_id || null,
|
|
13
|
-
decision: payload.decision || 'pending'
|
|
14
|
-
};
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export async function sendEvent({ apiFetch, baseUrl, machineApiKey, event }) {
|
|
18
|
-
const { ok, status, payload } = await apiFetch(baseUrl, '/api/events', {
|
|
19
|
-
method: 'POST',
|
|
20
|
-
headers: { authorization: `Bearer ${machineApiKey}` },
|
|
21
|
-
body: JSON.stringify(event)
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
if (!ok) {
|
|
25
|
-
const errorMessage = payload?.error?.message || payload?.error || `http_${status}`;
|
|
26
|
-
throw new Error(`send_event_failed:${errorMessage}`);
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
return normalizeEventResponse(payload);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export async function getEventDecision({ apiFetch, baseUrl, machineApiKey, eventId }) {
|
|
33
|
-
const response = await apiFetch(
|
|
34
|
-
baseUrl,
|
|
35
|
-
`/api/cli/events/${encodeURIComponent(eventId)}/decision`,
|
|
36
|
-
{
|
|
37
|
-
method: 'GET',
|
|
38
|
-
headers: { authorization: `Bearer ${machineApiKey}` }
|
|
39
|
-
}
|
|
40
|
-
);
|
|
41
|
-
|
|
42
|
-
if (!response.ok) {
|
|
43
|
-
throw new Error(`status_failed:${response.payload?.error || response.status}`);
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
return response.payload?.decision || 'pending';
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export async function getEventAnswer({ apiFetch, baseUrl, machineApiKey, eventId }) {
|
|
50
|
-
const response = await apiFetch(
|
|
51
|
-
baseUrl,
|
|
52
|
-
`/api/cli/events/${encodeURIComponent(eventId)}/answer`,
|
|
53
|
-
{
|
|
54
|
-
method: 'GET',
|
|
55
|
-
headers: { authorization: `Bearer ${machineApiKey}` }
|
|
56
|
-
}
|
|
57
|
-
);
|
|
58
|
-
|
|
59
|
-
if (!response.ok) {
|
|
60
|
-
return null;
|
|
61
|
-
}
|
|
62
|
-
return response.payload?.answer ?? null;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
export async function pollDecision({
|
|
66
|
-
apiFetch,
|
|
67
|
-
baseUrl,
|
|
68
|
-
machineApiKey,
|
|
69
|
-
eventId,
|
|
70
|
-
sleep,
|
|
71
|
-
timeoutMs = 5 * 60 * 1000
|
|
72
|
-
}) {
|
|
73
|
-
const startedAt = Date.now();
|
|
74
|
-
while (Date.now() - startedAt < timeoutMs) {
|
|
75
|
-
const decision = await getEventDecision({ apiFetch, baseUrl, machineApiKey, eventId }).catch(
|
|
76
|
-
() => 'pending'
|
|
77
|
-
);
|
|
78
|
-
if (decision !== 'pending') {
|
|
79
|
-
return decision;
|
|
80
|
-
}
|
|
81
|
-
await sleep(2000);
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
return 'pending';
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
export async function pollAnswer({
|
|
88
|
-
apiFetch,
|
|
89
|
-
baseUrl,
|
|
90
|
-
machineApiKey,
|
|
91
|
-
eventId,
|
|
92
|
-
sleep,
|
|
93
|
-
timeoutMs = 5 * 60 * 1000
|
|
94
|
-
}) {
|
|
95
|
-
const startedAt = Date.now();
|
|
96
|
-
while (Date.now() - startedAt < timeoutMs) {
|
|
97
|
-
const answer = await getEventAnswer({ apiFetch, baseUrl, machineApiKey, eventId }).catch(
|
|
98
|
-
() => null
|
|
99
|
-
);
|
|
100
|
-
if (typeof answer === 'string' && answer.trim()) {
|
|
101
|
-
return answer.trim();
|
|
102
|
-
}
|
|
103
|
-
await sleep(2000);
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
return null;
|
|
107
|
-
}
|
|
1
|
+
function normalizeEventResponse(payload) {
|
|
2
|
+
if (!payload || typeof payload !== 'object') return { eventId: null, decision: 'pending' };
|
|
3
|
+
|
|
4
|
+
if (payload.data && typeof payload.data === 'object') {
|
|
5
|
+
return {
|
|
6
|
+
eventId: payload.data.event_id || null,
|
|
7
|
+
decision: payload.data.decision || 'pending'
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
return {
|
|
12
|
+
eventId: payload.event_id || null,
|
|
13
|
+
decision: payload.decision || 'pending'
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function sendEvent({ apiFetch, baseUrl, machineApiKey, event }) {
|
|
18
|
+
const { ok, status, payload } = await apiFetch(baseUrl, '/api/events', {
|
|
19
|
+
method: 'POST',
|
|
20
|
+
headers: { authorization: `Bearer ${machineApiKey}` },
|
|
21
|
+
body: JSON.stringify(event)
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
if (!ok) {
|
|
25
|
+
const errorMessage = payload?.error?.message || payload?.error || `http_${status}`;
|
|
26
|
+
throw new Error(`send_event_failed:${errorMessage}`);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return normalizeEventResponse(payload);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function getEventDecision({ apiFetch, baseUrl, machineApiKey, eventId }) {
|
|
33
|
+
const response = await apiFetch(
|
|
34
|
+
baseUrl,
|
|
35
|
+
`/api/cli/events/${encodeURIComponent(eventId)}/decision`,
|
|
36
|
+
{
|
|
37
|
+
method: 'GET',
|
|
38
|
+
headers: { authorization: `Bearer ${machineApiKey}` }
|
|
39
|
+
}
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
if (!response.ok) {
|
|
43
|
+
throw new Error(`status_failed:${response.payload?.error || response.status}`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return response.payload?.decision || 'pending';
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export async function getEventAnswer({ apiFetch, baseUrl, machineApiKey, eventId }) {
|
|
50
|
+
const response = await apiFetch(
|
|
51
|
+
baseUrl,
|
|
52
|
+
`/api/cli/events/${encodeURIComponent(eventId)}/answer`,
|
|
53
|
+
{
|
|
54
|
+
method: 'GET',
|
|
55
|
+
headers: { authorization: `Bearer ${machineApiKey}` }
|
|
56
|
+
}
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
if (!response.ok) {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
return response.payload?.answer ?? null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export async function pollDecision({
|
|
66
|
+
apiFetch,
|
|
67
|
+
baseUrl,
|
|
68
|
+
machineApiKey,
|
|
69
|
+
eventId,
|
|
70
|
+
sleep,
|
|
71
|
+
timeoutMs = 5 * 60 * 1000
|
|
72
|
+
}) {
|
|
73
|
+
const startedAt = Date.now();
|
|
74
|
+
while (Date.now() - startedAt < timeoutMs) {
|
|
75
|
+
const decision = await getEventDecision({ apiFetch, baseUrl, machineApiKey, eventId }).catch(
|
|
76
|
+
() => 'pending'
|
|
77
|
+
);
|
|
78
|
+
if (decision !== 'pending') {
|
|
79
|
+
return decision;
|
|
80
|
+
}
|
|
81
|
+
await sleep(2000);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return 'pending';
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export async function pollAnswer({
|
|
88
|
+
apiFetch,
|
|
89
|
+
baseUrl,
|
|
90
|
+
machineApiKey,
|
|
91
|
+
eventId,
|
|
92
|
+
sleep,
|
|
93
|
+
timeoutMs = 5 * 60 * 1000
|
|
94
|
+
}) {
|
|
95
|
+
const startedAt = Date.now();
|
|
96
|
+
while (Date.now() - startedAt < timeoutMs) {
|
|
97
|
+
const answer = await getEventAnswer({ apiFetch, baseUrl, machineApiKey, eventId }).catch(
|
|
98
|
+
() => null
|
|
99
|
+
);
|
|
100
|
+
if (typeof answer === 'string' && answer.trim()) {
|
|
101
|
+
return answer.trim();
|
|
102
|
+
}
|
|
103
|
+
await sleep(2000);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return null;
|
|
107
|
+
}
|