@uptomic/career-mentor-cli 0.1.0

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/dist/index.js ADDED
@@ -0,0 +1,1945 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli.ts
4
+ import { realpathSync } from "node:fs";
5
+ import { readFile as readFileFromDisk } from "node:fs/promises";
6
+ import { basename, extname } from "node:path";
7
+ import { pathToFileURL } from "node:url";
8
+
9
+ // ../career-mentor-sdk/dist/index.js
10
+ class CareerMentorError extends Error {
11
+ code;
12
+ status;
13
+ requestId;
14
+ retryAfter;
15
+ constructor(message, options) {
16
+ super(message);
17
+ this.name = "CareerMentorError";
18
+ this.code = options.code;
19
+ this.status = options.status;
20
+ this.requestId = options.requestId;
21
+ this.retryAfter = options.retryAfter;
22
+ }
23
+ }
24
+
25
+ class CareerMentorApiError extends CareerMentorError {
26
+ constructor(message, options) {
27
+ super(message, options);
28
+ this.name = "CareerMentorApiError";
29
+ }
30
+ }
31
+
32
+ class CareerMentorRequestError extends CareerMentorError {
33
+ constructor(message, options) {
34
+ super(message, options);
35
+ this.name = "CareerMentorRequestError";
36
+ }
37
+ }
38
+ var SDK_VERSION = "0.1.0";
39
+ var DEFAULT_TIMEOUT_MS = 30000;
40
+ var DEFAULT_USER_AGENT = `career-mentor-typescript/${SDK_VERSION}`;
41
+ var MAX_ERROR_MESSAGE_LENGTH = 300;
42
+ var MAX_HEADER_VALUE_LENGTH = 512;
43
+ var MAX_RUN_EVENT_LIMIT = 1000;
44
+ var MAX_SSE_BUFFER_LENGTH = 1e6;
45
+ var RUN_WAIT_RECONNECT_INITIAL_DELAY_MS = 100;
46
+ var RUN_WAIT_RECONNECT_MAX_DELAY_MS = 1000;
47
+ var CONTROL_CHARACTER = /[\u0000-\u001f\u007f]/;
48
+ var CONTROL_CHARACTERS = /[\u0000-\u001f\u007f]/g;
49
+ var EVENT_CURSOR_PATTERN = /^\d+-\d+$/;
50
+ var UUID_V7_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
51
+ function invalidConfiguration(message) {
52
+ throw new CareerMentorRequestError(message, { code: "INVALID_CONFIGURATION" });
53
+ }
54
+ function normalizeBaseUrl(value) {
55
+ if (!value.trim()) {
56
+ invalidConfiguration("Career Mentor API baseUrl is required.");
57
+ }
58
+ let parsed;
59
+ try {
60
+ parsed = new URL(value);
61
+ } catch {
62
+ invalidConfiguration("Career Mentor API baseUrl must be an absolute HTTP(S) URL.");
63
+ }
64
+ if (!["http:", "https:"].includes(parsed.protocol)) {
65
+ invalidConfiguration("Career Mentor API baseUrl must use HTTP or HTTPS.");
66
+ }
67
+ const loopbackHost = parsed.hostname === "localhost" || parsed.hostname === "127.0.0.1" || parsed.hostname === "[::1]";
68
+ if (parsed.protocol === "http:" && !loopbackHost) {
69
+ invalidConfiguration("Career Mentor API baseUrl must use HTTPS unless it targets the local loopback interface.");
70
+ }
71
+ if (parsed.username || parsed.password || parsed.search || parsed.hash) {
72
+ invalidConfiguration("Career Mentor API baseUrl must not include credentials, query, or hash.");
73
+ }
74
+ parsed.pathname = parsed.pathname.replace(/\/+$/, "");
75
+ return parsed.toString().replace(/\/$/, "");
76
+ }
77
+ function requirePositiveTimeout(value) {
78
+ if (!(Number.isFinite(value) && value > 0)) {
79
+ invalidConfiguration("Career Mentor request timeoutMs must be a positive finite number.");
80
+ }
81
+ return value;
82
+ }
83
+ function requireToken(value) {
84
+ const token = value.trim();
85
+ if (!token || CONTROL_CHARACTER.test(token)) {
86
+ invalidConfiguration("Career Mentor capability token is invalid.");
87
+ }
88
+ return token;
89
+ }
90
+ function requireRunId(value) {
91
+ const runId = value.trim();
92
+ if (!UUID_V7_PATTERN.test(runId)) {
93
+ invalidConfiguration("Career Mentor capability runId must be a UUIDv7 identifier.");
94
+ }
95
+ return runId;
96
+ }
97
+ function requireEventCursor(value, label) {
98
+ if (value === undefined)
99
+ return;
100
+ if (!EVENT_CURSOR_PATTERN.test(value)) {
101
+ invalidConfiguration(`${label} must be a Redis stream cursor such as 0-0.`);
102
+ }
103
+ return value;
104
+ }
105
+ function requireEventLimit(value) {
106
+ if (value === undefined)
107
+ return;
108
+ if (!(Number.isInteger(value) && value >= 1 && value <= MAX_RUN_EVENT_LIMIT)) {
109
+ invalidConfiguration(`Career Mentor run event limit must be between 1 and ${MAX_RUN_EVENT_LIMIT}.`);
110
+ }
111
+ return value;
112
+ }
113
+ function runEventsPath(runId, options) {
114
+ const search = new URLSearchParams;
115
+ const after = requireEventCursor(options.after, "Career Mentor run event after cursor");
116
+ const limit = requireEventLimit(options.limit);
117
+ if (after !== undefined)
118
+ search.set("after", after);
119
+ if (limit !== undefined)
120
+ search.set("limit", String(limit));
121
+ const query = search.toString();
122
+ return `/api/v1/runs/${encodeURIComponent(requireRunId(runId))}/events${query ? `?${query}` : ""}`;
123
+ }
124
+ function setTransportHeader(headers, name, value) {
125
+ if (value === undefined)
126
+ return;
127
+ if (!value || value.length > MAX_HEADER_VALUE_LENGTH || CONTROL_CHARACTER.test(value)) {
128
+ throw new CareerMentorRequestError(`${name} is invalid.`, { code: "INVALID_HEADER" });
129
+ }
130
+ headers.set(name, value);
131
+ }
132
+ function isRecord(value) {
133
+ return typeof value === "object" && value !== null && !Array.isArray(value);
134
+ }
135
+ function assertJsonSafe(value, seen = new Set) {
136
+ if (value === null || typeof value === "string" || typeof value === "boolean")
137
+ return;
138
+ if (typeof value === "number") {
139
+ if (!Number.isFinite(value)) {
140
+ throw new CareerMentorRequestError("Capability input must contain finite numbers.", {
141
+ code: "INVALID_JSON"
142
+ });
143
+ }
144
+ return;
145
+ }
146
+ if (typeof value !== "object") {
147
+ throw new CareerMentorRequestError("Capability input must be JSON-safe.", {
148
+ code: "INVALID_JSON"
149
+ });
150
+ }
151
+ if (seen.has(value)) {
152
+ throw new CareerMentorRequestError("Capability input must not contain cycles.", {
153
+ code: "INVALID_JSON"
154
+ });
155
+ }
156
+ seen.add(value);
157
+ if (Array.isArray(value)) {
158
+ for (const item of value)
159
+ assertJsonSafe(item, seen);
160
+ } else {
161
+ if (Object.getOwnPropertySymbols(value).length > 0) {
162
+ throw new CareerMentorRequestError("Capability input must not contain symbol keys.", {
163
+ code: "INVALID_JSON"
164
+ });
165
+ }
166
+ const prototype = Object.getPrototypeOf(value);
167
+ if (prototype !== Object.prototype && prototype !== null) {
168
+ throw new CareerMentorRequestError("Capability input must contain plain JSON objects.", {
169
+ code: "INVALID_JSON"
170
+ });
171
+ }
172
+ for (const item of Object.values(value))
173
+ assertJsonSafe(item, seen);
174
+ }
175
+ seen.delete(value);
176
+ }
177
+ function serializeJson(value) {
178
+ assertJsonSafe(value);
179
+ return JSON.stringify(value);
180
+ }
181
+ function readSafeString(value) {
182
+ if (typeof value !== "string")
183
+ return;
184
+ const normalized = value.replace(CONTROL_CHARACTERS, " ").trim();
185
+ return normalized ? normalized.slice(0, MAX_ERROR_MESSAGE_LENGTH) : undefined;
186
+ }
187
+ function parseRetryAfter(value) {
188
+ if (!value)
189
+ return;
190
+ const seconds = Number(value);
191
+ if (Number.isFinite(seconds) && seconds >= 0)
192
+ return seconds;
193
+ const date = Date.parse(value);
194
+ if (Number.isNaN(date))
195
+ return;
196
+ return Math.max(0, Math.ceil((date - Date.now()) / 1000));
197
+ }
198
+ async function readJson(response, requestId) {
199
+ try {
200
+ return await response.json();
201
+ } catch {
202
+ throw new CareerMentorApiError("Career Mentor API returned invalid JSON.", {
203
+ code: "INVALID_RESPONSE",
204
+ status: response.status,
205
+ requestId: response.headers.get("x-request-id") ?? requestId
206
+ });
207
+ }
208
+ }
209
+ function apiErrorFromResponse(response, payload, fallbackRequestId) {
210
+ const error = isRecord(payload) && isRecord(payload["error"]) ? payload["error"] : undefined;
211
+ const code = readSafeString(error?.["code"]) ?? `HTTP_${response.status}`;
212
+ const requestId = (isRecord(payload) ? readSafeString(payload["requestId"]) : undefined) ?? readSafeString(response.headers.get("x-request-id")) ?? fallbackRequestId;
213
+ const apiMessage = readSafeString(error?.["message"]);
214
+ const message = response.status >= 500 ? "Career Mentor API request failed." : apiMessage ?? `Career Mentor API request failed with status ${response.status}.`;
215
+ return new CareerMentorApiError(message, {
216
+ code,
217
+ status: response.status,
218
+ requestId,
219
+ retryAfter: parseRetryAfter(response.headers.get("retry-after"))
220
+ });
221
+ }
222
+ function invalidResponse(response, fallbackRequestId) {
223
+ return new CareerMentorApiError("Career Mentor API returned an invalid response.", {
224
+ code: "INVALID_RESPONSE",
225
+ status: response.status,
226
+ requestId: response.headers.get("x-request-id") ?? fallbackRequestId
227
+ });
228
+ }
229
+ function translateTransportError(error, didTimeout, options) {
230
+ if (didTimeout) {
231
+ return new CareerMentorRequestError("Career Mentor API request timed out.", {
232
+ code: "TIMEOUT",
233
+ requestId: options.requestId
234
+ });
235
+ }
236
+ if (options.signal?.aborted) {
237
+ return new CareerMentorRequestError("Career Mentor API request was aborted.", {
238
+ code: "REQUEST_ABORTED",
239
+ requestId: options.requestId
240
+ });
241
+ }
242
+ if (error instanceof CareerMentorError)
243
+ return error;
244
+ return new CareerMentorRequestError("Career Mentor API request failed.", {
245
+ code: "NETWORK_ERROR",
246
+ requestId: options.requestId
247
+ });
248
+ }
249
+ function runWaitTimeoutError(requestId) {
250
+ return new CareerMentorRequestError("Career Mentor API request timed out.", {
251
+ code: "TIMEOUT",
252
+ requestId
253
+ });
254
+ }
255
+ function runWaitAbortedError(requestId) {
256
+ return new CareerMentorRequestError("Career Mentor API request was aborted.", {
257
+ code: "REQUEST_ABORTED",
258
+ requestId
259
+ });
260
+ }
261
+ function remainingRunWaitTimeout(deadline, options) {
262
+ if (options.signal?.aborted)
263
+ throw runWaitAbortedError(options.requestId);
264
+ const remaining = deadline - Date.now();
265
+ if (remaining <= 0)
266
+ throw runWaitTimeoutError(options.requestId);
267
+ return remaining;
268
+ }
269
+ async function waitBeforeRunReconnect(delayMs, deadline, options) {
270
+ const remaining = remainingRunWaitTimeout(deadline, options);
271
+ const boundedDelay = Math.min(delayMs, remaining);
272
+ await new Promise((resolve, reject) => {
273
+ let timer;
274
+ const cleanup = () => {
275
+ if (timer !== undefined)
276
+ clearTimeout(timer);
277
+ options.signal?.removeEventListener("abort", abort);
278
+ };
279
+ const abort = () => {
280
+ cleanup();
281
+ reject(runWaitAbortedError(options.requestId));
282
+ };
283
+ if (options.signal?.aborted) {
284
+ abort();
285
+ return;
286
+ }
287
+ options.signal?.addEventListener("abort", abort, { once: true });
288
+ timer = setTimeout(() => {
289
+ cleanup();
290
+ resolve();
291
+ }, boundedDelay);
292
+ });
293
+ remainingRunWaitTimeout(deadline, options);
294
+ }
295
+ async function* readSseLines(stream) {
296
+ const reader = stream.getReader();
297
+ const decoder = new TextDecoder;
298
+ let buffer = "";
299
+ let completed = false;
300
+ try {
301
+ while (true) {
302
+ const next = await reader.read();
303
+ if (next.done) {
304
+ completed = true;
305
+ buffer += decoder.decode();
306
+ break;
307
+ }
308
+ buffer += decoder.decode(next.value, { stream: true });
309
+ if (buffer.length > MAX_SSE_BUFFER_LENGTH) {
310
+ throw new Error("Career Mentor SSE frame exceeded the maximum size.");
311
+ }
312
+ let newline = buffer.indexOf(`
313
+ `);
314
+ while (newline !== -1) {
315
+ const rawLine = buffer.slice(0, newline);
316
+ buffer = buffer.slice(newline + 1);
317
+ yield rawLine.endsWith("\r") ? rawLine.slice(0, -1) : rawLine;
318
+ newline = buffer.indexOf(`
319
+ `);
320
+ }
321
+ }
322
+ if (buffer)
323
+ yield buffer.endsWith("\r") ? buffer.slice(0, -1) : buffer;
324
+ } finally {
325
+ if (!completed) {
326
+ try {
327
+ await reader.cancel();
328
+ } catch {}
329
+ }
330
+ reader.releaseLock();
331
+ }
332
+ }
333
+ function parseSseEvent(frame, response, fallbackRequestId) {
334
+ if (frame.data.length === 0)
335
+ return;
336
+ if (!(frame.id && frame.eventType && EVENT_CURSOR_PATTERN.test(frame.id))) {
337
+ throw invalidResponse(response, fallbackRequestId);
338
+ }
339
+ let parsed;
340
+ try {
341
+ parsed = JSON.parse(frame.data.join(`
342
+ `));
343
+ } catch {
344
+ throw invalidResponse(response, fallbackRequestId);
345
+ }
346
+ if (!isRecord(parsed) || parsed["id"] !== frame.id || parsed["type"] !== frame.eventType || typeof parsed["status"] !== "string" || typeof parsed["timestamp"] !== "string" || typeof parsed["terminal"] !== "boolean" || !Object.hasOwn(parsed, "data")) {
347
+ throw invalidResponse(response, fallbackRequestId);
348
+ }
349
+ return parsed;
350
+ }
351
+ async function* parseSseEvents(stream, response, fallbackRequestId) {
352
+ let frame = { data: [], eventType: undefined, id: undefined };
353
+ for await (const line of readSseLines(stream)) {
354
+ if (line === "") {
355
+ const event = parseSseEvent(frame, response, fallbackRequestId);
356
+ if (event)
357
+ yield event;
358
+ frame = { data: [], eventType: undefined, id: undefined };
359
+ continue;
360
+ }
361
+ if (line.startsWith(":"))
362
+ continue;
363
+ const separator = line.indexOf(":");
364
+ const field = separator === -1 ? line : line.slice(0, separator);
365
+ const rawValue = separator === -1 ? "" : line.slice(separator + 1);
366
+ const value = rawValue.startsWith(" ") ? rawValue.slice(1) : rawValue;
367
+ if (field === "data")
368
+ frame.data.push(value);
369
+ else if (field === "event")
370
+ frame.eventType = value;
371
+ else if (field === "id")
372
+ frame.id = value;
373
+ }
374
+ const finalEvent = parseSseEvent(frame, response, fallbackRequestId);
375
+ if (finalEvent)
376
+ yield finalEvent;
377
+ }
378
+
379
+ class CareerMentorClient {
380
+ baseUrl;
381
+ timeoutMs;
382
+ userAgent;
383
+ token;
384
+ fetch;
385
+ constructor(options) {
386
+ this.baseUrl = normalizeBaseUrl(options.baseUrl);
387
+ this.token = requireToken(options.token);
388
+ this.timeoutMs = requirePositiveTimeout(options.timeoutMs ?? DEFAULT_TIMEOUT_MS);
389
+ this.userAgent = options.userAgent?.trim() || DEFAULT_USER_AGENT;
390
+ if (this.userAgent.length > MAX_HEADER_VALUE_LENGTH || CONTROL_CHARACTER.test(this.userAgent)) {
391
+ invalidConfiguration("Career Mentor SDK userAgent is invalid.");
392
+ }
393
+ const fetchImplementation = options.fetch ?? globalThis.fetch;
394
+ if (typeof fetchImplementation !== "function") {
395
+ invalidConfiguration("A Fetch API implementation is required.");
396
+ }
397
+ this.fetch = fetchImplementation;
398
+ }
399
+ async list(options = {}) {
400
+ return await this.request("/api/v1/capabilities", "GET", undefined, options, [200]);
401
+ }
402
+ async get(name, options = {}) {
403
+ return await this.request(`/api/v1/capabilities/${encodeURIComponent(name)}`, "GET", undefined, options, [200]);
404
+ }
405
+ async invoke(name, input, options = {}) {
406
+ return await this.request(`/api/v1/capabilities/${encodeURIComponent(name)}/invoke`, "POST", serializeJson(input), options, [200, 202]);
407
+ }
408
+ async getRun(runId, options = {}) {
409
+ return await this.request(`/api/v1/runs/${encodeURIComponent(requireRunId(runId))}`, "GET", undefined, options, [200]);
410
+ }
411
+ async listRunEvents(runId, options = {}) {
412
+ return await this.request(runEventsPath(runId, options), "GET", undefined, options, [200]);
413
+ }
414
+ async* streamRunEvents(runId, options = {}) {
415
+ const lastEventId = requireEventCursor(options.lastEventId, "Career Mentor Last-Event-ID cursor");
416
+ const opened = await this.openRequest(runEventsPath(runId, options), "GET", undefined, options, {
417
+ accept: "text/event-stream",
418
+ ...lastEventId ? { lastEventId } : {}
419
+ });
420
+ try {
421
+ if (opened.response.status !== 200) {
422
+ const payload = await readJson(opened.response, options.requestId);
423
+ throw apiErrorFromResponse(opened.response, payload, options.requestId);
424
+ }
425
+ if (!opened.response.headers.get("content-type")?.toLowerCase().startsWith("text/event-stream")) {
426
+ throw invalidResponse(opened.response, options.requestId);
427
+ }
428
+ if (!opened.response.body)
429
+ throw invalidResponse(opened.response, options.requestId);
430
+ for await (const event of parseSseEvents(opened.response.body, opened.response, options.requestId)) {
431
+ if (opened.controller.signal.aborted)
432
+ throw opened.controller.signal.reason;
433
+ yield event;
434
+ }
435
+ if (opened.controller.signal.aborted)
436
+ throw opened.controller.signal.reason;
437
+ } catch (error) {
438
+ throw translateTransportError(error, opened.didTimeout(), options);
439
+ } finally {
440
+ opened.cleanup();
441
+ }
442
+ }
443
+ async waitForRun(runId, options = {}) {
444
+ const timeoutMs = requirePositiveTimeout(options.timeoutMs ?? this.timeoutMs);
445
+ const deadline = Date.now() + timeoutMs;
446
+ let lastEventId = requireEventCursor(options.lastEventId, "Career Mentor Last-Event-ID cursor");
447
+ let reconnectAttempt = 0;
448
+ while (true) {
449
+ const remainingTimeoutMs = remainingRunWaitTimeout(deadline, options);
450
+ for await (const event of this.streamRunEvents(runId, {
451
+ ...options,
452
+ timeoutMs: remainingTimeoutMs,
453
+ ...lastEventId ? { lastEventId } : {}
454
+ })) {
455
+ remainingRunWaitTimeout(deadline, options);
456
+ lastEventId = event.id;
457
+ if (event.terminal)
458
+ return event;
459
+ }
460
+ const reconnectDelayMs = Math.min(RUN_WAIT_RECONNECT_INITIAL_DELAY_MS * 2 ** Math.min(reconnectAttempt, 10), RUN_WAIT_RECONNECT_MAX_DELAY_MS);
461
+ reconnectAttempt += 1;
462
+ await waitBeforeRunReconnect(reconnectDelayMs, deadline, options);
463
+ }
464
+ }
465
+ async request(path, method, body, options, acceptedStatuses) {
466
+ const opened = await this.openRequest(path, method, body, options, {
467
+ accept: "application/json"
468
+ });
469
+ try {
470
+ const payload = await readJson(opened.response, options.requestId);
471
+ if (opened.controller.signal.aborted)
472
+ throw opened.controller.signal.reason;
473
+ if (!acceptedStatuses.includes(opened.response.status)) {
474
+ throw apiErrorFromResponse(opened.response, payload, options.requestId);
475
+ }
476
+ if (!isRecord(payload))
477
+ throw invalidResponse(opened.response, options.requestId);
478
+ return payload;
479
+ } catch (error) {
480
+ throw translateTransportError(error, opened.didTimeout(), options);
481
+ } finally {
482
+ opened.cleanup();
483
+ }
484
+ }
485
+ async openRequest(path, method, body, options, requestHeaders) {
486
+ const headers = new Headers({
487
+ Accept: requestHeaders.accept,
488
+ Authorization: `Bearer ${this.token}`,
489
+ "User-Agent": this.userAgent,
490
+ "X-Career-Mentor-SDK-Version": SDK_VERSION
491
+ });
492
+ if (body !== undefined)
493
+ headers.set("Content-Type", "application/json");
494
+ setTransportHeader(headers, "X-Request-Id", options.requestId);
495
+ setTransportHeader(headers, "X-Agent-Session-Id", options.agentSessionId);
496
+ setTransportHeader(headers, "Idempotency-Key", options.idempotencyKey);
497
+ setTransportHeader(headers, "Last-Event-ID", requestHeaders.lastEventId);
498
+ const timeoutMs = requirePositiveTimeout(options.timeoutMs ?? this.timeoutMs);
499
+ const controller = new AbortController;
500
+ let didTimeout = false;
501
+ const forwardAbort = () => controller.abort(options.signal?.reason);
502
+ if (options.signal?.aborted)
503
+ forwardAbort();
504
+ else
505
+ options.signal?.addEventListener("abort", forwardAbort, { once: true });
506
+ const timeout = setTimeout(() => {
507
+ didTimeout = true;
508
+ controller.abort();
509
+ }, timeoutMs);
510
+ const cleanup = () => {
511
+ clearTimeout(timeout);
512
+ options.signal?.removeEventListener("abort", forwardAbort);
513
+ };
514
+ try {
515
+ const response = await this.fetch(`${this.baseUrl}${path}`, {
516
+ method,
517
+ headers,
518
+ signal: controller.signal,
519
+ ...body === undefined ? {} : { body }
520
+ });
521
+ return {
522
+ cleanup,
523
+ controller,
524
+ didTimeout: () => didTimeout,
525
+ response
526
+ };
527
+ } catch (error) {
528
+ cleanup();
529
+ throw translateTransportError(error, didTimeout, options);
530
+ }
531
+ }
532
+ }
533
+
534
+ // src/auth.ts
535
+ import { spawn } from "node:child_process";
536
+ import { constants } from "node:fs";
537
+ import { chmod, lstat, mkdir, open, rename, rm, unlink, writeFile } from "node:fs/promises";
538
+ import { createServer } from "node:http";
539
+ import { homedir } from "node:os";
540
+ import { dirname, join } from "node:path";
541
+ import { createHash, randomBytes } from "node:crypto";
542
+ var CAPABILITY_TOKEN_ENV = "CAREER_MENTOR_CAPABILITY_TOKEN";
543
+ var CAPABILITY_TOKEN_FILE_ENV = "CAREER_MENTOR_CAPABILITY_TOKEN_FILE";
544
+ var AUTH_APP_URL_ENV = "CAREER_MENTOR_AUTH_APP_URL";
545
+ var DEFAULT_AUTH_APP_URL = "https://app.uptomic.com";
546
+ var DEFAULT_AUTH_EXPIRES_IN_DAYS = 7;
547
+ var DEFAULT_AUTH_TIMEOUT_MS = 5 * 60 * 1000;
548
+ var MAX_CREDENTIAL_BYTES = 8192;
549
+ var MAX_SCOPE_COUNT = 50;
550
+ var TOKEN_PATTERN = /^cmcp_[A-Za-z0-9_-]+$/;
551
+ var SCOPE_PATTERN = /^[a-z][a-z0-9_]*:[a-z][a-z0-9_]*$/;
552
+
553
+ class BrowserAuthError extends Error {
554
+ code;
555
+ constructor(code, message) {
556
+ super(message);
557
+ this.name = "BrowserAuthError";
558
+ this.code = code;
559
+ }
560
+ }
561
+ function isNodeError(error) {
562
+ return error instanceof Error && "code" in error;
563
+ }
564
+ function requireCapabilityToken(value) {
565
+ const token = value.trim();
566
+ if (!(token.length <= MAX_CREDENTIAL_BYTES && TOKEN_PATTERN.test(token))) {
567
+ throw new BrowserAuthError("AUTH_CREDENTIAL_INVALID", "Career Mentor capability token is invalid.");
568
+ }
569
+ return token;
570
+ }
571
+ function defaultCredentialFile() {
572
+ return join(homedir(), ".career-mentor", "capability-token");
573
+ }
574
+ function resolveCredentialFile(explicitPath, environmentPath) {
575
+ return explicitPath?.trim() || environmentPath?.trim() || defaultCredentialFile();
576
+ }
577
+ function parseRequestedScopes(value) {
578
+ if (!value?.trim())
579
+ return [];
580
+ const scopes = Array.from(new Set(value.split(/[,\s]+/).map((scope) => scope.trim()).filter(Boolean)));
581
+ if (scopes.length > MAX_SCOPE_COUNT || scopes.some((scope) => !SCOPE_PATTERN.test(scope))) {
582
+ throw new BrowserAuthError("AUTH_INPUT_INVALID", "Requested capability scopes are invalid.");
583
+ }
584
+ return scopes;
585
+ }
586
+ function parseSecureAppUrl(value) {
587
+ let appUrl;
588
+ try {
589
+ appUrl = new URL(value);
590
+ } catch {
591
+ throw new BrowserAuthError("AUTH_INPUT_INVALID", "Career Mentor auth app URL is invalid.");
592
+ }
593
+ const loopbackHost = appUrl.hostname === "localhost" || appUrl.hostname === "127.0.0.1" || appUrl.hostname === "[::1]";
594
+ if (!["http:", "https:"].includes(appUrl.protocol) || appUrl.protocol === "http:" && !loopbackHost || appUrl.username || appUrl.password || appUrl.search || appUrl.hash) {
595
+ throw new BrowserAuthError("AUTH_INPUT_INVALID", "Career Mentor auth app URL must use HTTPS unless it targets local loopback.");
596
+ }
597
+ return appUrl;
598
+ }
599
+ function buildAgentAuthUrl(params) {
600
+ const appUrl = parseSecureAppUrl(params.appUrl);
601
+ if (!(Number.isInteger(params.expiresInDays) && params.expiresInDays >= 1)) {
602
+ throw new BrowserAuthError("AUTH_INPUT_INVALID", "Token expiry must be a positive integer.");
603
+ }
604
+ const url = new URL("/account/agent-auth", appUrl);
605
+ url.searchParams.set("client", params.clientName);
606
+ url.searchParams.set("code_challenge", params.codeChallenge);
607
+ url.searchParams.set("code_challenge_method", "S256");
608
+ url.searchParams.set("expires_in_days", String(params.expiresInDays));
609
+ url.searchParams.set("redirect_uri", params.redirectUri);
610
+ if (params.scopes.length > 0)
611
+ url.searchParams.set("scope", params.scopes.join(" "));
612
+ url.searchParams.set("state", params.state);
613
+ return url.toString();
614
+ }
615
+ async function readManagedCredential(filePath) {
616
+ let handle;
617
+ try {
618
+ const noFollow = process.platform === "win32" ? 0 : constants.O_NOFOLLOW;
619
+ handle = await open(filePath, constants.O_RDONLY | noFollow);
620
+ } catch (error) {
621
+ if (isNodeError(error) && error.code === "ENOENT")
622
+ return;
623
+ throw new BrowserAuthError("AUTH_FILE_INVALID", "Managed credential file could not be opened.");
624
+ }
625
+ try {
626
+ const metadata = await handle.stat();
627
+ if (!metadata.isFile() || metadata.size > MAX_CREDENTIAL_BYTES) {
628
+ throw new BrowserAuthError("AUTH_FILE_INVALID", "Managed credential file is invalid.");
629
+ }
630
+ if (process.platform !== "win32" && (metadata.mode & 63) !== 0) {
631
+ throw new BrowserAuthError("AUTH_FILE_PERMISSIONS", "Managed credential file permissions must be 0600.");
632
+ }
633
+ return requireCapabilityToken(await handle.readFile("utf8"));
634
+ } finally {
635
+ await handle.close();
636
+ }
637
+ }
638
+ async function resolveCredential(params) {
639
+ if (params.flagToken?.trim()) {
640
+ return { source: "--token", token: requireCapabilityToken(params.flagToken) };
641
+ }
642
+ if (params.envToken?.trim()) {
643
+ return {
644
+ source: "CAREER_MENTOR_CAPABILITY_TOKEN",
645
+ token: requireCapabilityToken(params.envToken)
646
+ };
647
+ }
648
+ const token = await readManagedCredential(params.credentialFile);
649
+ return token ? { source: "managed_file", token } : undefined;
650
+ }
651
+ async function writeCredentialFile(filePath, token) {
652
+ const validatedToken = requireCapabilityToken(token);
653
+ const directory = dirname(filePath);
654
+ const temporaryFile = join(directory, `.capability-token.${process.pid}.${randomBytes(8).toString("hex")}.tmp`);
655
+ try {
656
+ await mkdir(directory, { mode: 448, recursive: true });
657
+ await writeFile(temporaryFile, `${validatedToken}
658
+ `, {
659
+ flag: "wx",
660
+ mode: 384
661
+ });
662
+ await chmod(temporaryFile, 384);
663
+ await rename(temporaryFile, filePath);
664
+ await chmod(filePath, 384);
665
+ } catch (error) {
666
+ if (error instanceof BrowserAuthError)
667
+ throw error;
668
+ throw new BrowserAuthError("AUTH_FILE_INVALID", "Managed credential file could not be written.");
669
+ } finally {
670
+ await rm(temporaryFile, { force: true });
671
+ }
672
+ }
673
+ async function logoutCredential(filePath) {
674
+ try {
675
+ const metadata = await lstat(filePath);
676
+ if (metadata.isDirectory()) {
677
+ throw new BrowserAuthError("AUTH_FILE_INVALID", "Managed credential path is not a file.");
678
+ }
679
+ await unlink(filePath);
680
+ return { removed: true };
681
+ } catch (error) {
682
+ if (error instanceof BrowserAuthError)
683
+ throw error;
684
+ if (isNodeError(error) && error.code === "ENOENT")
685
+ return { removed: false };
686
+ throw new BrowserAuthError("AUTH_FILE_INVALID", "Managed credential file could not be removed.");
687
+ }
688
+ }
689
+ function sendHtml(response, status, message) {
690
+ response.writeHead(status, {
691
+ "cache-control": "no-store",
692
+ "content-security-policy": "default-src 'none'; script-src 'unsafe-inline'",
693
+ "content-type": "text/html; charset=utf-8",
694
+ "referrer-policy": "no-referrer",
695
+ "x-content-type-options": "nosniff"
696
+ });
697
+ response.end(`<!doctype html>
698
+ <html lang="en">
699
+ <head>
700
+ <meta charset="utf-8">
701
+ <title>Career Mentor Agent Access</title>
702
+ <script>history.replaceState(null, document.title, location.pathname);</script>
703
+ </head>
704
+ <body>
705
+ <h1>${message}</h1>
706
+ <p>This window can be closed.</p>
707
+ </body>
708
+ </html>`);
709
+ }
710
+ function listen(server, port) {
711
+ return new Promise((resolve, reject) => {
712
+ const onError = () => reject(new BrowserAuthError("AUTH_SERVER_FAILED", "Loopback server failed."));
713
+ server.once("error", onError);
714
+ server.listen(port ?? 0, "127.0.0.1", () => {
715
+ server.off("error", onError);
716
+ resolve();
717
+ });
718
+ });
719
+ }
720
+ function closeServer(server) {
721
+ if (!server.listening)
722
+ return Promise.resolve();
723
+ return new Promise((resolve, reject) => {
724
+ server.close((error) => {
725
+ if (error)
726
+ reject(new BrowserAuthError("AUTH_SERVER_FAILED", "Loopback server failed."));
727
+ else
728
+ resolve();
729
+ });
730
+ });
731
+ }
732
+ async function startLoopbackCallbackServer(params) {
733
+ if (!(params.state && Number.isFinite(params.timeoutMs) && params.timeoutMs > 0)) {
734
+ throw new BrowserAuthError("AUTH_SERVER_FAILED", "Loopback server configuration is invalid.");
735
+ }
736
+ let settled = false;
737
+ let timeout;
738
+ let resolveCallback = () => {
739
+ return;
740
+ };
741
+ let rejectCallback = () => {
742
+ return;
743
+ };
744
+ const waitForCallback = new Promise((resolve, reject) => {
745
+ resolveCallback = resolve;
746
+ rejectCallback = reject;
747
+ });
748
+ const settleSuccess = (token) => {
749
+ if (settled)
750
+ return;
751
+ settled = true;
752
+ if (timeout)
753
+ clearTimeout(timeout);
754
+ resolveCallback(token);
755
+ };
756
+ const settleError = (error) => {
757
+ if (settled)
758
+ return;
759
+ settled = true;
760
+ if (timeout)
761
+ clearTimeout(timeout);
762
+ rejectCallback(error);
763
+ };
764
+ const abort = () => settleError(new BrowserAuthError("AUTH_ABORTED", "Browser authorization was aborted."));
765
+ const server = createServer((request, response) => {
766
+ const requestUrl = new URL(request.url ?? "/", "http://127.0.0.1");
767
+ if (request.method !== "GET" || requestUrl.pathname !== "/callback") {
768
+ sendHtml(response, 404, "Unknown callback path.");
769
+ return;
770
+ }
771
+ if (requestUrl.searchParams.get("error")) {
772
+ sendHtml(response, 400, "Agent token authorization failed.");
773
+ settleError(new BrowserAuthError("AUTH_CALLBACK_FAILED", "Browser authorization failed."));
774
+ return;
775
+ }
776
+ if (requestUrl.searchParams.get("state") !== params.state) {
777
+ sendHtml(response, 400, "Invalid authorization state.");
778
+ settleError(new BrowserAuthError("AUTH_STATE_MISMATCH", "Browser authorization state did not match."));
779
+ return;
780
+ }
781
+ const code = requestUrl.searchParams.get("code");
782
+ if (!(code && /^[A-Za-z0-9_-]{8,512}$/.test(code))) {
783
+ sendHtml(response, 400, "Missing or invalid authorization code.");
784
+ settleError(new BrowserAuthError("AUTH_CALLBACK_FAILED", "Browser authorization code was invalid."));
785
+ return;
786
+ }
787
+ sendHtml(response, 200, "Agent token authorized. You can return to the terminal.");
788
+ settleSuccess({ code });
789
+ });
790
+ try {
791
+ await listen(server, params.port);
792
+ } catch (error) {
793
+ await closeServer(server);
794
+ throw error;
795
+ }
796
+ const address = server.address();
797
+ if (!address || typeof address === "string") {
798
+ await closeServer(server);
799
+ throw new BrowserAuthError("AUTH_SERVER_FAILED", "Loopback callback port was unavailable.");
800
+ }
801
+ timeout = setTimeout(() => settleError(new BrowserAuthError("AUTH_TIMEOUT", "Browser authorization timed out.")), params.timeoutMs);
802
+ if (params.signal?.aborted)
803
+ abort();
804
+ else
805
+ params.signal?.addEventListener("abort", abort, { once: true });
806
+ server.on("error", () => {
807
+ settleError(new BrowserAuthError("AUTH_SERVER_FAILED", "Loopback server failed."));
808
+ });
809
+ return {
810
+ close: async () => {
811
+ if (timeout)
812
+ clearTimeout(timeout);
813
+ params.signal?.removeEventListener("abort", abort);
814
+ await closeServer(server);
815
+ },
816
+ port: address.port,
817
+ redirectUri: `http://127.0.0.1:${address.port}/callback`,
818
+ waitForCallback
819
+ };
820
+ }
821
+ function browserLaunchCommand(url, platform = process.platform) {
822
+ if (platform === "darwin")
823
+ return { command: "open", args: [url] };
824
+ if (platform === "win32") {
825
+ return { command: "rundll32.exe", args: ["url.dll,FileProtocolHandler", url] };
826
+ }
827
+ return { command: "xdg-open", args: [url] };
828
+ }
829
+ async function openUrlInBrowser(url) {
830
+ const { args, command } = browserLaunchCommand(url);
831
+ await new Promise((resolve, reject) => {
832
+ const child = spawn(command, args, { detached: true, stdio: "ignore" });
833
+ child.once("error", () => reject(new BrowserAuthError("AUTH_BROWSER_OPEN_FAILED", "Browser could not be opened.")));
834
+ child.once("spawn", () => {
835
+ child.unref();
836
+ resolve();
837
+ });
838
+ });
839
+ }
840
+ async function exchangeAuthorizationCode(params) {
841
+ const controller = new AbortController;
842
+ let timedOut = false;
843
+ const forwardAbort = () => controller.abort(params.signal?.reason);
844
+ if (params.signal?.aborted)
845
+ forwardAbort();
846
+ else
847
+ params.signal?.addEventListener("abort", forwardAbort, { once: true });
848
+ const timeout = setTimeout(() => {
849
+ timedOut = true;
850
+ controller.abort();
851
+ }, params.timeoutMs);
852
+ try {
853
+ const appUrl = parseSecureAppUrl(params.appUrl);
854
+ const exchangeInput = {
855
+ code: params.code,
856
+ codeVerifier: params.codeVerifier,
857
+ redirectUri: params.redirectUri
858
+ };
859
+ const response = await params.fetch(new URL("/api/v1/auth/agent/exchange", appUrl), {
860
+ method: "POST",
861
+ headers: { Accept: "application/json", "Content-Type": "application/json" },
862
+ body: JSON.stringify(exchangeInput),
863
+ signal: controller.signal
864
+ });
865
+ if (!response.ok) {
866
+ if (response.status === 408 || response.status === 429 || response.status >= 500) {
867
+ throw new BrowserAuthError("AUTH_SERVER_FAILED", "Authorization exchange is temporarily unavailable.");
868
+ }
869
+ if (response.status === 422) {
870
+ throw new BrowserAuthError("AUTH_INPUT_INVALID", "Authorization exchange input was invalid.");
871
+ }
872
+ throw new BrowserAuthError("AUTH_CALLBACK_FAILED", "Authorization exchange was rejected.");
873
+ }
874
+ let payload;
875
+ try {
876
+ payload = await response.json();
877
+ } catch {
878
+ throw new BrowserAuthError("AUTH_CALLBACK_FAILED", "Authorization exchange was invalid.");
879
+ }
880
+ if (typeof payload !== "object" || payload === null || Array.isArray(payload) || !Array.isArray(payload["scopes"])) {
881
+ throw new BrowserAuthError("AUTH_CALLBACK_FAILED", "Authorization exchange was invalid.");
882
+ }
883
+ const record = payload;
884
+ const scopes = record["scopes"];
885
+ const expiresAt = record["expiresAt"];
886
+ if (typeof record["token"] !== "string" || typeof record["tokenPrefix"] !== "string" || !record["token"].startsWith(`${record["tokenPrefix"]}_`) || !Array.isArray(scopes) || scopes.some((scope) => typeof scope !== "string") || typeof expiresAt !== "string" || Number.isNaN(Date.parse(expiresAt))) {
887
+ throw new BrowserAuthError("AUTH_CALLBACK_FAILED", "Authorization exchange was invalid.");
888
+ }
889
+ return {
890
+ expiresAt,
891
+ scopes,
892
+ token: requireCapabilityToken(record["token"]),
893
+ tokenPrefix: record["tokenPrefix"]
894
+ };
895
+ } catch (error) {
896
+ if (timedOut) {
897
+ throw new BrowserAuthError("AUTH_TIMEOUT", "Browser authorization timed out.");
898
+ }
899
+ if (params.signal?.aborted) {
900
+ throw new BrowserAuthError("AUTH_ABORTED", "Browser authorization was aborted.");
901
+ }
902
+ if (error instanceof BrowserAuthError)
903
+ throw error;
904
+ throw new BrowserAuthError("AUTH_SERVER_FAILED", "Authorization exchange failed.");
905
+ } finally {
906
+ clearTimeout(timeout);
907
+ params.signal?.removeEventListener("abort", forwardAbort);
908
+ }
909
+ }
910
+ async function runBrowserAuthLogin(options, overrides = {}) {
911
+ const startedAt = Date.now();
912
+ const state = (overrides.generateState ?? (() => randomBytes(32).toString("base64url")))();
913
+ const codeVerifier = (overrides.generateCodeVerifier ?? (() => randomBytes(48).toString("base64url")))();
914
+ if (!/^[A-Za-z0-9._~-]{43,128}$/.test(codeVerifier)) {
915
+ throw new BrowserAuthError("AUTH_INPUT_INVALID", "PKCE code verifier is invalid.");
916
+ }
917
+ const codeChallenge = createHash("sha256").update(codeVerifier).digest("base64url");
918
+ const callback = await startLoopbackCallbackServer({
919
+ ...options.port !== undefined ? { port: options.port } : {},
920
+ ...options.signal ? { signal: options.signal } : {},
921
+ state,
922
+ timeoutMs: options.timeoutMs
923
+ });
924
+ try {
925
+ const authorizationUrl = buildAgentAuthUrl({
926
+ appUrl: options.appUrl,
927
+ clientName: options.clientName,
928
+ codeChallenge,
929
+ expiresInDays: options.expiresInDays,
930
+ redirectUri: callback.redirectUri,
931
+ scopes: options.scopes,
932
+ state
933
+ });
934
+ await options.onLaunch({
935
+ authorizationUrl,
936
+ openBrowser: options.openBrowser,
937
+ redirectUri: callback.redirectUri
938
+ });
939
+ if (options.openBrowser)
940
+ await (overrides.openUrl ?? openUrlInBrowser)(authorizationUrl);
941
+ const captured = await callback.waitForCallback;
942
+ const remainingTimeoutMs = Math.max(1, options.timeoutMs - (Date.now() - startedAt));
943
+ const exchanged = await exchangeAuthorizationCode({
944
+ appUrl: options.appUrl,
945
+ code: captured.code,
946
+ codeVerifier,
947
+ fetch: overrides.fetch ?? globalThis.fetch,
948
+ redirectUri: callback.redirectUri,
949
+ ...options.signal ? { signal: options.signal } : {},
950
+ timeoutMs: remainingTimeoutMs
951
+ });
952
+ await writeCredentialFile(options.credentialFile, exchanged.token);
953
+ return {
954
+ credentialFile: options.credentialFile,
955
+ expiresAt: exchanged.expiresAt,
956
+ scopes: [...exchanged.scopes]
957
+ };
958
+ } finally {
959
+ await callback.close();
960
+ }
961
+ }
962
+
963
+ // src/cli.ts
964
+ var CLI_VERSION = "0.1.0";
965
+ var DEFAULT_API_URL = "https://app.uptomic.com";
966
+ var CLI_EXIT_CODE = Object.freeze({
967
+ SUCCESS: 0,
968
+ INTERNAL: 1,
969
+ USAGE: 2,
970
+ AUTH: 3,
971
+ NOT_FOUND: 4,
972
+ CONFLICT_OR_LIMIT: 5,
973
+ UNAVAILABLE: 6,
974
+ API_ERROR: 7,
975
+ RUN_FAILED: 8,
976
+ RUN_CANCELLED: 9,
977
+ INTERRUPTED: 130
978
+ });
979
+ var TERMINAL_RUN_STATUS = {
980
+ SUCCEEDED: "succeeded",
981
+ FAILED: "failed",
982
+ CANCELLED: "cancelled"
983
+ };
984
+ var CONTROL_CHARACTERS2 = /[\u0000-\u001f\u007f]/g;
985
+ var CONTROL_CHARACTER2 = /[\u0000-\u001f\u007f]/;
986
+ var MAX_SAFE_MESSAGE_LENGTH = 500;
987
+ var MAX_TIMEOUT_MS = 2147483647;
988
+ var MAX_ARTIFACT_UPLOAD_BYTES = 10485760;
989
+ var ARTIFACT_CONTENT_TYPES = new Map([
990
+ [".doc", "application/msword"],
991
+ [".docx", "application/vnd.openxmlformats-officedocument.wordprocessingml.document"],
992
+ [".jpeg", "image/jpeg"],
993
+ [".jpg", "image/jpeg"],
994
+ [".md", "text/markdown"],
995
+ [".pdf", "application/pdf"],
996
+ [".png", "image/png"],
997
+ [".txt", "text/plain"],
998
+ [".webp", "image/webp"]
999
+ ]);
1000
+ var ARTIFACT_TYPES = new Set([
1001
+ "resume",
1002
+ "certificate",
1003
+ "portfolio",
1004
+ "recommendation",
1005
+ "performance_review",
1006
+ "award",
1007
+ "course_completion",
1008
+ "project",
1009
+ "publication",
1010
+ "other"
1011
+ ]);
1012
+ var VALUE_FLAGS = new Set([
1013
+ "agent-session-id",
1014
+ "after",
1015
+ "app-url",
1016
+ "client",
1017
+ "credential-file",
1018
+ "endpoint",
1019
+ "expires-in-days",
1020
+ "idempotency-key",
1021
+ "input",
1022
+ "input-file",
1023
+ "last-event-id",
1024
+ "limit",
1025
+ "name",
1026
+ "port",
1027
+ "request-id",
1028
+ "scopes",
1029
+ "timeout-ms",
1030
+ "timeout-seconds",
1031
+ "token",
1032
+ "type"
1033
+ ]);
1034
+ var BOOLEAN_FLAGS = new Set(["create", "help", "no-open", "version"]);
1035
+ var TRANSPORT_FLAGS = new Set([
1036
+ "agent-session-id",
1037
+ "credential-file",
1038
+ "endpoint",
1039
+ "request-id",
1040
+ "timeout-ms",
1041
+ "token"
1042
+ ]);
1043
+
1044
+ class CliUsageError extends Error {
1045
+ code;
1046
+ constructor(code, message) {
1047
+ super(message);
1048
+ this.name = "CliUsageError";
1049
+ this.code = code;
1050
+ }
1051
+ }
1052
+ function usageError(code, message) {
1053
+ throw new CliUsageError(code, message);
1054
+ }
1055
+ function parseFlagToken(argv, index) {
1056
+ const token = argv[index];
1057
+ if (!token?.startsWith("--") || token === "--") {
1058
+ usageError("INVALID_FLAG", "Expected a long-form CLI flag.");
1059
+ }
1060
+ const separator = token.indexOf("=");
1061
+ const name = token.slice(2, separator === -1 ? undefined : separator);
1062
+ const inlineValue = separator === -1 ? undefined : token.slice(separator + 1);
1063
+ if (!(VALUE_FLAGS.has(name) || BOOLEAN_FLAGS.has(name))) {
1064
+ usageError("UNKNOWN_FLAG", `Unknown flag --${name}.`);
1065
+ }
1066
+ if (BOOLEAN_FLAGS.has(name)) {
1067
+ if (inlineValue !== undefined) {
1068
+ usageError("INVALID_FLAG", `Flag --${name} does not accept a value.`);
1069
+ }
1070
+ return { consumed: 1, name, value: true };
1071
+ }
1072
+ const nextValue = inlineValue ?? argv[index + 1];
1073
+ if (nextValue === undefined || inlineValue === undefined && nextValue.startsWith("--")) {
1074
+ usageError("MISSING_FLAG_VALUE", `Flag --${name} requires a value.`);
1075
+ }
1076
+ return { consumed: inlineValue === undefined ? 2 : 1, name, value: nextValue };
1077
+ }
1078
+ function parseArgs(argv) {
1079
+ const flags = {};
1080
+ const positionals = [];
1081
+ let parseFlags = true;
1082
+ for (let index = 0;index < argv.length; ) {
1083
+ const token = argv[index];
1084
+ if (token === undefined)
1085
+ break;
1086
+ if (parseFlags && token === "--") {
1087
+ parseFlags = false;
1088
+ index += 1;
1089
+ continue;
1090
+ }
1091
+ if (parseFlags && token.startsWith("--")) {
1092
+ const parsed = parseFlagToken(argv, index);
1093
+ if (flags[parsed.name] !== undefined) {
1094
+ usageError("DUPLICATE_FLAG", `Flag --${parsed.name} may only be provided once.`);
1095
+ }
1096
+ flags[parsed.name] = parsed.value;
1097
+ index += parsed.consumed;
1098
+ continue;
1099
+ }
1100
+ positionals.push(token);
1101
+ index += 1;
1102
+ }
1103
+ return { flags, positionals };
1104
+ }
1105
+ function stringFlag(flags, name) {
1106
+ const value = flags[name];
1107
+ return typeof value === "string" ? value : undefined;
1108
+ }
1109
+ function cleanOptional(value) {
1110
+ const normalized = value?.trim();
1111
+ return normalized ? normalized : undefined;
1112
+ }
1113
+ function normalizeEndpoint(value) {
1114
+ let endpoint;
1115
+ try {
1116
+ endpoint = new URL(value);
1117
+ } catch {
1118
+ usageError("INVALID_ENDPOINT", "The API endpoint must be an absolute HTTP(S) URL.");
1119
+ }
1120
+ if (!["http:", "https:"].includes(endpoint.protocol)) {
1121
+ usageError("INVALID_ENDPOINT", "The API endpoint must use HTTP or HTTPS.");
1122
+ }
1123
+ if (endpoint.username || endpoint.password || endpoint.search || endpoint.hash) {
1124
+ usageError("INVALID_ENDPOINT", "The API endpoint must not include credentials, query, or hash.");
1125
+ }
1126
+ endpoint.pathname = endpoint.pathname.replace(/\/+$/, "");
1127
+ return endpoint.toString().replace(/\/$/, "");
1128
+ }
1129
+ function parseTimeout(value) {
1130
+ if (value === undefined)
1131
+ return 30000;
1132
+ if (!/^\d+$/.test(value)) {
1133
+ usageError("INVALID_TIMEOUT", "Timeout must be a positive integer in milliseconds.");
1134
+ }
1135
+ const timeoutMs = Number(value);
1136
+ if (!(timeoutMs > 0 && timeoutMs <= MAX_TIMEOUT_MS)) {
1137
+ usageError("INVALID_TIMEOUT", `Timeout must be between 1 and ${MAX_TIMEOUT_MS} milliseconds.`);
1138
+ }
1139
+ return timeoutMs;
1140
+ }
1141
+ function parseIntegerFlag(flags, name, defaultValue, minimum, maximum) {
1142
+ const rawValue = cleanOptional(stringFlag(flags, name));
1143
+ if (rawValue === undefined)
1144
+ return defaultValue;
1145
+ if (!/^\d+$/.test(rawValue)) {
1146
+ usageError("INVALID_FLAG_VALUE", `Flag --${name} must be an integer.`);
1147
+ }
1148
+ const value = Number(rawValue);
1149
+ if (!(value >= minimum && value <= maximum)) {
1150
+ usageError("INVALID_FLAG_VALUE", `Flag --${name} must be between ${minimum} and ${maximum}.`);
1151
+ }
1152
+ return value;
1153
+ }
1154
+ function authClientName(value) {
1155
+ const clientName = (value ?? "Career Mentor CLI").replace(CONTROL_CHARACTERS2, "").replace(/\s+/g, " ").trim();
1156
+ return (clientName || "Career Mentor CLI").slice(0, 80);
1157
+ }
1158
+ async function resolveConfig(flags, env) {
1159
+ const flagToken = cleanOptional(stringFlag(flags, "token"));
1160
+ const envToken = cleanOptional(env[CAPABILITY_TOKEN_ENV]);
1161
+ const credentialFile = resolveCredentialFile(cleanOptional(stringFlag(flags, "credential-file")), cleanOptional(env[CAPABILITY_TOKEN_FILE_ENV]));
1162
+ const credential = await resolveCredential({
1163
+ credentialFile,
1164
+ ...envToken ? { envToken } : {},
1165
+ ...flagToken ? { flagToken } : {}
1166
+ });
1167
+ const endpoint = normalizeEndpoint(cleanOptional(stringFlag(flags, "endpoint")) ?? cleanOptional(env["CAREER_MENTOR_API_URL"]) ?? DEFAULT_API_URL);
1168
+ return {
1169
+ agentSessionId: cleanOptional(stringFlag(flags, "agent-session-id")) ?? cleanOptional(env["CAREER_MENTOR_AGENT_SESSION_ID"]),
1170
+ credentialFile,
1171
+ endpoint,
1172
+ idempotencyKey: cleanOptional(stringFlag(flags, "idempotency-key")) ?? cleanOptional(env["CAREER_MENTOR_IDEMPOTENCY_KEY"]),
1173
+ requestId: cleanOptional(stringFlag(flags, "request-id")) ?? cleanOptional(env["CAREER_MENTOR_REQUEST_ID"]),
1174
+ timeoutMs: parseTimeout(cleanOptional(stringFlag(flags, "timeout-ms")) ?? cleanOptional(env["CAREER_MENTOR_TIMEOUT_MS"])),
1175
+ token: credential?.token,
1176
+ tokenSource: credential?.source ?? null
1177
+ };
1178
+ }
1179
+ function requireToken2(config) {
1180
+ if (!config.token) {
1181
+ usageError("MISSING_CAPABILITY_TOKEN", "Run cm auth login, set CAREER_MENTOR_CAPABILITY_TOKEN, or pass --token.");
1182
+ }
1183
+ return config.token;
1184
+ }
1185
+ function validateFlags(flags, allowed) {
1186
+ for (const name of Object.keys(flags)) {
1187
+ if (!(allowed.has(name) || name === "help" || name === "version")) {
1188
+ usageError("UNSUPPORTED_FLAG", `Flag --${name} is not valid for this command.`);
1189
+ }
1190
+ }
1191
+ }
1192
+ function expectPositionals(positionals, expected, usage) {
1193
+ if (positionals.length !== expected) {
1194
+ usageError("INVALID_COMMAND", `Usage: ${usage}`);
1195
+ }
1196
+ }
1197
+ function makeClient(config, fetch) {
1198
+ return new CareerMentorClient({
1199
+ baseUrl: config.endpoint,
1200
+ token: requireToken2(config),
1201
+ timeoutMs: config.timeoutMs,
1202
+ userAgent: `career-mentor-cli/${CLI_VERSION}`,
1203
+ ...fetch ? { fetch } : {}
1204
+ });
1205
+ }
1206
+ function requestOptions(config, signal) {
1207
+ return {
1208
+ timeoutMs: config.timeoutMs,
1209
+ ...config.requestId ? { requestId: config.requestId } : {},
1210
+ ...config.agentSessionId ? { agentSessionId: config.agentSessionId } : {},
1211
+ ...signal ? { signal } : {}
1212
+ };
1213
+ }
1214
+ async function defaultReadStdin() {
1215
+ const chunks = [];
1216
+ for await (const chunk of process.stdin)
1217
+ chunks.push(Buffer.from(chunk));
1218
+ return Buffer.concat(chunks).toString("utf8");
1219
+ }
1220
+ function parseInputJson(value) {
1221
+ let parsed;
1222
+ try {
1223
+ parsed = JSON.parse(value.replace(/^\uFEFF/, ""));
1224
+ } catch {
1225
+ usageError("INVALID_INPUT_JSON", "Capability input must be valid JSON.");
1226
+ }
1227
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
1228
+ usageError("INVALID_INPUT_JSON", "Capability input must be a JSON object.");
1229
+ }
1230
+ return parsed;
1231
+ }
1232
+ async function readCapabilityInput(flags, readFile, readStdin) {
1233
+ const inline = stringFlag(flags, "input");
1234
+ const inputFile = stringFlag(flags, "input-file");
1235
+ if (inline !== undefined && inputFile !== undefined) {
1236
+ usageError("DUPLICATE_INPUT", "Use either --input or --input-file, not both.");
1237
+ }
1238
+ if (inline !== undefined)
1239
+ return parseInputJson(inline);
1240
+ if (inputFile === undefined)
1241
+ return {};
1242
+ let contents;
1243
+ try {
1244
+ contents = inputFile === "-" ? await readStdin() : await readFile(inputFile);
1245
+ } catch {
1246
+ usageError("INPUT_READ_FAILED", "Could not read the capability input file.");
1247
+ }
1248
+ return parseInputJson(contents);
1249
+ }
1250
+ function sanitizeMessage(message, secrets) {
1251
+ let sanitized = message.replace(CONTROL_CHARACTERS2, " ").trim();
1252
+ for (const secret of [...secrets].filter(Boolean).sort((a, b) => b.length - a.length)) {
1253
+ sanitized = sanitized.split(secret).join("[REDACTED]");
1254
+ }
1255
+ return sanitized.slice(0, MAX_SAFE_MESSAGE_LENGTH) || "Career Mentor CLI request failed.";
1256
+ }
1257
+ function apiExitCode(status) {
1258
+ if (status === 400 || status === 422)
1259
+ return CLI_EXIT_CODE.USAGE;
1260
+ if (status === 401 || status === 403)
1261
+ return CLI_EXIT_CODE.AUTH;
1262
+ if (status === 404)
1263
+ return CLI_EXIT_CODE.NOT_FOUND;
1264
+ if (status === 409 || status === 429)
1265
+ return CLI_EXIT_CODE.CONFLICT_OR_LIMIT;
1266
+ if (status === 408 || status >= 500)
1267
+ return CLI_EXIT_CODE.UNAVAILABLE;
1268
+ return CLI_EXIT_CODE.API_ERROR;
1269
+ }
1270
+ function terminalRunExitCode(event) {
1271
+ if (event.status === TERMINAL_RUN_STATUS.SUCCEEDED)
1272
+ return CLI_EXIT_CODE.SUCCESS;
1273
+ if (event.status === TERMINAL_RUN_STATUS.FAILED)
1274
+ return CLI_EXIT_CODE.RUN_FAILED;
1275
+ if (event.status === TERMINAL_RUN_STATUS.CANCELLED)
1276
+ return CLI_EXIT_CODE.RUN_CANCELLED;
1277
+ throw new CareerMentorError("Career Mentor returned an invalid terminal run event.", {
1278
+ code: "INVALID_RESPONSE"
1279
+ });
1280
+ }
1281
+ function mapError(error, secrets) {
1282
+ if (error instanceof CliUsageError) {
1283
+ return {
1284
+ code: error.code,
1285
+ exitCode: CLI_EXIT_CODE.USAGE,
1286
+ message: sanitizeMessage(error.message, secrets)
1287
+ };
1288
+ }
1289
+ if (error instanceof BrowserAuthError) {
1290
+ const exitCode = error.code === "AUTH_ABORTED" ? CLI_EXIT_CODE.INTERRUPTED : error.code === "AUTH_TIMEOUT" || error.code === "AUTH_BROWSER_OPEN_FAILED" || error.code === "AUTH_SERVER_FAILED" ? CLI_EXIT_CODE.UNAVAILABLE : error.code === "AUTH_STATE_MISMATCH" || error.code === "AUTH_CALLBACK_FAILED" ? CLI_EXIT_CODE.AUTH : CLI_EXIT_CODE.USAGE;
1291
+ return {
1292
+ code: error.code,
1293
+ exitCode,
1294
+ message: sanitizeMessage(error.message, secrets)
1295
+ };
1296
+ }
1297
+ if (error instanceof CareerMentorRequestError) {
1298
+ const exitCode = error.code === "REQUEST_ABORTED" ? CLI_EXIT_CODE.INTERRUPTED : error.code === "NETWORK_ERROR" || error.code === "TIMEOUT" ? CLI_EXIT_CODE.UNAVAILABLE : CLI_EXIT_CODE.USAGE;
1299
+ return {
1300
+ code: error.code,
1301
+ exitCode,
1302
+ message: sanitizeMessage(error.message, secrets),
1303
+ ...error.requestId ? { requestId: error.requestId } : {}
1304
+ };
1305
+ }
1306
+ if (error instanceof CareerMentorApiError) {
1307
+ return {
1308
+ code: error.code,
1309
+ exitCode: apiExitCode(error.status),
1310
+ message: sanitizeMessage(error.message, secrets),
1311
+ status: error.status,
1312
+ ...error.requestId ? { requestId: error.requestId } : {},
1313
+ ...error.retryAfter !== undefined ? { retryAfter: error.retryAfter } : {}
1314
+ };
1315
+ }
1316
+ if (error instanceof CareerMentorError) {
1317
+ return {
1318
+ code: error.code,
1319
+ exitCode: CLI_EXIT_CODE.API_ERROR,
1320
+ message: sanitizeMessage(error.message, secrets),
1321
+ ...error.requestId ? { requestId: error.requestId } : {}
1322
+ };
1323
+ }
1324
+ return {
1325
+ code: "CLI_INTERNAL_ERROR",
1326
+ exitCode: CLI_EXIT_CODE.INTERNAL,
1327
+ message: "Career Mentor CLI failed."
1328
+ };
1329
+ }
1330
+ function helpDocument() {
1331
+ return {
1332
+ cli: "career-mentor",
1333
+ version: CLI_VERSION,
1334
+ sdkVersion: SDK_VERSION,
1335
+ usage: "cm <command> [arguments] [flags]",
1336
+ commands: {
1337
+ "auth login": {
1338
+ usage: "cm auth login [--no-open] [--scopes <csv>] [--expires-in-days <days>]",
1339
+ description: "Authorize and store a scoped user token through a loopback browser handoff."
1340
+ },
1341
+ "auth status": {
1342
+ usage: "cm auth status",
1343
+ description: "Show credential source and status without exposing the token."
1344
+ },
1345
+ "auth logout": {
1346
+ usage: "cm auth logout",
1347
+ description: "Delete only the locally managed credential file."
1348
+ },
1349
+ doctor: { usage: "cm doctor", description: "Verify configuration and hosted API access." },
1350
+ "capabilities list": {
1351
+ usage: "cm capabilities list",
1352
+ description: "List capabilities available to the current token."
1353
+ },
1354
+ "capabilities describe": {
1355
+ usage: "cm capabilities describe <name>",
1356
+ description: "Return the exact manifest and JSON schemas for one capability."
1357
+ },
1358
+ invoke: {
1359
+ usage: "cm invoke <name> [--input <json> | --input-file <path|->]",
1360
+ description: "Invoke a published capability without polling."
1361
+ },
1362
+ "artifacts upload": {
1363
+ usage: "cm artifacts upload <path> [--create --type <artifact-type> [--name <name>]]",
1364
+ description: "Upload and register a local Career Vault file; create and analyze an artifact only when --create is explicit."
1365
+ },
1366
+ "photos list": {
1367
+ usage: "cm photos list",
1368
+ description: "List current-user profile photos in display order."
1369
+ },
1370
+ "photos add": {
1371
+ usage: "cm photos add <registered-file-id>",
1372
+ description: "Attach an already registered current-user image file."
1373
+ },
1374
+ "photos default": {
1375
+ usage: "cm photos default <photo-id>",
1376
+ description: "Set one owned profile photo as default."
1377
+ },
1378
+ "photos reorder": {
1379
+ usage: "cm photos reorder <photo-id> [photo-id...]",
1380
+ description: "Replace the complete owned photo order."
1381
+ },
1382
+ "photos delete": {
1383
+ usage: "cm photos delete <photo-id>",
1384
+ description: "Prepare reviewed profile-photo deletion; this command never auto-confirms."
1385
+ },
1386
+ "files delete": {
1387
+ usage: "cm files delete <registered-file-id>",
1388
+ description: "Prepare reviewed registered-file deletion only when it has no authoritative references."
1389
+ },
1390
+ "runs get": {
1391
+ usage: "cm runs get <run-id>",
1392
+ description: "Get current status for a user-owned asynchronous run."
1393
+ },
1394
+ "runs events": {
1395
+ usage: "cm runs events <run-id> [--after <cursor>] [--limit <count>]",
1396
+ description: "Replay a bounded page of run events."
1397
+ },
1398
+ "runs wait": {
1399
+ usage: "cm runs wait <run-id> [--after <cursor>] [--last-event-id <cursor>] [--limit <count>]",
1400
+ description: "Reconnect across bounded streams, print one terminal event, and return its outcome exit code."
1401
+ },
1402
+ "runs watch": {
1403
+ usage: "cm runs watch <run-id> [--after <cursor>] [--last-event-id <cursor>] [--limit <count>]",
1404
+ description: "Subscribe to run events as JSON Lines until the SSE session closes."
1405
+ }
1406
+ },
1407
+ flags: {
1408
+ "--endpoint": "Career Mentor origin; defaults to CAREER_MENTOR_API_URL or production.",
1409
+ "--token": "Capability token; prefer CAREER_MENTOR_CAPABILITY_TOKEN to avoid shell history.",
1410
+ "--timeout-ms": "Positive request timeout; runs wait applies one overall budget.",
1411
+ "--request-id": "Request correlation ID.",
1412
+ "--agent-session-id": "Agent session correlation ID.",
1413
+ "--idempotency-key": "Invocation idempotency key.",
1414
+ "--input": "Inline capability input JSON object.",
1415
+ "--input-file": "Capability input JSON file; use - for stdin.",
1416
+ "--create": "Create and analyze a Career Vault artifact after file registration.",
1417
+ "--type": "Artifact type required with --create.",
1418
+ "--name": "Artifact display name used with --create; defaults to the filename.",
1419
+ "--after": "Replay events strictly after a Redis stream cursor.",
1420
+ "--last-event-id": "SSE reconnect cursor; takes precedence over --after.",
1421
+ "--limit": "Run event page size from 1 to 1000.",
1422
+ "--credential-file": "Managed token file; defaults to ~/.career-mentor/capability-token.",
1423
+ "--app-url": "Career Mentor browser authorization origin.",
1424
+ "--scopes": "Explicit comma- or whitespace-separated token scopes.",
1425
+ "--expires-in-days": "Requested browser-auth token lifetime.",
1426
+ "--timeout-seconds": "Bounded browser callback wait.",
1427
+ "--no-open": "Print the authorization URL diagnostic without opening a browser."
1428
+ },
1429
+ environment: {
1430
+ CAREER_MENTOR_API_URL: "Hosted API origin.",
1431
+ CAREER_MENTOR_CAPABILITY_TOKEN: "Managed user capability token.",
1432
+ CAREER_MENTOR_CAPABILITY_TOKEN_FILE: "Managed capability token file.",
1433
+ CAREER_MENTOR_AUTH_APP_URL: "Browser authorization origin.",
1434
+ CAREER_MENTOR_TIMEOUT_MS: "Default request timeout in milliseconds.",
1435
+ CAREER_MENTOR_REQUEST_ID: "Default request correlation ID.",
1436
+ CAREER_MENTOR_AGENT_SESSION_ID: "Default agent session correlation ID.",
1437
+ CAREER_MENTOR_IDEMPOTENCY_KEY: "Default invocation idempotency key."
1438
+ },
1439
+ exitCodes: {
1440
+ [CLI_EXIT_CODE.SUCCESS]: "success",
1441
+ [CLI_EXIT_CODE.INTERNAL]: "unexpected_internal_error",
1442
+ [CLI_EXIT_CODE.USAGE]: "usage_or_input",
1443
+ [CLI_EXIT_CODE.AUTH]: "authentication_or_authorization",
1444
+ [CLI_EXIT_CODE.NOT_FOUND]: "not_found",
1445
+ [CLI_EXIT_CODE.CONFLICT_OR_LIMIT]: "conflict_or_rate_limit",
1446
+ [CLI_EXIT_CODE.UNAVAILABLE]: "network_timeout_or_service_unavailable",
1447
+ [CLI_EXIT_CODE.API_ERROR]: "other_api_error",
1448
+ [CLI_EXIT_CODE.RUN_FAILED]: "capability_run_failed",
1449
+ [CLI_EXIT_CODE.RUN_CANCELLED]: "capability_run_cancelled",
1450
+ [CLI_EXIT_CODE.INTERRUPTED]: "interrupted"
1451
+ }
1452
+ };
1453
+ }
1454
+ function writeJson(write, value) {
1455
+ write(`${JSON.stringify(value)}
1456
+ `);
1457
+ }
1458
+ async function invokePublishedCapability(client, name, input, options) {
1459
+ return await client.invoke(name, input, options);
1460
+ }
1461
+ function isRecord2(value) {
1462
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1463
+ }
1464
+ function capabilityResultData(value, capabilityName) {
1465
+ const result = isRecord2(value) ? value["result"] : undefined;
1466
+ const data = isRecord2(result) ? result["data"] : undefined;
1467
+ if (!isRecord2(data)) {
1468
+ throw new CareerMentorError(`Career Mentor returned invalid ${capabilityName} data.`, {
1469
+ code: "INVALID_RESPONSE"
1470
+ });
1471
+ }
1472
+ return data;
1473
+ }
1474
+ function artifactContentType(filePath) {
1475
+ const extension = extname(filePath).toLowerCase();
1476
+ const contentType = ARTIFACT_CONTENT_TYPES.get(extension);
1477
+ if (!contentType) {
1478
+ usageError("UNSUPPORTED_FILE_TYPE", "Supported artifact files are PDF, DOC, DOCX, JPEG, PNG, WebP, Markdown, and plain text.");
1479
+ }
1480
+ return contentType;
1481
+ }
1482
+ function artifactType(flags) {
1483
+ const type = cleanOptional(stringFlag(flags, "type"));
1484
+ if (type !== undefined && !ARTIFACT_TYPES.has(type)) {
1485
+ usageError("INVALID_ARTIFACT_TYPE", `Unsupported artifact type: ${type}.`);
1486
+ }
1487
+ return type;
1488
+ }
1489
+ function isLoopbackHost(hostname) {
1490
+ return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1";
1491
+ }
1492
+ function parseUploadAuthorization(response, expected) {
1493
+ const data = capabilityResultData(response, "upload authorization");
1494
+ const requiredHeaders = data["requiredHeaders"];
1495
+ const uploadUrl = data["uploadUrl"];
1496
+ const s3Key = data["s3Key"];
1497
+ if (typeof uploadUrl !== "string" || typeof s3Key !== "string" || !isRecord2(requiredHeaders) || requiredHeaders["contentType"] !== expected.contentType || requiredHeaders["contentLength"] !== expected.size) {
1498
+ throw new CareerMentorError("Career Mentor returned invalid upload authorization data.", {
1499
+ code: "INVALID_RESPONSE"
1500
+ });
1501
+ }
1502
+ let parsedUrl;
1503
+ try {
1504
+ parsedUrl = new URL(uploadUrl);
1505
+ } catch {
1506
+ throw new CareerMentorError("Career Mentor returned an invalid upload URL.", {
1507
+ code: "INVALID_RESPONSE"
1508
+ });
1509
+ }
1510
+ if (parsedUrl.username || parsedUrl.password || parsedUrl.protocol !== "https:" && !(parsedUrl.protocol === "http:" && isLoopbackHost(parsedUrl.hostname))) {
1511
+ throw new CareerMentorError("Career Mentor returned an unsafe upload URL.", {
1512
+ code: "INVALID_RESPONSE"
1513
+ });
1514
+ }
1515
+ return { s3Key, uploadUrl: parsedUrl.toString() };
1516
+ }
1517
+ async function putArtifactBytes(params) {
1518
+ const controller = new AbortController;
1519
+ let didTimeout = false;
1520
+ const forwardAbort = () => controller.abort(params.signal?.reason);
1521
+ if (params.signal?.aborted)
1522
+ forwardAbort();
1523
+ else
1524
+ params.signal?.addEventListener("abort", forwardAbort, { once: true });
1525
+ const timeout = setTimeout(() => {
1526
+ didTimeout = true;
1527
+ controller.abort();
1528
+ }, params.timeoutMs);
1529
+ const requestBody = new ArrayBuffer(params.bytes.byteLength);
1530
+ new Uint8Array(requestBody).set(params.bytes);
1531
+ try {
1532
+ const response = await params.fetch(params.uploadUrl, {
1533
+ body: requestBody,
1534
+ headers: {
1535
+ "Content-Length": String(params.bytes.byteLength),
1536
+ "Content-Type": params.contentType
1537
+ },
1538
+ method: "PUT",
1539
+ redirect: "error",
1540
+ signal: controller.signal
1541
+ });
1542
+ if (!response.ok) {
1543
+ throw new CareerMentorApiError("Artifact byte upload failed.", {
1544
+ code: "ARTIFACT_UPLOAD_FAILED",
1545
+ status: response.status
1546
+ });
1547
+ }
1548
+ } catch (error) {
1549
+ if (didTimeout) {
1550
+ throw new CareerMentorRequestError("Artifact byte upload timed out.", { code: "TIMEOUT" });
1551
+ }
1552
+ if (params.signal?.aborted) {
1553
+ throw new CareerMentorRequestError("Artifact byte upload was aborted.", {
1554
+ code: "REQUEST_ABORTED"
1555
+ });
1556
+ }
1557
+ if (error instanceof CareerMentorError)
1558
+ throw error;
1559
+ throw new CareerMentorRequestError("Artifact byte upload failed.", { code: "NETWORK_ERROR" });
1560
+ } finally {
1561
+ clearTimeout(timeout);
1562
+ params.signal?.removeEventListener("abort", forwardAbort);
1563
+ }
1564
+ }
1565
+ async function runCli(argv, options = {}) {
1566
+ const stdout = options.stdout ?? ((text) => void process.stdout.write(text));
1567
+ const stderr = options.stderr ?? ((text) => void process.stderr.write(text));
1568
+ const env = options.env ?? process.env;
1569
+ let secrets = [];
1570
+ try {
1571
+ const parsed = parseArgs(argv);
1572
+ if (parsed.flags["version"] === true) {
1573
+ writeJson(stdout, { cli: "career-mentor", version: CLI_VERSION });
1574
+ return CLI_EXIT_CODE.SUCCESS;
1575
+ }
1576
+ if (parsed.flags["help"] === true || parsed.positionals[0] === "help" || parsed.positionals.length === 0) {
1577
+ writeJson(stdout, helpDocument());
1578
+ return CLI_EXIT_CODE.SUCCESS;
1579
+ }
1580
+ const command = parsed.positionals[0];
1581
+ const subcommand = parsed.positionals[1];
1582
+ if (command === "auth" && subcommand === "login") {
1583
+ expectPositionals(parsed.positionals, 2, "cm auth login [browser auth flags]");
1584
+ validateFlags(parsed.flags, new Set([
1585
+ "app-url",
1586
+ "client",
1587
+ "credential-file",
1588
+ "expires-in-days",
1589
+ "no-open",
1590
+ "port",
1591
+ "scopes",
1592
+ "timeout-seconds"
1593
+ ]));
1594
+ const credentialFile = resolveCredentialFile(cleanOptional(stringFlag(parsed.flags, "credential-file")), cleanOptional(env[CAPABILITY_TOKEN_FILE_ENV]));
1595
+ const expiresInDays = parseIntegerFlag(parsed.flags, "expires-in-days", DEFAULT_AUTH_EXPIRES_IN_DAYS, 1, 365) ?? DEFAULT_AUTH_EXPIRES_IN_DAYS;
1596
+ const timeoutSeconds = parseIntegerFlag(parsed.flags, "timeout-seconds", DEFAULT_AUTH_TIMEOUT_MS / 1000, 1, 1800) ?? DEFAULT_AUTH_TIMEOUT_MS / 1000;
1597
+ const port = parseIntegerFlag(parsed.flags, "port", undefined, 1, 65535);
1598
+ const openBrowser = parsed.flags["no-open"] !== true;
1599
+ const result = await runBrowserAuthLogin({
1600
+ appUrl: normalizeEndpoint(cleanOptional(stringFlag(parsed.flags, "app-url")) ?? cleanOptional(env[AUTH_APP_URL_ENV]) ?? cleanOptional(env["CAREER_MENTOR_API_URL"]) ?? DEFAULT_AUTH_APP_URL),
1601
+ clientName: authClientName(stringFlag(parsed.flags, "client")),
1602
+ credentialFile,
1603
+ expiresInDays,
1604
+ onLaunch: async (launch) => {
1605
+ writeJson(stderr, openBrowser ? {
1606
+ diagnostic: {
1607
+ code: "AUTH_BROWSER_OPENING",
1608
+ message: "Complete authorization in the opened browser."
1609
+ }
1610
+ } : {
1611
+ diagnostic: {
1612
+ code: "AUTHORIZATION_URL",
1613
+ message: "Open this URL to authorize the CLI.",
1614
+ url: launch.authorizationUrl
1615
+ }
1616
+ });
1617
+ },
1618
+ openBrowser,
1619
+ ...port !== undefined ? { port } : {},
1620
+ scopes: parseRequestedScopes(stringFlag(parsed.flags, "scopes")),
1621
+ ...options.signal ? { signal: options.signal } : {},
1622
+ timeoutMs: timeoutSeconds * 1000
1623
+ }, {
1624
+ ...options.fetch ? { fetch: options.fetch } : {},
1625
+ ...options.browserAuth
1626
+ });
1627
+ writeJson(stdout, {
1628
+ ok: true,
1629
+ credential: { source: "managed_file", file: result.credentialFile },
1630
+ scopes: result.scopes,
1631
+ expiresAt: result.expiresAt
1632
+ });
1633
+ return CLI_EXIT_CODE.SUCCESS;
1634
+ }
1635
+ if (command === "auth" && subcommand === "logout") {
1636
+ expectPositionals(parsed.positionals, 2, "cm auth logout [--credential-file <path>]");
1637
+ validateFlags(parsed.flags, new Set(["credential-file"]));
1638
+ const credentialFile = resolveCredentialFile(cleanOptional(stringFlag(parsed.flags, "credential-file")), cleanOptional(env[CAPABILITY_TOKEN_FILE_ENV]));
1639
+ const result = await logoutCredential(credentialFile);
1640
+ writeJson(stdout, { ok: true, credentialFile, removed: result.removed });
1641
+ return CLI_EXIT_CODE.SUCCESS;
1642
+ }
1643
+ const config = await resolveConfig(parsed.flags, env);
1644
+ if (config.token)
1645
+ secrets = [config.token];
1646
+ if (command === "auth") {
1647
+ expectPositionals(parsed.positionals, 2, "cm auth status [credential flags]");
1648
+ if (subcommand !== "status") {
1649
+ usageError("UNKNOWN_COMMAND", "Unknown auth command. Use login, status, or logout.");
1650
+ }
1651
+ validateFlags(parsed.flags, new Set(["credential-file", "endpoint", "token"]));
1652
+ const ok = Boolean(config.token);
1653
+ writeJson(stdout, {
1654
+ ok,
1655
+ endpoint: config.endpoint,
1656
+ credential: { configured: ok, source: config.tokenSource }
1657
+ });
1658
+ return ok ? CLI_EXIT_CODE.SUCCESS : CLI_EXIT_CODE.USAGE;
1659
+ }
1660
+ if (command === "doctor") {
1661
+ expectPositionals(parsed.positionals, 1, "cm doctor [transport flags]");
1662
+ validateFlags(parsed.flags, TRANSPORT_FLAGS);
1663
+ if (!config.token) {
1664
+ writeJson(stdout, {
1665
+ ok: false,
1666
+ endpoint: config.endpoint,
1667
+ checks: [
1668
+ { name: "endpoint", ok: true },
1669
+ { name: "token", ok: false },
1670
+ { name: "api", ok: false, skipped: true, reason: "token_not_configured" }
1671
+ ]
1672
+ });
1673
+ return CLI_EXIT_CODE.USAGE;
1674
+ }
1675
+ try {
1676
+ const catalog = await makeClient(config, options.fetch).list(requestOptions(config, options.signal));
1677
+ writeJson(stdout, {
1678
+ ok: true,
1679
+ endpoint: config.endpoint,
1680
+ capabilityCount: catalog.capabilities.length,
1681
+ checks: [
1682
+ { name: "endpoint", ok: true },
1683
+ { name: "token", ok: true },
1684
+ { name: "api", ok: true }
1685
+ ]
1686
+ });
1687
+ return CLI_EXIT_CODE.SUCCESS;
1688
+ } catch (error) {
1689
+ const mapped = mapError(error, secrets);
1690
+ writeJson(stdout, {
1691
+ ok: false,
1692
+ endpoint: config.endpoint,
1693
+ checks: [
1694
+ { name: "endpoint", ok: true },
1695
+ { name: "token", ok: true },
1696
+ {
1697
+ name: "api",
1698
+ ok: false,
1699
+ error: {
1700
+ code: mapped.code,
1701
+ message: mapped.message,
1702
+ ...mapped.status ? { status: mapped.status } : {},
1703
+ ...mapped.requestId ? { requestId: mapped.requestId } : {}
1704
+ }
1705
+ }
1706
+ ]
1707
+ });
1708
+ return mapped.exitCode;
1709
+ }
1710
+ }
1711
+ if (!(command === "invoke" || command === "runs" || command === "capabilities" || command === "artifacts" || command === "photos" || command === "files")) {
1712
+ usageError("UNKNOWN_COMMAND", "Unknown command. Run cm help for the command contract.");
1713
+ }
1714
+ const client = makeClient(config, options.fetch);
1715
+ const baseRequestOptions = requestOptions(config, options.signal);
1716
+ const invokeOptions = {
1717
+ ...baseRequestOptions,
1718
+ ...config.idempotencyKey ? { idempotencyKey: config.idempotencyKey } : {}
1719
+ };
1720
+ if (command === "photos") {
1721
+ validateFlags(parsed.flags, new Set([...TRANSPORT_FLAGS, "idempotency-key"]));
1722
+ let capabilityName;
1723
+ let input;
1724
+ if (subcommand === "list") {
1725
+ expectPositionals(parsed.positionals, 2, "cm photos list");
1726
+ capabilityName = "list_profile_photos";
1727
+ input = {};
1728
+ } else if (subcommand === "add") {
1729
+ expectPositionals(parsed.positionals, 3, "cm photos add <registered-file-id>");
1730
+ capabilityName = "add_profile_photo";
1731
+ input = { fileId: parsed.positionals[2] };
1732
+ } else if (subcommand === "default") {
1733
+ expectPositionals(parsed.positionals, 3, "cm photos default <photo-id>");
1734
+ capabilityName = "set_default_profile_photo";
1735
+ input = { photoId: parsed.positionals[2] };
1736
+ } else if (subcommand === "delete") {
1737
+ expectPositionals(parsed.positionals, 3, "cm photos delete <photo-id>");
1738
+ capabilityName = "propose_delete_profile_photo";
1739
+ input = { photoId: parsed.positionals[2] };
1740
+ } else if (subcommand === "reorder") {
1741
+ if (parsed.positionals.length < 3) {
1742
+ usageError("INVALID_COMMAND", "Usage: cm photos reorder <photo-id> [photo-id...]");
1743
+ }
1744
+ capabilityName = "reorder_profile_photos";
1745
+ input = { photoIds: parsed.positionals.slice(2) };
1746
+ } else {
1747
+ usageError("UNKNOWN_COMMAND", "Unknown photos command. Use list, add, default, reorder, or delete.");
1748
+ }
1749
+ writeJson(stdout, await invokePublishedCapability(client, capabilityName, input, invokeOptions));
1750
+ return CLI_EXIT_CODE.SUCCESS;
1751
+ }
1752
+ if (command === "files") {
1753
+ expectPositionals(parsed.positionals, 3, "cm files delete <registered-file-id>");
1754
+ if (subcommand !== "delete") {
1755
+ usageError("UNKNOWN_COMMAND", "Unknown files command. Use delete.");
1756
+ }
1757
+ validateFlags(parsed.flags, new Set([...TRANSPORT_FLAGS, "idempotency-key"]));
1758
+ writeJson(stdout, await invokePublishedCapability(client, "propose_delete_registered_file", { fileId: parsed.positionals[2] }, invokeOptions));
1759
+ return CLI_EXIT_CODE.SUCCESS;
1760
+ }
1761
+ if (command === "artifacts") {
1762
+ expectPositionals(parsed.positionals, 3, "cm artifacts upload <path> [--create --type <artifact-type> [--name <name>]]");
1763
+ if (subcommand !== "upload") {
1764
+ usageError("UNKNOWN_COMMAND", "Unknown artifacts command. Use upload.");
1765
+ }
1766
+ validateFlags(parsed.flags, new Set([...TRANSPORT_FLAGS, "create", "idempotency-key", "name", "type"]));
1767
+ const filePath = parsed.positionals[2];
1768
+ if (!filePath)
1769
+ usageError("MISSING_FILE_PATH", "Artifact file path is required.");
1770
+ const filename = basename(filePath);
1771
+ if (!filename || filename.length > 200 || filename === "." || filename === ".." || CONTROL_CHARACTER2.test(filename)) {
1772
+ usageError("INVALID_FILENAME", "Artifact filename must be 1 to 200 safe characters.");
1773
+ }
1774
+ const contentType = artifactContentType(filePath);
1775
+ const shouldCreateArtifact = parsed.flags["create"] === true;
1776
+ const requestedArtifactType = artifactType(parsed.flags);
1777
+ const requestedName = cleanOptional(stringFlag(parsed.flags, "name"));
1778
+ if (!shouldCreateArtifact && (requestedArtifactType !== undefined || requestedName !== undefined)) {
1779
+ usageError("CREATE_FLAG_REQUIRED", "Use --create with --type or --name.");
1780
+ }
1781
+ if (shouldCreateArtifact && requestedArtifactType === undefined) {
1782
+ usageError("MISSING_ARTIFACT_TYPE", "Flag --type is required with --create.");
1783
+ }
1784
+ let bytes;
1785
+ try {
1786
+ bytes = options.readBinaryFile ? await options.readBinaryFile(filePath) : new Uint8Array(await readFileFromDisk(filePath));
1787
+ } catch {
1788
+ usageError("FILE_READ_FAILED", "Could not read the artifact file.");
1789
+ }
1790
+ if (!(bytes.byteLength >= 1 && bytes.byteLength <= MAX_ARTIFACT_UPLOAD_BYTES)) {
1791
+ usageError("INVALID_FILE_SIZE", `Artifact file size must be between 1 and ${MAX_ARTIFACT_UPLOAD_BYTES} bytes.`);
1792
+ }
1793
+ const authorizationResponse = await invokePublishedCapability(client, "initiate_artifact_upload", { filename, contentType, size: bytes.byteLength }, invokeOptions);
1794
+ const authorization = parseUploadAuthorization(authorizationResponse, {
1795
+ contentType,
1796
+ size: bytes.byteLength
1797
+ });
1798
+ secrets = [...secrets, authorization.uploadUrl, authorization.s3Key];
1799
+ await putArtifactBytes({
1800
+ bytes,
1801
+ contentType,
1802
+ fetch: options.fetch ?? globalThis.fetch,
1803
+ ...options.signal ? { signal: options.signal } : {},
1804
+ timeoutMs: config.timeoutMs,
1805
+ uploadUrl: authorization.uploadUrl
1806
+ });
1807
+ const registrationResponse = await invokePublishedCapability(client, "register_artifact_upload", {
1808
+ filename,
1809
+ contentType,
1810
+ size: bytes.byteLength,
1811
+ s3Key: authorization.s3Key,
1812
+ parseText: false,
1813
+ calculateHash: true
1814
+ }, baseRequestOptions);
1815
+ const file = capabilityResultData(registrationResponse, "file registration");
1816
+ if (!shouldCreateArtifact) {
1817
+ writeJson(stdout, { ok: true, file });
1818
+ return CLI_EXIT_CODE.SUCCESS;
1819
+ }
1820
+ const fileId = file["fileId"];
1821
+ if (typeof fileId !== "string") {
1822
+ throw new CareerMentorError("Career Mentor returned invalid file registration data.", {
1823
+ code: "INVALID_RESPONSE"
1824
+ });
1825
+ }
1826
+ const defaultArtifactName = basename(filename, extname(filename));
1827
+ const artifactResponse = await invokePublishedCapability(client, "create_artifact", {
1828
+ fileId,
1829
+ name: requestedName ?? defaultArtifactName,
1830
+ type: requestedArtifactType
1831
+ }, baseRequestOptions);
1832
+ writeJson(stdout, {
1833
+ ok: true,
1834
+ file,
1835
+ artifact: capabilityResultData(artifactResponse, "artifact creation")
1836
+ });
1837
+ return CLI_EXIT_CODE.SUCCESS;
1838
+ }
1839
+ if (command === "invoke") {
1840
+ expectPositionals(parsed.positionals, 2, "cm invoke <name> [--input <json> | --input-file <path|->]");
1841
+ validateFlags(parsed.flags, new Set([...TRANSPORT_FLAGS, "idempotency-key", "input", "input-file"]));
1842
+ const name = parsed.positionals[1];
1843
+ if (!name)
1844
+ usageError("MISSING_CAPABILITY_NAME", "Capability name is required.");
1845
+ const input = await readCapabilityInput(parsed.flags, options.readFile ?? (async (path) => await readFileFromDisk(path, "utf8")), options.readStdin ?? defaultReadStdin);
1846
+ writeJson(stdout, await invokePublishedCapability(client, name, input, invokeOptions));
1847
+ return CLI_EXIT_CODE.SUCCESS;
1848
+ }
1849
+ if (command === "runs") {
1850
+ expectPositionals(parsed.positionals, 3, "cm runs <get|events|wait|watch> <run-id>");
1851
+ const runId = parsed.positionals[2];
1852
+ if (!runId)
1853
+ usageError("MISSING_RUN_ID", "Capability run ID is required.");
1854
+ if (subcommand === "get") {
1855
+ validateFlags(parsed.flags, TRANSPORT_FLAGS);
1856
+ writeJson(stdout, await client.getRun(runId, baseRequestOptions));
1857
+ return CLI_EXIT_CODE.SUCCESS;
1858
+ }
1859
+ const after = cleanOptional(stringFlag(parsed.flags, "after"));
1860
+ const rawLimit = cleanOptional(stringFlag(parsed.flags, "limit"));
1861
+ const eventOptions = {
1862
+ ...baseRequestOptions,
1863
+ ...after ? { after } : {},
1864
+ ...rawLimit ? { limit: Number(rawLimit) } : {}
1865
+ };
1866
+ if (subcommand === "events") {
1867
+ validateFlags(parsed.flags, new Set([...TRANSPORT_FLAGS, "after", "limit"]));
1868
+ writeJson(stdout, await client.listRunEvents(runId, eventOptions));
1869
+ return CLI_EXIT_CODE.SUCCESS;
1870
+ }
1871
+ if (subcommand === "watch") {
1872
+ validateFlags(parsed.flags, new Set([...TRANSPORT_FLAGS, "after", "last-event-id", "limit"]));
1873
+ const lastEventId = cleanOptional(stringFlag(parsed.flags, "last-event-id"));
1874
+ for await (const event of client.streamRunEvents(runId, {
1875
+ ...eventOptions,
1876
+ ...lastEventId ? { lastEventId } : {}
1877
+ })) {
1878
+ writeJson(stdout, event);
1879
+ }
1880
+ return CLI_EXIT_CODE.SUCCESS;
1881
+ }
1882
+ if (subcommand === "wait") {
1883
+ validateFlags(parsed.flags, new Set([...TRANSPORT_FLAGS, "after", "last-event-id", "limit"]));
1884
+ const lastEventId = cleanOptional(stringFlag(parsed.flags, "last-event-id"));
1885
+ const terminal = await client.waitForRun(runId, {
1886
+ ...eventOptions,
1887
+ ...lastEventId ? { lastEventId } : {}
1888
+ });
1889
+ const exitCode = terminalRunExitCode(terminal);
1890
+ writeJson(stdout, terminal);
1891
+ return exitCode;
1892
+ }
1893
+ usageError("UNKNOWN_COMMAND", "Unknown runs command. Use get, events, wait, or watch.");
1894
+ }
1895
+ if (subcommand === "list") {
1896
+ expectPositionals(parsed.positionals, 2, "cm capabilities list [transport flags]");
1897
+ validateFlags(parsed.flags, TRANSPORT_FLAGS);
1898
+ writeJson(stdout, await client.list(baseRequestOptions));
1899
+ return CLI_EXIT_CODE.SUCCESS;
1900
+ }
1901
+ if (subcommand === "describe") {
1902
+ expectPositionals(parsed.positionals, 3, "cm capabilities describe <name> [transport flags]");
1903
+ validateFlags(parsed.flags, TRANSPORT_FLAGS);
1904
+ const name = parsed.positionals[2];
1905
+ if (!name)
1906
+ usageError("MISSING_CAPABILITY_NAME", "Capability name is required.");
1907
+ writeJson(stdout, await client.get(name, baseRequestOptions));
1908
+ return CLI_EXIT_CODE.SUCCESS;
1909
+ }
1910
+ usageError("UNKNOWN_COMMAND", "Unknown capabilities command. Use list or describe.");
1911
+ } catch (error) {
1912
+ const mapped = mapError(error, secrets);
1913
+ writeJson(stderr, { ok: false, error: mapped });
1914
+ return mapped.exitCode;
1915
+ }
1916
+ }
1917
+ function isEntrypoint() {
1918
+ const entrypoint = process.argv[1];
1919
+ if (entrypoint === undefined)
1920
+ return false;
1921
+ try {
1922
+ return pathToFileURL(realpathSync(entrypoint)).href === import.meta.url;
1923
+ } catch {
1924
+ return false;
1925
+ }
1926
+ }
1927
+ if (isEntrypoint()) {
1928
+ const controller = new AbortController;
1929
+ const abort = () => controller.abort();
1930
+ process.once("SIGINT", abort);
1931
+ process.once("SIGTERM", abort);
1932
+ try {
1933
+ process.exitCode = await runCli(process.argv.slice(2), { signal: controller.signal });
1934
+ } finally {
1935
+ process.removeListener("SIGINT", abort);
1936
+ process.removeListener("SIGTERM", abort);
1937
+ }
1938
+ }
1939
+ export {
1940
+ runCli,
1941
+ parseArgs,
1942
+ DEFAULT_API_URL,
1943
+ CLI_VERSION,
1944
+ CLI_EXIT_CODE
1945
+ };