@phnx-labs/agents-cli 1.20.50 → 1.20.52
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 +40 -0
- package/dist/commands/browser-picker.js +1 -18
- package/dist/commands/browser.js +215 -7
- package/dist/commands/cloud.js +7 -25
- package/dist/commands/computer.d.ts +1 -0
- package/dist/commands/computer.js +129 -8
- package/dist/commands/events.d.ts +1 -1
- package/dist/commands/events.js +2 -3
- package/dist/commands/exec.js +66 -8
- package/dist/commands/factory.js +9 -4
- package/dist/commands/feed.d.ts +9 -0
- package/dist/commands/feed.js +69 -0
- package/dist/commands/inspect.js +1 -11
- package/dist/commands/logs.d.ts +5 -1
- package/dist/commands/logs.js +248 -3
- package/dist/commands/mcp.js +9 -6
- package/dist/commands/message.js +1 -4
- package/dist/commands/profiles.js +1 -18
- package/dist/commands/repo.js +33 -14
- package/dist/commands/resource-view.d.ts +1 -0
- package/dist/commands/resource-view.js +5 -17
- package/dist/commands/secrets.d.ts +23 -0
- package/dist/commands/secrets.js +174 -70
- package/dist/commands/sessions-picker.js +1 -18
- package/dist/commands/sessions.js +6 -8
- package/dist/commands/teams-picker.js +1 -32
- package/dist/commands/teams.js +5 -27
- package/dist/commands/tmux.js +1 -3
- package/dist/commands/view.js +1 -9
- package/dist/commands/worktree.js +1 -4
- package/dist/index.js +6 -2
- package/dist/lib/agents.d.ts +0 -4
- package/dist/lib/agents.js +20 -33
- package/dist/lib/auto-dispatch-linear.d.ts +18 -0
- package/dist/lib/auto-dispatch-linear.js +107 -0
- package/dist/lib/auto-dispatch-provider.d.ts +10 -0
- package/dist/lib/auto-dispatch-provider.js +25 -0
- package/dist/lib/auto-dispatch.d.ts +87 -0
- package/dist/lib/auto-dispatch.js +142 -0
- package/dist/lib/browser/cdp.js +11 -2
- package/dist/lib/browser/drivers/ssh.d.ts +28 -10
- package/dist/lib/browser/drivers/ssh.js +57 -18
- 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/refs.js +1 -5
- 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/cli-resources.d.ts +0 -2
- package/dist/lib/cli-resources.js +30 -13
- package/dist/lib/cloud/rush.d.ts +15 -24
- package/dist/lib/cloud/rush.js +7 -32
- package/dist/lib/crabbox/cli.js +4 -1
- package/dist/lib/crabbox/lease.d.ts +6 -0
- package/dist/lib/crabbox/lease.js +40 -10
- 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 +73 -17
- package/dist/lib/events.d.ts +31 -5
- package/dist/lib/events.js +288 -101
- package/dist/lib/exec.js +32 -14
- package/dist/lib/feed.d.ts +56 -0
- package/dist/lib/feed.js +251 -0
- package/dist/lib/format.d.ts +38 -0
- package/dist/lib/format.js +108 -0
- package/dist/lib/git.d.ts +21 -0
- package/dist/lib/git.js +92 -0
- package/dist/lib/hooks/cache.d.ts +9 -2
- package/dist/lib/hooks/cache.js +220 -8
- package/dist/lib/hooks.js +24 -10
- package/dist/lib/hosts/passthrough.js +1 -0
- package/dist/lib/platform/exec.d.ts +4 -1
- package/dist/lib/platform/exec.js +8 -2
- package/dist/lib/resources.d.ts +0 -8
- package/dist/lib/resources.js +0 -10
- package/dist/lib/rotate.js +2 -0
- package/dist/lib/runner.js +10 -2
- 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 +38 -8
- package/dist/lib/secrets/icloud-import.d.ts +70 -0
- package/dist/lib/secrets/icloud-import.js +173 -0
- package/dist/lib/secrets/index.d.ts +36 -0
- package/dist/lib/secrets/index.js +99 -9
- package/dist/lib/secrets/remote.js +1 -1
- package/dist/lib/secrets/sync.js +1 -1
- package/dist/lib/session/active.d.ts +11 -1
- package/dist/lib/session/active.js +3 -0
- package/dist/lib/session/db.d.ts +1 -4
- package/dist/lib/session/db.js +20 -25
- package/dist/lib/session/discover.d.ts +2 -2
- package/dist/lib/session/discover.js +60 -48
- package/dist/lib/session/parse.js +35 -34
- package/dist/lib/session/render.d.ts +7 -3
- package/dist/lib/session/render.js +15 -9
- package/dist/lib/session/state.d.ts +55 -0
- package/dist/lib/session/state.js +100 -11
- package/dist/lib/session/types.d.ts +9 -0
- package/dist/lib/shims.d.ts +9 -3
- package/dist/lib/shims.js +44 -8
- package/dist/lib/ssh-tunnel.d.ts +33 -2
- package/dist/lib/ssh-tunnel.js +94 -7
- package/dist/lib/staleness/types.d.ts +0 -1
- package/dist/lib/startup/command-registry.d.ts +1 -0
- package/dist/lib/startup/command-registry.js +2 -0
- package/dist/lib/state.d.ts +2 -0
- package/dist/lib/state.js +25 -8
- package/dist/lib/teams/agents.js +6 -3
- package/dist/lib/types.d.ts +24 -1
- package/dist/lib/versions.d.ts +0 -26
- package/dist/lib/versions.js +2 -145
- package/dist/lib/warn-unpushed.d.ts +40 -0
- package/dist/lib/warn-unpushed.js +128 -0
- package/dist/lib/whats-new.d.ts +5 -3
- package/dist/lib/whats-new.js +25 -5
- package/package.json +3 -1
- package/dist/lib/resources/index.d.ts +0 -53
- package/dist/lib/resources/index.js +0 -76
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,26 @@ 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
|
-
// Resolved lazily + memoized: importing this module must NOT call getLogsDir()
|
|
30
|
-
// at eval time. events.ts is pulled in transitively (skills/versions/exec/
|
|
31
|
-
// runner), and several tests mock './state.js' with partial factories that omit
|
|
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
|
+
let _eventsPath;
|
|
26
|
+
function eventsPath() {
|
|
27
|
+
return (_eventsPath ??= path.join(getUserAgentsDir(), 'events.jsonl'));
|
|
28
|
+
}
|
|
29
|
+
function eventsDir() {
|
|
30
|
+
return path.dirname(eventsPath());
|
|
37
31
|
}
|
|
38
32
|
/** Default retention period in days. */
|
|
39
33
|
const DEFAULT_RETENTION_DAYS = 7;
|
|
40
34
|
/** Default max length for truncated strings. */
|
|
41
35
|
const DEFAULT_TRUNCATE_LENGTH = 500;
|
|
36
|
+
/** Gzip rotation threshold in bytes (10 MB). */
|
|
37
|
+
const GZIP_ROTATION_BYTES = 10 * 1024 * 1024;
|
|
42
38
|
/** Environment variable to disable event logging. */
|
|
43
39
|
const DISABLE_ENV_VAR = 'AGENTS_DISABLE_EVENT_LOG';
|
|
44
40
|
/** Check if audit logging is disabled via environment variable. */
|
|
@@ -50,6 +46,26 @@ function isDisabled() {
|
|
|
50
46
|
const DIR_MODE = 0o700;
|
|
51
47
|
/** File permissions (owner read/write only). */
|
|
52
48
|
const FILE_MODE = 0o600;
|
|
49
|
+
const AUDIT_EVENTS = new Set([
|
|
50
|
+
'command.start', 'command.end',
|
|
51
|
+
'secrets.get', 'secrets.set', 'secrets.delete', 'secrets.rename',
|
|
52
|
+
'teams.create', 'teams.add', 'teams.start', 'teams.complete', 'teams.disband',
|
|
53
|
+
'cloud.dispatch', 'cloud.complete', 'cloud.cancel', 'cloud.message',
|
|
54
|
+
'version.install', 'version.switch', 'version.remove',
|
|
55
|
+
'skill.install', 'skill.remove',
|
|
56
|
+
'mcp.add', 'mcp.remove', 'mcp.register',
|
|
57
|
+
'rotation.resolved',
|
|
58
|
+
'session.start', 'session.end',
|
|
59
|
+
]);
|
|
60
|
+
export function levelFor(event) {
|
|
61
|
+
if (event === 'warn')
|
|
62
|
+
return 'warn';
|
|
63
|
+
if (event === 'debug')
|
|
64
|
+
return 'debug';
|
|
65
|
+
if (AUDIT_EVENTS.has(event))
|
|
66
|
+
return 'audit';
|
|
67
|
+
return 'info';
|
|
68
|
+
}
|
|
53
69
|
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
54
70
|
function getTimezoneOffset() {
|
|
55
71
|
const offset = new Date().getTimezoneOffset();
|
|
@@ -66,20 +82,14 @@ function getTimezoneName() {
|
|
|
66
82
|
return 'Unknown';
|
|
67
83
|
}
|
|
68
84
|
}
|
|
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
85
|
function ensureLogsDir() {
|
|
76
|
-
if (!fs.existsSync(
|
|
77
|
-
fs.mkdirSync(
|
|
86
|
+
if (!fs.existsSync(eventsDir())) {
|
|
87
|
+
fs.mkdirSync(eventsDir(), { recursive: true, mode: DIR_MODE });
|
|
78
88
|
}
|
|
79
89
|
else {
|
|
80
90
|
// Ensure permissions are correct on existing dir
|
|
81
91
|
try {
|
|
82
|
-
fs.chmodSync(
|
|
92
|
+
fs.chmodSync(eventsDir(), DIR_MODE);
|
|
83
93
|
}
|
|
84
94
|
catch {
|
|
85
95
|
// May fail if not owner
|
|
@@ -101,6 +111,16 @@ export function redactPrompt(prompt) {
|
|
|
101
111
|
}
|
|
102
112
|
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
113
|
const SECRET_PATH = /\/(secrets|credentials|\.env|user\.yaml)\b/i;
|
|
114
|
+
const SENSITIVE_ARG_NAME = /password|secret|token|key|api[-_]?key|auth/i;
|
|
115
|
+
const SENSITIVE_PAYLOAD_KEY = /password|secret|token|api[-_]?key|auth/i;
|
|
116
|
+
const RESERVED_META_KEYS = new Set([
|
|
117
|
+
'ts', 'tz', 'tzName', 'hostname', 'platform', 'arch', 'pid', 'ppid',
|
|
118
|
+
'event', 'level', 'caller', 'session', 'osUser', 'transport', 'sshClientIp',
|
|
119
|
+
]);
|
|
120
|
+
function promptMarker(value) {
|
|
121
|
+
const { prompt_length, prompt_sha256 } = redactPrompt(value);
|
|
122
|
+
return `[REDACTED prompt length=${prompt_length} sha256=${prompt_sha256}]`;
|
|
123
|
+
}
|
|
104
124
|
/**
|
|
105
125
|
* Mask argv entries that look like tokens or secret paths. Preserves structure
|
|
106
126
|
* for debugging but drops the sensitive substring.
|
|
@@ -108,13 +128,59 @@ const SECRET_PATH = /\/(secrets|credentials|\.env|user\.yaml)\b/i;
|
|
|
108
128
|
export function redactArgs(args) {
|
|
109
129
|
if (!args)
|
|
110
130
|
return undefined;
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
131
|
+
const result = [];
|
|
132
|
+
let redactNext = false;
|
|
133
|
+
let promptNext = false;
|
|
134
|
+
for (const arg of args) {
|
|
135
|
+
if (redactNext) {
|
|
136
|
+
if (arg.startsWith('-')) {
|
|
137
|
+
redactNext = false;
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
result.push('[REDACTED]');
|
|
141
|
+
redactNext = false;
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
if (promptNext) {
|
|
146
|
+
if (arg.startsWith('-')) {
|
|
147
|
+
promptNext = false;
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
result.push(arg.length > 200 ? promptMarker(arg) :
|
|
151
|
+
TOKEN_LIKE.test(arg) || SECRET_PATH.test(arg) ? '[REDACTED]' : arg);
|
|
152
|
+
promptNext = false;
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
const equals = arg.indexOf('=');
|
|
157
|
+
const flag = equals >= 0 ? arg.slice(0, equals) : arg;
|
|
158
|
+
const value = equals >= 0 ? arg.slice(equals + 1) : undefined;
|
|
159
|
+
if (flag.startsWith('-') && SENSITIVE_ARG_NAME.test(flag)) {
|
|
160
|
+
result.push(value === undefined ? flag : `${flag}=[REDACTED]`);
|
|
161
|
+
redactNext = value === undefined;
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
if (flag === '--body' || flag === '--value') {
|
|
165
|
+
result.push(value === undefined ? flag : `${flag}=[REDACTED]`);
|
|
166
|
+
redactNext = value === undefined;
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
if (flag === '--prompt') {
|
|
170
|
+
if (value === undefined) {
|
|
171
|
+
result.push(flag);
|
|
172
|
+
promptNext = true;
|
|
173
|
+
}
|
|
174
|
+
else {
|
|
175
|
+
const safe = value.length > 200 ? promptMarker(value) :
|
|
176
|
+
TOKEN_LIKE.test(value) || SECRET_PATH.test(value) ? '[REDACTED]' : value;
|
|
177
|
+
result.push(`${flag}=${safe}`);
|
|
178
|
+
}
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
result.push(TOKEN_LIKE.test(arg) || SECRET_PATH.test(arg) ? '[REDACTED]' : arg);
|
|
182
|
+
}
|
|
183
|
+
return result;
|
|
118
184
|
}
|
|
119
185
|
// ─── Truncation ───────────────────────────────────────────────────────────────
|
|
120
186
|
/**
|
|
@@ -131,22 +197,65 @@ export function truncate(str, maxLength = DEFAULT_TRUNCATE_LENGTH) {
|
|
|
131
197
|
/**
|
|
132
198
|
* Truncate all string values in a payload object.
|
|
133
199
|
*/
|
|
134
|
-
function
|
|
200
|
+
function sanitizeNested(value, key, maxLength) {
|
|
201
|
+
if (SENSITIVE_PAYLOAD_KEY.test(key))
|
|
202
|
+
return '[REDACTED]';
|
|
203
|
+
if (typeof value === 'string') {
|
|
204
|
+
if (TOKEN_LIKE.test(value) || SECRET_PATH.test(value))
|
|
205
|
+
return '[REDACTED]';
|
|
206
|
+
return truncate(value, maxLength);
|
|
207
|
+
}
|
|
208
|
+
if (Array.isArray(value)) {
|
|
209
|
+
return value.slice(0, 10).map((item) => sanitizeNested(item, '', maxLength));
|
|
210
|
+
}
|
|
211
|
+
if (value && typeof value === 'object') {
|
|
212
|
+
const result = {};
|
|
213
|
+
for (const [nestedKey, nestedValue] of Object.entries(value)) {
|
|
214
|
+
result[nestedKey] = sanitizeNested(nestedValue, nestedKey, maxLength);
|
|
215
|
+
}
|
|
216
|
+
return result;
|
|
217
|
+
}
|
|
218
|
+
return value;
|
|
219
|
+
}
|
|
220
|
+
function sanitizePayload(payload, maxLength = DEFAULT_TRUNCATE_LENGTH) {
|
|
135
221
|
const result = {};
|
|
136
222
|
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);
|
|
223
|
+
if (RESERVED_META_KEYS.has(key))
|
|
224
|
+
continue;
|
|
225
|
+
if (key === 'args' && Array.isArray(value)) {
|
|
226
|
+
result.args = redactArgs(value.filter((item) => typeof item === 'string'));
|
|
227
|
+
continue;
|
|
143
228
|
}
|
|
144
|
-
|
|
145
|
-
result
|
|
229
|
+
if (key.toLowerCase() === 'prompt' && typeof value === 'string') {
|
|
230
|
+
Object.assign(result, redactPrompt(value));
|
|
231
|
+
continue;
|
|
146
232
|
}
|
|
233
|
+
result[key] = sanitizeNested(value, key, maxLength);
|
|
147
234
|
}
|
|
148
235
|
return result;
|
|
149
236
|
}
|
|
237
|
+
const TERMINAL_CALLERS = {
|
|
238
|
+
cc: 'claude', cl: 'claude',
|
|
239
|
+
cx: 'codex',
|
|
240
|
+
gx: 'gemini', gm: 'gemini',
|
|
241
|
+
cr: 'cursor',
|
|
242
|
+
oc: 'opencode',
|
|
243
|
+
sh: 'shell',
|
|
244
|
+
ag: 'antigravity',
|
|
245
|
+
gk: 'grok',
|
|
246
|
+
};
|
|
247
|
+
/** Identify the environment that invoked agents-cli, not the source callsite. */
|
|
248
|
+
export function detectCaller(env = process.env, stdoutIsTTY = Boolean(process.stdout.isTTY)) {
|
|
249
|
+
const session = env.AGENT_SESSION_ID?.slice(0, 8) || undefined;
|
|
250
|
+
if (env.CLAUDECODE === '1')
|
|
251
|
+
return { kind: 'claude-code', ...(session ? { session } : {}) };
|
|
252
|
+
const terminalId = env.AGENT_TERMINAL_ID;
|
|
253
|
+
if (terminalId) {
|
|
254
|
+
const prefix = terminalId.split('-')[0].toLowerCase();
|
|
255
|
+
return { kind: TERMINAL_CALLERS[prefix] ?? 'agent', ...(session ? { session } : {}) };
|
|
256
|
+
}
|
|
257
|
+
return { kind: stdoutIsTTY ? 'terminal' : 'script' };
|
|
258
|
+
}
|
|
150
259
|
/**
|
|
151
260
|
* Who is running this process and from where. Derived once per process from the
|
|
152
261
|
* OS user and $SSH_CONNECTION (via the same parser the sessions layer uses), then
|
|
@@ -173,7 +282,7 @@ function auditOrigin() {
|
|
|
173
282
|
}
|
|
174
283
|
// ─── Core API ─────────────────────────────────────────────────────────────────
|
|
175
284
|
/**
|
|
176
|
-
* Emit a structured event to the
|
|
285
|
+
* Emit a structured event to the append-only audit log.
|
|
177
286
|
*
|
|
178
287
|
* @param event - The event type
|
|
179
288
|
* @param payload - Event-specific data (agent, version, cwd, etc.)
|
|
@@ -183,7 +292,10 @@ export function emit(event, payload = {}) {
|
|
|
183
292
|
return;
|
|
184
293
|
try {
|
|
185
294
|
ensureLogsDir();
|
|
295
|
+
const caller = detectCaller();
|
|
296
|
+
const safePayload = sanitizePayload(payload);
|
|
186
297
|
const record = {
|
|
298
|
+
...safePayload,
|
|
187
299
|
ts: new Date().toISOString(),
|
|
188
300
|
tz: getTimezoneOffset(),
|
|
189
301
|
tzName: getTimezoneName(),
|
|
@@ -193,26 +305,28 @@ export function emit(event, payload = {}) {
|
|
|
193
305
|
pid: process.pid,
|
|
194
306
|
ppid: process.ppid,
|
|
195
307
|
event,
|
|
308
|
+
level: levelFor(event),
|
|
309
|
+
caller: caller.kind,
|
|
310
|
+
...(caller.session ? { session: caller.session } : {}),
|
|
196
311
|
...auditOrigin(),
|
|
197
|
-
...truncatePayload(payload),
|
|
198
312
|
};
|
|
199
313
|
const line = JSON.stringify(record) + '\n';
|
|
200
|
-
const logPath =
|
|
314
|
+
const logPath = eventsPath();
|
|
201
315
|
const isNew = !fs.existsSync(logPath);
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
// May fail if not owner
|
|
316
|
+
ensureLockTarget(logPath, '', DIR_MODE);
|
|
317
|
+
withFileLock(logPath, () => {
|
|
318
|
+
fs.appendFileSync(logPath, line, { mode: FILE_MODE });
|
|
319
|
+
if (isNew || logPath !== _chmoddedPath) {
|
|
320
|
+
_chmoddedPath = logPath;
|
|
321
|
+
try {
|
|
322
|
+
fs.chmodSync(logPath, FILE_MODE);
|
|
323
|
+
}
|
|
324
|
+
catch {
|
|
325
|
+
// May fail if not owner
|
|
326
|
+
}
|
|
214
327
|
}
|
|
215
|
-
|
|
328
|
+
maybeGzipRotateLocked(logPath);
|
|
329
|
+
});
|
|
216
330
|
}
|
|
217
331
|
catch {
|
|
218
332
|
// Silent failure - logging should never break the CLI
|
|
@@ -401,29 +515,54 @@ export function emitError(err, payload = {}) {
|
|
|
401
515
|
errorStack: truncate(error.stack, 1000),
|
|
402
516
|
});
|
|
403
517
|
}
|
|
518
|
+
// ─── Gzip rotation ──────────────────────────────────────────────────────────
|
|
519
|
+
/** Rotate the active file while its append lock is held. */
|
|
520
|
+
function maybeGzipRotateLocked(logPath) {
|
|
521
|
+
const stat = fs.statSync(logPath);
|
|
522
|
+
if (stat.size < GZIP_ROTATION_BYTES)
|
|
523
|
+
return;
|
|
524
|
+
const raw = fs.readFileSync(logPath);
|
|
525
|
+
const tmpArchive = path.join(eventsDir(), `.events.1.jsonl.gz.${process.pid}.tmp`);
|
|
526
|
+
fs.writeFileSync(tmpArchive, gzipSync(raw), { mode: FILE_MODE });
|
|
527
|
+
try {
|
|
528
|
+
const archives = fs.readdirSync(eventsDir())
|
|
529
|
+
.map((file) => ({ file, match: file.match(/^events\.(\d+)\.jsonl\.gz$/) }))
|
|
530
|
+
.filter((entry) => entry.match !== null)
|
|
531
|
+
.map((entry) => ({ file: entry.file, number: Number(entry.match[1]) }))
|
|
532
|
+
.sort((a, b) => b.number - a.number);
|
|
533
|
+
for (const archive of archives) {
|
|
534
|
+
fs.renameSync(path.join(eventsDir(), archive.file), path.join(eventsDir(), `events.${archive.number + 1}.jsonl.gz`));
|
|
535
|
+
}
|
|
536
|
+
fs.renameSync(tmpArchive, path.join(eventsDir(), 'events.1.jsonl.gz'));
|
|
537
|
+
fs.truncateSync(logPath, 0);
|
|
538
|
+
}
|
|
539
|
+
catch (err) {
|
|
540
|
+
try {
|
|
541
|
+
fs.unlinkSync(tmpArchive);
|
|
542
|
+
}
|
|
543
|
+
catch { /* best-effort cleanup */ }
|
|
544
|
+
throw err;
|
|
545
|
+
}
|
|
546
|
+
}
|
|
404
547
|
// ─── Rotation ─────────────────────────────────────────────────────────────────
|
|
405
548
|
/**
|
|
406
549
|
* Remove log files older than the retention period.
|
|
407
|
-
*
|
|
550
|
+
* Removes numbered gzip archives whose filesystem mtime exceeds retention.
|
|
408
551
|
*
|
|
409
552
|
* @param retentionDays - Number of days to keep (default 7, from DEFAULT_RETENTION_DAYS)
|
|
410
553
|
* @returns Number of files removed
|
|
411
554
|
*/
|
|
412
555
|
export function rotate(retentionDays = DEFAULT_RETENTION_DAYS) {
|
|
413
556
|
try {
|
|
414
|
-
if (!fs.existsSync(
|
|
557
|
+
if (!fs.existsSync(eventsDir()))
|
|
415
558
|
return 0;
|
|
416
559
|
const cutoff = Date.now() - retentionDays * 24 * 60 * 60 * 1000;
|
|
417
|
-
const files = fs.readdirSync(
|
|
560
|
+
const files = fs.readdirSync(eventsDir()).filter(f => /^events\.\d+\.jsonl\.gz$/.test(f));
|
|
418
561
|
let removed = 0;
|
|
419
562
|
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));
|
|
563
|
+
const filePath = path.join(eventsDir(), file);
|
|
564
|
+
if (fs.statSync(filePath).mtimeMs < cutoff) {
|
|
565
|
+
fs.unlinkSync(filePath);
|
|
427
566
|
removed++;
|
|
428
567
|
}
|
|
429
568
|
}
|
|
@@ -453,42 +592,40 @@ export function maybeRotate() {
|
|
|
453
592
|
* @returns Array of event records
|
|
454
593
|
*/
|
|
455
594
|
export function query(options) {
|
|
456
|
-
const { startDate, endDate = new Date(), eventTypes, agent, command, module, limit } = options;
|
|
595
|
+
const { startDate, endDate = new Date(), eventTypes, level, agent, caller, command, module, limit } = options;
|
|
457
596
|
const results = [];
|
|
458
|
-
if (!fs.existsSync(
|
|
597
|
+
if (!fs.existsSync(eventsDir()))
|
|
459
598
|
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;
|
|
599
|
+
const files = [];
|
|
600
|
+
if (fs.existsSync(eventsPath()))
|
|
601
|
+
files.push({ path: eventsPath(), gzip: false });
|
|
602
|
+
const archives = fs.readdirSync(eventsDir())
|
|
603
|
+
.map((file) => ({ file, match: file.match(/^events\.(\d+)\.jsonl\.gz$/) }))
|
|
604
|
+
.filter((entry) => entry.match !== null)
|
|
605
|
+
.map((entry) => ({ file: entry.file, number: Number(entry.match[1]) }))
|
|
606
|
+
.sort((a, b) => a.number - b.number);
|
|
607
|
+
for (const archive of archives) {
|
|
608
|
+
files.push({ path: path.join(eventsDir(), archive.file), gzip: true });
|
|
609
|
+
}
|
|
474
610
|
const startMs = startDate?.getTime();
|
|
475
611
|
const endMs = endDate?.getTime();
|
|
476
612
|
for (const file of files) {
|
|
477
|
-
|
|
478
|
-
if (
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
613
|
+
let content;
|
|
614
|
+
if (file.gzip) {
|
|
615
|
+
try {
|
|
616
|
+
content = gunzipSync(fs.readFileSync(file.path)).toString('utf-8');
|
|
617
|
+
}
|
|
618
|
+
catch {
|
|
619
|
+
continue;
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
else {
|
|
623
|
+
content = fs.readFileSync(file.path, 'utf-8');
|
|
624
|
+
}
|
|
487
625
|
const lines = content.trim().split('\n').filter(Boolean);
|
|
488
626
|
for (const line of lines.reverse()) {
|
|
489
627
|
try {
|
|
490
628
|
const record = JSON.parse(line);
|
|
491
|
-
// Precise per-record window — the file skip above is day-granular only.
|
|
492
629
|
const recMs = Date.parse(record.ts);
|
|
493
630
|
if (startMs !== undefined && !isNaN(recMs) && recMs < startMs)
|
|
494
631
|
continue;
|
|
@@ -496,11 +633,12 @@ export function query(options) {
|
|
|
496
633
|
continue;
|
|
497
634
|
if (eventTypes && !eventTypes.includes(record.event))
|
|
498
635
|
continue;
|
|
636
|
+
if (level && (record.level ?? levelFor(record.event)) !== level)
|
|
637
|
+
continue;
|
|
499
638
|
if (agent && record.agent !== agent)
|
|
500
639
|
continue;
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
// "teams create" stays exact.
|
|
640
|
+
if (caller && record.caller !== caller)
|
|
641
|
+
continue;
|
|
504
642
|
if (command && record.command !== command &&
|
|
505
643
|
!(typeof record.command === 'string' && record.command.startsWith(command + ' ')))
|
|
506
644
|
continue;
|
|
@@ -543,7 +681,56 @@ export function getTimingStats(label, options = {}) {
|
|
|
543
681
|
p95Ms: durations[Math.floor(durations.length * 0.95)],
|
|
544
682
|
};
|
|
545
683
|
}
|
|
684
|
+
export function stats(options = {}) {
|
|
685
|
+
const days = options.days ?? 7;
|
|
686
|
+
const startDate = new Date();
|
|
687
|
+
startDate.setDate(startDate.getDate() - days);
|
|
688
|
+
const records = query({ startDate, limit: 100_000 });
|
|
689
|
+
const byLevel = {};
|
|
690
|
+
const byEvent = {};
|
|
691
|
+
const byModule = {};
|
|
692
|
+
const byUser = {};
|
|
693
|
+
for (const r of records) {
|
|
694
|
+
const lvl = r.level ?? levelFor(r.event);
|
|
695
|
+
byLevel[lvl] = (byLevel[lvl] ?? 0) + 1;
|
|
696
|
+
byEvent[r.event] = (byEvent[r.event] ?? 0) + 1;
|
|
697
|
+
if (r.module)
|
|
698
|
+
byModule[r.module] = (byModule[r.module] ?? 0) + 1;
|
|
699
|
+
const user = `${r.osUser ?? '?'}@${r.hostname}`;
|
|
700
|
+
byUser[user] = (byUser[user] ?? 0) + 1;
|
|
701
|
+
}
|
|
702
|
+
let fileCount = 0;
|
|
703
|
+
let totalBytes = 0;
|
|
704
|
+
try {
|
|
705
|
+
if (fs.existsSync(eventsDir())) {
|
|
706
|
+
const files = fs.readdirSync(eventsDir()).filter(f => f === 'events.jsonl' || /^events\.\d+\.jsonl\.gz$/.test(f));
|
|
707
|
+
fileCount = files.length;
|
|
708
|
+
for (const f of files) {
|
|
709
|
+
try {
|
|
710
|
+
totalBytes += fs.statSync(path.join(eventsDir(), f)).size;
|
|
711
|
+
}
|
|
712
|
+
catch { /* skip */ }
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
catch { /* skip */ }
|
|
717
|
+
return {
|
|
718
|
+
totalEvents: records.length,
|
|
719
|
+
byLevel,
|
|
720
|
+
byEvent,
|
|
721
|
+
byModule,
|
|
722
|
+
byUser,
|
|
723
|
+
fileCount,
|
|
724
|
+
totalBytes,
|
|
725
|
+
};
|
|
726
|
+
}
|
|
546
727
|
// ─── Exports ──────────────────────────────────────────────────────────────────
|
|
547
728
|
export function getLogsPath() {
|
|
548
|
-
return
|
|
729
|
+
return eventsPath();
|
|
730
|
+
}
|
|
731
|
+
export function _resetForTest(overrideEventsPath) {
|
|
732
|
+
_eventsPath = overrideEventsPath;
|
|
733
|
+
_origin = undefined;
|
|
734
|
+
_chmoddedPath = undefined;
|
|
735
|
+
lastRotationCheck = 0;
|
|
549
736
|
}
|
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.
|
|
@@ -307,12 +308,12 @@ export const AGENT_COMMANDS = {
|
|
|
307
308
|
promptFlag: 'positional',
|
|
308
309
|
resume: { subcommand: 'resume' },
|
|
309
310
|
modeFlags: {
|
|
310
|
-
|
|
311
|
-
//
|
|
312
|
-
//
|
|
313
|
-
|
|
314
|
-
edit: ['--sandbox', 'workspace-write', '
|
|
315
|
-
// skip drops the sandbox entirely
|
|
311
|
+
plan: ['--sandbox', 'read-only'],
|
|
312
|
+
// Sandboxed writes inside the workspace. Network stays on (workspace-write
|
|
313
|
+
// disables it by default) so edit-mode runs can still use git/gh/package
|
|
314
|
+
// installs. No approval bypass here — only `skip` drops the guardrails.
|
|
315
|
+
edit: ['--sandbox', 'workspace-write', '-c', 'sandbox_workspace_write.network_access=true'],
|
|
316
|
+
// skip = codex --yolo: drops the sandbox entirely and approves anything.
|
|
316
317
|
skip: ['--dangerously-bypass-approvals-and-sandbox'],
|
|
317
318
|
},
|
|
318
319
|
jsonFlags: ['--json'],
|
|
@@ -563,14 +564,25 @@ export function buildExecCommand(options) {
|
|
|
563
564
|
throw new Error(`Internal error: ${options.agent} declares '${resolvedMode}' in capabilities.modes but has no entry in AGENT_COMMANDS.modeFlags.${resolvedMode}.`);
|
|
564
565
|
}
|
|
565
566
|
if (resumeSpec && 'subcommand' in resumeSpec) {
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
// the bypass or it stalls on approval prompts; plan and interactive resume
|
|
570
|
-
// inherit codex's default sandbox and pass no flag.
|
|
571
|
-
if (!interactive && resolvedMode !== 'plan') {
|
|
567
|
+
if (resolvedMode === 'skip') {
|
|
568
|
+
// skip = yolo on resume too; both `codex resume` (TUI) and
|
|
569
|
+
// `codex exec resume` accept the bypass flag.
|
|
572
570
|
cmd.push('--dangerously-bypass-approvals-and-sandbox');
|
|
573
571
|
}
|
|
572
|
+
else if (interactive) {
|
|
573
|
+
// `codex resume` (TUI) accepts the same -s/--sandbox flags as a fresh run.
|
|
574
|
+
cmd.push(...modeFlags);
|
|
575
|
+
}
|
|
576
|
+
else {
|
|
577
|
+
// `codex exec resume` rejects `--sandbox <mode>` (verified against
|
|
578
|
+
// `codex exec resume --help` on 0.142.5), but takes -c config overrides —
|
|
579
|
+
// map the mode through sandbox_mode so a non-skip resume never gets the
|
|
580
|
+
// approval/sandbox bypass.
|
|
581
|
+
cmd.push('-c', `sandbox_mode=${resolvedMode === 'plan' ? 'read-only' : 'workspace-write'}`);
|
|
582
|
+
if (resolvedMode !== 'plan') {
|
|
583
|
+
cmd.push('-c', 'sandbox_workspace_write.network_access=true');
|
|
584
|
+
}
|
|
585
|
+
}
|
|
574
586
|
}
|
|
575
587
|
else if (options.agent === 'kimi' && !interactive) {
|
|
576
588
|
// kimi's headless prompt mode (`-p`/`--prompt`) is self-contained and REFUSES
|
|
@@ -659,8 +671,14 @@ export function buildExecCommand(options) {
|
|
|
659
671
|
cmd.push(template.promptFlag, options.prompt);
|
|
660
672
|
}
|
|
661
673
|
}
|
|
662
|
-
// Claude
|
|
663
|
-
|
|
674
|
+
// Extra writable dirs. Claude and Codex both take `--add-dir`; for Codex it
|
|
675
|
+
// widens the workspace-write sandbox (teams relies on this to let teammates
|
|
676
|
+
// write ~/.agents), so it must actually be forwarded — it used to be
|
|
677
|
+
// claude-only, silently dropped for codex and masked by edit mode carrying
|
|
678
|
+
// the approval/sandbox bypass. Codex's resume forms reject --add-dir, so
|
|
679
|
+
// skip it there (claude's flag-based resume accepts it).
|
|
680
|
+
if (options.addDirs &&
|
|
681
|
+
(options.agent === 'claude' || (options.agent === 'codex' && !options.resume))) {
|
|
664
682
|
for (const dir of options.addDirs) {
|
|
665
683
|
cmd.push('--add-dir', dir);
|
|
666
684
|
}
|