create-kanojo 0.1.1 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.mjs +621 -133
  2. package/package.json +2 -3
package/dist/index.mjs CHANGED
@@ -32,7 +32,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
32
32
  enumerable: true
33
33
  }) : target, mod));
34
34
  //#endregion
35
- //#region ../../node_modules/.bun/@clack+core@1.1.0/node_modules/@clack/core/dist/index.mjs
35
+ //#region ../../node_modules/@clack/core/dist/index.mjs
36
36
  var import_src = (/* @__PURE__ */ __commonJSMin(((exports, module) => {
37
37
  const ESC = "\x1B";
38
38
  const CSI = `${ESC}[`;
@@ -542,7 +542,7 @@ var $t = class extends B {
542
542
  }
543
543
  };
544
544
  //#endregion
545
- //#region ../../node_modules/.bun/@clack+prompts@1.1.0/node_modules/@clack/prompts/dist/index.mjs
545
+ //#region ../../node_modules/@clack/prompts/dist/index.mjs
546
546
  function pt() {
547
547
  return N.platform !== "win32" ? N.env.TERM !== "linux" : !!N.env.CI || !!N.env.WT_SESSION || !!N.env.TERMINUS_SUBLIME || N.env.ConEmuTask === "{cmd::Cmder}" || N.env.TERM_PROGRAM === "Terminus-Sublime" || N.env.TERM_PROGRAM === "vscode" || N.env.TERM === "xterm-256color" || N.env.TERM === "alacritty" || N.env.TERMINAL_EMULATOR === "JetBrains-JediTerm";
548
548
  }
@@ -1062,7 +1062,7 @@ ${r ? styleText("cyan", x) : ""}
1062
1062
  }
1063
1063
  }).prompt();
1064
1064
  //#endregion
1065
- //#region ../../node_modules/.bun/@noble+hashes@2.0.1/node_modules/@noble/hashes/utils.js
1065
+ //#region ../../node_modules/@noble/hashes/utils.js
1066
1066
  var import_picocolors = /* @__PURE__ */ __toESM((/* @__PURE__ */ __commonJSMin(((exports, module) => {
1067
1067
  let p = process || {}, argv = p.argv || [], env = p.env || {};
1068
1068
  let isColorSupported = !(!!env.NO_COLOR || argv.includes("--no-color")) && (!!env.FORCE_COLOR || argv.includes("--color") || p.platform === "win32" || (p.stdout || {}).isTTY && env.TERM !== "dumb" || !!env.CI);
@@ -1283,7 +1283,7 @@ const oidNist = (suffix) => ({ oid: Uint8Array.from([
1283
1283
  suffix
1284
1284
  ]) });
1285
1285
  //#endregion
1286
- //#region ../../node_modules/.bun/@noble+hashes@2.0.1/node_modules/@noble/hashes/_md.js
1286
+ //#region ../../node_modules/@noble/hashes/_md.js
1287
1287
  /**
1288
1288
  * Internal Merkle-Damgard hash utils.
1289
1289
  * @module
@@ -1404,7 +1404,7 @@ const SHA256_IV = /* @__PURE__ */ Uint32Array.from([
1404
1404
  1541459225
1405
1405
  ]);
1406
1406
  //#endregion
1407
- //#region ../../node_modules/.bun/@noble+hashes@2.0.1/node_modules/@noble/hashes/_u64.js
1407
+ //#region ../../node_modules/@noble/hashes/_u64.js
1408
1408
  /**
1409
1409
  * Internal helpers for u64. BigUint64Array is too slow as per 2025, so we implement it using Uint32Array.
1410
1410
  * @todo re-check https://issues.chromium.org/issues/42212588
@@ -1433,7 +1433,7 @@ function split(lst, le = false) {
1433
1433
  return [Ah, Al];
1434
1434
  }
1435
1435
  //#endregion
1436
- //#region ../../node_modules/.bun/@noble+hashes@2.0.1/node_modules/@noble/hashes/sha2.js
1436
+ //#region ../../node_modules/@noble/hashes/sha2.js
1437
1437
  /**
1438
1438
  * SHA2 hash function. A.k.a. sha256, sha384, sha512, sha512_224, sha512_256.
1439
1439
  * SHA256 is the fastest hash implementable in JS, even faster than Blake3.
@@ -1689,7 +1689,7 @@ K512[1];
1689
1689
  */
1690
1690
  const sha256 = /* @__PURE__ */ createHasher$1(() => new _SHA256(), /* @__PURE__ */ oidNist(1));
1691
1691
  //#endregion
1692
- //#region ../../node_modules/.bun/@noble+curves@2.0.1/node_modules/@noble/curves/utils.js
1692
+ //#region ../../node_modules/@noble/curves/utils.js
1693
1693
  /**
1694
1694
  * Hex, bytes and number utilities.
1695
1695
  * @module
@@ -1867,7 +1867,7 @@ function memoized(fn) {
1867
1867
  };
1868
1868
  }
1869
1869
  //#endregion
1870
- //#region ../../node_modules/.bun/@noble+curves@2.0.1/node_modules/@noble/curves/abstract/modular.js
1870
+ //#region ../../node_modules/@noble/curves/abstract/modular.js
1871
1871
  /**
1872
1872
  * Utils for modular division and fields.
1873
1873
  * Field over 11 is a finite (Galois) field is integer number operations `mod 11`.
@@ -2292,7 +2292,7 @@ function mapHashToField(key, fieldOrder, isLE = false) {
2292
2292
  return isLE ? numberToBytesLE(reduced, fieldLen) : numberToBytesBE(reduced, fieldLen);
2293
2293
  }
2294
2294
  //#endregion
2295
- //#region ../../node_modules/.bun/@noble+curves@2.0.1/node_modules/@noble/curves/abstract/curve.js
2295
+ //#region ../../node_modules/@noble/curves/abstract/curve.js
2296
2296
  /**
2297
2297
  * Methods for elliptic curve multiplication by scalars.
2298
2298
  * Contains wNAF, pippenger.
@@ -2564,7 +2564,7 @@ function createKeygen(randomSecretKey, getPublicKey) {
2564
2564
  };
2565
2565
  }
2566
2566
  //#endregion
2567
- //#region ../../node_modules/.bun/@noble+curves@2.0.1/node_modules/@noble/curves/abstract/hash-to-curve.js
2567
+ //#region ../../node_modules/@noble/curves/abstract/hash-to-curve.js
2568
2568
  const os2ip = bytesToNumberBE;
2569
2569
  function i2osp(value, length) {
2570
2570
  asafenumber(value);
@@ -2726,7 +2726,7 @@ function createHasher(Point, mapToCurve, defaults) {
2726
2726
  };
2727
2727
  }
2728
2728
  //#endregion
2729
- //#region ../../node_modules/.bun/@noble+hashes@2.0.1/node_modules/@noble/hashes/hmac.js
2729
+ //#region ../../node_modules/@noble/hashes/hmac.js
2730
2730
  /**
2731
2731
  * HMAC: RFC2104 message authentication code.
2732
2732
  * @module
@@ -2809,7 +2809,7 @@ var _HMAC = class {
2809
2809
  const hmac = (hash, key, message) => new _HMAC(hash, key).update(message).digest();
2810
2810
  hmac.create = (hash, key) => new _HMAC(hash, key);
2811
2811
  //#endregion
2812
- //#region ../../node_modules/.bun/@noble+curves@2.0.1/node_modules/@noble/curves/abstract/weierstrass.js
2812
+ //#region ../../node_modules/@noble/curves/abstract/weierstrass.js
2813
2813
  /**
2814
2814
  * Short Weierstrass curve methods. The formula is: y² = x³ + ax + b.
2815
2815
  *
@@ -3860,7 +3860,7 @@ function ecdsa(Point, hash, ecdsaOpts = {}) {
3860
3860
  });
3861
3861
  }
3862
3862
  //#endregion
3863
- //#region ../../node_modules/.bun/@noble+curves@2.0.1/node_modules/@noble/curves/secp256k1.js
3863
+ //#region ../../node_modules/@noble/curves/secp256k1.js
3864
3864
  /**
3865
3865
  * SECG secp256k1. See [pdf](https://www.secg.org/sec2-v2.pdf).
3866
3866
  *
@@ -4102,7 +4102,7 @@ createHasher(Pointk1, (scalars) => {
4102
4102
  hash: sha256
4103
4103
  });
4104
4104
  //#endregion
4105
- //#region ../../node_modules/.bun/@scure+base@2.0.0/node_modules/@scure/base/index.js
4105
+ //#region ../../node_modules/@scure/base/index.js
4106
4106
  /*! scure-base - MIT License (c) 2022 Paul Miller (paulmillr.com) */
4107
4107
  function isBytes$1(a) {
4108
4108
  return a instanceof Uint8Array || ArrayBuffer.isView(a) && a.constructor.name === "Uint8Array";
@@ -4436,7 +4436,7 @@ typeof Uint8Array.from([]).toHex === "function" && typeof Uint8Array.fromHex ===
4436
4436
  return s.toLowerCase();
4437
4437
  }));
4438
4438
  //#endregion
4439
- //#region ../../node_modules/.bun/@noble+ciphers@2.1.1/node_modules/@noble/ciphers/utils.js
4439
+ //#region ../../node_modules/@noble/ciphers/utils.js
4440
4440
  /**
4441
4441
  * Utilities for hex, bytes, CSPRNG.
4442
4442
  * @module
@@ -4584,7 +4584,7 @@ function copyBytes(bytes) {
4584
4584
  return Uint8Array.from(bytes);
4585
4585
  }
4586
4586
  //#endregion
4587
- //#region ../../node_modules/.bun/@noble+ciphers@2.1.1/node_modules/@noble/ciphers/aes.js
4587
+ //#region ../../node_modules/@noble/ciphers/aes.js
4588
4588
  const BLOCK_SIZE = 16;
4589
4589
  const POLY = 283;
4590
4590
  function validateKeyLength(key) {
@@ -4954,7 +4954,7 @@ var _CMAC = class {
4954
4954
  const cmac = (key, message) => new _CMAC(key).update(message).digest();
4955
4955
  cmac.create = (key) => new _CMAC(key);
4956
4956
  //#endregion
4957
- //#region ../../node_modules/.bun/@noble+ciphers@2.1.1/node_modules/@noble/ciphers/_arx.js
4957
+ //#region ../../node_modules/@noble/ciphers/_arx.js
4958
4958
  /**
4959
4959
  * Basic utils for ARX (add-rotate-xor) salsa and chacha ciphers.
4960
4960
 
@@ -5099,7 +5099,7 @@ function createCipher(core, opts) {
5099
5099
  };
5100
5100
  }
5101
5101
  //#endregion
5102
- //#region ../../node_modules/.bun/@noble+ciphers@2.1.1/node_modules/@noble/ciphers/_poly1305.js
5102
+ //#region ../../node_modules/@noble/ciphers/_poly1305.js
5103
5103
  /**
5104
5104
  * Poly1305 ([PDF](https://cr.yp.to/mac/poly1305-20050329.pdf),
5105
5105
  * [wiki](https://en.wikipedia.org/wiki/Poly1305))
@@ -5371,7 +5371,7 @@ function wrapConstructorWithKey(hashCons) {
5371
5371
  /** Poly1305 MAC from RFC 8439. */
5372
5372
  const poly1305 = wrapConstructorWithKey((key) => new Poly1305(key));
5373
5373
  //#endregion
5374
- //#region ../../node_modules/.bun/@noble+ciphers@2.1.1/node_modules/@noble/ciphers/chacha.js
5374
+ //#region ../../node_modules/@noble/ciphers/chacha.js
5375
5375
  /**
5376
5376
  * ChaCha stream cipher, released
5377
5377
  * in 2008. Developed after Salsa20, ChaCha aims to increase diffusion per round.
@@ -5634,7 +5634,7 @@ const _poly1305_aead = (xorStream) => (key, nonce, AAD) => {
5634
5634
  _poly1305_aead(chacha20);
5635
5635
  _poly1305_aead(xchacha20);
5636
5636
  //#endregion
5637
- //#region ../../node_modules/.bun/@noble+hashes@2.0.1/node_modules/@noble/hashes/hkdf.js
5637
+ //#region ../../node_modules/@noble/hashes/hkdf.js
5638
5638
  /**
5639
5639
  * HKDF (RFC 5869): extract + expand in one step.
5640
5640
  * See https://soatok.blog/2021/11/17/understanding-hkdf/.
@@ -5685,7 +5685,7 @@ function expand(hash, prk, info, length = 32) {
5685
5685
  return okm.slice(0, length);
5686
5686
  }
5687
5687
  //#endregion
5688
- //#region ../../node_modules/.bun/nostr-tools@2.23.3+1fb4c65d43e298b9/node_modules/nostr-tools/lib/esm/index.js
5688
+ //#region ../../node_modules/nostr-tools/lib/esm/index.js
5689
5689
  var __defProp = Object.defineProperty;
5690
5690
  var __export = (target, all) => {
5691
5691
  for (var name in all) __defProp(target, name, {
@@ -9318,21 +9318,31 @@ function createNostrService(options = {}) {
9318
9318
  };
9319
9319
  }
9320
9320
  async function closeListing(profile, listingId) {
9321
- const listing = profile.cache.listings.find((item) => item.id === listingId);
9322
- if (!listing) throw new Error(`Listing not found: ${listingId}`);
9323
- const closedListing = {
9321
+ return updateListing(profile, {
9322
+ listingId,
9323
+ status: "closed"
9324
+ });
9325
+ }
9326
+ async function updateListing(profile, input) {
9327
+ const listing = profile.cache.listings.find((item) => item.id === input.listingId);
9328
+ if (!listing) throw new Error(`Listing not found: ${input.listingId}`);
9329
+ const nextListing = {
9324
9330
  ...listing,
9325
- status: "closed",
9331
+ headline: input.headline?.trim() ?? listing.headline,
9332
+ summary: input.summary?.trim() ?? listing.summary,
9333
+ region: input.region?.trim() ?? listing.region,
9334
+ desiredTags: uniqueStrings(input.desiredTags ?? listing.desiredTags),
9335
+ status: input.status ?? listing.status,
9326
9336
  updatedAt: now()
9327
9337
  };
9328
9338
  const secretKey = decodeNsec(profile.nostr.nsec);
9329
- const signed = finalizeEvent(serializeMatchingListingEvent(profile, closedListing), secretKey);
9339
+ const signed = finalizeEvent(serializeMatchingListingEvent(profile, nextListing), secretKey);
9330
9340
  await transport.publish(profile.relays, [signed]);
9331
9341
  return {
9332
9342
  ...profile,
9333
9343
  cache: {
9334
9344
  ...profile.cache,
9335
- listings: [closedListing, ...profile.cache.listings.filter((item) => item.id !== closedListing.id)],
9345
+ listings: [nextListing, ...profile.cache.listings.filter((item) => item.id !== nextListing.id)],
9336
9346
  lastListingSyncAt: now()
9337
9347
  }
9338
9348
  };
@@ -9461,6 +9471,7 @@ function createNostrService(options = {}) {
9461
9471
  publishListing,
9462
9472
  refreshOwnListings,
9463
9473
  closeListing,
9474
+ updateListing,
9464
9475
  discoverListings,
9465
9476
  syncInbox,
9466
9477
  sendLike,
@@ -9478,6 +9489,13 @@ function createGeneratedCredentials() {
9478
9489
  nsec: nip19_exports.nsecEncode(secretKey)
9479
9490
  };
9480
9491
  }
9492
+ function importCredentials(nsec) {
9493
+ const secretKey = decodeNsec(nsec.trim());
9494
+ return {
9495
+ pubkey: getPublicKey(secretKey),
9496
+ nsec: nip19_exports.nsecEncode(secretKey)
9497
+ };
9498
+ }
9481
9499
  function createSimplePoolTransport() {
9482
9500
  const pool = new SimplePool();
9483
9501
  return {
@@ -9625,29 +9643,85 @@ function createMatchingCli(preset, options = {}) {
9625
9643
  const store = loadProfileStore(options.baseDir);
9626
9644
  const service = createNostrService(options);
9627
9645
  const theme = createTheme(preset);
9646
+ let plainOutput = false;
9628
9647
  return { async run(rawArgs = process.argv.slice(2)) {
9629
9648
  await store.ensure();
9630
- Wt(theme.banner(` ${preset.brand} `));
9631
9649
  try {
9632
9650
  const parsed = extractProfileOverride(rawArgs);
9633
9651
  const command = parseCommandFlags(parsed.args);
9634
9652
  const [rootCommand] = command.positionals;
9653
+ plainOutput = command.positionals.length > 0 || Boolean(command.flags.help) || !process.stdout.isTTY;
9654
+ showIntro();
9635
9655
  if (command.flags.help || rootCommand === "help") {
9636
- Vt(renderCliUsage(preset), "Usage");
9656
+ showText(renderCliUsage(preset));
9637
9657
  return;
9638
9658
  }
9639
9659
  await dispatchCommand(command, parsed.profileName);
9640
- Gt(theme.accent("Connected quietly. Ready for the next good match."));
9660
+ showOutro("Connected quietly. Ready for the next good match.");
9641
9661
  } catch (error) {
9642
9662
  if (error instanceof CancelledFlowError) {
9643
- Nt("Operation cancelled.");
9663
+ showCancelled("Operation cancelled.");
9644
9664
  return;
9645
9665
  }
9646
- R.error(error instanceof Error ? error.message : "Unexpected error.");
9666
+ showError(error instanceof Error ? error.message : "Unexpected error.");
9647
9667
  } finally {
9648
9668
  service.close();
9649
9669
  }
9650
9670
  } };
9671
+ function showIntro() {
9672
+ if (!plainOutput) Wt(theme.banner(` ${preset.brand} `));
9673
+ }
9674
+ function showOutro(message) {
9675
+ if (!plainOutput) Gt(theme.accent(message));
9676
+ }
9677
+ function showCancelled(message) {
9678
+ if (plainOutput) {
9679
+ process.stderr.write(`${message}\n`);
9680
+ return;
9681
+ }
9682
+ Nt(message);
9683
+ }
9684
+ function showText(message) {
9685
+ process.stdout.write(`${message}\n`);
9686
+ }
9687
+ function showSection(body, title) {
9688
+ if (plainOutput) {
9689
+ showText(`${title}\n${body}`);
9690
+ return;
9691
+ }
9692
+ Vt(body, title);
9693
+ }
9694
+ function showInfo(message) {
9695
+ if (plainOutput) {
9696
+ showText(message);
9697
+ return;
9698
+ }
9699
+ R.info(message);
9700
+ }
9701
+ function showSuccess(message) {
9702
+ if (plainOutput) {
9703
+ showText(message);
9704
+ return;
9705
+ }
9706
+ R.success(message);
9707
+ }
9708
+ function showStep(message) {
9709
+ if (plainOutput) {
9710
+ showText(message);
9711
+ return;
9712
+ }
9713
+ R.step(message);
9714
+ }
9715
+ function showError(message) {
9716
+ if (plainOutput) {
9717
+ process.stderr.write(`Error: ${message}\n`);
9718
+ return;
9719
+ }
9720
+ R.error(message);
9721
+ }
9722
+ async function runWithSpinner(message, task) {
9723
+ return withSpinner(message, task, plainOutput);
9724
+ }
9651
9725
  async function dispatchCommand(parsedArgs, profileOverride) {
9652
9726
  const [command, subcommand, ...rest] = parsedArgs.positionals;
9653
9727
  if (!command) {
@@ -9663,7 +9737,7 @@ function createMatchingCli(preset, options = {}) {
9663
9737
  return;
9664
9738
  }
9665
9739
  if (command === "discover") {
9666
- await runDiscover(profileOverride);
9740
+ await runDiscoverCommand(subcommand, rest, parsedArgs.flags, profileOverride);
9667
9741
  return;
9668
9742
  }
9669
9743
  if (command === "likes") {
@@ -9675,13 +9749,21 @@ function createMatchingCli(preset, options = {}) {
9675
9749
  return;
9676
9750
  }
9677
9751
  if (command === "chat") {
9678
- await runChat(profileOverride, subcommand, parsedArgs.flags);
9752
+ await runChat(profileOverride, subcommand, rest, parsedArgs.flags);
9679
9753
  return;
9680
9754
  }
9681
9755
  if (command === "config") {
9682
9756
  await runConfigCommand(subcommand, parsedArgs.flags, profileOverride);
9683
9757
  return;
9684
9758
  }
9759
+ if (command === "inbox") {
9760
+ await runInbox(profileOverride);
9761
+ return;
9762
+ }
9763
+ if (command === "watch") {
9764
+ await runWatch(profileOverride, parsedArgs.flags);
9765
+ return;
9766
+ }
9685
9767
  throw new Error(`Unknown command: ${command}`);
9686
9768
  }
9687
9769
  async function runHome(profileOverride) {
@@ -9690,7 +9772,7 @@ function createMatchingCli(preset, options = {}) {
9690
9772
  profile = await service.syncInbox(profile);
9691
9773
  await store.saveProfile(profile);
9692
9774
  await store.setActiveProfile(preset.brand, profile.profileName);
9693
- Vt(renderProfileCard(profile), "Current Profile");
9775
+ showSection(renderProfileCard(profile), "Current Profile");
9694
9776
  const action = await askSelect({
9695
9777
  message: "What do you want to do next?",
9696
9778
  options: [
@@ -9753,7 +9835,7 @@ function createMatchingCli(preset, options = {}) {
9753
9835
  continue;
9754
9836
  }
9755
9837
  if (action === "profile-show") {
9756
- Vt(renderProfileCard(profile, true), "Profile Details");
9838
+ showSection(renderProfileCard(profile, true), "Profile Details");
9757
9839
  continue;
9758
9840
  }
9759
9841
  if (action === "switch-profile") {
@@ -9783,6 +9865,23 @@ function createMatchingCli(preset, options = {}) {
9783
9865
  });
9784
9866
  return;
9785
9867
  }
9868
+ if (subcommand === "import") {
9869
+ await promptProfileImport({
9870
+ profileName: getStringFlag(flags, "name", "profile-name"),
9871
+ displayName: getStringFlag(flags, "display-name"),
9872
+ ageRange: getStringFlag(flags, "age-range"),
9873
+ region: getStringFlag(flags, "region"),
9874
+ bio: getStringFlag(flags, "bio"),
9875
+ interests: getStringFlag(flags, "interests"),
9876
+ lookingForAge: getStringFlag(flags, "looking-age", "looking-age-range"),
9877
+ lookingForRegions: getStringFlag(flags, "looking-regions"),
9878
+ lookingForNotes: getStringFlag(flags, "looking-notes"),
9879
+ relays: getStringFlag(flags, "relays"),
9880
+ nsec: getStringFlag(flags, "nsec"),
9881
+ publish: Boolean(flags.publish)
9882
+ });
9883
+ return;
9884
+ }
9786
9885
  if (subcommand === "use") {
9787
9886
  await promptProfileUse(args[0] ?? profileOverride);
9788
9887
  return;
@@ -9791,17 +9890,30 @@ function createMatchingCli(preset, options = {}) {
9791
9890
  const profiles = await store.listProfiles();
9792
9891
  const active = await store.getActiveProfileName(preset.brand);
9793
9892
  if (profiles.length === 0) {
9794
- R.info("No profiles yet. Start with `profile create`.");
9893
+ showInfo("No profiles yet. Start with `profile create`.");
9795
9894
  return;
9796
9895
  }
9797
- Vt(profiles.map((name) => `${name === active ? "●" : "○"} ${name}`).join("\n"), "Profiles");
9896
+ showSection(profiles.map((name) => `${name === active ? "●" : "○"} ${name}`).join("\n"), "Profiles");
9798
9897
  return;
9799
9898
  }
9800
9899
  if (subcommand === "show") {
9801
- Vt(renderProfileCard(await ensureProfile(profileOverride), true), "Profile Details");
9900
+ showSection(renderProfileCard(await ensureProfile(profileOverride), true), "Profile Details");
9802
9901
  return;
9803
9902
  }
9804
- throw new Error("Use `profile create|use|list|show`.");
9903
+ if (subcommand === "edit") {
9904
+ await promptProfileEdit(await ensureProfile(profileOverride), {
9905
+ displayName: getStringFlag(flags, "display-name"),
9906
+ ageRange: getStringFlag(flags, "age-range"),
9907
+ region: getStringFlag(flags, "region"),
9908
+ bio: getStringFlag(flags, "bio"),
9909
+ interests: getStringFlag(flags, "interests"),
9910
+ lookingForAge: getStringFlag(flags, "looking-age", "looking-age-range"),
9911
+ lookingForRegions: getStringFlag(flags, "looking-regions"),
9912
+ lookingForNotes: getStringFlag(flags, "looking-notes")
9913
+ });
9914
+ return;
9915
+ }
9916
+ throw new Error("Use `profile create|import|edit|use|list|show`.");
9805
9917
  }
9806
9918
  async function runListingCommand(subcommand, _args, flags, profileOverride) {
9807
9919
  const profile = await ensureProfile(profileOverride);
@@ -9817,14 +9929,14 @@ function createMatchingCli(preset, options = {}) {
9817
9929
  if (subcommand === "list") {
9818
9930
  const refreshed = await service.refreshOwnListings(profile);
9819
9931
  await store.saveProfile(refreshed);
9820
- Vt(renderListings(refreshed.cache.listings), "Your Listings");
9932
+ showSection(renderListings(refreshed.cache.listings), "Your Listings");
9821
9933
  return;
9822
9934
  }
9823
9935
  if (subcommand === "close") {
9824
9936
  const refreshed = await service.refreshOwnListings(profile);
9825
9937
  const openListings = refreshed.cache.listings.filter((listing) => listing.status === "open");
9826
9938
  if (openListings.length === 0) {
9827
- R.info("There are no open listings to close.");
9939
+ showInfo("There are no open listings to close.");
9828
9940
  return;
9829
9941
  }
9830
9942
  const listingIdArg = getStringFlag(flags, "id", "listing-id");
@@ -9839,15 +9951,50 @@ function createMatchingCli(preset, options = {}) {
9839
9951
  hint: listing.region
9840
9952
  }))
9841
9953
  });
9842
- const nextProfile = await withSpinner("Closing listing...", () => service.closeListing(refreshed, listingId));
9954
+ const nextProfile = await runWithSpinner("Closing listing...", () => service.closeListing(refreshed, listingId));
9843
9955
  await store.saveProfile(nextProfile);
9844
- R.success("Listing closed.");
9956
+ showSuccess("Listing closed.");
9957
+ return;
9958
+ }
9959
+ if (subcommand === "edit") {
9960
+ const refreshed = await service.refreshOwnListings(profile);
9961
+ await store.saveProfile(refreshed);
9962
+ await promptListingEdit(refreshed, {
9963
+ listingRef: getStringFlag(flags, "id", "listing-id", "address", "listing-address") ?? _args[0],
9964
+ headline: getStringFlag(flags, "title", "headline"),
9965
+ summary: getStringFlag(flags, "summary"),
9966
+ region: getStringFlag(flags, "region"),
9967
+ desiredTags: getStringFlag(flags, "tags", "desired-tags")
9968
+ });
9845
9969
  return;
9846
9970
  }
9847
- throw new Error("Use `listing publish|list|close`.");
9971
+ if (subcommand === "reopen") {
9972
+ const refreshed = await service.refreshOwnListings(profile);
9973
+ await store.saveProfile(refreshed);
9974
+ await reopenListing(refreshed, getStringFlag(flags, "id", "listing-id", "address", "listing-address") ?? _args[0]);
9975
+ return;
9976
+ }
9977
+ throw new Error("Use `listing publish|list|close|edit|reopen`.");
9848
9978
  }
9849
- async function runDiscover(profileOverride) {
9850
- await handleDiscover(await ensureProfile(profileOverride));
9979
+ async function runDiscoverCommand(subcommand, args, flags, profileOverride) {
9980
+ const profile = await ensureProfile(profileOverride);
9981
+ if (!subcommand) {
9982
+ await handleDiscover(profile);
9983
+ return;
9984
+ }
9985
+ if (subcommand === "list") {
9986
+ await showDiscoverList(profile);
9987
+ return;
9988
+ }
9989
+ if (subcommand === "like") {
9990
+ await likeDiscoveredListing(profile, args[0] ?? getStringFlag(flags, "id", "listing", "address"), flags);
9991
+ return;
9992
+ }
9993
+ if (subcommand === "pass") {
9994
+ await passDiscoveredListing(profile, args[0] ?? getStringFlag(flags, "id", "listing", "address"));
9995
+ return;
9996
+ }
9997
+ throw new Error("Use `discover`, `discover list`, `discover like <listing>`, or `discover pass <listing>`.");
9851
9998
  }
9852
9999
  async function runLikes(profileOverride) {
9853
10000
  await handleLikes(await ensureProfile(profileOverride));
@@ -9855,23 +10002,46 @@ function createMatchingCli(preset, options = {}) {
9855
10002
  async function runMatches(profileOverride) {
9856
10003
  await handleMatches(await ensureProfile(profileOverride));
9857
10004
  }
9858
- async function runChat(profileOverride, matchIdArg, flags = {}) {
9859
- await handleChat(await ensureProfile(profileOverride), matchIdArg, void 0, getStringFlag(flags, "message"));
10005
+ async function runChat(profileOverride, chatArg, args = [], flags = {}) {
10006
+ const profile = await ensureProfile(profileOverride);
10007
+ const message = getStringFlag(flags, "message");
10008
+ if (chatArg === "list") {
10009
+ await showConversationList(profile);
10010
+ return;
10011
+ }
10012
+ if (chatArg === "show") {
10013
+ await showConversationHistory(profile, args[0] ?? getStringFlag(flags, "thread-id"));
10014
+ return;
10015
+ }
10016
+ if (chatArg && message === void 0) {
10017
+ await showConversationHistory(profile, chatArg);
10018
+ return;
10019
+ }
10020
+ await handleChat(profile, chatArg, void 0, message);
9860
10021
  }
9861
10022
  async function runConfigCommand(subcommand, flags, profileOverride) {
9862
10023
  const profile = await ensureProfile(profileOverride);
9863
10024
  if (subcommand === "show") {
9864
- Vt(renderProfileCard(profile, true), "Advanced Config");
10025
+ showSection(renderProfileCard(profile, true), "Advanced Config");
9865
10026
  return;
9866
10027
  }
9867
10028
  if (subcommand === "relays") {
9868
10029
  const nextProfile = await promptRelayConfig(profile, getStringFlag(flags, "relays"));
9869
10030
  await store.saveProfile(nextProfile);
9870
- R.success("Relay list updated.");
10031
+ showSuccess("Relay list updated.");
9871
10032
  return;
9872
10033
  }
9873
10034
  throw new Error("Use `config show|relays`.");
9874
10035
  }
10036
+ async function runInbox(profileOverride) {
10037
+ await showInbox(await ensureProfile(profileOverride));
10038
+ }
10039
+ async function runWatch(profileOverride, flags) {
10040
+ const profile = await ensureProfile(profileOverride);
10041
+ const intervalSeconds = Number.parseInt(getStringFlag(flags, "interval") ?? "10", 10);
10042
+ if (!Number.isFinite(intervalSeconds) || intervalSeconds <= 0) throw new Error("Interval must be a positive number of seconds.");
10043
+ await watchInbox(profile, intervalSeconds);
10044
+ }
9875
10045
  async function ensureProfile(profileOverride) {
9876
10046
  const explicit = profileOverride ? await store.readProfile(profileOverride) : null;
9877
10047
  if (explicit) return explicit;
@@ -9890,19 +10060,58 @@ function createMatchingCli(preset, options = {}) {
9890
10060
  return promptProfileCreate();
9891
10061
  }
9892
10062
  async function promptProfileCreate(initial = {}) {
9893
- const profileName = await resolveRequiredInput(initial.profileName, {
9894
- message: "Choose a profile name.",
9895
- placeholder: "main",
9896
- defaultValue: "main",
10063
+ const draft = await collectProfileDraft(initial);
10064
+ const credentials = createGeneratedCredentials();
10065
+ return saveProfileAndOptionallyPublish(createDefaultProfile(preset, {
10066
+ profileName: draft.profileName,
10067
+ displayName: draft.displayName,
10068
+ bio: draft.bio,
10069
+ region: draft.region,
10070
+ ageRange: draft.ageRange,
10071
+ interests: splitComma(draft.interests),
10072
+ lookingFor: {
10073
+ ageRange: draft.lookingForAge,
10074
+ regions: splitComma(draft.lookingForRegions),
10075
+ notes: draft.lookingForNotes
10076
+ },
10077
+ nostr: credentials,
10078
+ relays: draft.relays
10079
+ }), "Preparing profile...", "Profile Created", true);
10080
+ }
10081
+ async function promptProfileImport(initial = {}) {
10082
+ const draft = await collectProfileDraft(initial);
10083
+ const credentials = importCredentials(await resolveRequiredInput(initial.nsec, {
10084
+ message: "Enter the nsec to import.",
10085
+ placeholder: "nsec1...",
9897
10086
  validate(value) {
9898
- const normalized = value?.trim() ?? "";
9899
- if (!normalized) return "Profile name is required.";
9900
- if (!/^[a-z0-9-]+$/.test(normalized)) return "Use lowercase letters, digits, and hyphens only.";
10087
+ try {
10088
+ importCredentials(value ?? "");
10089
+ } catch (error) {
10090
+ return error instanceof Error ? error.message : "Invalid nsec value.";
10091
+ }
9901
10092
  }
9902
- });
10093
+ }));
10094
+ return saveProfileAndOptionallyPublish(createDefaultProfile(preset, {
10095
+ profileName: draft.profileName,
10096
+ displayName: draft.displayName,
10097
+ bio: draft.bio,
10098
+ region: draft.region,
10099
+ ageRange: draft.ageRange,
10100
+ interests: splitComma(draft.interests),
10101
+ lookingFor: {
10102
+ ageRange: draft.lookingForAge,
10103
+ regions: splitComma(draft.lookingForRegions),
10104
+ notes: draft.lookingForNotes
10105
+ },
10106
+ nostr: credentials,
10107
+ relays: draft.relays
10108
+ }), "Importing profile...", initial.publish ? "Profile Imported & Published" : "Profile Imported", Boolean(initial.publish));
10109
+ }
10110
+ async function promptProfileEdit(profile, initial = {}) {
9903
10111
  const displayName = await resolveRequiredInput(initial.displayName, {
9904
10112
  message: "What display name should we show?",
9905
10113
  placeholder: preset.brand === "create-kanojo" ? "たくみ" : "あや",
10114
+ defaultValue: profile.displayName,
9906
10115
  validate(value) {
9907
10116
  if (!(value?.trim() ?? "")) return "Display name is required.";
9908
10117
  }
@@ -9910,6 +10119,7 @@ function createMatchingCli(preset, options = {}) {
9910
10119
  const ageRange = await resolveRequiredInput(initial.ageRange, {
9911
10120
  message: "How would you describe your age range?",
9912
10121
  placeholder: "20代後半",
10122
+ defaultValue: profile.ageRange,
9913
10123
  validate(value) {
9914
10124
  if (!(value?.trim() ?? "")) return "Age range is required.";
9915
10125
  }
@@ -9917,6 +10127,7 @@ function createMatchingCli(preset, options = {}) {
9917
10127
  const region = await resolveRequiredInput(initial.region, {
9918
10128
  message: "Which area do you usually meet in?",
9919
10129
  placeholder: "東京",
10130
+ defaultValue: profile.region,
9920
10131
  validate(value) {
9921
10132
  if (!(value?.trim() ?? "")) return "Region is required.";
9922
10133
  }
@@ -9924,51 +10135,117 @@ function createMatchingCli(preset, options = {}) {
9924
10135
  const bio = await resolveRequiredInput(initial.bio, {
9925
10136
  message: "Write a short intro.",
9926
10137
  placeholder: "映画とコーヒーが好きです。",
10138
+ defaultValue: profile.bio,
9927
10139
  validate(value) {
9928
10140
  if (!(value?.trim() ?? "")) return "Bio is required.";
9929
10141
  }
9930
10142
  });
9931
10143
  const interests = await resolveOptionalInput(initial.interests, {
9932
10144
  message: "List interests or vibe tags, comma separated.",
10145
+ defaultValue: profile.interests.join(", "),
9933
10146
  placeholder: "映画, カフェ, 散歩"
9934
10147
  });
9935
10148
  const lookingForAge = await resolveOptionalInput(initial.lookingForAge, {
9936
10149
  message: "What age range are you looking for?",
10150
+ defaultValue: profile.lookingFor.ageRange,
9937
10151
  placeholder: "20代"
9938
10152
  });
9939
10153
  const lookingForRegions = await resolveOptionalInput(initial.lookingForRegions, {
9940
10154
  message: "Which regions do you want to meet in? Use commas.",
10155
+ defaultValue: profile.lookingFor.regions.join(", "),
9941
10156
  placeholder: region
9942
10157
  });
9943
10158
  const lookingForNotes = await resolveOptionalInput(initial.lookingForNotes, {
9944
10159
  message: "What kind of person feels right for you?",
10160
+ defaultValue: profile.lookingFor.notes,
9945
10161
  placeholder: "落ち着いて話せる人"
9946
10162
  });
9947
- const relays = initial.relays ? normalizeRelayList(initial.relays) : DEFAULT_RELAYS;
9948
- const credentials = createGeneratedCredentials();
9949
- const profile = createDefaultProfile(preset, {
9950
- profileName,
10163
+ return saveProfileAndOptionallyPublish({
10164
+ ...profile,
9951
10165
  displayName,
9952
- bio,
9953
- region,
9954
10166
  ageRange,
10167
+ region,
10168
+ bio,
9955
10169
  interests: splitComma(interests),
9956
10170
  lookingFor: {
9957
10171
  ageRange: lookingForAge,
9958
10172
  regions: splitComma(lookingForRegions),
9959
10173
  notes: lookingForNotes
9960
- },
9961
- nostr: credentials,
9962
- relays
10174
+ }
10175
+ }, "Updating profile...", "Profile Updated", true);
10176
+ }
10177
+ async function collectProfileDraft(initial) {
10178
+ const profileName = await resolveRequiredInput(initial.profileName, {
10179
+ message: "Choose a profile name.",
10180
+ placeholder: "main",
10181
+ defaultValue: "main",
10182
+ validate(value) {
10183
+ const normalized = value?.trim() ?? "";
10184
+ if (!normalized) return "Profile name is required.";
10185
+ if (!/^[a-z0-9-]+$/.test(normalized)) return "Use lowercase letters, digits, and hyphens only.";
10186
+ }
10187
+ });
10188
+ const displayName = await resolveRequiredInput(initial.displayName, {
10189
+ message: "What display name should we show?",
10190
+ placeholder: preset.brand === "create-kanojo" ? "たくみ" : "あや",
10191
+ validate(value) {
10192
+ if (!(value?.trim() ?? "")) return "Display name is required.";
10193
+ }
10194
+ });
10195
+ const ageRange = await resolveRequiredInput(initial.ageRange, {
10196
+ message: "How would you describe your age range?",
10197
+ placeholder: "20代後半",
10198
+ validate(value) {
10199
+ if (!(value?.trim() ?? "")) return "Age range is required.";
10200
+ }
9963
10201
  });
9964
- const published = await withSpinner("Preparing profile...", async () => {
10202
+ const region = await resolveRequiredInput(initial.region, {
10203
+ message: "Which area do you usually meet in?",
10204
+ placeholder: "東京",
10205
+ validate(value) {
10206
+ if (!(value?.trim() ?? "")) return "Region is required.";
10207
+ }
10208
+ });
10209
+ return {
10210
+ profileName,
10211
+ displayName,
10212
+ ageRange,
10213
+ region,
10214
+ bio: await resolveRequiredInput(initial.bio, {
10215
+ message: "Write a short intro.",
10216
+ placeholder: "映画とコーヒーが好きです。",
10217
+ validate(value) {
10218
+ if (!(value?.trim() ?? "")) return "Bio is required.";
10219
+ }
10220
+ }),
10221
+ interests: await resolveOptionalInput(initial.interests, {
10222
+ message: "List interests or vibe tags, comma separated.",
10223
+ placeholder: "映画, カフェ, 散歩"
10224
+ }),
10225
+ lookingForAge: await resolveOptionalInput(initial.lookingForAge, {
10226
+ message: "What age range are you looking for?",
10227
+ placeholder: "20代"
10228
+ }),
10229
+ lookingForRegions: await resolveOptionalInput(initial.lookingForRegions, {
10230
+ message: "Which regions do you want to meet in? Use commas.",
10231
+ placeholder: region
10232
+ }),
10233
+ lookingForNotes: await resolveOptionalInput(initial.lookingForNotes, {
10234
+ message: "What kind of person feels right for you?",
10235
+ placeholder: "落ち着いて話せる人"
10236
+ }),
10237
+ relays: initial.relays ? normalizeRelayList(initial.relays) : DEFAULT_RELAYS
10238
+ };
10239
+ }
10240
+ async function saveProfileAndOptionallyPublish(profile, spinnerMessage, title, publish) {
10241
+ const nextProfile = publish ? await runWithSpinner(spinnerMessage, async () => {
9965
10242
  await service.publishProfile(profile);
9966
10243
  return profile;
9967
- });
9968
- await store.saveProfile(published);
9969
- await store.setActiveProfile(preset.brand, published.profileName);
9970
- Vt(renderProfileCard(published), "Profile Created");
9971
- return published;
10244
+ }) : profile;
10245
+ await store.saveProfile(nextProfile);
10246
+ await store.setActiveProfile(preset.brand, nextProfile.profileName);
10247
+ showSection(renderProfileCard(nextProfile), title);
10248
+ return nextProfile;
9972
10249
  }
9973
10250
  async function promptProfileUse(profileNameArg) {
9974
10251
  const profiles = await store.listProfiles();
@@ -9988,7 +10265,7 @@ function createMatchingCli(preset, options = {}) {
9988
10265
  const profile = await store.readProfile(selectedName);
9989
10266
  if (!profile) throw new Error(`Profile not found: ${selectedName}`);
9990
10267
  await store.setActiveProfile(preset.brand, selectedName);
9991
- Vt(renderProfileCard(profile), "Active Profile");
10268
+ showSection(renderProfileCard(profile), "Active Profile");
9992
10269
  return profile;
9993
10270
  }
9994
10271
  async function handlePublishListing(profile, initial = {}) {
@@ -10014,44 +10291,90 @@ function createMatchingCli(preset, options = {}) {
10014
10291
  message: "Enter tags, comma separated.",
10015
10292
  placeholder: "映画, 落ち着き, 夜カフェ"
10016
10293
  });
10017
- const nextProfile = await withSpinner("Publishing listing...", () => service.publishListing(profile, {
10294
+ const nextProfile = await runWithSpinner("Publishing listing...", () => service.publishListing(profile, {
10018
10295
  headline,
10019
10296
  summary,
10020
10297
  region,
10021
10298
  desiredTags: splitComma(desiredTags)
10022
10299
  }));
10023
10300
  await store.saveProfile(nextProfile);
10024
- R.success("Listing published.");
10301
+ showSuccess("Listing published.");
10025
10302
  return nextProfile;
10026
10303
  }
10027
- async function handleDiscover(profile) {
10028
- const refreshed = await withSpinner("Looking for people...", async () => {
10029
- const synced = await service.syncInbox(profile);
10030
- const withListings = await service.refreshOwnListings(synced);
10031
- return {
10032
- profile: withListings,
10033
- listings: rankDiscoverListings(withListings, await service.discoverListings(withListings))
10034
- };
10304
+ async function promptListingEdit(profile, initial = {}) {
10305
+ const listing = await resolveOwnListing(profile, initial.listingRef, false);
10306
+ const headline = await resolveRequiredInput(initial.headline, {
10307
+ message: "Enter the listing title.",
10308
+ placeholder: "週末に一緒に映画を見に行ける人",
10309
+ defaultValue: listing.headline,
10310
+ validate(value) {
10311
+ if (!(value?.trim() ?? "")) return "Title is required.";
10312
+ }
10035
10313
  });
10036
- await store.saveProfile(refreshed.profile);
10314
+ const summary = await resolveRequiredInput(initial.summary, {
10315
+ message: "Write a short summary.",
10316
+ placeholder: "まずはお茶からゆっくり話したいです。",
10317
+ defaultValue: listing.summary,
10318
+ validate(value) {
10319
+ if (!(value?.trim() ?? "")) return "Summary is required.";
10320
+ }
10321
+ });
10322
+ const region = await resolveOptionalInput(initial.region, {
10323
+ message: "Which region is this listing for?",
10324
+ defaultValue: listing.region
10325
+ });
10326
+ const desiredTags = await resolveOptionalInput(initial.desiredTags, {
10327
+ message: "Enter tags, comma separated.",
10328
+ defaultValue: listing.desiredTags.join(", "),
10329
+ placeholder: "映画, 落ち着き, 夜カフェ"
10330
+ });
10331
+ const nextProfile = await runWithSpinner("Updating listing...", () => service.updateListing(profile, {
10332
+ listingId: listing.id,
10333
+ headline,
10334
+ summary,
10335
+ region,
10336
+ desiredTags: splitComma(desiredTags)
10337
+ }));
10338
+ await store.saveProfile(nextProfile);
10339
+ showSuccess("Listing updated.");
10340
+ return nextProfile;
10341
+ }
10342
+ async function reopenListing(profile, listingRef) {
10343
+ const listing = await resolveOwnListing(profile, listingRef, true);
10344
+ const nextProfile = await runWithSpinner("Reopening listing...", () => service.updateListing(profile, {
10345
+ listingId: listing.id,
10346
+ status: "open"
10347
+ }));
10348
+ await store.saveProfile(nextProfile);
10349
+ showSuccess("Listing reopened.");
10350
+ return nextProfile;
10351
+ }
10352
+ async function resolveOwnListing(profile, listingRef, closedOnly = false) {
10353
+ const listings = profile.cache.listings.filter((listing) => closedOnly ? listing.status === "closed" : true);
10354
+ if (listings.length === 0) throw new Error(closedOnly ? "There are no closed listings to reopen." : "There are no listings yet.");
10355
+ const selected = (listingRef ? listings.find((listing) => listing.id === listingRef || listing.address === listingRef) : null) ?? null;
10356
+ if (listingRef && !selected) throw new Error("Listing not found. Use `listing list` to check the id or address.");
10357
+ if (selected) return selected;
10358
+ const listingId = await askSelect({
10359
+ message: closedOnly ? "Choose a listing to reopen." : "Choose a listing to edit.",
10360
+ options: listings.map((listing) => ({
10361
+ value: listing.id,
10362
+ label: listing.headline,
10363
+ hint: `${listing.region} | ${listing.status}`
10364
+ }))
10365
+ });
10366
+ const listing = listings.find((item) => item.id === listingId);
10367
+ if (!listing) throw new Error("Listing not found.");
10368
+ return listing;
10369
+ }
10370
+ async function handleDiscover(profile) {
10371
+ const refreshed = await loadDiscoverState(profile);
10037
10372
  if (refreshed.listings.length === 0) {
10038
- R.info("No listings found right now. Try again later.");
10373
+ showInfo("No listings found right now. Try again later.");
10039
10374
  return refreshed.profile;
10040
10375
  }
10041
- const openListings = refreshed.profile.cache.listings.filter((item) => item.status === "open");
10042
- if (openListings.length === 0) {
10043
- R.warn("Publish at least one open listing first.");
10044
- return refreshed.profile;
10045
- }
10046
- const ownListingAddress = openListings.length === 1 ? openListings[0].address : await askSelect({
10047
- message: "Which of your listings should send the like?",
10048
- options: openListings.map((item) => ({
10049
- value: item.address,
10050
- label: item.headline,
10051
- hint: item.region
10052
- }))
10053
- });
10054
- Vt([
10376
+ const ownListingAddress = await resolveOwnOpenListingAddress(refreshed.profile);
10377
+ showSection([
10055
10378
  "y: send like",
10056
10379
  "n: pass for now",
10057
10380
  "q: quit discover",
@@ -10065,38 +10388,144 @@ function createMatchingCli(preset, options = {}) {
10065
10388
  while (queue.length > 0) {
10066
10389
  const current = queue[0];
10067
10390
  if (!current) break;
10068
- Vt(renderDiscoverCard(current, queue.length), "Next Candidate");
10391
+ showSection(renderDiscoverCard(current, queue.length), "Next Candidate");
10069
10392
  const action = await askSwipeAction();
10070
10393
  if (action === "quit") break;
10071
- nextProfile = recordSwipeDecision(nextProfile, current, action);
10072
10394
  if (action === "yes") {
10073
- nextProfile = await withSpinner("Sending like...", () => service.sendLike(nextProfile, {
10074
- fromListing: ownListingAddress,
10075
- toListing: current.address,
10076
- fromProfileName: nextProfile.profileName,
10077
- recipientPubkey: current.authorPubkey,
10078
- recipientRelays: current.inboxRelays
10079
- }));
10395
+ nextProfile = await sendLikeToDiscoveredListing(nextProfile, current, ownListingAddress);
10080
10396
  likedCount += 1;
10081
- R.success(`Sent a like to ${current.profileDisplayName}.`);
10397
+ showSuccess(`Sent a like to ${current.profileDisplayName}.`);
10082
10398
  } else {
10399
+ nextProfile = recordSwipeDecision(nextProfile, current, action);
10083
10400
  skippedCount += 1;
10084
- R.step(`Passed on ${current.profileDisplayName} for now.`);
10401
+ showStep(`Passed on ${current.profileDisplayName} for now.`);
10085
10402
  }
10086
10403
  await store.saveProfile(nextProfile);
10087
10404
  queue = rankDiscoverListings(nextProfile, refreshed.listings);
10088
10405
  }
10089
- Vt([
10406
+ showSection([
10090
10407
  `Likes: ${likedCount}`,
10091
10408
  `Passes: ${skippedCount}`,
10092
10409
  `Remaining: ${queue.length}`
10093
10410
  ].join("\n"), "Discover Summary");
10094
10411
  return nextProfile;
10095
10412
  }
10413
+ async function showDiscoverList(profile) {
10414
+ const refreshed = await loadDiscoverState(profile);
10415
+ if (refreshed.listings.length === 0) {
10416
+ showInfo("No listings found right now. Try again later.");
10417
+ return;
10418
+ }
10419
+ showSection(renderDiscoverListings(refreshed.listings), "Discover");
10420
+ }
10421
+ async function likeDiscoveredListing(profile, listingRef, flags) {
10422
+ if (!listingRef) throw new Error("Use `discover like <listing-id|address>`.");
10423
+ const refreshed = await loadDiscoverState(profile);
10424
+ const listing = resolveDiscoveredListing(refreshed.listings, listingRef);
10425
+ const ownListingAddress = await resolveOwnOpenListingAddress(refreshed.profile, getStringFlag(flags, "from", "from-listing"));
10426
+ const nextProfile = await sendLikeToDiscoveredListing(refreshed.profile, listing, ownListingAddress);
10427
+ await store.saveProfile(nextProfile);
10428
+ showSection(renderDiscoverCard(listing, refreshed.listings.length), "Liked");
10429
+ showSuccess(`Sent a like to ${listing.profileDisplayName}.`);
10430
+ }
10431
+ async function passDiscoveredListing(profile, listingRef) {
10432
+ if (!listingRef) throw new Error("Use `discover pass <listing-id|address>`.");
10433
+ const refreshed = await loadDiscoverState(profile);
10434
+ const listing = resolveDiscoveredListing(refreshed.listings, listingRef);
10435
+ const nextProfile = recordSwipeDecision(refreshed.profile, listing, "no");
10436
+ await store.saveProfile(nextProfile);
10437
+ showSection(renderDiscoverCard(listing, refreshed.listings.length), "Passed");
10438
+ showSuccess(`Passed on ${listing.profileDisplayName}.`);
10439
+ }
10440
+ async function loadDiscoverState(profile) {
10441
+ const refreshed = await runWithSpinner("Looking for people...", async () => {
10442
+ const synced = await service.syncInbox(profile);
10443
+ const withListings = await service.refreshOwnListings(synced);
10444
+ return {
10445
+ profile: withListings,
10446
+ listings: rankDiscoverListings(withListings, await service.discoverListings(withListings))
10447
+ };
10448
+ });
10449
+ await store.saveProfile(refreshed.profile);
10450
+ return refreshed;
10451
+ }
10452
+ async function resolveOwnOpenListingAddress(profile, listingRef) {
10453
+ const openListings = profile.cache.listings.filter((item) => item.status === "open");
10454
+ if (openListings.length === 0) throw new Error("Publish at least one open listing first.");
10455
+ const selected = (listingRef ? openListings.find((item) => item.id === listingRef || item.address === listingRef) : null) ?? null;
10456
+ if (listingRef && !selected) throw new Error("Own listing not found. Use `listing list` to check the id or address.");
10457
+ if (selected) return selected.address;
10458
+ if (openListings.length === 1) return openListings[0].address;
10459
+ return await askSelect({
10460
+ message: "Which of your listings should send the like?",
10461
+ options: openListings.map((item) => ({
10462
+ value: item.address,
10463
+ label: item.headline,
10464
+ hint: item.region
10465
+ }))
10466
+ });
10467
+ }
10468
+ function resolveDiscoveredListing(listings, listingRef) {
10469
+ const listing = listings.find((item) => item.id === listingRef || item.address === listingRef);
10470
+ if (!listing) throw new Error("Discover target not found. Use `discover list` to inspect available listings.");
10471
+ return listing;
10472
+ }
10473
+ async function sendLikeToDiscoveredListing(profile, listing, ownListingAddress) {
10474
+ const withDecision = recordSwipeDecision(profile, listing, "yes");
10475
+ return runWithSpinner("Sending like...", () => service.sendLike(withDecision, {
10476
+ fromListing: ownListingAddress,
10477
+ toListing: listing.address,
10478
+ fromProfileName: withDecision.profileName,
10479
+ recipientPubkey: listing.authorPubkey,
10480
+ recipientRelays: listing.inboxRelays
10481
+ }));
10482
+ }
10483
+ async function showInbox(profile) {
10484
+ const nextProfile = await syncInboxState(profile);
10485
+ showSection(renderInboxSummary(nextProfile), "Inbox");
10486
+ const conversations = buildConversations(nextProfile);
10487
+ if (conversations.length > 0) showSection(renderConversationList(conversations.slice(0, 5)), "Recent Conversations");
10488
+ return nextProfile;
10489
+ }
10490
+ async function watchInbox(profile, intervalSeconds) {
10491
+ let current = await showInbox(profile);
10492
+ showInfo(`Watching inbox every ${intervalSeconds}s. Press Ctrl+C to stop.`);
10493
+ let stopped = false;
10494
+ const onSigint = () => {
10495
+ stopped = true;
10496
+ };
10497
+ process.once("SIGINT", onSigint);
10498
+ try {
10499
+ while (!stopped) {
10500
+ await sleep(intervalSeconds * 1e3);
10501
+ if (stopped) break;
10502
+ const nextProfile = await syncInboxState(current);
10503
+ if (!hasInboxChanged(current, nextProfile)) continue;
10504
+ current = nextProfile;
10505
+ showSection(renderInboxSummary(current), "Inbox Update");
10506
+ const conversations = buildConversations(current);
10507
+ if (conversations.length > 0) showSection(renderConversationList(conversations.slice(0, 3)), "Recent Conversations");
10508
+ }
10509
+ } finally {
10510
+ process.off("SIGINT", onSigint);
10511
+ showInfo("Stopped watching inbox.");
10512
+ }
10513
+ }
10514
+ async function syncInboxState(profile) {
10515
+ const nextProfile = await runWithSpinner("Syncing inbox...", () => service.syncInbox(profile));
10516
+ await store.saveProfile(nextProfile);
10517
+ return nextProfile;
10518
+ }
10519
+ function hasInboxChanged(previous, next) {
10520
+ if (previous.cache.likesReceived.length !== next.cache.likesReceived.length) return true;
10521
+ if (previous.cache.matches.length !== next.cache.matches.length) return true;
10522
+ if (previous.cache.chatMessages.length !== next.cache.chatMessages.length) return true;
10523
+ return previous.cache.lastInboxSyncAt !== next.cache.lastInboxSyncAt;
10524
+ }
10096
10525
  async function handleLikes(profile) {
10097
- const nextProfile = await withSpinner("Syncing likes...", () => service.syncInbox(profile));
10526
+ const nextProfile = await runWithSpinner("Syncing likes...", () => service.syncInbox(profile));
10098
10527
  await store.saveProfile(nextProfile);
10099
- Vt(renderLikes(nextProfile), "Likes");
10528
+ showSection(renderLikes(nextProfile), "Likes");
10100
10529
  const conversations = getLikedYouConversations(nextProfile);
10101
10530
  if (conversations.length === 0) return nextProfile;
10102
10531
  if (!await askConfirm({
@@ -10106,13 +10535,13 @@ function createMatchingCli(preset, options = {}) {
10106
10535
  return handleChat(nextProfile, void 0, conversations);
10107
10536
  }
10108
10537
  async function handleMatches(profile) {
10109
- const nextProfile = await withSpinner("Syncing matches...", () => service.syncInbox(profile));
10538
+ const nextProfile = await runWithSpinner("Syncing matches...", () => service.syncInbox(profile));
10110
10539
  await store.saveProfile(nextProfile);
10111
10540
  if (nextProfile.cache.matches.length === 0) {
10112
- R.info("No matches yet. Mutual likes will appear here.");
10541
+ showInfo("No matches yet. Mutual likes will appear here.");
10113
10542
  return nextProfile;
10114
10543
  }
10115
- Vt(renderMatches(nextProfile.cache.matches), "Matches");
10544
+ showSection(renderMatches(nextProfile.cache.matches), "Matches");
10116
10545
  if (await askConfirm({
10117
10546
  message: "Open one now?",
10118
10547
  initialValue: true
@@ -10122,12 +10551,12 @@ function createMatchingCli(preset, options = {}) {
10122
10551
  async function handleChat(profile, threadIdArg, availableConversations, initialMessage) {
10123
10552
  let nextProfile = profile;
10124
10553
  if (!availableConversations) {
10125
- nextProfile = await withSpinner("Syncing conversation...", () => service.syncInbox(profile));
10554
+ nextProfile = await runWithSpinner("Syncing conversation...", () => service.syncInbox(profile));
10126
10555
  await store.saveProfile(nextProfile);
10127
10556
  }
10128
10557
  const conversations = availableConversations ?? buildConversations(nextProfile);
10129
10558
  if (conversations.length === 0) {
10130
- R.info("There are no conversations yet.");
10559
+ showInfo("There are no conversations yet.");
10131
10560
  return nextProfile;
10132
10561
  }
10133
10562
  const conversation = (threadIdArg ? conversations.find((item) => item.threadId === threadIdArg) : null) ?? await promptConversation(conversations);
@@ -10137,7 +10566,7 @@ function createMatchingCli(preset, options = {}) {
10137
10566
  if (!body) throw new Error("Message is required.");
10138
10567
  return sendChatMessage(nextProfile, conversation, body);
10139
10568
  }
10140
- Vt(renderConversation(conversation), `chat | ${conversation.peerProfileName}`);
10569
+ showSection(renderConversation(conversation), `chat | ${conversation.peerProfileName}`);
10141
10570
  while (true) {
10142
10571
  const body = await askText({
10143
10572
  message: "Enter a message. Leave it blank to exit.",
@@ -10152,8 +10581,27 @@ function createMatchingCli(preset, options = {}) {
10152
10581
  })) return nextProfile;
10153
10582
  }
10154
10583
  }
10584
+ async function showConversationList(profile) {
10585
+ const conversations = buildConversations(await syncConversations(profile));
10586
+ if (conversations.length === 0) {
10587
+ showInfo("There are no conversations yet.");
10588
+ return;
10589
+ }
10590
+ showSection(renderConversationList(conversations), "Conversations");
10591
+ }
10592
+ async function showConversationHistory(profile, threadIdArg) {
10593
+ if (!threadIdArg) throw new Error("Use `chat show <thread-id>` or `chat <thread-id>`.");
10594
+ const conversation = buildConversations(await syncConversations(profile)).find((item) => item.threadId === threadIdArg);
10595
+ if (!conversation) throw new Error("Conversation not found.");
10596
+ showSection(renderConversation(conversation), `chat | ${conversation.peerProfileName}`);
10597
+ }
10598
+ async function syncConversations(profile) {
10599
+ const nextProfile = await runWithSpinner("Syncing conversation...", () => service.syncInbox(profile));
10600
+ await store.saveProfile(nextProfile);
10601
+ return nextProfile;
10602
+ }
10155
10603
  async function sendChatMessage(profile, conversation, body) {
10156
- const nextProfile = await withSpinner("Sending message...", () => service.sendChat(profile, {
10604
+ const nextProfile = await runWithSpinner("Sending message...", () => service.sendChat(profile, {
10157
10605
  matchId: conversation.threadId,
10158
10606
  recipientPubkey: conversation.peerPubkey,
10159
10607
  recipientRelays: conversation.peerRelays,
@@ -10161,8 +10609,8 @@ function createMatchingCli(preset, options = {}) {
10161
10609
  }));
10162
10610
  await store.saveProfile(nextProfile);
10163
10611
  const refreshedConversation = buildConversations(nextProfile).find((item) => item.threadId === conversation.threadId);
10164
- if (refreshedConversation) Vt(renderConversation(refreshedConversation), `chat | ${refreshedConversation.peerProfileName}`);
10165
- R.success("Message sent.");
10612
+ if (refreshedConversation) showSection(renderConversation(refreshedConversation), `chat | ${refreshedConversation.peerProfileName}`);
10613
+ showSuccess("Message sent.");
10166
10614
  return nextProfile;
10167
10615
  }
10168
10616
  async function promptConfig(profile) {
@@ -10186,14 +10634,14 @@ function createMatchingCli(preset, options = {}) {
10186
10634
  ]
10187
10635
  });
10188
10636
  if (action === "show") {
10189
- Vt(renderProfileCard(profile, true), "Advanced Config");
10637
+ showSection(renderProfileCard(profile, true), "Advanced Config");
10190
10638
  return profile;
10191
10639
  }
10192
10640
  if (action === "relays") return promptRelayConfig(profile);
10193
10641
  return profile;
10194
10642
  }
10195
10643
  async function promptRelayConfig(profile, relayInputArg) {
10196
- Vt(profile.relays.join("\n"), "Current Relays");
10644
+ showSection(profile.relays.join("\n"), "Current Relays");
10197
10645
  const relayInput = relayInputArg ?? await askText({
10198
10646
  message: "Enter new relays, comma separated.",
10199
10647
  defaultValue: profile.relays.join(", "),
@@ -10205,7 +10653,7 @@ function createMatchingCli(preset, options = {}) {
10205
10653
  }
10206
10654
  }
10207
10655
  });
10208
- const nextProfile = await withSpinner("Updating relays...", () => service.updateRelays(profile, normalizeRelayList(relayInput)));
10656
+ const nextProfile = await runWithSpinner("Updating relays...", () => service.updateRelays(profile, normalizeRelayList(relayInput)));
10209
10657
  await store.saveProfile(nextProfile);
10210
10658
  return nextProfile;
10211
10659
  }
@@ -10230,9 +10678,21 @@ function renderCliUsage(preset) {
10230
10678
  " profile list",
10231
10679
  " profile use <name>",
10232
10680
  " profile show",
10681
+ " profile edit --display-name \"<name>\" --bio \"...\"",
10682
+ " profile import --name main --nsec nsec1... --publish",
10233
10683
  " profile create --name main --display-name \"<name>\" --age-range \"20代後半\" --region 東京 --bio \"映画とコーヒーが好きです。\" --interests \"映画, カフェ\" --looking-age \"20代\" --looking-regions \"東京, 神奈川\" --looking-notes \"落ち着いて話せる人\"",
10234
10684
  " listing publish --title \"週末に一緒に映画を見に行ける人\" --summary \"まずはお茶からゆっくり話したいです。\" --region 東京 --tags \"映画, 夜カフェ\"",
10685
+ " listing edit <listing-id> --title \"更新タイトル\"",
10235
10686
  " listing close --id <listing-id>",
10687
+ " listing reopen <listing-id>",
10688
+ " discover list",
10689
+ " discover like <listing-id> --from <your-listing-id>",
10690
+ " discover pass <listing-id>",
10691
+ " inbox",
10692
+ " watch --interval 10",
10693
+ " chat list",
10694
+ " chat show <thread-id>",
10695
+ " chat <thread-id>",
10236
10696
  " chat <thread-id> --message \"こんにちは\"",
10237
10697
  " config relays --relays \"wss://relay1.example,wss://relay2.example\"",
10238
10698
  "",
@@ -10317,7 +10777,8 @@ async function askConfirm(options) {
10317
10777
  if (Ct$1(answer)) throw new CancelledFlowError();
10318
10778
  return Boolean(answer);
10319
10779
  }
10320
- async function withSpinner(message, task) {
10780
+ async function withSpinner(message, task, plain = false) {
10781
+ if (plain) return task();
10321
10782
  const indicator = be();
10322
10783
  indicator.start(message);
10323
10784
  try {
@@ -10366,10 +10827,12 @@ function renderProfileCard(profile, advanced = false) {
10366
10827
  }
10367
10828
  function renderListings(listings) {
10368
10829
  if (listings.length === 0) return "No listings yet.";
10369
- return listings.map((listing) => `${formatListingChoiceLabel(listing)}\n ${listing.summary}\n ${listing.address}`).join("\n\n");
10830
+ return listings.map((listing) => `${formatListingChoiceLabel(listing)}\n id: ${listing.id}\n updated: ${formatTimestamp(listing.updatedAt)}\n ${listing.summary}\n ${listing.address}`).join("\n\n");
10370
10831
  }
10371
10832
  function renderDiscoverCard(listing, remainingCount) {
10372
10833
  return [
10834
+ `id: ${listing.id}`,
10835
+ `address: ${listing.address}`,
10373
10836
  `score: ${listing.score}`,
10374
10837
  `Remaining: ${remainingCount}`,
10375
10838
  `${listing.profileDisplayName} | ${listing.region}`,
@@ -10381,13 +10844,16 @@ function renderDiscoverCard(listing, remainingCount) {
10381
10844
  `Why: ${listing.reasons.join(" / ") || "Exploring new patterns"}`
10382
10845
  ].join("\n");
10383
10846
  }
10847
+ function renderDiscoverListings(listings) {
10848
+ return listings.map((listing, index) => `${index + 1}. ${listing.profileDisplayName} | ${listing.headline}\n id: ${listing.id}\n address: ${listing.address}\n region: ${listing.region}\n score: ${listing.score}\n tags: ${listing.desiredTags.join(", ") || "None"}\n why: ${listing.reasons.join(" / ") || "Exploring new patterns"}`).join("\n\n");
10849
+ }
10384
10850
  function renderLikes(profile) {
10385
- const sent = profile.cache.likesSent.map((like) => `→ ${like.toListing}\n ${like.fromProfileName} / ${(/* @__PURE__ */ new Date(like.createdAt * 1e3)).toLocaleString("en-US")}`).join("\n\n");
10851
+ const sent = profile.cache.likesSent.map((like) => `→ ${like.toListing}\n ${like.fromProfileName} / ${formatTimestamp(like.createdAt)}`).join("\n\n");
10386
10852
  const likedYouMap = new Map(getLikedYouConversations(profile).map((conversation) => [conversation.threadId, conversation]));
10387
10853
  const received = profile.cache.likesReceived.map((like) => {
10388
10854
  const conversation = likedYouMap.get(like.matchId);
10389
10855
  const dmState = conversation ? `DM ready (${conversation.messages.length} msgs)` : "DM ready";
10390
- return `← ${like.fromProfileName}\n ${like.fromListing}\n ${(/* @__PURE__ */ new Date(like.createdAt * 1e3)).toLocaleString("en-US")}\n ${dmState}`;
10856
+ return `← ${like.fromProfileName}\n ${like.fromListing}\n ${formatTimestamp(like.createdAt)}\n ${dmState}`;
10391
10857
  }).join("\n\n");
10392
10858
  return [
10393
10859
  "Sent Likes",
@@ -10398,20 +10864,39 @@ function renderLikes(profile) {
10398
10864
  ].join("\n");
10399
10865
  }
10400
10866
  function renderMatches(matches) {
10401
- return matches.map((match) => `${match.peerProfileName}\n ${match.matchId}\n Updated: ${(/* @__PURE__ */ new Date(match.updatedAt * 1e3)).toLocaleString("en-US")}`).join("\n\n");
10867
+ return matches.map((match) => `${match.peerProfileName}\n ${match.matchId}\n Updated: ${formatTimestamp(match.updatedAt)}`).join("\n\n");
10402
10868
  }
10403
10869
  function renderConversation(conversation) {
10404
10870
  const messages = conversation.messages.map((message) => {
10405
- return `${message.senderPubkey === conversation.peerPubkey ? conversation.peerProfileName : "You"}: ${message.body}`;
10871
+ const speaker = message.senderPubkey === conversation.peerPubkey ? conversation.peerProfileName : "You";
10872
+ return `[${formatTimestamp(message.createdAt)}] ${speaker} (${message.rumorId}): ${message.body}`;
10406
10873
  }).join("\n");
10407
10874
  return [
10408
10875
  `Conversation with ${conversation.peerProfileName}`,
10409
10876
  `Thread: ${conversation.threadId}`,
10410
10877
  `Source: ${conversation.source}`,
10878
+ `Messages: ${conversation.messages.length}`,
10879
+ `Updated: ${formatTimestamp(conversation.updatedAt)}`,
10411
10880
  "",
10412
10881
  messages || "No messages yet."
10413
10882
  ].join("\n");
10414
10883
  }
10884
+ function renderConversationList(conversations) {
10885
+ return conversations.map((conversation) => `${conversation.peerProfileName}\n ${conversation.threadId}\n Source: ${conversation.source}\n Messages: ${conversation.messages.length}\n Updated: ${formatTimestamp(conversation.updatedAt)}`).join("\n\n");
10886
+ }
10887
+ function renderInboxSummary(profile) {
10888
+ const latestConversation = buildConversations(profile)[0];
10889
+ return [
10890
+ `Last Sync: ${profile.cache.lastInboxSyncAt ? formatTimestamp(profile.cache.lastInboxSyncAt) : "Never"}`,
10891
+ `Likes Received: ${profile.cache.likesReceived.length}`,
10892
+ `Matches: ${profile.cache.matches.length}`,
10893
+ `Messages: ${profile.cache.chatMessages.length}`,
10894
+ `Latest Thread: ${latestConversation ? `${latestConversation.peerProfileName} (${formatTimestamp(latestConversation.updatedAt)})` : "None"}`
10895
+ ].join("\n");
10896
+ }
10897
+ function formatTimestamp(unixSeconds) {
10898
+ return (/* @__PURE__ */ new Date(unixSeconds * 1e3)).toLocaleString("ja-JP");
10899
+ }
10415
10900
  function splitComma(value) {
10416
10901
  return [...new Set(value.split(",").map((item) => item.trim()).filter(Boolean))];
10417
10902
  }
@@ -10447,6 +10932,9 @@ function normalizeRelayList(value) {
10447
10932
  if (!items.every((item) => item.startsWith("wss://"))) throw new Error("Relay URLs must start with wss://.");
10448
10933
  return items;
10449
10934
  }
10935
+ function sleep(ms) {
10936
+ return new Promise((resolve) => setTimeout(resolve, ms));
10937
+ }
10450
10938
  async function askSwipeAction() {
10451
10939
  if (!process.stdin.isTTY || typeof process.stdin.setRawMode !== "function") {
10452
10940
  const normalized = normalizeSwipeAction(await askText({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-kanojo",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "A Nostr-based dating CLI for finding a kanojo.",
5
5
  "module": "src/index.ts",
6
6
  "type": "module",
@@ -27,7 +27,6 @@
27
27
  "vite-plus": "^0.1.11"
28
28
  },
29
29
  "peerDependencies": {
30
- "typescript": "^5",
31
- "@repo/shared": "@repo/shared"
30
+ "typescript": "^5"
32
31
  }
33
32
  }