@phnx-labs/agents-cli 1.20.51 → 1.20.53
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/CHANGELOG.md +71 -0
- package/README.md +12 -0
- package/dist/bin/agents +0 -0
- package/dist/commands/browser.js +215 -7
- package/dist/commands/cloud.d.ts +3 -0
- package/dist/commands/cloud.js +8 -1
- package/dist/commands/events.d.ts +1 -1
- package/dist/commands/events.js +2 -3
- package/dist/commands/exec.js +82 -3
- package/dist/commands/factory.js +8 -0
- package/dist/commands/feed.d.ts +38 -0
- package/dist/commands/feed.js +274 -0
- package/dist/commands/logs.d.ts +5 -1
- package/dist/commands/logs.js +248 -3
- package/dist/commands/mcp.js +7 -0
- package/dist/commands/memory.d.ts +9 -0
- package/dist/commands/memory.js +164 -0
- package/dist/commands/message.d.ts +11 -6
- package/dist/commands/message.js +140 -5
- package/dist/commands/routines.js +12 -0
- package/dist/commands/secrets-migrate.d.ts +2 -1
- package/dist/commands/secrets-migrate.js +88 -13
- package/dist/commands/secrets.d.ts +22 -0
- package/dist/commands/secrets.js +176 -42
- package/dist/commands/sessions.js +10 -1
- package/dist/commands/teams.js +4 -0
- package/dist/commands/worktree.js +4 -2
- package/dist/index.js +21 -22
- package/dist/lib/agents.js +249 -17
- package/dist/lib/answer-router.d.ts +75 -0
- package/dist/lib/answer-router.js +149 -0
- package/dist/lib/ask-classifier.d.ts +71 -0
- package/dist/lib/ask-classifier.js +197 -0
- package/dist/lib/browser/login-detection.d.ts +94 -0
- package/dist/lib/browser/login-detection.js +274 -0
- package/dist/lib/browser/profiles.d.ts +17 -8
- package/dist/lib/browser/profiles.js +27 -8
- package/dist/lib/browser/secret-ref.d.ts +10 -0
- package/dist/lib/browser/secret-ref.js +14 -0
- package/dist/lib/browser/service.js +14 -12
- package/dist/lib/cloud/antigravity.d.ts +0 -2
- package/dist/lib/cloud/antigravity.js +2 -17
- package/dist/lib/cloud/codex.js +3 -18
- package/dist/lib/cloud/rush.d.ts +15 -0
- package/dist/lib/cloud/rush.js +10 -16
- package/dist/lib/cloud/stream.js +2 -0
- package/dist/lib/cloud/types.d.ts +21 -0
- package/dist/lib/cloud/types.js +81 -0
- package/dist/lib/crabbox/cli.d.ts +1 -1
- package/dist/lib/crabbox/cli.js +12 -2
- package/dist/lib/crabbox/lease.d.ts +19 -0
- package/dist/lib/crabbox/lease.js +22 -11
- package/dist/lib/crabbox/progress.d.ts +62 -0
- package/dist/lib/crabbox/progress.js +129 -0
- package/dist/lib/crabbox/runtimes.d.ts +38 -1
- package/dist/lib/crabbox/runtimes.js +98 -5
- package/dist/lib/daemon.d.ts +12 -9
- package/dist/lib/daemon.js +32 -17
- package/dist/lib/events.d.ts +31 -5
- package/dist/lib/events.js +291 -101
- package/dist/lib/exec.js +20 -1
- package/dist/lib/feed-outcome.d.ts +101 -0
- package/dist/lib/feed-outcome.js +244 -0
- package/dist/lib/feed-policy.d.ts +30 -0
- package/dist/lib/feed-policy.js +133 -0
- package/dist/lib/feed.d.ts +180 -0
- package/dist/lib/feed.js +627 -0
- package/dist/lib/git.d.ts +17 -1
- package/dist/lib/git.js +20 -1
- package/dist/lib/hooks.js +529 -14
- package/dist/lib/hosts/passthrough.d.ts +3 -3
- package/dist/lib/hosts/passthrough.js +3 -3
- package/dist/lib/mailbox-gc.d.ts +22 -0
- package/dist/lib/mailbox-gc.js +161 -0
- package/dist/lib/mailbox.d.ts +26 -2
- package/dist/lib/mailbox.js +80 -5
- package/dist/lib/mcp.js +82 -0
- package/dist/lib/memory.d.ts +55 -0
- package/dist/lib/memory.js +274 -0
- package/dist/lib/notify.d.ts +16 -0
- package/dist/lib/notify.js +61 -0
- package/dist/lib/operator.d.ts +26 -0
- package/dist/lib/operator.js +107 -0
- package/dist/lib/plugins.d.ts +35 -0
- package/dist/lib/plugins.js +217 -0
- package/dist/lib/remote-agents-json.d.ts +14 -0
- package/dist/lib/remote-agents-json.js +94 -0
- package/dist/lib/resources/mcp.js +44 -0
- package/dist/lib/resources/memory.d.ts +15 -0
- package/dist/lib/resources/memory.js +46 -0
- package/dist/lib/resources/types.d.ts +2 -2
- package/dist/lib/rotate.js +2 -0
- package/dist/lib/runner.d.ts +43 -0
- package/dist/lib/runner.js +323 -74
- package/dist/lib/sandbox.js +6 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
- package/dist/lib/secrets/agent.d.ts +21 -0
- package/dist/lib/secrets/agent.js +63 -1
- package/dist/lib/secrets/bundles.d.ts +33 -1
- package/dist/lib/secrets/bundles.js +76 -21
- package/dist/lib/secrets/icloud-import.d.ts +79 -0
- package/dist/lib/secrets/icloud-import.js +203 -0
- package/dist/lib/secrets/index.d.ts +142 -2
- package/dist/lib/secrets/index.js +695 -30
- package/dist/lib/secrets/remote.js +1 -1
- package/dist/lib/secrets/sync.js +1 -1
- package/dist/lib/session/active.d.ts +18 -0
- package/dist/lib/session/active.js +47 -17
- package/dist/lib/session/db.d.ts +9 -1
- package/dist/lib/session/db.js +18 -3
- package/dist/lib/session/discover.d.ts +13 -0
- package/dist/lib/session/discover.js +32 -2
- package/dist/lib/session/parse.d.ts +8 -0
- package/dist/lib/session/parse.js +42 -21
- package/dist/lib/session/remote-active.js +8 -89
- package/dist/lib/session/state.d.ts +11 -0
- package/dist/lib/session/state.js +50 -1
- package/dist/lib/session/tail.d.ts +23 -4
- package/dist/lib/session/tail.js +34 -16
- package/dist/lib/session/throughput.d.ts +30 -0
- package/dist/lib/session/throughput.js +86 -0
- package/dist/lib/shim-heal.d.ts +12 -3
- package/dist/lib/shim-heal.js +12 -6
- package/dist/lib/staleness/detectors/subagents.js +57 -3
- package/dist/lib/staleness/writers/hooks.js +7 -3
- package/dist/lib/staleness/writers/subagents.js +37 -6
- package/dist/lib/startup/command-registry.d.ts +2 -0
- package/dist/lib/startup/command-registry.js +4 -0
- package/dist/lib/state.d.ts +2 -0
- package/dist/lib/state.js +25 -8
- package/dist/lib/subagents.d.ts +52 -0
- package/dist/lib/subagents.js +315 -12
- package/dist/lib/teams/agents.js +6 -3
- package/dist/lib/teams/worktree.d.ts +8 -0
- package/dist/lib/teams/worktree.js +8 -0
- package/dist/lib/types.d.ts +20 -2
- package/dist/lib/versions.js +38 -48
- package/dist/lib/whats-new.d.ts +5 -3
- package/dist/lib/whats-new.js +25 -5
- package/package.json +4 -3
- package/scripts/postinstall.js +61 -1
package/dist/lib/events.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Centralized event logging for agents-cli.
|
|
3
3
|
*
|
|
4
|
-
* Structured JSONL
|
|
5
|
-
*
|
|
4
|
+
* Structured JSONL audit log at ~/.agents/events.jsonl with lossless numbered
|
|
5
|
+
* gzip rotation at 10 MB and rich metadata for debugging/auditing.
|
|
6
6
|
*
|
|
7
7
|
* Features:
|
|
8
8
|
* - Rich metadata: hostname, platform, arch, pid, timezone
|
|
@@ -15,30 +15,29 @@ import * as fs from 'fs';
|
|
|
15
15
|
import * as path from 'path';
|
|
16
16
|
import * as os from 'os';
|
|
17
17
|
import { createHash } from 'node:crypto';
|
|
18
|
+
import { gzipSync, gunzipSync } from 'node:zlib';
|
|
18
19
|
import { parseSshConnection } from './session/provenance.js';
|
|
19
|
-
import {
|
|
20
|
+
import { ensureLockTarget, withFileLock } from './fs-atomic.js';
|
|
21
|
+
import { getUserAgentsDir } from './state.js';
|
|
20
22
|
// ─── Constants ────────────────────────────────────────────────────────────────
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
//
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
// getLogsDir — an eager call would crash those on import. Deferring to first use
|
|
33
|
-
// keeps a bare import side-effect-free while staying a one-time resolution.
|
|
34
|
-
let _logsDir;
|
|
35
|
-
function logsDir() {
|
|
36
|
-
return (_logsDir ??= getLogsDir());
|
|
23
|
+
// Resolved lazily: events.ts is imported transitively by most CLI surfaces, and
|
|
24
|
+
// import itself must stay side-effect free. Tests may override the exact path.
|
|
25
|
+
// AGENTS_EVENTS_PATH redirects the sink — a test seam like AGENTS_SECRETS_AGENT_DIR:
|
|
26
|
+
// unlike _resetForTest it survives a bare reset AND propagates to CLI subprocesses
|
|
27
|
+
// a test spawns, so fixture events can never land in the user's real log (#910).
|
|
28
|
+
let _eventsPath;
|
|
29
|
+
function eventsPath() {
|
|
30
|
+
return (_eventsPath ??= process.env.AGENTS_EVENTS_PATH || path.join(getUserAgentsDir(), 'events.jsonl'));
|
|
31
|
+
}
|
|
32
|
+
function eventsDir() {
|
|
33
|
+
return path.dirname(eventsPath());
|
|
37
34
|
}
|
|
38
35
|
/** Default retention period in days. */
|
|
39
36
|
const DEFAULT_RETENTION_DAYS = 7;
|
|
40
37
|
/** Default max length for truncated strings. */
|
|
41
38
|
const DEFAULT_TRUNCATE_LENGTH = 500;
|
|
39
|
+
/** Gzip rotation threshold in bytes (10 MB). */
|
|
40
|
+
const GZIP_ROTATION_BYTES = 10 * 1024 * 1024;
|
|
42
41
|
/** Environment variable to disable event logging. */
|
|
43
42
|
const DISABLE_ENV_VAR = 'AGENTS_DISABLE_EVENT_LOG';
|
|
44
43
|
/** Check if audit logging is disabled via environment variable. */
|
|
@@ -50,6 +49,26 @@ function isDisabled() {
|
|
|
50
49
|
const DIR_MODE = 0o700;
|
|
51
50
|
/** File permissions (owner read/write only). */
|
|
52
51
|
const FILE_MODE = 0o600;
|
|
52
|
+
const AUDIT_EVENTS = new Set([
|
|
53
|
+
'command.start', 'command.end',
|
|
54
|
+
'secrets.get', 'secrets.set', 'secrets.delete', 'secrets.rename',
|
|
55
|
+
'teams.create', 'teams.add', 'teams.start', 'teams.complete', 'teams.disband',
|
|
56
|
+
'cloud.dispatch', 'cloud.complete', 'cloud.cancel', 'cloud.message',
|
|
57
|
+
'version.install', 'version.switch', 'version.remove',
|
|
58
|
+
'skill.install', 'skill.remove',
|
|
59
|
+
'mcp.add', 'mcp.remove', 'mcp.register',
|
|
60
|
+
'rotation.resolved',
|
|
61
|
+
'session.start', 'session.end',
|
|
62
|
+
]);
|
|
63
|
+
export function levelFor(event) {
|
|
64
|
+
if (event === 'warn')
|
|
65
|
+
return 'warn';
|
|
66
|
+
if (event === 'debug')
|
|
67
|
+
return 'debug';
|
|
68
|
+
if (AUDIT_EVENTS.has(event))
|
|
69
|
+
return 'audit';
|
|
70
|
+
return 'info';
|
|
71
|
+
}
|
|
53
72
|
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
54
73
|
function getTimezoneOffset() {
|
|
55
74
|
const offset = new Date().getTimezoneOffset();
|
|
@@ -66,20 +85,14 @@ function getTimezoneName() {
|
|
|
66
85
|
return 'Unknown';
|
|
67
86
|
}
|
|
68
87
|
}
|
|
69
|
-
function getLogFilePath(date = new Date()) {
|
|
70
|
-
const yyyy = date.getFullYear();
|
|
71
|
-
const mm = String(date.getMonth() + 1).padStart(2, '0');
|
|
72
|
-
const dd = String(date.getDate()).padStart(2, '0');
|
|
73
|
-
return path.join(logsDir(), `events-${yyyy}-${mm}-${dd}.jsonl`);
|
|
74
|
-
}
|
|
75
88
|
function ensureLogsDir() {
|
|
76
|
-
if (!fs.existsSync(
|
|
77
|
-
fs.mkdirSync(
|
|
89
|
+
if (!fs.existsSync(eventsDir())) {
|
|
90
|
+
fs.mkdirSync(eventsDir(), { recursive: true, mode: DIR_MODE });
|
|
78
91
|
}
|
|
79
92
|
else {
|
|
80
93
|
// Ensure permissions are correct on existing dir
|
|
81
94
|
try {
|
|
82
|
-
fs.chmodSync(
|
|
95
|
+
fs.chmodSync(eventsDir(), DIR_MODE);
|
|
83
96
|
}
|
|
84
97
|
catch {
|
|
85
98
|
// May fail if not owner
|
|
@@ -101,6 +114,16 @@ export function redactPrompt(prompt) {
|
|
|
101
114
|
}
|
|
102
115
|
const TOKEN_LIKE = /(sk_(?:live|test)_|pk_(?:live|test)_|ghp_|gho_|ghu_|ghs_|xox[bpars]-|AKIA|ASIA|AIza|Bearer\s+|eyJ[A-Za-z0-9_-]+\.)/i;
|
|
103
116
|
const SECRET_PATH = /\/(secrets|credentials|\.env|user\.yaml)\b/i;
|
|
117
|
+
const SENSITIVE_ARG_NAME = /password|secret|token|key|api[-_]?key|auth/i;
|
|
118
|
+
const SENSITIVE_PAYLOAD_KEY = /password|secret|token|api[-_]?key|auth/i;
|
|
119
|
+
const RESERVED_META_KEYS = new Set([
|
|
120
|
+
'ts', 'tz', 'tzName', 'hostname', 'platform', 'arch', 'pid', 'ppid',
|
|
121
|
+
'event', 'level', 'caller', 'session', 'osUser', 'transport', 'sshClientIp',
|
|
122
|
+
]);
|
|
123
|
+
function promptMarker(value) {
|
|
124
|
+
const { prompt_length, prompt_sha256 } = redactPrompt(value);
|
|
125
|
+
return `[REDACTED prompt length=${prompt_length} sha256=${prompt_sha256}]`;
|
|
126
|
+
}
|
|
104
127
|
/**
|
|
105
128
|
* Mask argv entries that look like tokens or secret paths. Preserves structure
|
|
106
129
|
* for debugging but drops the sensitive substring.
|
|
@@ -108,13 +131,59 @@ const SECRET_PATH = /\/(secrets|credentials|\.env|user\.yaml)\b/i;
|
|
|
108
131
|
export function redactArgs(args) {
|
|
109
132
|
if (!args)
|
|
110
133
|
return undefined;
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
134
|
+
const result = [];
|
|
135
|
+
let redactNext = false;
|
|
136
|
+
let promptNext = false;
|
|
137
|
+
for (const arg of args) {
|
|
138
|
+
if (redactNext) {
|
|
139
|
+
if (arg.startsWith('-')) {
|
|
140
|
+
redactNext = false;
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
result.push('[REDACTED]');
|
|
144
|
+
redactNext = false;
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
if (promptNext) {
|
|
149
|
+
if (arg.startsWith('-')) {
|
|
150
|
+
promptNext = false;
|
|
151
|
+
}
|
|
152
|
+
else {
|
|
153
|
+
result.push(arg.length > 200 ? promptMarker(arg) :
|
|
154
|
+
TOKEN_LIKE.test(arg) || SECRET_PATH.test(arg) ? '[REDACTED]' : arg);
|
|
155
|
+
promptNext = false;
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
const equals = arg.indexOf('=');
|
|
160
|
+
const flag = equals >= 0 ? arg.slice(0, equals) : arg;
|
|
161
|
+
const value = equals >= 0 ? arg.slice(equals + 1) : undefined;
|
|
162
|
+
if (flag.startsWith('-') && SENSITIVE_ARG_NAME.test(flag)) {
|
|
163
|
+
result.push(value === undefined ? flag : `${flag}=[REDACTED]`);
|
|
164
|
+
redactNext = value === undefined;
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
if (flag === '--body' || flag === '--value') {
|
|
168
|
+
result.push(value === undefined ? flag : `${flag}=[REDACTED]`);
|
|
169
|
+
redactNext = value === undefined;
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
if (flag === '--prompt') {
|
|
173
|
+
if (value === undefined) {
|
|
174
|
+
result.push(flag);
|
|
175
|
+
promptNext = true;
|
|
176
|
+
}
|
|
177
|
+
else {
|
|
178
|
+
const safe = value.length > 200 ? promptMarker(value) :
|
|
179
|
+
TOKEN_LIKE.test(value) || SECRET_PATH.test(value) ? '[REDACTED]' : value;
|
|
180
|
+
result.push(`${flag}=${safe}`);
|
|
181
|
+
}
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
result.push(TOKEN_LIKE.test(arg) || SECRET_PATH.test(arg) ? '[REDACTED]' : arg);
|
|
185
|
+
}
|
|
186
|
+
return result;
|
|
118
187
|
}
|
|
119
188
|
// ─── Truncation ───────────────────────────────────────────────────────────────
|
|
120
189
|
/**
|
|
@@ -131,22 +200,65 @@ export function truncate(str, maxLength = DEFAULT_TRUNCATE_LENGTH) {
|
|
|
131
200
|
/**
|
|
132
201
|
* Truncate all string values in a payload object.
|
|
133
202
|
*/
|
|
134
|
-
function
|
|
203
|
+
function sanitizeNested(value, key, maxLength) {
|
|
204
|
+
if (SENSITIVE_PAYLOAD_KEY.test(key))
|
|
205
|
+
return '[REDACTED]';
|
|
206
|
+
if (typeof value === 'string') {
|
|
207
|
+
if (TOKEN_LIKE.test(value) || SECRET_PATH.test(value))
|
|
208
|
+
return '[REDACTED]';
|
|
209
|
+
return truncate(value, maxLength);
|
|
210
|
+
}
|
|
211
|
+
if (Array.isArray(value)) {
|
|
212
|
+
return value.slice(0, 10).map((item) => sanitizeNested(item, '', maxLength));
|
|
213
|
+
}
|
|
214
|
+
if (value && typeof value === 'object') {
|
|
215
|
+
const result = {};
|
|
216
|
+
for (const [nestedKey, nestedValue] of Object.entries(value)) {
|
|
217
|
+
result[nestedKey] = sanitizeNested(nestedValue, nestedKey, maxLength);
|
|
218
|
+
}
|
|
219
|
+
return result;
|
|
220
|
+
}
|
|
221
|
+
return value;
|
|
222
|
+
}
|
|
223
|
+
function sanitizePayload(payload, maxLength = DEFAULT_TRUNCATE_LENGTH) {
|
|
135
224
|
const result = {};
|
|
136
225
|
for (const [key, value] of Object.entries(payload)) {
|
|
137
|
-
if (
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
result[key] = value.slice(0, 10).map(v => typeof v === 'string' ? truncate(v, maxLength) : v);
|
|
226
|
+
if (RESERVED_META_KEYS.has(key))
|
|
227
|
+
continue;
|
|
228
|
+
if (key === 'args' && Array.isArray(value)) {
|
|
229
|
+
result.args = redactArgs(value.filter((item) => typeof item === 'string'));
|
|
230
|
+
continue;
|
|
143
231
|
}
|
|
144
|
-
|
|
145
|
-
result
|
|
232
|
+
if (key.toLowerCase() === 'prompt' && typeof value === 'string') {
|
|
233
|
+
Object.assign(result, redactPrompt(value));
|
|
234
|
+
continue;
|
|
146
235
|
}
|
|
236
|
+
result[key] = sanitizeNested(value, key, maxLength);
|
|
147
237
|
}
|
|
148
238
|
return result;
|
|
149
239
|
}
|
|
240
|
+
const TERMINAL_CALLERS = {
|
|
241
|
+
cc: 'claude', cl: 'claude',
|
|
242
|
+
cx: 'codex',
|
|
243
|
+
gx: 'gemini', gm: 'gemini',
|
|
244
|
+
cr: 'cursor',
|
|
245
|
+
oc: 'opencode',
|
|
246
|
+
sh: 'shell',
|
|
247
|
+
ag: 'antigravity',
|
|
248
|
+
gk: 'grok',
|
|
249
|
+
};
|
|
250
|
+
/** Identify the environment that invoked agents-cli, not the source callsite. */
|
|
251
|
+
export function detectCaller(env = process.env, stdoutIsTTY = Boolean(process.stdout.isTTY)) {
|
|
252
|
+
const session = env.AGENT_SESSION_ID?.slice(0, 8) || undefined;
|
|
253
|
+
if (env.CLAUDECODE === '1')
|
|
254
|
+
return { kind: 'claude-code', ...(session ? { session } : {}) };
|
|
255
|
+
const terminalId = env.AGENT_TERMINAL_ID;
|
|
256
|
+
if (terminalId) {
|
|
257
|
+
const prefix = terminalId.split('-')[0].toLowerCase();
|
|
258
|
+
return { kind: TERMINAL_CALLERS[prefix] ?? 'agent', ...(session ? { session } : {}) };
|
|
259
|
+
}
|
|
260
|
+
return { kind: stdoutIsTTY ? 'terminal' : 'script' };
|
|
261
|
+
}
|
|
150
262
|
/**
|
|
151
263
|
* Who is running this process and from where. Derived once per process from the
|
|
152
264
|
* OS user and $SSH_CONNECTION (via the same parser the sessions layer uses), then
|
|
@@ -173,7 +285,7 @@ function auditOrigin() {
|
|
|
173
285
|
}
|
|
174
286
|
// ─── Core API ─────────────────────────────────────────────────────────────────
|
|
175
287
|
/**
|
|
176
|
-
* Emit a structured event to the
|
|
288
|
+
* Emit a structured event to the append-only audit log.
|
|
177
289
|
*
|
|
178
290
|
* @param event - The event type
|
|
179
291
|
* @param payload - Event-specific data (agent, version, cwd, etc.)
|
|
@@ -183,7 +295,10 @@ export function emit(event, payload = {}) {
|
|
|
183
295
|
return;
|
|
184
296
|
try {
|
|
185
297
|
ensureLogsDir();
|
|
298
|
+
const caller = detectCaller();
|
|
299
|
+
const safePayload = sanitizePayload(payload);
|
|
186
300
|
const record = {
|
|
301
|
+
...safePayload,
|
|
187
302
|
ts: new Date().toISOString(),
|
|
188
303
|
tz: getTimezoneOffset(),
|
|
189
304
|
tzName: getTimezoneName(),
|
|
@@ -193,26 +308,28 @@ export function emit(event, payload = {}) {
|
|
|
193
308
|
pid: process.pid,
|
|
194
309
|
ppid: process.ppid,
|
|
195
310
|
event,
|
|
311
|
+
level: levelFor(event),
|
|
312
|
+
caller: caller.kind,
|
|
313
|
+
...(caller.session ? { session: caller.session } : {}),
|
|
196
314
|
...auditOrigin(),
|
|
197
|
-
...truncatePayload(payload),
|
|
198
315
|
};
|
|
199
316
|
const line = JSON.stringify(record) + '\n';
|
|
200
|
-
const logPath =
|
|
317
|
+
const logPath = eventsPath();
|
|
201
318
|
const isNew = !fs.existsSync(logPath);
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
// May fail if not owner
|
|
319
|
+
ensureLockTarget(logPath, '', DIR_MODE);
|
|
320
|
+
withFileLock(logPath, () => {
|
|
321
|
+
fs.appendFileSync(logPath, line, { mode: FILE_MODE });
|
|
322
|
+
if (isNew || logPath !== _chmoddedPath) {
|
|
323
|
+
_chmoddedPath = logPath;
|
|
324
|
+
try {
|
|
325
|
+
fs.chmodSync(logPath, FILE_MODE);
|
|
326
|
+
}
|
|
327
|
+
catch {
|
|
328
|
+
// May fail if not owner
|
|
329
|
+
}
|
|
214
330
|
}
|
|
215
|
-
|
|
331
|
+
maybeGzipRotateLocked(logPath);
|
|
332
|
+
});
|
|
216
333
|
}
|
|
217
334
|
catch {
|
|
218
335
|
// Silent failure - logging should never break the CLI
|
|
@@ -401,29 +518,54 @@ export function emitError(err, payload = {}) {
|
|
|
401
518
|
errorStack: truncate(error.stack, 1000),
|
|
402
519
|
});
|
|
403
520
|
}
|
|
521
|
+
// ─── Gzip rotation ──────────────────────────────────────────────────────────
|
|
522
|
+
/** Rotate the active file while its append lock is held. */
|
|
523
|
+
function maybeGzipRotateLocked(logPath) {
|
|
524
|
+
const stat = fs.statSync(logPath);
|
|
525
|
+
if (stat.size < GZIP_ROTATION_BYTES)
|
|
526
|
+
return;
|
|
527
|
+
const raw = fs.readFileSync(logPath);
|
|
528
|
+
const tmpArchive = path.join(eventsDir(), `.events.1.jsonl.gz.${process.pid}.tmp`);
|
|
529
|
+
fs.writeFileSync(tmpArchive, gzipSync(raw), { mode: FILE_MODE });
|
|
530
|
+
try {
|
|
531
|
+
const archives = fs.readdirSync(eventsDir())
|
|
532
|
+
.map((file) => ({ file, match: file.match(/^events\.(\d+)\.jsonl\.gz$/) }))
|
|
533
|
+
.filter((entry) => entry.match !== null)
|
|
534
|
+
.map((entry) => ({ file: entry.file, number: Number(entry.match[1]) }))
|
|
535
|
+
.sort((a, b) => b.number - a.number);
|
|
536
|
+
for (const archive of archives) {
|
|
537
|
+
fs.renameSync(path.join(eventsDir(), archive.file), path.join(eventsDir(), `events.${archive.number + 1}.jsonl.gz`));
|
|
538
|
+
}
|
|
539
|
+
fs.renameSync(tmpArchive, path.join(eventsDir(), 'events.1.jsonl.gz'));
|
|
540
|
+
fs.truncateSync(logPath, 0);
|
|
541
|
+
}
|
|
542
|
+
catch (err) {
|
|
543
|
+
try {
|
|
544
|
+
fs.unlinkSync(tmpArchive);
|
|
545
|
+
}
|
|
546
|
+
catch { /* best-effort cleanup */ }
|
|
547
|
+
throw err;
|
|
548
|
+
}
|
|
549
|
+
}
|
|
404
550
|
// ─── Rotation ─────────────────────────────────────────────────────────────────
|
|
405
551
|
/**
|
|
406
552
|
* Remove log files older than the retention period.
|
|
407
|
-
*
|
|
553
|
+
* Removes numbered gzip archives whose filesystem mtime exceeds retention.
|
|
408
554
|
*
|
|
409
555
|
* @param retentionDays - Number of days to keep (default 7, from DEFAULT_RETENTION_DAYS)
|
|
410
556
|
* @returns Number of files removed
|
|
411
557
|
*/
|
|
412
558
|
export function rotate(retentionDays = DEFAULT_RETENTION_DAYS) {
|
|
413
559
|
try {
|
|
414
|
-
if (!fs.existsSync(
|
|
560
|
+
if (!fs.existsSync(eventsDir()))
|
|
415
561
|
return 0;
|
|
416
562
|
const cutoff = Date.now() - retentionDays * 24 * 60 * 60 * 1000;
|
|
417
|
-
const files = fs.readdirSync(
|
|
563
|
+
const files = fs.readdirSync(eventsDir()).filter(f => /^events\.\d+\.jsonl\.gz$/.test(f));
|
|
418
564
|
let removed = 0;
|
|
419
565
|
for (const file of files) {
|
|
420
|
-
const
|
|
421
|
-
if (
|
|
422
|
-
|
|
423
|
-
const [, yyyy, mm, dd] = match;
|
|
424
|
-
const fileDate = new Date(parseInt(yyyy), parseInt(mm) - 1, parseInt(dd));
|
|
425
|
-
if (fileDate.getTime() < cutoff) {
|
|
426
|
-
fs.unlinkSync(path.join(logsDir(), file));
|
|
566
|
+
const filePath = path.join(eventsDir(), file);
|
|
567
|
+
if (fs.statSync(filePath).mtimeMs < cutoff) {
|
|
568
|
+
fs.unlinkSync(filePath);
|
|
427
569
|
removed++;
|
|
428
570
|
}
|
|
429
571
|
}
|
|
@@ -453,42 +595,40 @@ export function maybeRotate() {
|
|
|
453
595
|
* @returns Array of event records
|
|
454
596
|
*/
|
|
455
597
|
export function query(options) {
|
|
456
|
-
const { startDate, endDate = new Date(), eventTypes, agent, command, module, limit } = options;
|
|
598
|
+
const { startDate, endDate = new Date(), eventTypes, level, agent, caller, command, module, limit } = options;
|
|
457
599
|
const results = [];
|
|
458
|
-
if (!fs.existsSync(
|
|
600
|
+
if (!fs.existsSync(eventsDir()))
|
|
459
601
|
return results;
|
|
460
|
-
const files =
|
|
461
|
-
|
|
462
|
-
.
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
const
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
const endDay = endDate
|
|
472
|
-
? new Date(endDate.getFullYear(), endDate.getMonth(), endDate.getDate())
|
|
473
|
-
: undefined;
|
|
602
|
+
const files = [];
|
|
603
|
+
if (fs.existsSync(eventsPath()))
|
|
604
|
+
files.push({ path: eventsPath(), gzip: false });
|
|
605
|
+
const archives = fs.readdirSync(eventsDir())
|
|
606
|
+
.map((file) => ({ file, match: file.match(/^events\.(\d+)\.jsonl\.gz$/) }))
|
|
607
|
+
.filter((entry) => entry.match !== null)
|
|
608
|
+
.map((entry) => ({ file: entry.file, number: Number(entry.match[1]) }))
|
|
609
|
+
.sort((a, b) => a.number - b.number);
|
|
610
|
+
for (const archive of archives) {
|
|
611
|
+
files.push({ path: path.join(eventsDir(), archive.file), gzip: true });
|
|
612
|
+
}
|
|
474
613
|
const startMs = startDate?.getTime();
|
|
475
614
|
const endMs = endDate?.getTime();
|
|
476
615
|
for (const file of files) {
|
|
477
|
-
|
|
478
|
-
if (
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
616
|
+
let content;
|
|
617
|
+
if (file.gzip) {
|
|
618
|
+
try {
|
|
619
|
+
content = gunzipSync(fs.readFileSync(file.path)).toString('utf-8');
|
|
620
|
+
}
|
|
621
|
+
catch {
|
|
622
|
+
continue;
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
else {
|
|
626
|
+
content = fs.readFileSync(file.path, 'utf-8');
|
|
627
|
+
}
|
|
487
628
|
const lines = content.trim().split('\n').filter(Boolean);
|
|
488
629
|
for (const line of lines.reverse()) {
|
|
489
630
|
try {
|
|
490
631
|
const record = JSON.parse(line);
|
|
491
|
-
// Precise per-record window — the file skip above is day-granular only.
|
|
492
632
|
const recMs = Date.parse(record.ts);
|
|
493
633
|
if (startMs !== undefined && !isNaN(recMs) && recMs < startMs)
|
|
494
634
|
continue;
|
|
@@ -496,11 +636,12 @@ export function query(options) {
|
|
|
496
636
|
continue;
|
|
497
637
|
if (eventTypes && !eventTypes.includes(record.event))
|
|
498
638
|
continue;
|
|
639
|
+
if (level && (record.level ?? levelFor(record.event)) !== level)
|
|
640
|
+
continue;
|
|
499
641
|
if (agent && record.agent !== agent)
|
|
500
642
|
continue;
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
// "teams create" stays exact.
|
|
643
|
+
if (caller && record.caller !== caller)
|
|
644
|
+
continue;
|
|
504
645
|
if (command && record.command !== command &&
|
|
505
646
|
!(typeof record.command === 'string' && record.command.startsWith(command + ' ')))
|
|
506
647
|
continue;
|
|
@@ -543,7 +684,56 @@ export function getTimingStats(label, options = {}) {
|
|
|
543
684
|
p95Ms: durations[Math.floor(durations.length * 0.95)],
|
|
544
685
|
};
|
|
545
686
|
}
|
|
687
|
+
export function stats(options = {}) {
|
|
688
|
+
const days = options.days ?? 7;
|
|
689
|
+
const startDate = new Date();
|
|
690
|
+
startDate.setDate(startDate.getDate() - days);
|
|
691
|
+
const records = query({ startDate, limit: 100_000 });
|
|
692
|
+
const byLevel = {};
|
|
693
|
+
const byEvent = {};
|
|
694
|
+
const byModule = {};
|
|
695
|
+
const byUser = {};
|
|
696
|
+
for (const r of records) {
|
|
697
|
+
const lvl = r.level ?? levelFor(r.event);
|
|
698
|
+
byLevel[lvl] = (byLevel[lvl] ?? 0) + 1;
|
|
699
|
+
byEvent[r.event] = (byEvent[r.event] ?? 0) + 1;
|
|
700
|
+
if (r.module)
|
|
701
|
+
byModule[r.module] = (byModule[r.module] ?? 0) + 1;
|
|
702
|
+
const user = `${r.osUser ?? '?'}@${r.hostname}`;
|
|
703
|
+
byUser[user] = (byUser[user] ?? 0) + 1;
|
|
704
|
+
}
|
|
705
|
+
let fileCount = 0;
|
|
706
|
+
let totalBytes = 0;
|
|
707
|
+
try {
|
|
708
|
+
if (fs.existsSync(eventsDir())) {
|
|
709
|
+
const files = fs.readdirSync(eventsDir()).filter(f => f === 'events.jsonl' || /^events\.\d+\.jsonl\.gz$/.test(f));
|
|
710
|
+
fileCount = files.length;
|
|
711
|
+
for (const f of files) {
|
|
712
|
+
try {
|
|
713
|
+
totalBytes += fs.statSync(path.join(eventsDir(), f)).size;
|
|
714
|
+
}
|
|
715
|
+
catch { /* skip */ }
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
catch { /* skip */ }
|
|
720
|
+
return {
|
|
721
|
+
totalEvents: records.length,
|
|
722
|
+
byLevel,
|
|
723
|
+
byEvent,
|
|
724
|
+
byModule,
|
|
725
|
+
byUser,
|
|
726
|
+
fileCount,
|
|
727
|
+
totalBytes,
|
|
728
|
+
};
|
|
729
|
+
}
|
|
546
730
|
// ─── Exports ──────────────────────────────────────────────────────────────────
|
|
547
731
|
export function getLogsPath() {
|
|
548
|
-
return
|
|
732
|
+
return eventsPath();
|
|
733
|
+
}
|
|
734
|
+
export function _resetForTest(overrideEventsPath) {
|
|
735
|
+
_eventsPath = overrideEventsPath;
|
|
736
|
+
_origin = undefined;
|
|
737
|
+
_chmoddedPath = undefined;
|
|
738
|
+
lastRotationCheck = 0;
|
|
549
739
|
}
|
package/dist/lib/exec.js
CHANGED
|
@@ -269,6 +269,7 @@ export function buildExecEnv(options) {
|
|
|
269
269
|
if (options.sessionId && isValidMailboxId(options.sessionId)) {
|
|
270
270
|
result.AGENTS_MAILBOX_DIR = mailboxDir(options.sessionId);
|
|
271
271
|
}
|
|
272
|
+
result.AGENTS_RUNTIME = resolveInteractive(options) ? 'terminal' : 'headless';
|
|
272
273
|
// Export the run's durable name (companion to AGENT_SESSION_ID) so a
|
|
273
274
|
// SessionStart hook / the agent can associate its transcript with the handle
|
|
274
275
|
// the user gave the run. Only set when --name was passed.
|
|
@@ -397,7 +398,10 @@ export const AGENT_COMMANDS = {
|
|
|
397
398
|
modelFlag: '--model',
|
|
398
399
|
},
|
|
399
400
|
kiro: {
|
|
400
|
-
|
|
401
|
+
// Standalone hooks live under ~/.kiro/hooks/*.json and only fire on the
|
|
402
|
+
// v3 engine (opt-in via --v3; see https://kiro.dev/docs/cli/v3/hooks/).
|
|
403
|
+
// Without this flag agents-cli would write v3 hook files that never run.
|
|
404
|
+
base: ['kiro-cli', '--v3'],
|
|
401
405
|
promptFlag: 'positional',
|
|
402
406
|
modeFlags: {
|
|
403
407
|
// kiro-cli has no permission flags — edit is the default behavior.
|
|
@@ -470,6 +474,21 @@ export const AGENT_COMMANDS = {
|
|
|
470
474
|
jsonFlags: ['-o', 'stream-json'],
|
|
471
475
|
modelFlag: '-m',
|
|
472
476
|
},
|
|
477
|
+
hermes: {
|
|
478
|
+
base: ['hermes', 'chat'],
|
|
479
|
+
promptFlag: 'positional',
|
|
480
|
+
modeFlags: {
|
|
481
|
+
edit: [],
|
|
482
|
+
},
|
|
483
|
+
modelFlag: '--model',
|
|
484
|
+
},
|
|
485
|
+
forge: {
|
|
486
|
+
base: ['forge'],
|
|
487
|
+
promptFlag: 'positional',
|
|
488
|
+
modeFlags: {
|
|
489
|
+
edit: [],
|
|
490
|
+
},
|
|
491
|
+
},
|
|
473
492
|
};
|
|
474
493
|
/**
|
|
475
494
|
* Whether `agent` has a native resume form (Tier 1). Derived solely from the
|