numo-cli 1.3.0 → 1.6.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 (4) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +13 -118
  3. package/dist/cli.cjs +1287 -1206
  4. package/package.json +14 -14
package/dist/cli.cjs CHANGED
@@ -3217,6 +3217,390 @@ 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(/\beyJ[\w-]+\.[\w-]+\.[\w-]+\b/g, "<jwt>").replace(/[\w.+-]+@[\w-]+\.[\w.-]+/g, "<email>").replace(/[A-Za-z0-9_=+/-]{32,}/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-base.ts
3377
+ function isLoopback(host) {
3378
+ return host === "localhost" || host === "127.0.0.1" || host === "::1";
3379
+ }
3380
+ function classifyApiBase(base = API_BASE, fromEnv = FROM_ENV) {
3381
+ let u2;
3382
+ try {
3383
+ u2 = new URL(base);
3384
+ } catch {
3385
+ return { ok: false, message: `Invalid NUMO_API_URL: ${base}` };
3386
+ }
3387
+ if (u2.protocol !== "http:" && u2.protocol !== "https:") {
3388
+ return { ok: false, message: `NUMO_API_URL must be http(s) \u2014 got "${u2.protocol}"` };
3389
+ }
3390
+ const insecure = u2.protocol === "http:" && !isLoopback(u2.hostname);
3391
+ const trusted = isLoopback(u2.hostname) || u2.hostname === "numo.ai" || u2.hostname.endsWith(".numo.ai");
3392
+ if (fromEnv && !trusted && process.env.NUMO_ALLOW_CUSTOM_HOST !== "1") {
3393
+ return {
3394
+ ok: false,
3395
+ message: `Refusing to send credentials to untrusted host "${u2.hostname}". Use https://api.numo.ai, or set NUMO_ALLOW_CUSTOM_HOST=1 for a self-hosted/staging server.`
3396
+ };
3397
+ }
3398
+ return { ok: true, insecure };
3399
+ }
3400
+ function assertSafeApiBase() {
3401
+ const verdict = classifyApiBase();
3402
+ if (!verdict.ok) {
3403
+ throw new CliError("CONFIG_ERROR" /* CONFIG_ERROR */, verdict.message, ExitCode.CONFIG, {
3404
+ suggestion: "export NUMO_ALLOW_CUSTOM_HOST=1"
3405
+ });
3406
+ }
3407
+ if (verdict.insecure && !warnedInsecure) {
3408
+ warnedInsecure = true;
3409
+ process.stderr.write("[warn] NUMO_API_URL uses HTTP \u2014 tokens sent unencrypted. Use HTTPS in production.\n");
3410
+ }
3411
+ }
3412
+ var API_BASE, FROM_ENV, warnedInsecure;
3413
+ var init_api_base = __esm({
3414
+ "src/cli/lib/api-base.ts"() {
3415
+ "use strict";
3416
+ init_errors();
3417
+ API_BASE = process.env.NUMO_API_URL ?? (true ? "https://api.numo.ai" : "http://localhost:3000");
3418
+ FROM_ENV = process.env.NUMO_API_URL !== void 0;
3419
+ warnedInsecure = false;
3420
+ }
3421
+ });
3422
+
3423
+ // src/cli/lib/api-client.ts
3424
+ var api_client_exports = {};
3425
+ __export(api_client_exports, {
3426
+ API_BASE: () => API_BASE,
3427
+ api: () => api
3428
+ });
3429
+ function toCliError(err) {
3430
+ if (err instanceof CliError) return err;
3431
+ const httpErr = err;
3432
+ if (httpErr.code === "ECONNABORTED" || httpErr.code === "ETIMEDOUT") {
3433
+ return new CliError("TIMEOUT" /* TIMEOUT */, "Request timed out", ExitCode.TEMP_FAIL, {
3434
+ hint: "The API server took too long to respond.",
3435
+ retryable: true
3436
+ });
3437
+ }
3438
+ if (httpErr.code === "ECONNREFUSED" || httpErr.code === "ECONNRESET" || httpErr.code === "ENOTFOUND") {
3439
+ return new CliError("NETWORK_ERROR" /* NETWORK_ERROR */, "Can't reach Numo API", ExitCode.UNAVAILABLE, {
3440
+ hint: "Is the API server running? Check NUMO_API_URL.",
3441
+ retryable: true
3442
+ });
3443
+ }
3444
+ const body = httpErr.response?.data;
3445
+ if (body?.error) {
3446
+ const e = body.error;
3447
+ const kind = e.kind ?? "UNKNOWN" /* UNKNOWN */;
3448
+ const exitCode = KIND_EXIT[kind] ?? ExitCode.GENERAL;
3449
+ return new CliError(kind, sanitizeErrorMessage(e.message ?? "Unknown error"), exitCode, {
3450
+ retryable: e.retryable,
3451
+ retryAfter: e.retryAfter
3452
+ });
3453
+ }
3454
+ return new CliError("UNKNOWN" /* UNKNOWN */, sanitizeErrorMessage(httpErr.message ?? "Unknown error"), ExitCode.GENERAL);
3455
+ }
3456
+ async function apiHeaders() {
3457
+ assertSafeApiBase();
3458
+ const token = await getIdToken();
3459
+ return {
3460
+ Authorization: `Bearer ${token}`,
3461
+ "Content-Type": "application/json"
3462
+ };
3463
+ }
3464
+ function url(path3, params) {
3465
+ const u2 = `${API_BASE}${path3}`;
3466
+ if (!params) return u2;
3467
+ const sp = new URLSearchParams();
3468
+ for (const [k2, v] of Object.entries(params)) {
3469
+ if (v !== void 0) sp.set(k2, v);
3470
+ }
3471
+ const qs = sp.toString();
3472
+ return qs ? `${u2}?${qs}` : u2;
3473
+ }
3474
+ var KIND_EXIT, api;
3475
+ var init_api_client = __esm({
3476
+ "src/cli/lib/api-client.ts"() {
3477
+ "use strict";
3478
+ init_credentials();
3479
+ init_http();
3480
+ init_errors();
3481
+ init_api_base();
3482
+ KIND_EXIT = {
3483
+ AUTH_REQUIRED: ExitCode.NO_PERM,
3484
+ AUTH_EXPIRED: ExitCode.NO_PERM,
3485
+ AUTH_FORBIDDEN: ExitCode.NO_PERM,
3486
+ INVALID_INPUT: ExitCode.USAGE,
3487
+ MISSING_ARGUMENT: ExitCode.USAGE,
3488
+ NOT_FOUND: ExitCode.NOT_FOUND,
3489
+ CONFLICT: ExitCode.CONFLICT,
3490
+ RATE_LIMITED: ExitCode.TEMP_FAIL,
3491
+ NETWORK_ERROR: ExitCode.UNAVAILABLE,
3492
+ TIMEOUT: ExitCode.TEMP_FAIL,
3493
+ SERVICE_UNAVAILABLE: ExitCode.UNAVAILABLE
3494
+ };
3495
+ api = {
3496
+ async get(path3, params) {
3497
+ try {
3498
+ const resp = await http.get(url(path3, params), { headers: await apiHeaders() });
3499
+ return resp.data;
3500
+ } catch (err) {
3501
+ throw toCliError(err);
3502
+ }
3503
+ },
3504
+ async post(path3, body) {
3505
+ try {
3506
+ const resp = await http.post(url(path3), body, { headers: await apiHeaders() });
3507
+ return resp.data;
3508
+ } catch (err) {
3509
+ throw toCliError(err);
3510
+ }
3511
+ },
3512
+ async patch(path3, body) {
3513
+ try {
3514
+ const resp = await http.patch(url(path3), body, { headers: await apiHeaders() });
3515
+ return resp.data;
3516
+ } catch (err) {
3517
+ throw toCliError(err);
3518
+ }
3519
+ },
3520
+ async del(path3) {
3521
+ try {
3522
+ const resp = await http.delete(url(path3), { headers: await apiHeaders() });
3523
+ return resp.data;
3524
+ } catch (err) {
3525
+ throw toCliError(err);
3526
+ }
3527
+ }
3528
+ };
3529
+ }
3530
+ });
3531
+
3532
+ // src/cli/auth/credentials.ts
3533
+ function loadCredentials() {
3534
+ try {
3535
+ const path3 = getCredentialsPath();
3536
+ if (process.platform !== "win32" && fs2.statSync(path3).mode & 63) {
3537
+ process.stderr.write(`[warn] credentials file is group/other-readable. Run: chmod 600 ${path3}
3538
+ `);
3539
+ }
3540
+ const data = JSON.parse(fs2.readFileSync(path3, "utf8"));
3541
+ if (typeof data?.refreshToken !== "string" || typeof data?.uid !== "string" || typeof data?.email !== "string") {
3542
+ return null;
3543
+ }
3544
+ return data;
3545
+ } catch {
3546
+ return null;
3547
+ }
3548
+ }
3549
+ function saveCredentials(creds) {
3550
+ ensureConfigDir();
3551
+ const path3 = getCredentialsPath();
3552
+ fs2.writeFileSync(path3, JSON.stringify(creds, null, 2), { mode: 384 });
3553
+ if (process.platform !== "win32") fs2.chmodSync(path3, 384);
3554
+ }
3555
+ function clearCredentials() {
3556
+ try {
3557
+ const credPath = getCredentialsPath();
3558
+ const stat = fs2.statSync(credPath);
3559
+ fs2.writeFileSync(credPath, crypto.randomBytes(stat.size));
3560
+ fs2.unlinkSync(credPath);
3561
+ } catch {
3562
+ }
3563
+ }
3564
+ async function getIdToken() {
3565
+ const envToken = process.env.NUMO_TOKEN;
3566
+ if (envToken) return envToken;
3567
+ const creds = loadCredentials();
3568
+ if (!creds) throw new Error("Not logged in. Run: numo login");
3569
+ if (creds.idToken && creds.idTokenExpiry && Date.now() < creds.idTokenExpiry - 6e4) {
3570
+ return creds.idToken;
3571
+ }
3572
+ if (refreshInFlight) return refreshInFlight;
3573
+ refreshInFlight = performRefresh(creds).finally(() => {
3574
+ refreshInFlight = null;
3575
+ });
3576
+ return refreshInFlight;
3577
+ }
3578
+ async function performRefresh(creds) {
3579
+ assertSafeApiBase();
3580
+ const { API_BASE: apiBase } = await Promise.resolve().then(() => (init_api_client(), api_client_exports));
3581
+ const { http: http2 } = await Promise.resolve().then(() => (init_http(), http_exports));
3582
+ const resp = await http2.post(
3583
+ `${apiBase}/api/auth/refresh`,
3584
+ { refreshToken: creds.refreshToken }
3585
+ );
3586
+ creds.idToken = resp.data.idToken;
3587
+ creds.refreshToken = resp.data.refreshToken ?? creds.refreshToken;
3588
+ creds.idTokenExpiry = Date.now() + (resp.data.expiresIn || 3600) * 1e3;
3589
+ saveCredentials(creds);
3590
+ return creds.idToken;
3591
+ }
3592
+ var fs2, crypto, refreshInFlight;
3593
+ var init_credentials = __esm({
3594
+ "src/cli/auth/credentials.ts"() {
3595
+ "use strict";
3596
+ fs2 = __toESM(require("fs"), 1);
3597
+ crypto = __toESM(require("crypto"), 1);
3598
+ init_dirs();
3599
+ init_api_base();
3600
+ refreshInFlight = null;
3601
+ }
3602
+ });
3603
+
3220
3604
  // src/cli/lib/tty.ts
3221
3605
  function isInteractive() {
3222
3606
  if (!process.stdin.isTTY || !process.stdout.isTTY) return false;
@@ -4931,7 +5315,12 @@ async function loadClack() {
4931
5315
  }
4932
5316
  async function promptText(opts) {
4933
5317
  if (!isInteractive()) {
4934
- throw new Error(`Missing required input: ${opts.message}. Use flags in non-interactive mode.`);
5318
+ throw new CliError(
5319
+ "MISSING_ARGUMENT" /* MISSING_ARGUMENT */,
5320
+ `Missing required input: ${opts.message}`,
5321
+ ExitCode.USAGE,
5322
+ { hint: "Use flags in non-interactive mode." }
5323
+ );
4935
5324
  }
4936
5325
  const p = await loadClack();
4937
5326
  const value = await p.text({
@@ -4946,7 +5335,12 @@ async function promptText(opts) {
4946
5335
  }
4947
5336
  async function promptPassword(opts) {
4948
5337
  if (!isInteractive()) {
4949
- throw new Error(`Missing required input: ${opts.message}. Use flags in non-interactive mode.`);
5338
+ throw new CliError(
5339
+ "MISSING_ARGUMENT" /* MISSING_ARGUMENT */,
5340
+ `Missing required input: ${opts.message}`,
5341
+ ExitCode.USAGE,
5342
+ { hint: "Use flags in non-interactive mode." }
5343
+ );
4950
5344
  }
4951
5345
  const p = await loadClack();
4952
5346
  const value = await p.password({
@@ -4960,7 +5354,12 @@ async function promptPassword(opts) {
4960
5354
  }
4961
5355
  async function promptSelect(opts) {
4962
5356
  if (!isInteractive()) {
4963
- throw new Error(`Missing required input: ${opts.message}. Use flags in non-interactive mode.`);
5357
+ throw new CliError(
5358
+ "MISSING_ARGUMENT" /* MISSING_ARGUMENT */,
5359
+ `Missing required input: ${opts.message}`,
5360
+ ExitCode.USAGE,
5361
+ { hint: "Use flags in non-interactive mode." }
5362
+ );
4964
5363
  }
4965
5364
  const p = await loadClack();
4966
5365
  const value = await p.select({
@@ -4988,137 +5387,39 @@ async function promptConfirm(opts) {
4988
5387
  }
4989
5388
  async function promptMultiSelect(opts) {
4990
5389
  if (!isInteractive()) {
4991
- throw new Error(`Missing required input: ${opts.message}. Use flags in non-interactive mode.`);
4992
- }
4993
- const p = await loadClack();
4994
- const value = await p.multiselect({
4995
- 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);
5390
+ throw new CliError(
5391
+ "MISSING_ARGUMENT" /* MISSING_ARGUMENT */,
5392
+ `Missing required input: ${opts.message}`,
5393
+ ExitCode.USAGE,
5394
+ { hint: "Use flags in non-interactive mode." }
5395
+ );
5041
5396
  }
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
- });
5397
+ const p = await loadClack();
5398
+ const value = await p.multiselect({
5399
+ message: opts.message,
5400
+ options: opts.options,
5401
+ required: opts.required ?? false
5402
+ });
5403
+ if (p.isCancel(value)) {
5404
+ process.exit(130);
5047
5405
  }
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 });
5406
+ return value;
5052
5407
  }
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>");
5408
+ async function promptForMissing(opts) {
5409
+ if (opts.value !== void 0 && opts.value !== "") {
5410
+ return opts.value;
5411
+ }
5412
+ return promptText({
5413
+ message: opts.message,
5414
+ placeholder: opts.placeholder,
5415
+ required: opts.required ?? true
5416
+ });
5055
5417
  }
5056
- var ExitCode, CliError, Errors;
5057
- var init_errors = __esm({
5058
- "src/cli/lib/errors.ts"() {
5418
+ var init_prompts = __esm({
5419
+ "src/cli/lib/prompts.ts"() {
5059
5420
  "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
- };
5421
+ init_tty();
5422
+ init_errors();
5122
5423
  }
5123
5424
  });
5124
5425
 
@@ -5128,6 +5429,7 @@ __export(phone_login_exports, {
5128
5429
  authenticateWithPhone: () => authenticateWithPhone
5129
5430
  });
5130
5431
  async function authenticateWithPhone(spinner) {
5432
+ assertSafeApiBase();
5131
5433
  const p = await Promise.resolve().then(() => (init_dist2(), dist_exports));
5132
5434
  const phone = await promptText({
5133
5435
  message: "Phone number (with country code)",
@@ -5144,7 +5446,7 @@ async function authenticateWithPhone(spinner) {
5144
5446
  const { sessionId, verifyUrl } = startResp.data;
5145
5447
  spinner.stop("");
5146
5448
  p.log.info("Opening browser for phone verification...");
5147
- p.log.info(import_picocolors.default.dim(`If the browser does not open, visit: ${verifyUrl}`));
5449
+ p.log.info(import_picocolors4.default.dim(`If the browser does not open, visit: ${verifyUrl}`));
5148
5450
  const { default: open } = await import("open");
5149
5451
  const cp = await open(verifyUrl);
5150
5452
  cp.unref();
@@ -5171,15 +5473,16 @@ async function authenticateWithPhone(spinner) {
5171
5473
  }
5172
5474
  throw Errors.networkError("Phone verification timed out. Try again.");
5173
5475
  }
5174
- var import_picocolors, API_BASE, POLL_INTERVAL, POLL_TIMEOUT;
5476
+ var import_picocolors4, POLL_INTERVAL, POLL_TIMEOUT;
5175
5477
  var init_phone_login = __esm({
5176
5478
  "src/cli/auth/phone-login.ts"() {
5177
5479
  "use strict";
5178
5480
  init_http();
5179
- import_picocolors = __toESM(require_picocolors(), 1);
5481
+ import_picocolors4 = __toESM(require_picocolors(), 1);
5180
5482
  init_errors();
5181
5483
  init_prompts();
5182
- API_BASE = process.env.NUMO_API_URL ?? "http://localhost:3000";
5484
+ init_api_client();
5485
+ init_api_base();
5183
5486
  POLL_INTERVAL = 2e3;
5184
5487
  POLL_TIMEOUT = 5 * 60 * 1e3;
5185
5488
  }
@@ -5203,297 +5506,82 @@ var {
5203
5506
  } = import_index.default;
5204
5507
 
5205
5508
  // src/cli/cli.ts
5206
- var import_picocolors13 = __toESM(require_picocolors(), 1);
5509
+ var import_picocolors12 = __toESM(require_picocolors(), 1);
5207
5510
 
5208
5511
  // src/cli/auth/login.ts
5209
5512
  init_http();
5210
- 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
- }
5513
+ var import_picocolors5 = __toESM(require_picocolors(), 1);
5514
+ init_credentials();
5315
5515
 
5316
- // src/cli/auth/login.ts
5317
- init_prompts();
5318
- init_errors();
5319
- var API_BASE2 = process.env.NUMO_API_URL ?? "http://localhost:3000";
5320
- async function authenticateWithEmail(spinner) {
5321
- const email = await promptText({ message: "Email", required: true });
5322
- const password = await promptPassword({ message: "Password" });
5323
- spinner.start("Signing in...");
5324
- const resp = await http.post(
5325
- `${API_BASE2}/api/auth/login`,
5326
- { email, password }
5327
- );
5328
- return {
5329
- refreshToken: resp.data.refreshToken,
5330
- uid: resp.data.uid,
5331
- displayName: resp.data.email ?? email,
5332
- idToken: resp.data.idToken,
5333
- idTokenExpiry: Date.now() + (resp.data.expiresIn || 3600) * 1e3
5334
- };
5335
- }
5336
- function printSuccess(displayName) {
5337
- const lines = [
5338
- ` ${import_picocolors2.default.dim("$")} numo tasks list --date YYYY-MM-DD List tasks for a date`,
5339
- ` ${import_picocolors2.default.dim("$")} numo tasks create --text "..." Create a task`,
5340
- ` ${import_picocolors2.default.dim("$")} numo profile View your profile`
5341
- ];
5342
- console.log(`
5343
- ${import_picocolors2.default.bold("Available commands:")}
5344
- ${lines.join("\n")}
5345
- `);
5346
- }
5347
- async function login(options = {}) {
5348
- const p = await Promise.resolve().then(() => (init_dist2(), dist_exports));
5349
- p.intro(import_picocolors2.default.bold("Numo \u2014 Login"));
5350
- let method = options.phone ? "phone" : "email";
5351
- if (!options.phone) {
5352
- method = await promptSelect({
5353
- message: "How would you like to sign in?",
5354
- options: [
5355
- { value: "email", label: "Email & password" },
5356
- { value: "phone", label: "Phone number (SMS)" }
5357
- ]
5358
- });
5359
- }
5360
- const s = p.spinner();
5361
- try {
5362
- let result;
5363
- if (method === "phone") {
5364
- const { authenticateWithPhone: authenticateWithPhone2 } = await Promise.resolve().then(() => (init_phone_login(), phone_login_exports));
5365
- result = await authenticateWithPhone2(s);
5366
- } else {
5367
- result = await authenticateWithEmail(s);
5368
- }
5369
- saveCredentials({
5370
- refreshToken: result.refreshToken,
5371
- uid: result.uid,
5372
- email: result.displayName,
5373
- idToken: result.idToken,
5374
- idTokenExpiry: result.idTokenExpiry
5375
- });
5376
- s.stop(`Logged in as ${import_picocolors2.default.green(result.displayName)}`);
5377
- p.outro("You are ready to go!");
5378
- printSuccess(result.displayName);
5379
- } catch (err) {
5380
- const classified = err instanceof CliError ? err : classifyError(err);
5381
- s.stop(import_picocolors2.default.red("Login failed"));
5382
- p.log.error(classified.message);
5383
- if (classified.options.hint) p.log.warning(classified.options.hint);
5384
- process.exit(classified.exitCode);
5516
+ // src/cli/lib/command-map.ts
5517
+ var import_picocolors = __toESM(require_picocolors(), 1);
5518
+ function collectCommands(root) {
5519
+ const out = [];
5520
+ function walk(cmd, prefix) {
5521
+ for (const sub of cmd.commands) {
5522
+ const fullName = prefix ? `${prefix} ${sub.name()}` : sub.name();
5523
+ if (sub.commands.length > 0) {
5524
+ walk(sub, fullName);
5525
+ } else {
5526
+ out.push({
5527
+ name: fullName,
5528
+ description: sub.description(),
5529
+ options: sub.options.map((o) => o.flags)
5530
+ });
5531
+ }
5532
+ }
5533
+ }
5534
+ walk(root, "");
5535
+ return out;
5536
+ }
5537
+ var FOCUS_GROUPS = /* @__PURE__ */ new Set(["tasks", "posts", "profile", "doctor"]);
5538
+ function focusCommands(commands) {
5539
+ return commands.filter((c) => FOCUS_GROUPS.has(c.name.split(" ")[0]));
5540
+ }
5541
+ function formatCommandMap(commands) {
5542
+ const lines = [];
5543
+ let lastGroup = "";
5544
+ for (const cmd of commands) {
5545
+ const group = cmd.name.split(" ")[0];
5546
+ if (group !== lastGroup) {
5547
+ if (lastGroup) lines.push("");
5548
+ lines.push(` ${import_picocolors.default.bold(group.charAt(0).toUpperCase() + group.slice(1) + ":")}`);
5549
+ lastGroup = group;
5550
+ }
5551
+ lines.push(` numo ${cmd.name.padEnd(30)} ${import_picocolors.default.dim(cmd.description)}`);
5385
5552
  }
5553
+ return lines.join("\n");
5386
5554
  }
5387
5555
 
5388
- // src/cli/auth/register.ts
5389
- init_http();
5390
- var import_picocolors3 = __toESM(require_picocolors(), 1);
5556
+ // src/cli/auth/login.ts
5391
5557
  init_prompts();
5392
5558
  init_errors();
5559
+ init_api_client();
5560
+ init_api_base();
5561
+
5562
+ // src/cli/lib/quiet.ts
5393
5563
  init_tty();
5394
- var API_BASE3 = process.env.NUMO_API_URL ?? "http://localhost:3000";
5395
- function validateEmail(email) {
5396
- const trimmed = email.trim();
5397
- if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(trimmed)) {
5398
- throw Errors.invalidInput("Invalid email address");
5399
- }
5400
- return trimmed;
5401
- }
5402
- function validatePassword(password) {
5403
- if (password.length < 6) {
5404
- throw Errors.invalidInput("Password is too weak (min 6 characters)");
5405
- }
5406
- return password;
5407
- }
5408
- function classifySignUpError(err) {
5409
- if (err instanceof CliError) return err;
5410
- const resp = err?.response?.data?.error;
5411
- const kind = resp?.kind ?? "";
5412
- const msg = resp?.message ?? "";
5413
- if (kind === "CONFLICT" || msg.includes("already in use")) {
5414
- return Errors.invalidInput(
5415
- "Email already in use",
5416
- "Already have an account? Run: numo login"
5417
- );
5418
- }
5419
- if (msg.includes("Invalid email")) {
5420
- return Errors.invalidInput("Invalid email address");
5421
- }
5422
- if (msg.includes("Password too weak") || msg.includes("min 6")) {
5423
- return Errors.invalidInput("Password is too weak (min 6 characters)");
5424
- }
5425
- return classifyError(err);
5426
- }
5427
- async function signUp(email, password) {
5428
- try {
5429
- const resp = await http.post(`${API_BASE3}/api/auth/register`, {
5430
- email,
5431
- password,
5432
- tz: Intl.DateTimeFormat().resolvedOptions().timeZone,
5433
- tzOffset: (/* @__PURE__ */ new Date()).getTimezoneOffset()
5434
- });
5435
- return {
5436
- refreshToken: resp.data.refreshToken,
5437
- uid: resp.data.uid,
5438
- displayName: resp.data.email ?? email,
5439
- idToken: resp.data.idToken,
5440
- idTokenExpiry: Date.now() + (resp.data.expiresIn || 3600) * 1e3
5441
- };
5442
- } catch (err) {
5443
- throw classifySignUpError(err);
5564
+ var noopSpinner = {
5565
+ start: () => {
5566
+ },
5567
+ stop: () => {
5444
5568
  }
5569
+ };
5570
+ function isQuietMode(opts = {}) {
5571
+ return !!(opts.quiet || opts.json || !isInteractive());
5445
5572
  }
5446
- async function register(options = {}) {
5573
+ async function makeClackSpinner(quietMode) {
5574
+ if (quietMode) return noopSpinner;
5447
5575
  const p = await Promise.resolve().then(() => (init_dist2(), dist_exports));
5448
- p.intro(import_picocolors3.default.bold("Numo \u2014 Register"));
5449
- const s = p.spinner();
5450
- let spinnerActive = false;
5451
- try {
5452
- const email = validateEmail(
5453
- options.email ?? await promptText({ message: "Email", required: true })
5454
- );
5455
- const rawPassword = options.password ?? await promptPassword({ message: "Password" });
5456
- if (!options.password && isInteractive()) {
5457
- const confirm = await promptPassword({ message: "Confirm password" });
5458
- if (rawPassword !== confirm) {
5459
- throw Errors.invalidInput("Passwords do not match");
5460
- }
5461
- }
5462
- const password = validatePassword(rawPassword);
5463
- s.start("Creating account...");
5464
- spinnerActive = true;
5465
- const result = await signUp(email, password);
5466
- saveCredentials({
5467
- refreshToken: result.refreshToken,
5468
- uid: result.uid,
5469
- email: result.displayName,
5470
- idToken: result.idToken,
5471
- idTokenExpiry: result.idTokenExpiry
5472
- });
5473
- s.stop(`Registered as ${import_picocolors3.default.green(result.displayName)}`);
5474
- p.outro("Welcome to Numo!");
5475
- printSuccess(result.displayName);
5476
- } catch (err) {
5477
- const classified = err instanceof CliError ? err : classifySignUpError(err);
5478
- if (spinnerActive) s.stop(import_picocolors3.default.red("Registration failed"));
5479
- p.log.error(classified.message);
5480
- if (classified.options.hint) p.log.warning(classified.options.hint);
5481
- process.exit(classified.exitCode);
5482
- }
5576
+ return p.spinner();
5483
5577
  }
5484
5578
 
5485
- // src/cli/commands/tasks.ts
5486
- var import_picocolors8 = __toESM(require_picocolors(), 1);
5487
-
5488
- // src/cli/lib/actions.ts
5489
- init_tty();
5490
-
5491
5579
  // src/cli/lib/output.ts
5492
- var import_picocolors5 = __toESM(require_picocolors(), 1);
5580
+ var import_picocolors3 = __toESM(require_picocolors(), 1);
5493
5581
  init_tty();
5494
5582
 
5495
5583
  // src/cli/lib/table.ts
5496
- var import_picocolors4 = __toESM(require_picocolors(), 1);
5584
+ var import_picocolors2 = __toESM(require_picocolors(), 1);
5497
5585
  init_tty();
5498
5586
  var BOX = isUnicodeSupported ? {
5499
5587
  topLeft: String.fromCodePoint(9484),
@@ -5532,17 +5620,17 @@ var BOX = isUnicodeSupported ? {
5532
5620
  cross: "+"
5533
5621
  };
5534
5622
  function renderTable(headers, rows, emptyMessage = "(no results)") {
5535
- if (rows.length === 0) return import_picocolors4.default.dim(emptyMessage);
5623
+ if (rows.length === 0) return import_picocolors2.default.dim(emptyMessage);
5536
5624
  const widths = headers.map(
5537
5625
  (h2, i) => Math.max(h2.length, ...rows.map((r) => (r[i] ?? "").length))
5538
5626
  );
5539
- const pad = (s, w) => s + " ".repeat(Math.max(0, w - s.length));
5627
+ const pad2 = (s, w) => s + " ".repeat(Math.max(0, w - s.length));
5540
5628
  const topBorder = BOX.topLeft + widths.map((w) => BOX.horizontal.repeat(w + 2)).join(BOX.midTop) + BOX.topRight;
5541
5629
  const midBorder = BOX.midLeft + widths.map((w) => BOX.horizontal.repeat(w + 2)).join(BOX.cross) + BOX.midRight;
5542
5630
  const bottomBorder = BOX.bottomLeft + widths.map((w) => BOX.horizontal.repeat(w + 2)).join(BOX.midBottom) + BOX.bottomRight;
5543
5631
  const formatRow = (cells, bold = false) => BOX.vertical + cells.map((c, i) => {
5544
- const padded = pad(c, widths[i]);
5545
- return ` ${bold ? import_picocolors4.default.bold(padded) : padded} `;
5632
+ const padded = pad2(c, widths[i]);
5633
+ return ` ${bold ? import_picocolors2.default.bold(padded) : padded} `;
5546
5634
  }).join(BOX.vertical) + BOX.vertical;
5547
5635
  const lines = [
5548
5636
  topBorder,
@@ -5576,17 +5664,15 @@ function printRecord(fields) {
5576
5664
  const visible = fields.filter(([, v]) => v != null && v !== "");
5577
5665
  const maxLabel = Math.max(...visible.map(([l]) => l.length));
5578
5666
  for (const [label, value] of visible) {
5579
- console.log(` ${import_picocolors5.default.bold(label.padEnd(maxLabel))} ${value}`);
5667
+ console.log(` ${import_picocolors3.default.bold(label.padEnd(maxLabel))} ${value}`);
5580
5668
  }
5581
5669
  }
5582
5670
  function outputResult(data, asJson) {
5583
- if (asJson) {
5584
- printJson(data);
5585
- } else if (typeof data === "string") {
5671
+ if (!asJson && typeof data === "string") {
5586
5672
  console.log(data);
5587
- } else {
5588
- printJson(data);
5673
+ return;
5589
5674
  }
5675
+ printJson(data);
5590
5676
  }
5591
5677
  function pickFields(obj, fields) {
5592
5678
  const result = {};
@@ -5621,148 +5707,145 @@ function outputError(err, asJson) {
5621
5707
  console.error(JSON.stringify(structured.toJSON(), null, 2));
5622
5708
  } else {
5623
5709
  console.error(`
5624
- ${import_picocolors5.default.red("Error")}: ${structured.message}`);
5710
+ ${import_picocolors3.default.red("Error")}: ${structured.message}`);
5625
5711
  if (structured.options.suggestion) {
5626
5712
  console.error(`
5627
- ${import_picocolors5.default.dim("Fix:")} ${import_picocolors5.default.cyan("$")} ${import_picocolors5.default.bold(structured.options.suggestion)}`);
5713
+ ${import_picocolors3.default.dim("Fix:")} ${import_picocolors3.default.cyan("$")} ${import_picocolors3.default.bold(structured.options.suggestion)}`);
5628
5714
  }
5629
5715
  if (structured.options.hint) {
5630
- console.error(` ${import_picocolors5.default.dim(structured.options.hint)}`);
5716
+ console.error(` ${import_picocolors3.default.dim(structured.options.hint)}`);
5631
5717
  }
5632
5718
  console.error("");
5633
5719
  }
5634
5720
  process.exit(structured.exitCode);
5635
5721
  }
5636
5722
 
5637
- // src/cli/lib/spinner.ts
5638
- var import_picocolors6 = __toESM(require_picocolors(), 1);
5639
- init_tty();
5640
-
5641
- // src/cli/lib/symbols.ts
5642
- init_tty();
5643
- var u = isUnicodeSupported;
5644
- var SYM = {
5645
- check: u ? "\u2714" : "v",
5646
- // ✓
5647
- cross: u ? "\u2718" : "x",
5648
- // ✗
5649
- circle: u ? "\u25CB" : "o",
5650
- // ○
5651
- repeat: u ? "\u21BB" : "R",
5652
- // ↻
5653
- undo: u ? "\u21A9" : "<-",
5654
- // ↩
5655
- fire: u ? "\u{1F525}" : "*",
5656
- // 🔥
5657
- ellipsis: u ? "\u2026" : "...",
5658
- // …
5659
- dash: u ? "\u2500" : "-",
5660
- // ─
5661
- bullet: u ? "\u2022" : "*",
5662
- // •
5663
- arrow: u ? "\u2192" : "->"
5664
- // →
5665
- };
5666
-
5667
- // src/cli/lib/spinner.ts
5668
- var FRAMES = isUnicodeSupported ? [
5669
- String.fromCodePoint(10251),
5670
- String.fromCodePoint(10265),
5671
- String.fromCodePoint(10297),
5672
- String.fromCodePoint(10296),
5673
- String.fromCodePoint(10300),
5674
- String.fromCodePoint(10292),
5675
- String.fromCodePoint(10278),
5676
- String.fromCodePoint(10279),
5677
- String.fromCodePoint(10247),
5678
- String.fromCodePoint(10255)
5679
- ] : ["-", "\\", "|", "/"];
5680
- var INTERVAL = 80;
5681
- var MAX_RETRIES2 = 3;
5682
- function createSpinner(interactive) {
5683
- if (!interactive) {
5684
- return { start() {
5685
- }, update() {
5686
- }, stop() {
5687
- }, fail() {
5688
- } };
5689
- }
5690
- let timer = null;
5691
- let frameIdx = 0;
5692
- let currentMsg = "";
5693
- function clear() {
5694
- process.stderr.write("\r\x1B[K");
5695
- }
5723
+ // src/cli/auth/login.ts
5724
+ async function postLogin(email, password) {
5725
+ assertSafeApiBase();
5726
+ const resp = await http.post(`${API_BASE}/api/auth/login`, { email, password });
5696
5727
  return {
5697
- start(msg) {
5698
- currentMsg = msg;
5699
- frameIdx = 0;
5700
- clear();
5701
- process.stderr.write(`${import_picocolors6.default.cyan(FRAMES[0])} ${msg}`);
5702
- timer = setInterval(() => {
5703
- frameIdx = (frameIdx + 1) % FRAMES.length;
5704
- clear();
5705
- process.stderr.write(`${import_picocolors6.default.cyan(FRAMES[frameIdx])} ${currentMsg}`);
5706
- }, INTERVAL);
5707
- },
5708
- update(msg) {
5709
- currentMsg = msg;
5710
- },
5711
- stop(msg) {
5712
- if (timer) {
5713
- clearInterval(timer);
5714
- timer = null;
5715
- }
5716
- clear();
5717
- process.stderr.write(`${import_picocolors6.default.green(SYM.check)} ${msg ?? currentMsg}
5718
- `);
5719
- },
5720
- fail(msg) {
5721
- if (timer) {
5722
- clearInterval(timer);
5723
- timer = null;
5724
- }
5725
- clear();
5726
- process.stderr.write(`${import_picocolors6.default.red(SYM.cross)} ${msg}
5727
- `);
5728
- }
5728
+ refreshToken: resp.data.refreshToken,
5729
+ uid: resp.data.uid,
5730
+ displayName: resp.data.email ?? email,
5731
+ idToken: resp.data.idToken,
5732
+ idTokenExpiry: Date.now() + (resp.data.expiresIn || 3600) * 1e3
5729
5733
  };
5730
5734
  }
5731
- function delay2(ms) {
5732
- return new Promise((r) => setTimeout(r, ms));
5735
+ async function authenticateInteractive(spinner) {
5736
+ const email = await promptText({ message: "Email", required: true });
5737
+ const password = await promptPassword({ message: "Password" });
5738
+ spinner.start("Signing in...");
5739
+ return postLogin(email, password);
5740
+ }
5741
+ function printSuccess(root) {
5742
+ const body = root ? formatCommandMap(focusCommands(collectCommands(root))) : ` ${import_picocolors5.default.dim("Run")} numo commands ${import_picocolors5.default.dim("to list every command.")}`;
5743
+ console.log(`
5744
+ ${import_picocolors5.default.bold("Available commands:")}
5745
+ ${body}`);
5746
+ console.log(` ${import_picocolors5.default.dim("Run")} numo commands ${import_picocolors5.default.dim("to see all commands.")}
5747
+ `);
5748
+ }
5749
+ async function login(options = {}, root) {
5750
+ const envEmail = process.env.NUMO_LOGIN_EMAIL;
5751
+ const envPassword = process.env.NUMO_LOGIN_PASSWORD;
5752
+ const hasEnvCreds = !!(envEmail && envPassword);
5753
+ const quietMode = isQuietMode(options);
5754
+ if (quietMode && options.phone) {
5755
+ outputError(
5756
+ Errors.invalidInput(
5757
+ "--phone requires an interactive terminal for SMS OTP entry",
5758
+ "Use NUMO_LOGIN_EMAIL + NUMO_LOGIN_PASSWORD env vars for non-interactive login."
5759
+ ),
5760
+ true
5761
+ );
5762
+ }
5763
+ if (quietMode && !hasEnvCreds && !options.phone) {
5764
+ outputError(
5765
+ Errors.configMissing("NUMO_LOGIN_EMAIL and NUMO_LOGIN_PASSWORD"),
5766
+ true
5767
+ );
5768
+ }
5769
+ const p = await Promise.resolve().then(() => (init_dist2(), dist_exports));
5770
+ if (!quietMode) p.intro(import_picocolors5.default.bold("Numo \u2014 Login"));
5771
+ let method = options.phone ? "phone" : "email";
5772
+ if (!options.phone && !hasEnvCreds && !quietMode) {
5773
+ method = await promptSelect({
5774
+ message: "How would you like to sign in?",
5775
+ options: [
5776
+ { value: "email", label: "Email & password" },
5777
+ { value: "phone", label: "Phone number (SMS)" }
5778
+ ]
5779
+ });
5780
+ }
5781
+ const s = await makeClackSpinner(quietMode);
5782
+ try {
5783
+ let result;
5784
+ if (method === "phone") {
5785
+ const { authenticateWithPhone: authenticateWithPhone2 } = await Promise.resolve().then(() => (init_phone_login(), phone_login_exports));
5786
+ result = await authenticateWithPhone2(s);
5787
+ } else if (hasEnvCreds) {
5788
+ s.start("Signing in...");
5789
+ result = await postLogin(envEmail, envPassword);
5790
+ } else {
5791
+ result = await authenticateInteractive(s);
5792
+ }
5793
+ saveCredentials({
5794
+ refreshToken: result.refreshToken,
5795
+ uid: result.uid,
5796
+ email: result.displayName,
5797
+ idToken: result.idToken,
5798
+ idTokenExpiry: result.idTokenExpiry
5799
+ });
5800
+ if (quietMode) {
5801
+ printJson({
5802
+ ok: true,
5803
+ uid: result.uid,
5804
+ email: result.displayName,
5805
+ idToken: result.idToken,
5806
+ idTokenExpiry: result.idTokenExpiry
5807
+ });
5808
+ return;
5809
+ }
5810
+ s.stop(`Logged in as ${import_picocolors5.default.green(result.displayName)}`);
5811
+ p.outro("You are ready to go!");
5812
+ printSuccess(root);
5813
+ } catch (err) {
5814
+ const classified = err instanceof CliError ? err : classifyError(err);
5815
+ if (quietMode) {
5816
+ outputError(classified, true);
5817
+ }
5818
+ s.stop(import_picocolors5.default.red("Login failed"));
5819
+ p.log.error(classified.message);
5820
+ if (classified.options.hint) p.log.warning(classified.options.hint);
5821
+ process.exit(classified.exitCode);
5822
+ }
5733
5823
  }
5824
+
5825
+ // src/cli/cli.ts
5826
+ init_credentials();
5827
+ init_dirs();
5828
+
5829
+ // src/cli/commands/tasks.ts
5830
+ var import_picocolors7 = __toESM(require_picocolors(), 1);
5831
+
5832
+ // src/cli/lib/spinner.ts
5734
5833
  async function withSpinner(interactive, message, fn) {
5735
- const spinner = createSpinner(interactive);
5834
+ const spinner = await makeClackSpinner(!interactive);
5736
5835
  spinner.start(message);
5737
- for (let attempt = 0; attempt <= MAX_RETRIES2; attempt++) {
5738
- try {
5739
- const result = await fn();
5740
- spinner.stop(message);
5741
- return result;
5742
- } catch (err) {
5743
- const status = err.response?.status;
5744
- if (status === 429 && attempt < MAX_RETRIES2) {
5745
- const retryAfter = err.response?.headers?.["retry-after"];
5746
- const waitMs = retryAfter ? parseInt(retryAfter, 10) * 1e3 : 1e3 * Math.pow(2, attempt);
5747
- spinner.update(`Rate limited, retrying in ${Math.round(waitMs / 1e3)}s...`);
5748
- await delay2(waitMs);
5749
- spinner.update(message);
5750
- continue;
5751
- }
5752
- spinner.fail(message);
5753
- throw err;
5754
- }
5836
+ try {
5837
+ const result = await fn();
5838
+ spinner.stop(message);
5839
+ return result;
5840
+ } catch (err) {
5841
+ spinner.stop(message, 2);
5842
+ throw err;
5755
5843
  }
5756
- throw new Error("Max retries exceeded");
5757
5844
  }
5758
5845
 
5759
5846
  // src/cli/lib/actions.ts
5760
- function useJson(opts) {
5761
- return !!(opts.json || opts.quiet || !isInteractive());
5762
- }
5763
- function useSpinner(opts) {
5764
- return isInteractive() && !opts.quiet;
5765
- }
5847
+ var useJson = isQuietMode;
5848
+ var useSpinner = (opts) => !isQuietMode(opts);
5766
5849
  async function runGet(opts) {
5767
5850
  try {
5768
5851
  const result = await withSpinner(
@@ -5848,157 +5931,81 @@ async function runWrite(opts) {
5848
5931
  outputError(err, useJson(opts.global));
5849
5932
  }
5850
5933
  }
5851
- async function runDelete(opts) {
5852
- try {
5853
- await withSpinner(
5854
- useSpinner(opts.global),
5855
- opts.spinnerMessage ?? "Deleting...",
5856
- opts.fn
5857
- );
5858
- if (!opts.global.quiet) {
5859
- console.log(opts.successMessage);
5860
- }
5861
- } catch (err) {
5862
- outputError(err, useJson(opts.global));
5863
- }
5864
- }
5865
5934
 
5866
5935
  // src/cli/lib/uid.ts
5936
+ init_credentials();
5867
5937
  init_errors();
5938
+ function uidFromToken(token) {
5939
+ try {
5940
+ const payload = JSON.parse(Buffer.from(token.split(".")[1] ?? "", "base64").toString("utf8"));
5941
+ if (typeof payload.user_id === "string") return payload.user_id;
5942
+ if (typeof payload.sub === "string") return payload.sub;
5943
+ return null;
5944
+ } catch {
5945
+ return null;
5946
+ }
5947
+ }
5868
5948
  function requireUid() {
5949
+ if (process.env.NUMO_TOKEN) return uidFromToken(process.env.NUMO_TOKEN) ?? "";
5869
5950
  const creds = loadCredentials();
5870
5951
  if (!creds) throw Errors.authRequired();
5871
5952
  return creds.uid;
5872
5953
  }
5873
5954
 
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
5955
  // src/cli/services/tasks.ts
5974
- async function listTasks(uid, opts) {
5956
+ init_api_client();
5957
+ async function listTasks(opts) {
5975
5958
  return api.get("/api/tasks", {
5976
5959
  date: opts.date,
5977
5960
  backlog: opts.backlog ? "true" : void 0,
5978
5961
  tag: opts.tag
5979
5962
  });
5980
5963
  }
5981
- async function getTask(uid, id) {
5964
+ async function getTask(id) {
5982
5965
  return api.get(`/api/tasks/${encodeURIComponent(id)}`);
5983
5966
  }
5984
- async function createTask(uid, body) {
5967
+ async function createTask(body) {
5985
5968
  return api.post("/api/tasks", body);
5986
5969
  }
5987
- async function updateTask(uid, id, body) {
5970
+ async function updateTask(id, body) {
5988
5971
  return api.patch(`/api/tasks/${encodeURIComponent(id)}`, body);
5989
5972
  }
5990
- async function deleteTask(uid, id) {
5973
+ async function deleteTask(id) {
5991
5974
  return api.del(`/api/tasks/${encodeURIComponent(id)}`);
5992
5975
  }
5993
- async function completeTask(uid, id, date) {
5976
+ async function completeTask(id, date) {
5994
5977
  return api.post(`/api/tasks/${encodeURIComponent(id)}/complete`, date ? { date } : void 0);
5995
5978
  }
5996
- async function uncompleteTask(uid, taskHistoryId) {
5979
+ async function uncompleteTask(taskHistoryId) {
5997
5980
  return api.post(`/api/tasks/${encodeURIComponent(taskHistoryId)}/uncomplete`);
5998
5981
  }
5999
5982
 
6000
5983
  // src/cli/lib/format.ts
6001
- var import_picocolors7 = __toESM(require_picocolors(), 1);
5984
+ var import_picocolors6 = __toESM(require_picocolors(), 1);
5985
+
5986
+ // src/cli/lib/symbols.ts
5987
+ init_tty();
5988
+ var u = isUnicodeSupported;
5989
+ var SYM = {
5990
+ check: u ? "\u2714" : "v",
5991
+ // ✓
5992
+ cross: u ? "\u2718" : "x",
5993
+ // ✗
5994
+ circle: u ? "\u25CB" : "o",
5995
+ // ○
5996
+ repeat: u ? "\u21BB" : "R",
5997
+ // ↻
5998
+ undo: u ? "\u21A9" : "<-",
5999
+ // ↩
6000
+ fire: u ? "\u{1F525}" : "*",
6001
+ // 🔥
6002
+ ellipsis: u ? "\u2026" : "...",
6003
+ // …
6004
+ dash: u ? "\u2500" : "-"
6005
+ // ─
6006
+ };
6007
+
6008
+ // src/cli/lib/format.ts
6002
6009
  function formatDate(ts) {
6003
6010
  if (ts == null) return "";
6004
6011
  const d = typeof ts === "number" ? new Date(ts) : new Date(ts);
@@ -6026,7 +6033,7 @@ function formatRelativeDate(ts) {
6026
6033
  }
6027
6034
  function formatTags(tags) {
6028
6035
  if (!Array.isArray(tags) || tags.length === 0) return "";
6029
- return tags.map((t2) => import_picocolors7.default.cyan(`#${t2}`)).join(" ");
6036
+ return tags.map((t2) => import_picocolors6.default.cyan(`#${t2}`)).join(" ");
6030
6037
  }
6031
6038
  function formatDifficulty(d) {
6032
6039
  if (d == null) return "";
@@ -6066,21 +6073,21 @@ function formatWeekdayHeader(date, streakCount) {
6066
6073
  const cols = process.stdout.columns ?? 80;
6067
6074
  const fire = SYM.fire;
6068
6075
  const streakStr = streakCount && streakCount > 0 ? `${fire} ${streakCount}` : "";
6069
- const pad = Math.max(0, cols - weekday.length - streakStr.length - 4);
6070
- const line1 = ` ${import_picocolors7.default.bold(weekday)}${" ".repeat(pad)}${streakStr}`;
6071
- const line2 = ` ${import_picocolors7.default.dim(fullDate)}`;
6076
+ const pad2 = Math.max(0, cols - weekday.length - streakStr.length - 4);
6077
+ const line1 = ` ${import_picocolors6.default.bold(weekday)}${" ".repeat(pad2)}${streakStr}`;
6078
+ const line2 = ` ${import_picocolors6.default.dim(fullDate)}`;
6072
6079
  return `${line1}
6073
6080
  ${line2}`;
6074
6081
  }
6075
6082
  function formatKarmaGain(points, checksInRow) {
6076
- const base = import_picocolors7.default.green(`+${points} karma`);
6083
+ const base = import_picocolors6.default.green(`+${points} karma`);
6077
6084
  if (checksInRow && checksInRow > 1) {
6078
- return `${base} ${import_picocolors7.default.yellow(`streak x${checksInRow}!`)}`;
6085
+ return `${base} ${import_picocolors6.default.yellow(`streak x${checksInRow}!`)}`;
6079
6086
  }
6080
6087
  return base;
6081
6088
  }
6082
6089
  function formatProgressSummary(completed, total) {
6083
- return import_picocolors7.default.dim(`${completed}/${total} done today`);
6090
+ return import_picocolors6.default.dim(`${completed}/${total} done today`);
6084
6091
  }
6085
6092
  function formatTagsSummary(tasks) {
6086
6093
  const counts = {};
@@ -6092,7 +6099,7 @@ function formatTagsSummary(tasks) {
6092
6099
  }
6093
6100
  const entries = Object.entries(counts);
6094
6101
  if (entries.length === 0) return "";
6095
- return entries.map(([tag, count]) => `${import_picocolors7.default.cyan("#" + tag)}${import_picocolors7.default.dim("(" + count + ")")}`).join(" ");
6102
+ return entries.map(([tag, count]) => `${import_picocolors6.default.cyan("#" + tag)}${import_picocolors6.default.dim("(" + count + ")")}`).join(" ");
6096
6103
  }
6097
6104
 
6098
6105
  // src/cli/commands/tasks.ts
@@ -8966,6 +8973,99 @@ function parseHumanDateOnly(input) {
8966
8973
  return result.slice(0, 10);
8967
8974
  }
8968
8975
 
8976
+ // src/cli/lib/task-dates.ts
8977
+ function pad(n) {
8978
+ return String(n).padStart(2, "0");
8979
+ }
8980
+ function localDateOnly(d = /* @__PURE__ */ new Date()) {
8981
+ return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
8982
+ }
8983
+ function localDateOffset(n, base = /* @__PURE__ */ new Date()) {
8984
+ const d = new Date(base);
8985
+ d.setDate(d.getDate() + n);
8986
+ return localDateOnly(d);
8987
+ }
8988
+ function toApiDueDate(s) {
8989
+ return /^\d{4}-\d{2}-\d{2}$/.test(s) ? `${s} 00:00` : s.slice(0, 16);
8990
+ }
8991
+ function dueDateHasTime(dueDate) {
8992
+ if (!dueDate) return false;
8993
+ const m = dueDate.match(/ (\d{2}):(\d{2})$/);
8994
+ if (!m) return false;
8995
+ return !(m[1] === "00" && m[2] === "00");
8996
+ }
8997
+ function normalizeDueDateInBody(body) {
8998
+ if (typeof body.dueDate === "string") {
8999
+ const due = toApiDueDate(body.dueDate);
9000
+ body.dueDate = due;
9001
+ body.withTime = dueDateHasTime(due);
9002
+ }
9003
+ }
9004
+ function isCompletableDate(date, now2 = /* @__PURE__ */ new Date()) {
9005
+ const dateOnly = date.slice(0, 10);
9006
+ return dateOnly === localDateOnly(now2) || dateOnly === localDateOffset(-1, now2);
9007
+ }
9008
+
9009
+ // src/cli/lib/task-repeat.ts
9010
+ init_errors();
9011
+ var WEEKDAY_LOOKUP = {
9012
+ mon: "Mon",
9013
+ monday: "Mon",
9014
+ tue: "Tue",
9015
+ tues: "Tue",
9016
+ tuesday: "Tue",
9017
+ wed: "Wed",
9018
+ weds: "Wed",
9019
+ wednesday: "Wed",
9020
+ thu: "Thu",
9021
+ thur: "Thu",
9022
+ thurs: "Thu",
9023
+ thursday: "Thu",
9024
+ fri: "Fri",
9025
+ friday: "Fri",
9026
+ sat: "Sat",
9027
+ saturday: "Sat",
9028
+ sun: "Sun",
9029
+ sunday: "Sun"
9030
+ };
9031
+ var REPEAT_TYPES = ["daily", "weekly", "monthly", "none"];
9032
+ function normalizeWeekday(s) {
9033
+ const day = WEEKDAY_LOOKUP[s.trim().toLowerCase()];
9034
+ if (!day) throw Errors.invalidInput(`Invalid weekday: "${s}". Use Mon,Tue,Wed,Thu,Fri,Sat,Sun`);
9035
+ return day;
9036
+ }
9037
+ function buildRepeatConfig(opts) {
9038
+ if (opts.repeat === void 0) return void 0;
9039
+ const type = opts.repeat;
9040
+ if (!REPEAT_TYPES.includes(type)) {
9041
+ throw Errors.invalidInput(`--repeat must be one of: ${REPEAT_TYPES.join(", ")}`);
9042
+ }
9043
+ const every = opts.every !== void 0 ? parseInt(opts.every, 10) : 1;
9044
+ if (!Number.isFinite(every) || every < 1 || every > 365) {
9045
+ throw Errors.invalidInput("--every must be an integer 1-365");
9046
+ }
9047
+ const repeat = { type, every, custom: false, monthDays: null, weekDays: null };
9048
+ if (type === "weekly" && opts.weekdays) {
9049
+ repeat.weekDays = opts.weekdays.split(",").map((d) => normalizeWeekday(d));
9050
+ }
9051
+ if (type === "monthly" && opts.monthDays) {
9052
+ repeat.monthDays = opts.monthDays.split(",").map((s) => {
9053
+ const n = parseInt(s.trim(), 10);
9054
+ if (!Number.isFinite(n) || n < 1 || n > 31) {
9055
+ throw Errors.invalidInput(`--month-days must be 1-31 (got "${s.trim()}")`);
9056
+ }
9057
+ return n;
9058
+ });
9059
+ }
9060
+ return repeat;
9061
+ }
9062
+
9063
+ // src/cli/lib/task-subtasks.ts
9064
+ var import_node_crypto = require("node:crypto");
9065
+ function buildSubtasks(texts) {
9066
+ return texts.map((t2) => t2.trim()).filter((t2) => t2.length > 0).map((text) => ({ id: (0, import_node_crypto.randomUUID)(), text, completed: false }));
9067
+ }
9068
+
8969
9069
  // src/cli/lib/stdin.ts
8970
9070
  var fs3 = __toESM(require("fs"), 1);
8971
9071
  function readStdinLines() {
@@ -8974,13 +9074,16 @@ function readStdinLines() {
8974
9074
  }
8975
9075
 
8976
9076
  // src/cli/commands/tasks.ts
8977
- async function pickTask(uid, id, actionName) {
9077
+ function collectValue(value, previous) {
9078
+ return previous.concat([value]);
9079
+ }
9080
+ async function pickTask(id, actionName) {
8978
9081
  if (id) return id;
8979
9082
  if (!isInteractive()) {
8980
9083
  throw Errors.missingArg("Task ID", "id");
8981
9084
  }
8982
- const today2 = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
8983
- const { tasks } = await listTasks(uid, { date: today2 });
9085
+ const today2 = localDateOnly();
9086
+ const { tasks } = await listTasks({ date: today2 });
8984
9087
  const pending = tasks.filter((t2) => !t2.completed);
8985
9088
  if (pending.length === 0) {
8986
9089
  throw Errors.invalidInput(`No pending tasks for today (${today2}). Use: numo tasks ${actionName} <id>`);
@@ -8989,23 +9092,21 @@ async function pickTask(uid, id, actionName) {
8989
9092
  message: `Select task to ${actionName}`,
8990
9093
  options: pending.map((t2) => ({
8991
9094
  value: t2.id,
8992
- label: `${truncate(t2.text, 50)} ${import_picocolors8.default.dim(t2.id)}`
9095
+ label: `${truncate(t2.text, 50)} ${import_picocolors7.default.dim(t2.id)}`
8993
9096
  }))
8994
9097
  });
8995
9098
  return selected;
8996
9099
  }
8997
9100
  function resolveDate(opts) {
8998
9101
  if (opts.backlog) return void 0;
8999
- const fmt = (d) => d.toISOString().slice(0, 10);
9000
- const today2 = /* @__PURE__ */ new Date();
9001
- if (opts.yesterday) return fmt(new Date(today2.getTime() - 864e5));
9002
- if (opts.tomorrow) return fmt(new Date(today2.getTime() + 864e5));
9102
+ if (opts.yesterday) return localDateOffset(-1);
9103
+ if (opts.tomorrow) return localDateOffset(1);
9003
9104
  if (opts.date) {
9004
9105
  const parsed = parseHumanDateOnly(opts.date);
9005
9106
  if (!parsed) throw Errors.invalidInput(`Cannot parse date: "${opts.date}". Use YYYY-MM-DD or natural language (tomorrow, next monday, etc.)`);
9006
9107
  return parsed;
9007
9108
  }
9008
- return fmt(today2);
9109
+ return localDateOnly();
9009
9110
  }
9010
9111
  function extractTime(dueDate) {
9011
9112
  if (typeof dueDate !== "string") return "";
@@ -9018,9 +9119,9 @@ function isRepeating(t2) {
9018
9119
  return !!t2.repeat?.type && t2.repeat.type !== "none";
9019
9120
  }
9020
9121
  function getCheckIndicator(t2) {
9021
- if (t2.completed) return import_picocolors8.default.green(SYM.check);
9022
- if (isRepeating(t2)) return import_picocolors8.default.blue(SYM.repeat);
9023
- return import_picocolors8.default.dim(SYM.circle);
9122
+ if (t2.completed) return import_picocolors7.default.green(SYM.check);
9123
+ if (isRepeating(t2)) return import_picocolors7.default.blue(SYM.repeat);
9124
+ return import_picocolors7.default.dim(SYM.circle);
9024
9125
  }
9025
9126
  function sortTasksForDisplay(tasks) {
9026
9127
  const repeatOrder = { daily: 0, weekly: 1, monthly: 2 };
@@ -9043,50 +9144,60 @@ function sortTasksForDisplay(tasks) {
9043
9144
  });
9044
9145
  }
9045
9146
  function printTaskDetail(t2) {
9046
- const dim = import_picocolors8.default.dim;
9147
+ const dim = import_picocolors7.default.dim;
9047
9148
  console.log("");
9048
9149
  printRecord([
9049
9150
  ["ID", dim(t2.id)],
9050
9151
  ["Text", t2.text],
9051
9152
  ["Due", formatDate(t2.dueDate) || dim("none (backlog)")],
9052
- ["Status", t2.completed ? import_picocolors8.default.green("completed") : import_picocolors8.default.yellow("pending")],
9153
+ ["Status", t2.completed ? import_picocolors7.default.green("completed") : import_picocolors7.default.yellow("pending")],
9053
9154
  ["Tags", formatTags(t2.tags) || dim("none")],
9054
9155
  ["Difficulty", formatDifficulty(t2.difficulty) || dim("not set")],
9055
9156
  ["Duration", formatDuration(t2.duration) || dim("not set")],
9056
9157
  ["Repeat", formatRepeat(t2.repeat) || dim("none")],
9057
9158
  ["Note", t2.note || dim("none")],
9058
- ["Public", t2.isPublic ? import_picocolors8.default.green("yes") : import_picocolors8.default.yellow("no")],
9159
+ ["Public", t2.isPublic ? import_picocolors7.default.green("yes") : import_picocolors7.default.yellow("no")],
9059
9160
  ["Completions", String(t2.completions ?? 0)],
9060
9161
  ["Created", formatDate(t2.createdAt)]
9061
9162
  ]);
9163
+ if (Array.isArray(t2.subtasks) && t2.subtasks.length > 0) {
9164
+ console.log(` ${import_picocolors7.default.bold("Subtasks")}`);
9165
+ for (const s of t2.subtasks) {
9166
+ const box = s.completed ? import_picocolors7.default.green(SYM.check) : import_picocolors7.default.dim(SYM.circle);
9167
+ console.log(` ${box} ${s.completed ? import_picocolors7.default.strikethrough(dim(s.text)) : s.text}`);
9168
+ }
9169
+ }
9062
9170
  console.log("");
9063
9171
  }
9064
9172
  function printTaskLine(t2) {
9065
9173
  const check = getCheckIndicator(t2);
9066
9174
  const rawText = truncate(t2.text, 50);
9067
- const text = t2.completed ? import_picocolors8.default.strikethrough(import_picocolors8.default.dim(rawText)) : rawText;
9175
+ const text = t2.completed ? import_picocolors7.default.strikethrough(import_picocolors7.default.dim(rawText)) : rawText;
9068
9176
  const time = extractTime(t2.dueDate);
9069
9177
  const tags = formatTags(t2.tags);
9070
9178
  const difficulty = formatDifficulty(t2.difficulty);
9071
- const id = import_picocolors8.default.dim(t2.id);
9179
+ const id = import_picocolors7.default.dim(t2.id);
9072
9180
  const parts = [check, text];
9073
- if (time) parts.push(import_picocolors8.default.cyan(time));
9181
+ if (time) parts.push(import_picocolors7.default.cyan(time));
9074
9182
  if (tags) parts.push(tags);
9075
- if (difficulty) parts.push(import_picocolors8.default.dim(`[${difficulty}]`));
9183
+ if (difficulty) parts.push(import_picocolors7.default.dim(`[${difficulty}]`));
9076
9184
  parts.push(id);
9077
9185
  console.log(" " + parts.join(" "));
9078
9186
  }
9187
+ function printPartialNotice(failed) {
9188
+ console.log(` ${import_picocolors7.default.yellow("! some bookkeeping was deferred")} ${import_picocolors7.default.dim(`(${(failed ?? []).join(", ")})`)}`);
9189
+ }
9079
9190
  function registerTasksCommands(program3) {
9080
9191
  const tasks = program3.command("tasks").description("Manage tasks");
9081
- tasks.command("list").description("List tasks by date or backlog").option("--date <date>", "YYYY-MM-DD").option("--backlog", "Show backlog tasks").option("--tag <tag>", "Filter by tag").option("--yesterday", "Show yesterday's tasks").option("--tomorrow", "Show tomorrow's tasks").action(async function() {
9192
+ tasks.command("list").description("List tasks by date or backlog").option("--date <date>", 'YYYY-MM-DD or natural language ("tomorrow", "next monday")').option("--backlog", "Show backlog tasks").option("--tag <tag>", "Filter by tag").option("--yesterday", "Show yesterday's tasks").option("--tomorrow", "Show tomorrow's tasks").action(async function() {
9082
9193
  const opts = this.optsWithGlobals();
9083
- const uid = requireUid();
9194
+ requireUid();
9084
9195
  const date = resolveDate(opts);
9085
9196
  await runList({
9086
9197
  global: opts,
9087
- fn: () => listTasks(uid, { date, backlog: opts.backlog, tag: opts.tag }),
9198
+ fn: () => listTasks({ date, backlog: opts.backlog, tag: opts.tag }),
9088
9199
  dataKey: "tasks",
9089
- columns: ["id", "text", "dueDate", "completed", "tags", "priority"],
9200
+ columns: ["id", "text", "dueDate", "completed", "tags"],
9090
9201
  spinnerMessage: "Fetching tasks...",
9091
9202
  onInteractive: (payload) => {
9092
9203
  const items = payload.tasks;
@@ -9094,7 +9205,7 @@ function registerTasksCommands(program3) {
9094
9205
  const completed = items.filter((t2) => t2.completed);
9095
9206
  console.log("");
9096
9207
  if (opts.backlog) {
9097
- console.log(` ${import_picocolors8.default.bold("Backlog")} ${import_picocolors8.default.dim(`(${items.length})`)}`);
9208
+ console.log(` ${import_picocolors7.default.bold("Backlog")} ${import_picocolors7.default.dim(`(${items.length})`)}`);
9098
9209
  } else {
9099
9210
  const viewDate = date ? /* @__PURE__ */ new Date(date + "T00:00:00") : /* @__PURE__ */ new Date();
9100
9211
  console.log(formatWeekdayHeader(viewDate, completed.length));
@@ -9107,18 +9218,18 @@ function registerTasksCommands(program3) {
9107
9218
  const viewDate = date ? /* @__PURE__ */ new Date(date + "T00:00:00") : /* @__PURE__ */ new Date();
9108
9219
  const dayName = viewDate.toLocaleDateString("en-US", { weekday: "long" });
9109
9220
  console.log(`
9110
- ${import_picocolors8.default.dim(`No tasks for ${dayName}. Enjoy your day!`)}`);
9111
- console.log(` ${import_picocolors8.default.dim("--yesterday \xB7 --tomorrow \xB7 --date YYYY-MM-DD")}`);
9221
+ ${import_picocolors7.default.dim(`No tasks for ${dayName}. Enjoy your day!`)}`);
9222
+ console.log(` ${import_picocolors7.default.dim("--yesterday \xB7 --tomorrow \xB7 --date YYYY-MM-DD")}`);
9112
9223
  } else {
9113
9224
  console.log(`
9114
- ${import_picocolors8.default.dim("No backlog tasks.")}`);
9225
+ ${import_picocolors7.default.dim("No backlog tasks.")}`);
9115
9226
  }
9116
9227
  console.log("");
9117
9228
  return;
9118
9229
  }
9119
9230
  if (pending.length > 0) {
9120
9231
  console.log(`
9121
- ${import_picocolors8.default.bold("Pending")} ${import_picocolors8.default.dim(`(${pending.length})`)}
9232
+ ${import_picocolors7.default.bold("Pending")} ${import_picocolors7.default.dim(`(${pending.length})`)}
9122
9233
  `);
9123
9234
  for (const t2 of pending) {
9124
9235
  printTaskLine(t2);
@@ -9126,7 +9237,7 @@ function registerTasksCommands(program3) {
9126
9237
  }
9127
9238
  if (completed.length > 0) {
9128
9239
  console.log(`
9129
- ${import_picocolors8.default.dim(`Completed (${completed.length})`)}
9240
+ ${import_picocolors7.default.dim(`Completed (${completed.length})`)}
9130
9241
  `);
9131
9242
  for (const t2 of completed) {
9132
9243
  printTaskLine(t2);
@@ -9135,7 +9246,7 @@ function registerTasksCommands(program3) {
9135
9246
  if (!opts.backlog) {
9136
9247
  console.log(`
9137
9248
  ${formatProgressSummary(completed.length, items.length)}`);
9138
- console.log(` ${import_picocolors8.default.dim("--yesterday \xB7 --tomorrow \xB7 --date YYYY-MM-DD")}`);
9249
+ console.log(` ${import_picocolors7.default.dim("--yesterday \xB7 --tomorrow \xB7 --date YYYY-MM-DD")}`);
9139
9250
  }
9140
9251
  console.log("");
9141
9252
  }
@@ -9150,11 +9261,11 @@ Examples:
9150
9261
  $ numo tasks list --tag Work # Filter by tag
9151
9262
  $ numo tasks list --json | jq '.tasks[].text'`);
9152
9263
  tasks.command("get [id]").description("Get a task by ID").action(async function(id) {
9153
- const uid = requireUid();
9264
+ requireUid();
9154
9265
  const taskId = await promptForMissing({ value: id, message: "Task ID" });
9155
9266
  await runGet({
9156
9267
  global: this.optsWithGlobals(),
9157
- fn: () => getTask(uid, taskId),
9268
+ fn: () => getTask(taskId),
9158
9269
  spinnerMessage: "Fetching task...",
9159
9270
  onInteractive: printTaskDetail
9160
9271
  });
@@ -9162,171 +9273,165 @@ Examples:
9162
9273
  Examples:
9163
9274
  $ numo tasks get abc123
9164
9275
  $ numo tasks get abc123 --json | jq '.text'`);
9165
- tasks.command("create").description("Create a new task").option("--text <text>", "Task text").option("--due <date>", "Due date YYYY-MM-DD").option("--tags <tags>", "Comma-separated tags").option("--public", "Make task public (default)").option("--private", "Make task private").option("--note <note>", "Task note").option("--priority <n>", "Priority 0.1\u20131.0").option("--difficulty <n>", "Difficulty 0\u20133").option("--duration <n>", "Duration in minutes").action(async function() {
9276
+ tasks.command("create [text...]").description("Create a task \u2014 quick via text/flags, or an interactive wizard").option("--text <text>", "Task text (alternative to positional text)").option("--due <date>", 'Due date: YYYY-MM-DD, "YYYY-MM-DD HH:mm", or natural language').option("--backlog", "No due date (Someday / backlog)").option("--repeat <type>", "Recurring routine: daily | weekly | monthly").option("--weekdays <days>", "For --repeat weekly: comma list e.g. Mon,Wed,Fri").option("--month-days <days>", "For --repeat monthly: comma list e.g. 1,15").option("--every <n>", "Repeat interval (every N days/weeks/months)").option("--tags <tags>", "Comma-separated tags").option("--public", "Make task public").option("--private", "Make task private (default)").option("--note <note>", "Private note").option("--difficulty <n>", "Effort 0\u20133 (S/M/L/XL)").option("--duration <n>", "Duration in minutes").option("--subtask <text>", 'Add a subtask (repeatable: --subtask "a" --subtask "b")', collectValue, []).option("--client-task-id <id>", "Idempotency key \u2014 retrying with the same id returns the existing task instead of duplicating").action(async function(textParts) {
9166
9277
  const opts = this.optsWithGlobals();
9167
- const uid = requireUid();
9168
- const text = await promptForMissing({ value: opts.text, message: "Task text", placeholder: "What do you need to do?" });
9169
- const body = { text };
9170
- if (isInteractive() && !opts.json) {
9171
- if (!opts.due) {
9172
- const today2 = /* @__PURE__ */ new Date();
9173
- const tomorrow2 = new Date(today2);
9174
- tomorrow2.setDate(tomorrow2.getDate() + 1);
9175
- const fmt = (d) => d.toISOString().slice(0, 10);
9176
- const schedule = await promptSelect({
9177
- message: "When?",
9278
+ requireUid();
9279
+ const providedText = (textParts && textParts.length ? textParts.join(" ") : void 0) ?? opts.text;
9280
+ const hasQuickInput = !!providedText || opts.due || opts.backlog || opts.repeat || opts.tags || opts.note || opts.difficulty !== void 0 || opts.duration || opts.public || opts.private || opts.subtask && opts.subtask.length;
9281
+ const useWizard = isInteractive() && !opts.json && !hasQuickInput;
9282
+ const body = {};
9283
+ if (useWizard) {
9284
+ body.text = await promptForMissing({ value: providedText, message: "Task text", placeholder: "What do you need to do?" });
9285
+ const fmt = (d) => localDateOnly(d);
9286
+ const today2 = /* @__PURE__ */ new Date();
9287
+ const tomorrow2 = new Date(today2);
9288
+ tomorrow2.setDate(tomorrow2.getDate() + 1);
9289
+ const schedule = await promptSelect({
9290
+ message: "When?",
9291
+ options: [
9292
+ { value: "today", label: `Today (${fmt(today2)})` },
9293
+ { value: "tomorrow", label: `Tomorrow (${fmt(tomorrow2)})` },
9294
+ { value: "pick", label: "Pick a date..." },
9295
+ { value: "someday", label: "Someday (backlog)" },
9296
+ { value: "daily", label: "Every day (routine)" },
9297
+ { value: "weekly", label: "Every week (routine)" },
9298
+ { value: "monthly", label: "Every month (routine)" }
9299
+ ]
9300
+ });
9301
+ if (schedule === "today") {
9302
+ body.dueDate = fmt(today2);
9303
+ } else if (schedule === "tomorrow") {
9304
+ body.dueDate = fmt(tomorrow2);
9305
+ } else if (schedule === "pick") {
9306
+ body.dueDate = await promptText({ message: "Date", placeholder: fmt(tomorrow2), required: true });
9307
+ } else if (schedule === "someday") {
9308
+ body.backlog = true;
9309
+ } else {
9310
+ const repeat = { type: schedule, every: 1, custom: false, monthDays: null, weekDays: null };
9311
+ if (schedule === "weekly") {
9312
+ repeat.weekDays = await promptMultiSelect({
9313
+ message: "Days of week",
9314
+ options: [
9315
+ { value: "Mon", label: "Monday" },
9316
+ { value: "Tue", label: "Tuesday" },
9317
+ { value: "Wed", label: "Wednesday" },
9318
+ { value: "Thu", label: "Thursday" },
9319
+ { value: "Fri", label: "Friday" },
9320
+ { value: "Sat", label: "Saturday" },
9321
+ { value: "Sun", label: "Sunday" }
9322
+ ],
9323
+ required: true
9324
+ });
9325
+ } else if (schedule === "monthly") {
9326
+ const daysInput = await promptText({ message: "Days of month", placeholder: "1,15", required: true });
9327
+ repeat.monthDays = daysInput.split(",").map((s) => parseInt(s.trim()));
9328
+ }
9329
+ body.repeat = repeat;
9330
+ body.dueDate = fmt(today2);
9331
+ }
9332
+ const visibility = await promptSelect({
9333
+ message: "Visibility",
9334
+ options: [
9335
+ { value: "private", label: "Private \u2014 only you can see it" },
9336
+ { value: "public", label: "Public \u2014 visible in community" }
9337
+ ]
9338
+ });
9339
+ body.isPublic = visibility === "public";
9340
+ if (await promptConfirm({ message: "Add details? (tags, effort, time, note)", initialValue: false })) {
9341
+ const tags = await promptMultiSelect({
9342
+ message: "Tags",
9178
9343
  options: [
9179
- { value: "today", label: `Today (${fmt(today2)})` },
9180
- { value: "tomorrow", label: `Tomorrow (${fmt(tomorrow2)})` },
9181
- { value: "pick", label: "Pick a date..." },
9182
- { value: "someday", label: "Someday (backlog)" },
9183
- { value: "daily", label: "Every day (routine)" },
9184
- { value: "weekly", label: "Every week (routine)" },
9185
- { value: "monthly", label: "Every month (routine)" }
9344
+ { value: "House", label: "House" },
9345
+ { value: "Work", label: "Work" },
9346
+ { value: "Study", label: "Study" },
9347
+ { value: "Hobby", label: "Hobby" },
9348
+ { value: "Health", label: "Health" },
9349
+ { value: "Relationship", label: "Relationship" },
9350
+ { value: "Self-care", label: "Self-care" },
9351
+ { value: "Relax", label: "Relax" },
9352
+ { value: "Kids", label: "Kids" }
9186
9353
  ]
9187
9354
  });
9188
- if (schedule === "today") {
9189
- body.dueDate = fmt(today2);
9190
- } else if (schedule === "tomorrow") {
9191
- body.dueDate = fmt(tomorrow2);
9192
- } else if (schedule === "pick") {
9193
- const date = await promptText({ message: "Date", placeholder: fmt(tomorrow2), required: true });
9194
- body.dueDate = date;
9195
- } else if (["daily", "weekly", "monthly"].includes(schedule)) {
9196
- const repeat = {
9197
- type: schedule,
9198
- every: 1,
9199
- end: "never",
9200
- endDate: null,
9201
- endAfter: null,
9202
- monthDays: null,
9203
- weekDays: null
9204
- };
9205
- if (schedule === "weekly") {
9206
- const days = await promptMultiSelect({
9207
- message: "Days of week",
9208
- options: [
9209
- { value: "Mon", label: "Monday" },
9210
- { value: "Tue", label: "Tuesday" },
9211
- { value: "Wed", label: "Wednesday" },
9212
- { value: "Thu", label: "Thursday" },
9213
- { value: "Fri", label: "Friday" },
9214
- { value: "Sat", label: "Saturday" },
9215
- { value: "Sun", label: "Sunday" }
9216
- ],
9217
- required: true
9218
- });
9219
- repeat.weekDays = days;
9220
- } else if (schedule === "monthly") {
9221
- const daysInput = await promptText({ message: "Days of month", placeholder: "1,15", required: true });
9222
- repeat.monthDays = daysInput.split(",").map((s) => parseInt(s.trim()));
9223
- }
9224
- body.repeat = repeat;
9225
- body.dueDate = fmt(today2);
9226
- }
9227
- }
9228
- if (!opts.private && !opts.public) {
9229
- const visibility = await promptSelect({
9230
- message: "Visibility",
9355
+ if (tags.length > 0) body.tags = tags;
9356
+ const difficulty = await promptSelect({
9357
+ message: "Effort",
9231
9358
  options: [
9232
- { value: "public", label: "Public \u2014 visible to your squad" },
9233
- { value: "private", label: "Private \u2014 only you can see it" }
9359
+ { value: "skip", label: "Skip" },
9360
+ { value: "0", label: "S \u2014 Tiny" },
9361
+ { value: "1", label: "M \u2014 Medium" },
9362
+ { value: "2", label: "L \u2014 High" },
9363
+ { value: "3", label: "XL \u2014 Huge" }
9234
9364
  ]
9235
9365
  });
9236
- if (visibility === "private") body.isPublic = false;
9237
- }
9238
- const addDetails = await promptConfirm({
9239
- message: "Add details? (tags, effort, time, note)",
9240
- initialValue: false
9241
- });
9242
- if (addDetails) {
9243
- if (!opts.tags) {
9244
- const tags = await promptMultiSelect({
9245
- message: "Tags",
9246
- options: [
9247
- { value: "House", label: "House" },
9248
- { value: "Work", label: "Work" },
9249
- { value: "Study", label: "Study" },
9250
- { value: "Hobby", label: "Hobby" },
9251
- { value: "Health", label: "Health" },
9252
- { value: "Relationship", label: "Relationship" },
9253
- { value: "Self-care", label: "Self-care" },
9254
- { value: "Relax", label: "Relax" },
9255
- { value: "Kids", label: "Kids" }
9256
- ]
9257
- });
9258
- if (tags.length > 0) body.tags = tags;
9259
- }
9260
- if (opts.difficulty === void 0) {
9261
- const difficulty = await promptSelect({
9262
- message: "Effort",
9263
- options: [
9264
- { value: "skip", label: "Skip" },
9265
- { value: "0", label: "S \u2014 Tiny" },
9266
- { value: "1", label: "M \u2014 Medium" },
9267
- { value: "2", label: "L \u2014 High" },
9268
- { value: "3", label: "XL \u2014 Huge" }
9269
- ]
9270
- });
9271
- if (difficulty !== "skip") body.difficulty = parseInt(difficulty);
9272
- }
9273
- const addTime = await promptConfirm({ message: "Add a specific time?", initialValue: false });
9274
- if (addTime && body.dueDate) {
9366
+ if (difficulty !== "skip") body.difficulty = parseInt(difficulty);
9367
+ if (body.dueDate && await promptConfirm({ message: "Add a specific time?", initialValue: false })) {
9275
9368
  const time = await promptText({ message: "Time", placeholder: "09:30", required: true });
9276
9369
  body.dueDate = `${body.dueDate} ${time}`;
9277
9370
  }
9278
- if (!opts.note) {
9279
- const note = await promptText({ message: "Note (enter to skip)", placeholder: "Private note", required: false });
9280
- if (note) body.note = note;
9371
+ const note = await promptText({ message: "Note (enter to skip)", placeholder: "Private note", required: false });
9372
+ if (note) body.note = note;
9373
+ const subs = await promptText({ message: "Subtasks (comma-separated, enter to skip)", placeholder: "Step 1, Step 2", required: false });
9374
+ if (subs) {
9375
+ const built = buildSubtasks(subs.split(","));
9376
+ if (built.length) body.subtasks = built;
9281
9377
  }
9282
9378
  }
9283
- if (opts.public) body.isPublic = true;
9284
- if (opts.private) body.isPublic = false;
9285
- if (opts.tags && !body.tags) body.tags = opts.tags.split(",");
9286
- if (opts.note && !body.note) body.note = opts.note;
9287
9379
  } else {
9288
- if (opts.due) {
9380
+ const text = providedText ?? (isInteractive() && !opts.json ? await promptText({ message: "Task text", placeholder: "What do you need to do?", required: true }) : void 0);
9381
+ if (!text) throw Errors.missingArg("Task text", "text");
9382
+ body.text = text;
9383
+ if (opts.backlog) {
9384
+ body.backlog = true;
9385
+ } else if (opts.due) {
9289
9386
  const parsed = parseHumanDate(opts.due);
9290
9387
  if (!parsed) throw Errors.invalidInput(`Cannot parse date: "${opts.due}"`);
9291
9388
  body.dueDate = parsed;
9389
+ } else {
9390
+ body.dueDate = localDateOnly();
9292
9391
  }
9392
+ const repeat = buildRepeatConfig(opts);
9393
+ if (repeat) body.repeat = repeat;
9293
9394
  if (opts.tags) body.tags = opts.tags.split(",");
9294
9395
  if (opts.note) body.note = opts.note;
9295
- if (opts.priority) body.priority = parseFloat(opts.priority);
9296
9396
  if (opts.difficulty !== void 0) body.difficulty = parseInt(opts.difficulty);
9297
9397
  if (opts.duration) body.duration = parseInt(opts.duration);
9298
- if (opts.public) body.isPublic = true;
9299
- if (opts.private) body.isPublic = false;
9398
+ if (opts.subtask && opts.subtask.length) body.subtasks = buildSubtasks(opts.subtask);
9399
+ body.isPublic = opts.public ? true : false;
9300
9400
  }
9401
+ if (opts.clientTaskId) body.clientTaskId = opts.clientTaskId;
9402
+ body.listPosition = "top";
9403
+ normalizeDueDateInBody(body);
9301
9404
  await runCreate({
9302
9405
  global: opts,
9303
- fn: () => createTask(uid, body),
9406
+ fn: () => createTask(body),
9304
9407
  dataKey: "task",
9305
9408
  spinnerMessage: "Creating task...",
9306
9409
  onInteractive: (_task, payload) => {
9307
- const { task, karma } = payload;
9308
- const check = import_picocolors8.default.green(SYM.check);
9410
+ const { task, karma, idempotentReplay } = payload;
9411
+ const check = import_picocolors7.default.green(SYM.check);
9309
9412
  console.log(`
9310
- ${check} Created ${task.text}`);
9311
- if (karma) {
9312
- console.log(` ${formatKarmaGain(karma)}${" ".repeat(20)}${import_picocolors8.default.dim(task.id)}`);
9313
- }
9413
+ ${check} ${idempotentReplay ? "Exists" : "Created"} ${task.text} ${import_picocolors7.default.dim(task.id)}`);
9414
+ if (karma) console.log(` ${formatKarmaGain(karma)}`);
9314
9415
  console.log("");
9315
9416
  }
9316
9417
  });
9317
9418
  }).addHelpText("after", `
9318
9419
  Examples:
9319
- $ numo tasks create # Interactive wizard
9320
- $ numo tasks create --text "Buy groceries" # Quick create (today)
9321
- $ numo tasks create --text "Meeting" --due 2026-03-27
9322
- $ numo tasks create --text "Workout" --tags Health --difficulty 2
9323
- $ numo tasks create --text "Review PR" --due 2026-03-27 --tags Work --private`);
9324
- tasks.command("update [id]").description("Update a task").option("--text <text>", "Task text").option("--due <date>", 'Due date YYYY-MM-DD or "YYYY-MM-DD HH:mm"').option("--tags <tags>", "Comma-separated tags").option("--public", "Make task public").option("--private", "Make task private").option("--note <note>", "Task note").option("--priority <n>", "Priority 0.1-1.0").option("--difficulty <n>", "Difficulty 0-3 (S/M/L/XL)").option("--duration <n>", "Duration in minutes").action(async function(id) {
9420
+ $ numo tasks create # Interactive wizard
9421
+ $ numo tasks create "Buy groceries" # Quick (today, private, top)
9422
+ $ numo tasks create "Meeting" --due "2026-03-27 14:30" # With a time
9423
+ $ numo tasks create "Standup" --repeat weekly --weekdays Mon,Wed,Fri
9424
+ $ numo tasks create "Pay rent" --repeat monthly --month-days 1
9425
+ $ numo tasks create "Read later" --backlog # Someday / no due date
9426
+ $ numo tasks create "Trip" --subtask "Book hotel" --subtask "Pack"
9427
+ $ numo tasks create "Review PR" --tags Work --difficulty 2 --public`);
9428
+ tasks.command("update [id]").description("Update a task").option("--text <text>", "Task text").option("--due <date>", 'Due date: YYYY-MM-DD, "YYYY-MM-DD HH:mm", or natural language').option("--clear-time", "Strip time-of-day; treat task as all-day").option("--no-time", "Alias of --clear-time").option("--tags <tags>", "Comma-separated tags").option("--public", "Make task public").option("--private", "Make task private").option("--note <note>", "Task note").option("--difficulty <n>", "Difficulty 0-3 (S/M/L/XL)").option("--duration <n>", "Duration in minutes").option("--repeat <type>", "Set recurrence: daily | weekly | monthly | none").option("--weekdays <days>", "For --repeat weekly: comma list e.g. Mon,Wed").option("--month-days <days>", "For --repeat monthly: comma list e.g. 1,15").option("--every <n>", "Repeat interval (every N)").option("--backlog", "Move to backlog (clear the due date)").option("--subtask <text>", 'Replace subtasks (repeatable: --subtask "a" --subtask "b")', collectValue, []).action(async function(id) {
9325
9429
  const opts = this.optsWithGlobals();
9326
- const uid = requireUid();
9430
+ requireUid();
9327
9431
  const taskId = await promptForMissing({ value: id, message: "Task ID" });
9432
+ const clearTime = opts.clearTime === true || opts.time === false;
9328
9433
  const body = {};
9329
- const hasAnyFlag = opts.text || opts.due || opts.tags || opts.public || opts.private || opts.note || opts.priority || opts.difficulty !== void 0 || opts.duration;
9434
+ const hasAnyFlag = opts.text || opts.due || opts.tags || opts.public || opts.private || opts.note || opts.difficulty !== void 0 || opts.duration || clearTime || opts.repeat !== void 0 || opts.backlog || opts.weekdays || opts.monthDays || opts.every || opts.subtask && opts.subtask.length;
9330
9435
  if (!hasAnyFlag && isInteractive() && !opts.json) {
9331
9436
  const text = await promptText({ message: "Text (enter to skip)", required: false });
9332
9437
  if (text) body.text = text;
@@ -9336,8 +9441,6 @@ Examples:
9336
9441
  if (tags) body.tags = tags.split(",");
9337
9442
  const note = await promptText({ message: "Note (enter to skip)", required: false });
9338
9443
  if (note) body.note = note;
9339
- const priority = await promptText({ message: "Priority (enter to skip)", placeholder: "0.1\u20131.0", required: false });
9340
- if (priority) body.priority = parseFloat(priority);
9341
9444
  const difficulty = await promptText({ message: "Difficulty (enter to skip)", placeholder: "0\u20133", required: false });
9342
9445
  if (difficulty) body.difficulty = parseInt(difficulty);
9343
9446
  const duration = await promptText({ message: "Duration in minutes (enter to skip)", placeholder: "10", required: false });
@@ -9353,18 +9456,28 @@ Examples:
9353
9456
  if (opts.public) body.isPublic = true;
9354
9457
  if (opts.private) body.isPublic = false;
9355
9458
  if (opts.note) body.note = opts.note;
9356
- if (opts.priority) body.priority = parseFloat(opts.priority);
9357
9459
  if (opts.difficulty !== void 0) body.difficulty = parseInt(opts.difficulty);
9358
9460
  if (opts.duration) body.duration = parseInt(opts.duration);
9461
+ const repeat = buildRepeatConfig(opts);
9462
+ if (repeat) body.repeat = repeat;
9463
+ if (opts.backlog) body.dueDate = null;
9464
+ if (opts.subtask && opts.subtask.length) body.subtasks = buildSubtasks(opts.subtask);
9465
+ }
9466
+ if (clearTime) {
9467
+ const currentDue = typeof body.dueDate === "string" ? body.dueDate : (await getTask(taskId)).dueDate;
9468
+ if (currentDue && currentDue.length >= 10) {
9469
+ body.dueDate = `${currentDue.slice(0, 10)} 00:00`;
9470
+ }
9359
9471
  }
9472
+ normalizeDueDateInBody(body);
9360
9473
  await runWrite({
9361
9474
  global: opts,
9362
- fn: () => updateTask(uid, taskId, body),
9475
+ fn: () => updateTask(taskId, body),
9363
9476
  dataKey: "task",
9364
9477
  spinnerMessage: "Updating task...",
9365
9478
  onInteractive: (payload) => {
9366
9479
  console.log(`
9367
- ${import_picocolors8.default.green("Updated!")} ${payload.task.text} ${import_picocolors8.default.dim(payload.task.id)}
9480
+ ${import_picocolors7.default.green("Updated!")} ${payload.task.text} ${import_picocolors7.default.dim(payload.task.id)}
9368
9481
  `);
9369
9482
  }
9370
9483
  });
@@ -9372,28 +9485,35 @@ Examples:
9372
9485
  Examples:
9373
9486
  $ numo tasks update abc123 --text "Updated text"
9374
9487
  $ numo tasks update abc123 --due 2026-03-28
9488
+ $ numo tasks update abc123 --no-time # strip time-of-day; task becomes all-day
9489
+ $ numo tasks update abc123 --clear-time # same as --no-time
9375
9490
  $ numo tasks update abc123 --tags Work,Health
9376
- $ numo tasks update abc123 --difficulty 2 --note "Important"`);
9491
+ $ numo tasks update abc123 --difficulty 2 --note "Important"
9492
+ $ numo tasks update abc123 --repeat weekly --weekdays Mon,Thu
9493
+ $ numo tasks update abc123 --repeat none # stop repeating
9494
+ $ numo tasks update abc123 --backlog # clear due date
9495
+ $ numo tasks update abc123 --subtask "Step 1" --subtask "Step 2" # replaces subtasks`);
9377
9496
  tasks.command("delete [id]").description("Delete a task").option("--yes", "Skip confirmation prompt").option("--stdin", "Read task IDs from stdin (one per line)").action(async function(id) {
9378
9497
  const opts = this.optsWithGlobals();
9379
- const uid = requireUid();
9498
+ requireUid();
9380
9499
  if (opts.stdin) {
9381
9500
  const ids = readStdinLines();
9382
9501
  for (const lineId of ids) {
9383
9502
  try {
9384
- const result = await deleteTask(uid, lineId);
9503
+ const result = await deleteTask(lineId);
9385
9504
  printNdjsonLine({ id: lineId, ...result });
9386
9505
  } catch (err) {
9506
+ process.exitCode = ExitCode.GENERAL;
9387
9507
  printNdjsonLine({ id: lineId, error: err.message });
9388
9508
  }
9389
9509
  }
9390
9510
  return;
9391
9511
  }
9392
- const taskId = await pickTask(uid, id, "delete");
9512
+ const taskId = await pickTask(id, "delete");
9393
9513
  if (isInteractive() && !opts.yes && !opts.json) {
9394
9514
  let taskText = taskId;
9395
9515
  try {
9396
- const task = await getTask(uid, taskId);
9516
+ const task = await getTask(taskId);
9397
9517
  taskText = task.text ?? taskId;
9398
9518
  } catch {
9399
9519
  }
@@ -9402,19 +9522,20 @@ Examples:
9402
9522
  initialValue: false
9403
9523
  });
9404
9524
  if (!confirmed) {
9405
- console.log(import_picocolors8.default.dim(" Cancelled."));
9525
+ console.log(import_picocolors7.default.dim(" Cancelled."));
9406
9526
  return;
9407
9527
  }
9408
9528
  }
9409
9529
  await runWrite({
9410
9530
  global: opts,
9411
- fn: () => deleteTask(uid, taskId),
9531
+ fn: () => deleteTask(taskId),
9412
9532
  spinnerMessage: "Deleting task...",
9413
9533
  onInteractive: (data) => {
9414
- const cross = import_picocolors8.default.red(SYM.cross);
9534
+ const cross = import_picocolors7.default.red(SYM.cross);
9415
9535
  console.log(`
9416
9536
  ${cross} Deleted ${data.taskText || taskId}`);
9417
- if (data.archived) console.log(` ${import_picocolors8.default.dim("Archived")}`);
9537
+ if (data.archived) console.log(` ${import_picocolors7.default.dim("Archived")}`);
9538
+ if (data.partial) printPartialNotice(data.failed);
9418
9539
  console.log("");
9419
9540
  }
9420
9541
  });
@@ -9425,31 +9546,41 @@ Examples:
9425
9546
  $ numo tasks delete abc123 --json`);
9426
9547
  tasks.command("complete [id]").description("Mark task as complete").option("--date <datetime>", 'Completion datetime "YYYY-MM-DD HH:mm"').option("--stdin", "Read task IDs from stdin (one per line)").action(async function(id) {
9427
9548
  const opts = this.optsWithGlobals();
9428
- const uid = requireUid();
9549
+ requireUid();
9550
+ if (opts.date && !isCompletableDate(opts.date)) {
9551
+ throw Errors.invalidInput("completion date must be today or yesterday");
9552
+ }
9429
9553
  if (opts.stdin) {
9430
9554
  const ids = readStdinLines();
9431
9555
  for (const lineId of ids) {
9432
9556
  try {
9433
- const result = await completeTask(uid, lineId, opts.date);
9557
+ const result = await completeTask(lineId, opts.date);
9434
9558
  printNdjsonLine({ id: lineId, ...result });
9435
9559
  } catch (err) {
9560
+ process.exitCode = ExitCode.GENERAL;
9436
9561
  printNdjsonLine({ id: lineId, error: err.message });
9437
9562
  }
9438
9563
  }
9439
9564
  return;
9440
9565
  }
9441
- const taskId = await pickTask(uid, id, "complete");
9566
+ const taskId = await pickTask(id, "complete");
9442
9567
  await runWrite({
9443
9568
  global: opts,
9444
- fn: () => completeTask(uid, taskId, opts.date),
9569
+ fn: () => completeTask(taskId, opts.date),
9445
9570
  spinnerMessage: "Completing task...",
9446
9571
  onInteractive: (data) => {
9447
- const check = import_picocolors8.default.green(SYM.check);
9448
- console.log(`
9572
+ const check = import_picocolors7.default.green(SYM.check);
9573
+ if (data.alreadyCompleted) {
9574
+ console.log(`
9575
+ ${check} Already done ${data.taskText ?? taskId}`);
9576
+ } else {
9577
+ console.log(`
9449
9578
  ${check} Done! ${data.taskText ?? taskId}`);
9450
- if (data.karma) {
9451
- console.log(` ${formatKarmaGain(data.karma, data.checksInRow)}`);
9579
+ if (data.karma) {
9580
+ console.log(` ${formatKarmaGain(data.karma, data.checksInRow)}`);
9581
+ }
9452
9582
  }
9583
+ if (data.partial) printPartialNotice(data.failed);
9453
9584
  console.log("");
9454
9585
  }
9455
9586
  });
@@ -9459,14 +9590,15 @@ Examples:
9459
9590
  $ numo tasks complete abc123 --date "2026-03-26 14:30"`);
9460
9591
  tasks.command("uncomplete [id]").description("Mark task as incomplete").option("--stdin", "Read task IDs from stdin (one per line)").action(async function(id) {
9461
9592
  const opts = this.optsWithGlobals();
9462
- const uid = requireUid();
9593
+ requireUid();
9463
9594
  if (opts.stdin) {
9464
9595
  const ids = readStdinLines();
9465
9596
  for (const lineId of ids) {
9466
9597
  try {
9467
- const result = await uncompleteTask(uid, lineId);
9598
+ const result = await uncompleteTask(lineId);
9468
9599
  printNdjsonLine({ id: lineId, ...result });
9469
9600
  } catch (err) {
9601
+ process.exitCode = ExitCode.GENERAL;
9470
9602
  printNdjsonLine({ id: lineId, error: err.message });
9471
9603
  }
9472
9604
  }
@@ -9475,14 +9607,15 @@ Examples:
9475
9607
  const taskId = await promptForMissing({ value: id, message: "Task ID" });
9476
9608
  await runWrite({
9477
9609
  global: opts,
9478
- fn: () => uncompleteTask(uid, taskId),
9610
+ fn: () => uncompleteTask(taskId),
9479
9611
  dataKey: "task",
9480
9612
  spinnerMessage: "Uncompleting task...",
9481
9613
  onInteractive: (data) => {
9482
9614
  const arrow = SYM.undo;
9483
9615
  console.log(`
9484
- ${import_picocolors8.default.yellow(arrow)} Reverted ${data.task.text ?? taskId}`);
9485
- console.log(` ${import_picocolors8.default.dim("Karma adjustment applied")}`);
9616
+ ${import_picocolors7.default.yellow(arrow)} Reverted ${data.task.text ?? taskId}`);
9617
+ console.log(` ${import_picocolors7.default.dim("Karma adjustment applied")}`);
9618
+ if (data.partial) printPartialNotice(data.failed);
9486
9619
  console.log("");
9487
9620
  }
9488
9621
  });
@@ -9492,19 +9625,20 @@ Examples:
9492
9625
  }
9493
9626
 
9494
9627
  // src/cli/commands/posts.ts
9495
- var import_picocolors10 = __toESM(require_picocolors(), 1);
9628
+ var import_picocolors9 = __toESM(require_picocolors(), 1);
9496
9629
 
9497
9630
  // src/cli/lib/pagination.ts
9498
- var import_picocolors9 = __toESM(require_picocolors(), 1);
9631
+ var import_picocolors8 = __toESM(require_picocolors(), 1);
9499
9632
  function printPaginationHint(opts) {
9500
9633
  if (!opts.nextCursor) return;
9501
9634
  const parts = [opts.command, `--cursor ${opts.nextCursor}`];
9502
9635
  if (opts.limit) parts.push(`--limit ${opts.limit}`);
9503
9636
  console.log(`
9504
- ${import_picocolors9.default.dim("Next page:")} ${import_picocolors9.default.dim("$")} numo ${parts.join(" ")}`);
9637
+ ${import_picocolors8.default.dim("Next page:")} ${import_picocolors8.default.dim("$")} numo ${parts.join(" ")}`);
9505
9638
  }
9506
9639
 
9507
9640
  // src/cli/services/posts.ts
9641
+ init_api_client();
9508
9642
  async function listPosts(opts) {
9509
9643
  return api.get("/api/posts", {
9510
9644
  cursor: opts.cursor,
@@ -9514,99 +9648,54 @@ async function listPosts(opts) {
9514
9648
  async function getPost(id) {
9515
9649
  return api.get(`/api/posts/${encodeURIComponent(id)}`);
9516
9650
  }
9517
- async function createPost(uid, body) {
9518
- return api.post("/api/posts", body);
9519
- }
9520
- async function updatePost(uid, id, body) {
9521
- return api.patch(`/api/posts/${encodeURIComponent(id)}`, body);
9522
- }
9523
- async function deletePost(uid, id) {
9524
- await api.del(`/api/posts/${encodeURIComponent(id)}`);
9525
- }
9526
9651
 
9527
9652
  // src/cli/services/comments.ts
9653
+ init_api_client();
9528
9654
  async function listComments(postId, opts) {
9529
9655
  return api.get(`/api/posts/${encodeURIComponent(postId)}/comments`, {
9530
9656
  cursor: opts.cursor,
9531
9657
  limit: opts.limit?.toString()
9532
9658
  });
9533
9659
  }
9534
- async function createComment(uid, postId, text) {
9535
- return api.post(`/api/posts/${encodeURIComponent(postId)}/comments`, { text });
9536
- }
9537
- async function deleteComment(uid, postId, commentId) {
9538
- await api.del(`/api/posts/${encodeURIComponent(postId)}/comments/${encodeURIComponent(commentId)}`);
9539
- }
9540
9660
 
9541
9661
  // src/cli/services/replies.ts
9662
+ init_api_client();
9542
9663
  async function listReplies(postId, commentId, opts) {
9543
9664
  return api.get(`/api/posts/${encodeURIComponent(postId)}/comments/${encodeURIComponent(commentId)}/replies`, {
9544
9665
  cursor: opts.cursor,
9545
9666
  limit: opts.limit?.toString()
9546
9667
  });
9547
9668
  }
9548
- async function createReply(uid, postId, commentId, text) {
9549
- return api.post(`/api/posts/${encodeURIComponent(postId)}/comments/${encodeURIComponent(commentId)}/replies`, { text });
9550
- }
9551
- async function deleteReply(uid, postId, commentId, replyId) {
9552
- await api.del(`/api/posts/${encodeURIComponent(postId)}/comments/${encodeURIComponent(commentId)}/replies/${encodeURIComponent(replyId)}`);
9553
- }
9554
-
9555
- // src/cli/types/api.ts
9556
- var POST_TAGS = ["general", "hack", "story", "meme", "other", "question", "hack-tip", "activity"];
9557
9669
 
9558
9670
  // src/cli/commands/posts.ts
9559
9671
  init_prompts();
9560
- init_tty();
9561
- async function pickComment(postId, commentId) {
9562
- if (commentId) return commentId;
9563
- if (!isInteractive()) throw new Error("Missing required argument: commentId. Use flags in non-interactive mode.");
9564
- const { comments } = await listComments(postId, { limit: 50 });
9565
- if (comments.length === 0) throw new Error("No comments on this post.");
9566
- return promptSelect({
9567
- message: "Select comment",
9568
- options: comments.map((c) => ({
9569
- value: c.id,
9570
- label: `${truncate(c.text ?? "", 60)} ${import_picocolors10.default.dim(c.authorName ?? c.userId ?? "")}`
9571
- }))
9572
- });
9573
- }
9574
- async function pickReply(postId, commentId, replyId) {
9575
- if (replyId) return replyId;
9576
- if (!isInteractive()) throw new Error("Missing required argument: replyId. Use flags in non-interactive mode.");
9577
- const { replies } = await listReplies(postId, commentId, { limit: 50 });
9578
- if (replies.length === 0) throw new Error("No replies on this comment.");
9579
- return promptSelect({
9580
- message: "Select reply",
9581
- options: replies.map((r) => ({
9582
- value: r.id,
9583
- label: `${truncate(r.text ?? "", 60)} ${import_picocolors10.default.dim(r.userId ?? "")}`
9584
- }))
9585
- });
9586
- }
9587
9672
  function printPostLine(p) {
9588
- const tag = import_picocolors10.default.cyan(p.tag ?? "");
9673
+ const tag = import_picocolors9.default.cyan(p.tag ?? "");
9589
9674
  const title = truncate(p.title, 55);
9590
- const comments = p.commentsCount ? import_picocolors10.default.dim(`${p.commentsCount} comments`) : "";
9675
+ const author = p.authorName ?? p.authorId;
9676
+ const comments = p.commentsCount ? import_picocolors9.default.dim(`${p.commentsCount} comments`) : "";
9677
+ const likes = p.likesCount ? import_picocolors9.default.dim(`${p.likesCount} likes`) : "";
9591
9678
  const time = formatRelativeDate(p.createdAt);
9592
- const id = import_picocolors10.default.dim(p.id);
9593
- const parts = [tag, import_picocolors10.default.bold(title)];
9679
+ const id = import_picocolors9.default.dim(p.id);
9680
+ const parts = [tag, import_picocolors9.default.bold(title)];
9681
+ if (author) parts.push(import_picocolors9.default.dim(`by ${author}`));
9594
9682
  if (comments) parts.push(comments);
9595
- parts.push(import_picocolors10.default.dim(time));
9683
+ if (likes) parts.push(likes);
9684
+ parts.push(import_picocolors9.default.dim(time));
9596
9685
  parts.push(id);
9597
9686
  console.log(" " + parts.join(" "));
9598
9687
  }
9599
9688
  function printPostDetail(p) {
9600
9689
  console.log("");
9601
- console.log(` ${import_picocolors10.default.bold(p.title)}`);
9690
+ console.log(` ${import_picocolors9.default.bold(p.title)}`);
9602
9691
  if (p.body) {
9603
- console.log(` ${import_picocolors10.default.dim(SYM.dash.repeat(40))}`);
9692
+ console.log(` ${import_picocolors9.default.dim(SYM.dash.repeat(40))}`);
9604
9693
  console.log(` ${p.body}`);
9605
9694
  }
9606
- console.log(` ${import_picocolors10.default.dim(SYM.dash.repeat(40))}`);
9695
+ console.log(` ${import_picocolors9.default.dim(SYM.dash.repeat(40))}`);
9607
9696
  printRecord([
9608
- ["ID", import_picocolors10.default.dim(p.id)],
9609
- ["Tag", import_picocolors10.default.cyan(p.tag ?? "")],
9697
+ ["ID", import_picocolors9.default.dim(p.id)],
9698
+ ["Tag", import_picocolors9.default.cyan(p.tag ?? "")],
9610
9699
  ["Author", p.authorName ?? p.authorId],
9611
9700
  ["Comments", p.commentsCount != null ? String(p.commentsCount) : null],
9612
9701
  ["Likes", p.likesCount != null ? String(p.likesCount) : null],
@@ -9615,9 +9704,9 @@ function printPostDetail(p) {
9615
9704
  console.log("");
9616
9705
  }
9617
9706
  function printCommentLine(c) {
9618
- const author = import_picocolors10.default.bold(c.authorName ?? c.userId ?? "");
9619
- const time = import_picocolors10.default.dim(formatRelativeDate(c.createdAt));
9620
- const replies = c.repliesCount ? import_picocolors10.default.dim(`\xB7 ${c.repliesCount} replies`) : "";
9707
+ const author = import_picocolors9.default.bold(c.authorName ?? c.userId ?? "");
9708
+ const time = import_picocolors9.default.dim(formatRelativeDate(c.createdAt));
9709
+ const replies = c.repliesCount ? import_picocolors9.default.dim(`\xB7 ${c.repliesCount} replies`) : "";
9621
9710
  const text = c.text ?? "";
9622
9711
  console.log(` ${author} ${time}${replies}`);
9623
9712
  console.log(` ${text}`);
@@ -9626,86 +9715,77 @@ function printCommentLine(c) {
9626
9715
  function printReplyLine(r) {
9627
9716
  const text = truncate(r.text ?? "", 60);
9628
9717
  const time = formatRelativeDate(r.createdAt);
9629
- const id = import_picocolors10.default.dim(r.id);
9630
- console.log(` ${text} ${import_picocolors10.default.dim(time)} ${id}`);
9718
+ const id = import_picocolors9.default.dim(r.id);
9719
+ console.log(` ${text} ${import_picocolors9.default.dim(time)} ${id}`);
9631
9720
  }
9632
9721
  function registerPostsCommands(program3) {
9633
- const posts = program3.command("posts").description("Manage posts and comments");
9722
+ const posts = program3.command("posts").description("Browse community posts and comments");
9634
9723
  posts.command("list").description("List posts").option("--cursor <cursor>").option("--limit <n>", "Max results (<=50)", "20").action(async function() {
9635
9724
  const opts = this.optsWithGlobals();
9636
- const limit = parseInt(opts.limit);
9637
9725
  await runList({
9638
9726
  global: opts,
9639
- fn: () => listPosts({ cursor: opts.cursor, limit }),
9727
+ fn: () => listPosts({ cursor: opts.cursor, limit: opts.limit ? parseInt(opts.limit) : void 0 }),
9640
9728
  dataKey: "posts",
9641
- columns: ["id", "title", "tag", "authorId", "createdAt"],
9729
+ columns: ["id", "tag", "title", "authorName", "commentsCount", "likesCount", "createdAt"],
9642
9730
  spinnerMessage: "Fetching posts...",
9643
9731
  onInteractive: (payload) => {
9644
9732
  const items = payload.posts;
9645
9733
  if (items.length === 0) {
9646
- console.log(import_picocolors10.default.dim(" No posts found."));
9734
+ console.log(import_picocolors9.default.dim(" No posts found."));
9647
9735
  return;
9648
9736
  }
9649
- console.log(`
9650
- ${import_picocolors10.default.bold("Posts")} ${import_picocolors10.default.dim(`(${items.length})`)}
9651
- `);
9652
- for (const p of items) {
9653
- printPostLine(p);
9654
- }
9737
+ console.log("");
9738
+ for (const p of items) printPostLine(p);
9739
+ console.log("");
9655
9740
  printPaginationHint({
9656
9741
  nextCursor: payload.nextCursor,
9657
9742
  command: "posts list",
9658
9743
  limit: opts.limit
9659
9744
  });
9660
- console.log("");
9661
9745
  }
9662
9746
  });
9663
- }).addHelpText("after", `
9664
- Examples:
9665
- $ numo posts list
9666
- $ numo posts list --limit 10
9667
- $ numo posts list --json | jq '.posts[].title'`);
9668
- posts.command("get [id]").description("Get a post by ID").action(async function(id) {
9747
+ });
9748
+ posts.command("get [id]").description("Get post details").action(async function(id) {
9749
+ const opts = this.optsWithGlobals();
9669
9750
  const postId = await promptForMissing({ value: id, message: "Post ID" });
9670
9751
  await runGet({
9671
- global: this.optsWithGlobals(),
9752
+ global: opts,
9672
9753
  fn: () => getPost(postId),
9673
9754
  spinnerMessage: "Fetching post...",
9674
- onInteractive: printPostDetail
9755
+ onInteractive: (post) => printPostDetail(post)
9675
9756
  });
9676
- }).addHelpText("after", `
9677
- Examples:
9678
- $ numo posts get abc123
9679
- $ numo posts get abc123 --json`);
9680
- posts.command("comments [postId]").description("List comments on a post").option("--cursor <cursor>").action(async function(postId) {
9757
+ });
9758
+ posts.command("comments [postId]").description("List comments on a post").option("--cursor <cursor>").option("--limit <n>", "Max results (<=50)", "20").action(async function(postId) {
9681
9759
  const opts = this.optsWithGlobals();
9682
9760
  const resolvedPostId = await promptForMissing({ value: postId, message: "Post ID" });
9683
9761
  await runList({
9684
9762
  global: opts,
9685
- fn: () => listComments(resolvedPostId, { cursor: opts.cursor }),
9763
+ fn: () => listComments(resolvedPostId, { cursor: opts.cursor, limit: opts.limit ? parseInt(opts.limit) : void 0 }),
9686
9764
  dataKey: "comments",
9687
- columns: ["id", "userId", "text", "createdAt"],
9765
+ columns: ["id", "userId", "authorName", "text", "repliesCount", "createdAt"],
9688
9766
  spinnerMessage: "Fetching comments...",
9689
9767
  onInteractive: (payload) => {
9690
9768
  const items = payload.comments;
9691
9769
  if (items.length === 0) {
9692
- console.log(import_picocolors10.default.dim(" No comments yet."));
9770
+ console.log(import_picocolors9.default.dim(" No comments yet."));
9693
9771
  return;
9694
9772
  }
9695
9773
  console.log(`
9696
- ${import_picocolors10.default.bold("Comments")} ${import_picocolors10.default.dim(`(${items.length})`)}
9774
+ ${import_picocolors9.default.bold("Comments")} ${import_picocolors9.default.dim(`(${items.length})`)}
9697
9775
  `);
9698
- for (const c of items) {
9699
- printCommentLine(c);
9700
- }
9701
- console.log("");
9776
+ for (const c of items) printCommentLine(c);
9777
+ printPaginationHint({
9778
+ nextCursor: payload.nextCursor,
9779
+ command: `posts comments ${resolvedPostId}`,
9780
+ limit: opts.limit
9781
+ });
9702
9782
  }
9703
9783
  });
9704
9784
  });
9705
- posts.command("replies [postId] [commentId]").description("List replies to a comment").option("--cursor <cursor>").action(async function(postId, commentId) {
9785
+ posts.command("replies [postId] [commentId]").description("List replies on a comment").option("--cursor <cursor>").option("--limit <n>", "Max results (<=50)", "20").action(async function(postId, commentId) {
9706
9786
  const opts = this.optsWithGlobals();
9707
9787
  const resolvedPostId = await promptForMissing({ value: postId, message: "Post ID" });
9708
- const resolvedCommentId = await pickComment(resolvedPostId, commentId);
9788
+ const resolvedCommentId = await promptForMissing({ value: commentId, message: "Comment ID" });
9709
9789
  await runList({
9710
9790
  global: opts,
9711
9791
  fn: () => listReplies(resolvedPostId, resolvedCommentId, { cursor: opts.cursor }),
@@ -9715,11 +9795,11 @@ Examples:
9715
9795
  onInteractive: (payload) => {
9716
9796
  const items = payload.replies;
9717
9797
  if (items.length === 0) {
9718
- console.log(import_picocolors10.default.dim(" No replies yet."));
9798
+ console.log(import_picocolors9.default.dim(" No replies yet."));
9719
9799
  return;
9720
9800
  }
9721
9801
  console.log(`
9722
- ${import_picocolors10.default.bold("Replies")} ${import_picocolors10.default.dim(`(${items.length})`)}
9802
+ ${import_picocolors9.default.bold("Replies")} ${import_picocolors9.default.dim(`(${items.length})`)}
9723
9803
  `);
9724
9804
  for (const r of items) {
9725
9805
  printReplyLine(r);
@@ -9728,132 +9808,10 @@ Examples:
9728
9808
  }
9729
9809
  });
9730
9810
  });
9731
- posts.command("create").description("Create a new post").option("--title <title>").option("--body <body>").option("--tag <tag>", "general|hack|story|meme|other|question|hack-tip|activity").action(async function() {
9732
- const opts = this.optsWithGlobals();
9733
- const uid = requireUid();
9734
- const title = await promptForMissing({ value: opts.title, message: "Title", placeholder: "Post title" });
9735
- const postBody = await promptForMissing({ value: opts.body, message: "Body", placeholder: "Post body" });
9736
- let tag = opts.tag;
9737
- if (!tag) {
9738
- tag = await promptSelect({
9739
- message: "Tag",
9740
- options: POST_TAGS.map((t2) => ({ value: t2, label: t2 }))
9741
- });
9742
- }
9743
- const body = { title, body: postBody, tag };
9744
- await runCreate({
9745
- global: opts,
9746
- fn: () => createPost(uid, body),
9747
- dataKey: "post",
9748
- spinnerMessage: "Creating post...",
9749
- onInteractive: (post, payload) => {
9750
- console.log(`
9751
- ${import_picocolors10.default.green("Posted!")} ${payload.post.title} ${import_picocolors10.default.dim(payload.post.id)}
9752
- `);
9753
- }
9754
- });
9755
- });
9756
- posts.command("update [id]").description("Update a post").option("--title <title>").option("--body <body>").option("--tag <tag>").action(async function(id) {
9757
- const opts = this.optsWithGlobals();
9758
- const uid = requireUid();
9759
- const postId = await promptForMissing({ value: id, message: "Post ID" });
9760
- const body = {};
9761
- const hasAnyFlag = opts.title || opts.body || opts.tag;
9762
- if (!hasAnyFlag && isInteractive() && !opts.json) {
9763
- const title = await promptText({ message: "Title (enter to skip)", required: false });
9764
- if (title) body.title = title;
9765
- const postBody = await promptText({ message: "Body (enter to skip)", required: false });
9766
- if (postBody) body.body = postBody;
9767
- const changeTag = await promptText({ message: "Tag (enter to skip)", placeholder: POST_TAGS.join("|"), required: false });
9768
- if (changeTag) body.tag = changeTag;
9769
- } else {
9770
- if (opts.title) body.title = opts.title;
9771
- if (opts.body) body.body = opts.body;
9772
- if (opts.tag) body.tag = opts.tag;
9773
- }
9774
- await runWrite({
9775
- global: opts,
9776
- fn: () => updatePost(uid, postId, body),
9777
- dataKey: "post",
9778
- spinnerMessage: "Updating post...",
9779
- onInteractive: (payload) => {
9780
- console.log(`
9781
- ${import_picocolors10.default.green("Updated!")} ${payload.post.title} ${import_picocolors10.default.dim(payload.post.id)}
9782
- `);
9783
- }
9784
- });
9785
- });
9786
- posts.command("delete [id]").description("Delete a post").action(async function(id) {
9787
- const uid = requireUid();
9788
- const postId = await promptForMissing({ value: id, message: "Post ID" });
9789
- await runDelete({
9790
- global: this.optsWithGlobals(),
9791
- fn: () => deletePost(uid, postId),
9792
- successMessage: ` ${import_picocolors10.default.green("Deleted!")} Post ${import_picocolors10.default.dim(postId)}`,
9793
- spinnerMessage: "Deleting post..."
9794
- });
9795
- });
9796
- posts.command("comment [postId]").description("Add a comment to a post").option("--text <text>").action(async function(postId) {
9797
- const opts = this.optsWithGlobals();
9798
- const uid = requireUid();
9799
- const resolvedPostId = await promptForMissing({ value: postId, message: "Post ID" });
9800
- const text = await promptForMissing({ value: opts.text, message: "Comment text", placeholder: "Your comment" });
9801
- await runCreate({
9802
- global: opts,
9803
- fn: () => createComment(uid, resolvedPostId, text),
9804
- dataKey: "comment",
9805
- spinnerMessage: "Adding comment...",
9806
- onInteractive: (comment, payload) => {
9807
- console.log(`
9808
- ${import_picocolors10.default.green("Commented!")} ${truncate(payload.comment.text ?? "", 50)} ${import_picocolors10.default.dim(payload.comment.id)}
9809
- `);
9810
- }
9811
- });
9812
- });
9813
- posts.command("comment-delete [postId] [commentId]").description("Delete a comment").action(async function(postId, commentId) {
9814
- const uid = requireUid();
9815
- const resolvedPostId = await promptForMissing({ value: postId, message: "Post ID" });
9816
- const resolvedCommentId = await pickComment(resolvedPostId, commentId);
9817
- await runDelete({
9818
- global: this.optsWithGlobals(),
9819
- fn: () => deleteComment(uid, resolvedPostId, resolvedCommentId),
9820
- successMessage: ` ${import_picocolors10.default.green("Deleted!")} Comment ${import_picocolors10.default.dim(resolvedCommentId)}`,
9821
- spinnerMessage: "Deleting comment..."
9822
- });
9823
- });
9824
- posts.command("reply [postId] [commentId]").description("Add a reply to a comment").option("--text <text>").action(async function(postId, commentId) {
9825
- const opts = this.optsWithGlobals();
9826
- const uid = requireUid();
9827
- const resolvedPostId = await promptForMissing({ value: postId, message: "Post ID" });
9828
- const resolvedCommentId = await pickComment(resolvedPostId, commentId);
9829
- const text = await promptForMissing({ value: opts.text, message: "Reply text", placeholder: "Your reply" });
9830
- await runCreate({
9831
- global: opts,
9832
- fn: () => createReply(uid, resolvedPostId, resolvedCommentId, text),
9833
- dataKey: "reply",
9834
- spinnerMessage: "Adding reply...",
9835
- onInteractive: (reply, payload) => {
9836
- console.log(`
9837
- ${import_picocolors10.default.green("Replied!")} ${truncate(payload.reply.text ?? "", 50)} ${import_picocolors10.default.dim(payload.reply.id)}
9838
- `);
9839
- }
9840
- });
9841
- });
9842
- posts.command("reply-delete [postId] [commentId] [replyId]").description("Delete a reply").action(async function(postId, commentId, replyId) {
9843
- const uid = requireUid();
9844
- const resolvedPostId = await promptForMissing({ value: postId, message: "Post ID" });
9845
- const resolvedCommentId = await pickComment(resolvedPostId, commentId);
9846
- const resolvedReplyId = await pickReply(resolvedPostId, resolvedCommentId, replyId);
9847
- await runDelete({
9848
- global: this.optsWithGlobals(),
9849
- fn: () => deleteReply(uid, resolvedPostId, resolvedCommentId, resolvedReplyId),
9850
- successMessage: ` ${import_picocolors10.default.green("Deleted!")} Reply ${import_picocolors10.default.dim(resolvedReplyId)}`,
9851
- spinnerMessage: "Deleting reply..."
9852
- });
9853
- });
9854
9811
  }
9855
9812
 
9856
9813
  // src/cli/services/profile.ts
9814
+ init_api_client();
9857
9815
  async function getProfile() {
9858
9816
  return api.get("/api/profile");
9859
9817
  }
@@ -9879,9 +9837,47 @@ function registerProfileCommands(program3) {
9879
9837
  }
9880
9838
 
9881
9839
  // src/cli/commands/doctor.ts
9882
- var import_picocolors11 = __toESM(require_picocolors(), 1);
9883
- init_tty();
9884
- var API_BASE5 = process.env.NUMO_API_URL ?? "http://localhost:3000";
9840
+ var import_picocolors10 = __toESM(require_picocolors(), 1);
9841
+ var import_dns = require("dns");
9842
+ var import_net = require("net");
9843
+ var import_tls = __toESM(require("tls"), 1);
9844
+ init_credentials();
9845
+ init_api_client();
9846
+ init_api_base();
9847
+ init_errors();
9848
+ function errMessage(err) {
9849
+ return sanitizeErrorMessage(err instanceof Error ? err.message : String(err));
9850
+ }
9851
+ async function checkDns(hostname) {
9852
+ if ((0, import_net.isIP)(hostname) !== 0 || hostname === "localhost") {
9853
+ return { name: "dns", status: "ok", message: `DNS skipped (${hostname} is a literal address)` };
9854
+ }
9855
+ try {
9856
+ const addrs = await import_dns.promises.resolve(hostname);
9857
+ return { name: "dns", status: "ok", message: `DNS ${hostname} \u2192 ${addrs[0]}` };
9858
+ } catch (err) {
9859
+ return { name: "dns", status: "fail", message: `DNS lookup failed for ${hostname}: ${errMessage(err)}` };
9860
+ }
9861
+ }
9862
+ async function checkTls(hostname) {
9863
+ return new Promise((resolve) => {
9864
+ const socket = import_tls.default.connect(
9865
+ { host: hostname, port: 443, timeout: 5e3, servername: hostname },
9866
+ () => {
9867
+ const proto = socket.getProtocol() ?? "unknown";
9868
+ socket.end();
9869
+ resolve({ name: "tls", status: "ok", message: `TLS handshake OK (${proto})` });
9870
+ }
9871
+ );
9872
+ socket.on("error", (err) => {
9873
+ resolve({ name: "tls", status: "fail", message: `TLS handshake failed: ${errMessage(err)}` });
9874
+ });
9875
+ socket.on("timeout", () => {
9876
+ socket.destroy();
9877
+ resolve({ name: "tls", status: "fail", message: "TLS handshake timed out after 5s" });
9878
+ });
9879
+ });
9880
+ }
9885
9881
  async function runChecks() {
9886
9882
  const checks = [];
9887
9883
  const nodeVersion = process.version;
@@ -9891,68 +9887,128 @@ async function runChecks() {
9891
9887
  status: major >= 18 ? "ok" : "fail",
9892
9888
  message: major >= 18 ? `Node ${nodeVersion}` : `Node ${nodeVersion} \u2014 requires >= 18`
9893
9889
  });
9890
+ const verdict = classifyApiBase();
9891
+ if (!verdict.ok) {
9892
+ checks.push({
9893
+ name: "api_url",
9894
+ status: "fail",
9895
+ message: verdict.message,
9896
+ fix: "Use https://api.numo.ai, or export NUMO_ALLOW_CUSTOM_HOST=1 for a self-hosted server"
9897
+ });
9898
+ return checks;
9899
+ }
9900
+ const apiUrl = new URL(API_BASE);
9894
9901
  checks.push({
9895
9902
  name: "api_url",
9896
- 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})`
9903
+ status: verdict.insecure ? "warn" : process.env.NUMO_API_URL ? "ok" : "warn",
9904
+ message: verdict.insecure ? `API URL: ${API_BASE} (HTTP \u2014 tokens unencrypted)` : process.env.NUMO_API_URL ? `API URL: ${API_BASE}` : `NUMO_API_URL not set (using default: ${API_BASE})`
9898
9905
  });
9906
+ checks.push(await checkDns(apiUrl.hostname));
9907
+ if (apiUrl.protocol === "https:") {
9908
+ checks.push(await checkTls(apiUrl.hostname));
9909
+ }
9899
9910
  const creds = loadCredentials();
9900
9911
  checks.push({
9901
9912
  name: "credentials",
9902
9913
  status: creds ? "ok" : "fail",
9903
- message: creds ? `Logged in as ${creds.email}` : "Not logged in (run: numo login)"
9914
+ message: creds ? `Logged in as ${creds.email}` : "Not logged in",
9915
+ fix: creds ? void 0 : "numo login"
9904
9916
  });
9905
9917
  if (creds) {
9906
9918
  try {
9907
9919
  await getIdToken();
9908
9920
  checks.push({ name: "token", status: "ok", message: "Token valid / refreshed" });
9909
9921
  } catch (err) {
9910
- checks.push({ name: "token", status: "fail", message: `Token refresh failed: ${err.message}` });
9922
+ checks.push({
9923
+ name: "token",
9924
+ status: "fail",
9925
+ message: `Token refresh failed: ${errMessage(err)}`,
9926
+ fix: "numo login"
9927
+ });
9911
9928
  }
9912
9929
  } else {
9913
9930
  checks.push({ name: "token", status: "fail", message: "Skipped (no credentials)" });
9914
9931
  }
9915
9932
  try {
9916
- const resp = await fetch(`${API_BASE5}/api/health`, { signal: AbortSignal.timeout(5e3) });
9917
- checks.push({ name: "api_reachable", status: "ok", message: `API server reachable (HTTP ${resp.status})` });
9933
+ const resp = await fetch(`${API_BASE}/api/health`, { signal: AbortSignal.timeout(5e3) });
9934
+ checks.push({
9935
+ name: "api_reachable",
9936
+ status: resp.ok ? "ok" : "fail",
9937
+ message: `API /api/health \u2192 HTTP ${resp.status}`,
9938
+ fix: resp.ok ? void 0 : "Check NUMO_API_URL and your network connection"
9939
+ });
9918
9940
  } catch (err) {
9919
- checks.push({ name: "api_reachable", status: "fail", message: `API server unreachable: ${err.message}` });
9941
+ checks.push({
9942
+ name: "api_reachable",
9943
+ status: "fail",
9944
+ message: `API server unreachable: ${errMessage(err)}`,
9945
+ fix: "Check NUMO_API_URL and your network connection"
9946
+ });
9947
+ }
9948
+ if (creds) {
9949
+ try {
9950
+ const token = await getIdToken();
9951
+ const resp = await fetch(`${API_BASE}/api/tasks?backlog=true`, {
9952
+ headers: { Authorization: `Bearer ${token}` },
9953
+ signal: AbortSignal.timeout(5e3)
9954
+ });
9955
+ checks.push({
9956
+ name: "auth",
9957
+ status: resp.ok ? "ok" : "fail",
9958
+ message: resp.ok ? `Authenticated request OK (HTTP ${resp.status})` : `Authenticated request failed: HTTP ${resp.status}`,
9959
+ fix: resp.ok ? void 0 : "numo login"
9960
+ });
9961
+ } catch (err) {
9962
+ checks.push({
9963
+ name: "auth",
9964
+ status: "fail",
9965
+ message: `Authenticated request error: ${errMessage(err)}`,
9966
+ fix: "numo login"
9967
+ });
9968
+ }
9920
9969
  }
9921
9970
  return checks;
9922
9971
  }
9923
9972
  function registerDoctorCommand(program3) {
9924
9973
  program3.command("doctor").description("Check CLI health and connectivity").action(async function() {
9925
9974
  const opts = this.optsWithGlobals();
9926
- const asJson = !!(opts.json || opts.quiet || !isInteractive());
9975
+ const asJson = isQuietMode(opts);
9927
9976
  const checks = await runChecks();
9928
9977
  const ok = checks.every((c) => c.status !== "fail");
9929
9978
  if (asJson) {
9930
- printJson({ ok, checks });
9979
+ printJson({ ok, exitCode: ok ? 0 : 1, checks });
9931
9980
  } else {
9932
9981
  console.log("");
9933
9982
  for (const check of checks) {
9934
- const icon = check.status === "ok" ? import_picocolors11.default.green(SYM.check) : check.status === "warn" ? import_picocolors11.default.yellow("!") : import_picocolors11.default.red(SYM.cross);
9983
+ const icon = check.status === "ok" ? import_picocolors10.default.green(SYM.check) : check.status === "warn" ? import_picocolors10.default.yellow("!") : import_picocolors10.default.red(SYM.cross);
9935
9984
  console.log(` ${icon} ${check.message}`);
9985
+ if (check.fix) {
9986
+ console.log(` ${import_picocolors10.default.dim("Fix:")} ${import_picocolors10.default.cyan("$")} ${import_picocolors10.default.bold(check.fix)}`);
9987
+ }
9936
9988
  }
9937
9989
  console.log("");
9938
- if (ok) {
9939
- console.log(` ${import_picocolors11.default.green("All checks passed.")}`);
9940
- } else {
9941
- console.log(` ${import_picocolors11.default.red("Some checks failed.")}`);
9942
- }
9990
+ console.log(` ${ok ? import_picocolors10.default.green("All checks passed.") : import_picocolors10.default.red("Some checks failed.")}`);
9943
9991
  console.log("");
9944
9992
  }
9945
9993
  if (!ok) process.exit(1);
9946
9994
  });
9947
9995
  }
9948
9996
 
9997
+ // src/cli/cli.ts
9998
+ init_dirs();
9999
+
9949
10000
  // src/cli/lib/update-check.ts
9950
10001
  var fs4 = __toESM(require("fs"), 1);
9951
10002
  var path2 = __toESM(require("path"), 1);
9952
- var import_picocolors12 = __toESM(require_picocolors(), 1);
10003
+ var import_picocolors11 = __toESM(require_picocolors(), 1);
10004
+ init_dirs();
9953
10005
  init_tty();
9954
10006
  var CHECK_INTERVAL = 24 * 60 * 60 * 1e3;
9955
10007
  var PACKAGE_NAME = "numo-cli";
10008
+ var REPO = "mindistio/numo-cli";
10009
+ var INSTALL_SCRIPT_URL = `https://raw.githubusercontent.com/${REPO}/main/install.sh`;
10010
+ var UPGRADE_NPM = `npm i -g ${PACKAGE_NAME}`;
10011
+ var UPGRADE_BINARY = `curl -fsSL ${INSTALL_SCRIPT_URL} | bash`;
9956
10012
  function getStatePath() {
9957
10013
  return path2.join(getConfigDir(), "update-check.json");
9958
10014
  }
@@ -9979,6 +10035,12 @@ function semverGt(a, b) {
9979
10035
  }
9980
10036
  return false;
9981
10037
  }
10038
+ function isBinaryInstall() {
10039
+ return !!process.versions.bun;
10040
+ }
10041
+ function upgradeCommand(isBinary = isBinaryInstall()) {
10042
+ return isBinary ? UPGRADE_BINARY : UPGRADE_NPM;
10043
+ }
9982
10044
  function checkForUpdate(currentVersion) {
9983
10045
  if (!isInteractive()) return;
9984
10046
  if (process.env.CI || process.env.NUMO_NO_UPDATE_CHECK) return;
@@ -9987,8 +10049,8 @@ function checkForUpdate(currentVersion) {
9987
10049
  if (state.latestVersion && semverGt(state.latestVersion, currentVersion)) {
9988
10050
  process.stderr.write(
9989
10051
  `
9990
- ${import_picocolors12.default.yellow("Update available")} ${import_picocolors12.default.dim(currentVersion)} ${import_picocolors12.default.dim("\u2192")} ${import_picocolors12.default.green(state.latestVersion)}
9991
- Run ${import_picocolors12.default.cyan("npm i -g numo-cli")} to update
10052
+ ${import_picocolors11.default.yellow("Update available")} ${import_picocolors11.default.dim(currentVersion)} ${import_picocolors11.default.dim("\u2192")} ${import_picocolors11.default.green(state.latestVersion)}
10053
+ Run ${import_picocolors11.default.cyan(upgradeCommand())} to update
9992
10054
 
9993
10055
  `
9994
10056
  );
@@ -10012,10 +10074,21 @@ function fetchLatestVersion(state) {
10012
10074
  }
10013
10075
  }
10014
10076
 
10077
+ // src/cli/lib/guide.ts
10078
+ var import_fs = require("fs");
10079
+ var import_path = require("path");
10080
+ function getAgentGuide() {
10081
+ if (true) return '# AGENTS.md \u2014 numo-cli for AI Agents\n\nInstructions for AI agents (Claude, GPT, Cursor, Copilot, etc.) integrating with [numo-cli](https://www.npmjs.com/package/numo-cli).\n\n## What is numo-cli?\n\n`numo` is the command-line client for the **Numo ADHD planner** \u2014 programmatic access to tasks, community posts, and profiles for any agent that can run shell commands. Invoke it as `numo <command>`; assume it is already installed and on `PATH`.\n\n## Authentication\n\n### Interactive (humans)\n\n```bash\nnumo login\n```\nPrompts for email + password, or use `--phone` for SMS OTP. Credentials are stored locally.\n\n### Non-interactive (agents / CI)\n\n**For long-running sessions (recommended) \u2014 email + password via env vars:**\n\n```bash\nexport NUMO_LOGIN_EMAIL="you@example.com"\nexport NUMO_LOGIN_PASSWORD="..."\nnumo login --json\n# stdout: {"ok":true,"uid":"...","email":"...","idToken":"...","idTokenExpiry":...}\n```\n\nThis path caches credentials locally and auto-refreshes the ID token in the background \u2014 every subsequent `numo` invocation in the session keeps working past the ~1-hour ID-token expiry.\n\n**For one-shot calls \u2014 pre-existing ID token:**\n\n```bash\nexport NUMO_TOKEN="<id-token>"\nnumo tasks list --json\n```\n\n`NUMO_TOKEN` does **not** auto-refresh \u2014 it is treated as opaque, single-use credentials. When the ID token expires (typically ~1 hour after issue), API calls will start returning 401 with no recovery path. Use this only for short scripts; otherwise prefer the email/password path above. Inspect remaining validity with `numo whoami --json` (decodes the JWT `exp` claim and reports `autoRefresh: false`).\n\n**Custom server host:** by default credentials are only sent to `*.numo.ai` (or loopback). To point `NUMO_API_URL` at a self-hosted/staging host, set `NUMO_ALLOW_CUSTOM_HOST=1` \u2014 otherwise credential-sending commands fail with a `CONFIG_ERROR` (exit 78). Offline commands (`commands`, `schema`, `--help`) are never gated.\n\n## JSON Mode\n\nAll commands output JSON when:\n1. stdout is piped (automatic detection)\n2. `--json` flag is passed\n3. `-q` / `--quiet` flag is passed (implies `--json`, suppresses interactive output)\n\n`numo tasks create` defaults to **private** tasks (`isPublic: false`) in every mode \u2014 interactive shells, JSON mode, scripts. Public tasks require an explicit `--public` flag (or picking "Public" in the interactive Visibility prompt). This is a stability guarantee per W-121 \u2014 the CLI must not accidentally expose tasks to the public community feed. New tasks are always inserted at the **top** of the list (`listPosition: \'top\'`).\n\n## Core Commands\n\n### Tasks\n\n```bash\n# List today\'s tasks\nnumo tasks list --json\n# \u2192 {"tasks":[...],"count":N,"pendingCount":N,"completedCount":N}\n\n# List by date (accepts natural language: tomorrow, next monday, in 3 days)\nnumo tasks list --date "next monday" --json\n\n# List backlog (undated tasks)\nnumo tasks list --backlog --json\n\n# Create a task (positional text or --text; private + inserted at top by default)\nnumo tasks create "Buy groceries" --due "2026-04-03" --json\nnumo tasks create --text "Weekly review" --due "next monday" --json\n# \u2192 {"task":{...},"karma":N}\n\n# Quick add (today, private), and recurring routines\nnumo tasks create "Call dentist" --due tomorrow --tags Health --json\nnumo tasks create "Standup" --repeat weekly --weekdays Mon,Wed,Fri --json\nnumo tasks create "Read later" --backlog --json\nnumo tasks create "Trip" --subtask "Book hotel" --subtask "Pack" --json # repeatable --subtask\n\n# Get task details\nnumo tasks get <taskId> --json\n\n# Update a task\nnumo tasks update <taskId> --text "New text" --due "2026-04-05" --json\nnumo tasks update <taskId> --no-time --json # strip time-of-day (all-day task)\n\n# Complete a task\nnumo tasks complete <taskId> --json\n# \u2192 {"completed":true,"task":{...}|null,"taskHistory":{...},"karma":N,"checksInRow":N}\n\n# Uncomplete (restore from history)\nnumo tasks uncomplete <historyId> --json\n\n# Delete a task (archives it)\nnumo tasks delete <taskId> --json\n```\n\n### Recurring reminders (natural-language requests)\n\nA request like *"remind me to plan my week every Monday"* maps to a recurring task. There is no separate "reminder" entity \u2014 a reminder **is** a task with a `remindDate`. Resolve two things the phrasing leaves implicit:\n\n1. **Anchor the first occurrence.** `--repeat weekly --weekdays Mon` alone sets `dueDate` to *today*, so the first instance can land before the intended weekday. Pass `--due "next monday"` to anchor it.\n2. **"Remind" needs a time.** An all-day task has `remindDate: null` (no notification fires). Add a time so the app derives `remindDate` (default lead ~3h before due).\n\n```bash\n# "remind me to plan my week every Monday" \u2192\nnumo tasks create "Plan my week" --due "next monday 09:00" --repeat weekly --weekdays Mon --json\n# \u2192 dueDate 2026-06-22 09:00, remindDate 2026-06-22 06:00, repeat weekly on Mon\n```\n\nPick a sensible default time (e.g. 09:00) or ask the user. `--due` accepts natural language (`"next monday"`, `"next monday 09:00"`).\n\n### Community (read-only)\n\n```bash\nnumo posts list --json # list posts (with commentsCount + likesCount)\nnumo posts get <postId> --json # post details\nnumo posts comments <postId> --json # list comments on a post\nnumo posts replies <postId> <commentId> --json # list replies to a comment\n```\n\n### Profile & Discovery\n\n```bash\nnumo profile --json # Current user profile\nnumo commands --json # List all available commands\nnumo schema "tasks create" # JSON schema for a specific command\nnumo doctor --json # Health check (DNS, TLS, /api/health, auth)\n```\n\n## Batch Operations\n\n```bash\n# Complete multiple tasks via stdin (one ID per line)\necho -e "taskId1\\ntaskId2" | numo tasks complete --stdin --json\n\n# Pipe IDs from list\nnumo tasks list --json --tag Work | jq -r \'.tasks[].id\' | numo tasks complete --stdin\n```\n\n## Error Handling\n\nAll errors return structured JSON on stderr:\n\n```json\n{\n "error": {\n "kind": "NOT_FOUND",\n "code": 100,\n "message": "Task not found",\n "suggestion": "numo tasks list"\n }\n}\n```\n\n### Error kinds\n\n`AUTH_REQUIRED`, `AUTH_EXPIRED`, `AUTH_FORBIDDEN`, `INVALID_INPUT`, `MISSING_ARGUMENT`, `NOT_FOUND`, `CONFLICT`, `NETWORK_ERROR`, `TIMEOUT`, `RATE_LIMITED`, `SERVICE_UNAVAILABLE`, `CONFIG_ERROR`, `INTERNAL`, `UNKNOWN`.\n\n### Exit codes\n\n| Code | Meaning |\n|------|---------|\n| `0` | OK |\n| `1` | General error |\n| `2` | Usage error (missing argument, invalid input) |\n| `69` | Service unavailable (network, server down) |\n| `75` | Temporary failure (timeout, rate limit) |\n| `77` | No permission (auth required / forbidden) |\n| `78` | Configuration error (missing env var, etc.) |\n| `100`| Not found |\n| `101`| Conflict |\n\n## Tips for Agents\n\n- Always pass `--json` or `-q` for structured output.\n- Use `numo commands --json` and `numo schema <command>` for runtime introspection. Both payloads include `schemaVersion` and `cliVersion` at the root \u2014 agents that pin behavior should branch on `schemaVersion`.\n- Natural language dates work for both `--date` and `--due`: `"tomorrow"`, `"next monday"`, `"in 3 days"`.\n- Task IDs are stable; store them for later operations.\n- `numo tasks create` defaults to **private** in every mode (see "JSON Mode" above) and inserts new tasks at the top of the list. Pass `--public` to make a task public.\n- For rate-limited errors (`429`), the response includes `retryAfter` (seconds) and `retryable: true`.\n- **PII in profile output:** `numo profile --json` includes `photoURL`, a signed storage URL containing a long-lived access token in the query string. Do not pipe `profile` output to public logs, build outputs, or screenshots \u2014 the URL grants read access to the avatar bytes for as long as it stays valid.\n';
10082
+ try {
10083
+ return (0, import_fs.readFileSync)((0, import_path.join)(process.cwd(), "AGENTS.md"), "utf8");
10084
+ } catch {
10085
+ return "Agent guide is unavailable in this build.\nSee https://github.com/mindistio/numo-cli/blob/main/AGENTS.md";
10086
+ }
10087
+ }
10088
+
10015
10089
  // src/cli/cli.ts
10016
- init_tty();
10017
10090
  init_errors();
10018
- var CLI_VERSION = true ? "1.3.0" : "0.0.0-dev";
10091
+ var CLI_VERSION = true ? "1.6.0" : "0.0.0-dev";
10019
10092
  var program2 = new Command();
10020
10093
  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
10094
  const opts = thisCommand.optsWithGlobals();
@@ -10023,130 +10096,129 @@ program2.name("numo").description("CLI for Numo \u2014 programmatic access for h
10023
10096
  thisCommand.setOptionValue("json", true);
10024
10097
  }
10025
10098
  }).addHelpText("after", `
10026
- ${import_picocolors13.default.bold("Output modes:")}
10099
+ ${import_picocolors12.default.bold("Output modes:")}
10027
10100
  Interactive (TTY) Tables, colors, spinners
10028
10101
  Piped / --json Clean JSON for scripting and agents
10029
10102
 
10030
- ${import_picocolors13.default.bold("Examples:")}
10031
- ${import_picocolors13.default.dim("$")} numo login
10032
- ${import_picocolors13.default.dim("$")} numo tasks list --date 2025-01-15
10033
- ${import_picocolors13.default.dim("$")} numo tasks create --text "Buy groceries" --due 2025-01-16`);
10034
- program2.command("login").description("Login with your Numo account").option("--phone", "Login with phone number (SMS OTP)").action(async (opts) => {
10035
- await login(opts);
10036
- }).addHelpText("after", `
10037
- Examples:
10038
- $ numo login # Interactive (email/password)
10039
- $ numo login --phone # SMS OTP flow`);
10040
- program2.command("register").description("Create a new Numo account").option("--email <email>", "Email address").option("--password <password>", "Password (min 6 chars; visible in ps/history \u2014 prefer interactive mode)").action(async (opts) => {
10041
- await register(opts);
10103
+ ${import_picocolors12.default.bold("Examples:")}
10104
+ ${import_picocolors12.default.dim("$")} numo login
10105
+ ${import_picocolors12.default.dim("$")} numo tasks list
10106
+ ${import_picocolors12.default.dim("$")} numo tasks create --text "Buy groceries" --due tomorrow
10107
+
10108
+ ${import_picocolors12.default.bold("Environment:")}
10109
+ NUMO_API_URL API server URL
10110
+ NUMO_TOKEN Pre-existing ID token (skips local credentials)
10111
+ NUMO_LOGIN_EMAIL Email for non-interactive login
10112
+ NUMO_LOGIN_PASSWORD Password for non-interactive login
10113
+ NUMO_NO_UPDATE_CHECK Disable update notifications`);
10114
+ program2.command("login").description("Login with your Numo account").option("--phone", "Login with phone number (SMS OTP)").action(async function() {
10115
+ await login(this.optsWithGlobals(), program2);
10042
10116
  }).addHelpText("after", `
10043
10117
  Examples:
10044
- $ numo register # Interactive
10045
- $ numo register --email user@example.com --password s3cret # Non-interactive`);
10118
+ $ numo login # Interactive (email/password)
10119
+ $ numo login --phone # SMS OTP flow
10120
+ $ NUMO_LOGIN_EMAIL=\u2026 NUMO_LOGIN_PASSWORD=\u2026 numo login --json # Non-interactive (CI/agents)`);
10046
10121
  program2.command("logout").description("Clear stored credentials").action(() => {
10047
10122
  clearCredentials();
10048
- console.log(import_picocolors13.default.green("Logged out."));
10049
- });
10050
- program2.command("whoami").description("Show current auth status (no API call)").action(function() {
10123
+ console.log(import_picocolors12.default.green("Logged out."));
10124
+ if (process.env.NUMO_TOKEN) {
10125
+ console.log(import_picocolors12.default.yellow("\n Note: NUMO_TOKEN env var is still set. Unset it to fully de-authenticate."));
10126
+ }
10127
+ }).addHelpText("after", `
10128
+ Examples:
10129
+ $ numo logout
10130
+
10131
+ If NUMO_TOKEN env var is set, it is not cleared by logout. Unset it separately:
10132
+ $ unset NUMO_TOKEN`);
10133
+ program2.command("whoami").description("Show current auth status (no API call)").addHelpText("after", `
10134
+ Examples:
10135
+ $ numo whoami
10136
+ $ numo whoami --json # \u2192 {"email":"...","uid":"...","tokenValid":true,"expiresIn":N,"source":"..."}`).action(function() {
10051
10137
  const opts = this.optsWithGlobals();
10052
- const asJson = !!(opts.json || opts.quiet || !isInteractive());
10138
+ const asJson = isQuietMode(opts);
10139
+ const envToken = process.env.NUMO_TOKEN;
10053
10140
  const creds = loadCredentials();
10054
- if (!creds) {
10141
+ if (!creds && envToken) {
10142
+ let tokenValid2 = false;
10143
+ let expiresIn2 = 0;
10144
+ let email = null;
10145
+ let uid = null;
10146
+ try {
10147
+ const payloadB64 = envToken.split(".")[1] ?? "";
10148
+ const payload = JSON.parse(Buffer.from(payloadB64, "base64").toString("utf8"));
10149
+ if (typeof payload.exp === "number") {
10150
+ const expMs = payload.exp * 1e3;
10151
+ tokenValid2 = Date.now() < expMs;
10152
+ expiresIn2 = Math.max(0, Math.floor((expMs - Date.now()) / 1e3));
10153
+ }
10154
+ if (typeof payload.email === "string") email = payload.email;
10155
+ if (typeof payload.user_id === "string") uid = payload.user_id;
10156
+ else if (typeof payload.sub === "string") uid = payload.sub;
10157
+ } catch {
10158
+ }
10055
10159
  if (asJson) {
10056
- console.error(JSON.stringify({ error: { message: "Not logged in", code: "AUTH_REQUIRED" } }));
10160
+ printJson({ email, uid, tokenValid: tokenValid2, expiresIn: expiresIn2, source: "NUMO_TOKEN", autoRefresh: false });
10057
10161
  } else {
10058
- console.error(`${import_picocolors13.default.red("Not logged in")}
10059
-
10060
- $ numo login
10061
- `);
10162
+ if (email) console.log(` ${import_picocolors12.default.bold("Email")} ${email}`);
10163
+ if (uid) console.log(` ${import_picocolors12.default.bold("UID")} ${uid}`);
10164
+ console.log(` ${import_picocolors12.default.bold("Token")} ${tokenValid2 ? import_picocolors12.default.green(`valid (expires in ${Math.floor(expiresIn2 / 60)}m)`) : import_picocolors12.default.red("expired or malformed")}`);
10165
+ console.log(` ${import_picocolors12.default.bold("Auth")} NUMO_TOKEN env var ${import_picocolors12.default.dim("(no auto-refresh; use NUMO_LOGIN_EMAIL/PASSWORD for long sessions)")}`);
10062
10166
  }
10063
- process.exit(ExitCode.NO_PERM);
10167
+ if (!tokenValid2) process.exitCode = ExitCode.NO_PERM;
10168
+ return;
10169
+ }
10170
+ if (!creds) {
10171
+ outputError(Errors.authRequired(), asJson);
10064
10172
  return;
10065
10173
  }
10066
10174
  const tokenValid = !!(creds.idToken && creds.idTokenExpiry && Date.now() < creds.idTokenExpiry);
10067
10175
  const expiresIn = creds.idTokenExpiry ? Math.max(0, Math.floor((creds.idTokenExpiry - Date.now()) / 1e3)) : 0;
10068
- const source = process.env.NUMO_TOKEN ? "NUMO_TOKEN" : "credentials_file";
10176
+ const source = "credentials_file";
10069
10177
  if (asJson) {
10070
- printJson({ email: creds.email, uid: creds.uid, tokenValid, expiresIn, source });
10178
+ printJson({ email: creds.email, uid: creds.uid, tokenValid, expiresIn, source, autoRefresh: true });
10071
10179
  } else {
10072
- console.log(` ${import_picocolors13.default.bold("Email")} ${creds.email}`);
10073
- console.log(` ${import_picocolors13.default.bold("UID")} ${creds.uid}`);
10074
- console.log(` ${import_picocolors13.default.bold("Token")} ${tokenValid ? import_picocolors13.default.green(`valid (expires in ${Math.floor(expiresIn / 60)}m)`) : import_picocolors13.default.yellow("expired (will auto-refresh)")}`);
10075
- console.log(` ${import_picocolors13.default.bold("Auth")} ${source === "NUMO_TOKEN" ? "NUMO_TOKEN env var" : "~/.numo/credentials.json"}`);
10180
+ console.log(` ${import_picocolors12.default.bold("Email")} ${creds.email}`);
10181
+ console.log(` ${import_picocolors12.default.bold("UID")} ${creds.uid}`);
10182
+ console.log(` ${import_picocolors12.default.bold("Token")} ${tokenValid ? import_picocolors12.default.green(`valid (expires in ${Math.floor(expiresIn / 60)}m)`) : import_picocolors12.default.yellow("expired (will auto-refresh)")}`);
10183
+ console.log(` ${import_picocolors12.default.bold("Auth")} ${getCredentialsPath()}`);
10076
10184
  }
10077
10185
  });
10078
10186
  registerTasksCommands(program2);
10079
- program2.command("add [text...]").description("Quick-add a task (today, public, no wizard)").option("--due <date>", "Due date YYYY-MM-DD (default: today)").option("--tags <tags>", "Comma-separated tags").option("--public", "Make task public (default)").option("--private", "Make task private").action(async function(textParts) {
10080
- const opts = this.optsWithGlobals();
10081
- const uid = requireUid();
10082
- const text = textParts?.join(" ");
10083
- if (!text) {
10084
- console.error('Usage: numo add "task text"');
10085
- process.exit(ExitCode.USAGE);
10086
- }
10087
- const body = {
10088
- text,
10089
- dueDate: opts.due ? parseHumanDate(opts.due) ?? opts.due : (/* @__PURE__ */ new Date()).toISOString().slice(0, 10)
10090
- };
10091
- if (opts.tags) body.tags = opts.tags.split(",");
10092
- if (opts.public) body.isPublic = true;
10093
- if (opts.private) body.isPublic = false;
10094
- await runCreate({
10095
- global: opts,
10096
- fn: () => createTask(uid, body),
10097
- dataKey: "task",
10098
- spinnerMessage: "Creating task...",
10099
- onInteractive: (_task, payload) => {
10100
- const { task, karma } = payload;
10101
- const check = import_picocolors13.default.green(SYM.check);
10102
- console.log(`
10103
- ${check} Created ${task.text} ${import_picocolors13.default.dim(task.id)}`);
10104
- if (karma) console.log(` ${formatKarmaGain(karma)}`);
10105
- console.log("");
10106
- }
10107
- });
10108
- });
10109
10187
  registerPostsCommands(program2);
10110
10188
  registerProfileCommands(program2);
10111
10189
  registerDoctorCommand(program2);
10112
10190
  program2.command("commands").description("List all available commands").action(function() {
10113
10191
  const opts = this.optsWithGlobals();
10114
- const useJson2 = !!(opts.json || opts.quiet || !isInteractive());
10115
- const commands = [];
10116
- function walk(cmd, prefix) {
10117
- for (const sub of cmd.commands) {
10118
- const fullName = prefix ? `${prefix} ${sub.name()}` : sub.name();
10119
- if (sub.commands.length > 0) {
10120
- walk(sub, fullName);
10121
- } else {
10122
- commands.push({
10123
- name: fullName,
10124
- description: sub.description(),
10125
- options: sub.options.map((o) => o.flags)
10126
- });
10127
- }
10128
- }
10129
- }
10130
- walk(program2, "");
10192
+ const useJson2 = isQuietMode(opts);
10193
+ const commands = collectCommands(program2);
10131
10194
  if (useJson2) {
10132
- console.log(JSON.stringify({ commands }));
10195
+ console.log(JSON.stringify({ schemaVersion: "1", cliVersion: CLI_VERSION, commands }));
10133
10196
  } else {
10134
- console.log("");
10135
- let lastGroup = "";
10136
- for (const cmd of commands) {
10137
- const group = cmd.name.split(" ")[0];
10138
- if (group !== lastGroup) {
10139
- if (lastGroup) console.log("");
10140
- console.log(` ${import_picocolors13.default.bold(group.charAt(0).toUpperCase() + group.slice(1) + ":")}`);
10141
- lastGroup = group;
10142
- }
10143
- console.log(` numo ${cmd.name.padEnd(30)} ${import_picocolors13.default.dim(cmd.description)}`);
10144
- }
10145
10197
  console.log(`
10146
- ${import_picocolors13.default.dim("Run numo <command> --help for details.")}
10198
+ ${formatCommandMap(commands)}`);
10199
+ console.log(`
10200
+ ${import_picocolors12.default.dim("Run numo <command> --help for details.")}
10147
10201
  `);
10148
10202
  }
10149
10203
  });
10204
+ program2.command("guide").alias("agents").description("Print the full agent integration guide (AGENTS.md)").addHelpText("after", `
10205
+ Examples:
10206
+ $ numo guide # full agent guide (Markdown)
10207
+ $ numo agents # alias
10208
+ $ numo guide --json # \u2192 {"schemaVersion":"1","cliVersion":"...","guide":"..."}`).action(function() {
10209
+ const opts = this.optsWithGlobals();
10210
+ const useJson2 = isQuietMode(opts);
10211
+ const guide = getAgentGuide();
10212
+ if (useJson2) {
10213
+ console.log(JSON.stringify({ schemaVersion: "1", cliVersion: CLI_VERSION, guide }));
10214
+ } else {
10215
+ console.log(guide);
10216
+ }
10217
+ });
10218
+ var OPTION_ENUMS = {
10219
+ "--repeat": ["daily", "weekly", "monthly", "none"],
10220
+ "--difficulty": [0, 1, 2, 3]
10221
+ };
10150
10222
  function buildCommandSchema(cmd, fullName) {
10151
10223
  return {
10152
10224
  name: fullName,
@@ -10154,14 +10226,23 @@ function buildCommandSchema(cmd, fullName) {
10154
10226
  arguments: cmd.registeredArguments?.map((a) => ({
10155
10227
  name: a.name(),
10156
10228
  required: a.required,
10229
+ variadic: a.variadic,
10157
10230
  description: a.description
10158
10231
  })) ?? [],
10159
- options: cmd.options.filter((o) => !["--json", "-q, --quiet"].includes(o.flags)).map((o) => ({
10160
- flags: o.flags,
10161
- description: o.description,
10162
- required: o.required,
10163
- default: o.defaultValue
10164
- }))
10232
+ options: cmd.options.filter((o) => !["--json", "-q, --quiet"].includes(o.flags)).map((o) => {
10233
+ const takesValue = o.required || o.optional;
10234
+ const repeatable = Array.isArray(o.defaultValue);
10235
+ const opt = {
10236
+ flags: o.flags,
10237
+ description: o.description,
10238
+ type: takesValue ? repeatable ? "string[]" : "string" : "boolean",
10239
+ required: o.required,
10240
+ default: o.defaultValue
10241
+ };
10242
+ if (repeatable) opt.repeatable = true;
10243
+ if (OPTION_ENUMS[o.long]) opt.enum = OPTION_ENUMS[o.long];
10244
+ return opt;
10245
+ })
10165
10246
  };
10166
10247
  }
10167
10248
  program2.command("schema [command]").description("Print JSON schema for a command (for AI agents)").action(function(cmdPath) {
@@ -10176,7 +10257,7 @@ program2.command("schema [command]").description("Print JSON schema for a comman
10176
10257
  var walk = walk2;
10177
10258
  const schemas = [];
10178
10259
  walk2(program2, "");
10179
- console.log(JSON.stringify({ commands: schemas }, null, 2));
10260
+ console.log(JSON.stringify({ schemaVersion: "1", cliVersion: CLI_VERSION, commands: schemas }, null, 2));
10180
10261
  return;
10181
10262
  }
10182
10263
  const parts = cmdPath.split(" ");
@@ -10190,7 +10271,7 @@ program2.command("schema [command]").description("Print JSON schema for a comman
10190
10271
  }
10191
10272
  cmd = sub;
10192
10273
  }
10193
- console.log(JSON.stringify(buildCommandSchema(cmd, cmdPath), null, 2));
10274
+ console.log(JSON.stringify({ schemaVersion: "1", cliVersion: CLI_VERSION, ...buildCommandSchema(cmd, cmdPath) }, null, 2));
10194
10275
  });
10195
10276
  program2.command("completion <shell>").description("Generate shell completion script").action(function(shell) {
10196
10277
  if (shell !== "zsh") {