numo-cli 1.3.0 → 1.5.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.
Files changed (2) hide show
  1. package/dist/cli.cjs +374 -337
  2. package/package.json +1 -1
package/dist/cli.cjs CHANGED
@@ -3217,6 +3217,336 @@ var init_http = __esm({
3217
3217
  }
3218
3218
  });
3219
3219
 
3220
+ // src/cli/lib/dirs.ts
3221
+ function getConfigDir() {
3222
+ if (process.env.NUMO_CONFIG_DIR) {
3223
+ return process.env.NUMO_CONFIG_DIR;
3224
+ }
3225
+ const xdgHome = process.env.XDG_CONFIG_HOME || path.join(os.homedir(), ".config");
3226
+ const xdgDir = path.join(xdgHome, "numo");
3227
+ if (fs.existsSync(xdgDir)) return xdgDir;
3228
+ if (fs.existsSync(LEGACY_DIR)) return LEGACY_DIR;
3229
+ return xdgDir;
3230
+ }
3231
+ function ensureConfigDir() {
3232
+ const dir = getConfigDir();
3233
+ if (!fs.existsSync(dir)) {
3234
+ fs.mkdirSync(dir, { recursive: true, mode: 448 });
3235
+ }
3236
+ return dir;
3237
+ }
3238
+ function getCredentialsPath() {
3239
+ return path.join(getConfigDir(), "credentials.json");
3240
+ }
3241
+ function migrateIfNeeded() {
3242
+ const xdgHome = process.env.XDG_CONFIG_HOME || path.join(os.homedir(), ".config");
3243
+ const xdgDir = path.join(xdgHome, "numo");
3244
+ if (process.env.NUMO_CONFIG_DIR) return;
3245
+ const legacyCreds = path.join(LEGACY_DIR, "credentials.json");
3246
+ if (!fs.existsSync(legacyCreds) || fs.existsSync(xdgDir)) return;
3247
+ try {
3248
+ fs.mkdirSync(xdgDir, { recursive: true, mode: 448 });
3249
+ const data = fs.readFileSync(legacyCreds, "utf8");
3250
+ fs.writeFileSync(path.join(xdgDir, "credentials.json"), data, { mode: 384 });
3251
+ const legacyStreaks = path.join(LEGACY_DIR, "streaks.json");
3252
+ if (fs.existsSync(legacyStreaks)) {
3253
+ const streaksData = fs.readFileSync(legacyStreaks, "utf8");
3254
+ fs.writeFileSync(path.join(xdgDir, "streaks.json"), streaksData, { mode: 384 });
3255
+ }
3256
+ process.stderr.write(`Migrated config from ${LEGACY_DIR} to ${xdgDir}
3257
+ `);
3258
+ } catch {
3259
+ }
3260
+ }
3261
+ var fs, path, os, LEGACY_DIR;
3262
+ var init_dirs = __esm({
3263
+ "src/cli/lib/dirs.ts"() {
3264
+ "use strict";
3265
+ fs = __toESM(require("fs"), 1);
3266
+ path = __toESM(require("path"), 1);
3267
+ os = __toESM(require("os"), 1);
3268
+ LEGACY_DIR = path.join(os.homedir(), ".numo");
3269
+ }
3270
+ });
3271
+
3272
+ // src/cli/lib/errors.ts
3273
+ function classifyError(err) {
3274
+ if (err instanceof CliError) return err;
3275
+ const axiosErr = err;
3276
+ if (axiosErr.code === "ECONNABORTED" || axiosErr.code === "ETIMEDOUT") return Errors.timeout();
3277
+ if (axiosErr.code === "ENOTFOUND" || axiosErr.code === "EAI_AGAIN") return Errors.networkError();
3278
+ if (axiosErr.code === "ECONNREFUSED" || axiosErr.code === "ECONNRESET") {
3279
+ return Errors.networkError("Service may be temporarily down. Try again in a moment.");
3280
+ }
3281
+ const status = axiosErr.response?.status;
3282
+ if (status === 401) return Errors.authRequired();
3283
+ if (status === 403) {
3284
+ return new CliError("AUTH_FORBIDDEN" /* AUTH_FORBIDDEN */, "Access denied", ExitCode.NO_PERM, {
3285
+ hint: "You don't have permission for this action."
3286
+ });
3287
+ }
3288
+ if (status === 404) return Errors.notFound("Resource");
3289
+ if (status === 429) {
3290
+ const retryAfter = parseInt(axiosErr.response?.headers?.["retry-after"] ?? "");
3291
+ return Errors.rateLimited(isNaN(retryAfter) ? void 0 : retryAfter);
3292
+ }
3293
+ if (status && status >= 500) {
3294
+ return new CliError("SERVICE_UNAVAILABLE" /* SERVICE_UNAVAILABLE */, "Server error", ExitCode.UNAVAILABLE, {
3295
+ hint: "This is on our end. Try again in a moment.",
3296
+ retryable: true
3297
+ });
3298
+ }
3299
+ const body = axiosErr.response?.data;
3300
+ const raw = body?.error?.message ?? axiosErr.message ?? "Unknown error";
3301
+ const message = sanitizeErrorMessage(raw);
3302
+ return new CliError("UNKNOWN" /* UNKNOWN */, message, ExitCode.GENERAL, { cause: err });
3303
+ }
3304
+ function sanitizeErrorMessage(msg) {
3305
+ return msg.replace(/https?:\/\/\S+/g, "<url>").replace(/\/(?:Users|home|var|tmp)\/\S+/g, "<path>").replace(/[A-Za-z0-9_-]{40,}/g, "<token>");
3306
+ }
3307
+ var ExitCode, CliError, Errors;
3308
+ var init_errors = __esm({
3309
+ "src/cli/lib/errors.ts"() {
3310
+ "use strict";
3311
+ ExitCode = {
3312
+ OK: 0,
3313
+ GENERAL: 1,
3314
+ USAGE: 2,
3315
+ UNAVAILABLE: 69,
3316
+ TEMP_FAIL: 75,
3317
+ NO_PERM: 77,
3318
+ CONFIG: 78,
3319
+ NOT_FOUND: 100,
3320
+ CONFLICT: 101
3321
+ };
3322
+ CliError = class extends Error {
3323
+ constructor(kind, message, exitCode = ExitCode.GENERAL, options = {}) {
3324
+ super(message);
3325
+ this.kind = kind;
3326
+ this.exitCode = exitCode;
3327
+ this.options = options;
3328
+ this.name = "CliError";
3329
+ }
3330
+ toJSON() {
3331
+ return {
3332
+ error: {
3333
+ kind: this.kind,
3334
+ code: this.exitCode,
3335
+ message: this.message,
3336
+ ...this.options.suggestion && { suggestion: this.options.suggestion },
3337
+ ...this.options.hint && { hint: this.options.hint },
3338
+ retryable: this.options.retryable ?? false,
3339
+ ...this.options.retryAfter != null && { retryAfter: this.options.retryAfter }
3340
+ }
3341
+ };
3342
+ }
3343
+ };
3344
+ Errors = {
3345
+ authRequired: () => new CliError("AUTH_REQUIRED" /* AUTH_REQUIRED */, "Not logged in", ExitCode.NO_PERM, {
3346
+ suggestion: "numo login"
3347
+ }),
3348
+ notFound: (resource, id) => new CliError("NOT_FOUND" /* NOT_FOUND */, `${resource} not found${id ? `: ${id}` : ""}`, ExitCode.NOT_FOUND, {
3349
+ suggestion: `numo ${resource.toLowerCase()}s list`
3350
+ }),
3351
+ missingArg: (name, flag) => new CliError("MISSING_ARGUMENT" /* MISSING_ARGUMENT */, `${name} is required`, ExitCode.USAGE, {
3352
+ suggestion: `Use --${flag}`,
3353
+ hint: "Run with --help for all options."
3354
+ }),
3355
+ invalidInput: (message, hint) => new CliError("INVALID_INPUT" /* INVALID_INPUT */, message, ExitCode.USAGE, { hint }),
3356
+ configMissing: (key) => new CliError("CONFIG_ERROR" /* CONFIG_ERROR */, `${key} not set`, ExitCode.CONFIG, {
3357
+ suggestion: `export ${key}=<value>`
3358
+ }),
3359
+ networkError: (hint) => new CliError("NETWORK_ERROR" /* NETWORK_ERROR */, "Can't reach Numo servers", ExitCode.UNAVAILABLE, {
3360
+ hint: hint ?? "Check your internet connection.",
3361
+ retryable: true
3362
+ }),
3363
+ timeout: () => new CliError("TIMEOUT" /* TIMEOUT */, "Request timed out", ExitCode.TEMP_FAIL, {
3364
+ hint: "The server took too long to respond. Try again.",
3365
+ retryable: true
3366
+ }),
3367
+ rateLimited: (retryAfter) => new CliError("RATE_LIMITED" /* RATE_LIMITED */, "Too many requests", ExitCode.TEMP_FAIL, {
3368
+ hint: retryAfter ? `Wait ${retryAfter} seconds and try again.` : "Wait a moment and try again.",
3369
+ retryable: true,
3370
+ retryAfter
3371
+ })
3372
+ };
3373
+ }
3374
+ });
3375
+
3376
+ // src/cli/lib/api-client.ts
3377
+ var api_client_exports = {};
3378
+ __export(api_client_exports, {
3379
+ API_BASE: () => API_BASE,
3380
+ api: () => api
3381
+ });
3382
+ function toCliError(err) {
3383
+ if (err instanceof CliError) return err;
3384
+ const httpErr = err;
3385
+ if (httpErr.code === "ECONNABORTED" || httpErr.code === "ETIMEDOUT") {
3386
+ return new CliError("TIMEOUT" /* TIMEOUT */, "Request timed out", ExitCode.TEMP_FAIL, {
3387
+ hint: "The API server took too long to respond.",
3388
+ retryable: true
3389
+ });
3390
+ }
3391
+ if (httpErr.code === "ECONNREFUSED" || httpErr.code === "ECONNRESET" || httpErr.code === "ENOTFOUND") {
3392
+ return new CliError("NETWORK_ERROR" /* NETWORK_ERROR */, "Can't reach Numo API", ExitCode.UNAVAILABLE, {
3393
+ hint: "Is the API server running? Check NUMO_API_URL.",
3394
+ retryable: true
3395
+ });
3396
+ }
3397
+ const body = httpErr.response?.data;
3398
+ if (body?.error) {
3399
+ const e = body.error;
3400
+ const kind = e.kind ?? "UNKNOWN" /* UNKNOWN */;
3401
+ const exitCode = KIND_EXIT[kind] ?? ExitCode.GENERAL;
3402
+ return new CliError(kind, e.message ?? "Unknown error", exitCode, {
3403
+ retryable: e.retryable,
3404
+ retryAfter: e.retryAfter
3405
+ });
3406
+ }
3407
+ return new CliError("UNKNOWN" /* UNKNOWN */, httpErr.message ?? "Unknown error", ExitCode.GENERAL);
3408
+ }
3409
+ async function apiHeaders() {
3410
+ const token = await getIdToken();
3411
+ return {
3412
+ Authorization: `Bearer ${token}`,
3413
+ "Content-Type": "application/json"
3414
+ };
3415
+ }
3416
+ function url(path3, params) {
3417
+ const u2 = `${API_BASE}${path3}`;
3418
+ if (!params) return u2;
3419
+ const sp = new URLSearchParams();
3420
+ for (const [k2, v] of Object.entries(params)) {
3421
+ if (v !== void 0) sp.set(k2, v);
3422
+ }
3423
+ const qs = sp.toString();
3424
+ return qs ? `${u2}?${qs}` : u2;
3425
+ }
3426
+ var API_BASE, KIND_EXIT, api;
3427
+ var init_api_client = __esm({
3428
+ "src/cli/lib/api-client.ts"() {
3429
+ "use strict";
3430
+ init_credentials();
3431
+ init_http();
3432
+ init_errors();
3433
+ API_BASE = process.env.NUMO_API_URL ?? (true ? "https://api.numo.ai" : "http://localhost:3000");
3434
+ if (API_BASE !== "http://localhost:3000" && API_BASE.startsWith("http://")) {
3435
+ process.stderr.write("[warn] NUMO_API_URL uses HTTP \u2014 tokens sent unencrypted. Use HTTPS in production.\n");
3436
+ }
3437
+ KIND_EXIT = {
3438
+ AUTH_REQUIRED: ExitCode.NO_PERM,
3439
+ AUTH_EXPIRED: ExitCode.NO_PERM,
3440
+ AUTH_FORBIDDEN: ExitCode.NO_PERM,
3441
+ INVALID_INPUT: ExitCode.USAGE,
3442
+ MISSING_ARGUMENT: ExitCode.USAGE,
3443
+ NOT_FOUND: ExitCode.NOT_FOUND,
3444
+ CONFLICT: ExitCode.CONFLICT,
3445
+ RATE_LIMITED: ExitCode.TEMP_FAIL,
3446
+ NETWORK_ERROR: ExitCode.UNAVAILABLE,
3447
+ TIMEOUT: ExitCode.TEMP_FAIL,
3448
+ SERVICE_UNAVAILABLE: ExitCode.UNAVAILABLE
3449
+ };
3450
+ api = {
3451
+ async get(path3, params) {
3452
+ try {
3453
+ const resp = await http.get(url(path3, params), { headers: await apiHeaders() });
3454
+ return resp.data;
3455
+ } catch (err) {
3456
+ throw toCliError(err);
3457
+ }
3458
+ },
3459
+ async post(path3, body) {
3460
+ try {
3461
+ const resp = await http.post(url(path3), body, { headers: await apiHeaders() });
3462
+ return resp.data;
3463
+ } catch (err) {
3464
+ throw toCliError(err);
3465
+ }
3466
+ },
3467
+ async patch(path3, body) {
3468
+ try {
3469
+ const resp = await http.patch(url(path3), body, { headers: await apiHeaders() });
3470
+ return resp.data;
3471
+ } catch (err) {
3472
+ throw toCliError(err);
3473
+ }
3474
+ },
3475
+ async del(path3) {
3476
+ try {
3477
+ const resp = await http.delete(url(path3), { headers: await apiHeaders() });
3478
+ return resp.data;
3479
+ } catch (err) {
3480
+ throw toCliError(err);
3481
+ }
3482
+ }
3483
+ };
3484
+ }
3485
+ });
3486
+
3487
+ // src/cli/auth/credentials.ts
3488
+ function loadCredentials() {
3489
+ try {
3490
+ const data = JSON.parse(fs2.readFileSync(getCredentialsPath(), "utf8"));
3491
+ if (typeof data?.refreshToken !== "string" || typeof data?.uid !== "string" || typeof data?.email !== "string") {
3492
+ return null;
3493
+ }
3494
+ return data;
3495
+ } catch {
3496
+ return null;
3497
+ }
3498
+ }
3499
+ function saveCredentials(creds) {
3500
+ ensureConfigDir();
3501
+ fs2.writeFileSync(getCredentialsPath(), JSON.stringify(creds, null, 2), { mode: 384 });
3502
+ }
3503
+ function clearCredentials() {
3504
+ try {
3505
+ const credPath = getCredentialsPath();
3506
+ const stat = fs2.statSync(credPath);
3507
+ fs2.writeFileSync(credPath, crypto.randomBytes(stat.size));
3508
+ fs2.unlinkSync(credPath);
3509
+ } catch {
3510
+ }
3511
+ }
3512
+ async function getIdToken() {
3513
+ const envToken = process.env.NUMO_TOKEN;
3514
+ if (envToken) return envToken;
3515
+ const creds = loadCredentials();
3516
+ if (!creds) throw new Error("Not logged in. Run: numo login");
3517
+ if (creds.idToken && creds.idTokenExpiry && Date.now() < creds.idTokenExpiry - 6e4) {
3518
+ return creds.idToken;
3519
+ }
3520
+ if (refreshInFlight) return refreshInFlight;
3521
+ refreshInFlight = performRefresh(creds).finally(() => {
3522
+ refreshInFlight = null;
3523
+ });
3524
+ return refreshInFlight;
3525
+ }
3526
+ async function performRefresh(creds) {
3527
+ const { API_BASE: apiBase } = await Promise.resolve().then(() => (init_api_client(), api_client_exports));
3528
+ const { http: http2 } = await Promise.resolve().then(() => (init_http(), http_exports));
3529
+ const resp = await http2.post(
3530
+ `${apiBase}/api/auth/refresh`,
3531
+ { refreshToken: creds.refreshToken }
3532
+ );
3533
+ creds.idToken = resp.data.idToken;
3534
+ creds.refreshToken = resp.data.refreshToken ?? creds.refreshToken;
3535
+ creds.idTokenExpiry = Date.now() + (resp.data.expiresIn || 3600) * 1e3;
3536
+ saveCredentials(creds);
3537
+ return creds.idToken;
3538
+ }
3539
+ var fs2, crypto, refreshInFlight;
3540
+ var init_credentials = __esm({
3541
+ "src/cli/auth/credentials.ts"() {
3542
+ "use strict";
3543
+ fs2 = __toESM(require("fs"), 1);
3544
+ crypto = __toESM(require("crypto"), 1);
3545
+ init_dirs();
3546
+ refreshInFlight = null;
3547
+ }
3548
+ });
3549
+
3220
3550
  // src/cli/lib/tty.ts
3221
3551
  function isInteractive() {
3222
3552
  if (!process.stdin.isTTY || !process.stdout.isTTY) return false;
@@ -4993,132 +5323,28 @@ async function promptMultiSelect(opts) {
4993
5323
  const p = await loadClack();
4994
5324
  const value = await p.multiselect({
4995
5325
  message: opts.message,
4996
- options: opts.options,
4997
- required: opts.required ?? false
4998
- });
4999
- if (p.isCancel(value)) {
5000
- process.exit(130);
5001
- }
5002
- return value;
5003
- }
5004
- async function promptForMissing(opts) {
5005
- if (opts.value !== void 0 && opts.value !== "") {
5006
- return opts.value;
5007
- }
5008
- return promptText({
5009
- message: opts.message,
5010
- placeholder: opts.placeholder,
5011
- required: opts.required ?? true
5012
- });
5013
- }
5014
- var init_prompts = __esm({
5015
- "src/cli/lib/prompts.ts"() {
5016
- "use strict";
5017
- init_tty();
5018
- }
5019
- });
5020
-
5021
- // src/cli/lib/errors.ts
5022
- function classifyError(err) {
5023
- if (err instanceof CliError) return err;
5024
- const axiosErr = err;
5025
- if (axiosErr.code === "ECONNABORTED" || axiosErr.code === "ETIMEDOUT") return Errors.timeout();
5026
- if (axiosErr.code === "ENOTFOUND" || axiosErr.code === "EAI_AGAIN") return Errors.networkError();
5027
- if (axiosErr.code === "ECONNREFUSED" || axiosErr.code === "ECONNRESET") {
5028
- return Errors.networkError("Service may be temporarily down. Try again in a moment.");
5029
- }
5030
- const status = axiosErr.response?.status;
5031
- if (status === 401) return Errors.authRequired();
5032
- if (status === 403) {
5033
- return new CliError("AUTH_FORBIDDEN" /* AUTH_FORBIDDEN */, "Access denied", ExitCode.NO_PERM, {
5034
- hint: "You don't have permission for this action."
5035
- });
5036
- }
5037
- if (status === 404) return Errors.notFound("Resource");
5038
- if (status === 429) {
5039
- const retryAfter = parseInt(axiosErr.response?.headers?.["retry-after"] ?? "");
5040
- return Errors.rateLimited(isNaN(retryAfter) ? void 0 : retryAfter);
5041
- }
5042
- if (status && status >= 500) {
5043
- return new CliError("SERVICE_UNAVAILABLE" /* SERVICE_UNAVAILABLE */, "Server error", ExitCode.UNAVAILABLE, {
5044
- hint: "This is on our end. Try again in a moment.",
5045
- retryable: true
5046
- });
5326
+ options: opts.options,
5327
+ required: opts.required ?? false
5328
+ });
5329
+ if (p.isCancel(value)) {
5330
+ process.exit(130);
5047
5331
  }
5048
- const body = axiosErr.response?.data;
5049
- const raw = body?.error?.message ?? axiosErr.message ?? "Unknown error";
5050
- const message = sanitizeErrorMessage(raw);
5051
- return new CliError("UNKNOWN" /* UNKNOWN */, message, ExitCode.GENERAL, { cause: err });
5332
+ return value;
5052
5333
  }
5053
- function sanitizeErrorMessage(msg) {
5054
- return msg.replace(/https?:\/\/\S+/g, "<url>").replace(/\/(?:Users|home|var|tmp)\/\S+/g, "<path>").replace(/[A-Za-z0-9_-]{40,}/g, "<token>");
5334
+ async function promptForMissing(opts) {
5335
+ if (opts.value !== void 0 && opts.value !== "") {
5336
+ return opts.value;
5337
+ }
5338
+ return promptText({
5339
+ message: opts.message,
5340
+ placeholder: opts.placeholder,
5341
+ required: opts.required ?? true
5342
+ });
5055
5343
  }
5056
- var ExitCode, CliError, Errors;
5057
- var init_errors = __esm({
5058
- "src/cli/lib/errors.ts"() {
5344
+ var init_prompts = __esm({
5345
+ "src/cli/lib/prompts.ts"() {
5059
5346
  "use strict";
5060
- ExitCode = {
5061
- OK: 0,
5062
- GENERAL: 1,
5063
- USAGE: 2,
5064
- UNAVAILABLE: 69,
5065
- TEMP_FAIL: 75,
5066
- NO_PERM: 77,
5067
- CONFIG: 78,
5068
- NOT_FOUND: 100,
5069
- CONFLICT: 101
5070
- };
5071
- CliError = class extends Error {
5072
- constructor(kind, message, exitCode = ExitCode.GENERAL, options = {}) {
5073
- super(message);
5074
- this.kind = kind;
5075
- this.exitCode = exitCode;
5076
- this.options = options;
5077
- this.name = "CliError";
5078
- }
5079
- toJSON() {
5080
- return {
5081
- error: {
5082
- kind: this.kind,
5083
- code: this.exitCode,
5084
- message: this.message,
5085
- ...this.options.suggestion && { suggestion: this.options.suggestion },
5086
- ...this.options.hint && { hint: this.options.hint },
5087
- retryable: this.options.retryable ?? false,
5088
- ...this.options.retryAfter != null && { retryAfter: this.options.retryAfter }
5089
- }
5090
- };
5091
- }
5092
- };
5093
- Errors = {
5094
- authRequired: () => new CliError("AUTH_REQUIRED" /* AUTH_REQUIRED */, "Not logged in", ExitCode.NO_PERM, {
5095
- suggestion: "numo login"
5096
- }),
5097
- notFound: (resource, id) => new CliError("NOT_FOUND" /* NOT_FOUND */, `${resource} not found${id ? `: ${id}` : ""}`, ExitCode.NOT_FOUND, {
5098
- suggestion: `numo ${resource.toLowerCase()}s list`
5099
- }),
5100
- missingArg: (name, flag) => new CliError("MISSING_ARGUMENT" /* MISSING_ARGUMENT */, `${name} is required`, ExitCode.USAGE, {
5101
- suggestion: `Use --${flag}`,
5102
- hint: "Run with --help for all options."
5103
- }),
5104
- invalidInput: (message, hint) => new CliError("INVALID_INPUT" /* INVALID_INPUT */, message, ExitCode.USAGE, { hint }),
5105
- configMissing: (key) => new CliError("CONFIG_ERROR" /* CONFIG_ERROR */, `${key} not set`, ExitCode.CONFIG, {
5106
- suggestion: `export ${key}=<value>`
5107
- }),
5108
- networkError: (hint) => new CliError("NETWORK_ERROR" /* NETWORK_ERROR */, "Can't reach Numo servers", ExitCode.UNAVAILABLE, {
5109
- hint: hint ?? "Check your internet connection.",
5110
- retryable: true
5111
- }),
5112
- timeout: () => new CliError("TIMEOUT" /* TIMEOUT */, "Request timed out", ExitCode.TEMP_FAIL, {
5113
- hint: "The server took too long to respond. Try again.",
5114
- retryable: true
5115
- }),
5116
- rateLimited: (retryAfter) => new CliError("RATE_LIMITED" /* RATE_LIMITED */, "Too many requests", ExitCode.TEMP_FAIL, {
5117
- hint: retryAfter ? `Wait ${retryAfter} seconds and try again.` : "Wait a moment and try again.",
5118
- retryable: true,
5119
- retryAfter
5120
- })
5121
- };
5347
+ init_tty();
5122
5348
  }
5123
5349
  });
5124
5350
 
@@ -5171,7 +5397,7 @@ async function authenticateWithPhone(spinner) {
5171
5397
  }
5172
5398
  throw Errors.networkError("Phone verification timed out. Try again.");
5173
5399
  }
5174
- var import_picocolors, API_BASE, POLL_INTERVAL, POLL_TIMEOUT;
5400
+ var import_picocolors, POLL_INTERVAL, POLL_TIMEOUT;
5175
5401
  var init_phone_login = __esm({
5176
5402
  "src/cli/auth/phone-login.ts"() {
5177
5403
  "use strict";
@@ -5179,7 +5405,7 @@ var init_phone_login = __esm({
5179
5405
  import_picocolors = __toESM(require_picocolors(), 1);
5180
5406
  init_errors();
5181
5407
  init_prompts();
5182
- API_BASE = process.env.NUMO_API_URL ?? "http://localhost:3000";
5408
+ init_api_client();
5183
5409
  POLL_INTERVAL = 2e3;
5184
5410
  POLL_TIMEOUT = 5 * 60 * 1e3;
5185
5411
  }
@@ -5208,121 +5434,16 @@ var import_picocolors13 = __toESM(require_picocolors(), 1);
5208
5434
  // src/cli/auth/login.ts
5209
5435
  init_http();
5210
5436
  var import_picocolors2 = __toESM(require_picocolors(), 1);
5211
-
5212
- // src/cli/auth/credentials.ts
5213
- var fs2 = __toESM(require("fs"), 1);
5214
- var crypto = __toESM(require("crypto"), 1);
5215
-
5216
- // src/cli/lib/dirs.ts
5217
- var fs = __toESM(require("fs"), 1);
5218
- var path = __toESM(require("path"), 1);
5219
- var os = __toESM(require("os"), 1);
5220
- var LEGACY_DIR = path.join(os.homedir(), ".numo");
5221
- function getConfigDir() {
5222
- if (process.env.NUMO_CONFIG_DIR) {
5223
- return process.env.NUMO_CONFIG_DIR;
5224
- }
5225
- const xdgHome = process.env.XDG_CONFIG_HOME || path.join(os.homedir(), ".config");
5226
- const xdgDir = path.join(xdgHome, "numo");
5227
- if (fs.existsSync(xdgDir)) return xdgDir;
5228
- if (fs.existsSync(LEGACY_DIR)) return LEGACY_DIR;
5229
- return xdgDir;
5230
- }
5231
- function ensureConfigDir() {
5232
- const dir = getConfigDir();
5233
- if (!fs.existsSync(dir)) {
5234
- fs.mkdirSync(dir, { recursive: true, mode: 448 });
5235
- }
5236
- return dir;
5237
- }
5238
- function getCredentialsPath() {
5239
- return path.join(getConfigDir(), "credentials.json");
5240
- }
5241
- function migrateIfNeeded() {
5242
- const xdgHome = process.env.XDG_CONFIG_HOME || path.join(os.homedir(), ".config");
5243
- const xdgDir = path.join(xdgHome, "numo");
5244
- if (process.env.NUMO_CONFIG_DIR) return;
5245
- const legacyCreds = path.join(LEGACY_DIR, "credentials.json");
5246
- if (!fs.existsSync(legacyCreds) || fs.existsSync(xdgDir)) return;
5247
- try {
5248
- fs.mkdirSync(xdgDir, { recursive: true, mode: 448 });
5249
- const data = fs.readFileSync(legacyCreds, "utf8");
5250
- fs.writeFileSync(path.join(xdgDir, "credentials.json"), data, { mode: 384 });
5251
- const legacyStreaks = path.join(LEGACY_DIR, "streaks.json");
5252
- if (fs.existsSync(legacyStreaks)) {
5253
- const streaksData = fs.readFileSync(legacyStreaks, "utf8");
5254
- fs.writeFileSync(path.join(xdgDir, "streaks.json"), streaksData, { mode: 384 });
5255
- }
5256
- process.stderr.write(`Migrated config from ${LEGACY_DIR} to ${xdgDir}
5257
- `);
5258
- } catch {
5259
- }
5260
- }
5261
-
5262
- // src/cli/auth/credentials.ts
5263
- function loadCredentials() {
5264
- try {
5265
- const data = JSON.parse(fs2.readFileSync(getCredentialsPath(), "utf8"));
5266
- if (typeof data?.refreshToken !== "string" || typeof data?.uid !== "string" || typeof data?.email !== "string") {
5267
- return null;
5268
- }
5269
- return data;
5270
- } catch {
5271
- return null;
5272
- }
5273
- }
5274
- function saveCredentials(creds) {
5275
- ensureConfigDir();
5276
- fs2.writeFileSync(getCredentialsPath(), JSON.stringify(creds, null, 2), { mode: 384 });
5277
- }
5278
- function clearCredentials() {
5279
- try {
5280
- const credPath = getCredentialsPath();
5281
- const stat = fs2.statSync(credPath);
5282
- fs2.writeFileSync(credPath, crypto.randomBytes(stat.size));
5283
- fs2.unlinkSync(credPath);
5284
- } catch {
5285
- }
5286
- }
5287
- var refreshInFlight = null;
5288
- async function getIdToken() {
5289
- const envToken = process.env.NUMO_TOKEN;
5290
- if (envToken) return envToken;
5291
- const creds = loadCredentials();
5292
- if (!creds) throw new Error("Not logged in. Run: numo login");
5293
- if (creds.idToken && creds.idTokenExpiry && Date.now() < creds.idTokenExpiry - 6e4) {
5294
- return creds.idToken;
5295
- }
5296
- if (refreshInFlight) return refreshInFlight;
5297
- refreshInFlight = performRefresh(creds).finally(() => {
5298
- refreshInFlight = null;
5299
- });
5300
- return refreshInFlight;
5301
- }
5302
- async function performRefresh(creds) {
5303
- const apiBase = process.env.NUMO_API_URL ?? "http://localhost:3000";
5304
- const { http: http2 } = await Promise.resolve().then(() => (init_http(), http_exports));
5305
- const resp = await http2.post(
5306
- `${apiBase}/api/auth/refresh`,
5307
- { refreshToken: creds.refreshToken }
5308
- );
5309
- creds.idToken = resp.data.idToken;
5310
- creds.refreshToken = resp.data.refreshToken ?? creds.refreshToken;
5311
- creds.idTokenExpiry = Date.now() + (resp.data.expiresIn || 3600) * 1e3;
5312
- saveCredentials(creds);
5313
- return creds.idToken;
5314
- }
5315
-
5316
- // src/cli/auth/login.ts
5437
+ init_credentials();
5317
5438
  init_prompts();
5318
5439
  init_errors();
5319
- var API_BASE2 = process.env.NUMO_API_URL ?? "http://localhost:3000";
5440
+ init_api_client();
5320
5441
  async function authenticateWithEmail(spinner) {
5321
5442
  const email = await promptText({ message: "Email", required: true });
5322
5443
  const password = await promptPassword({ message: "Password" });
5323
5444
  spinner.start("Signing in...");
5324
5445
  const resp = await http.post(
5325
- `${API_BASE2}/api/auth/login`,
5446
+ `${API_BASE}/api/auth/login`,
5326
5447
  { email, password }
5327
5448
  );
5328
5449
  return {
@@ -5388,10 +5509,11 @@ async function login(options = {}) {
5388
5509
  // src/cli/auth/register.ts
5389
5510
  init_http();
5390
5511
  var import_picocolors3 = __toESM(require_picocolors(), 1);
5512
+ init_credentials();
5391
5513
  init_prompts();
5392
5514
  init_errors();
5393
5515
  init_tty();
5394
- var API_BASE3 = process.env.NUMO_API_URL ?? "http://localhost:3000";
5516
+ init_api_client();
5395
5517
  function validateEmail(email) {
5396
5518
  const trimmed = email.trim();
5397
5519
  if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(trimmed)) {
@@ -5426,7 +5548,7 @@ function classifySignUpError(err) {
5426
5548
  }
5427
5549
  async function signUp(email, password) {
5428
5550
  try {
5429
- const resp = await http.post(`${API_BASE3}/api/auth/register`, {
5551
+ const resp = await http.post(`${API_BASE}/api/auth/register`, {
5430
5552
  email,
5431
5553
  password,
5432
5554
  tz: Intl.DateTimeFormat().resolvedOptions().timeZone,
@@ -5482,6 +5604,9 @@ async function register(options = {}) {
5482
5604
  }
5483
5605
  }
5484
5606
 
5607
+ // src/cli/cli.ts
5608
+ init_credentials();
5609
+
5485
5610
  // src/cli/commands/tasks.ts
5486
5611
  var import_picocolors8 = __toESM(require_picocolors(), 1);
5487
5612
 
@@ -5864,6 +5989,7 @@ async function runDelete(opts) {
5864
5989
  }
5865
5990
 
5866
5991
  // src/cli/lib/uid.ts
5992
+ init_credentials();
5867
5993
  init_errors();
5868
5994
  function requireUid() {
5869
5995
  const creds = loadCredentials();
@@ -5871,106 +5997,8 @@ function requireUid() {
5871
5997
  return creds.uid;
5872
5998
  }
5873
5999
 
5874
- // src/cli/lib/api-client.ts
5875
- init_http();
5876
- init_errors();
5877
- var API_BASE4 = process.env.NUMO_API_URL ?? "http://localhost:3000";
5878
- if (API_BASE4 !== "http://localhost:3000" && API_BASE4.startsWith("http://")) {
5879
- process.stderr.write("[warn] NUMO_API_URL uses HTTP \u2014 tokens sent unencrypted. Use HTTPS in production.\n");
5880
- }
5881
- var KIND_EXIT = {
5882
- AUTH_REQUIRED: ExitCode.NO_PERM,
5883
- AUTH_EXPIRED: ExitCode.NO_PERM,
5884
- AUTH_FORBIDDEN: ExitCode.NO_PERM,
5885
- INVALID_INPUT: ExitCode.USAGE,
5886
- MISSING_ARGUMENT: ExitCode.USAGE,
5887
- NOT_FOUND: ExitCode.NOT_FOUND,
5888
- CONFLICT: ExitCode.CONFLICT,
5889
- RATE_LIMITED: ExitCode.TEMP_FAIL,
5890
- NETWORK_ERROR: ExitCode.UNAVAILABLE,
5891
- TIMEOUT: ExitCode.TEMP_FAIL,
5892
- SERVICE_UNAVAILABLE: ExitCode.UNAVAILABLE
5893
- };
5894
- function toCliError(err) {
5895
- if (err instanceof CliError) return err;
5896
- const httpErr = err;
5897
- if (httpErr.code === "ECONNABORTED" || httpErr.code === "ETIMEDOUT") {
5898
- return new CliError("TIMEOUT" /* TIMEOUT */, "Request timed out", ExitCode.TEMP_FAIL, {
5899
- hint: "The API server took too long to respond.",
5900
- retryable: true
5901
- });
5902
- }
5903
- if (httpErr.code === "ECONNREFUSED" || httpErr.code === "ECONNRESET" || httpErr.code === "ENOTFOUND") {
5904
- return new CliError("NETWORK_ERROR" /* NETWORK_ERROR */, "Can't reach Numo API", ExitCode.UNAVAILABLE, {
5905
- hint: "Is the API server running? Check NUMO_API_URL.",
5906
- retryable: true
5907
- });
5908
- }
5909
- const body = httpErr.response?.data;
5910
- if (body?.error) {
5911
- const e = body.error;
5912
- const kind = e.kind ?? "UNKNOWN" /* UNKNOWN */;
5913
- const exitCode = KIND_EXIT[kind] ?? ExitCode.GENERAL;
5914
- return new CliError(kind, e.message ?? "Unknown error", exitCode, {
5915
- retryable: e.retryable,
5916
- retryAfter: e.retryAfter
5917
- });
5918
- }
5919
- return new CliError("UNKNOWN" /* UNKNOWN */, httpErr.message ?? "Unknown error", ExitCode.GENERAL);
5920
- }
5921
- async function apiHeaders() {
5922
- const token = await getIdToken();
5923
- return {
5924
- Authorization: `Bearer ${token}`,
5925
- "Content-Type": "application/json"
5926
- };
5927
- }
5928
- function url(path3, params) {
5929
- const u2 = `${API_BASE4}${path3}`;
5930
- if (!params) return u2;
5931
- const sp = new URLSearchParams();
5932
- for (const [k2, v] of Object.entries(params)) {
5933
- if (v !== void 0) sp.set(k2, v);
5934
- }
5935
- const qs = sp.toString();
5936
- return qs ? `${u2}?${qs}` : u2;
5937
- }
5938
- var api = {
5939
- async get(path3, params) {
5940
- try {
5941
- const resp = await http.get(url(path3, params), { headers: await apiHeaders() });
5942
- return resp.data;
5943
- } catch (err) {
5944
- throw toCliError(err);
5945
- }
5946
- },
5947
- async post(path3, body) {
5948
- try {
5949
- const resp = await http.post(url(path3), body, { headers: await apiHeaders() });
5950
- return resp.data;
5951
- } catch (err) {
5952
- throw toCliError(err);
5953
- }
5954
- },
5955
- async patch(path3, body) {
5956
- try {
5957
- const resp = await http.patch(url(path3), body, { headers: await apiHeaders() });
5958
- return resp.data;
5959
- } catch (err) {
5960
- throw toCliError(err);
5961
- }
5962
- },
5963
- async del(path3) {
5964
- try {
5965
- const resp = await http.delete(url(path3), { headers: await apiHeaders() });
5966
- return resp.data;
5967
- } catch (err) {
5968
- throw toCliError(err);
5969
- }
5970
- }
5971
- };
5972
-
5973
6000
  // src/cli/services/tasks.ts
6001
+ init_api_client();
5974
6002
  async function listTasks(uid, opts) {
5975
6003
  return api.get("/api/tasks", {
5976
6004
  date: opts.date,
@@ -9505,6 +9533,7 @@ ${import_picocolors9.default.dim("Next page:")} ${import_picocolors9.default.dim
9505
9533
  }
9506
9534
 
9507
9535
  // src/cli/services/posts.ts
9536
+ init_api_client();
9508
9537
  async function listPosts(opts) {
9509
9538
  return api.get("/api/posts", {
9510
9539
  cursor: opts.cursor,
@@ -9525,6 +9554,7 @@ async function deletePost(uid, id) {
9525
9554
  }
9526
9555
 
9527
9556
  // src/cli/services/comments.ts
9557
+ init_api_client();
9528
9558
  async function listComments(postId, opts) {
9529
9559
  return api.get(`/api/posts/${encodeURIComponent(postId)}/comments`, {
9530
9560
  cursor: opts.cursor,
@@ -9539,6 +9569,7 @@ async function deleteComment(uid, postId, commentId) {
9539
9569
  }
9540
9570
 
9541
9571
  // src/cli/services/replies.ts
9572
+ init_api_client();
9542
9573
  async function listReplies(postId, commentId, opts) {
9543
9574
  return api.get(`/api/posts/${encodeURIComponent(postId)}/comments/${encodeURIComponent(commentId)}/replies`, {
9544
9575
  cursor: opts.cursor,
@@ -9854,6 +9885,7 @@ Examples:
9854
9885
  }
9855
9886
 
9856
9887
  // src/cli/services/profile.ts
9888
+ init_api_client();
9857
9889
  async function getProfile() {
9858
9890
  return api.get("/api/profile");
9859
9891
  }
@@ -9880,8 +9912,9 @@ function registerProfileCommands(program3) {
9880
9912
 
9881
9913
  // src/cli/commands/doctor.ts
9882
9914
  var import_picocolors11 = __toESM(require_picocolors(), 1);
9915
+ init_credentials();
9916
+ init_api_client();
9883
9917
  init_tty();
9884
- var API_BASE5 = process.env.NUMO_API_URL ?? "http://localhost:3000";
9885
9918
  async function runChecks() {
9886
9919
  const checks = [];
9887
9920
  const nodeVersion = process.version;
@@ -9894,7 +9927,7 @@ async function runChecks() {
9894
9927
  checks.push({
9895
9928
  name: "api_url",
9896
9929
  status: process.env.NUMO_API_URL ? "ok" : "warn",
9897
- message: process.env.NUMO_API_URL ? `API URL: ${API_BASE5}` : `NUMO_API_URL not set (using default: ${API_BASE5})`
9930
+ message: process.env.NUMO_API_URL ? `API URL: ${API_BASE}` : `NUMO_API_URL not set (using default: ${API_BASE})`
9898
9931
  });
9899
9932
  const creds = loadCredentials();
9900
9933
  checks.push({
@@ -9913,7 +9946,7 @@ async function runChecks() {
9913
9946
  checks.push({ name: "token", status: "fail", message: "Skipped (no credentials)" });
9914
9947
  }
9915
9948
  try {
9916
- const resp = await fetch(`${API_BASE5}/api/health`, { signal: AbortSignal.timeout(5e3) });
9949
+ const resp = await fetch(`${API_BASE}/api/health`, { signal: AbortSignal.timeout(5e3) });
9917
9950
  checks.push({ name: "api_reachable", status: "ok", message: `API server reachable (HTTP ${resp.status})` });
9918
9951
  } catch (err) {
9919
9952
  checks.push({ name: "api_reachable", status: "fail", message: `API server unreachable: ${err.message}` });
@@ -9946,10 +9979,14 @@ function registerDoctorCommand(program3) {
9946
9979
  });
9947
9980
  }
9948
9981
 
9982
+ // src/cli/cli.ts
9983
+ init_dirs();
9984
+
9949
9985
  // src/cli/lib/update-check.ts
9950
9986
  var fs4 = __toESM(require("fs"), 1);
9951
9987
  var path2 = __toESM(require("path"), 1);
9952
9988
  var import_picocolors12 = __toESM(require_picocolors(), 1);
9989
+ init_dirs();
9953
9990
  init_tty();
9954
9991
  var CHECK_INTERVAL = 24 * 60 * 60 * 1e3;
9955
9992
  var PACKAGE_NAME = "numo-cli";
@@ -10015,7 +10052,7 @@ function fetchLatestVersion(state) {
10015
10052
  // src/cli/cli.ts
10016
10053
  init_tty();
10017
10054
  init_errors();
10018
- var CLI_VERSION = true ? "1.3.0" : "0.0.0-dev";
10055
+ var CLI_VERSION = true ? "1.5.0" : "0.0.0-dev";
10019
10056
  var program2 = new Command();
10020
10057
  program2.name("numo").description("CLI for Numo \u2014 programmatic access for humans and AI agents").version(CLI_VERSION).option("--json [fields]", "Output as JSON (optionally: comma-separated field names)").option("-q, --quiet", "Suppress interactive output, implies --json").hook("preAction", (thisCommand) => {
10021
10058
  const opts = thisCommand.optsWithGlobals();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "numo-cli",
3
- "version": "1.3.0",
3
+ "version": "1.5.0",
4
4
  "description": "CLI and programmatic API for Numo ADHD planner — create, complete, and manage tasks from the terminal or AI agents",
5
5
  "type": "module",
6
6
  "bin": {