@agentuity/cli 0.1.42 → 0.1.43

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.
Files changed (35) hide show
  1. package/dist/cmd/build/entry-generator.d.ts.map +1 -1
  2. package/dist/cmd/build/entry-generator.js +137 -1
  3. package/dist/cmd/build/entry-generator.js.map +1 -1
  4. package/dist/cmd/build/vite/metadata-generator.d.ts.map +1 -1
  5. package/dist/cmd/build/vite/metadata-generator.js +19 -9
  6. package/dist/cmd/build/vite/metadata-generator.js.map +1 -1
  7. package/dist/cmd/build/vite/public-asset-path-plugin.d.ts +25 -13
  8. package/dist/cmd/build/vite/public-asset-path-plugin.d.ts.map +1 -1
  9. package/dist/cmd/build/vite/public-asset-path-plugin.js +66 -40
  10. package/dist/cmd/build/vite/public-asset-path-plugin.js.map +1 -1
  11. package/dist/cmd/build/vite/vite-asset-server-config.d.ts.map +1 -1
  12. package/dist/cmd/build/vite/vite-asset-server-config.js +6 -5
  13. package/dist/cmd/build/vite/vite-asset-server-config.js.map +1 -1
  14. package/dist/cmd/build/vite/vite-asset-server.d.ts.map +1 -1
  15. package/dist/cmd/build/vite/vite-asset-server.js +1 -1
  16. package/dist/cmd/build/vite/vite-asset-server.js.map +1 -1
  17. package/dist/cmd/build/vite/vite-builder.d.ts.map +1 -1
  18. package/dist/cmd/build/vite/vite-builder.js +12 -11
  19. package/dist/cmd/build/vite/vite-builder.js.map +1 -1
  20. package/dist/cmd/support/report.d.ts.map +1 -1
  21. package/dist/cmd/support/report.js +58 -23
  22. package/dist/cmd/support/report.js.map +1 -1
  23. package/dist/internal-logger.d.ts +7 -0
  24. package/dist/internal-logger.d.ts.map +1 -1
  25. package/dist/internal-logger.js +82 -28
  26. package/dist/internal-logger.js.map +1 -1
  27. package/package.json +8 -7
  28. package/src/cmd/build/entry-generator.ts +137 -1
  29. package/src/cmd/build/vite/metadata-generator.ts +20 -9
  30. package/src/cmd/build/vite/public-asset-path-plugin.ts +76 -46
  31. package/src/cmd/build/vite/vite-asset-server-config.ts +6 -5
  32. package/src/cmd/build/vite/vite-asset-server.ts +3 -1
  33. package/src/cmd/build/vite/vite-builder.ts +21 -20
  34. package/src/cmd/support/report.ts +82 -28
  35. package/src/internal-logger.ts +91 -27
@@ -1,13 +1,34 @@
1
1
  import { createSubcommand } from '../../types';
2
2
  import { z } from 'zod';
3
- import { existsSync, readFileSync } from 'node:fs';
4
- import { join } from 'node:path';
3
+ import { readFileSync } from 'node:fs';
4
+ import { join, basename } from 'node:path';
5
5
  import { tmpdir } from 'node:os';
6
- import { getLatestLogSession } from '../../internal-logger';
6
+ import { getLogSessionsInCurrentWindow } from '../../internal-logger';
7
7
  import * as tui from '../../tui';
8
8
  import { randomBytes } from 'node:crypto';
9
9
  import AdmZip from 'adm-zip';
10
10
  import { APIResponseSchema } from '@agentuity/server';
11
+ import { StructuredError } from '@agentuity/core';
12
+
13
+ // Structured errors for this module
14
+ const NoSessionDirectoriesError = StructuredError(
15
+ 'NoSessionDirectoriesError',
16
+ 'No session directories provided'
17
+ );
18
+
19
+ const ReportUploadError = StructuredError('ReportUploadError')<{
20
+ statusText: string;
21
+ status?: number;
22
+ }>();
23
+
24
+ const UploadUrlCreationError = StructuredError('UploadUrlCreationError');
25
+
26
+ const BrowserOpenError = StructuredError(
27
+ 'BrowserOpenError',
28
+ 'Failed to open browser. Please open the URL manually.'
29
+ )<{
30
+ exitCode?: number | null;
31
+ }>();
11
32
 
12
33
  const argsSchema = z.object({});
13
34
 
@@ -33,22 +54,34 @@ const ReportUploadDataSchema = z.object({
33
54
  const ReportUploadResponseSchema = APIResponseSchema(ReportUploadDataSchema);
34
55
 
35
56
  /**
36
- * Create a zip file containing session and logs
57
+ * Create a zip file containing session and logs from multiple session directories
37
58
  */
38
- async function createReportZip(sessionDir: string): Promise<string> {
39
- const sessionFile = join(sessionDir, 'session.json');
40
- const logsFile = join(sessionDir, 'logs.jsonl');
41
-
42
- if (!existsSync(sessionFile) || !existsSync(logsFile)) {
43
- throw new Error('Session files not found');
59
+ async function createReportZip(sessionDirs: string[]): Promise<string> {
60
+ if (sessionDirs.length === 0) {
61
+ throw NoSessionDirectoriesError();
44
62
  }
45
63
 
46
64
  // Create zip in temp directory
47
65
  const tempZip = join(tmpdir(), `agentuity-report-${randomBytes(8).toString('hex')}.zip`);
48
66
 
49
67
  const zip = new AdmZip();
50
- zip.addLocalFile(sessionFile);
51
- zip.addLocalFile(logsFile);
68
+
69
+ for (const sessionDir of sessionDirs) {
70
+ const sessionFile = join(sessionDir, 'session.json');
71
+ const logsFile = join(sessionDir, 'logs.jsonl');
72
+
73
+ // Extract session ID from directory name cross-platform
74
+ const sessionId = basename(sessionDir) || 'unknown';
75
+
76
+ // Add files with session ID prefix to avoid conflicts
77
+ if (await Bun.file(sessionFile).exists()) {
78
+ zip.addLocalFile(sessionFile, sessionId);
79
+ }
80
+ if (await Bun.file(logsFile).exists()) {
81
+ zip.addLocalFile(logsFile, sessionId);
82
+ }
83
+ }
84
+
52
85
  zip.writeZip(tempZip);
53
86
 
54
87
  return tempZip;
@@ -76,7 +109,11 @@ async function uploadReport(
76
109
  if (!response.ok) {
77
110
  const errorText = await response.text();
78
111
  logger.error('Upload failed', { status: response.status, error: errorText });
79
- throw new Error(`Upload failed: ${response.statusText}`);
112
+ throw new ReportUploadError({
113
+ message: `Upload failed: ${response.statusText}`,
114
+ statusText: response.statusText,
115
+ status: response.status,
116
+ });
80
117
  }
81
118
  }
82
119
 
@@ -161,11 +198,11 @@ async function openBrowser(url: string, logger: import('../../types').Logger): P
161
198
  await proc.exited;
162
199
 
163
200
  if (proc.exitCode !== 0) {
164
- throw new Error(`Browser process exited with code ${proc.exitCode}`);
201
+ throw new BrowserOpenError({ exitCode: proc.exitCode });
165
202
  }
166
203
  } catch (error) {
167
204
  logger.error('Failed to open browser', { error });
168
- throw new Error('Failed to open browser. Please open the URL manually.');
205
+ throw new BrowserOpenError({ exitCode: null, cause: error });
169
206
  }
170
207
  }
171
208
 
@@ -184,9 +221,9 @@ export default createSubcommand({
184
221
  const { opts, logger, apiClient } = ctx;
185
222
  const isJsonMode = ctx.options.json;
186
223
 
187
- // Get the latest log session
188
- const sessionDir = getLatestLogSession();
189
- if (!sessionDir) {
224
+ // Get all log sessions in the current time window (current + previous bucket)
225
+ const sessionDirs = getLogSessionsInCurrentWindow();
226
+ if (sessionDirs.length === 0) {
190
227
  if (isJsonMode) {
191
228
  console.log(JSON.stringify({ success: false, error: 'No CLI logs found' }));
192
229
  } else {
@@ -196,10 +233,27 @@ export default createSubcommand({
196
233
  return;
197
234
  }
198
235
 
199
- // Read session data to get CLI version
200
- const sessionFile = join(sessionDir, 'session.json');
201
- const sessionData = JSON.parse(readFileSync(sessionFile, 'utf-8'));
202
- const cliVersion = sessionData.cli?.version || 'unknown';
236
+ // Use the first (most recent) session for metadata
237
+ const primarySessionDir = sessionDirs[0]!;
238
+ const sessionFile = join(primarySessionDir, 'session.json');
239
+
240
+ // Safely read session data with fallback for corrupt/missing session.json
241
+ let sessionData: SessionData = {};
242
+ let cliVersion = 'unknown';
243
+ try {
244
+ if (await Bun.file(sessionFile).exists()) {
245
+ sessionData = JSON.parse(readFileSync(sessionFile, 'utf-8'));
246
+ cliVersion = sessionData.cli?.version || 'unknown';
247
+ }
248
+ } catch {
249
+ // Fall back to defaults if session.json is corrupt or unreadable
250
+ logger.trace('Failed to read session.json, using defaults');
251
+ }
252
+
253
+ // Log how many sessions we're including
254
+ if (!isJsonMode && sessionDirs.length > 1) {
255
+ tui.info(`Found ${sessionDirs.length} session(s) in the current time window`);
256
+ }
203
257
 
204
258
  // Get issue description from:
205
259
  // 1. --description flag
@@ -281,11 +335,11 @@ export default createSubcommand({
281
335
  // Debug: log the response
282
336
  logger.debug('Upload response received', { uploadResponse });
283
337
 
284
- if (!uploadResponse.success) {
285
- const errorMsg = uploadResponse.message || 'Failed to create upload URL';
286
- logger.error('Upload URL creation failed', { uploadResponse, errorMsg });
287
- throw new Error(errorMsg);
288
- }
338
+ if (!uploadResponse.success) {
339
+ const errorMsg = uploadResponse.message || 'Failed to create upload URL';
340
+ logger.error('Upload URL creation failed', { uploadResponse, errorMsg });
341
+ throw new UploadUrlCreationError({ message: errorMsg });
342
+ }
289
343
 
290
344
  const { presigned_url, url: reportUrl, report_id: reportId } = uploadResponse.data;
291
345
 
@@ -294,7 +348,7 @@ export default createSubcommand({
294
348
  tui.info('Creating report archive...');
295
349
  }
296
350
 
297
- const zipPath = await createReportZip(sessionDir);
351
+ const zipPath = await createReportZip(sessionDirs);
298
352
 
299
353
  // Step 3: Upload to S3
300
354
  if (!isJsonMode) {
@@ -45,6 +45,9 @@ const SENSITIVE_ENV_PATTERNS = [
45
45
 
46
46
  interface SessionMetadata {
47
47
  sessionId: string;
48
+ bucket: number;
49
+ pid: number;
50
+ ppid: number;
48
51
  command: string;
49
52
  args: string[];
50
53
  timestamp: string;
@@ -107,9 +110,30 @@ function getLogsDir(): string {
107
110
  }
108
111
 
109
112
  /**
110
- * Clean up old log directories, keeping only the most recent one
113
+ * Calculate the current 5-minute bucket number
114
+ * Each bucket represents a 5-minute window (300000ms)
111
115
  */
112
- function cleanupOldLogs(currentSessionId: string): void {
116
+ function getCurrentBucket(): number {
117
+ return Math.floor(Date.now() / 300000);
118
+ }
119
+
120
+ /**
121
+ * Parse bucket number from directory name
122
+ * Directory format: {bucket}-{uuid}
123
+ * Returns null for legacy directories (uuid-only format)
124
+ */
125
+ function parseBucketFromDirName(dirName: string): number | null {
126
+ const match = dirName.match(/^(\d+)-/);
127
+ if (match && match[1]) {
128
+ return parseInt(match[1], 10);
129
+ }
130
+ return null;
131
+ }
132
+
133
+ /**
134
+ * Clean up old log directories, keeping only directories in the current bucket
135
+ */
136
+ function cleanupOldLogs(currentBucket: number): void {
113
137
  // Skip cleanup when inheriting a parent's session ID to avoid
114
138
  // deleting the parent's session directory (race condition).
115
139
  // This applies to forked deploy processes and any subprocess
@@ -125,19 +149,24 @@ function cleanupOldLogs(currentSessionId: string): void {
125
149
 
126
150
  try {
127
151
  const entries = readdirSync(logsDir, { withFileTypes: true });
128
- const dirs = entries
129
- .filter((e) => e.isDirectory())
130
- .map((e) => e.name)
131
- .filter((name) => name !== currentSessionId);
152
+ const dirs = entries.filter((e) => e.isDirectory()).map((e) => e.name);
132
153
 
133
- // Remove all directories except the current one
154
+ // Remove directories with bucket < currentBucket (old buckets)
155
+ // Also remove legacy directories (no bucket prefix) for backward compatibility
134
156
  for (const dir of dirs) {
135
- const dirPath = join(logsDir, dir);
136
- try {
137
- rmSync(dirPath, { recursive: true, force: true });
138
- } catch (err) {
139
- // Ignore errors during cleanup
140
- console.debug(`Failed to remove old log directory ${dir}: ${err}`);
157
+ const bucket = parseBucketFromDirName(dir);
158
+
159
+ // Delete if:
160
+ // 1. Legacy directory (no bucket prefix) - clean up old format
161
+ // 2. Bucket is older than current bucket
162
+ if (bucket === null || bucket < currentBucket) {
163
+ const dirPath = join(logsDir, dir);
164
+ try {
165
+ rmSync(dirPath, { recursive: true, force: true });
166
+ } catch (err) {
167
+ // Ignore errors during cleanup
168
+ console.debug(`Failed to remove old log directory ${dir}: ${err}`);
169
+ }
141
170
  }
142
171
  }
143
172
  } catch (err) {
@@ -154,6 +183,7 @@ export class InternalLogger implements Logger {
154
183
  private sessionDir: string;
155
184
  private sessionFile: string;
156
185
  private logsFile: string;
186
+ private bucket: number;
157
187
  private initialized = false;
158
188
  private disabled = false;
159
189
 
@@ -161,16 +191,23 @@ export class InternalLogger implements Logger {
161
191
  private cliVersion: string,
162
192
  private cliName: string
163
193
  ) {
194
+ // Calculate current 5-minute bucket
195
+ this.bucket = getCurrentBucket();
196
+
164
197
  // When a parent session ID is set in the environment, use it to ensure
165
198
  // all CLI invocations (parent and any subprocesses) write to the same log file.
166
199
  // This prevents race conditions where child processes delete parent's logs.
167
200
  const parentSessionId = process.env.AGENTUITY_INTERNAL_SESSION_ID;
168
201
  if (parentSessionId) {
169
202
  this.sessionId = parentSessionId;
203
+ // Parent session ID already includes bucket prefix, use as-is for directory name
204
+ this.sessionDir = join(getLogsDir(), parentSessionId);
170
205
  } else {
171
- this.sessionId = randomUUID();
206
+ // Generate new session ID with bucket prefix: {bucket}-{uuid}
207
+ const uuid = randomUUID();
208
+ this.sessionId = `${this.bucket}-${uuid}`;
209
+ this.sessionDir = join(getLogsDir(), this.sessionId);
172
210
  }
173
- this.sessionDir = join(getLogsDir(), this.sessionId);
174
211
  this.sessionFile = join(this.sessionDir, 'session.json');
175
212
  this.logsFile = join(this.sessionDir, 'logs.jsonl');
176
213
  }
@@ -189,9 +226,9 @@ export class InternalLogger implements Logger {
189
226
  // Create logs directory (may already exist if we're a child process)
190
227
  mkdirSync(this.sessionDir, { recursive: true, mode: 0o700 });
191
228
 
192
- // Clean up old logs (keep only this session)
229
+ // Clean up old logs (directories with bucket < current bucket)
193
230
  // This is skipped for child processes to avoid deleting parent's session
194
- cleanupOldLogs(this.sessionId);
231
+ cleanupOldLogs(this.bucket);
195
232
 
196
233
  // When inheriting a parent's session ID, skip session.json creation
197
234
  // (parent already created it) but enable logging
@@ -232,6 +269,9 @@ export class InternalLogger implements Logger {
232
269
  // Gather session metadata
233
270
  const sessionMetadata: SessionMetadata = {
234
271
  sessionId: this.sessionId,
272
+ bucket: this.bucket,
273
+ pid: process.pid,
274
+ ppid: process.ppid,
235
275
  command,
236
276
  args,
237
277
  timestamp: new Date().toISOString(),
@@ -422,30 +462,54 @@ export function createInternalLogger(cliVersion: string, cliName: string): Inter
422
462
  }
423
463
 
424
464
  /**
425
- * Get the latest log session directory (if any)
465
+ * Get all log session directories in the current time window
466
+ * Returns directories from current bucket AND previous bucket (to handle boundary cases)
426
467
  */
427
- export function getLatestLogSession(): string | null {
468
+ export function getLogSessionsInCurrentWindow(): string[] {
428
469
  const logsDir = getLogsDir();
429
470
  if (!existsSync(logsDir)) {
430
- return null;
471
+ return [];
431
472
  }
432
473
 
433
474
  try {
475
+ const currentBucket = getCurrentBucket();
476
+ const previousBucket = currentBucket - 1;
477
+
434
478
  const entries = readdirSync(logsDir, { withFileTypes: true });
435
479
  const dirs = entries.filter((e) => e.isDirectory()).map((e) => e.name);
436
480
 
437
- const firstDir = dirs[0];
438
- if (!firstDir) {
439
- return null;
440
- }
481
+ // Filter to directories in current or previous bucket
482
+ const validDirs = dirs.filter((dir) => {
483
+ const bucket = parseBucketFromDirName(dir);
484
+ // Include current bucket, previous bucket, and legacy directories (for backward compat)
485
+ return bucket === currentBucket || bucket === previousBucket || bucket === null;
486
+ });
487
+
488
+ // Sort by bucket (descending) then by name to get most recent first
489
+ validDirs.sort((a, b) => {
490
+ const bucketA = parseBucketFromDirName(a) ?? 0;
491
+ const bucketB = parseBucketFromDirName(b) ?? 0;
492
+ if (bucketA !== bucketB) {
493
+ return bucketB - bucketA; // Higher bucket first
494
+ }
495
+ return b.localeCompare(a); // Then by name descending
496
+ });
441
497
 
442
- // Return the first directory (should be the only one due to cleanup)
443
- return join(logsDir, firstDir);
498
+ return validDirs.map((dir) => join(logsDir, dir));
444
499
  } catch {
445
- return null;
500
+ return [];
446
501
  }
447
502
  }
448
503
 
504
+ /**
505
+ * Get the latest log session directory (if any)
506
+ * For backward compatibility, returns the first session in the current window
507
+ */
508
+ export function getLatestLogSession(): string | null {
509
+ const sessions = getLogSessionsInCurrentWindow();
510
+ return sessions[0] ?? null;
511
+ }
512
+
449
513
  /**
450
514
  * Get the logs directory path (exported for external use)
451
515
  */