@codemieai/code 0.0.57 → 0.0.58

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/README.md +124 -19
  2. package/bin/proxy-daemon.js +10 -0
  3. package/dist/agents/core/session/SessionStore.d.ts +4 -0
  4. package/dist/agents/core/session/SessionStore.d.ts.map +1 -1
  5. package/dist/agents/core/session/SessionStore.js +40 -1
  6. package/dist/agents/core/session/SessionStore.js.map +1 -1
  7. package/dist/agents/core/session/types.d.ts +7 -0
  8. package/dist/agents/core/session/types.d.ts.map +1 -1
  9. package/dist/agents/plugins/claude/plugin/.claude-plugin/plugin.json +1 -1
  10. package/dist/agents/plugins/claude/session/processors/claude.conversations-processor.d.ts +1 -0
  11. package/dist/agents/plugins/claude/session/processors/claude.conversations-processor.d.ts.map +1 -1
  12. package/dist/agents/plugins/claude/session/processors/claude.conversations-processor.js +32 -20
  13. package/dist/agents/plugins/claude/session/processors/claude.conversations-processor.js.map +1 -1
  14. package/dist/agents/plugins/claude/session/processors/claude.metrics-processor.d.ts +2 -0
  15. package/dist/agents/plugins/claude/session/processors/claude.metrics-processor.d.ts.map +1 -1
  16. package/dist/agents/plugins/claude/session/processors/claude.metrics-processor.js +24 -1
  17. package/dist/agents/plugins/claude/session/processors/claude.metrics-processor.js.map +1 -1
  18. package/dist/bin/proxy-daemon.d.ts +2 -0
  19. package/dist/bin/proxy-daemon.d.ts.map +1 -0
  20. package/dist/bin/proxy-daemon.js +138 -0
  21. package/dist/bin/proxy-daemon.js.map +1 -0
  22. package/dist/cli/commands/proxy/connectors/desktop.d.ts +55 -0
  23. package/dist/cli/commands/proxy/connectors/desktop.d.ts.map +1 -0
  24. package/dist/cli/commands/proxy/connectors/desktop.js +175 -0
  25. package/dist/cli/commands/proxy/connectors/desktop.js.map +1 -0
  26. package/dist/cli/commands/proxy/daemon-manager.d.ts +35 -0
  27. package/dist/cli/commands/proxy/daemon-manager.d.ts.map +1 -0
  28. package/dist/cli/commands/proxy/daemon-manager.js +107 -0
  29. package/dist/cli/commands/proxy/daemon-manager.js.map +1 -0
  30. package/dist/cli/commands/proxy/index.d.ts +3 -0
  31. package/dist/cli/commands/proxy/index.d.ts.map +1 -0
  32. package/dist/cli/commands/proxy/index.js +221 -0
  33. package/dist/cli/commands/proxy/index.js.map +1 -0
  34. package/dist/cli/commands/proxy/inspect-desktop.d.ts +28 -0
  35. package/dist/cli/commands/proxy/inspect-desktop.d.ts.map +1 -0
  36. package/dist/cli/commands/proxy/inspect-desktop.js +134 -0
  37. package/dist/cli/commands/proxy/inspect-desktop.js.map +1 -0
  38. package/dist/cli/index.js +12 -0
  39. package/dist/cli/index.js.map +1 -1
  40. package/dist/providers/plugins/sso/proxy/plugins/gateway-key.plugin.d.ts +9 -0
  41. package/dist/providers/plugins/sso/proxy/plugins/gateway-key.plugin.d.ts.map +1 -0
  42. package/dist/providers/plugins/sso/proxy/plugins/gateway-key.plugin.js +36 -0
  43. package/dist/providers/plugins/sso/proxy/plugins/gateway-key.plugin.js.map +1 -0
  44. package/dist/providers/plugins/sso/proxy/plugins/index.d.ts +2 -1
  45. package/dist/providers/plugins/sso/proxy/plugins/index.d.ts.map +1 -1
  46. package/dist/providers/plugins/sso/proxy/plugins/index.js +3 -1
  47. package/dist/providers/plugins/sso/proxy/plugins/index.js.map +1 -1
  48. package/dist/providers/plugins/sso/proxy/proxy-types.d.ts +4 -0
  49. package/dist/providers/plugins/sso/proxy/proxy-types.d.ts.map +1 -1
  50. package/dist/providers/plugins/sso/proxy/sso.proxy.d.ts.map +1 -1
  51. package/dist/providers/plugins/sso/proxy/sso.proxy.js +4 -3
  52. package/dist/providers/plugins/sso/proxy/sso.proxy.js.map +1 -1
  53. package/dist/telemetry/clients/claude-desktop/ClaudeDesktopTelemetryAdapter.d.ts +15 -0
  54. package/dist/telemetry/clients/claude-desktop/ClaudeDesktopTelemetryAdapter.d.ts.map +1 -0
  55. package/dist/telemetry/clients/claude-desktop/ClaudeDesktopTelemetryAdapter.js +117 -0
  56. package/dist/telemetry/clients/claude-desktop/ClaudeDesktopTelemetryAdapter.js.map +1 -0
  57. package/dist/telemetry/clients/claude-desktop/claude-desktop.discovery.d.ts +3 -0
  58. package/dist/telemetry/clients/claude-desktop/claude-desktop.discovery.d.ts.map +1 -0
  59. package/dist/telemetry/clients/claude-desktop/claude-desktop.discovery.js +138 -0
  60. package/dist/telemetry/clients/claude-desktop/claude-desktop.discovery.js.map +1 -0
  61. package/dist/telemetry/clients/claude-desktop/claude-desktop.metrics.d.ts +4 -0
  62. package/dist/telemetry/clients/claude-desktop/claude-desktop.metrics.d.ts.map +1 -0
  63. package/dist/telemetry/clients/claude-desktop/claude-desktop.metrics.js +53 -0
  64. package/dist/telemetry/clients/claude-desktop/claude-desktop.metrics.js.map +1 -0
  65. package/dist/telemetry/clients/claude-desktop/claude-desktop.parser.d.ts +4 -0
  66. package/dist/telemetry/clients/claude-desktop/claude-desktop.parser.d.ts.map +1 -0
  67. package/dist/telemetry/clients/claude-desktop/claude-desktop.parser.js +60 -0
  68. package/dist/telemetry/clients/claude-desktop/claude-desktop.parser.js.map +1 -0
  69. package/dist/telemetry/clients/claude-desktop/claude-desktop.paths.d.ts +4 -0
  70. package/dist/telemetry/clients/claude-desktop/claude-desktop.paths.d.ts.map +1 -0
  71. package/dist/telemetry/clients/claude-desktop/claude-desktop.paths.js +19 -0
  72. package/dist/telemetry/clients/claude-desktop/claude-desktop.paths.js.map +1 -0
  73. package/dist/telemetry/runtime/DesktopTelemetryRuntime.d.ts +23 -0
  74. package/dist/telemetry/runtime/DesktopTelemetryRuntime.d.ts.map +1 -0
  75. package/dist/telemetry/runtime/DesktopTelemetryRuntime.js +243 -0
  76. package/dist/telemetry/runtime/DesktopTelemetryRuntime.js.map +1 -0
  77. package/dist/telemetry/runtime/checkpoints.d.ts +10 -0
  78. package/dist/telemetry/runtime/checkpoints.d.ts.map +1 -0
  79. package/dist/telemetry/runtime/checkpoints.js +8 -0
  80. package/dist/telemetry/runtime/checkpoints.js.map +1 -0
  81. package/dist/telemetry/runtime/types.d.ts +32 -0
  82. package/dist/telemetry/runtime/types.d.ts.map +1 -0
  83. package/dist/telemetry/runtime/types.js +2 -0
  84. package/dist/telemetry/runtime/types.js.map +1 -0
  85. package/dist/utils/processes.d.ts +6 -0
  86. package/dist/utils/processes.d.ts.map +1 -1
  87. package/dist/utils/processes.js +13 -3
  88. package/dist/utils/processes.js.map +1 -1
  89. package/package.json +4 -2
  90. package/scripts/README.md +15 -1
  91. package/scripts/prepare-install-artifacts.mjs +66 -0
  92. package/scripts/test-desktop-telemetry.js +229 -0
  93. package/scripts/test-proxy-endpoint.js +173 -0
@@ -0,0 +1,229 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { existsSync, readFileSync } from 'fs';
4
+ import { randomUUID } from 'crypto';
5
+ import { resolve } from 'path';
6
+
7
+ function printUsage() {
8
+ console.log(`Usage: node scripts/test-desktop-telemetry.js [options]
9
+
10
+ Options:
11
+ --latest Use the most recently updated Desktop session (default)
12
+ --session <local_id> Use a specific Desktop local session id
13
+ --dry-run Build and sync through processors without remote writes (default)
14
+ --live Attempt real sync using stored SSO credentials
15
+ --target-url <url> Override API base URL for sync
16
+ --codemie-url <url> Override CodeMie URL for credential lookup
17
+ --codemie-home <dir> Override CODEMIE_HOME for isolated local output
18
+ --limit <count> Number of recent sessions to search while resolving --session (default: 20)
19
+ --help Show this help
20
+ `);
21
+ }
22
+
23
+ function parseArgs(argv) {
24
+ const args = {
25
+ latest: true,
26
+ sessionId: undefined,
27
+ dryRun: true,
28
+ targetUrl: undefined,
29
+ codeMieUrl: undefined,
30
+ codemieHome: undefined,
31
+ limit: 20
32
+ };
33
+
34
+ for (let index = 0; index < argv.length; index += 1) {
35
+ const arg = argv[index];
36
+
37
+ switch (arg) {
38
+ case '--latest':
39
+ args.latest = true;
40
+ break;
41
+ case '--session':
42
+ args.sessionId = argv[index + 1];
43
+ args.latest = false;
44
+ index += 1;
45
+ break;
46
+ case '--dry-run':
47
+ args.dryRun = true;
48
+ break;
49
+ case '--live':
50
+ args.dryRun = false;
51
+ break;
52
+ case '--target-url':
53
+ args.targetUrl = argv[index + 1];
54
+ index += 1;
55
+ break;
56
+ case '--codemie-url':
57
+ args.codeMieUrl = argv[index + 1];
58
+ index += 1;
59
+ break;
60
+ case '--codemie-home':
61
+ args.codemieHome = argv[index + 1];
62
+ index += 1;
63
+ break;
64
+ case '--limit':
65
+ args.limit = Number.parseInt(argv[index + 1], 10);
66
+ index += 1;
67
+ break;
68
+ case '--help':
69
+ printUsage();
70
+ process.exit(0);
71
+ default:
72
+ console.error(`Unknown argument: ${arg}`);
73
+ printUsage();
74
+ process.exit(1);
75
+ }
76
+ }
77
+
78
+ return args;
79
+ }
80
+
81
+ const args = parseArgs(process.argv.slice(2));
82
+ if (args.codemieHome) {
83
+ process.env.CODEMIE_HOME = resolve(args.codemieHome);
84
+ }
85
+
86
+ const distRoot = resolve(process.cwd(), 'dist');
87
+ if (!existsSync(distRoot)) {
88
+ console.error('dist/ is missing. Run: rtk npm run build');
89
+ process.exit(1);
90
+ }
91
+
92
+ const [
93
+ { discoverClaudeDesktopSessions },
94
+ { ClaudeDesktopTelemetryAdapter },
95
+ { SessionStore },
96
+ { getSessionConversationPath, getSessionMetricsPath },
97
+ { SessionSyncer },
98
+ { CodeMieSSO },
99
+ { detectGitBranch, detectGitRemoteRepo },
100
+ ] = await Promise.all([
101
+ import('../dist/telemetry/clients/claude-desktop/claude-desktop.discovery.js'),
102
+ import('../dist/telemetry/clients/claude-desktop/ClaudeDesktopTelemetryAdapter.js'),
103
+ import('../dist/agents/core/session/SessionStore.js'),
104
+ import('../dist/agents/core/session/session-config.js'),
105
+ import('../dist/providers/plugins/sso/session/SessionSyncer.js'),
106
+ import('../dist/providers/plugins/sso/sso.auth.js'),
107
+ import('../dist/utils/processes.js')
108
+ ]);
109
+
110
+ const discoveredSessions = await discoverClaudeDesktopSessions(0);
111
+ const recentSessions = discoveredSessions
112
+ .sort((a, b) => b.updatedAt - a.updatedAt)
113
+ .slice(0, Number.isFinite(args.limit) && args.limit > 0 ? args.limit : 20);
114
+
115
+ const selected = args.sessionId
116
+ ? recentSessions.find((session) => session.externalSessionId === args.sessionId)
117
+ : recentSessions[0];
118
+
119
+ if (!selected) {
120
+ console.error('No matching Claude Desktop local session found.');
121
+ process.exit(1);
122
+ }
123
+
124
+ const sessionStore = new SessionStore();
125
+ let session = await sessionStore.findSessionByExternalId('claude-desktop', selected.externalSessionId);
126
+
127
+ if (!session) {
128
+ const [gitBranch, repository] = await Promise.all([
129
+ detectGitBranch(selected.workingDirectory),
130
+ detectGitRemoteRepo(selected.workingDirectory)
131
+ ]);
132
+
133
+ session = {
134
+ sessionId: randomUUID(),
135
+ agentName: 'claude-desktop',
136
+ provider: 'ai-run-sso',
137
+ startTime: selected.createdAt,
138
+ workingDirectory: selected.workingDirectory,
139
+ gitBranch: gitBranch || undefined,
140
+ repository: repository || undefined,
141
+ status: 'active',
142
+ activeDurationMs: 0,
143
+ correlation: {
144
+ status: 'matched',
145
+ agentSessionId: selected.agentSessionId,
146
+ agentSessionFile: selected.transcriptPath,
147
+ retryCount: 0
148
+ },
149
+ runtimeCheckpoint: {
150
+ externalSessionId: selected.externalSessionId,
151
+ transcriptPath: selected.transcriptPath,
152
+ lastDiscoveredAt: Date.now(),
153
+ lastSeenActivityAt: selected.updatedAt
154
+ }
155
+ };
156
+
157
+ await sessionStore.saveSession(session);
158
+ }
159
+
160
+ const targetUrl = args.targetUrl
161
+ || process.env.CODEMIE_TEST_API_URL
162
+ || process.env.CODEMIE_API_URL
163
+ || 'http://127.0.0.1:3000';
164
+ const codeMieUrl = args.codeMieUrl
165
+ || process.env.CODEMIE_TEST_CODEMIE_URL
166
+ || process.env.CODEMIE_URL
167
+ || targetUrl;
168
+
169
+ let cookies = '';
170
+ if (!args.dryRun) {
171
+ const credentials = await new CodeMieSSO().getStoredCredentials(codeMieUrl);
172
+ if (!credentials?.cookies) {
173
+ console.error(`No stored SSO credentials found for ${codeMieUrl}`);
174
+ process.exit(1);
175
+ }
176
+ cookies = Object.entries(credentials.cookies)
177
+ .map(([key, value]) => `${key}=${value}`)
178
+ .join('; ');
179
+ }
180
+
181
+ const adapter = new ClaudeDesktopTelemetryAdapter();
182
+ const parsedSession = await adapter.parseSession(selected, session.sessionId);
183
+ const context = {
184
+ apiBaseUrl: targetUrl,
185
+ cookies,
186
+ clientType: 'claude-desktop',
187
+ version: readCliVersion(),
188
+ dryRun: args.dryRun,
189
+ sessionId: session.sessionId,
190
+ agentSessionId: selected.agentSessionId,
191
+ agentSessionFile: selected.transcriptPath
192
+ };
193
+
194
+ const processed = await adapter.processParsedSession(parsedSession, context);
195
+ const syncResult = await new SessionSyncer().sync(session.sessionId, context);
196
+ const metricsPath = getSessionMetricsPath(session.sessionId);
197
+ const conversationPath = getSessionConversationPath(session.sessionId);
198
+
199
+ console.log('Claude Desktop telemetry simulation');
200
+ console.log(` mode: ${args.dryRun ? 'dry-run' : 'live'}`);
201
+ console.log(` local session: ${selected.externalSessionId}`);
202
+ console.log(` cli session: ${selected.agentSessionId}`);
203
+ console.log(` codemie session: ${session.sessionId}`);
204
+ console.log(` updated at: ${new Date(selected.updatedAt).toISOString()}`);
205
+ console.log(` transcript: ${selected.transcriptPath}`);
206
+ console.log(` messages parsed: ${parsedSession.messages.length}`);
207
+ console.log(` metrics path: ${metricsPath}${existsSync(metricsPath) ? '' : ' (missing)'}`);
208
+ console.log(` conversation path: ${conversationPath}${existsSync(conversationPath) ? '' : ' (missing)'}`);
209
+ console.log(` processors: ${processed.success ? 'ok' : 'failed'} (${processed.totalRecords} records)`);
210
+ console.log(` sync: ${syncResult.success ? 'ok' : 'failed'} (${syncResult.message})`);
211
+
212
+ if (existsSync(metricsPath)) {
213
+ const lines = readFileSync(metricsPath, 'utf-8').split('\n').filter(Boolean).length;
214
+ console.log(` metrics lines: ${lines}`);
215
+ }
216
+
217
+ if (existsSync(conversationPath)) {
218
+ const lines = readFileSync(conversationPath, 'utf-8').split('\n').filter(Boolean).length;
219
+ console.log(` conversation lines:${String(lines).padStart(2, ' ')}`);
220
+ }
221
+
222
+ function readCliVersion() {
223
+ try {
224
+ const pkg = JSON.parse(readFileSync(resolve(process.cwd(), 'package.json'), 'utf-8'));
225
+ return pkg.version || '0.0.0';
226
+ } catch {
227
+ return '0.0.0';
228
+ }
229
+ }
@@ -0,0 +1,173 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Send a sample /v1/messages request to the local CodeMie proxy daemon.
4
+ *
5
+ * - Auto-discovers URL and gateway key from ~/.codemie/proxy-daemon.json
6
+ * (override with --url and --gateway-key)
7
+ * - Sends Anthropic-style headers and a tiny payload
8
+ * - Supports both buffered and streaming (SSE) responses
9
+ *
10
+ * Usage:
11
+ * node scripts/test-proxy-endpoint.js # auto-discover, non-streaming
12
+ * node scripts/test-proxy-endpoint.js --stream # SSE streaming
13
+ * node scripts/test-proxy-endpoint.js --url http://localhost:4001 --gateway-key codemie-proxy
14
+ * node scripts/test-proxy-endpoint.js --model claude-sonnet-4-5 --message "Hello"
15
+ */
16
+ import { readFile } from 'node:fs/promises';
17
+ import { join } from 'node:path';
18
+ import { homedir } from 'node:os';
19
+
20
+ const STATE_FILE = process.env.CODEMIE_HOME
21
+ ? join(process.env.CODEMIE_HOME, 'proxy-daemon.json')
22
+ : join(homedir(), '.codemie', 'proxy-daemon.json');
23
+
24
+ function printUsage() {
25
+ console.log(`Usage: node scripts/test-proxy-endpoint.js [options]
26
+
27
+ Options:
28
+ --url <base-url> Proxy base URL (default: read from state file)
29
+ --gateway-key <key> Static bearer key (default: read from state file)
30
+ --endpoint <path> Endpoint path (default: /v1/messages)
31
+ --model <id> Model ID (default: claude-sonnet-4-5)
32
+ --message <text> Prompt text (default: "Test proxy endpoint")
33
+ --max-tokens <n> Max tokens (default: 32)
34
+ --stream Use SSE streaming
35
+ --no-anthropic-version Don't send anthropic-version header
36
+ -h, --help Show this help`);
37
+ }
38
+
39
+ function parseArgs(argv) {
40
+ const opts = {
41
+ endpoint: '/v1/messages',
42
+ message: 'Test proxy endpoint',
43
+ model: 'claude-sonnet-4-5',
44
+ maxTokens: 32,
45
+ stream: false,
46
+ sendAnthropicVersion: true,
47
+ };
48
+
49
+ for (let i = 0; i < argv.length; i += 1) {
50
+ const arg = argv[i];
51
+ if (arg === '--help' || arg === '-h') { opts.help = true; return opts; }
52
+ if (arg === '--url') { opts.url = argv[++i]; continue; }
53
+ if (arg === '--gateway-key') { opts.gatewayKey = argv[++i]; continue; }
54
+ if (arg === '--endpoint') { opts.endpoint = argv[++i] || opts.endpoint; continue; }
55
+ if (arg === '--message') { opts.message = argv[++i] || opts.message; continue; }
56
+ if (arg === '--model') { opts.model = argv[++i] || opts.model; continue; }
57
+ if (arg === '--max-tokens') {
58
+ const v = Number.parseInt(argv[++i] || '', 10);
59
+ if (Number.isFinite(v) && v > 0) opts.maxTokens = v;
60
+ continue;
61
+ }
62
+ if (arg === '--stream') { opts.stream = true; continue; }
63
+ if (arg === '--no-anthropic-version') { opts.sendAnthropicVersion = false; continue; }
64
+ }
65
+ return opts;
66
+ }
67
+
68
+ async function loadDaemonState() {
69
+ try {
70
+ const raw = await readFile(STATE_FILE, 'utf-8');
71
+ return JSON.parse(raw);
72
+ } catch {
73
+ return null;
74
+ }
75
+ }
76
+
77
+ async function main() {
78
+ const args = parseArgs(process.argv.slice(2));
79
+ if (args.help) { printUsage(); process.exit(0); }
80
+
81
+ if (!args.url || !args.gatewayKey) {
82
+ const state = await loadDaemonState();
83
+ if (state) {
84
+ args.url ??= state.url;
85
+ args.gatewayKey ??= state.gatewayKey;
86
+ console.log(`[test] Using daemon at ${args.url} (profile: ${state.profile})`);
87
+ }
88
+ }
89
+
90
+ if (!args.url) {
91
+ console.error(`No proxy URL. Provide --url or start the daemon: codemie proxy start`);
92
+ process.exit(1);
93
+ }
94
+ if (!args.gatewayKey) {
95
+ console.error(`No gateway key. Provide --gateway-key or start the daemon: codemie proxy start`);
96
+ process.exit(1);
97
+ }
98
+
99
+ let baseUrl;
100
+ try { baseUrl = new URL(args.url); }
101
+ catch { console.error(`Invalid URL: ${args.url}`); process.exit(1); }
102
+
103
+ const endpoint = args.endpoint.startsWith('/') ? args.endpoint : `/${args.endpoint}`;
104
+ const targetUrl = new URL(endpoint, baseUrl);
105
+
106
+ const body = {
107
+ model: args.model,
108
+ messages: [{ role: 'user', content: args.message }],
109
+ max_tokens: args.maxTokens,
110
+ stream: args.stream,
111
+ };
112
+
113
+ const headers = {
114
+ 'Content-Type': 'application/json',
115
+ Accept: args.stream ? 'text/event-stream' : 'application/json',
116
+ Authorization: `Bearer ${args.gatewayKey}`,
117
+ };
118
+ if (args.sendAnthropicVersion) headers['anthropic-version'] = '2023-06-01';
119
+
120
+ console.log(`[test] POST ${targetUrl.href}`);
121
+ console.log(`[test] Headers: ${JSON.stringify({ ...headers, Authorization: 'Bearer ***' })}`);
122
+ console.log(`[test] Body: ${JSON.stringify(body)}`);
123
+
124
+ const startMs = Date.now();
125
+ let response;
126
+ try {
127
+ response = await fetch(targetUrl, {
128
+ method: 'POST',
129
+ headers,
130
+ body: JSON.stringify(body),
131
+ });
132
+ } catch (error) {
133
+ console.error(`[test] Request failed: ${error.message}`);
134
+ process.exit(1);
135
+ }
136
+
137
+ const ttfbMs = Date.now() - startMs;
138
+ console.log(`\n[test] Status: ${response.status} ${response.statusText} (TTFB ${ttfbMs}ms)`);
139
+ console.log(`[test] Response headers:`);
140
+ for (const [k, v] of response.headers.entries()) {
141
+ console.log(` ${k}: ${v}`);
142
+ }
143
+
144
+ if (args.stream) {
145
+ if (!response.body) {
146
+ console.error('[test] No response body for streaming');
147
+ process.exit(1);
148
+ }
149
+ console.log('\n[test] --- SSE stream ---');
150
+ const reader = response.body.getReader();
151
+ const decoder = new TextDecoder();
152
+ let chunkCount = 0;
153
+ let totalBytes = 0;
154
+ while (true) {
155
+ const { value, done } = await reader.read();
156
+ if (done) break;
157
+ chunkCount += 1;
158
+ totalBytes += value.length;
159
+ process.stdout.write(decoder.decode(value, { stream: true }));
160
+ }
161
+ const totalMs = Date.now() - startMs;
162
+ console.log(`\n[test] --- end (${chunkCount} chunks, ${totalBytes} bytes, ${totalMs}ms total) ---`);
163
+ } else {
164
+ const text = await response.text();
165
+ const totalMs = Date.now() - startMs;
166
+ console.log(`\n[test] Body (${text.length} bytes, ${totalMs}ms total):`);
167
+ console.log(text);
168
+ }
169
+
170
+ if (!response.ok) process.exit(1);
171
+ }
172
+
173
+ main().catch(err => { console.error(err); process.exit(1); });