@klars/agentobs 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/dist/adapters/process-wrap.js +62 -13
- package/dist/cli.js +14 -0
- package/package.json +1 -1
|
@@ -61,14 +61,20 @@ export async function runWrapped(command, opts = {}) {
|
|
|
61
61
|
// Resolving the .cmd/.bat shim directly keeps npm-installed CLIs working
|
|
62
62
|
// (`agentobs run -- claude`) while spawn still escapes each argument.
|
|
63
63
|
const executable = process.platform === 'win32' ? resolveWindowsExecutable(command[0]) : command[0];
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
64
|
+
// A .cmd/.bat file is a batch script: Windows cannot exec it directly, it
|
|
65
|
+
// must run through cmd.exe. Spawning cmd.exe explicitly with /d /s /c and
|
|
66
|
+
// a quoted command line is the only form that survives BOTH a path
|
|
67
|
+
// containing spaces (C:\Program Files\nodejs\npm.cmd) and arguments
|
|
68
|
+
// containing spaces. `shell: true` cannot do this - it concatenates
|
|
69
|
+
// without quoting (Node DEP0190), so the path breaks at the first space.
|
|
70
|
+
const isBatch = process.platform === 'win32' && /\.(cmd|bat)$/i.test(executable);
|
|
71
|
+
const child = isBatch
|
|
72
|
+
? spawn(process.env.COMSPEC ?? 'cmd.exe', ['/d', '/s', '/c', `"${quoteWindows(executable, command.slice(1))}"`], { cwd, stdio: 'inherit', windowsHide: true, windowsVerbatimArguments: true })
|
|
73
|
+
: spawn(executable, command.slice(1), {
|
|
74
|
+
cwd,
|
|
75
|
+
stdio: 'inherit',
|
|
76
|
+
windowsHide: true,
|
|
77
|
+
});
|
|
72
78
|
const finish = (code) => {
|
|
73
79
|
sink({ type: 'session_end', sessionId, exitCode: code });
|
|
74
80
|
resolve(code);
|
|
@@ -76,7 +82,19 @@ export async function runWrapped(command, opts = {}) {
|
|
|
76
82
|
child.on('error', (err) => {
|
|
77
83
|
// Spawn failure (command not found) - record it as a failed session
|
|
78
84
|
// rather than losing the attempt entirely.
|
|
79
|
-
|
|
85
|
+
// A shell builtin (dir, echo, cd, type) is not a program on disk, so
|
|
86
|
+
// there is nothing to spawn. Saying only "ENOENT" sends the user
|
|
87
|
+
// hunting for a broken install; naming the actual cause and the
|
|
88
|
+
// workaround is the difference between a dead end and a fix.
|
|
89
|
+
if (process.platform === 'win32' && WINDOWS_BUILTINS.has(command[0].toLowerCase())) {
|
|
90
|
+
console.error(`[agentobs] "${command[0]}" is a cmd.exe builtin, not a program, so there is ` +
|
|
91
|
+
`nothing to wrap.\n` +
|
|
92
|
+
` Try: agentobs run -- cmd /c ${command.join(' ')}`);
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
console.error(`[agentobs] failed to start ${command[0]}: ${err.message}\n` +
|
|
96
|
+
` Check the command exists and is on your PATH.`);
|
|
97
|
+
}
|
|
80
98
|
finish(127);
|
|
81
99
|
});
|
|
82
100
|
child.on('close', (code, signal) => {
|
|
@@ -99,6 +117,28 @@ export async function runWrapped(command, opts = {}) {
|
|
|
99
117
|
}
|
|
100
118
|
});
|
|
101
119
|
}
|
|
120
|
+
/**
|
|
121
|
+
* cmd.exe builtins - commands that exist only inside the shell, with no
|
|
122
|
+
* executable on disk. Spawning them always fails with ENOENT, so they get a
|
|
123
|
+
* message that explains why rather than one that looks like a broken install.
|
|
124
|
+
*/
|
|
125
|
+
const WINDOWS_BUILTINS = new Set([
|
|
126
|
+
'dir', 'echo', 'cd', 'chdir', 'type', 'copy', 'move', 'del', 'erase', 'md',
|
|
127
|
+
'mkdir', 'rd', 'rmdir', 'ren', 'rename', 'cls', 'set', 'ver', 'vol', 'path',
|
|
128
|
+
'pause', 'title', 'prompt', 'assoc', 'ftype', 'exit', 'call', 'start',
|
|
129
|
+
]);
|
|
130
|
+
/**
|
|
131
|
+
* Builds a cmd.exe command line, quoting each part that needs it.
|
|
132
|
+
*
|
|
133
|
+
* Used with windowsVerbatimArguments so Node passes this string through
|
|
134
|
+
* untouched; cmd.exe then does its own parsing. Each element is quoted only
|
|
135
|
+
* when it contains a space or a quote, because unnecessary quoting can change
|
|
136
|
+
* how some batch scripts interpret their arguments.
|
|
137
|
+
*/
|
|
138
|
+
function quoteWindows(executable, args) {
|
|
139
|
+
const quote = (s) => /[\s"]/.test(s) ? `"${s.replace(/"/g, '\\"')}"` : s;
|
|
140
|
+
return [quote(executable), ...args.map(quote)].join(' ');
|
|
141
|
+
}
|
|
102
142
|
/**
|
|
103
143
|
* Finds the real file behind a bare command name on Windows.
|
|
104
144
|
*
|
|
@@ -117,14 +157,23 @@ function resolveWindowsExecutable(command) {
|
|
|
117
157
|
const exts = (process.env.PATHEXT ?? '.COM;.EXE;.BAT;.CMD').split(';').filter(Boolean);
|
|
118
158
|
const dirs = (process.env.PATH ?? '').split(';').filter(Boolean);
|
|
119
159
|
for (const dir of dirs) {
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
160
|
+
// Try the PATHEXT extensions BEFORE the bare name. Node ships both `npm`
|
|
161
|
+
// (an extensionless shell script, for Git Bash) and `npm.cmd` in the same
|
|
162
|
+
// directory; the bare file exists but Windows cannot execute it, so
|
|
163
|
+
// checking it first resolves to something that fails with ENOENT.
|
|
164
|
+
for (const ext of exts) {
|
|
165
|
+
const lower = join(dir, command + ext.toLowerCase());
|
|
166
|
+
if (existsSync(lower))
|
|
167
|
+
return lower;
|
|
124
168
|
const upper = join(dir, command + ext);
|
|
125
169
|
if (existsSync(upper))
|
|
126
170
|
return upper;
|
|
127
171
|
}
|
|
172
|
+
// Only fall back to the bare name if no extension matched - covers a real
|
|
173
|
+
// extensionless executable, which is rare on Windows but not impossible.
|
|
174
|
+
const bare = join(dir, command);
|
|
175
|
+
if (existsSync(bare))
|
|
176
|
+
return bare;
|
|
128
177
|
}
|
|
129
178
|
return command;
|
|
130
179
|
}
|
package/dist/cli.js
CHANGED
|
@@ -48,6 +48,12 @@ export function buildProgram() {
|
|
|
48
48
|
.command('watch')
|
|
49
49
|
.argument('<file>', 'JSONL file to tail')
|
|
50
50
|
.description('Ingest a newline-delimited JSON agent log')
|
|
51
|
+
.addHelpText('after', `
|
|
52
|
+
Example:
|
|
53
|
+
agentobs watch ./agent-log.jsonl --agent my-agent
|
|
54
|
+
|
|
55
|
+
The file should contain one JSON object per line with a "type" field
|
|
56
|
+
(session_start, tool_call_start, tool_call_end, session_end).`)
|
|
51
57
|
.option('--agent <name>', 'agent name to record', 'generic')
|
|
52
58
|
.option('--no-follow', 'process existing lines then exit')
|
|
53
59
|
.action(async (file, opts) => {
|
|
@@ -57,6 +63,14 @@ export function buildProgram() {
|
|
|
57
63
|
program
|
|
58
64
|
.command('run')
|
|
59
65
|
.description('Run a command under observation (coarse: duration and exit code)')
|
|
66
|
+
.addHelpText('after', `
|
|
67
|
+
Examples:
|
|
68
|
+
agentobs run -- npm test
|
|
69
|
+
agentobs run -- claude
|
|
70
|
+
agentobs run -- git status
|
|
71
|
+
|
|
72
|
+
Note the "--": everything after it is the command to observe.
|
|
73
|
+
On Windows, cmd.exe builtins (dir, echo, type) need: agentobs run -- cmd /c dir`)
|
|
60
74
|
.argument('<command...>', 'command to run, after --')
|
|
61
75
|
.option('--agent <name>', 'agent name to record')
|
|
62
76
|
.allowUnknownOption()
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@klars/agentobs",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"description": "Observability and control layer for AI coding agents - see every tool call, token, and dollar your agents spend, and stop them before they do something risky.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Klars AI",
|