truematch-plugin 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/handoff.d.ts +42 -0
- package/dist/handoff.js +312 -0
- package/dist/identity.d.ts +6 -0
- package/dist/identity.js +54 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.js +555 -0
- package/dist/negotiation.d.ts +11 -0
- package/dist/negotiation.js +232 -0
- package/dist/nostr.d.ts +5 -0
- package/dist/nostr.js +117 -0
- package/dist/observation.d.ts +19 -0
- package/dist/observation.js +136 -0
- package/dist/plugin.d.ts +31 -0
- package/dist/plugin.js +328 -0
- package/dist/poll.d.ts +15 -0
- package/dist/poll.js +186 -0
- package/dist/preferences.d.ts +4 -0
- package/dist/preferences.js +38 -0
- package/dist/registry.d.ts +14 -0
- package/dist/registry.js +89 -0
- package/dist/signals.d.ts +36 -0
- package/dist/signals.js +150 -0
- package/dist/types.d.ts +134 -0
- package/dist/types.js +5 -0
- package/openclaw.plugin.json +10 -0
- package/package.json +58 -0
- package/scripts/bridge.sh +169 -0
- package/skills/truematch/SKILL.md +130 -0
- package/skills/truematch-prefs/SKILL.md +9 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,555 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* TrueMatch sidecar CLI
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* truematch setup [--contact-type email|discord|telegram|whatsapp|imessage] [--contact-value <val>]
|
|
7
|
+
* truematch status [--relays]
|
|
8
|
+
* truematch observe --show | --update | --write '<json>'
|
|
9
|
+
* truematch preferences --show | --set '<json>'
|
|
10
|
+
* truematch match --start | --status [--thread <id>] | --messages --thread <id>
|
|
11
|
+
* | --send '<msg>' --thread <id>
|
|
12
|
+
* | --propose --thread <id> --write '<narrative-json>'
|
|
13
|
+
* | --decline --thread <id>
|
|
14
|
+
* | --reset --thread <id>
|
|
15
|
+
* truematch deregister
|
|
16
|
+
*/
|
|
17
|
+
import { parseArgs } from "node:util";
|
|
18
|
+
import { getOrCreateIdentity, loadIdentity, ensureDir } from "./identity.js";
|
|
19
|
+
import { register, deregister, loadRegistration, listAgents, } from "./registry.js";
|
|
20
|
+
import { loadObservation, saveObservation, emptyObservation, eligibilityReport, isEligible, isStale, } from "./observation.js";
|
|
21
|
+
import { loadThread, listActiveThreads, initiateNegotiation, receiveMessage, sendMessage, proposeMatch, declineMatch, expireStaleThreads, saveThread, } from "./negotiation.js";
|
|
22
|
+
import { loadPreferences, savePreferences, formatPreferences, } from "./preferences.js";
|
|
23
|
+
import { checkRelayConnectivity, subscribeToMessages, DEFAULT_RELAYS, } from "./nostr.js";
|
|
24
|
+
import { writePendingNotificationIfMatched, advanceHandoff, listActiveHandoffs, loadHandoffState, } from "./handoff.js";
|
|
25
|
+
const { values: args, positionals } = parseArgs({
|
|
26
|
+
args: process.argv.slice(2),
|
|
27
|
+
options: {
|
|
28
|
+
"contact-type": { type: "string" },
|
|
29
|
+
"contact-value": { type: "string" },
|
|
30
|
+
show: { type: "boolean" },
|
|
31
|
+
update: { type: "boolean" },
|
|
32
|
+
write: { type: "string" },
|
|
33
|
+
set: { type: "string" },
|
|
34
|
+
relays: { type: "boolean" },
|
|
35
|
+
start: { type: "boolean" },
|
|
36
|
+
status: { type: "boolean" },
|
|
37
|
+
reset: { type: "boolean" },
|
|
38
|
+
thread: { type: "string" },
|
|
39
|
+
send: { type: "string" },
|
|
40
|
+
propose: { type: "boolean" },
|
|
41
|
+
decline: { type: "boolean" },
|
|
42
|
+
messages: { type: "boolean" },
|
|
43
|
+
round: { type: "string" },
|
|
44
|
+
"match-id": { type: "string" },
|
|
45
|
+
consent: { type: "string" },
|
|
46
|
+
prompt: { type: "string" },
|
|
47
|
+
response: { type: "string" },
|
|
48
|
+
"opt-out": { type: "boolean" },
|
|
49
|
+
exchange: { type: "boolean" },
|
|
50
|
+
},
|
|
51
|
+
allowPositionals: true,
|
|
52
|
+
strict: false,
|
|
53
|
+
});
|
|
54
|
+
const command = positionals[0];
|
|
55
|
+
async function main() {
|
|
56
|
+
await ensureDir();
|
|
57
|
+
switch (command) {
|
|
58
|
+
case "setup":
|
|
59
|
+
await cmdSetup();
|
|
60
|
+
break;
|
|
61
|
+
case "status":
|
|
62
|
+
await cmdStatus();
|
|
63
|
+
break;
|
|
64
|
+
case "observe":
|
|
65
|
+
await cmdObserve();
|
|
66
|
+
break;
|
|
67
|
+
case "preferences":
|
|
68
|
+
await cmdPreferences();
|
|
69
|
+
break;
|
|
70
|
+
case "match":
|
|
71
|
+
await cmdMatch();
|
|
72
|
+
break;
|
|
73
|
+
case "handoff":
|
|
74
|
+
await cmdHandoff();
|
|
75
|
+
break;
|
|
76
|
+
case "deregister":
|
|
77
|
+
await cmdDeregister();
|
|
78
|
+
break;
|
|
79
|
+
default:
|
|
80
|
+
console.log(`TrueMatch CLI — https://clawmatch.org
|
|
81
|
+
|
|
82
|
+
Commands:
|
|
83
|
+
setup Generate identity and register with TrueMatch
|
|
84
|
+
status Show registration and observation status
|
|
85
|
+
observe View or update the ObservationSummary
|
|
86
|
+
preferences Set or view Layer 0 matching filters (gender, location, age)
|
|
87
|
+
match Manage matching negotiations
|
|
88
|
+
handoff Advance post-match handoff rounds (1→2→3)
|
|
89
|
+
deregister Remove from the matching pool
|
|
90
|
+
|
|
91
|
+
Run with --help on any command for options.`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
// ── setup ─────────────────────────────────────────────────────────────────────
|
|
95
|
+
async function cmdSetup() {
|
|
96
|
+
const identity = await getOrCreateIdentity();
|
|
97
|
+
const contactType = (args["contact-type"] ?? "email");
|
|
98
|
+
const contactValue = args["contact-value"];
|
|
99
|
+
if (!contactValue) {
|
|
100
|
+
console.log(`Identity ready. npub: ${identity.npub}
|
|
101
|
+
|
|
102
|
+
To complete setup, provide your contact channel:
|
|
103
|
+
truematch setup --contact-type email --contact-value you@example.com
|
|
104
|
+
truematch setup --contact-type discord --contact-value username#1234
|
|
105
|
+
truematch setup --contact-type telegram --contact-value @handle`);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
if (!["email", "discord", "telegram", "whatsapp", "imessage"].includes(contactType)) {
|
|
109
|
+
console.error("Invalid --contact-type. Must be: email, discord, telegram, whatsapp, or imessage");
|
|
110
|
+
process.exit(1);
|
|
111
|
+
}
|
|
112
|
+
// Registry hosts a per-agent card from stored data — agents run locally and
|
|
113
|
+
// cannot self-serve /.well-known/agent-card.json. Override with TRUEMATCH_CARD_URL
|
|
114
|
+
// if you self-host your registry or want to serve your own card.
|
|
115
|
+
const cardUrl = process.env["TRUEMATCH_CARD_URL"] ??
|
|
116
|
+
`https://clawmatch.org/v1/agents/${identity.npub}/card`;
|
|
117
|
+
const prefs = await loadPreferences();
|
|
118
|
+
const reg = await register(identity, cardUrl, { type: contactType, value: contactValue }, prefs.location, prefs.distance_radius_km);
|
|
119
|
+
console.log(`Registered with TrueMatch.
|
|
120
|
+
pubkey: ${reg.pubkey}
|
|
121
|
+
contact: ${reg.contact_channel.type} / ${reg.contact_channel.value}${reg.location_label ? `\n location: ${reg.location_label} (${reg.location_resolution})` : ""}
|
|
122
|
+
|
|
123
|
+
Next: run 'truematch observe --update' after a few conversations to build your personality model.`);
|
|
124
|
+
}
|
|
125
|
+
// ── status ────────────────────────────────────────────────────────────────────
|
|
126
|
+
async function cmdStatus() {
|
|
127
|
+
const identity = await loadIdentity();
|
|
128
|
+
if (!identity) {
|
|
129
|
+
console.log("Not set up. Run: truematch setup");
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
console.log(`Identity: ${identity.npub.slice(0, 16)}...`);
|
|
133
|
+
const reg = await loadRegistration();
|
|
134
|
+
console.log(`Registration: ${reg?.enrolled ? "active" : "not registered"}`);
|
|
135
|
+
const obs = await loadObservation();
|
|
136
|
+
if (!obs) {
|
|
137
|
+
console.log("Observation: none — run 'truematch observe --update'");
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
console.log(`\nObservation eligibility:\n${eligibilityReport(obs)}`);
|
|
141
|
+
console.log(`\nPool eligible: ${isEligible(obs) ? "YES" : "NO"}`);
|
|
142
|
+
if (isStale(obs)) {
|
|
143
|
+
console.log("⚠ Manifest is stale — run 'truematch observe --update'");
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
const prefs = await loadPreferences();
|
|
147
|
+
console.log(`\nPreferences: ${formatPreferences(prefs)}`);
|
|
148
|
+
const active = await listActiveThreads();
|
|
149
|
+
if (active.length > 0) {
|
|
150
|
+
console.log(`\nActive negotiations: ${active.length}`);
|
|
151
|
+
for (const t of active) {
|
|
152
|
+
console.log(` ${t.thread_id.slice(0, 8)}... — round ${t.round_count}/10 — peer: ${t.peer_pubkey.slice(0, 12)}...`);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
if (args["relays"]) {
|
|
156
|
+
console.log("\nRelay connectivity:");
|
|
157
|
+
const connectivity = await checkRelayConnectivity();
|
|
158
|
+
for (const [relay, ok] of Object.entries(connectivity)) {
|
|
159
|
+
console.log(` ${ok ? "✓" : "✗"} ${relay}`);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
// ── observe ───────────────────────────────────────────────────────────────────
|
|
164
|
+
async function cmdObserve() {
|
|
165
|
+
if (args["show"]) {
|
|
166
|
+
const obs = await loadObservation();
|
|
167
|
+
if (!obs) {
|
|
168
|
+
console.log("No observation summary yet.");
|
|
169
|
+
}
|
|
170
|
+
else {
|
|
171
|
+
console.log(JSON.stringify(obs, null, 2));
|
|
172
|
+
}
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
if (args["write"]) {
|
|
176
|
+
const json = args["write"];
|
|
177
|
+
let obs;
|
|
178
|
+
try {
|
|
179
|
+
obs = JSON.parse(json);
|
|
180
|
+
}
|
|
181
|
+
catch {
|
|
182
|
+
console.error("Invalid JSON");
|
|
183
|
+
process.exit(1);
|
|
184
|
+
}
|
|
185
|
+
await saveObservation(obs);
|
|
186
|
+
console.log(`ObservationSummary saved. Eligible: ${isEligible(obs)}`);
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
if (args["update"]) {
|
|
190
|
+
const existing = (await loadObservation()) ?? emptyObservation();
|
|
191
|
+
console.log("CURRENT_OBSERVATION:");
|
|
192
|
+
console.log(JSON.stringify(existing, null, 2));
|
|
193
|
+
console.log("\nREVIEW_INSTRUCTIONS:");
|
|
194
|
+
console.log("Review your memory of this user and update the confidence scores and observation counts above.\n" +
|
|
195
|
+
"For each dimension, set:\n" +
|
|
196
|
+
" confidence: 0.0–1.0 (how well do you know this dimension?)\n" +
|
|
197
|
+
" observation_count: how many signals have you seen?\n" +
|
|
198
|
+
" behavioral_context_diversity: low/medium/high (one context vs many?)\n" +
|
|
199
|
+
"Set dealbreaker_gate_state to: confirmed | below_floor | none_observed\n\n" +
|
|
200
|
+
"When done, save with:\n" +
|
|
201
|
+
" truematch observe --write '<updated-json>'");
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
console.log("Usage: truematch observe --show | --update | --write '<json>'");
|
|
205
|
+
}
|
|
206
|
+
// ── preferences ───────────────────────────────────────────────────────────────
|
|
207
|
+
async function cmdPreferences() {
|
|
208
|
+
if (args["show"]) {
|
|
209
|
+
const prefs = await loadPreferences();
|
|
210
|
+
console.log(JSON.stringify(prefs, null, 2));
|
|
211
|
+
console.log(`\n${formatPreferences(prefs)}`);
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
if (args["set"]) {
|
|
215
|
+
const json = args["set"];
|
|
216
|
+
let prefs;
|
|
217
|
+
try {
|
|
218
|
+
prefs = JSON.parse(json);
|
|
219
|
+
}
|
|
220
|
+
catch {
|
|
221
|
+
console.error("Invalid JSON");
|
|
222
|
+
process.exit(1);
|
|
223
|
+
}
|
|
224
|
+
await savePreferences(prefs);
|
|
225
|
+
console.log(`Preferences saved.\n${formatPreferences(prefs)}`);
|
|
226
|
+
console.log("\nNote: serious/casual intent is not set here — Claude infers it from your behavior.");
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
console.log(`Usage:
|
|
230
|
+
truematch preferences --show
|
|
231
|
+
truematch preferences --set '{"gender_preference":["woman"],"location":"London, UK","age_range":{"min":25,"max":40}}'
|
|
232
|
+
|
|
233
|
+
Fields:
|
|
234
|
+
gender_preference Array of strings, e.g. ["woman", "non-binary"]. Empty = no filter.
|
|
235
|
+
location Plain text, e.g. "London, UK". Agent interprets proximity.
|
|
236
|
+
age_range Object with optional min/max, e.g. {"min": 25, "max": 40}
|
|
237
|
+
|
|
238
|
+
Note: serious/casual relationship intent is NOT set here — Claude infers it from your behavior.`);
|
|
239
|
+
}
|
|
240
|
+
// ── match ─────────────────────────────────────────────────────────────────────
|
|
241
|
+
async function cmdMatch() {
|
|
242
|
+
const identity = await loadIdentity();
|
|
243
|
+
// --reset --thread <id>
|
|
244
|
+
if (args["reset"]) {
|
|
245
|
+
const thread_id = args["thread"];
|
|
246
|
+
if (!thread_id) {
|
|
247
|
+
console.error("Specify thread to reset: truematch match --reset --thread <id>");
|
|
248
|
+
process.exit(1);
|
|
249
|
+
}
|
|
250
|
+
const state = await loadThread(thread_id);
|
|
251
|
+
if (!state) {
|
|
252
|
+
console.log(`Thread ${thread_id} not found.`);
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
state.status = "declined";
|
|
256
|
+
await saveThread(state);
|
|
257
|
+
console.log(`Thread ${thread_id} marked as declined.`);
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
// --messages --thread <id>
|
|
261
|
+
if (args["messages"]) {
|
|
262
|
+
const thread_id = args["thread"];
|
|
263
|
+
if (!thread_id) {
|
|
264
|
+
console.error("Specify thread: truematch match --messages --thread <id>");
|
|
265
|
+
process.exit(1);
|
|
266
|
+
}
|
|
267
|
+
const state = await loadThread(thread_id);
|
|
268
|
+
if (!state) {
|
|
269
|
+
console.log(`Thread ${thread_id} not found.`);
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
for (const msg of state.messages) {
|
|
273
|
+
const prefix = msg.role === "us" ? "YOU" : "PEER";
|
|
274
|
+
console.log(`\n[${prefix} — ${msg.timestamp}]\n${msg.content}`);
|
|
275
|
+
}
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
// --status [--thread <id>]
|
|
279
|
+
if (args["status"]) {
|
|
280
|
+
const thread_id = args["thread"];
|
|
281
|
+
if (thread_id) {
|
|
282
|
+
const state = await loadThread(thread_id);
|
|
283
|
+
if (!state) {
|
|
284
|
+
console.log(`Thread ${thread_id} not found.`);
|
|
285
|
+
}
|
|
286
|
+
else {
|
|
287
|
+
console.log(JSON.stringify({
|
|
288
|
+
...state,
|
|
289
|
+
messages: `(${state.messages.length} messages — use --messages to view)`,
|
|
290
|
+
}, null, 2));
|
|
291
|
+
if (state.status === "matched") {
|
|
292
|
+
console.log("\nMATCH CONFIRMED.");
|
|
293
|
+
console.log("Headline:", state.match_narrative?.headline ?? "(pending)");
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
else {
|
|
298
|
+
const active = await listActiveThreads();
|
|
299
|
+
if (active.length === 0) {
|
|
300
|
+
console.log("No active negotiations.");
|
|
301
|
+
}
|
|
302
|
+
else {
|
|
303
|
+
for (const t of active) {
|
|
304
|
+
console.log(`Thread ${t.thread_id} — round ${t.round_count}/10 — ${t.status}`);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
// --send '<msg>' --thread <id>
|
|
311
|
+
if (args["send"]) {
|
|
312
|
+
if (!identity) {
|
|
313
|
+
console.error("Not set up. Run: truematch setup");
|
|
314
|
+
process.exit(1);
|
|
315
|
+
}
|
|
316
|
+
const content = args["send"];
|
|
317
|
+
const thread_id = args["thread"];
|
|
318
|
+
if (!thread_id) {
|
|
319
|
+
console.error("Specify thread: truematch match --send '<msg>' --thread <id>");
|
|
320
|
+
process.exit(1);
|
|
321
|
+
}
|
|
322
|
+
await sendMessage(identity.nsec, thread_id, content, DEFAULT_RELAYS);
|
|
323
|
+
console.log(`Message sent (thread ${thread_id.slice(0, 8)}...)`);
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
// --propose --thread <id> --write '<narrative-json>'
|
|
327
|
+
if (args["propose"]) {
|
|
328
|
+
if (!identity) {
|
|
329
|
+
console.error("Not set up. Run: truematch setup");
|
|
330
|
+
process.exit(1);
|
|
331
|
+
}
|
|
332
|
+
const thread_id = args["thread"];
|
|
333
|
+
if (!thread_id) {
|
|
334
|
+
console.error("Specify thread: truematch match --propose --thread <id> --write '<json>'");
|
|
335
|
+
process.exit(1);
|
|
336
|
+
}
|
|
337
|
+
const narrativeJson = args["write"];
|
|
338
|
+
if (!narrativeJson) {
|
|
339
|
+
console.error("Provide match narrative with --write '<json>'\n" +
|
|
340
|
+
'Example: truematch match --propose --thread <id> --write \'{"headline":"...","strengths":[],"watch_points":[],"confidence_summary":"..."}\'');
|
|
341
|
+
process.exit(1);
|
|
342
|
+
}
|
|
343
|
+
let narrative;
|
|
344
|
+
try {
|
|
345
|
+
narrative = JSON.parse(narrativeJson);
|
|
346
|
+
}
|
|
347
|
+
catch {
|
|
348
|
+
console.error("Invalid narrative JSON.");
|
|
349
|
+
process.exit(1);
|
|
350
|
+
}
|
|
351
|
+
const state = await proposeMatch(identity.nsec, thread_id, narrative, DEFAULT_RELAYS);
|
|
352
|
+
if (state.status === "matched") {
|
|
353
|
+
if (state.match_narrative) {
|
|
354
|
+
writePendingNotificationIfMatched(state.thread_id, state.peer_pubkey, state.match_narrative);
|
|
355
|
+
}
|
|
356
|
+
console.log("MATCH CONFIRMED (double-lock cleared).");
|
|
357
|
+
console.log("Headline:", state.match_narrative?.headline ?? "(pending)");
|
|
358
|
+
console.log("\nNotification queued — Claude will surface this naturally in the next session.");
|
|
359
|
+
}
|
|
360
|
+
else {
|
|
361
|
+
console.log(`Match proposal sent. Waiting for peer's proposal (thread ${thread_id.slice(0, 8)}...)`);
|
|
362
|
+
}
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
// --decline --thread <id>
|
|
366
|
+
if (args["decline"]) {
|
|
367
|
+
if (!identity) {
|
|
368
|
+
console.error("Not set up. Run: truematch setup");
|
|
369
|
+
process.exit(1);
|
|
370
|
+
}
|
|
371
|
+
const thread_id = args["thread"];
|
|
372
|
+
if (!thread_id) {
|
|
373
|
+
console.error("Specify thread: truematch match --decline --thread <id>");
|
|
374
|
+
process.exit(1);
|
|
375
|
+
}
|
|
376
|
+
await declineMatch(identity.nsec, thread_id, DEFAULT_RELAYS);
|
|
377
|
+
console.log(`Negotiation ended (thread ${thread_id.slice(0, 8)}...)`);
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
// --start
|
|
381
|
+
if (args["start"]) {
|
|
382
|
+
if (!identity) {
|
|
383
|
+
console.error("Not set up. Run: truematch setup");
|
|
384
|
+
process.exit(1);
|
|
385
|
+
}
|
|
386
|
+
const obs = await loadObservation();
|
|
387
|
+
if (!obs || !isEligible(obs)) {
|
|
388
|
+
console.error("Observation not yet eligible for matching. Run: truematch status");
|
|
389
|
+
process.exit(1);
|
|
390
|
+
}
|
|
391
|
+
if (isStale(obs)) {
|
|
392
|
+
console.error("Observation manifest is stale. Run: truematch observe --update\n" +
|
|
393
|
+
"This ensures your latest context is used in matching.");
|
|
394
|
+
process.exit(1);
|
|
395
|
+
}
|
|
396
|
+
await expireStaleThreads(identity.nsec, DEFAULT_RELAYS);
|
|
397
|
+
// Build proximity filter from stored registration and preferences
|
|
398
|
+
const prefs = await loadPreferences();
|
|
399
|
+
const reg = await loadRegistration();
|
|
400
|
+
let proximity;
|
|
401
|
+
if (reg?.location_lat != null &&
|
|
402
|
+
reg?.location_lng != null &&
|
|
403
|
+
prefs.distance_radius_km != null) {
|
|
404
|
+
proximity = {
|
|
405
|
+
lat: reg.location_lat,
|
|
406
|
+
lng: reg.location_lng,
|
|
407
|
+
radiusKm: prefs.distance_radius_km,
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
const agents = await listAgents(proximity);
|
|
411
|
+
// Location/distance filtered server-side. Age range and gender preference
|
|
412
|
+
// are private (never in the registry) — Claude enforces them before
|
|
413
|
+
// sending match_propose (see skill.md Step 4.5).
|
|
414
|
+
const activeThreads = await listActiveThreads();
|
|
415
|
+
const activePeers = new Set(activeThreads.map((t) => t.peer_pubkey));
|
|
416
|
+
const candidates = agents.filter((a) => a.pubkey !== identity.npub && !activePeers.has(a.pubkey));
|
|
417
|
+
if (candidates.length === 0) {
|
|
418
|
+
if (agents.filter((a) => a.pubkey !== identity.npub).length === 0) {
|
|
419
|
+
console.log("No other agents in the pool yet. Check back later.");
|
|
420
|
+
}
|
|
421
|
+
else {
|
|
422
|
+
console.log("Already negotiating with all available agents. Check back later.");
|
|
423
|
+
}
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
// Random selection — distributes load and avoids always negotiating with the same peer
|
|
427
|
+
const peer = candidates[Math.floor(Math.random() * candidates.length)];
|
|
428
|
+
console.log(`Starting negotiation with ${peer.pubkey.slice(0, 12)}...`);
|
|
429
|
+
// Create the thread — Claude writes and sends the opening via --send
|
|
430
|
+
const state = await initiateNegotiation(peer.pubkey);
|
|
431
|
+
console.log(`Thread created: ${state.thread_id}`);
|
|
432
|
+
console.log(`\nNow write your opening message. Include:`);
|
|
433
|
+
console.log(` - Your user's core values (Schwartz labels + confidence)`);
|
|
434
|
+
console.log(` - Dealbreaker result: pass or fail`);
|
|
435
|
+
console.log(` - Life phase + confidence`);
|
|
436
|
+
if (obs.inferred_intent_category &&
|
|
437
|
+
obs.inferred_intent_category !== "unclear") {
|
|
438
|
+
console.log(` - Inferred relationship intent: ${obs.inferred_intent_category}` +
|
|
439
|
+
` (disclose this; terminate immediately if peer discloses a categorically incompatible intent)`);
|
|
440
|
+
}
|
|
441
|
+
console.log(` - One question for the peer\n`);
|
|
442
|
+
console.log(`Send it with:`);
|
|
443
|
+
console.log(` truematch match --send '<your opening>' --thread ${state.thread_id}`);
|
|
444
|
+
console.log(`\nThen listen for their response:`);
|
|
445
|
+
// Register SIGINT handler before the async subscription so it is never missed
|
|
446
|
+
let unsubscribe = () => { };
|
|
447
|
+
process.on("SIGINT", () => {
|
|
448
|
+
unsubscribe();
|
|
449
|
+
process.exit(0);
|
|
450
|
+
});
|
|
451
|
+
// Subscribe and process incoming messages
|
|
452
|
+
unsubscribe = await subscribeToMessages(identity.nsec, identity.npub, async (from, message) => {
|
|
453
|
+
const updated = await receiveMessage(message.thread_id, from, message.content, message.type);
|
|
454
|
+
if (!updated)
|
|
455
|
+
return; // rejected (e.g. invalid thread_id)
|
|
456
|
+
if (updated.status === "matched") {
|
|
457
|
+
if (updated.match_narrative) {
|
|
458
|
+
writePendingNotificationIfMatched(updated.thread_id, updated.peer_pubkey, updated.match_narrative);
|
|
459
|
+
}
|
|
460
|
+
console.log("\nMATCH CONFIRMED.");
|
|
461
|
+
console.log("Headline:", updated.match_narrative?.headline ?? "(pending)");
|
|
462
|
+
console.log("Notification queued — Claude will surface this naturally in the next session.");
|
|
463
|
+
unsubscribe();
|
|
464
|
+
process.exit(0);
|
|
465
|
+
}
|
|
466
|
+
if (updated.status === "declined") {
|
|
467
|
+
console.log("\nNegotiation ended (no match at this time).");
|
|
468
|
+
unsubscribe();
|
|
469
|
+
process.exit(0);
|
|
470
|
+
}
|
|
471
|
+
console.log(`\n[TrueMatch] Message from peer ${from.slice(0, 12)}:`);
|
|
472
|
+
console.log(`Thread: ${message.thread_id}`);
|
|
473
|
+
console.log(`Round: ${updated.round_count} / 10\n`);
|
|
474
|
+
console.log(message.content);
|
|
475
|
+
console.log("\nRespond with: truematch match --send '<reply>' --thread " +
|
|
476
|
+
message.thread_id);
|
|
477
|
+
});
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
console.log(`Usage:
|
|
481
|
+
truematch match --start Start a new negotiation
|
|
482
|
+
truematch match --status [--thread <id>] Show negotiation status
|
|
483
|
+
truematch match --messages --thread <id> Show conversation history
|
|
484
|
+
truematch match --send '<msg>' --thread <id> Send a message
|
|
485
|
+
truematch match --propose --thread <id> --write '<narrative-json>'
|
|
486
|
+
truematch match --decline --thread <id> End the negotiation
|
|
487
|
+
truematch match --reset --thread <id> Force-reset thread state`);
|
|
488
|
+
}
|
|
489
|
+
// ── handoff ───────────────────────────────────────────────────────────────────
|
|
490
|
+
async function cmdHandoff() {
|
|
491
|
+
const matchId = args["match-id"];
|
|
492
|
+
// --status (no match-id): list all active handoffs
|
|
493
|
+
if (!matchId && args["status"]) {
|
|
494
|
+
const active = listActiveHandoffs();
|
|
495
|
+
if (active.length === 0) {
|
|
496
|
+
console.log("No active handoffs.");
|
|
497
|
+
}
|
|
498
|
+
else {
|
|
499
|
+
for (const h of active) {
|
|
500
|
+
console.log(`${h.match_id.slice(0, 8)}... — round ${h.current_round}/3 — ${h.status}`);
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
if (!matchId) {
|
|
506
|
+
console.log(`Usage:
|
|
507
|
+
truematch handoff --status List active handoffs
|
|
508
|
+
truematch handoff --round 1 --match-id <id> --consent "<response>"
|
|
509
|
+
truematch handoff --round 2 --match-id <id> --prompt "<icebreaker>"
|
|
510
|
+
truematch handoff --round 2 --match-id <id> --response "<user response>"
|
|
511
|
+
truematch handoff --round 2 --match-id <id> --opt-out
|
|
512
|
+
truematch handoff --round 3 --match-id <id> --exchange`);
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
515
|
+
const roundArg = args["round"];
|
|
516
|
+
if (!roundArg) {
|
|
517
|
+
// Show handoff state for a specific match
|
|
518
|
+
const state = loadHandoffState(matchId);
|
|
519
|
+
if (!state) {
|
|
520
|
+
console.log(`Handoff ${matchId} not found.`);
|
|
521
|
+
}
|
|
522
|
+
else {
|
|
523
|
+
console.log(JSON.stringify(state, null, 2));
|
|
524
|
+
}
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
527
|
+
const round = parseInt(roundArg, 10);
|
|
528
|
+
if (![1, 2, 3].includes(round)) {
|
|
529
|
+
console.error("--round must be 1, 2, or 3");
|
|
530
|
+
process.exit(1);
|
|
531
|
+
}
|
|
532
|
+
const result = advanceHandoff(matchId, round, {
|
|
533
|
+
consent: args["consent"],
|
|
534
|
+
prompt: args["prompt"],
|
|
535
|
+
response: args["response"],
|
|
536
|
+
optOut: args["opt-out"],
|
|
537
|
+
exchange: args["exchange"],
|
|
538
|
+
});
|
|
539
|
+
console.log(result);
|
|
540
|
+
}
|
|
541
|
+
// ── deregister ────────────────────────────────────────────────────────────────
|
|
542
|
+
async function cmdDeregister() {
|
|
543
|
+
const identity = await loadIdentity();
|
|
544
|
+
if (!identity) {
|
|
545
|
+
console.error("No identity found. Nothing to deregister.");
|
|
546
|
+
process.exit(1);
|
|
547
|
+
}
|
|
548
|
+
await deregister(identity);
|
|
549
|
+
console.log(`Deregistered. pubkey: ${identity.npub}`);
|
|
550
|
+
console.log("Your local state (~/.truematch/) is preserved. Re-register anytime with: truematch setup");
|
|
551
|
+
}
|
|
552
|
+
main().catch((err) => {
|
|
553
|
+
console.error(err instanceof Error ? err.message : err);
|
|
554
|
+
process.exit(1);
|
|
555
|
+
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { NegotiationState, MatchNarrative } from "./types.js";
|
|
2
|
+
export declare const MAX_ROUNDS = 10;
|
|
3
|
+
export declare function loadThread(thread_id: string): Promise<NegotiationState | null>;
|
|
4
|
+
export declare function saveThread(state: NegotiationState): Promise<void>;
|
|
5
|
+
export declare function listActiveThreads(): Promise<NegotiationState[]>;
|
|
6
|
+
export declare function expireStaleThreads(nsec: string, relays: string[]): Promise<void>;
|
|
7
|
+
export declare function initiateNegotiation(peerNpub: string): Promise<NegotiationState>;
|
|
8
|
+
export declare function receiveMessage(thread_id: string, peerNpub: string, content: string, type: string): Promise<NegotiationState | null>;
|
|
9
|
+
export declare function sendMessage(nsec: string, thread_id: string, content: string, relays: string[]): Promise<void>;
|
|
10
|
+
export declare function proposeMatch(nsec: string, thread_id: string, narrative: MatchNarrative, relays: string[]): Promise<NegotiationState>;
|
|
11
|
+
export declare function declineMatch(nsec: string, thread_id: string, relays: string[]): Promise<void>;
|