numo-cli 1.5.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 +929 -885
  4. package/package.json +14 -14
package/dist/cli.cjs CHANGED
@@ -3302,7 +3302,7 @@ function classifyError(err) {
3302
3302
  return new CliError("UNKNOWN" /* UNKNOWN */, message, ExitCode.GENERAL, { cause: err });
3303
3303
  }
3304
3304
  function sanitizeErrorMessage(msg) {
3305
- return msg.replace(/https?:\/\/\S+/g, "<url>").replace(/\/(?:Users|home|var|tmp)\/\S+/g, "<path>").replace(/[A-Za-z0-9_-]{40,}/g, "<token>");
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
3306
  }
3307
3307
  var ExitCode, CliError, Errors;
3308
3308
  var init_errors = __esm({
@@ -3373,6 +3373,53 @@ var init_errors = __esm({
3373
3373
  }
3374
3374
  });
3375
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
+
3376
3423
  // src/cli/lib/api-client.ts
3377
3424
  var api_client_exports = {};
3378
3425
  __export(api_client_exports, {
@@ -3399,14 +3446,15 @@ function toCliError(err) {
3399
3446
  const e = body.error;
3400
3447
  const kind = e.kind ?? "UNKNOWN" /* UNKNOWN */;
3401
3448
  const exitCode = KIND_EXIT[kind] ?? ExitCode.GENERAL;
3402
- return new CliError(kind, e.message ?? "Unknown error", exitCode, {
3449
+ return new CliError(kind, sanitizeErrorMessage(e.message ?? "Unknown error"), exitCode, {
3403
3450
  retryable: e.retryable,
3404
3451
  retryAfter: e.retryAfter
3405
3452
  });
3406
3453
  }
3407
- return new CliError("UNKNOWN" /* UNKNOWN */, httpErr.message ?? "Unknown error", ExitCode.GENERAL);
3454
+ return new CliError("UNKNOWN" /* UNKNOWN */, sanitizeErrorMessage(httpErr.message ?? "Unknown error"), ExitCode.GENERAL);
3408
3455
  }
3409
3456
  async function apiHeaders() {
3457
+ assertSafeApiBase();
3410
3458
  const token = await getIdToken();
3411
3459
  return {
3412
3460
  Authorization: `Bearer ${token}`,
@@ -3423,17 +3471,14 @@ function url(path3, params) {
3423
3471
  const qs = sp.toString();
3424
3472
  return qs ? `${u2}?${qs}` : u2;
3425
3473
  }
3426
- var API_BASE, KIND_EXIT, api;
3474
+ var KIND_EXIT, api;
3427
3475
  var init_api_client = __esm({
3428
3476
  "src/cli/lib/api-client.ts"() {
3429
3477
  "use strict";
3430
3478
  init_credentials();
3431
3479
  init_http();
3432
3480
  init_errors();
3433
- API_BASE = process.env.NUMO_API_URL ?? (true ? "https://api.numo.ai" : "http://localhost:3000");
3434
- if (API_BASE !== "http://localhost:3000" && API_BASE.startsWith("http://")) {
3435
- process.stderr.write("[warn] NUMO_API_URL uses HTTP \u2014 tokens sent unencrypted. Use HTTPS in production.\n");
3436
- }
3481
+ init_api_base();
3437
3482
  KIND_EXIT = {
3438
3483
  AUTH_REQUIRED: ExitCode.NO_PERM,
3439
3484
  AUTH_EXPIRED: ExitCode.NO_PERM,
@@ -3487,7 +3532,12 @@ var init_api_client = __esm({
3487
3532
  // src/cli/auth/credentials.ts
3488
3533
  function loadCredentials() {
3489
3534
  try {
3490
- const data = JSON.parse(fs2.readFileSync(getCredentialsPath(), "utf8"));
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"));
3491
3541
  if (typeof data?.refreshToken !== "string" || typeof data?.uid !== "string" || typeof data?.email !== "string") {
3492
3542
  return null;
3493
3543
  }
@@ -3498,7 +3548,9 @@ function loadCredentials() {
3498
3548
  }
3499
3549
  function saveCredentials(creds) {
3500
3550
  ensureConfigDir();
3501
- fs2.writeFileSync(getCredentialsPath(), JSON.stringify(creds, null, 2), { mode: 384 });
3551
+ const path3 = getCredentialsPath();
3552
+ fs2.writeFileSync(path3, JSON.stringify(creds, null, 2), { mode: 384 });
3553
+ if (process.platform !== "win32") fs2.chmodSync(path3, 384);
3502
3554
  }
3503
3555
  function clearCredentials() {
3504
3556
  try {
@@ -3524,6 +3576,7 @@ async function getIdToken() {
3524
3576
  return refreshInFlight;
3525
3577
  }
3526
3578
  async function performRefresh(creds) {
3579
+ assertSafeApiBase();
3527
3580
  const { API_BASE: apiBase } = await Promise.resolve().then(() => (init_api_client(), api_client_exports));
3528
3581
  const { http: http2 } = await Promise.resolve().then(() => (init_http(), http_exports));
3529
3582
  const resp = await http2.post(
@@ -3543,6 +3596,7 @@ var init_credentials = __esm({
3543
3596
  fs2 = __toESM(require("fs"), 1);
3544
3597
  crypto = __toESM(require("crypto"), 1);
3545
3598
  init_dirs();
3599
+ init_api_base();
3546
3600
  refreshInFlight = null;
3547
3601
  }
3548
3602
  });
@@ -5261,7 +5315,12 @@ async function loadClack() {
5261
5315
  }
5262
5316
  async function promptText(opts) {
5263
5317
  if (!isInteractive()) {
5264
- 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
+ );
5265
5324
  }
5266
5325
  const p = await loadClack();
5267
5326
  const value = await p.text({
@@ -5276,7 +5335,12 @@ async function promptText(opts) {
5276
5335
  }
5277
5336
  async function promptPassword(opts) {
5278
5337
  if (!isInteractive()) {
5279
- 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
+ );
5280
5344
  }
5281
5345
  const p = await loadClack();
5282
5346
  const value = await p.password({
@@ -5290,7 +5354,12 @@ async function promptPassword(opts) {
5290
5354
  }
5291
5355
  async function promptSelect(opts) {
5292
5356
  if (!isInteractive()) {
5293
- 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
+ );
5294
5363
  }
5295
5364
  const p = await loadClack();
5296
5365
  const value = await p.select({
@@ -5318,7 +5387,12 @@ async function promptConfirm(opts) {
5318
5387
  }
5319
5388
  async function promptMultiSelect(opts) {
5320
5389
  if (!isInteractive()) {
5321
- throw new Error(`Missing required input: ${opts.message}. Use flags in non-interactive mode.`);
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
+ );
5322
5396
  }
5323
5397
  const p = await loadClack();
5324
5398
  const value = await p.multiselect({
@@ -5345,6 +5419,7 @@ var init_prompts = __esm({
5345
5419
  "src/cli/lib/prompts.ts"() {
5346
5420
  "use strict";
5347
5421
  init_tty();
5422
+ init_errors();
5348
5423
  }
5349
5424
  });
5350
5425
 
@@ -5354,6 +5429,7 @@ __export(phone_login_exports, {
5354
5429
  authenticateWithPhone: () => authenticateWithPhone
5355
5430
  });
5356
5431
  async function authenticateWithPhone(spinner) {
5432
+ assertSafeApiBase();
5357
5433
  const p = await Promise.resolve().then(() => (init_dist2(), dist_exports));
5358
5434
  const phone = await promptText({
5359
5435
  message: "Phone number (with country code)",
@@ -5370,7 +5446,7 @@ async function authenticateWithPhone(spinner) {
5370
5446
  const { sessionId, verifyUrl } = startResp.data;
5371
5447
  spinner.stop("");
5372
5448
  p.log.info("Opening browser for phone verification...");
5373
- 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}`));
5374
5450
  const { default: open } = await import("open");
5375
5451
  const cp = await open(verifyUrl);
5376
5452
  cp.unref();
@@ -5397,15 +5473,16 @@ async function authenticateWithPhone(spinner) {
5397
5473
  }
5398
5474
  throw Errors.networkError("Phone verification timed out. Try again.");
5399
5475
  }
5400
- var import_picocolors, POLL_INTERVAL, POLL_TIMEOUT;
5476
+ var import_picocolors4, POLL_INTERVAL, POLL_TIMEOUT;
5401
5477
  var init_phone_login = __esm({
5402
5478
  "src/cli/auth/phone-login.ts"() {
5403
5479
  "use strict";
5404
5480
  init_http();
5405
- import_picocolors = __toESM(require_picocolors(), 1);
5481
+ import_picocolors4 = __toESM(require_picocolors(), 1);
5406
5482
  init_errors();
5407
5483
  init_prompts();
5408
5484
  init_api_client();
5485
+ init_api_base();
5409
5486
  POLL_INTERVAL = 2e3;
5410
5487
  POLL_TIMEOUT = 5 * 60 * 1e3;
5411
5488
  }
@@ -5429,196 +5506,82 @@ var {
5429
5506
  } = import_index.default;
5430
5507
 
5431
5508
  // src/cli/cli.ts
5432
- var import_picocolors13 = __toESM(require_picocolors(), 1);
5509
+ var import_picocolors12 = __toESM(require_picocolors(), 1);
5433
5510
 
5434
5511
  // src/cli/auth/login.ts
5435
5512
  init_http();
5436
- var import_picocolors2 = __toESM(require_picocolors(), 1);
5513
+ var import_picocolors5 = __toESM(require_picocolors(), 1);
5437
5514
  init_credentials();
5438
- init_prompts();
5439
- init_errors();
5440
- init_api_client();
5441
- async function authenticateWithEmail(spinner) {
5442
- const email = await promptText({ message: "Email", required: true });
5443
- const password = await promptPassword({ message: "Password" });
5444
- spinner.start("Signing in...");
5445
- const resp = await http.post(
5446
- `${API_BASE}/api/auth/login`,
5447
- { email, password }
5448
- );
5449
- return {
5450
- refreshToken: resp.data.refreshToken,
5451
- uid: resp.data.uid,
5452
- displayName: resp.data.email ?? email,
5453
- idToken: resp.data.idToken,
5454
- idTokenExpiry: Date.now() + (resp.data.expiresIn || 3600) * 1e3
5455
- };
5515
+
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;
5456
5536
  }
5457
- function printSuccess(displayName) {
5458
- const lines = [
5459
- ` ${import_picocolors2.default.dim("$")} numo tasks list --date YYYY-MM-DD List tasks for a date`,
5460
- ` ${import_picocolors2.default.dim("$")} numo tasks create --text "..." Create a task`,
5461
- ` ${import_picocolors2.default.dim("$")} numo profile View your profile`
5462
- ];
5463
- console.log(`
5464
- ${import_picocolors2.default.bold("Available commands:")}
5465
- ${lines.join("\n")}
5466
- `);
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]));
5467
5540
  }
5468
- async function login(options = {}) {
5469
- const p = await Promise.resolve().then(() => (init_dist2(), dist_exports));
5470
- p.intro(import_picocolors2.default.bold("Numo \u2014 Login"));
5471
- let method = options.phone ? "phone" : "email";
5472
- if (!options.phone) {
5473
- method = await promptSelect({
5474
- message: "How would you like to sign in?",
5475
- options: [
5476
- { value: "email", label: "Email & password" },
5477
- { value: "phone", label: "Phone number (SMS)" }
5478
- ]
5479
- });
5480
- }
5481
- const s = p.spinner();
5482
- try {
5483
- let result;
5484
- if (method === "phone") {
5485
- const { authenticateWithPhone: authenticateWithPhone2 } = await Promise.resolve().then(() => (init_phone_login(), phone_login_exports));
5486
- result = await authenticateWithPhone2(s);
5487
- } else {
5488
- result = await authenticateWithEmail(s);
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;
5489
5550
  }
5490
- saveCredentials({
5491
- refreshToken: result.refreshToken,
5492
- uid: result.uid,
5493
- email: result.displayName,
5494
- idToken: result.idToken,
5495
- idTokenExpiry: result.idTokenExpiry
5496
- });
5497
- s.stop(`Logged in as ${import_picocolors2.default.green(result.displayName)}`);
5498
- p.outro("You are ready to go!");
5499
- printSuccess(result.displayName);
5500
- } catch (err) {
5501
- const classified = err instanceof CliError ? err : classifyError(err);
5502
- s.stop(import_picocolors2.default.red("Login failed"));
5503
- p.log.error(classified.message);
5504
- if (classified.options.hint) p.log.warning(classified.options.hint);
5505
- process.exit(classified.exitCode);
5551
+ lines.push(` numo ${cmd.name.padEnd(30)} ${import_picocolors.default.dim(cmd.description)}`);
5506
5552
  }
5553
+ return lines.join("\n");
5507
5554
  }
5508
5555
 
5509
- // src/cli/auth/register.ts
5510
- init_http();
5511
- var import_picocolors3 = __toESM(require_picocolors(), 1);
5512
- init_credentials();
5556
+ // src/cli/auth/login.ts
5513
5557
  init_prompts();
5514
5558
  init_errors();
5515
- init_tty();
5516
5559
  init_api_client();
5517
- function validateEmail(email) {
5518
- const trimmed = email.trim();
5519
- if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(trimmed)) {
5520
- throw Errors.invalidInput("Invalid email address");
5521
- }
5522
- return trimmed;
5523
- }
5524
- function validatePassword(password) {
5525
- if (password.length < 6) {
5526
- throw Errors.invalidInput("Password is too weak (min 6 characters)");
5527
- }
5528
- return password;
5529
- }
5530
- function classifySignUpError(err) {
5531
- if (err instanceof CliError) return err;
5532
- const resp = err?.response?.data?.error;
5533
- const kind = resp?.kind ?? "";
5534
- const msg = resp?.message ?? "";
5535
- if (kind === "CONFLICT" || msg.includes("already in use")) {
5536
- return Errors.invalidInput(
5537
- "Email already in use",
5538
- "Already have an account? Run: numo login"
5539
- );
5540
- }
5541
- if (msg.includes("Invalid email")) {
5542
- return Errors.invalidInput("Invalid email address");
5543
- }
5544
- if (msg.includes("Password too weak") || msg.includes("min 6")) {
5545
- return Errors.invalidInput("Password is too weak (min 6 characters)");
5546
- }
5547
- return classifyError(err);
5548
- }
5549
- async function signUp(email, password) {
5550
- try {
5551
- const resp = await http.post(`${API_BASE}/api/auth/register`, {
5552
- email,
5553
- password,
5554
- tz: Intl.DateTimeFormat().resolvedOptions().timeZone,
5555
- tzOffset: (/* @__PURE__ */ new Date()).getTimezoneOffset()
5556
- });
5557
- return {
5558
- refreshToken: resp.data.refreshToken,
5559
- uid: resp.data.uid,
5560
- displayName: resp.data.email ?? email,
5561
- idToken: resp.data.idToken,
5562
- idTokenExpiry: Date.now() + (resp.data.expiresIn || 3600) * 1e3
5563
- };
5564
- } catch (err) {
5565
- throw classifySignUpError(err);
5560
+ init_api_base();
5561
+
5562
+ // src/cli/lib/quiet.ts
5563
+ init_tty();
5564
+ var noopSpinner = {
5565
+ start: () => {
5566
+ },
5567
+ stop: () => {
5566
5568
  }
5569
+ };
5570
+ function isQuietMode(opts = {}) {
5571
+ return !!(opts.quiet || opts.json || !isInteractive());
5567
5572
  }
5568
- async function register(options = {}) {
5573
+ async function makeClackSpinner(quietMode) {
5574
+ if (quietMode) return noopSpinner;
5569
5575
  const p = await Promise.resolve().then(() => (init_dist2(), dist_exports));
5570
- p.intro(import_picocolors3.default.bold("Numo \u2014 Register"));
5571
- const s = p.spinner();
5572
- let spinnerActive = false;
5573
- try {
5574
- const email = validateEmail(
5575
- options.email ?? await promptText({ message: "Email", required: true })
5576
- );
5577
- const rawPassword = options.password ?? await promptPassword({ message: "Password" });
5578
- if (!options.password && isInteractive()) {
5579
- const confirm = await promptPassword({ message: "Confirm password" });
5580
- if (rawPassword !== confirm) {
5581
- throw Errors.invalidInput("Passwords do not match");
5582
- }
5583
- }
5584
- const password = validatePassword(rawPassword);
5585
- s.start("Creating account...");
5586
- spinnerActive = true;
5587
- const result = await signUp(email, password);
5588
- saveCredentials({
5589
- refreshToken: result.refreshToken,
5590
- uid: result.uid,
5591
- email: result.displayName,
5592
- idToken: result.idToken,
5593
- idTokenExpiry: result.idTokenExpiry
5594
- });
5595
- s.stop(`Registered as ${import_picocolors3.default.green(result.displayName)}`);
5596
- p.outro("Welcome to Numo!");
5597
- printSuccess(result.displayName);
5598
- } catch (err) {
5599
- const classified = err instanceof CliError ? err : classifySignUpError(err);
5600
- if (spinnerActive) s.stop(import_picocolors3.default.red("Registration failed"));
5601
- p.log.error(classified.message);
5602
- if (classified.options.hint) p.log.warning(classified.options.hint);
5603
- process.exit(classified.exitCode);
5604
- }
5576
+ return p.spinner();
5605
5577
  }
5606
5578
 
5607
- // src/cli/cli.ts
5608
- init_credentials();
5609
-
5610
- // src/cli/commands/tasks.ts
5611
- var import_picocolors8 = __toESM(require_picocolors(), 1);
5612
-
5613
- // src/cli/lib/actions.ts
5614
- init_tty();
5615
-
5616
5579
  // src/cli/lib/output.ts
5617
- var import_picocolors5 = __toESM(require_picocolors(), 1);
5580
+ var import_picocolors3 = __toESM(require_picocolors(), 1);
5618
5581
  init_tty();
5619
5582
 
5620
5583
  // src/cli/lib/table.ts
5621
- var import_picocolors4 = __toESM(require_picocolors(), 1);
5584
+ var import_picocolors2 = __toESM(require_picocolors(), 1);
5622
5585
  init_tty();
5623
5586
  var BOX = isUnicodeSupported ? {
5624
5587
  topLeft: String.fromCodePoint(9484),
@@ -5657,17 +5620,17 @@ var BOX = isUnicodeSupported ? {
5657
5620
  cross: "+"
5658
5621
  };
5659
5622
  function renderTable(headers, rows, emptyMessage = "(no results)") {
5660
- if (rows.length === 0) return import_picocolors4.default.dim(emptyMessage);
5623
+ if (rows.length === 0) return import_picocolors2.default.dim(emptyMessage);
5661
5624
  const widths = headers.map(
5662
5625
  (h2, i) => Math.max(h2.length, ...rows.map((r) => (r[i] ?? "").length))
5663
5626
  );
5664
- 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));
5665
5628
  const topBorder = BOX.topLeft + widths.map((w) => BOX.horizontal.repeat(w + 2)).join(BOX.midTop) + BOX.topRight;
5666
5629
  const midBorder = BOX.midLeft + widths.map((w) => BOX.horizontal.repeat(w + 2)).join(BOX.cross) + BOX.midRight;
5667
5630
  const bottomBorder = BOX.bottomLeft + widths.map((w) => BOX.horizontal.repeat(w + 2)).join(BOX.midBottom) + BOX.bottomRight;
5668
5631
  const formatRow = (cells, bold = false) => BOX.vertical + cells.map((c, i) => {
5669
- const padded = pad(c, widths[i]);
5670
- return ` ${bold ? import_picocolors4.default.bold(padded) : padded} `;
5632
+ const padded = pad2(c, widths[i]);
5633
+ return ` ${bold ? import_picocolors2.default.bold(padded) : padded} `;
5671
5634
  }).join(BOX.vertical) + BOX.vertical;
5672
5635
  const lines = [
5673
5636
  topBorder,
@@ -5701,17 +5664,15 @@ function printRecord(fields) {
5701
5664
  const visible = fields.filter(([, v]) => v != null && v !== "");
5702
5665
  const maxLabel = Math.max(...visible.map(([l]) => l.length));
5703
5666
  for (const [label, value] of visible) {
5704
- console.log(` ${import_picocolors5.default.bold(label.padEnd(maxLabel))} ${value}`);
5667
+ console.log(` ${import_picocolors3.default.bold(label.padEnd(maxLabel))} ${value}`);
5705
5668
  }
5706
5669
  }
5707
5670
  function outputResult(data, asJson) {
5708
- if (asJson) {
5709
- printJson(data);
5710
- } else if (typeof data === "string") {
5671
+ if (!asJson && typeof data === "string") {
5711
5672
  console.log(data);
5712
- } else {
5713
- printJson(data);
5673
+ return;
5714
5674
  }
5675
+ printJson(data);
5715
5676
  }
5716
5677
  function pickFields(obj, fields) {
5717
5678
  const result = {};
@@ -5746,148 +5707,145 @@ function outputError(err, asJson) {
5746
5707
  console.error(JSON.stringify(structured.toJSON(), null, 2));
5747
5708
  } else {
5748
5709
  console.error(`
5749
- ${import_picocolors5.default.red("Error")}: ${structured.message}`);
5710
+ ${import_picocolors3.default.red("Error")}: ${structured.message}`);
5750
5711
  if (structured.options.suggestion) {
5751
5712
  console.error(`
5752
- ${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)}`);
5753
5714
  }
5754
5715
  if (structured.options.hint) {
5755
- console.error(` ${import_picocolors5.default.dim(structured.options.hint)}`);
5716
+ console.error(` ${import_picocolors3.default.dim(structured.options.hint)}`);
5756
5717
  }
5757
5718
  console.error("");
5758
5719
  }
5759
5720
  process.exit(structured.exitCode);
5760
5721
  }
5761
5722
 
5762
- // src/cli/lib/spinner.ts
5763
- var import_picocolors6 = __toESM(require_picocolors(), 1);
5764
- init_tty();
5765
-
5766
- // src/cli/lib/symbols.ts
5767
- init_tty();
5768
- var u = isUnicodeSupported;
5769
- var SYM = {
5770
- check: u ? "\u2714" : "v",
5771
- // ✓
5772
- cross: u ? "\u2718" : "x",
5773
- // ✗
5774
- circle: u ? "\u25CB" : "o",
5775
- // ○
5776
- repeat: u ? "\u21BB" : "R",
5777
- // ↻
5778
- undo: u ? "\u21A9" : "<-",
5779
- // ↩
5780
- fire: u ? "\u{1F525}" : "*",
5781
- // 🔥
5782
- ellipsis: u ? "\u2026" : "...",
5783
- // …
5784
- dash: u ? "\u2500" : "-",
5785
- // ─
5786
- bullet: u ? "\u2022" : "*",
5787
- // •
5788
- arrow: u ? "\u2192" : "->"
5789
- // →
5790
- };
5791
-
5792
- // src/cli/lib/spinner.ts
5793
- var FRAMES = isUnicodeSupported ? [
5794
- String.fromCodePoint(10251),
5795
- String.fromCodePoint(10265),
5796
- String.fromCodePoint(10297),
5797
- String.fromCodePoint(10296),
5798
- String.fromCodePoint(10300),
5799
- String.fromCodePoint(10292),
5800
- String.fromCodePoint(10278),
5801
- String.fromCodePoint(10279),
5802
- String.fromCodePoint(10247),
5803
- String.fromCodePoint(10255)
5804
- ] : ["-", "\\", "|", "/"];
5805
- var INTERVAL = 80;
5806
- var MAX_RETRIES2 = 3;
5807
- function createSpinner(interactive) {
5808
- if (!interactive) {
5809
- return { start() {
5810
- }, update() {
5811
- }, stop() {
5812
- }, fail() {
5813
- } };
5814
- }
5815
- let timer = null;
5816
- let frameIdx = 0;
5817
- let currentMsg = "";
5818
- function clear() {
5819
- process.stderr.write("\r\x1B[K");
5820
- }
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 });
5821
5727
  return {
5822
- start(msg) {
5823
- currentMsg = msg;
5824
- frameIdx = 0;
5825
- clear();
5826
- process.stderr.write(`${import_picocolors6.default.cyan(FRAMES[0])} ${msg}`);
5827
- timer = setInterval(() => {
5828
- frameIdx = (frameIdx + 1) % FRAMES.length;
5829
- clear();
5830
- process.stderr.write(`${import_picocolors6.default.cyan(FRAMES[frameIdx])} ${currentMsg}`);
5831
- }, INTERVAL);
5832
- },
5833
- update(msg) {
5834
- currentMsg = msg;
5835
- },
5836
- stop(msg) {
5837
- if (timer) {
5838
- clearInterval(timer);
5839
- timer = null;
5840
- }
5841
- clear();
5842
- process.stderr.write(`${import_picocolors6.default.green(SYM.check)} ${msg ?? currentMsg}
5843
- `);
5844
- },
5845
- fail(msg) {
5846
- if (timer) {
5847
- clearInterval(timer);
5848
- timer = null;
5849
- }
5850
- clear();
5851
- process.stderr.write(`${import_picocolors6.default.red(SYM.cross)} ${msg}
5852
- `);
5853
- }
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
5854
5733
  };
5855
5734
  }
5856
- function delay2(ms) {
5857
- 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);
5858
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
+ }
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
5859
5833
  async function withSpinner(interactive, message, fn) {
5860
- const spinner = createSpinner(interactive);
5834
+ const spinner = await makeClackSpinner(!interactive);
5861
5835
  spinner.start(message);
5862
- for (let attempt = 0; attempt <= MAX_RETRIES2; attempt++) {
5863
- try {
5864
- const result = await fn();
5865
- spinner.stop(message);
5866
- return result;
5867
- } catch (err) {
5868
- const status = err.response?.status;
5869
- if (status === 429 && attempt < MAX_RETRIES2) {
5870
- const retryAfter = err.response?.headers?.["retry-after"];
5871
- const waitMs = retryAfter ? parseInt(retryAfter, 10) * 1e3 : 1e3 * Math.pow(2, attempt);
5872
- spinner.update(`Rate limited, retrying in ${Math.round(waitMs / 1e3)}s...`);
5873
- await delay2(waitMs);
5874
- spinner.update(message);
5875
- continue;
5876
- }
5877
- spinner.fail(message);
5878
- throw err;
5879
- }
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;
5880
5843
  }
5881
- throw new Error("Max retries exceeded");
5882
5844
  }
5883
5845
 
5884
5846
  // src/cli/lib/actions.ts
5885
- function useJson(opts) {
5886
- return !!(opts.json || opts.quiet || !isInteractive());
5887
- }
5888
- function useSpinner(opts) {
5889
- return isInteractive() && !opts.quiet;
5890
- }
5847
+ var useJson = isQuietMode;
5848
+ var useSpinner = (opts) => !isQuietMode(opts);
5891
5849
  async function runGet(opts) {
5892
5850
  try {
5893
5851
  const result = await withSpinner(
@@ -5973,25 +5931,22 @@ async function runWrite(opts) {
5973
5931
  outputError(err, useJson(opts.global));
5974
5932
  }
5975
5933
  }
5976
- async function runDelete(opts) {
5977
- try {
5978
- await withSpinner(
5979
- useSpinner(opts.global),
5980
- opts.spinnerMessage ?? "Deleting...",
5981
- opts.fn
5982
- );
5983
- if (!opts.global.quiet) {
5984
- console.log(opts.successMessage);
5985
- }
5986
- } catch (err) {
5987
- outputError(err, useJson(opts.global));
5988
- }
5989
- }
5990
5934
 
5991
5935
  // src/cli/lib/uid.ts
5992
5936
  init_credentials();
5993
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
+ }
5994
5948
  function requireUid() {
5949
+ if (process.env.NUMO_TOKEN) return uidFromToken(process.env.NUMO_TOKEN) ?? "";
5995
5950
  const creds = loadCredentials();
5996
5951
  if (!creds) throw Errors.authRequired();
5997
5952
  return creds.uid;
@@ -5999,34 +5954,58 @@ function requireUid() {
5999
5954
 
6000
5955
  // src/cli/services/tasks.ts
6001
5956
  init_api_client();
6002
- async function listTasks(uid, opts) {
5957
+ async function listTasks(opts) {
6003
5958
  return api.get("/api/tasks", {
6004
5959
  date: opts.date,
6005
5960
  backlog: opts.backlog ? "true" : void 0,
6006
5961
  tag: opts.tag
6007
5962
  });
6008
5963
  }
6009
- async function getTask(uid, id) {
5964
+ async function getTask(id) {
6010
5965
  return api.get(`/api/tasks/${encodeURIComponent(id)}`);
6011
5966
  }
6012
- async function createTask(uid, body) {
5967
+ async function createTask(body) {
6013
5968
  return api.post("/api/tasks", body);
6014
5969
  }
6015
- async function updateTask(uid, id, body) {
5970
+ async function updateTask(id, body) {
6016
5971
  return api.patch(`/api/tasks/${encodeURIComponent(id)}`, body);
6017
5972
  }
6018
- async function deleteTask(uid, id) {
5973
+ async function deleteTask(id) {
6019
5974
  return api.del(`/api/tasks/${encodeURIComponent(id)}`);
6020
5975
  }
6021
- async function completeTask(uid, id, date) {
5976
+ async function completeTask(id, date) {
6022
5977
  return api.post(`/api/tasks/${encodeURIComponent(id)}/complete`, date ? { date } : void 0);
6023
5978
  }
6024
- async function uncompleteTask(uid, taskHistoryId) {
5979
+ async function uncompleteTask(taskHistoryId) {
6025
5980
  return api.post(`/api/tasks/${encodeURIComponent(taskHistoryId)}/uncomplete`);
6026
5981
  }
6027
5982
 
6028
5983
  // src/cli/lib/format.ts
6029
- 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
6030
6009
  function formatDate(ts) {
6031
6010
  if (ts == null) return "";
6032
6011
  const d = typeof ts === "number" ? new Date(ts) : new Date(ts);
@@ -6054,7 +6033,7 @@ function formatRelativeDate(ts) {
6054
6033
  }
6055
6034
  function formatTags(tags) {
6056
6035
  if (!Array.isArray(tags) || tags.length === 0) return "";
6057
- return tags.map((t2) => import_picocolors7.default.cyan(`#${t2}`)).join(" ");
6036
+ return tags.map((t2) => import_picocolors6.default.cyan(`#${t2}`)).join(" ");
6058
6037
  }
6059
6038
  function formatDifficulty(d) {
6060
6039
  if (d == null) return "";
@@ -6094,21 +6073,21 @@ function formatWeekdayHeader(date, streakCount) {
6094
6073
  const cols = process.stdout.columns ?? 80;
6095
6074
  const fire = SYM.fire;
6096
6075
  const streakStr = streakCount && streakCount > 0 ? `${fire} ${streakCount}` : "";
6097
- const pad = Math.max(0, cols - weekday.length - streakStr.length - 4);
6098
- const line1 = ` ${import_picocolors7.default.bold(weekday)}${" ".repeat(pad)}${streakStr}`;
6099
- 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)}`;
6100
6079
  return `${line1}
6101
6080
  ${line2}`;
6102
6081
  }
6103
6082
  function formatKarmaGain(points, checksInRow) {
6104
- const base = import_picocolors7.default.green(`+${points} karma`);
6083
+ const base = import_picocolors6.default.green(`+${points} karma`);
6105
6084
  if (checksInRow && checksInRow > 1) {
6106
- return `${base} ${import_picocolors7.default.yellow(`streak x${checksInRow}!`)}`;
6085
+ return `${base} ${import_picocolors6.default.yellow(`streak x${checksInRow}!`)}`;
6107
6086
  }
6108
6087
  return base;
6109
6088
  }
6110
6089
  function formatProgressSummary(completed, total) {
6111
- return import_picocolors7.default.dim(`${completed}/${total} done today`);
6090
+ return import_picocolors6.default.dim(`${completed}/${total} done today`);
6112
6091
  }
6113
6092
  function formatTagsSummary(tasks) {
6114
6093
  const counts = {};
@@ -6120,7 +6099,7 @@ function formatTagsSummary(tasks) {
6120
6099
  }
6121
6100
  const entries = Object.entries(counts);
6122
6101
  if (entries.length === 0) return "";
6123
- 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(" ");
6124
6103
  }
6125
6104
 
6126
6105
  // src/cli/commands/tasks.ts
@@ -8994,6 +8973,99 @@ function parseHumanDateOnly(input) {
8994
8973
  return result.slice(0, 10);
8995
8974
  }
8996
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
+
8997
9069
  // src/cli/lib/stdin.ts
8998
9070
  var fs3 = __toESM(require("fs"), 1);
8999
9071
  function readStdinLines() {
@@ -9002,13 +9074,16 @@ function readStdinLines() {
9002
9074
  }
9003
9075
 
9004
9076
  // src/cli/commands/tasks.ts
9005
- async function pickTask(uid, id, actionName) {
9077
+ function collectValue(value, previous) {
9078
+ return previous.concat([value]);
9079
+ }
9080
+ async function pickTask(id, actionName) {
9006
9081
  if (id) return id;
9007
9082
  if (!isInteractive()) {
9008
9083
  throw Errors.missingArg("Task ID", "id");
9009
9084
  }
9010
- const today2 = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
9011
- const { tasks } = await listTasks(uid, { date: today2 });
9085
+ const today2 = localDateOnly();
9086
+ const { tasks } = await listTasks({ date: today2 });
9012
9087
  const pending = tasks.filter((t2) => !t2.completed);
9013
9088
  if (pending.length === 0) {
9014
9089
  throw Errors.invalidInput(`No pending tasks for today (${today2}). Use: numo tasks ${actionName} <id>`);
@@ -9017,23 +9092,21 @@ async function pickTask(uid, id, actionName) {
9017
9092
  message: `Select task to ${actionName}`,
9018
9093
  options: pending.map((t2) => ({
9019
9094
  value: t2.id,
9020
- label: `${truncate(t2.text, 50)} ${import_picocolors8.default.dim(t2.id)}`
9095
+ label: `${truncate(t2.text, 50)} ${import_picocolors7.default.dim(t2.id)}`
9021
9096
  }))
9022
9097
  });
9023
9098
  return selected;
9024
9099
  }
9025
9100
  function resolveDate(opts) {
9026
9101
  if (opts.backlog) return void 0;
9027
- const fmt = (d) => d.toISOString().slice(0, 10);
9028
- const today2 = /* @__PURE__ */ new Date();
9029
- if (opts.yesterday) return fmt(new Date(today2.getTime() - 864e5));
9030
- if (opts.tomorrow) return fmt(new Date(today2.getTime() + 864e5));
9102
+ if (opts.yesterday) return localDateOffset(-1);
9103
+ if (opts.tomorrow) return localDateOffset(1);
9031
9104
  if (opts.date) {
9032
9105
  const parsed = parseHumanDateOnly(opts.date);
9033
9106
  if (!parsed) throw Errors.invalidInput(`Cannot parse date: "${opts.date}". Use YYYY-MM-DD or natural language (tomorrow, next monday, etc.)`);
9034
9107
  return parsed;
9035
9108
  }
9036
- return fmt(today2);
9109
+ return localDateOnly();
9037
9110
  }
9038
9111
  function extractTime(dueDate) {
9039
9112
  if (typeof dueDate !== "string") return "";
@@ -9046,9 +9119,9 @@ function isRepeating(t2) {
9046
9119
  return !!t2.repeat?.type && t2.repeat.type !== "none";
9047
9120
  }
9048
9121
  function getCheckIndicator(t2) {
9049
- if (t2.completed) return import_picocolors8.default.green(SYM.check);
9050
- if (isRepeating(t2)) return import_picocolors8.default.blue(SYM.repeat);
9051
- 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);
9052
9125
  }
9053
9126
  function sortTasksForDisplay(tasks) {
9054
9127
  const repeatOrder = { daily: 0, weekly: 1, monthly: 2 };
@@ -9071,50 +9144,60 @@ function sortTasksForDisplay(tasks) {
9071
9144
  });
9072
9145
  }
9073
9146
  function printTaskDetail(t2) {
9074
- const dim = import_picocolors8.default.dim;
9147
+ const dim = import_picocolors7.default.dim;
9075
9148
  console.log("");
9076
9149
  printRecord([
9077
9150
  ["ID", dim(t2.id)],
9078
9151
  ["Text", t2.text],
9079
9152
  ["Due", formatDate(t2.dueDate) || dim("none (backlog)")],
9080
- ["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")],
9081
9154
  ["Tags", formatTags(t2.tags) || dim("none")],
9082
9155
  ["Difficulty", formatDifficulty(t2.difficulty) || dim("not set")],
9083
9156
  ["Duration", formatDuration(t2.duration) || dim("not set")],
9084
9157
  ["Repeat", formatRepeat(t2.repeat) || dim("none")],
9085
9158
  ["Note", t2.note || dim("none")],
9086
- ["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")],
9087
9160
  ["Completions", String(t2.completions ?? 0)],
9088
9161
  ["Created", formatDate(t2.createdAt)]
9089
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
+ }
9090
9170
  console.log("");
9091
9171
  }
9092
9172
  function printTaskLine(t2) {
9093
9173
  const check = getCheckIndicator(t2);
9094
9174
  const rawText = truncate(t2.text, 50);
9095
- 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;
9096
9176
  const time = extractTime(t2.dueDate);
9097
9177
  const tags = formatTags(t2.tags);
9098
9178
  const difficulty = formatDifficulty(t2.difficulty);
9099
- const id = import_picocolors8.default.dim(t2.id);
9179
+ const id = import_picocolors7.default.dim(t2.id);
9100
9180
  const parts = [check, text];
9101
- if (time) parts.push(import_picocolors8.default.cyan(time));
9181
+ if (time) parts.push(import_picocolors7.default.cyan(time));
9102
9182
  if (tags) parts.push(tags);
9103
- if (difficulty) parts.push(import_picocolors8.default.dim(`[${difficulty}]`));
9183
+ if (difficulty) parts.push(import_picocolors7.default.dim(`[${difficulty}]`));
9104
9184
  parts.push(id);
9105
9185
  console.log(" " + parts.join(" "));
9106
9186
  }
9187
+ function printPartialNotice(failed) {
9188
+ console.log(` ${import_picocolors7.default.yellow("! some bookkeeping was deferred")} ${import_picocolors7.default.dim(`(${(failed ?? []).join(", ")})`)}`);
9189
+ }
9107
9190
  function registerTasksCommands(program3) {
9108
9191
  const tasks = program3.command("tasks").description("Manage tasks");
9109
- 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() {
9110
9193
  const opts = this.optsWithGlobals();
9111
- const uid = requireUid();
9194
+ requireUid();
9112
9195
  const date = resolveDate(opts);
9113
9196
  await runList({
9114
9197
  global: opts,
9115
- fn: () => listTasks(uid, { date, backlog: opts.backlog, tag: opts.tag }),
9198
+ fn: () => listTasks({ date, backlog: opts.backlog, tag: opts.tag }),
9116
9199
  dataKey: "tasks",
9117
- columns: ["id", "text", "dueDate", "completed", "tags", "priority"],
9200
+ columns: ["id", "text", "dueDate", "completed", "tags"],
9118
9201
  spinnerMessage: "Fetching tasks...",
9119
9202
  onInteractive: (payload) => {
9120
9203
  const items = payload.tasks;
@@ -9122,7 +9205,7 @@ function registerTasksCommands(program3) {
9122
9205
  const completed = items.filter((t2) => t2.completed);
9123
9206
  console.log("");
9124
9207
  if (opts.backlog) {
9125
- 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})`)}`);
9126
9209
  } else {
9127
9210
  const viewDate = date ? /* @__PURE__ */ new Date(date + "T00:00:00") : /* @__PURE__ */ new Date();
9128
9211
  console.log(formatWeekdayHeader(viewDate, completed.length));
@@ -9135,18 +9218,18 @@ function registerTasksCommands(program3) {
9135
9218
  const viewDate = date ? /* @__PURE__ */ new Date(date + "T00:00:00") : /* @__PURE__ */ new Date();
9136
9219
  const dayName = viewDate.toLocaleDateString("en-US", { weekday: "long" });
9137
9220
  console.log(`
9138
- ${import_picocolors8.default.dim(`No tasks for ${dayName}. Enjoy your day!`)}`);
9139
- 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")}`);
9140
9223
  } else {
9141
9224
  console.log(`
9142
- ${import_picocolors8.default.dim("No backlog tasks.")}`);
9225
+ ${import_picocolors7.default.dim("No backlog tasks.")}`);
9143
9226
  }
9144
9227
  console.log("");
9145
9228
  return;
9146
9229
  }
9147
9230
  if (pending.length > 0) {
9148
9231
  console.log(`
9149
- ${import_picocolors8.default.bold("Pending")} ${import_picocolors8.default.dim(`(${pending.length})`)}
9232
+ ${import_picocolors7.default.bold("Pending")} ${import_picocolors7.default.dim(`(${pending.length})`)}
9150
9233
  `);
9151
9234
  for (const t2 of pending) {
9152
9235
  printTaskLine(t2);
@@ -9154,7 +9237,7 @@ function registerTasksCommands(program3) {
9154
9237
  }
9155
9238
  if (completed.length > 0) {
9156
9239
  console.log(`
9157
- ${import_picocolors8.default.dim(`Completed (${completed.length})`)}
9240
+ ${import_picocolors7.default.dim(`Completed (${completed.length})`)}
9158
9241
  `);
9159
9242
  for (const t2 of completed) {
9160
9243
  printTaskLine(t2);
@@ -9163,7 +9246,7 @@ function registerTasksCommands(program3) {
9163
9246
  if (!opts.backlog) {
9164
9247
  console.log(`
9165
9248
  ${formatProgressSummary(completed.length, items.length)}`);
9166
- 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")}`);
9167
9250
  }
9168
9251
  console.log("");
9169
9252
  }
@@ -9178,11 +9261,11 @@ Examples:
9178
9261
  $ numo tasks list --tag Work # Filter by tag
9179
9262
  $ numo tasks list --json | jq '.tasks[].text'`);
9180
9263
  tasks.command("get [id]").description("Get a task by ID").action(async function(id) {
9181
- const uid = requireUid();
9264
+ requireUid();
9182
9265
  const taskId = await promptForMissing({ value: id, message: "Task ID" });
9183
9266
  await runGet({
9184
9267
  global: this.optsWithGlobals(),
9185
- fn: () => getTask(uid, taskId),
9268
+ fn: () => getTask(taskId),
9186
9269
  spinnerMessage: "Fetching task...",
9187
9270
  onInteractive: printTaskDetail
9188
9271
  });
@@ -9190,171 +9273,165 @@ Examples:
9190
9273
  Examples:
9191
9274
  $ numo tasks get abc123
9192
9275
  $ numo tasks get abc123 --json | jq '.text'`);
9193
- 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) {
9194
9277
  const opts = this.optsWithGlobals();
9195
- const uid = requireUid();
9196
- const text = await promptForMissing({ value: opts.text, message: "Task text", placeholder: "What do you need to do?" });
9197
- const body = { text };
9198
- if (isInteractive() && !opts.json) {
9199
- if (!opts.due) {
9200
- const today2 = /* @__PURE__ */ new Date();
9201
- const tomorrow2 = new Date(today2);
9202
- tomorrow2.setDate(tomorrow2.getDate() + 1);
9203
- const fmt = (d) => d.toISOString().slice(0, 10);
9204
- const schedule = await promptSelect({
9205
- 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",
9206
9343
  options: [
9207
- { value: "today", label: `Today (${fmt(today2)})` },
9208
- { value: "tomorrow", label: `Tomorrow (${fmt(tomorrow2)})` },
9209
- { value: "pick", label: "Pick a date..." },
9210
- { value: "someday", label: "Someday (backlog)" },
9211
- { value: "daily", label: "Every day (routine)" },
9212
- { value: "weekly", label: "Every week (routine)" },
9213
- { 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" }
9214
9353
  ]
9215
9354
  });
9216
- if (schedule === "today") {
9217
- body.dueDate = fmt(today2);
9218
- } else if (schedule === "tomorrow") {
9219
- body.dueDate = fmt(tomorrow2);
9220
- } else if (schedule === "pick") {
9221
- const date = await promptText({ message: "Date", placeholder: fmt(tomorrow2), required: true });
9222
- body.dueDate = date;
9223
- } else if (["daily", "weekly", "monthly"].includes(schedule)) {
9224
- const repeat = {
9225
- type: schedule,
9226
- every: 1,
9227
- end: "never",
9228
- endDate: null,
9229
- endAfter: null,
9230
- monthDays: null,
9231
- weekDays: null
9232
- };
9233
- if (schedule === "weekly") {
9234
- const days = await promptMultiSelect({
9235
- message: "Days of week",
9236
- options: [
9237
- { value: "Mon", label: "Monday" },
9238
- { value: "Tue", label: "Tuesday" },
9239
- { value: "Wed", label: "Wednesday" },
9240
- { value: "Thu", label: "Thursday" },
9241
- { value: "Fri", label: "Friday" },
9242
- { value: "Sat", label: "Saturday" },
9243
- { value: "Sun", label: "Sunday" }
9244
- ],
9245
- required: true
9246
- });
9247
- repeat.weekDays = days;
9248
- } else if (schedule === "monthly") {
9249
- const daysInput = await promptText({ message: "Days of month", placeholder: "1,15", required: true });
9250
- repeat.monthDays = daysInput.split(",").map((s) => parseInt(s.trim()));
9251
- }
9252
- body.repeat = repeat;
9253
- body.dueDate = fmt(today2);
9254
- }
9255
- }
9256
- if (!opts.private && !opts.public) {
9257
- const visibility = await promptSelect({
9258
- message: "Visibility",
9355
+ if (tags.length > 0) body.tags = tags;
9356
+ const difficulty = await promptSelect({
9357
+ message: "Effort",
9259
9358
  options: [
9260
- { value: "public", label: "Public \u2014 visible to your squad" },
9261
- { 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" }
9262
9364
  ]
9263
9365
  });
9264
- if (visibility === "private") body.isPublic = false;
9265
- }
9266
- const addDetails = await promptConfirm({
9267
- message: "Add details? (tags, effort, time, note)",
9268
- initialValue: false
9269
- });
9270
- if (addDetails) {
9271
- if (!opts.tags) {
9272
- const tags = await promptMultiSelect({
9273
- message: "Tags",
9274
- options: [
9275
- { value: "House", label: "House" },
9276
- { value: "Work", label: "Work" },
9277
- { value: "Study", label: "Study" },
9278
- { value: "Hobby", label: "Hobby" },
9279
- { value: "Health", label: "Health" },
9280
- { value: "Relationship", label: "Relationship" },
9281
- { value: "Self-care", label: "Self-care" },
9282
- { value: "Relax", label: "Relax" },
9283
- { value: "Kids", label: "Kids" }
9284
- ]
9285
- });
9286
- if (tags.length > 0) body.tags = tags;
9287
- }
9288
- if (opts.difficulty === void 0) {
9289
- const difficulty = await promptSelect({
9290
- message: "Effort",
9291
- options: [
9292
- { value: "skip", label: "Skip" },
9293
- { value: "0", label: "S \u2014 Tiny" },
9294
- { value: "1", label: "M \u2014 Medium" },
9295
- { value: "2", label: "L \u2014 High" },
9296
- { value: "3", label: "XL \u2014 Huge" }
9297
- ]
9298
- });
9299
- if (difficulty !== "skip") body.difficulty = parseInt(difficulty);
9300
- }
9301
- const addTime = await promptConfirm({ message: "Add a specific time?", initialValue: false });
9302
- 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 })) {
9303
9368
  const time = await promptText({ message: "Time", placeholder: "09:30", required: true });
9304
9369
  body.dueDate = `${body.dueDate} ${time}`;
9305
9370
  }
9306
- if (!opts.note) {
9307
- const note = await promptText({ message: "Note (enter to skip)", placeholder: "Private note", required: false });
9308
- 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;
9309
9377
  }
9310
9378
  }
9311
- if (opts.public) body.isPublic = true;
9312
- if (opts.private) body.isPublic = false;
9313
- if (opts.tags && !body.tags) body.tags = opts.tags.split(",");
9314
- if (opts.note && !body.note) body.note = opts.note;
9315
9379
  } else {
9316
- 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) {
9317
9386
  const parsed = parseHumanDate(opts.due);
9318
9387
  if (!parsed) throw Errors.invalidInput(`Cannot parse date: "${opts.due}"`);
9319
9388
  body.dueDate = parsed;
9389
+ } else {
9390
+ body.dueDate = localDateOnly();
9320
9391
  }
9392
+ const repeat = buildRepeatConfig(opts);
9393
+ if (repeat) body.repeat = repeat;
9321
9394
  if (opts.tags) body.tags = opts.tags.split(",");
9322
9395
  if (opts.note) body.note = opts.note;
9323
- if (opts.priority) body.priority = parseFloat(opts.priority);
9324
9396
  if (opts.difficulty !== void 0) body.difficulty = parseInt(opts.difficulty);
9325
9397
  if (opts.duration) body.duration = parseInt(opts.duration);
9326
- if (opts.public) body.isPublic = true;
9327
- 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;
9328
9400
  }
9401
+ if (opts.clientTaskId) body.clientTaskId = opts.clientTaskId;
9402
+ body.listPosition = "top";
9403
+ normalizeDueDateInBody(body);
9329
9404
  await runCreate({
9330
9405
  global: opts,
9331
- fn: () => createTask(uid, body),
9406
+ fn: () => createTask(body),
9332
9407
  dataKey: "task",
9333
9408
  spinnerMessage: "Creating task...",
9334
9409
  onInteractive: (_task, payload) => {
9335
- const { task, karma } = payload;
9336
- const check = import_picocolors8.default.green(SYM.check);
9410
+ const { task, karma, idempotentReplay } = payload;
9411
+ const check = import_picocolors7.default.green(SYM.check);
9337
9412
  console.log(`
9338
- ${check} Created ${task.text}`);
9339
- if (karma) {
9340
- console.log(` ${formatKarmaGain(karma)}${" ".repeat(20)}${import_picocolors8.default.dim(task.id)}`);
9341
- }
9413
+ ${check} ${idempotentReplay ? "Exists" : "Created"} ${task.text} ${import_picocolors7.default.dim(task.id)}`);
9414
+ if (karma) console.log(` ${formatKarmaGain(karma)}`);
9342
9415
  console.log("");
9343
9416
  }
9344
9417
  });
9345
9418
  }).addHelpText("after", `
9346
9419
  Examples:
9347
- $ numo tasks create # Interactive wizard
9348
- $ numo tasks create --text "Buy groceries" # Quick create (today)
9349
- $ numo tasks create --text "Meeting" --due 2026-03-27
9350
- $ numo tasks create --text "Workout" --tags Health --difficulty 2
9351
- $ numo tasks create --text "Review PR" --due 2026-03-27 --tags Work --private`);
9352
- 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) {
9353
9429
  const opts = this.optsWithGlobals();
9354
- const uid = requireUid();
9430
+ requireUid();
9355
9431
  const taskId = await promptForMissing({ value: id, message: "Task ID" });
9432
+ const clearTime = opts.clearTime === true || opts.time === false;
9356
9433
  const body = {};
9357
- 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;
9358
9435
  if (!hasAnyFlag && isInteractive() && !opts.json) {
9359
9436
  const text = await promptText({ message: "Text (enter to skip)", required: false });
9360
9437
  if (text) body.text = text;
@@ -9364,8 +9441,6 @@ Examples:
9364
9441
  if (tags) body.tags = tags.split(",");
9365
9442
  const note = await promptText({ message: "Note (enter to skip)", required: false });
9366
9443
  if (note) body.note = note;
9367
- const priority = await promptText({ message: "Priority (enter to skip)", placeholder: "0.1\u20131.0", required: false });
9368
- if (priority) body.priority = parseFloat(priority);
9369
9444
  const difficulty = await promptText({ message: "Difficulty (enter to skip)", placeholder: "0\u20133", required: false });
9370
9445
  if (difficulty) body.difficulty = parseInt(difficulty);
9371
9446
  const duration = await promptText({ message: "Duration in minutes (enter to skip)", placeholder: "10", required: false });
@@ -9381,18 +9456,28 @@ Examples:
9381
9456
  if (opts.public) body.isPublic = true;
9382
9457
  if (opts.private) body.isPublic = false;
9383
9458
  if (opts.note) body.note = opts.note;
9384
- if (opts.priority) body.priority = parseFloat(opts.priority);
9385
9459
  if (opts.difficulty !== void 0) body.difficulty = parseInt(opts.difficulty);
9386
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
+ }
9387
9471
  }
9472
+ normalizeDueDateInBody(body);
9388
9473
  await runWrite({
9389
9474
  global: opts,
9390
- fn: () => updateTask(uid, taskId, body),
9475
+ fn: () => updateTask(taskId, body),
9391
9476
  dataKey: "task",
9392
9477
  spinnerMessage: "Updating task...",
9393
9478
  onInteractive: (payload) => {
9394
9479
  console.log(`
9395
- ${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)}
9396
9481
  `);
9397
9482
  }
9398
9483
  });
@@ -9400,28 +9485,35 @@ Examples:
9400
9485
  Examples:
9401
9486
  $ numo tasks update abc123 --text "Updated text"
9402
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
9403
9490
  $ numo tasks update abc123 --tags Work,Health
9404
- $ 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`);
9405
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) {
9406
9497
  const opts = this.optsWithGlobals();
9407
- const uid = requireUid();
9498
+ requireUid();
9408
9499
  if (opts.stdin) {
9409
9500
  const ids = readStdinLines();
9410
9501
  for (const lineId of ids) {
9411
9502
  try {
9412
- const result = await deleteTask(uid, lineId);
9503
+ const result = await deleteTask(lineId);
9413
9504
  printNdjsonLine({ id: lineId, ...result });
9414
9505
  } catch (err) {
9506
+ process.exitCode = ExitCode.GENERAL;
9415
9507
  printNdjsonLine({ id: lineId, error: err.message });
9416
9508
  }
9417
9509
  }
9418
9510
  return;
9419
9511
  }
9420
- const taskId = await pickTask(uid, id, "delete");
9512
+ const taskId = await pickTask(id, "delete");
9421
9513
  if (isInteractive() && !opts.yes && !opts.json) {
9422
9514
  let taskText = taskId;
9423
9515
  try {
9424
- const task = await getTask(uid, taskId);
9516
+ const task = await getTask(taskId);
9425
9517
  taskText = task.text ?? taskId;
9426
9518
  } catch {
9427
9519
  }
@@ -9430,19 +9522,20 @@ Examples:
9430
9522
  initialValue: false
9431
9523
  });
9432
9524
  if (!confirmed) {
9433
- console.log(import_picocolors8.default.dim(" Cancelled."));
9525
+ console.log(import_picocolors7.default.dim(" Cancelled."));
9434
9526
  return;
9435
9527
  }
9436
9528
  }
9437
9529
  await runWrite({
9438
9530
  global: opts,
9439
- fn: () => deleteTask(uid, taskId),
9531
+ fn: () => deleteTask(taskId),
9440
9532
  spinnerMessage: "Deleting task...",
9441
9533
  onInteractive: (data) => {
9442
- const cross = import_picocolors8.default.red(SYM.cross);
9534
+ const cross = import_picocolors7.default.red(SYM.cross);
9443
9535
  console.log(`
9444
9536
  ${cross} Deleted ${data.taskText || taskId}`);
9445
- 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);
9446
9539
  console.log("");
9447
9540
  }
9448
9541
  });
@@ -9453,31 +9546,41 @@ Examples:
9453
9546
  $ numo tasks delete abc123 --json`);
9454
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) {
9455
9548
  const opts = this.optsWithGlobals();
9456
- const uid = requireUid();
9549
+ requireUid();
9550
+ if (opts.date && !isCompletableDate(opts.date)) {
9551
+ throw Errors.invalidInput("completion date must be today or yesterday");
9552
+ }
9457
9553
  if (opts.stdin) {
9458
9554
  const ids = readStdinLines();
9459
9555
  for (const lineId of ids) {
9460
9556
  try {
9461
- const result = await completeTask(uid, lineId, opts.date);
9557
+ const result = await completeTask(lineId, opts.date);
9462
9558
  printNdjsonLine({ id: lineId, ...result });
9463
9559
  } catch (err) {
9560
+ process.exitCode = ExitCode.GENERAL;
9464
9561
  printNdjsonLine({ id: lineId, error: err.message });
9465
9562
  }
9466
9563
  }
9467
9564
  return;
9468
9565
  }
9469
- const taskId = await pickTask(uid, id, "complete");
9566
+ const taskId = await pickTask(id, "complete");
9470
9567
  await runWrite({
9471
9568
  global: opts,
9472
- fn: () => completeTask(uid, taskId, opts.date),
9569
+ fn: () => completeTask(taskId, opts.date),
9473
9570
  spinnerMessage: "Completing task...",
9474
9571
  onInteractive: (data) => {
9475
- const check = import_picocolors8.default.green(SYM.check);
9476
- 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(`
9477
9578
  ${check} Done! ${data.taskText ?? taskId}`);
9478
- if (data.karma) {
9479
- console.log(` ${formatKarmaGain(data.karma, data.checksInRow)}`);
9579
+ if (data.karma) {
9580
+ console.log(` ${formatKarmaGain(data.karma, data.checksInRow)}`);
9581
+ }
9480
9582
  }
9583
+ if (data.partial) printPartialNotice(data.failed);
9481
9584
  console.log("");
9482
9585
  }
9483
9586
  });
@@ -9487,14 +9590,15 @@ Examples:
9487
9590
  $ numo tasks complete abc123 --date "2026-03-26 14:30"`);
9488
9591
  tasks.command("uncomplete [id]").description("Mark task as incomplete").option("--stdin", "Read task IDs from stdin (one per line)").action(async function(id) {
9489
9592
  const opts = this.optsWithGlobals();
9490
- const uid = requireUid();
9593
+ requireUid();
9491
9594
  if (opts.stdin) {
9492
9595
  const ids = readStdinLines();
9493
9596
  for (const lineId of ids) {
9494
9597
  try {
9495
- const result = await uncompleteTask(uid, lineId);
9598
+ const result = await uncompleteTask(lineId);
9496
9599
  printNdjsonLine({ id: lineId, ...result });
9497
9600
  } catch (err) {
9601
+ process.exitCode = ExitCode.GENERAL;
9498
9602
  printNdjsonLine({ id: lineId, error: err.message });
9499
9603
  }
9500
9604
  }
@@ -9503,14 +9607,15 @@ Examples:
9503
9607
  const taskId = await promptForMissing({ value: id, message: "Task ID" });
9504
9608
  await runWrite({
9505
9609
  global: opts,
9506
- fn: () => uncompleteTask(uid, taskId),
9610
+ fn: () => uncompleteTask(taskId),
9507
9611
  dataKey: "task",
9508
9612
  spinnerMessage: "Uncompleting task...",
9509
9613
  onInteractive: (data) => {
9510
9614
  const arrow = SYM.undo;
9511
9615
  console.log(`
9512
- ${import_picocolors8.default.yellow(arrow)} Reverted ${data.task.text ?? taskId}`);
9513
- 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);
9514
9619
  console.log("");
9515
9620
  }
9516
9621
  });
@@ -9520,16 +9625,16 @@ Examples:
9520
9625
  }
9521
9626
 
9522
9627
  // src/cli/commands/posts.ts
9523
- var import_picocolors10 = __toESM(require_picocolors(), 1);
9628
+ var import_picocolors9 = __toESM(require_picocolors(), 1);
9524
9629
 
9525
9630
  // src/cli/lib/pagination.ts
9526
- var import_picocolors9 = __toESM(require_picocolors(), 1);
9631
+ var import_picocolors8 = __toESM(require_picocolors(), 1);
9527
9632
  function printPaginationHint(opts) {
9528
9633
  if (!opts.nextCursor) return;
9529
9634
  const parts = [opts.command, `--cursor ${opts.nextCursor}`];
9530
9635
  if (opts.limit) parts.push(`--limit ${opts.limit}`);
9531
9636
  console.log(`
9532
- ${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(" ")}`);
9533
9638
  }
9534
9639
 
9535
9640
  // src/cli/services/posts.ts
@@ -9543,15 +9648,6 @@ async function listPosts(opts) {
9543
9648
  async function getPost(id) {
9544
9649
  return api.get(`/api/posts/${encodeURIComponent(id)}`);
9545
9650
  }
9546
- async function createPost(uid, body) {
9547
- return api.post("/api/posts", body);
9548
- }
9549
- async function updatePost(uid, id, body) {
9550
- return api.patch(`/api/posts/${encodeURIComponent(id)}`, body);
9551
- }
9552
- async function deletePost(uid, id) {
9553
- await api.del(`/api/posts/${encodeURIComponent(id)}`);
9554
- }
9555
9651
 
9556
9652
  // src/cli/services/comments.ts
9557
9653
  init_api_client();
@@ -9561,12 +9657,6 @@ async function listComments(postId, opts) {
9561
9657
  limit: opts.limit?.toString()
9562
9658
  });
9563
9659
  }
9564
- async function createComment(uid, postId, text) {
9565
- return api.post(`/api/posts/${encodeURIComponent(postId)}/comments`, { text });
9566
- }
9567
- async function deleteComment(uid, postId, commentId) {
9568
- await api.del(`/api/posts/${encodeURIComponent(postId)}/comments/${encodeURIComponent(commentId)}`);
9569
- }
9570
9660
 
9571
9661
  // src/cli/services/replies.ts
9572
9662
  init_api_client();
@@ -9576,68 +9666,36 @@ async function listReplies(postId, commentId, opts) {
9576
9666
  limit: opts.limit?.toString()
9577
9667
  });
9578
9668
  }
9579
- async function createReply(uid, postId, commentId, text) {
9580
- return api.post(`/api/posts/${encodeURIComponent(postId)}/comments/${encodeURIComponent(commentId)}/replies`, { text });
9581
- }
9582
- async function deleteReply(uid, postId, commentId, replyId) {
9583
- await api.del(`/api/posts/${encodeURIComponent(postId)}/comments/${encodeURIComponent(commentId)}/replies/${encodeURIComponent(replyId)}`);
9584
- }
9585
-
9586
- // src/cli/types/api.ts
9587
- var POST_TAGS = ["general", "hack", "story", "meme", "other", "question", "hack-tip", "activity"];
9588
9669
 
9589
9670
  // src/cli/commands/posts.ts
9590
9671
  init_prompts();
9591
- init_tty();
9592
- async function pickComment(postId, commentId) {
9593
- if (commentId) return commentId;
9594
- if (!isInteractive()) throw new Error("Missing required argument: commentId. Use flags in non-interactive mode.");
9595
- const { comments } = await listComments(postId, { limit: 50 });
9596
- if (comments.length === 0) throw new Error("No comments on this post.");
9597
- return promptSelect({
9598
- message: "Select comment",
9599
- options: comments.map((c) => ({
9600
- value: c.id,
9601
- label: `${truncate(c.text ?? "", 60)} ${import_picocolors10.default.dim(c.authorName ?? c.userId ?? "")}`
9602
- }))
9603
- });
9604
- }
9605
- async function pickReply(postId, commentId, replyId) {
9606
- if (replyId) return replyId;
9607
- if (!isInteractive()) throw new Error("Missing required argument: replyId. Use flags in non-interactive mode.");
9608
- const { replies } = await listReplies(postId, commentId, { limit: 50 });
9609
- if (replies.length === 0) throw new Error("No replies on this comment.");
9610
- return promptSelect({
9611
- message: "Select reply",
9612
- options: replies.map((r) => ({
9613
- value: r.id,
9614
- label: `${truncate(r.text ?? "", 60)} ${import_picocolors10.default.dim(r.userId ?? "")}`
9615
- }))
9616
- });
9617
- }
9618
9672
  function printPostLine(p) {
9619
- const tag = import_picocolors10.default.cyan(p.tag ?? "");
9673
+ const tag = import_picocolors9.default.cyan(p.tag ?? "");
9620
9674
  const title = truncate(p.title, 55);
9621
- 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`) : "";
9622
9678
  const time = formatRelativeDate(p.createdAt);
9623
- const id = import_picocolors10.default.dim(p.id);
9624
- 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}`));
9625
9682
  if (comments) parts.push(comments);
9626
- parts.push(import_picocolors10.default.dim(time));
9683
+ if (likes) parts.push(likes);
9684
+ parts.push(import_picocolors9.default.dim(time));
9627
9685
  parts.push(id);
9628
9686
  console.log(" " + parts.join(" "));
9629
9687
  }
9630
9688
  function printPostDetail(p) {
9631
9689
  console.log("");
9632
- console.log(` ${import_picocolors10.default.bold(p.title)}`);
9690
+ console.log(` ${import_picocolors9.default.bold(p.title)}`);
9633
9691
  if (p.body) {
9634
- console.log(` ${import_picocolors10.default.dim(SYM.dash.repeat(40))}`);
9692
+ console.log(` ${import_picocolors9.default.dim(SYM.dash.repeat(40))}`);
9635
9693
  console.log(` ${p.body}`);
9636
9694
  }
9637
- console.log(` ${import_picocolors10.default.dim(SYM.dash.repeat(40))}`);
9695
+ console.log(` ${import_picocolors9.default.dim(SYM.dash.repeat(40))}`);
9638
9696
  printRecord([
9639
- ["ID", import_picocolors10.default.dim(p.id)],
9640
- ["Tag", import_picocolors10.default.cyan(p.tag ?? "")],
9697
+ ["ID", import_picocolors9.default.dim(p.id)],
9698
+ ["Tag", import_picocolors9.default.cyan(p.tag ?? "")],
9641
9699
  ["Author", p.authorName ?? p.authorId],
9642
9700
  ["Comments", p.commentsCount != null ? String(p.commentsCount) : null],
9643
9701
  ["Likes", p.likesCount != null ? String(p.likesCount) : null],
@@ -9646,9 +9704,9 @@ function printPostDetail(p) {
9646
9704
  console.log("");
9647
9705
  }
9648
9706
  function printCommentLine(c) {
9649
- const author = import_picocolors10.default.bold(c.authorName ?? c.userId ?? "");
9650
- const time = import_picocolors10.default.dim(formatRelativeDate(c.createdAt));
9651
- 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`) : "";
9652
9710
  const text = c.text ?? "";
9653
9711
  console.log(` ${author} ${time}${replies}`);
9654
9712
  console.log(` ${text}`);
@@ -9657,86 +9715,77 @@ function printCommentLine(c) {
9657
9715
  function printReplyLine(r) {
9658
9716
  const text = truncate(r.text ?? "", 60);
9659
9717
  const time = formatRelativeDate(r.createdAt);
9660
- const id = import_picocolors10.default.dim(r.id);
9661
- 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}`);
9662
9720
  }
9663
9721
  function registerPostsCommands(program3) {
9664
- const posts = program3.command("posts").description("Manage posts and comments");
9722
+ const posts = program3.command("posts").description("Browse community posts and comments");
9665
9723
  posts.command("list").description("List posts").option("--cursor <cursor>").option("--limit <n>", "Max results (<=50)", "20").action(async function() {
9666
9724
  const opts = this.optsWithGlobals();
9667
- const limit = parseInt(opts.limit);
9668
9725
  await runList({
9669
9726
  global: opts,
9670
- fn: () => listPosts({ cursor: opts.cursor, limit }),
9727
+ fn: () => listPosts({ cursor: opts.cursor, limit: opts.limit ? parseInt(opts.limit) : void 0 }),
9671
9728
  dataKey: "posts",
9672
- columns: ["id", "title", "tag", "authorId", "createdAt"],
9729
+ columns: ["id", "tag", "title", "authorName", "commentsCount", "likesCount", "createdAt"],
9673
9730
  spinnerMessage: "Fetching posts...",
9674
9731
  onInteractive: (payload) => {
9675
9732
  const items = payload.posts;
9676
9733
  if (items.length === 0) {
9677
- console.log(import_picocolors10.default.dim(" No posts found."));
9734
+ console.log(import_picocolors9.default.dim(" No posts found."));
9678
9735
  return;
9679
9736
  }
9680
- console.log(`
9681
- ${import_picocolors10.default.bold("Posts")} ${import_picocolors10.default.dim(`(${items.length})`)}
9682
- `);
9683
- for (const p of items) {
9684
- printPostLine(p);
9685
- }
9737
+ console.log("");
9738
+ for (const p of items) printPostLine(p);
9739
+ console.log("");
9686
9740
  printPaginationHint({
9687
9741
  nextCursor: payload.nextCursor,
9688
9742
  command: "posts list",
9689
9743
  limit: opts.limit
9690
9744
  });
9691
- console.log("");
9692
9745
  }
9693
9746
  });
9694
- }).addHelpText("after", `
9695
- Examples:
9696
- $ numo posts list
9697
- $ numo posts list --limit 10
9698
- $ numo posts list --json | jq '.posts[].title'`);
9699
- 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();
9700
9750
  const postId = await promptForMissing({ value: id, message: "Post ID" });
9701
9751
  await runGet({
9702
- global: this.optsWithGlobals(),
9752
+ global: opts,
9703
9753
  fn: () => getPost(postId),
9704
9754
  spinnerMessage: "Fetching post...",
9705
- onInteractive: printPostDetail
9755
+ onInteractive: (post) => printPostDetail(post)
9706
9756
  });
9707
- }).addHelpText("after", `
9708
- Examples:
9709
- $ numo posts get abc123
9710
- $ numo posts get abc123 --json`);
9711
- 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) {
9712
9759
  const opts = this.optsWithGlobals();
9713
9760
  const resolvedPostId = await promptForMissing({ value: postId, message: "Post ID" });
9714
9761
  await runList({
9715
9762
  global: opts,
9716
- fn: () => listComments(resolvedPostId, { cursor: opts.cursor }),
9763
+ fn: () => listComments(resolvedPostId, { cursor: opts.cursor, limit: opts.limit ? parseInt(opts.limit) : void 0 }),
9717
9764
  dataKey: "comments",
9718
- columns: ["id", "userId", "text", "createdAt"],
9765
+ columns: ["id", "userId", "authorName", "text", "repliesCount", "createdAt"],
9719
9766
  spinnerMessage: "Fetching comments...",
9720
9767
  onInteractive: (payload) => {
9721
9768
  const items = payload.comments;
9722
9769
  if (items.length === 0) {
9723
- console.log(import_picocolors10.default.dim(" No comments yet."));
9770
+ console.log(import_picocolors9.default.dim(" No comments yet."));
9724
9771
  return;
9725
9772
  }
9726
9773
  console.log(`
9727
- ${import_picocolors10.default.bold("Comments")} ${import_picocolors10.default.dim(`(${items.length})`)}
9774
+ ${import_picocolors9.default.bold("Comments")} ${import_picocolors9.default.dim(`(${items.length})`)}
9728
9775
  `);
9729
- for (const c of items) {
9730
- printCommentLine(c);
9731
- }
9732
- 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
+ });
9733
9782
  }
9734
9783
  });
9735
9784
  });
9736
- 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) {
9737
9786
  const opts = this.optsWithGlobals();
9738
9787
  const resolvedPostId = await promptForMissing({ value: postId, message: "Post ID" });
9739
- const resolvedCommentId = await pickComment(resolvedPostId, commentId);
9788
+ const resolvedCommentId = await promptForMissing({ value: commentId, message: "Comment ID" });
9740
9789
  await runList({
9741
9790
  global: opts,
9742
9791
  fn: () => listReplies(resolvedPostId, resolvedCommentId, { cursor: opts.cursor }),
@@ -9746,11 +9795,11 @@ Examples:
9746
9795
  onInteractive: (payload) => {
9747
9796
  const items = payload.replies;
9748
9797
  if (items.length === 0) {
9749
- console.log(import_picocolors10.default.dim(" No replies yet."));
9798
+ console.log(import_picocolors9.default.dim(" No replies yet."));
9750
9799
  return;
9751
9800
  }
9752
9801
  console.log(`
9753
- ${import_picocolors10.default.bold("Replies")} ${import_picocolors10.default.dim(`(${items.length})`)}
9802
+ ${import_picocolors9.default.bold("Replies")} ${import_picocolors9.default.dim(`(${items.length})`)}
9754
9803
  `);
9755
9804
  for (const r of items) {
9756
9805
  printReplyLine(r);
@@ -9759,129 +9808,6 @@ Examples:
9759
9808
  }
9760
9809
  });
9761
9810
  });
9762
- 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() {
9763
- const opts = this.optsWithGlobals();
9764
- const uid = requireUid();
9765
- const title = await promptForMissing({ value: opts.title, message: "Title", placeholder: "Post title" });
9766
- const postBody = await promptForMissing({ value: opts.body, message: "Body", placeholder: "Post body" });
9767
- let tag = opts.tag;
9768
- if (!tag) {
9769
- tag = await promptSelect({
9770
- message: "Tag",
9771
- options: POST_TAGS.map((t2) => ({ value: t2, label: t2 }))
9772
- });
9773
- }
9774
- const body = { title, body: postBody, tag };
9775
- await runCreate({
9776
- global: opts,
9777
- fn: () => createPost(uid, body),
9778
- dataKey: "post",
9779
- spinnerMessage: "Creating post...",
9780
- onInteractive: (post, payload) => {
9781
- console.log(`
9782
- ${import_picocolors10.default.green("Posted!")} ${payload.post.title} ${import_picocolors10.default.dim(payload.post.id)}
9783
- `);
9784
- }
9785
- });
9786
- });
9787
- posts.command("update [id]").description("Update a post").option("--title <title>").option("--body <body>").option("--tag <tag>").action(async function(id) {
9788
- const opts = this.optsWithGlobals();
9789
- const uid = requireUid();
9790
- const postId = await promptForMissing({ value: id, message: "Post ID" });
9791
- const body = {};
9792
- const hasAnyFlag = opts.title || opts.body || opts.tag;
9793
- if (!hasAnyFlag && isInteractive() && !opts.json) {
9794
- const title = await promptText({ message: "Title (enter to skip)", required: false });
9795
- if (title) body.title = title;
9796
- const postBody = await promptText({ message: "Body (enter to skip)", required: false });
9797
- if (postBody) body.body = postBody;
9798
- const changeTag = await promptText({ message: "Tag (enter to skip)", placeholder: POST_TAGS.join("|"), required: false });
9799
- if (changeTag) body.tag = changeTag;
9800
- } else {
9801
- if (opts.title) body.title = opts.title;
9802
- if (opts.body) body.body = opts.body;
9803
- if (opts.tag) body.tag = opts.tag;
9804
- }
9805
- await runWrite({
9806
- global: opts,
9807
- fn: () => updatePost(uid, postId, body),
9808
- dataKey: "post",
9809
- spinnerMessage: "Updating post...",
9810
- onInteractive: (payload) => {
9811
- console.log(`
9812
- ${import_picocolors10.default.green("Updated!")} ${payload.post.title} ${import_picocolors10.default.dim(payload.post.id)}
9813
- `);
9814
- }
9815
- });
9816
- });
9817
- posts.command("delete [id]").description("Delete a post").action(async function(id) {
9818
- const uid = requireUid();
9819
- const postId = await promptForMissing({ value: id, message: "Post ID" });
9820
- await runDelete({
9821
- global: this.optsWithGlobals(),
9822
- fn: () => deletePost(uid, postId),
9823
- successMessage: ` ${import_picocolors10.default.green("Deleted!")} Post ${import_picocolors10.default.dim(postId)}`,
9824
- spinnerMessage: "Deleting post..."
9825
- });
9826
- });
9827
- posts.command("comment [postId]").description("Add a comment to a post").option("--text <text>").action(async function(postId) {
9828
- const opts = this.optsWithGlobals();
9829
- const uid = requireUid();
9830
- const resolvedPostId = await promptForMissing({ value: postId, message: "Post ID" });
9831
- const text = await promptForMissing({ value: opts.text, message: "Comment text", placeholder: "Your comment" });
9832
- await runCreate({
9833
- global: opts,
9834
- fn: () => createComment(uid, resolvedPostId, text),
9835
- dataKey: "comment",
9836
- spinnerMessage: "Adding comment...",
9837
- onInteractive: (comment, payload) => {
9838
- console.log(`
9839
- ${import_picocolors10.default.green("Commented!")} ${truncate(payload.comment.text ?? "", 50)} ${import_picocolors10.default.dim(payload.comment.id)}
9840
- `);
9841
- }
9842
- });
9843
- });
9844
- posts.command("comment-delete [postId] [commentId]").description("Delete a comment").action(async function(postId, commentId) {
9845
- const uid = requireUid();
9846
- const resolvedPostId = await promptForMissing({ value: postId, message: "Post ID" });
9847
- const resolvedCommentId = await pickComment(resolvedPostId, commentId);
9848
- await runDelete({
9849
- global: this.optsWithGlobals(),
9850
- fn: () => deleteComment(uid, resolvedPostId, resolvedCommentId),
9851
- successMessage: ` ${import_picocolors10.default.green("Deleted!")} Comment ${import_picocolors10.default.dim(resolvedCommentId)}`,
9852
- spinnerMessage: "Deleting comment..."
9853
- });
9854
- });
9855
- posts.command("reply [postId] [commentId]").description("Add a reply to a comment").option("--text <text>").action(async function(postId, commentId) {
9856
- const opts = this.optsWithGlobals();
9857
- const uid = requireUid();
9858
- const resolvedPostId = await promptForMissing({ value: postId, message: "Post ID" });
9859
- const resolvedCommentId = await pickComment(resolvedPostId, commentId);
9860
- const text = await promptForMissing({ value: opts.text, message: "Reply text", placeholder: "Your reply" });
9861
- await runCreate({
9862
- global: opts,
9863
- fn: () => createReply(uid, resolvedPostId, resolvedCommentId, text),
9864
- dataKey: "reply",
9865
- spinnerMessage: "Adding reply...",
9866
- onInteractive: (reply, payload) => {
9867
- console.log(`
9868
- ${import_picocolors10.default.green("Replied!")} ${truncate(payload.reply.text ?? "", 50)} ${import_picocolors10.default.dim(payload.reply.id)}
9869
- `);
9870
- }
9871
- });
9872
- });
9873
- posts.command("reply-delete [postId] [commentId] [replyId]").description("Delete a reply").action(async function(postId, commentId, replyId) {
9874
- const uid = requireUid();
9875
- const resolvedPostId = await promptForMissing({ value: postId, message: "Post ID" });
9876
- const resolvedCommentId = await pickComment(resolvedPostId, commentId);
9877
- const resolvedReplyId = await pickReply(resolvedPostId, resolvedCommentId, replyId);
9878
- await runDelete({
9879
- global: this.optsWithGlobals(),
9880
- fn: () => deleteReply(uid, resolvedPostId, resolvedCommentId, resolvedReplyId),
9881
- successMessage: ` ${import_picocolors10.default.green("Deleted!")} Reply ${import_picocolors10.default.dim(resolvedReplyId)}`,
9882
- spinnerMessage: "Deleting reply..."
9883
- });
9884
- });
9885
9811
  }
9886
9812
 
9887
9813
  // src/cli/services/profile.ts
@@ -9911,10 +9837,47 @@ function registerProfileCommands(program3) {
9911
9837
  }
9912
9838
 
9913
9839
  // src/cli/commands/doctor.ts
9914
- var import_picocolors11 = __toESM(require_picocolors(), 1);
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);
9915
9844
  init_credentials();
9916
9845
  init_api_client();
9917
- init_tty();
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
+ }
9918
9881
  async function runChecks() {
9919
9882
  const checks = [];
9920
9883
  const nodeVersion = process.version;
@@ -9924,55 +9887,107 @@ async function runChecks() {
9924
9887
  status: major >= 18 ? "ok" : "fail",
9925
9888
  message: major >= 18 ? `Node ${nodeVersion}` : `Node ${nodeVersion} \u2014 requires >= 18`
9926
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);
9927
9901
  checks.push({
9928
9902
  name: "api_url",
9929
- status: process.env.NUMO_API_URL ? "ok" : "warn",
9930
- message: process.env.NUMO_API_URL ? `API URL: ${API_BASE}` : `NUMO_API_URL not set (using default: ${API_BASE})`
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})`
9931
9905
  });
9906
+ checks.push(await checkDns(apiUrl.hostname));
9907
+ if (apiUrl.protocol === "https:") {
9908
+ checks.push(await checkTls(apiUrl.hostname));
9909
+ }
9932
9910
  const creds = loadCredentials();
9933
9911
  checks.push({
9934
9912
  name: "credentials",
9935
9913
  status: creds ? "ok" : "fail",
9936
- 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"
9937
9916
  });
9938
9917
  if (creds) {
9939
9918
  try {
9940
9919
  await getIdToken();
9941
9920
  checks.push({ name: "token", status: "ok", message: "Token valid / refreshed" });
9942
9921
  } catch (err) {
9943
- 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
+ });
9944
9928
  }
9945
9929
  } else {
9946
9930
  checks.push({ name: "token", status: "fail", message: "Skipped (no credentials)" });
9947
9931
  }
9948
9932
  try {
9949
9933
  const resp = await fetch(`${API_BASE}/api/health`, { signal: AbortSignal.timeout(5e3) });
9950
- checks.push({ name: "api_reachable", status: "ok", message: `API server reachable (HTTP ${resp.status})` });
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
+ });
9951
9940
  } catch (err) {
9952
- 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
+ }
9953
9969
  }
9954
9970
  return checks;
9955
9971
  }
9956
9972
  function registerDoctorCommand(program3) {
9957
9973
  program3.command("doctor").description("Check CLI health and connectivity").action(async function() {
9958
9974
  const opts = this.optsWithGlobals();
9959
- const asJson = !!(opts.json || opts.quiet || !isInteractive());
9975
+ const asJson = isQuietMode(opts);
9960
9976
  const checks = await runChecks();
9961
9977
  const ok = checks.every((c) => c.status !== "fail");
9962
9978
  if (asJson) {
9963
- printJson({ ok, checks });
9979
+ printJson({ ok, exitCode: ok ? 0 : 1, checks });
9964
9980
  } else {
9965
9981
  console.log("");
9966
9982
  for (const check of checks) {
9967
- 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);
9968
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
+ }
9969
9988
  }
9970
9989
  console.log("");
9971
- if (ok) {
9972
- console.log(` ${import_picocolors11.default.green("All checks passed.")}`);
9973
- } else {
9974
- console.log(` ${import_picocolors11.default.red("Some checks failed.")}`);
9975
- }
9990
+ console.log(` ${ok ? import_picocolors10.default.green("All checks passed.") : import_picocolors10.default.red("Some checks failed.")}`);
9976
9991
  console.log("");
9977
9992
  }
9978
9993
  if (!ok) process.exit(1);
@@ -9985,11 +10000,15 @@ init_dirs();
9985
10000
  // src/cli/lib/update-check.ts
9986
10001
  var fs4 = __toESM(require("fs"), 1);
9987
10002
  var path2 = __toESM(require("path"), 1);
9988
- var import_picocolors12 = __toESM(require_picocolors(), 1);
10003
+ var import_picocolors11 = __toESM(require_picocolors(), 1);
9989
10004
  init_dirs();
9990
10005
  init_tty();
9991
10006
  var CHECK_INTERVAL = 24 * 60 * 60 * 1e3;
9992
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`;
9993
10012
  function getStatePath() {
9994
10013
  return path2.join(getConfigDir(), "update-check.json");
9995
10014
  }
@@ -10016,6 +10035,12 @@ function semverGt(a, b) {
10016
10035
  }
10017
10036
  return false;
10018
10037
  }
10038
+ function isBinaryInstall() {
10039
+ return !!process.versions.bun;
10040
+ }
10041
+ function upgradeCommand(isBinary = isBinaryInstall()) {
10042
+ return isBinary ? UPGRADE_BINARY : UPGRADE_NPM;
10043
+ }
10019
10044
  function checkForUpdate(currentVersion) {
10020
10045
  if (!isInteractive()) return;
10021
10046
  if (process.env.CI || process.env.NUMO_NO_UPDATE_CHECK) return;
@@ -10024,8 +10049,8 @@ function checkForUpdate(currentVersion) {
10024
10049
  if (state.latestVersion && semverGt(state.latestVersion, currentVersion)) {
10025
10050
  process.stderr.write(
10026
10051
  `
10027
- ${import_picocolors12.default.yellow("Update available")} ${import_picocolors12.default.dim(currentVersion)} ${import_picocolors12.default.dim("\u2192")} ${import_picocolors12.default.green(state.latestVersion)}
10028
- 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
10029
10054
 
10030
10055
  `
10031
10056
  );
@@ -10049,10 +10074,21 @@ function fetchLatestVersion(state) {
10049
10074
  }
10050
10075
  }
10051
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
+
10052
10089
  // src/cli/cli.ts
10053
- init_tty();
10054
10090
  init_errors();
10055
- var CLI_VERSION = true ? "1.5.0" : "0.0.0-dev";
10091
+ var CLI_VERSION = true ? "1.6.0" : "0.0.0-dev";
10056
10092
  var program2 = new Command();
10057
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) => {
10058
10094
  const opts = thisCommand.optsWithGlobals();
@@ -10060,130 +10096,129 @@ program2.name("numo").description("CLI for Numo \u2014 programmatic access for h
10060
10096
  thisCommand.setOptionValue("json", true);
10061
10097
  }
10062
10098
  }).addHelpText("after", `
10063
- ${import_picocolors13.default.bold("Output modes:")}
10099
+ ${import_picocolors12.default.bold("Output modes:")}
10064
10100
  Interactive (TTY) Tables, colors, spinners
10065
10101
  Piped / --json Clean JSON for scripting and agents
10066
10102
 
10067
- ${import_picocolors13.default.bold("Examples:")}
10068
- ${import_picocolors13.default.dim("$")} numo login
10069
- ${import_picocolors13.default.dim("$")} numo tasks list --date 2025-01-15
10070
- ${import_picocolors13.default.dim("$")} numo tasks create --text "Buy groceries" --due 2025-01-16`);
10071
- program2.command("login").description("Login with your Numo account").option("--phone", "Login with phone number (SMS OTP)").action(async (opts) => {
10072
- await login(opts);
10073
- }).addHelpText("after", `
10074
- Examples:
10075
- $ numo login # Interactive (email/password)
10076
- $ numo login --phone # SMS OTP flow`);
10077
- 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) => {
10078
- 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);
10079
10116
  }).addHelpText("after", `
10080
10117
  Examples:
10081
- $ numo register # Interactive
10082
- $ 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)`);
10083
10121
  program2.command("logout").description("Clear stored credentials").action(() => {
10084
10122
  clearCredentials();
10085
- console.log(import_picocolors13.default.green("Logged out."));
10086
- });
10087
- 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() {
10088
10137
  const opts = this.optsWithGlobals();
10089
- const asJson = !!(opts.json || opts.quiet || !isInteractive());
10138
+ const asJson = isQuietMode(opts);
10139
+ const envToken = process.env.NUMO_TOKEN;
10090
10140
  const creds = loadCredentials();
10091
- 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
+ }
10092
10159
  if (asJson) {
10093
- 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 });
10094
10161
  } else {
10095
- console.error(`${import_picocolors13.default.red("Not logged in")}
10096
-
10097
- $ numo login
10098
- `);
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)")}`);
10099
10166
  }
10100
- process.exit(ExitCode.NO_PERM);
10167
+ if (!tokenValid2) process.exitCode = ExitCode.NO_PERM;
10168
+ return;
10169
+ }
10170
+ if (!creds) {
10171
+ outputError(Errors.authRequired(), asJson);
10101
10172
  return;
10102
10173
  }
10103
10174
  const tokenValid = !!(creds.idToken && creds.idTokenExpiry && Date.now() < creds.idTokenExpiry);
10104
10175
  const expiresIn = creds.idTokenExpiry ? Math.max(0, Math.floor((creds.idTokenExpiry - Date.now()) / 1e3)) : 0;
10105
- const source = process.env.NUMO_TOKEN ? "NUMO_TOKEN" : "credentials_file";
10176
+ const source = "credentials_file";
10106
10177
  if (asJson) {
10107
- printJson({ email: creds.email, uid: creds.uid, tokenValid, expiresIn, source });
10178
+ printJson({ email: creds.email, uid: creds.uid, tokenValid, expiresIn, source, autoRefresh: true });
10108
10179
  } else {
10109
- console.log(` ${import_picocolors13.default.bold("Email")} ${creds.email}`);
10110
- console.log(` ${import_picocolors13.default.bold("UID")} ${creds.uid}`);
10111
- 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)")}`);
10112
- 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()}`);
10113
10184
  }
10114
10185
  });
10115
10186
  registerTasksCommands(program2);
10116
- 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) {
10117
- const opts = this.optsWithGlobals();
10118
- const uid = requireUid();
10119
- const text = textParts?.join(" ");
10120
- if (!text) {
10121
- console.error('Usage: numo add "task text"');
10122
- process.exit(ExitCode.USAGE);
10123
- }
10124
- const body = {
10125
- text,
10126
- dueDate: opts.due ? parseHumanDate(opts.due) ?? opts.due : (/* @__PURE__ */ new Date()).toISOString().slice(0, 10)
10127
- };
10128
- if (opts.tags) body.tags = opts.tags.split(",");
10129
- if (opts.public) body.isPublic = true;
10130
- if (opts.private) body.isPublic = false;
10131
- await runCreate({
10132
- global: opts,
10133
- fn: () => createTask(uid, body),
10134
- dataKey: "task",
10135
- spinnerMessage: "Creating task...",
10136
- onInteractive: (_task, payload) => {
10137
- const { task, karma } = payload;
10138
- const check = import_picocolors13.default.green(SYM.check);
10139
- console.log(`
10140
- ${check} Created ${task.text} ${import_picocolors13.default.dim(task.id)}`);
10141
- if (karma) console.log(` ${formatKarmaGain(karma)}`);
10142
- console.log("");
10143
- }
10144
- });
10145
- });
10146
10187
  registerPostsCommands(program2);
10147
10188
  registerProfileCommands(program2);
10148
10189
  registerDoctorCommand(program2);
10149
10190
  program2.command("commands").description("List all available commands").action(function() {
10150
10191
  const opts = this.optsWithGlobals();
10151
- const useJson2 = !!(opts.json || opts.quiet || !isInteractive());
10152
- const commands = [];
10153
- function walk(cmd, prefix) {
10154
- for (const sub of cmd.commands) {
10155
- const fullName = prefix ? `${prefix} ${sub.name()}` : sub.name();
10156
- if (sub.commands.length > 0) {
10157
- walk(sub, fullName);
10158
- } else {
10159
- commands.push({
10160
- name: fullName,
10161
- description: sub.description(),
10162
- options: sub.options.map((o) => o.flags)
10163
- });
10164
- }
10165
- }
10166
- }
10167
- walk(program2, "");
10192
+ const useJson2 = isQuietMode(opts);
10193
+ const commands = collectCommands(program2);
10168
10194
  if (useJson2) {
10169
- console.log(JSON.stringify({ commands }));
10195
+ console.log(JSON.stringify({ schemaVersion: "1", cliVersion: CLI_VERSION, commands }));
10170
10196
  } else {
10171
- console.log("");
10172
- let lastGroup = "";
10173
- for (const cmd of commands) {
10174
- const group = cmd.name.split(" ")[0];
10175
- if (group !== lastGroup) {
10176
- if (lastGroup) console.log("");
10177
- console.log(` ${import_picocolors13.default.bold(group.charAt(0).toUpperCase() + group.slice(1) + ":")}`);
10178
- lastGroup = group;
10179
- }
10180
- console.log(` numo ${cmd.name.padEnd(30)} ${import_picocolors13.default.dim(cmd.description)}`);
10181
- }
10182
10197
  console.log(`
10183
- ${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.")}
10184
10201
  `);
10185
10202
  }
10186
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
+ };
10187
10222
  function buildCommandSchema(cmd, fullName) {
10188
10223
  return {
10189
10224
  name: fullName,
@@ -10191,14 +10226,23 @@ function buildCommandSchema(cmd, fullName) {
10191
10226
  arguments: cmd.registeredArguments?.map((a) => ({
10192
10227
  name: a.name(),
10193
10228
  required: a.required,
10229
+ variadic: a.variadic,
10194
10230
  description: a.description
10195
10231
  })) ?? [],
10196
- options: cmd.options.filter((o) => !["--json", "-q, --quiet"].includes(o.flags)).map((o) => ({
10197
- flags: o.flags,
10198
- description: o.description,
10199
- required: o.required,
10200
- default: o.defaultValue
10201
- }))
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
+ })
10202
10246
  };
10203
10247
  }
10204
10248
  program2.command("schema [command]").description("Print JSON schema for a command (for AI agents)").action(function(cmdPath) {
@@ -10213,7 +10257,7 @@ program2.command("schema [command]").description("Print JSON schema for a comman
10213
10257
  var walk = walk2;
10214
10258
  const schemas = [];
10215
10259
  walk2(program2, "");
10216
- console.log(JSON.stringify({ commands: schemas }, null, 2));
10260
+ console.log(JSON.stringify({ schemaVersion: "1", cliVersion: CLI_VERSION, commands: schemas }, null, 2));
10217
10261
  return;
10218
10262
  }
10219
10263
  const parts = cmdPath.split(" ");
@@ -10227,7 +10271,7 @@ program2.command("schema [command]").description("Print JSON schema for a comman
10227
10271
  }
10228
10272
  cmd = sub;
10229
10273
  }
10230
- console.log(JSON.stringify(buildCommandSchema(cmd, cmdPath), null, 2));
10274
+ console.log(JSON.stringify({ schemaVersion: "1", cliVersion: CLI_VERSION, ...buildCommandSchema(cmd, cmdPath) }, null, 2));
10231
10275
  });
10232
10276
  program2.command("completion <shell>").description("Generate shell completion script").action(function(shell) {
10233
10277
  if (shell !== "zsh") {