create-kanojo 0.1.1 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.mjs +600 -112
- package/package.json +3 -3
package/dist/index.mjs
CHANGED
|
@@ -9318,21 +9318,31 @@ function createNostrService(options = {}) {
|
|
|
9318
9318
|
};
|
|
9319
9319
|
}
|
|
9320
9320
|
async function closeListing(profile, listingId) {
|
|
9321
|
-
|
|
9322
|
-
|
|
9323
|
-
|
|
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
|
-
|
|
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,
|
|
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: [
|
|
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
|
-
|
|
9656
|
+
showText(renderCliUsage(preset));
|
|
9637
9657
|
return;
|
|
9638
9658
|
}
|
|
9639
9659
|
await dispatchCommand(command, parsed.profileName);
|
|
9640
|
-
|
|
9660
|
+
showOutro("Connected quietly. Ready for the next good match.");
|
|
9641
9661
|
} catch (error) {
|
|
9642
9662
|
if (error instanceof CancelledFlowError) {
|
|
9643
|
-
|
|
9663
|
+
showCancelled("Operation cancelled.");
|
|
9644
9664
|
return;
|
|
9645
9665
|
}
|
|
9646
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
9893
|
+
showInfo("No profiles yet. Start with `profile create`.");
|
|
9795
9894
|
return;
|
|
9796
9895
|
}
|
|
9797
|
-
|
|
9896
|
+
showSection(profiles.map((name) => `${name === active ? "●" : "○"} ${name}`).join("\n"), "Profiles");
|
|
9798
9897
|
return;
|
|
9799
9898
|
}
|
|
9800
9899
|
if (subcommand === "show") {
|
|
9801
|
-
|
|
9900
|
+
showSection(renderProfileCard(await ensureProfile(profileOverride), true), "Profile Details");
|
|
9802
9901
|
return;
|
|
9803
9902
|
}
|
|
9804
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
9954
|
+
const nextProfile = await runWithSpinner("Closing listing...", () => service.closeListing(refreshed, listingId));
|
|
9843
9955
|
await store.saveProfile(nextProfile);
|
|
9844
|
-
|
|
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
|
-
|
|
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
|
|
9850
|
-
|
|
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,
|
|
9859
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
9894
|
-
|
|
9895
|
-
|
|
9896
|
-
|
|
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
|
-
|
|
9899
|
-
|
|
9900
|
-
|
|
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
|
-
|
|
9948
|
-
|
|
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
|
-
|
|
9962
|
-
|
|
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
|
|
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(
|
|
9969
|
-
await store.setActiveProfile(preset.brand,
|
|
9970
|
-
|
|
9971
|
-
return
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
10301
|
+
showSuccess("Listing published.");
|
|
10025
10302
|
return nextProfile;
|
|
10026
10303
|
}
|
|
10027
|
-
async function
|
|
10028
|
-
const
|
|
10029
|
-
|
|
10030
|
-
|
|
10031
|
-
|
|
10032
|
-
|
|
10033
|
-
|
|
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
|
|
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
|
-
|
|
10373
|
+
showInfo("No listings found right now. Try again later.");
|
|
10039
10374
|
return refreshed.profile;
|
|
10040
10375
|
}
|
|
10041
|
-
const
|
|
10042
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
10397
|
+
showSuccess(`Sent a like to ${current.profileDisplayName}.`);
|
|
10082
10398
|
} else {
|
|
10399
|
+
nextProfile = recordSwipeDecision(nextProfile, current, action);
|
|
10083
10400
|
skippedCount += 1;
|
|
10084
|
-
|
|
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
|
-
|
|
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
|
|
10526
|
+
const nextProfile = await runWithSpinner("Syncing likes...", () => service.syncInbox(profile));
|
|
10098
10527
|
await store.saveProfile(nextProfile);
|
|
10099
|
-
|
|
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
|
|
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
|
-
|
|
10541
|
+
showInfo("No matches yet. Mutual likes will appear here.");
|
|
10113
10542
|
return nextProfile;
|
|
10114
10543
|
}
|
|
10115
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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)
|
|
10165
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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} / ${(
|
|
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 ${(
|
|
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: ${(
|
|
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
|
-
|
|
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.
|
|
3
|
+
"version": "0.1.2",
|
|
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,7 @@
|
|
|
27
27
|
"vite-plus": "^0.1.11"
|
|
28
28
|
},
|
|
29
29
|
"peerDependencies": {
|
|
30
|
-
"
|
|
31
|
-
"
|
|
30
|
+
"@repo/shared": "workspace:*",
|
|
31
|
+
"typescript": "^5"
|
|
32
32
|
}
|
|
33
33
|
}
|