runsignal 0.1.0 → 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/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,37 @@
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 --provider telegram|slack`
21
- - `runsignal wrap -- <command>`
22
- - `runsignal send --message "..."`
23
- - `runsignal status <eventId>`
24
- - `runsignal logout`
25
-
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
+
26
26
  ## Quickstart
27
27
 
28
28
  ```bash
29
- runsignal login
30
- runsignal init --provider telegram
31
- runsignal wrap -- claude "refactor auth flow"
29
+ npx runsignal@latest login
30
+ npx runsignal@latest init
31
+ npx runsignal@latest wrap -- claude "refactor auth flow"
32
32
  ```
33
33
 
34
34
  ## Environment
35
35
 
36
- - `RUNSIGNAL_APP_URL` to override app URL (default: `http://localhost:3000`)
37
-
36
+ - `RUNSIGNAL_APP_URL` to override app URL (default: `https://runsignal.dev`)
37
+
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 || 'http://localhost:3000';
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
 
@@ -100,16 +98,6 @@ function sleep(ms) {
100
98
  return new Promise((resolve) => setTimeout(resolve, ms));
101
99
  }
102
100
 
103
- async function prompt(question) {
104
- const rl = readline.createInterface({input, output});
105
- try {
106
- const value = await rl.question(question);
107
- return value.trim();
108
- } finally {
109
- rl.close();
110
- }
111
- }
112
-
113
101
  function getMachineName() {
114
102
  return process.env.COMPUTERNAME || os.hostname() || 'machine';
115
103
  }
@@ -239,10 +227,20 @@ async function runInit(options) {
239
227
  throw new Error('not_logged_in');
240
228
  }
241
229
 
230
+ const hasSlackWebhook = typeof options.webhookUrl === 'string' && options.webhookUrl.length > 0;
231
+ const hasTelegramConfig =
232
+ typeof options.botToken === 'string' ||
233
+ typeof options.chatId === 'string' ||
234
+ typeof config.provider === 'string';
242
235
  let provider = String(options.provider || '').toLowerCase();
243
236
  if (provider !== 'telegram' && provider !== 'slack') {
244
- const answer = await prompt('Provider (telegram/slack): ');
245
- provider = answer.toLowerCase();
237
+ if (hasSlackWebhook) {
238
+ provider = 'slack';
239
+ } else if (hasTelegramConfig) {
240
+ provider = 'telegram';
241
+ } else {
242
+ provider = 'telegram';
243
+ }
246
244
  }
247
245
 
248
246
  if (provider !== 'telegram' && provider !== 'slack') {
@@ -253,21 +251,25 @@ async function runInit(options) {
253
251
  const providerConfig = {};
254
252
 
255
253
  if (provider === 'telegram') {
256
- const botToken = String(options.botToken || (await prompt('Telegram bot token: ')));
257
- const chatId = String(options.chatId || (await prompt('Telegram chat id: ')));
258
- if (!botToken || !chatId) {
254
+ const botToken = String(options.botToken || '').trim();
255
+ const chatId = String(options.chatId || '').trim();
256
+ if ((botToken && !chatId) || (!botToken && chatId)) {
259
257
  throw new Error('telegram_config_invalid');
260
258
  }
261
- providerConfig.bot_token = botToken;
262
- providerConfig.chat_id = chatId;
259
+ if (botToken && chatId) {
260
+ providerConfig.bot_token = botToken;
261
+ providerConfig.chat_id = chatId;
262
+ }
263
263
  }
264
264
 
265
265
  if (provider === 'slack') {
266
- const webhookUrl = String(options.webhookUrl || (await prompt('Slack webhook URL: ')));
267
- if (!webhookUrl.startsWith('https://')) {
268
- throw new Error('slack_webhook_invalid');
266
+ const webhookUrl = String(options.webhookUrl || '').trim();
267
+ if (webhookUrl) {
268
+ if (!webhookUrl.startsWith('https://')) {
269
+ throw new Error('slack_webhook_invalid');
270
+ }
271
+ providerConfig.webhook_url = webhookUrl;
269
272
  }
270
- providerConfig.webhook_url = webhookUrl;
271
273
  }
272
274
 
273
275
  const register = await apiFetch(baseUrl, '/api/cli/machines/register', {
@@ -607,7 +609,7 @@ async function runLogout() {
607
609
 
608
610
  const program = new Command();
609
611
 
610
- program.name('runsignal').description('RunSignal CLI').version('0.1.0');
612
+ program.name('runsignal').description('RunSignal CLI').version('0.1.2');
611
613
 
612
614
  program
613
615
  .command('login')
@@ -618,7 +620,7 @@ program
618
620
 
619
621
  program
620
622
  .command('init')
621
- .option('--provider <provider>', 'telegram or slack')
623
+ .option('--provider <provider>', 'telegram or slack (optional)')
622
624
  .option('--app-url <url>', 'RunSignal app base URL')
623
625
  .option('--machine-name <name>', 'Machine name override')
624
626
  .option('--bot-token <token>', 'Telegram bot token')
package/package.json CHANGED
@@ -1,48 +1,48 @@
1
- {
2
- "name": "runsignal",
3
- "version": "0.1.0",
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.2",
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
+ }