runsignal 0.1.2 → 0.1.4
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 +20 -3
- package/bin/runsignal.js +285 -12
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -19,9 +19,12 @@ npx runsignal --help
|
|
|
19
19
|
- `runsignal login`
|
|
20
20
|
- `runsignal init`
|
|
21
21
|
- `runsignal wrap -- <command>`
|
|
22
|
-
- `runsignal send --message "..."`
|
|
23
|
-
- `runsignal status <eventId>`
|
|
24
|
-
- `runsignal logout`
|
|
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`
|
|
25
28
|
|
|
26
29
|
## Quickstart
|
|
27
30
|
|
|
@@ -34,4 +37,18 @@ npx runsignal@latest wrap -- claude "refactor auth flow"
|
|
|
34
37
|
## Environment
|
|
35
38
|
|
|
36
39
|
- `RUNSIGNAL_APP_URL` to override app URL (default: `https://runsignal.dev`)
|
|
40
|
+
|
|
41
|
+
## Automatic Wrapping
|
|
42
|
+
|
|
43
|
+
To keep using `claude ...` / `codex ...` normally while routing through RunSignal:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
runsignal hooks install --shell powershell
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Then restart terminal and verify:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
runsignal hooks doctor --shell powershell
|
|
53
|
+
```
|
|
37
54
|
|
package/bin/runsignal.js
CHANGED
|
@@ -40,6 +40,8 @@ function toUserMessage(errorCode) {
|
|
|
40
40
|
if (code.includes('message_required')) return 'Message is required. Use --message "..."';
|
|
41
41
|
if (code.includes('event_id_required')) return 'Event id is required. Usage: runsignal status <eventId>';
|
|
42
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.';
|
|
43
45
|
return code || 'Unknown error';
|
|
44
46
|
}
|
|
45
47
|
|
|
@@ -98,6 +100,226 @@ function sleep(ms) {
|
|
|
98
100
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
99
101
|
}
|
|
100
102
|
|
|
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 RunSignal-Resolve-NativeCommand {
|
|
124
|
+
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
|
|
128
|
+
}
|
|
129
|
+
function claude {
|
|
130
|
+
$native = RunSignal-Resolve-NativeCommand "claude"
|
|
131
|
+
if (-not $native) { Write-Error "RunSignal hook: native claude executable not found."; return }
|
|
132
|
+
if ($args.Count -eq 0) { & $native; return }
|
|
133
|
+
runsignal wrap -- $native @args
|
|
134
|
+
}
|
|
135
|
+
function codex {
|
|
136
|
+
$native = RunSignal-Resolve-NativeCommand "codex"
|
|
137
|
+
if (-not $native) { Write-Error "RunSignal hook: native codex executable not found."; return }
|
|
138
|
+
if ($args.Count -eq 0) { & $native; return }
|
|
139
|
+
runsignal wrap -- $native @args
|
|
140
|
+
}
|
|
141
|
+
function cursor {
|
|
142
|
+
$native = RunSignal-Resolve-NativeCommand "cursor"
|
|
143
|
+
if (-not $native) { Write-Error "RunSignal hook: native cursor executable not found."; return }
|
|
144
|
+
if ($args.Count -eq 0) { & $native; return }
|
|
145
|
+
runsignal wrap -- $native @args
|
|
146
|
+
}
|
|
147
|
+
function gemini {
|
|
148
|
+
$native = RunSignal-Resolve-NativeCommand "gemini"
|
|
149
|
+
if (-not $native) { Write-Error "RunSignal hook: native gemini executable not found."; return }
|
|
150
|
+
if ($args.Count -eq 0) { & $native; return }
|
|
151
|
+
runsignal wrap -- $native @args
|
|
152
|
+
}
|
|
153
|
+
function aider {
|
|
154
|
+
$native = RunSignal-Resolve-NativeCommand "aider"
|
|
155
|
+
if (-not $native) { Write-Error "RunSignal hook: native aider executable not found."; return }
|
|
156
|
+
if ($args.Count -eq 0) { & $native; return }
|
|
157
|
+
runsignal wrap -- $native @args
|
|
158
|
+
}
|
|
159
|
+
${HOOK_END}
|
|
160
|
+
`;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return `${HOOK_START}
|
|
164
|
+
runsignal_resolve_native() {
|
|
165
|
+
type -P "$1" 2>/dev/null
|
|
166
|
+
}
|
|
167
|
+
claude() {
|
|
168
|
+
local native
|
|
169
|
+
native="$(runsignal_resolve_native claude)"
|
|
170
|
+
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
|
+
runsignal wrap -- "$native" "$@"
|
|
173
|
+
}
|
|
174
|
+
codex() {
|
|
175
|
+
local native
|
|
176
|
+
native="$(runsignal_resolve_native codex)"
|
|
177
|
+
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
|
+
runsignal wrap -- "$native" "$@"
|
|
180
|
+
}
|
|
181
|
+
cursor() {
|
|
182
|
+
local native
|
|
183
|
+
native="$(runsignal_resolve_native cursor)"
|
|
184
|
+
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
|
+
runsignal wrap -- "$native" "$@"
|
|
187
|
+
}
|
|
188
|
+
gemini() {
|
|
189
|
+
local native
|
|
190
|
+
native="$(runsignal_resolve_native gemini)"
|
|
191
|
+
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
|
+
runsignal wrap -- "$native" "$@"
|
|
194
|
+
}
|
|
195
|
+
aider() {
|
|
196
|
+
local native
|
|
197
|
+
native="$(runsignal_resolve_native aider)"
|
|
198
|
+
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
|
+
runsignal wrap -- "$native" "$@"
|
|
201
|
+
}
|
|
202
|
+
${HOOK_END}
|
|
203
|
+
`;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function resolveHookTargets(shellOption) {
|
|
207
|
+
const shell = String(shellOption || 'auto').toLowerCase();
|
|
208
|
+
const home = os.homedir();
|
|
209
|
+
|
|
210
|
+
if (process.platform === 'win32') {
|
|
211
|
+
const powershellTargets = [
|
|
212
|
+
{
|
|
213
|
+
path: path.join(home, 'Documents', 'PowerShell', 'Microsoft.PowerShell_profile.ps1'),
|
|
214
|
+
kind: 'powershell'
|
|
215
|
+
},
|
|
216
|
+
{
|
|
217
|
+
path: path.join(home, 'Documents', 'WindowsPowerShell', 'Microsoft.PowerShell_profile.ps1'),
|
|
218
|
+
kind: 'powershell'
|
|
219
|
+
}
|
|
220
|
+
];
|
|
221
|
+
if (shell === 'auto' || shell === 'powershell' || shell === 'all') {
|
|
222
|
+
return powershellTargets;
|
|
223
|
+
}
|
|
224
|
+
return [];
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (shell === 'zsh') return [{path: path.join(home, '.zshrc'), kind: 'posix'}];
|
|
228
|
+
if (shell === 'bash') return [{path: path.join(home, '.bashrc'), kind: 'posix'}];
|
|
229
|
+
if (shell === 'all') {
|
|
230
|
+
return [
|
|
231
|
+
{path: path.join(home, '.bashrc'), kind: 'posix'},
|
|
232
|
+
{path: path.join(home, '.zshrc'), kind: 'posix'}
|
|
233
|
+
];
|
|
234
|
+
}
|
|
235
|
+
if (shell === 'auto') {
|
|
236
|
+
const shellEnv = String(process.env.SHELL || '').toLowerCase();
|
|
237
|
+
if (shellEnv.includes('zsh')) return [{path: path.join(home, '.zshrc'), kind: 'posix'}];
|
|
238
|
+
return [{path: path.join(home, '.bashrc'), kind: 'posix'}];
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return [];
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
async function readTextFile(filePath) {
|
|
245
|
+
try {
|
|
246
|
+
return await fs.readFile(filePath, 'utf8');
|
|
247
|
+
} catch {
|
|
248
|
+
return '';
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
async function runHooksInstall(options) {
|
|
253
|
+
const targets = resolveHookTargets(options.shell);
|
|
254
|
+
if (!targets.length) {
|
|
255
|
+
throw new Error('hooks_shell_not_supported');
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
for (const target of targets) {
|
|
259
|
+
const existing = await readTextFile(target.path);
|
|
260
|
+
const cleaned = stripHookBlock(existing).trimEnd();
|
|
261
|
+
const block = buildHookBlock(target.kind);
|
|
262
|
+
const next = cleaned ? `${cleaned}\n\n${block}` : block;
|
|
263
|
+
await fs.mkdir(path.dirname(target.path), {recursive: true});
|
|
264
|
+
await fs.writeFile(target.path, next, 'utf8');
|
|
265
|
+
console.log(`Hooks installed: ${target.path}`);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
console.log('Restart your terminal to apply hooks.');
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
async function runHooksUninstall(options) {
|
|
272
|
+
const targets = resolveHookTargets(options.shell);
|
|
273
|
+
if (!targets.length) {
|
|
274
|
+
throw new Error('hooks_shell_not_supported');
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
for (const target of targets) {
|
|
278
|
+
const existing = await readTextFile(target.path);
|
|
279
|
+
if (!existing) {
|
|
280
|
+
console.log(`Hooks not present: ${target.path}`);
|
|
281
|
+
continue;
|
|
282
|
+
}
|
|
283
|
+
const next = stripHookBlock(existing);
|
|
284
|
+
if (next === existing) {
|
|
285
|
+
console.log(`Hooks not present: ${target.path}`);
|
|
286
|
+
continue;
|
|
287
|
+
}
|
|
288
|
+
await fs.writeFile(target.path, next, 'utf8');
|
|
289
|
+
console.log(`Hooks removed: ${target.path}`);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
async function runHooksDoctor(options) {
|
|
294
|
+
const targets = resolveHookTargets(options.shell);
|
|
295
|
+
if (!targets.length) {
|
|
296
|
+
throw new Error('hooks_shell_not_supported');
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
let healthy = true;
|
|
300
|
+
for (const target of targets) {
|
|
301
|
+
const existing = await readTextFile(target.path);
|
|
302
|
+
const hasHooks = existing.includes(HOOK_START) && existing.includes(HOOK_END);
|
|
303
|
+
if (!hasHooks) {
|
|
304
|
+
healthy = false;
|
|
305
|
+
}
|
|
306
|
+
console.log(
|
|
307
|
+
JSON.stringify(
|
|
308
|
+
{
|
|
309
|
+
path: target.path,
|
|
310
|
+
installed: hasHooks
|
|
311
|
+
},
|
|
312
|
+
null,
|
|
313
|
+
2
|
|
314
|
+
)
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
if (!healthy) {
|
|
319
|
+
process.exitCode = 1;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
101
323
|
function getMachineName() {
|
|
102
324
|
return process.env.COMPUTERNAME || os.hostname() || 'machine';
|
|
103
325
|
}
|
|
@@ -171,6 +393,18 @@ function extractInterestingLine(chunk) {
|
|
|
171
393
|
return null;
|
|
172
394
|
}
|
|
173
395
|
|
|
396
|
+
function quoteForShell(arg) {
|
|
397
|
+
const value = String(arg ?? '');
|
|
398
|
+
if (value.length === 0) {
|
|
399
|
+
return '""';
|
|
400
|
+
}
|
|
401
|
+
if (!/[\s"]/g.test(value)) {
|
|
402
|
+
return value;
|
|
403
|
+
}
|
|
404
|
+
const escaped = value.replace(/(["\\$`])/g, '\\$1');
|
|
405
|
+
return `"${escaped}"`;
|
|
406
|
+
}
|
|
407
|
+
|
|
174
408
|
async function runLogin(options) {
|
|
175
409
|
const config = await readConfig();
|
|
176
410
|
const baseUrl = String(options.appUrl || config.app_url || DEFAULT_APP_URL).replace(/\/$/, '');
|
|
@@ -325,9 +559,14 @@ async function runWrap(commandParts) {
|
|
|
325
559
|
agent_label: agentLabel
|
|
326
560
|
};
|
|
327
561
|
|
|
328
|
-
const commandString =
|
|
562
|
+
const commandString =
|
|
563
|
+
commandParts.length === 1
|
|
564
|
+
? String(commandParts[0])
|
|
565
|
+
: commandParts.map((part) => quoteForShell(part)).join(' ');
|
|
566
|
+
const interactivePassthrough =
|
|
567
|
+
commandParts.length === 1 && !/[\s"'`]/.test(String(commandParts[0]));
|
|
329
568
|
const child = spawn(commandString, {
|
|
330
|
-
stdio: ['inherit', 'pipe', 'pipe'],
|
|
569
|
+
stdio: interactivePassthrough ? 'inherit' : ['inherit', 'pipe', 'pipe'],
|
|
331
570
|
shell: true,
|
|
332
571
|
env: process.env
|
|
333
572
|
});
|
|
@@ -335,6 +574,12 @@ async function runWrap(commandParts) {
|
|
|
335
574
|
let sawApproval = false;
|
|
336
575
|
let sawQuestion = false;
|
|
337
576
|
let lastInterestingLine = '';
|
|
577
|
+
const pendingTasks = new Set();
|
|
578
|
+
|
|
579
|
+
const queueTask = (promise) => {
|
|
580
|
+
pendingTasks.add(promise);
|
|
581
|
+
promise.finally(() => pendingTasks.delete(promise));
|
|
582
|
+
};
|
|
338
583
|
|
|
339
584
|
const handleChunk = async (chunkRaw) => {
|
|
340
585
|
const chunk = chunkRaw.toString('utf8');
|
|
@@ -412,18 +657,21 @@ async function runWrap(commandParts) {
|
|
|
412
657
|
}
|
|
413
658
|
};
|
|
414
659
|
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
660
|
+
if (!interactivePassthrough) {
|
|
661
|
+
child.stdout.on('data', (chunk) => {
|
|
662
|
+
process.stdout.write(chunk);
|
|
663
|
+
queueTask(handleChunk(chunk));
|
|
664
|
+
});
|
|
665
|
+
child.stderr.on('data', (chunk) => {
|
|
666
|
+
process.stderr.write(chunk);
|
|
667
|
+
queueTask(handleChunk(chunk));
|
|
668
|
+
});
|
|
669
|
+
}
|
|
423
670
|
|
|
424
671
|
const exitCode = await new Promise((resolve) => {
|
|
425
672
|
child.on('close', (code) => resolve(code ?? 1));
|
|
426
673
|
});
|
|
674
|
+
await Promise.allSettled(Array.from(pendingTasks));
|
|
427
675
|
|
|
428
676
|
const completionMessage =
|
|
429
677
|
exitCode === 0
|
|
@@ -449,7 +697,9 @@ async function runWrap(commandParts) {
|
|
|
449
697
|
// no-op
|
|
450
698
|
}
|
|
451
699
|
|
|
452
|
-
process.
|
|
700
|
+
// Avoid hard process exit on Windows: let Node close handles cleanly.
|
|
701
|
+
process.exitCode = exitCode;
|
|
702
|
+
return;
|
|
453
703
|
}
|
|
454
704
|
|
|
455
705
|
async function runTest() {
|
|
@@ -609,7 +859,7 @@ async function runLogout() {
|
|
|
609
859
|
|
|
610
860
|
const program = new Command();
|
|
611
861
|
|
|
612
|
-
program.name('runsignal').description('RunSignal CLI').version('0.1.
|
|
862
|
+
program.name('runsignal').description('RunSignal CLI').version('0.1.4');
|
|
613
863
|
|
|
614
864
|
program
|
|
615
865
|
.command('login')
|
|
@@ -666,6 +916,29 @@ program.command('logout').action(async () => {
|
|
|
666
916
|
await runLogout();
|
|
667
917
|
});
|
|
668
918
|
|
|
919
|
+
const hooks = program.command('hooks').description('Manage shell hooks for automatic wrapping');
|
|
920
|
+
|
|
921
|
+
hooks
|
|
922
|
+
.command('install')
|
|
923
|
+
.option('--shell <shell>', 'auto|powershell|bash|zsh|all', 'auto')
|
|
924
|
+
.action(async (options) => {
|
|
925
|
+
await runHooksInstall(options);
|
|
926
|
+
});
|
|
927
|
+
|
|
928
|
+
hooks
|
|
929
|
+
.command('uninstall')
|
|
930
|
+
.option('--shell <shell>', 'auto|powershell|bash|zsh|all', 'auto')
|
|
931
|
+
.action(async (options) => {
|
|
932
|
+
await runHooksUninstall(options);
|
|
933
|
+
});
|
|
934
|
+
|
|
935
|
+
hooks
|
|
936
|
+
.command('doctor')
|
|
937
|
+
.option('--shell <shell>', 'auto|powershell|bash|zsh|all', 'auto')
|
|
938
|
+
.action(async (options) => {
|
|
939
|
+
await runHooksDoctor(options);
|
|
940
|
+
});
|
|
941
|
+
|
|
669
942
|
program.parseAsync(process.argv).catch((error) => {
|
|
670
943
|
const message = error instanceof Error ? error.message : String(error);
|
|
671
944
|
console.error(`RunSignal error: ${toUserMessage(message)}`);
|