runsignal 0.1.6 → 0.1.7

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 +130 -65
  2. package/package.json +1 -1
package/bin/runsignal.js CHANGED
@@ -18,6 +18,20 @@ const DEFAULT_APP_URL = process.env.RUNSIGNAL_APP_URL || 'https://runsignal.dev'
18
18
  const CONFIG_DIR = path.join(os.homedir(), '.runsignal');
19
19
  const CONFIG_PATH = path.join(CONFIG_DIR, 'config.json');
20
20
 
21
+ function extractErrorDetail(code, marker) {
22
+ const index = code.indexOf(marker);
23
+ if (index === -1) {
24
+ return '';
25
+ }
26
+
27
+ const detail = code.slice(index + marker.length).trim();
28
+ if (!detail) {
29
+ return '';
30
+ }
31
+
32
+ return detail.slice(0, 180);
33
+ }
34
+
21
35
  function toUserMessage(errorCode) {
22
36
  const code = String(errorCode || '');
23
37
  if (code.includes('not_logged_in')) return 'You are not logged in. Run: runsignal login';
@@ -32,8 +46,13 @@ function toUserMessage(errorCode) {
32
46
  if (code.includes('login_timeout')) return 'Login timed out. Retry runsignal login and validate quickly.';
33
47
  if (code.includes('login_poll_failed'))
34
48
  return 'Login polling failed. Check APP URL and server availability.';
35
- if (code.includes('send_event_failed'))
49
+ if (code.includes('send_event_failed')) {
50
+ const detail = extractErrorDetail(code, 'send_event_failed:');
51
+ if (detail) {
52
+ return `Event send failed (${detail}). Check machine key, network and server logs.`;
53
+ }
36
54
  return 'Event send failed. Check machine key, network and server logs.';
55
+ }
37
56
  if (code.includes('status_failed')) return 'Status check failed. Verify event id and machine ownership.';
38
57
  if (code.includes('event_type_invalid'))
39
58
  return 'Invalid event type. Use approval_required, task_completed, error, quota_warning, question.';
@@ -101,6 +120,39 @@ function sleep(ms) {
101
120
  return new Promise((resolve) => setTimeout(resolve, ms));
102
121
  }
103
122
 
123
+ function tryOpenBrowser(url) {
124
+ if (!url) return false;
125
+
126
+ try {
127
+ if (process.platform === 'win32') {
128
+ const child = spawn('cmd', ['/c', 'start', '', url], {
129
+ detached: true,
130
+ stdio: 'ignore'
131
+ });
132
+ child.unref();
133
+ return true;
134
+ }
135
+
136
+ if (process.platform === 'darwin') {
137
+ const child = spawn('open', [url], {
138
+ detached: true,
139
+ stdio: 'ignore'
140
+ });
141
+ child.unref();
142
+ return true;
143
+ }
144
+
145
+ const child = spawn('xdg-open', [url], {
146
+ detached: true,
147
+ stdio: 'ignore'
148
+ });
149
+ child.unref();
150
+ return true;
151
+ } catch {
152
+ return false;
153
+ }
154
+ }
155
+
104
156
  const HOOK_START = '# >>> RunSignal hooks start >>>';
105
157
  const HOOK_END = '# <<< RunSignal hooks end <<<';
106
158
 
@@ -479,7 +531,13 @@ async function runLogin(options) {
479
531
  throw new Error('login_payload_invalid');
480
532
  }
481
533
 
482
- console.log(`Open ${verificationUrl} and enter code: ${userCode}`);
534
+ const verificationUrlWithCode = `${verificationUrl}?code=${encodeURIComponent(String(userCode))}`;
535
+ const opened = tryOpenBrowser(verificationUrlWithCode);
536
+ if (opened) {
537
+ console.log('Browser opened.');
538
+ } else {
539
+ console.log(`Open ${verificationUrlWithCode} and enter code: ${userCode}`);
540
+ }
483
541
 
484
542
  let accessToken = null;
485
543
  const deadline = Date.now() + 10 * 60 * 1000;
@@ -604,7 +662,7 @@ async function runWrap(commandParts) {
604
662
  }
605
663
 
606
664
  const repo = getRepoFromGit();
607
- const branch = getBranchFromGit();
665
+ const branch = getBranchFromGit() || undefined;
608
666
  const machineName = config.machine_name || getMachineName();
609
667
  const agentLabel = inferAgentLabel(commandParts);
610
668
 
@@ -641,48 +699,52 @@ async function runWrap(commandParts) {
641
699
  env: process.env
642
700
  });
643
701
 
702
+ const startedAt = Date.now();
703
+ const sessionTarget = interactivePassthrough ? singleExecutable : commandString;
704
+ try {
705
+ await sendEvent({
706
+ apiFetch,
707
+ baseUrl,
708
+ machineApiKey,
709
+ event: {
710
+ message: `Session started: ${sessionTarget}`,
711
+ event_type: 'session_started',
712
+ repo,
713
+ branch,
714
+ severity: 'info',
715
+ context,
716
+ agent: agentLabel
717
+ }
718
+ });
719
+ } catch {
720
+ // non-blocking
721
+ }
722
+
644
723
  let heartbeatInterval = null;
645
724
  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
725
+ const heartbeatMin = Number.parseInt(String(process.env.RUNSIGNAL_HEARTBEAT_MIN ?? '0'), 10);
726
+ if (Number.isFinite(heartbeatMin) && heartbeatMin > 0) {
727
+ heartbeatInterval = setInterval(() => {
728
+ const elapsedMin = Math.max(1, Math.floor((Date.now() - startedAt) / 60_000));
729
+ sendEvent({
730
+ apiFetch,
731
+ baseUrl,
732
+ machineApiKey,
733
+ event: {
734
+ message: `Session running (${elapsedMin} min): ${singleExecutable}`,
735
+ event_type: 'session_running',
736
+ repo,
737
+ branch,
738
+ severity: 'info',
739
+ context,
740
+ agent: agentLabel
741
+ }
742
+ }).catch(() => {
743
+ // non-blocking
744
+ });
745
+ }, heartbeatMin * 60_000);
746
+ heartbeatInterval.unref?.();
664
747
  }
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
748
  }
687
749
 
688
750
  let sawApproval = false;
@@ -791,28 +853,30 @@ async function runWrap(commandParts) {
791
853
  }
792
854
  await Promise.allSettled(Array.from(pendingTasks));
793
855
 
794
- const completionMessage =
795
- exitCode === 0
796
- ? `Wrapped command completed: ${commandParts.join(' ')}`
797
- : `Wrapped command failed (code ${exitCode}): ${lastInterestingLine || commandParts.join(' ')}`;
856
+ if (!interactivePassthrough || exitCode !== 0) {
857
+ const completionMessage =
858
+ exitCode === 0
859
+ ? `Wrapped command completed: ${commandParts.join(' ')}`
860
+ : `Wrapped command failed (code ${exitCode}): ${lastInterestingLine || commandParts.join(' ')}`;
798
861
 
799
- try {
800
- await sendEvent({
801
- apiFetch,
802
- baseUrl,
803
- machineApiKey,
804
- event: {
805
- message: completionMessage.slice(0, 1000),
806
- event_type: exitCode === 0 ? 'task_completed' : 'error',
807
- repo,
808
- branch,
809
- severity: exitCode === 0 ? 'info' : 'critical',
810
- context,
811
- agent: agentLabel
812
- }
813
- });
814
- } catch {
815
- // no-op
862
+ try {
863
+ await sendEvent({
864
+ apiFetch,
865
+ baseUrl,
866
+ machineApiKey,
867
+ event: {
868
+ message: completionMessage.slice(0, 1000),
869
+ event_type: exitCode === 0 ? 'task_completed' : 'error',
870
+ repo,
871
+ branch,
872
+ severity: exitCode === 0 ? 'info' : 'critical',
873
+ context,
874
+ agent: agentLabel
875
+ }
876
+ });
877
+ } catch {
878
+ // no-op
879
+ }
816
880
  }
817
881
 
818
882
  // Avoid hard process exit on Windows: let Node close handles cleanly.
@@ -830,7 +894,7 @@ async function runTest() {
830
894
  }
831
895
 
832
896
  const repo = getRepoFromGit();
833
- const branch = getBranchFromGit();
897
+ const branch = getBranchFromGit() || undefined;
834
898
 
835
899
  const result = await sendEvent({
836
900
  apiFetch,
@@ -901,7 +965,8 @@ async function runSend(options) {
901
965
  }
902
966
 
903
967
  const repo = options.repo ? String(options.repo).trim() : getRepoFromGit();
904
- const branch = options.branch ? String(options.branch).trim() : getBranchFromGit();
968
+ const branchRaw = options.branch ? String(options.branch).trim() : getBranchFromGit();
969
+ const branch = branchRaw || undefined;
905
970
  const agent = options.agent ? String(options.agent).trim() : 'manual-cli';
906
971
 
907
972
  const result = await sendEvent({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "runsignal",
3
- "version": "0.1.6",
3
+ "version": "0.1.7",
4
4
  "description": "RunSignal CLI for wrapping AI agent runs and handling approvals",
5
5
  "type": "module",
6
6
  "license": "MIT",