@agentuity/cli 0.1.42 → 0.1.44

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 (93) hide show
  1. package/dist/auth.d.ts +2 -2
  2. package/dist/auth.d.ts.map +1 -1
  3. package/dist/auth.js +7 -5
  4. package/dist/auth.js.map +1 -1
  5. package/dist/cli.d.ts.map +1 -1
  6. package/dist/cli.js +24 -12
  7. package/dist/cli.js.map +1 -1
  8. package/dist/cmd/build/entry-generator.d.ts.map +1 -1
  9. package/dist/cmd/build/entry-generator.js +163 -18
  10. package/dist/cmd/build/entry-generator.js.map +1 -1
  11. package/dist/cmd/build/vite/metadata-generator.d.ts.map +1 -1
  12. package/dist/cmd/build/vite/metadata-generator.js +19 -9
  13. package/dist/cmd/build/vite/metadata-generator.js.map +1 -1
  14. package/dist/cmd/build/vite/public-asset-path-plugin.d.ts +24 -15
  15. package/dist/cmd/build/vite/public-asset-path-plugin.d.ts.map +1 -1
  16. package/dist/cmd/build/vite/public-asset-path-plugin.js +92 -47
  17. package/dist/cmd/build/vite/public-asset-path-plugin.js.map +1 -1
  18. package/dist/cmd/build/vite/vite-asset-server-config.d.ts.map +1 -1
  19. package/dist/cmd/build/vite/vite-asset-server-config.js +9 -6
  20. package/dist/cmd/build/vite/vite-asset-server-config.js.map +1 -1
  21. package/dist/cmd/build/vite/vite-asset-server.d.ts.map +1 -1
  22. package/dist/cmd/build/vite/vite-asset-server.js +1 -1
  23. package/dist/cmd/build/vite/vite-asset-server.js.map +1 -1
  24. package/dist/cmd/build/vite/vite-builder.d.ts.map +1 -1
  25. package/dist/cmd/build/vite/vite-builder.js +12 -11
  26. package/dist/cmd/build/vite/vite-builder.js.map +1 -1
  27. package/dist/cmd/cloud/env/org-util.d.ts +2 -1
  28. package/dist/cmd/cloud/env/org-util.d.ts.map +1 -1
  29. package/dist/cmd/cloud/env/org-util.js +4 -2
  30. package/dist/cmd/cloud/env/org-util.js.map +1 -1
  31. package/dist/cmd/cloud/stream/create.d.ts +3 -0
  32. package/dist/cmd/cloud/stream/create.d.ts.map +1 -0
  33. package/dist/cmd/cloud/stream/create.js +227 -0
  34. package/dist/cmd/cloud/stream/create.js.map +1 -0
  35. package/dist/cmd/cloud/stream/delete.d.ts.map +1 -1
  36. package/dist/cmd/cloud/stream/delete.js +2 -1
  37. package/dist/cmd/cloud/stream/delete.js.map +1 -1
  38. package/dist/cmd/cloud/stream/get.d.ts.map +1 -1
  39. package/dist/cmd/cloud/stream/get.js +2 -1
  40. package/dist/cmd/cloud/stream/get.js.map +1 -1
  41. package/dist/cmd/cloud/stream/index.d.ts.map +1 -1
  42. package/dist/cmd/cloud/stream/index.js +10 -1
  43. package/dist/cmd/cloud/stream/index.js.map +1 -1
  44. package/dist/cmd/cloud/stream/list.d.ts.map +1 -1
  45. package/dist/cmd/cloud/stream/list.js +2 -1
  46. package/dist/cmd/cloud/stream/list.js.map +1 -1
  47. package/dist/cmd/cloud/stream/util.d.ts +6 -5
  48. package/dist/cmd/cloud/stream/util.d.ts.map +1 -1
  49. package/dist/cmd/cloud/stream/util.js +26 -5
  50. package/dist/cmd/cloud/stream/util.js.map +1 -1
  51. package/dist/cmd/support/report.d.ts.map +1 -1
  52. package/dist/cmd/support/report.js +58 -23
  53. package/dist/cmd/support/report.js.map +1 -1
  54. package/dist/cmd/upgrade/index.d.ts.map +1 -1
  55. package/dist/cmd/upgrade/index.js +23 -0
  56. package/dist/cmd/upgrade/index.js.map +1 -1
  57. package/dist/cmd/upgrade/npm-availability.d.ts +44 -0
  58. package/dist/cmd/upgrade/npm-availability.d.ts.map +1 -0
  59. package/dist/cmd/upgrade/npm-availability.js +73 -0
  60. package/dist/cmd/upgrade/npm-availability.js.map +1 -0
  61. package/dist/internal-logger.d.ts +7 -0
  62. package/dist/internal-logger.d.ts.map +1 -1
  63. package/dist/internal-logger.js +82 -28
  64. package/dist/internal-logger.js.map +1 -1
  65. package/dist/tui.d.ts +9 -1
  66. package/dist/tui.d.ts.map +1 -1
  67. package/dist/tui.js +39 -14
  68. package/dist/tui.js.map +1 -1
  69. package/dist/version-check.d.ts.map +1 -1
  70. package/dist/version-check.js +13 -2
  71. package/dist/version-check.js.map +1 -1
  72. package/package.json +8 -7
  73. package/src/auth.ts +9 -5
  74. package/src/cli.ts +44 -12
  75. package/src/cmd/build/entry-generator.ts +163 -18
  76. package/src/cmd/build/vite/metadata-generator.ts +20 -9
  77. package/src/cmd/build/vite/public-asset-path-plugin.ts +105 -53
  78. package/src/cmd/build/vite/vite-asset-server-config.ts +9 -6
  79. package/src/cmd/build/vite/vite-asset-server.ts +3 -1
  80. package/src/cmd/build/vite/vite-builder.ts +21 -20
  81. package/src/cmd/cloud/env/org-util.ts +5 -2
  82. package/src/cmd/cloud/stream/create.ts +248 -0
  83. package/src/cmd/cloud/stream/delete.ts +2 -1
  84. package/src/cmd/cloud/stream/get.ts +2 -1
  85. package/src/cmd/cloud/stream/index.ts +10 -1
  86. package/src/cmd/cloud/stream/list.ts +2 -1
  87. package/src/cmd/cloud/stream/util.ts +39 -12
  88. package/src/cmd/support/report.ts +82 -28
  89. package/src/cmd/upgrade/index.ts +25 -0
  90. package/src/cmd/upgrade/npm-availability.ts +105 -0
  91. package/src/internal-logger.ts +91 -27
  92. package/src/tui.ts +42 -14
  93. package/src/version-check.ts +19 -3
@@ -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) {
@@ -185,6 +185,31 @@ export const command = createCommand({
185
185
  };
186
186
  }
187
187
 
188
+ // Verify the version is available on npm before proceeding
189
+ const isAvailable = await tui.spinner({
190
+ message: 'Verifying npm availability...',
191
+ clearOnSuccess: true,
192
+ callback: async () => {
193
+ const { waitForNpmAvailability } = await import('./npm-availability');
194
+ return await waitForNpmAvailability(latestVersion, {
195
+ maxAttempts: 6,
196
+ initialDelayMs: 2000,
197
+ });
198
+ },
199
+ });
200
+
201
+ if (!isAvailable) {
202
+ tui.warning('The new version is not yet available on npm.');
203
+ tui.info('This can happen right after a release. Please try again in a few minutes.');
204
+ tui.info(`You can also run: ${tui.muted('bun add -g @agentuity/cli@latest')}`);
205
+ return {
206
+ upgraded: false,
207
+ from: currentVersion,
208
+ to: latestVersion,
209
+ message: 'Version not yet available on npm',
210
+ };
211
+ }
212
+
188
213
  // Show version info
189
214
  if (!force) {
190
215
  tui.info(`Current version: ${tui.muted(normalizedCurrent)}`);
@@ -0,0 +1,105 @@
1
+ /**
2
+ * npm registry availability checking utilities.
3
+ * Used to verify a version is available on npm before attempting upgrade.
4
+ */
5
+
6
+ const NPM_REGISTRY_URL = 'https://registry.npmjs.org';
7
+ const PACKAGE_NAME = '@agentuity/cli';
8
+
9
+ /** Default timeout for quick checks (implicit version check) */
10
+ const QUICK_CHECK_TIMEOUT_MS = 1000;
11
+
12
+ /** Default timeout for explicit upgrade command */
13
+ const EXPLICIT_CHECK_TIMEOUT_MS = 5000;
14
+
15
+ export interface CheckNpmOptions {
16
+ /** Timeout in milliseconds (default: 5000 for explicit, 1000 for quick) */
17
+ timeoutMs?: number;
18
+ }
19
+
20
+ /**
21
+ * Check if a specific version of @agentuity/cli is available on npm registry.
22
+ * Uses the npm registry API directly for faster response than `npm view`.
23
+ *
24
+ * @param version - Version to check (with or without 'v' prefix)
25
+ * @param options - Optional configuration
26
+ * @returns true if version is available, false otherwise
27
+ */
28
+ export async function isVersionAvailableOnNpm(
29
+ version: string,
30
+ options: CheckNpmOptions = {}
31
+ ): Promise<boolean> {
32
+ const { timeoutMs = EXPLICIT_CHECK_TIMEOUT_MS } = options;
33
+ const normalizedVersion = version.replace(/^v/, '');
34
+ const url = `${NPM_REGISTRY_URL}/${encodeURIComponent(PACKAGE_NAME)}/${normalizedVersion}`;
35
+
36
+ try {
37
+ const response = await fetch(url, {
38
+ method: 'HEAD', // Only need to check existence, not full metadata
39
+ signal: AbortSignal.timeout(timeoutMs),
40
+ headers: {
41
+ Accept: 'application/json',
42
+ },
43
+ });
44
+ return response.ok;
45
+ } catch {
46
+ // Network error or timeout - assume not available
47
+ return false;
48
+ }
49
+ }
50
+
51
+ /**
52
+ * Quick check if a version is available on npm with a short timeout.
53
+ * Used for implicit version checks (auto-upgrade flow) to avoid blocking the user's command.
54
+ *
55
+ * @param version - Version to check (with or without 'v' prefix)
56
+ * @returns true if version is available, false if unavailable or timeout
57
+ */
58
+ export async function isVersionAvailableOnNpmQuick(version: string): Promise<boolean> {
59
+ return isVersionAvailableOnNpm(version, { timeoutMs: QUICK_CHECK_TIMEOUT_MS });
60
+ }
61
+
62
+ export interface WaitForNpmOptions {
63
+ /** Maximum number of attempts (default: 6) */
64
+ maxAttempts?: number;
65
+ /** Initial delay between attempts in ms (default: 2000) */
66
+ initialDelayMs?: number;
67
+ /** Maximum delay between attempts in ms (default: 10000) */
68
+ maxDelayMs?: number;
69
+ /** Callback called before each retry */
70
+ onRetry?: (attempt: number, delayMs: number) => void;
71
+ }
72
+
73
+ /**
74
+ * Wait for a version to become available on npm with exponential backoff.
75
+ *
76
+ * @param version - Version to wait for (with or without 'v' prefix)
77
+ * @param options - Configuration options
78
+ * @returns true if version became available, false if timed out
79
+ */
80
+ export async function waitForNpmAvailability(
81
+ version: string,
82
+ options: WaitForNpmOptions = {}
83
+ ): Promise<boolean> {
84
+ const { maxAttempts = 6, initialDelayMs = 2000, maxDelayMs = 10000, onRetry } = options;
85
+
86
+ // First check - no delay
87
+ if (await isVersionAvailableOnNpm(version)) {
88
+ return true;
89
+ }
90
+
91
+ // Retry with exponential backoff
92
+ let delay = initialDelayMs;
93
+ for (let attempt = 1; attempt < maxAttempts; attempt++) {
94
+ onRetry?.(attempt, delay);
95
+ await new Promise((resolve) => setTimeout(resolve, delay));
96
+
97
+ if (await isVersionAvailableOnNpm(version)) {
98
+ return true;
99
+ }
100
+
101
+ delay = Math.min(Math.round(delay * 1.5), maxDelayMs);
102
+ }
103
+
104
+ return false;
105
+ }
@@ -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
  */
package/src/tui.ts CHANGED
@@ -1764,9 +1764,18 @@ export async function prompt(message: string): Promise<string> {
1764
1764
  });
1765
1765
  }
1766
1766
 
1767
+ /**
1768
+ * Select an organization from a list.
1769
+ *
1770
+ * @param orgs - List of organizations to choose from
1771
+ * @param initial - Preferred org ID to pre-select (from saved preferences)
1772
+ * @param autoSelect - If true, auto-select preferred org without prompting (for --confirm or non-interactive)
1773
+ * @returns The selected organization ID
1774
+ */
1767
1775
  export async function selectOrganization(
1768
1776
  orgs: OrganizationList,
1769
- initial?: string
1777
+ initial?: string,
1778
+ autoSelect?: boolean
1770
1779
  ): Promise<string> {
1771
1780
  if (orgs.length === 0) {
1772
1781
  fatal(
@@ -1775,6 +1784,7 @@ export async function selectOrganization(
1775
1784
  );
1776
1785
  }
1777
1786
 
1787
+ // 1. Environment variable always takes precedence
1778
1788
  if (process.env.AGENTUITY_CLOUD_ORG_ID) {
1779
1789
  const org = orgs.find((o) => o.id === process.env.AGENTUITY_CLOUD_ORG_ID);
1780
1790
  if (org) {
@@ -1782,41 +1792,59 @@ export async function selectOrganization(
1782
1792
  }
1783
1793
  }
1784
1794
 
1785
- // Auto-select if only one org (regardless of TTY mode)
1795
+ // 2. Auto-select if only one org (regardless of TTY mode or autoSelect)
1786
1796
  if (orgs.length === 1 && orgs[0]) {
1787
1797
  return orgs[0].id;
1788
1798
  }
1789
1799
 
1790
- // Use saved preference if available (regardless of TTY mode)
1791
- // This allows consistent behavior without prompting on every command
1792
- if (initial) {
1793
- const initialOrg = orgs.find((o) => o.id === initial);
1794
- if (initialOrg) {
1795
- return initialOrg.id;
1800
+ // 3. Auto-select mode (--confirm flag or explicit autoSelect)
1801
+ // Use preferred org if set, otherwise fall back to first org
1802
+ if (autoSelect) {
1803
+ if (initial) {
1804
+ const initialOrg = orgs.find((o) => o.id === initial);
1805
+ if (initialOrg) {
1806
+ return initialOrg.id;
1807
+ }
1808
+ }
1809
+ // Fall back to first org with warning
1810
+ const firstOrg = orgs[0];
1811
+ if (firstOrg) {
1812
+ warning(
1813
+ `Multiple organizations found. Auto-selecting first org: ${firstOrg.name}. ` +
1814
+ `Set AGENTUITY_CLOUD_ORG_ID, use --org-id, or run 'agentuity auth org select' to set a default.`
1815
+ );
1816
+ return firstOrg.id;
1796
1817
  }
1797
1818
  }
1798
1819
 
1799
- // Check for non-interactive environment (check both stdin and stdout)
1820
+ // 4. Check for non-interactive environment (check both stdin and stdout)
1800
1821
  const isNonInteractive = !process.stdin.isTTY || !process.stdout.isTTY;
1801
1822
  if (isNonInteractive) {
1802
- // In non-interactive mode with multiple orgs, auto-select first org
1803
- // This allows scripts and CI/CD to work without explicit org selection
1823
+ // In non-interactive mode, use preferred org if set
1824
+ if (initial) {
1825
+ const initialOrg = orgs.find((o) => o.id === initial);
1826
+ if (initialOrg) {
1827
+ return initialOrg.id;
1828
+ }
1829
+ }
1830
+ // Fall back to first org with warning
1804
1831
  const firstOrg = orgs[0];
1805
1832
  if (firstOrg) {
1806
1833
  warning(
1807
1834
  `Multiple organizations found. Auto-selecting first org: ${firstOrg.name}. ` +
1808
- `Set AGENTUITY_CLOUD_ORG_ID or use --org-id to specify a different org.`
1835
+ `Set AGENTUITY_CLOUD_ORG_ID, use --org-id, or run 'agentuity auth org select' to set a default.`
1809
1836
  );
1810
1837
  return firstOrg.id;
1811
1838
  }
1812
1839
  }
1813
1840
 
1814
- // Interactive mode with no saved preference - prompt user
1841
+ // 5. Interactive mode - show selector with preferred org pre-selected
1842
+ const initialIndex = initial ? orgs.findIndex((o) => o.id === initial) : 0;
1815
1843
  const response = await enquirer.prompt<{ action: string }>({
1816
1844
  type: 'select',
1817
1845
  name: 'action',
1818
1846
  message: 'Select an organization',
1819
- initial: 0,
1847
+ initial: initialIndex >= 0 ? initialIndex : 0,
1820
1848
  choices: orgs.map((o) => ({ message: o.name, name: o.id })),
1821
1849
  });
1822
1850
 
@@ -233,18 +233,34 @@ export async function checkForUpdates(
233
233
  const currentVersion = getVersion();
234
234
  const latestVersion = await fetchLatestVersion();
235
235
 
236
- // Update the timestamp since we successfully checked
237
- await updateCheckTimestamp(config, logger);
238
-
239
236
  // Compare versions
240
237
  const normalizedCurrent = currentVersion.replace(/^v/, '');
241
238
  const normalizedLatest = latestVersion.replace(/^v/, '');
242
239
 
243
240
  if (normalizedCurrent === normalizedLatest) {
241
+ // Update timestamp - we confirmed we're on latest version
242
+ await updateCheckTimestamp(config, logger);
244
243
  logger.trace('Already on latest version: %s', currentVersion);
245
244
  return;
246
245
  }
247
246
 
247
+ // Quick npm availability check before prompting (short timeout, no retries)
248
+ // This avoids blocking the user's command if npm is slow or version not yet available
249
+ const { isVersionAvailableOnNpmQuick } = await import('./cmd/upgrade/npm-availability');
250
+ const isAvailable = await isVersionAvailableOnNpmQuick(latestVersion);
251
+
252
+ if (!isAvailable) {
253
+ // Don't update timestamp - we want to check again soon since npm may propagate
254
+ logger.debug(
255
+ 'Version %s not yet available on npm, skipping upgrade prompt',
256
+ latestVersion
257
+ );
258
+ return;
259
+ }
260
+
261
+ // Update timestamp - npm availability confirmed, we can proceed with prompt
262
+ await updateCheckTimestamp(config, logger);
263
+
248
264
  // New version available - prompt user
249
265
  const shouldUpgrade = await promptUpgrade(currentVersion, latestVersion);
250
266