runsignal 0.1.5 → 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.
Files changed (2) hide show
  1. package/bin/runsignal.js +113 -23
  2. package/package.json +1 -1
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,43 +123,44 @@ function buildHookBlock(shellKind) {
122
123
  return `${HOOK_START}
123
124
  function RunSignal-Resolve-NativeCommand {
124
125
  param([string]$Name)
125
- $cmd = Get-Command $Name -CommandType Application -ErrorAction SilentlyContinue | Select-Object -First 1
126
- if (-not $cmd) { return $null }
127
- return $cmd.Source
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
138
  if (-not $native) {
132
- $home = [Environment]::GetFolderPath("UserProfile")
133
- $fallback = Join-Path $home ".local\\bin\\claude.exe"
139
+ $userHome = [Environment]::GetFolderPath("UserProfile")
140
+ $fallback = Join-Path $userHome ".local\\bin\\claude.exe"
134
141
  if (Test-Path $fallback) { $native = $fallback }
135
142
  }
136
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 }
137
- if ($args.Count -eq 0) { & $native; return }
138
144
  runsignal wrap -- $native @args
139
145
  }
140
146
  function codex {
141
147
  $native = RunSignal-Resolve-NativeCommand "codex"
142
148
  if (-not $native) { Write-Error "RunSignal hook: native codex executable not found."; return }
143
- if ($args.Count -eq 0) { & $native; return }
144
149
  runsignal wrap -- $native @args
145
150
  }
146
151
  function cursor {
147
152
  $native = RunSignal-Resolve-NativeCommand "cursor"
148
153
  if (-not $native) { Write-Error "RunSignal hook: native cursor executable not found."; return }
149
- if ($args.Count -eq 0) { & $native; return }
150
154
  runsignal wrap -- $native @args
151
155
  }
152
156
  function gemini {
153
157
  $native = RunSignal-Resolve-NativeCommand "gemini"
154
158
  if (-not $native) { Write-Error "RunSignal hook: native gemini executable not found."; return }
155
- if ($args.Count -eq 0) { & $native; return }
156
159
  runsignal wrap -- $native @args
157
160
  }
158
161
  function aider {
159
162
  $native = RunSignal-Resolve-NativeCommand "aider"
160
163
  if (-not $native) { Write-Error "RunSignal hook: native aider executable not found."; return }
161
- if ($args.Count -eq 0) { & $native; return }
162
164
  runsignal wrap -- $native @args
163
165
  }
164
166
  ${HOOK_END}
@@ -183,35 +185,30 @@ claude() {
183
185
  local native
184
186
  native="$(runsignal_resolve_native claude)"
185
187
  if [ -z "$native" ]; then echo "RunSignal hook: native claude executable not found." >&2; return 127; fi
186
- if [ "$#" -eq 0 ]; then "$native"; return $?; fi
187
188
  runsignal wrap -- "$native" "$@"
188
189
  }
189
190
  codex() {
190
191
  local native
191
192
  native="$(runsignal_resolve_native codex)"
192
193
  if [ -z "$native" ]; then echo "RunSignal hook: native codex executable not found." >&2; return 127; fi
193
- if [ "$#" -eq 0 ]; then "$native"; return $?; fi
194
194
  runsignal wrap -- "$native" "$@"
195
195
  }
196
196
  cursor() {
197
197
  local native
198
198
  native="$(runsignal_resolve_native cursor)"
199
199
  if [ -z "$native" ]; then echo "RunSignal hook: native cursor executable not found." >&2; return 127; fi
200
- if [ "$#" -eq 0 ]; then "$native"; return $?; fi
201
200
  runsignal wrap -- "$native" "$@"
202
201
  }
203
202
  gemini() {
204
203
  local native
205
204
  native="$(runsignal_resolve_native gemini)"
206
205
  if [ -z "$native" ]; then echo "RunSignal hook: native gemini executable not found." >&2; return 127; fi
207
- if [ "$#" -eq 0 ]; then "$native"; return $?; fi
208
206
  runsignal wrap -- "$native" "$@"
209
207
  }
210
208
  aider() {
211
209
  local native
212
210
  native="$(runsignal_resolve_native aider)"
213
211
  if [ -z "$native" ]; then echo "RunSignal hook: native aider executable not found." >&2; return 127; fi
214
- if [ "$#" -eq 0 ]; then "$native"; return $?; fi
215
212
  runsignal wrap -- "$native" "$@"
216
213
  }
217
214
  ${HOOK_END}
@@ -435,6 +432,35 @@ function quoteForShell(arg) {
435
432
  return `"${escaped}"`;
436
433
  }
437
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
+
438
464
  async function runLogin(options) {
439
465
  const config = await readConfig();
440
466
  const baseUrl = String(options.appUrl || config.app_url || DEFAULT_APP_URL).replace(/\/$/, '');
@@ -593,13 +619,71 @@ async function runWrap(commandParts) {
593
619
  commandParts.length === 1
594
620
  ? String(commandParts[0])
595
621
  : commandParts.map((part) => quoteForShell(part)).join(' ');
622
+ const singleCommand = String(commandParts[0] || '');
623
+ const singleExecutable = normalizeWindowsExecutablePath(stripWrappingQuotes(singleCommand));
596
624
  const interactivePassthrough =
597
- commandParts.length === 1 && !/[\s"'`]/.test(String(commandParts[0]));
598
- const child = spawn(commandString, {
599
- stdio: interactivePassthrough ? 'inherit' : ['inherit', 'pipe', 'pipe'],
600
- shell: true,
601
- env: process.env
602
- });
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
+ }
603
687
 
604
688
  let sawApproval = false;
605
689
  let sawQuestion = false;
@@ -699,8 +783,12 @@ async function runWrap(commandParts) {
699
783
  }
700
784
 
701
785
  const exitCode = await new Promise((resolve) => {
786
+ child.on('error', () => resolve(1));
702
787
  child.on('close', (code) => resolve(code ?? 1));
703
788
  });
789
+ if (heartbeatInterval) {
790
+ clearInterval(heartbeatInterval);
791
+ }
704
792
  await Promise.allSettled(Array.from(pendingTasks));
705
793
 
706
794
  const completionMessage =
@@ -774,7 +862,9 @@ function normalizeEventType(value) {
774
862
  raw === 'task_completed' ||
775
863
  raw === 'error' ||
776
864
  raw === 'quota_warning' ||
777
- raw === 'question'
865
+ raw === 'question' ||
866
+ raw === 'session_started' ||
867
+ raw === 'session_running'
778
868
  ) {
779
869
  return raw;
780
870
  }
@@ -944,7 +1034,7 @@ async function runSetup(options) {
944
1034
 
945
1035
  const program = new Command();
946
1036
 
947
- program.name('runsignal').description('RunSignal CLI').version('0.1.5');
1037
+ program.name('runsignal').description('RunSignal CLI').version('0.1.6');
948
1038
 
949
1039
  program
950
1040
  .command('setup')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "runsignal",
3
- "version": "0.1.5",
3
+ "version": "0.1.6",
4
4
  "description": "RunSignal CLI for wrapping AI agent runs and handling approvals",
5
5
  "type": "module",
6
6
  "license": "MIT",