@klars/agentobs 0.1.1 → 0.1.3
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 -0
- package/dist/adapters/process-wrap.js +39 -12
- package/dist/commands/hook-config.js +25 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -168,6 +168,26 @@ across calls to manufacture a number.
|
|
|
168
168
|
Model prices live in `~/.agentobs/pricing.json` and are yours to edit. A model
|
|
169
169
|
missing from that file shows cost as `—`, never `$0.00`.
|
|
170
170
|
|
|
171
|
+
|
|
172
|
+
### Hook latency
|
|
173
|
+
|
|
174
|
+
The hook does its own work in **well under 1ms** (measured: ~0.3ms per
|
|
175
|
+
invocation, including the SQLite write). What you actually pay per tool call is
|
|
176
|
+
**Node.js process startup**, since Claude Code spawns the hook as a fresh
|
|
177
|
+
process each time.
|
|
178
|
+
|
|
179
|
+
On a typical Linux/macOS machine that is ~40-80ms. On Windows with real-time
|
|
180
|
+
antivirus scanning it can reach **1-1.5 seconds** - and that cost is not
|
|
181
|
+
specific to AgentObs: a bare `node -e "0"` measures the same. Check yours with:
|
|
182
|
+
|
|
183
|
+
```bash
|
|
184
|
+
node -e "0" # time this; it is the floor for any Node-based hook
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
If it is slow, adding an exclusion for your Node install directory and
|
|
188
|
+
`~/.agentobs` in your antivirus settings is the fix. There is no code change
|
|
189
|
+
that avoids it - the cost is paid before AgentObs runs at all.
|
|
190
|
+
|
|
171
191
|
---
|
|
172
192
|
|
|
173
193
|
## Dashboard access
|
|
@@ -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);
|
|
@@ -99,6 +105,18 @@ export async function runWrapped(command, opts = {}) {
|
|
|
99
105
|
}
|
|
100
106
|
});
|
|
101
107
|
}
|
|
108
|
+
/**
|
|
109
|
+
* Builds a cmd.exe command line, quoting each part that needs it.
|
|
110
|
+
*
|
|
111
|
+
* Used with windowsVerbatimArguments so Node passes this string through
|
|
112
|
+
* untouched; cmd.exe then does its own parsing. Each element is quoted only
|
|
113
|
+
* when it contains a space or a quote, because unnecessary quoting can change
|
|
114
|
+
* how some batch scripts interpret their arguments.
|
|
115
|
+
*/
|
|
116
|
+
function quoteWindows(executable, args) {
|
|
117
|
+
const quote = (s) => /[\s"]/.test(s) ? `"${s.replace(/"/g, '\\"')}"` : s;
|
|
118
|
+
return [quote(executable), ...args.map(quote)].join(' ');
|
|
119
|
+
}
|
|
102
120
|
/**
|
|
103
121
|
* Finds the real file behind a bare command name on Windows.
|
|
104
122
|
*
|
|
@@ -117,14 +135,23 @@ function resolveWindowsExecutable(command) {
|
|
|
117
135
|
const exts = (process.env.PATHEXT ?? '.COM;.EXE;.BAT;.CMD').split(';').filter(Boolean);
|
|
118
136
|
const dirs = (process.env.PATH ?? '').split(';').filter(Boolean);
|
|
119
137
|
for (const dir of dirs) {
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
138
|
+
// Try the PATHEXT extensions BEFORE the bare name. Node ships both `npm`
|
|
139
|
+
// (an extensionless shell script, for Git Bash) and `npm.cmd` in the same
|
|
140
|
+
// directory; the bare file exists but Windows cannot execute it, so
|
|
141
|
+
// checking it first resolves to something that fails with ENOENT.
|
|
142
|
+
for (const ext of exts) {
|
|
143
|
+
const lower = join(dir, command + ext.toLowerCase());
|
|
144
|
+
if (existsSync(lower))
|
|
145
|
+
return lower;
|
|
124
146
|
const upper = join(dir, command + ext);
|
|
125
147
|
if (existsSync(upper))
|
|
126
148
|
return upper;
|
|
127
149
|
}
|
|
150
|
+
// Only fall back to the bare name if no extension matched - covers a real
|
|
151
|
+
// extensionless executable, which is rare on Windows but not impossible.
|
|
152
|
+
const bare = join(dir, command);
|
|
153
|
+
if (existsSync(bare))
|
|
154
|
+
return bare;
|
|
128
155
|
}
|
|
129
156
|
return command;
|
|
130
157
|
}
|
|
@@ -22,7 +22,31 @@ import { existsSync } from 'node:fs';
|
|
|
22
22
|
*/
|
|
23
23
|
export function hookCommandPath() {
|
|
24
24
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
25
|
-
//
|
|
25
|
+
// On Windows, prefer npm's generated .cmd shim in the global bin directory.
|
|
26
|
+
//
|
|
27
|
+
// The raw bin/agentobs-hook file has no extension, and Windows cannot
|
|
28
|
+
// execute an extensionless file - cmd.exe reports "not recognized as an
|
|
29
|
+
// internal or external command". Claude Code swallows that, so the hook
|
|
30
|
+
// silently never runs and no data is ever recorded: the worst possible
|
|
31
|
+
// failure for an observability tool, because it looks like "no activity"
|
|
32
|
+
// rather than "broken". npm generates the .cmd shim for exactly this.
|
|
33
|
+
if (process.platform === 'win32') {
|
|
34
|
+
// dist/commands -> .../node_modules/@klars/agentobs -> up to the dir
|
|
35
|
+
// holding npm's shims (node_modules/.bin, or the global npm root).
|
|
36
|
+
const packageRoot = resolve(here, '..', '..');
|
|
37
|
+
const shims = [
|
|
38
|
+
resolve(packageRoot, '..', '..', '..', 'agentobs-hook.cmd'), // global npm root
|
|
39
|
+
resolve(packageRoot, '..', '..', '.bin', 'agentobs-hook.cmd'), // local node_modules/.bin
|
|
40
|
+
];
|
|
41
|
+
for (const shim of shims) {
|
|
42
|
+
if (existsSync(shim))
|
|
43
|
+
return shim;
|
|
44
|
+
}
|
|
45
|
+
// No shim found (e.g. running from a source checkout): fall back to the
|
|
46
|
+
// bare name so PATH resolution can still find it, rather than emitting a
|
|
47
|
+
// path that is guaranteed not to execute.
|
|
48
|
+
return 'agentobs-hook.cmd';
|
|
49
|
+
}
|
|
26
50
|
const candidate = resolve(here, '..', '..', 'bin', 'agentobs-hook');
|
|
27
51
|
if (existsSync(candidate))
|
|
28
52
|
return candidate;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@klars/agentobs",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
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",
|