runsignal 0.1.4 → 0.1.6
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 +26 -10
- package/bin/runsignal.js +212 -23
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -16,22 +16,31 @@ npx runsignal --help
|
|
|
16
16
|
|
|
17
17
|
## Commands
|
|
18
18
|
|
|
19
|
-
- `runsignal login`
|
|
19
|
+
- `runsignal login`
|
|
20
20
|
- `runsignal init`
|
|
21
|
-
- `runsignal
|
|
21
|
+
- `runsignal setup`
|
|
22
|
+
- `runsignal wrap -- <command>`
|
|
22
23
|
- `runsignal send --message "..."`
|
|
23
24
|
- `runsignal status <eventId>`
|
|
24
25
|
- `runsignal logout`
|
|
25
|
-
- `runsignal hooks install
|
|
26
|
-
- `runsignal hooks doctor
|
|
27
|
-
- `runsignal hooks uninstall
|
|
26
|
+
- `runsignal hooks install`
|
|
27
|
+
- `runsignal hooks doctor`
|
|
28
|
+
- `runsignal hooks uninstall`
|
|
28
29
|
|
|
29
30
|
## Quickstart
|
|
30
31
|
|
|
32
|
+
Canonical zero-friction onboarding:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
npx --yes runsignal@latest setup
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Equivalent manual flow:
|
|
39
|
+
|
|
31
40
|
```bash
|
|
32
|
-
npx runsignal@latest login
|
|
33
|
-
npx runsignal@latest init
|
|
34
|
-
npx runsignal@latest
|
|
41
|
+
npx --yes runsignal@latest login
|
|
42
|
+
npx --yes runsignal@latest init
|
|
43
|
+
npx --yes runsignal@latest hooks install
|
|
35
44
|
```
|
|
36
45
|
|
|
37
46
|
## Environment
|
|
@@ -43,12 +52,19 @@ npx runsignal@latest wrap -- claude "refactor auth flow"
|
|
|
43
52
|
To keep using `claude ...` / `codex ...` normally while routing through RunSignal:
|
|
44
53
|
|
|
45
54
|
```bash
|
|
46
|
-
runsignal hooks install
|
|
55
|
+
runsignal hooks install
|
|
47
56
|
```
|
|
48
57
|
|
|
49
58
|
Then restart terminal and verify:
|
|
50
59
|
|
|
51
60
|
```bash
|
|
52
|
-
runsignal hooks doctor
|
|
61
|
+
runsignal hooks doctor
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
After that, use your CLI agents normally:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
claude
|
|
68
|
+
codex
|
|
53
69
|
```
|
|
54
70
|
|
package/bin/runsignal.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import {Command} from 'commander';
|
|
4
4
|
import {spawn, execSync} from 'node:child_process';
|
|
5
5
|
import {promises as fs} from 'node:fs';
|
|
6
|
+
import {existsSync} from 'node:fs';
|
|
6
7
|
import os from 'node:os';
|
|
7
8
|
import path from 'node:path';
|
|
8
9
|
import {
|
|
@@ -122,38 +123,44 @@ function buildHookBlock(shellKind) {
|
|
|
122
123
|
return `${HOOK_START}
|
|
123
124
|
function RunSignal-Resolve-NativeCommand {
|
|
124
125
|
param([string]$Name)
|
|
125
|
-
$
|
|
126
|
-
if (-not $
|
|
127
|
-
|
|
126
|
+
$candidates = @(Get-Command $Name -CommandType Application -ErrorAction SilentlyContinue)
|
|
127
|
+
if (-not $candidates -or $candidates.Count -eq 0) { return $null }
|
|
128
|
+
$preferred = $candidates | Where-Object { $_.Source -match '\\.exe$' } | Select-Object -First 1
|
|
129
|
+
if ($preferred) { return $preferred.Source }
|
|
130
|
+
$launcher = $candidates | Where-Object { $_.Source -match '\\.(cmd|bat)$' } | Select-Object -First 1
|
|
131
|
+
if ($launcher) { return $launcher.Source }
|
|
132
|
+
$secondary = $candidates | Where-Object { $_.Source -match '\\.[A-Za-z0-9]+$' } | Select-Object -First 1
|
|
133
|
+
if ($secondary) { return $secondary.Source }
|
|
134
|
+
return $candidates[0].Source
|
|
128
135
|
}
|
|
129
136
|
function claude {
|
|
130
137
|
$native = RunSignal-Resolve-NativeCommand "claude"
|
|
131
|
-
if (-not $native) {
|
|
132
|
-
|
|
138
|
+
if (-not $native) {
|
|
139
|
+
$userHome = [Environment]::GetFolderPath("UserProfile")
|
|
140
|
+
$fallback = Join-Path $userHome ".local\\bin\\claude.exe"
|
|
141
|
+
if (Test-Path $fallback) { $native = $fallback }
|
|
142
|
+
}
|
|
143
|
+
if (-not $native) { Write-Error "RunSignal hook: native claude executable not found. Add your native installer path to PATH (example: %USERPROFILE%\\.local\\bin)."; return }
|
|
133
144
|
runsignal wrap -- $native @args
|
|
134
145
|
}
|
|
135
146
|
function codex {
|
|
136
147
|
$native = RunSignal-Resolve-NativeCommand "codex"
|
|
137
148
|
if (-not $native) { Write-Error "RunSignal hook: native codex executable not found."; return }
|
|
138
|
-
if ($args.Count -eq 0) { & $native; return }
|
|
139
149
|
runsignal wrap -- $native @args
|
|
140
150
|
}
|
|
141
151
|
function cursor {
|
|
142
152
|
$native = RunSignal-Resolve-NativeCommand "cursor"
|
|
143
153
|
if (-not $native) { Write-Error "RunSignal hook: native cursor executable not found."; return }
|
|
144
|
-
if ($args.Count -eq 0) { & $native; return }
|
|
145
154
|
runsignal wrap -- $native @args
|
|
146
155
|
}
|
|
147
156
|
function gemini {
|
|
148
157
|
$native = RunSignal-Resolve-NativeCommand "gemini"
|
|
149
158
|
if (-not $native) { Write-Error "RunSignal hook: native gemini executable not found."; return }
|
|
150
|
-
if ($args.Count -eq 0) { & $native; return }
|
|
151
159
|
runsignal wrap -- $native @args
|
|
152
160
|
}
|
|
153
161
|
function aider {
|
|
154
162
|
$native = RunSignal-Resolve-NativeCommand "aider"
|
|
155
163
|
if (-not $native) { Write-Error "RunSignal hook: native aider executable not found."; return }
|
|
156
|
-
if ($args.Count -eq 0) { & $native; return }
|
|
157
164
|
runsignal wrap -- $native @args
|
|
158
165
|
}
|
|
159
166
|
${HOOK_END}
|
|
@@ -162,41 +169,46 @@ ${HOOK_END}
|
|
|
162
169
|
|
|
163
170
|
return `${HOOK_START}
|
|
164
171
|
runsignal_resolve_native() {
|
|
165
|
-
|
|
172
|
+
local name
|
|
173
|
+
name="$1"
|
|
174
|
+
if type -P "$name" >/dev/null 2>&1; then
|
|
175
|
+
type -P "$name"
|
|
176
|
+
return 0
|
|
177
|
+
fi
|
|
178
|
+
if [ -x "$HOME/.local/bin/$name" ]; then
|
|
179
|
+
printf '%s\n' "$HOME/.local/bin/$name"
|
|
180
|
+
return 0
|
|
181
|
+
fi
|
|
182
|
+
return 1
|
|
166
183
|
}
|
|
167
184
|
claude() {
|
|
168
185
|
local native
|
|
169
186
|
native="$(runsignal_resolve_native claude)"
|
|
170
187
|
if [ -z "$native" ]; then echo "RunSignal hook: native claude executable not found." >&2; return 127; fi
|
|
171
|
-
if [ "$#" -eq 0 ]; then "$native"; return $?; fi
|
|
172
188
|
runsignal wrap -- "$native" "$@"
|
|
173
189
|
}
|
|
174
190
|
codex() {
|
|
175
191
|
local native
|
|
176
192
|
native="$(runsignal_resolve_native codex)"
|
|
177
193
|
if [ -z "$native" ]; then echo "RunSignal hook: native codex executable not found." >&2; return 127; fi
|
|
178
|
-
if [ "$#" -eq 0 ]; then "$native"; return $?; fi
|
|
179
194
|
runsignal wrap -- "$native" "$@"
|
|
180
195
|
}
|
|
181
196
|
cursor() {
|
|
182
197
|
local native
|
|
183
198
|
native="$(runsignal_resolve_native cursor)"
|
|
184
199
|
if [ -z "$native" ]; then echo "RunSignal hook: native cursor executable not found." >&2; return 127; fi
|
|
185
|
-
if [ "$#" -eq 0 ]; then "$native"; return $?; fi
|
|
186
200
|
runsignal wrap -- "$native" "$@"
|
|
187
201
|
}
|
|
188
202
|
gemini() {
|
|
189
203
|
local native
|
|
190
204
|
native="$(runsignal_resolve_native gemini)"
|
|
191
205
|
if [ -z "$native" ]; then echo "RunSignal hook: native gemini executable not found." >&2; return 127; fi
|
|
192
|
-
if [ "$#" -eq 0 ]; then "$native"; return $?; fi
|
|
193
206
|
runsignal wrap -- "$native" "$@"
|
|
194
207
|
}
|
|
195
208
|
aider() {
|
|
196
209
|
local native
|
|
197
210
|
native="$(runsignal_resolve_native aider)"
|
|
198
211
|
if [ -z "$native" ]; then echo "RunSignal hook: native aider executable not found." >&2; return 127; fi
|
|
199
|
-
if [ "$#" -eq 0 ]; then "$native"; return $?; fi
|
|
200
212
|
runsignal wrap -- "$native" "$@"
|
|
201
213
|
}
|
|
202
214
|
${HOOK_END}
|
|
@@ -320,6 +332,21 @@ async function runHooksDoctor(options) {
|
|
|
320
332
|
}
|
|
321
333
|
}
|
|
322
334
|
|
|
335
|
+
async function areHooksInstalled(shellOption) {
|
|
336
|
+
const targets = resolveHookTargets(shellOption);
|
|
337
|
+
if (!targets.length) {
|
|
338
|
+
throw new Error('hooks_shell_not_supported');
|
|
339
|
+
}
|
|
340
|
+
for (const target of targets) {
|
|
341
|
+
const existing = await readTextFile(target.path);
|
|
342
|
+
const hasHooks = existing.includes(HOOK_START) && existing.includes(HOOK_END);
|
|
343
|
+
if (!hasHooks) {
|
|
344
|
+
return false;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
return true;
|
|
348
|
+
}
|
|
349
|
+
|
|
323
350
|
function getMachineName() {
|
|
324
351
|
return process.env.COMPUTERNAME || os.hostname() || 'machine';
|
|
325
352
|
}
|
|
@@ -405,6 +432,35 @@ function quoteForShell(arg) {
|
|
|
405
432
|
return `"${escaped}"`;
|
|
406
433
|
}
|
|
407
434
|
|
|
435
|
+
function stripWrappingQuotes(value) {
|
|
436
|
+
const raw = String(value || '').trim();
|
|
437
|
+
if ((raw.startsWith('"') && raw.endsWith('"')) || (raw.startsWith("'") && raw.endsWith("'"))) {
|
|
438
|
+
return raw.slice(1, -1);
|
|
439
|
+
}
|
|
440
|
+
return raw;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function isWindowsScriptLauncher(filePath) {
|
|
444
|
+
if (process.platform !== 'win32') return false;
|
|
445
|
+
const ext = path.extname(String(filePath || '')).toLowerCase();
|
|
446
|
+
return ext === '.cmd' || ext === '.bat' || ext === '.ps1';
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
function normalizeWindowsExecutablePath(filePath) {
|
|
450
|
+
const raw = String(filePath || '');
|
|
451
|
+
if (process.platform !== 'win32') return raw;
|
|
452
|
+
if (!raw) return raw;
|
|
453
|
+
const ext = path.extname(raw).toLowerCase();
|
|
454
|
+
if (ext) return raw;
|
|
455
|
+
const exe = `${raw}.exe`;
|
|
456
|
+
if (existsSync(exe)) return exe;
|
|
457
|
+
const cmd = `${raw}.cmd`;
|
|
458
|
+
if (existsSync(cmd)) return cmd;
|
|
459
|
+
const bat = `${raw}.bat`;
|
|
460
|
+
if (existsSync(bat)) return bat;
|
|
461
|
+
return raw;
|
|
462
|
+
}
|
|
463
|
+
|
|
408
464
|
async function runLogin(options) {
|
|
409
465
|
const config = await readConfig();
|
|
410
466
|
const baseUrl = String(options.appUrl || config.app_url || DEFAULT_APP_URL).replace(/\/$/, '');
|
|
@@ -563,13 +619,71 @@ async function runWrap(commandParts) {
|
|
|
563
619
|
commandParts.length === 1
|
|
564
620
|
? String(commandParts[0])
|
|
565
621
|
: commandParts.map((part) => quoteForShell(part)).join(' ');
|
|
622
|
+
const singleCommand = String(commandParts[0] || '');
|
|
623
|
+
const singleExecutable = normalizeWindowsExecutablePath(stripWrappingQuotes(singleCommand));
|
|
566
624
|
const interactivePassthrough =
|
|
567
|
-
commandParts.length === 1 &&
|
|
568
|
-
const child =
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
625
|
+
commandParts.length === 1 && (existsSync(singleExecutable) || !/\s/.test(singleExecutable));
|
|
626
|
+
const child = interactivePassthrough
|
|
627
|
+
? isWindowsScriptLauncher(singleExecutable)
|
|
628
|
+
? spawn(singleExecutable, [], {
|
|
629
|
+
stdio: 'inherit',
|
|
630
|
+
shell: true,
|
|
631
|
+
env: process.env
|
|
632
|
+
})
|
|
633
|
+
: spawn(singleExecutable, [], {
|
|
634
|
+
stdio: 'inherit',
|
|
635
|
+
shell: false,
|
|
636
|
+
env: process.env
|
|
637
|
+
})
|
|
638
|
+
: spawn(commandString, {
|
|
639
|
+
stdio: ['inherit', 'pipe', 'pipe'],
|
|
640
|
+
shell: true,
|
|
641
|
+
env: process.env
|
|
642
|
+
});
|
|
643
|
+
|
|
644
|
+
let heartbeatInterval = null;
|
|
645
|
+
if (interactivePassthrough) {
|
|
646
|
+
const startedAt = Date.now();
|
|
647
|
+
try {
|
|
648
|
+
await sendEvent({
|
|
649
|
+
apiFetch,
|
|
650
|
+
baseUrl,
|
|
651
|
+
machineApiKey,
|
|
652
|
+
event: {
|
|
653
|
+
message: `Session started: ${singleExecutable}`,
|
|
654
|
+
event_type: 'session_started',
|
|
655
|
+
repo,
|
|
656
|
+
branch,
|
|
657
|
+
severity: 'info',
|
|
658
|
+
context,
|
|
659
|
+
agent: agentLabel
|
|
660
|
+
}
|
|
661
|
+
});
|
|
662
|
+
} catch {
|
|
663
|
+
// non-blocking
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
heartbeatInterval = setInterval(() => {
|
|
667
|
+
const elapsedMin = Math.max(1, Math.floor((Date.now() - startedAt) / 60_000));
|
|
668
|
+
sendEvent({
|
|
669
|
+
apiFetch,
|
|
670
|
+
baseUrl,
|
|
671
|
+
machineApiKey,
|
|
672
|
+
event: {
|
|
673
|
+
message: `Session running (${elapsedMin} min): ${singleExecutable}`,
|
|
674
|
+
event_type: 'session_running',
|
|
675
|
+
repo,
|
|
676
|
+
branch,
|
|
677
|
+
severity: 'info',
|
|
678
|
+
context,
|
|
679
|
+
agent: agentLabel
|
|
680
|
+
}
|
|
681
|
+
}).catch(() => {
|
|
682
|
+
// non-blocking
|
|
683
|
+
});
|
|
684
|
+
}, 120_000);
|
|
685
|
+
heartbeatInterval.unref?.();
|
|
686
|
+
}
|
|
573
687
|
|
|
574
688
|
let sawApproval = false;
|
|
575
689
|
let sawQuestion = false;
|
|
@@ -669,8 +783,12 @@ async function runWrap(commandParts) {
|
|
|
669
783
|
}
|
|
670
784
|
|
|
671
785
|
const exitCode = await new Promise((resolve) => {
|
|
786
|
+
child.on('error', () => resolve(1));
|
|
672
787
|
child.on('close', (code) => resolve(code ?? 1));
|
|
673
788
|
});
|
|
789
|
+
if (heartbeatInterval) {
|
|
790
|
+
clearInterval(heartbeatInterval);
|
|
791
|
+
}
|
|
674
792
|
await Promise.allSettled(Array.from(pendingTasks));
|
|
675
793
|
|
|
676
794
|
const completionMessage =
|
|
@@ -744,7 +862,9 @@ function normalizeEventType(value) {
|
|
|
744
862
|
raw === 'task_completed' ||
|
|
745
863
|
raw === 'error' ||
|
|
746
864
|
raw === 'quota_warning' ||
|
|
747
|
-
raw === 'question'
|
|
865
|
+
raw === 'question' ||
|
|
866
|
+
raw === 'session_started' ||
|
|
867
|
+
raw === 'session_running'
|
|
748
868
|
) {
|
|
749
869
|
return raw;
|
|
750
870
|
}
|
|
@@ -857,9 +977,78 @@ async function runLogout() {
|
|
|
857
977
|
console.log('Logged out.');
|
|
858
978
|
}
|
|
859
979
|
|
|
980
|
+
async function runSetup(options) {
|
|
981
|
+
const shell = String(options.shell || 'auto');
|
|
982
|
+
let config = await readConfig();
|
|
983
|
+
const baseUrl = String(options.appUrl || config.app_url || DEFAULT_APP_URL).replace(/\/$/, '');
|
|
984
|
+
|
|
985
|
+
if (config.app_url !== baseUrl) {
|
|
986
|
+
await writeConfig(mergeConfig(config, {app_url: baseUrl}));
|
|
987
|
+
config = await readConfig();
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
const summary = {
|
|
991
|
+
login: 'skipped',
|
|
992
|
+
init: 'skipped',
|
|
993
|
+
hooks: 'skipped'
|
|
994
|
+
};
|
|
995
|
+
|
|
996
|
+
if (!config.access_token) {
|
|
997
|
+
await runLogin({appUrl: baseUrl});
|
|
998
|
+
summary.login = 'ok';
|
|
999
|
+
config = await readConfig();
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
if (!config.machine_api_key) {
|
|
1003
|
+
await runInit({
|
|
1004
|
+
appUrl: baseUrl,
|
|
1005
|
+
machineName: options.machineName,
|
|
1006
|
+
provider: options.provider,
|
|
1007
|
+
botToken: options.botToken,
|
|
1008
|
+
chatId: options.chatId,
|
|
1009
|
+
webhookUrl: options.webhookUrl
|
|
1010
|
+
});
|
|
1011
|
+
summary.init = 'ok';
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
const hooksInstalled = await areHooksInstalled(shell);
|
|
1015
|
+
if (!hooksInstalled) {
|
|
1016
|
+
await runHooksInstall({shell});
|
|
1017
|
+
summary.hooks = 'ok';
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
console.log(
|
|
1021
|
+
JSON.stringify(
|
|
1022
|
+
{
|
|
1023
|
+
setup: 'ok',
|
|
1024
|
+
login: summary.login,
|
|
1025
|
+
init: summary.init,
|
|
1026
|
+
hooks: summary.hooks,
|
|
1027
|
+
next: 'Open a new terminal, then run claude or codex normally.'
|
|
1028
|
+
},
|
|
1029
|
+
null,
|
|
1030
|
+
2
|
|
1031
|
+
)
|
|
1032
|
+
);
|
|
1033
|
+
}
|
|
1034
|
+
|
|
860
1035
|
const program = new Command();
|
|
861
1036
|
|
|
862
|
-
program.name('runsignal').description('RunSignal CLI').version('0.1.
|
|
1037
|
+
program.name('runsignal').description('RunSignal CLI').version('0.1.6');
|
|
1038
|
+
|
|
1039
|
+
program
|
|
1040
|
+
.command('setup')
|
|
1041
|
+
.description('Zero-friction onboarding: login + init + hooks install')
|
|
1042
|
+
.option('--app-url <url>', 'RunSignal app base URL')
|
|
1043
|
+
.option('--shell <shell>', 'auto|powershell|bash|zsh|all', 'auto')
|
|
1044
|
+
.option('--provider <provider>', 'telegram or slack (optional)')
|
|
1045
|
+
.option('--machine-name <name>', 'Machine name override')
|
|
1046
|
+
.option('--bot-token <token>', 'Telegram bot token')
|
|
1047
|
+
.option('--chat-id <id>', 'Telegram chat id')
|
|
1048
|
+
.option('--webhook-url <url>', 'Slack webhook URL')
|
|
1049
|
+
.action(async (options) => {
|
|
1050
|
+
await runSetup(options);
|
|
1051
|
+
});
|
|
863
1052
|
|
|
864
1053
|
program
|
|
865
1054
|
.command('login')
|