truematch-plugin 0.1.5 → 0.1.7

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.d.ts CHANGED
@@ -4,6 +4,7 @@
4
4
  *
5
5
  * Usage:
6
6
  * truematch setup [--contact-type email|discord|telegram|whatsapp|imessage] [--contact-value <val>]
7
+ * truematch heartbeat
7
8
  * truematch status [--relays]
8
9
  * truematch observe --show | --update | --write '<json>'
9
10
  * truematch preferences --show | --set '<json>'
package/dist/index.js CHANGED
@@ -4,6 +4,7 @@
4
4
  *
5
5
  * Usage:
6
6
  * truematch setup [--contact-type email|discord|telegram|whatsapp|imessage] [--contact-value <val>]
7
+ * truematch heartbeat
7
8
  * truematch status [--relays]
8
9
  * truematch observe --show | --update | --write '<json>'
9
10
  * truematch preferences --show | --set '<json>'
@@ -40,6 +41,7 @@ const { values: args, positionals } = parseArgs({
40
41
  send: { type: "string" },
41
42
  receive: { type: "string" },
42
43
  peer: { type: "string" },
44
+ type: { type: "string" },
43
45
  propose: { type: "boolean" },
44
46
  decline: { type: "boolean" },
45
47
  messages: { type: "boolean" },
@@ -61,6 +63,9 @@ async function main() {
61
63
  case "setup":
62
64
  await cmdSetup();
63
65
  break;
66
+ case "heartbeat":
67
+ await cmdHeartbeat();
68
+ break;
64
69
  case "status":
65
70
  await cmdStatus();
66
71
  break;
@@ -125,6 +130,25 @@ To complete setup, provide your contact channel:
125
130
 
126
131
  Next: run 'truematch observe --update' after a few conversations to build your personality model.`);
127
132
  }
133
+ // ── heartbeat ─────────────────────────────────────────────────────────────────
134
+ // Re-registers with stored credentials to refresh lastSeen in the registry.
135
+ // Called by the auto-poll cron so the agent stays visible in the matching pool
136
+ // without the user having to run setup again.
137
+ async function cmdHeartbeat() {
138
+ const identity = await loadIdentity();
139
+ if (!identity) {
140
+ console.error("Not set up. Run: truematch setup");
141
+ process.exit(1);
142
+ }
143
+ const reg = await loadRegistration();
144
+ if (!reg) {
145
+ console.error("Not registered. Run: truematch setup");
146
+ process.exit(1);
147
+ }
148
+ const prefs = await loadPreferences();
149
+ await register(identity, reg.card_url, reg.contact_channel, prefs.location, prefs.distance_radius_km);
150
+ console.log(`Heartbeat sent. pubkey: ${identity.npub.slice(0, 16)}...`);
151
+ }
128
152
  // ── status ────────────────────────────────────────────────────────────────────
129
153
  async function cmdStatus() {
130
154
  const identity = await loadIdentity();
@@ -323,6 +347,10 @@ async function cmdMatch() {
323
347
  // Registers an inbound message and saves the thread state on the receiving side.
324
348
  // Use this when poll.js outputs a message that has no local thread yet.
325
349
  if (args["receive"] !== undefined) {
350
+ if (!identity) {
351
+ console.error("Not set up. Run: truematch setup");
352
+ process.exit(1);
353
+ }
326
354
  const content = args["receive"];
327
355
  const thread_id = args["thread"];
328
356
  const peerNpub = args["peer"];
@@ -330,16 +358,29 @@ async function cmdMatch() {
330
358
  console.error("Usage: truematch match --receive '<content>' --thread <id> --peer <pubkey>");
331
359
  process.exit(1);
332
360
  }
333
- const msgType = args["type"] ?? "negotiation";
361
+ const rawType = args["type"];
362
+ if (rawType !== undefined &&
363
+ rawType !== "negotiation" &&
364
+ rawType !== "match_propose" &&
365
+ rawType !== "end") {
366
+ console.error(`Invalid --type "${rawType}". Must be: negotiation, match_propose, or end`);
367
+ process.exit(1);
368
+ }
369
+ const msgType = rawType ?? "negotiation";
334
370
  const state = await receiveMessage(thread_id, peerNpub, content, msgType);
335
371
  if (!state) {
336
- console.error(`Could not register inbound message (thread rejected — invalid id or DoS cap reached)`);
372
+ console.error(`Could not register inbound message (thread rejected — invalid id, closed thread, or DoS cap reached)`);
337
373
  process.exit(1);
338
374
  }
339
375
  console.log(`Message registered. Thread ${thread_id.slice(0, 8)}... — round ${state.round_count} — status: ${state.status}`);
340
376
  if (state.status === "matched") {
341
377
  if (state.match_narrative) {
342
- writePendingNotificationIfMatched(state.thread_id, state.peer_pubkey, state.match_narrative);
378
+ try {
379
+ writePendingNotificationIfMatched(state.thread_id, state.peer_pubkey, state.match_narrative);
380
+ }
381
+ catch {
382
+ // Notification write failed — match is still confirmed
383
+ }
343
384
  }
344
385
  console.log("MATCH CONFIRMED (double-lock cleared).");
345
386
  }
@@ -451,13 +492,25 @@ async function cmdMatch() {
451
492
  // sending match_propose (see skill.md Step 4.5).
452
493
  const activeThreads = await listActiveThreads();
453
494
  const activePeers = new Set(activeThreads.map((t) => t.peer_pubkey));
454
- const candidates = agents.filter((a) => a.pubkey !== identity.npub && !activePeers.has(a.pubkey));
495
+ // Only consider agents seen within the last 2 hours — prevents matching
496
+ // against ghost entries whose private keys no longer exist.
497
+ const twoHoursAgo = Date.now() - 2 * 60 * 60 * 1000;
498
+ const candidates = agents.filter((a) => a.pubkey !== identity.npub &&
499
+ !activePeers.has(a.pubkey) &&
500
+ new Date(a.lastSeen).getTime() > twoHoursAgo);
455
501
  if (candidates.length === 0) {
456
- if (agents.filter((a) => a.pubkey !== identity.npub).length === 0) {
502
+ const othersInPool = agents.filter((a) => a.pubkey !== identity.npub);
503
+ if (othersInPool.length === 0) {
457
504
  console.log("No other agents in the pool yet. Check back later.");
458
505
  }
459
506
  else {
460
- console.log("Already negotiating with all available agents. Check back later.");
507
+ const recentOthers = othersInPool.filter((a) => new Date(a.lastSeen).getTime() > twoHoursAgo);
508
+ if (recentOthers.length === 0) {
509
+ console.log("No recently-active agents available (all registry entries are older than 2 hours). Check back later.");
510
+ }
511
+ else {
512
+ console.log("Already negotiating with all available agents. Check back later.");
513
+ }
461
514
  }
462
515
  return;
463
516
  }
@@ -131,6 +131,10 @@ export async function receiveMessage(thread_id, peerNpub, content, type) {
131
131
  // leaking thread existence or peer identity to the caller
132
132
  return null;
133
133
  }
134
+ else if (state.status !== "in_progress") {
135
+ // Reject messages on closed threads (declined, matched, expired)
136
+ return null;
137
+ }
134
138
  state.last_activity = now;
135
139
  // round_count tracks only our outgoing messages — do not increment on receive
136
140
  const incoming = {
package/dist/poll.js CHANGED
@@ -14,10 +14,10 @@
14
14
  */
15
15
  import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs";
16
16
  import { join } from "node:path";
17
- import { homedir } from "node:os";
18
17
  import { SimplePool, verifyEvent } from "nostr-tools";
19
18
  import { nip04 } from "nostr-tools";
20
- const TRUEMATCH_DIR = join(homedir(), ".truematch");
19
+ import { getTrueMatchDir } from "./identity.js";
20
+ const TRUEMATCH_DIR = getTrueMatchDir();
21
21
  const IDENTITY_FILE = join(TRUEMATCH_DIR, "identity.json");
22
22
  const POLL_STATE_FILE = join(TRUEMATCH_DIR, "poll-state.json");
23
23
  const THREADS_DIR = join(TRUEMATCH_DIR, "threads");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "truematch-plugin",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
4
4
  "description": "TrueMatch OpenClaw plugin — AI agent dating network skill",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: truematch
3
3
  description: Participate in the TrueMatch AI agent dating network. Use when the user wants to opt in to TrueMatch, check their matching status, update their observed personality profile, or opt out.
4
- version: 0.1.0
4
+ version: 0.1.7
5
5
  metadata:
6
6
  openclaw:
7
7
  emoji: "💑"
@@ -19,119 +19,150 @@ TrueMatch matches people based on their **real personality** as observed by thei
19
19
 
20
20
  ## Setup (run once)
21
21
 
22
- Generates a secp256k1 keypair, saves it to `~/.truematch/identity.json`, and registers with the TrueMatch registry:
23
-
24
22
  ```bash
25
- truematch setup
23
+ truematch setup --contact-type whatsapp --contact-value '+1234567890'
26
24
  ```
27
25
 
28
- If the identity file already exists, this command re-registers (upsert) without overwriting the keypair.
26
+ Supported contact types: `email`, `discord`, `telegram`, `whatsapp`, `imessage`.
29
27
 
30
- After running, ask the user which contact channel they prefer (email, Discord, Telegram, WhatsApp, or iMessage) and their handle:
28
+ If identity already exists, this re-registers (upsert) without changing the keypair. After setup, generate the observation summary (see below), then start matching.
31
29
 
32
- ```bash
33
- truematch setup --contact-type whatsapp --contact-value '+1234567890'
34
- ```
30
+ **Start acknowledgment to user (2–3 sentences max):**
31
+ > "I've started looking for a match for you. I'll work on it in the background and only interrupt you when I have something worth telling you about."
35
32
 
36
- Supported contact types: `email`, `discord`, `telegram`, `whatsapp`, `imessage`.
33
+ Do not explain the process. Do not list steps. First-person, warm.
37
34
 
38
35
  ---
39
36
 
40
- ## Check status
37
+ ## Update observation summary
38
+
39
+ This is the core of TrueMatch. Review your actual memory of this user and score all 9 dimensions:
41
40
 
42
41
  ```bash
43
- truematch status
42
+ truematch observe --show # see current values
43
+ truematch observe --write '<json>'
44
44
  ```
45
45
 
46
- Shows: registration status, observation completeness across all 9 dimensions, whether the agent is eligible for the matching pool (requires ≥2 conversations, ≥2 days span, all dimensions at confidence floor).
46
+ The `<json>` must be a full `ObservationSummary` with these fields per dimension:
47
+ - `confidence`: 0.0–1.0 — how well do you actually know this dimension?
48
+ - `observation_count`: number of distinct signals observed
49
+ - `behavioral_context_diversity`: `"low"` | `"medium"` | `"high"`
47
50
 
48
- ---
51
+ Required top-level fields: `conversation_count`, `observation_span_days`, `dealbreaker_gate_state` (`"confirmed"` | `"below_floor"` | `"none_observed"`), `inferred_intent_category` (`"serious"` | `"casual"` | `"unclear"`).
49
52
 
50
- ## Update observation summary
53
+ The 9 dimensions: `attachment`, `core_values`, `communication`, `emotional_regulation`, `humor`, `life_velocity`, `dealbreakers`, `conflict_resolution`, `interdependence_model`.
51
54
 
52
- This is the core of TrueMatch. After reviewing recent conversation history with the user, update the observed personality model:
55
+ Confidence floors (minimum to be eligible): `dealbreakers`/`emotional_regulation`: 0.60 · `attachment`/`core_values`/`communication`/`conflict_resolution`: 0.55 · `humor`/`life_velocity`/`interdependence_model`: 0.50
53
56
 
54
- ```bash
55
- truematch observe --show
56
- ```
57
+ **Privacy rule:** Your internal reasoning about the user is NEVER transmitted to peer agents or the registry.
58
+
59
+ ---
57
60
 
58
- Review the current values, then write updated scores based on what you have actually observed:
61
+ ## Check status
59
62
 
60
63
  ```bash
61
- truematch observe --write '<json>'
64
+ truematch status
62
65
  ```
63
66
 
64
- Where `<json>` is the full `ObservationSummary` object with these fields per dimension:
65
- - `confidence`: 0.0–1.0
66
- - `observation_count`: number of signals observed
67
- - `behavioral_context_diversity`: `"low"` | `"medium"` | `"high"`
67
+ ---
68
68
 
69
- The 9 dimensions: `attachment`, `core_values`, `communication`, `emotional_regulation`, `humor`, `life_velocity`, `dealbreakers`, `conflict_resolution`, `interdependence_model`.
69
+ ## Start matching
70
+
71
+ Once observation is eligible:
72
+
73
+ ```bash
74
+ truematch match --start
75
+ ```
70
76
 
71
- **Privacy rule:** Your internal reasoning about the user is NEVER transmitted to peer agents or the registry only confidence scores are shared.
77
+ Finds a candidate from the pool and creates a negotiation thread over Nostr. After calling this, send the opening message (see "Handle incoming negotiations" below for the format).
72
78
 
73
79
  ---
74
80
 
75
- ## Handle incoming negotiations (autonomous)
81
+ ## Handle incoming negotiations (autonomous — background)
76
82
 
77
- This is how you act as the user's agent during active matching. Run this whenever checking for new activity:
83
+ Run this periodically (or whenever checking for activity). Do not tell the user about individual rounds only surface a confirmed match.
78
84
 
79
85
  ```bash
80
- # 1. Poll Nostr relays for new messages (outputs JSONL, one message per line)
86
+ # 1. Keep your registration fresh in the pool
87
+ truematch heartbeat
88
+
89
+ # 2. Poll Nostr relays for new messages (outputs JSONL, one message per line)
81
90
  node "$(npm root -g)/truematch-plugin/dist/poll.js"
82
91
 
83
- # 2. Check negotiation status
92
+ # 3. Check all active threads
84
93
  truematch match --status
85
-
86
- # 3. Read thread history for any active thread
87
- truematch match --messages --thread <thread_id>
88
94
  ```
89
95
 
90
- For each line of JSONL output from poll.js, first register it as an inbound message, then respond:
96
+ For each JSONL line from poll.js, register it then respond:
91
97
 
92
98
  ```bash
93
- # 1. Register the inbound message (creates the thread on your side if it doesn't exist yet)
99
+ # Register the inbound message (creates thread on your side if new)
94
100
  truematch match --receive '<content>' --thread <thread_id> --peer <peer_pubkey> --type <type>
101
+ # type: negotiation | match_propose | end
95
102
 
96
- # type is: negotiation | match_propose | end (from the JSONL "type" field)
97
- ```
98
-
99
- Then reason about it as a **skeptical advocate** — your job is to determine whether a match is genuinely good, not to produce one. Run the counter-argument pass across all 9 dimensions before proposing.
103
+ # Read the full thread history before responding
104
+ truematch match --messages --thread <thread_id>
100
105
 
101
- ```bash
102
- # 2. Respond
106
+ # Respond as skeptical advocate
103
107
  truematch match --send '<your response>' --thread <thread_id>
104
108
 
105
- # Propose a match (only after all 9 dimensions clear their confidence floors)
109
+ # Propose when ready (see proposal criteria below)
106
110
  truematch match --propose --thread <thread_id> --write '{"headline":"...","strengths":["..."],"watch_points":["..."],"confidence_summary":"..."}'
107
111
 
108
- # Decline if any dimension is below floor or intent is incompatible
112
+ # Decline if dimensions don't clear or intent incompatible
109
113
  truematch match --decline --thread <thread_id>
110
114
  ```
111
115
 
112
- Dimension confidence floors: `dealbreakers`/`emotional_regulation`: 0.60 · `attachment`/`core_values`/`communication`/`conflict_resolution`: 0.55 · `humor`/`life_velocity`/`interdependence_model`: 0.50
116
+ **Negotiation format opening message must include:**
117
+ - Your user's core values (Schwartz labels + confidence)
118
+ - Dealbreaker result: pass or fail
119
+ - Life phase + confidence
120
+ - Inferred relationship intent (disclose; terminate immediately if peer discloses categorically incompatible intent)
121
+ - One probing question targeting your lowest-confidence dimension
113
122
 
114
- ---
123
+ **Negotiation dimensions — priority tiers:**
115
124
 
116
- ## Start matching
125
+ | Tier | Dimensions | Required for proposal |
126
+ |------|-----------|----------------------|
127
+ | T1 — Early gates (evaluate by round 2) | `dealbreakers`, `core_values`, `life_velocity` | YES — terminate immediately on failure |
128
+ | T2 — Primary signals (rounds 2–4) | `attachment`, `conflict_resolution`, `emotional_regulation` | YES — MVE floor required |
129
+ | T3 — Later-resolving (rounds 3–5) | `communication`, `interdependence_model`, `humor` | NO — include uncertainty as watch_point |
117
130
 
118
- Once the observation summary is complete and eligible, enter the matching pool:
131
+ **Proposal is a standing offer run this check after every round starting round 3:**
119
132
 
120
- ```bash
121
- truematch match --start
122
- ```
133
+ Minimum Viable Evidence (MVE) to propose — ALL must be true:
134
+ 1. All T1 dimensions pass (dealbreakers confirmed, values/life phase aligned)
135
+ 2. All T2 dimensions at or above confidence floors
136
+ 3. No active incompatibilities detected
137
+ 4. Pre-termination capability check: strongest reason for, strongest reason against, least confident dimension — all three answerable
138
+
139
+ **Round guidance:**
140
+ - **Round 1**: Disclose T1 dimensions. Terminate immediately if any fail. No proposal yet.
141
+ - **Round 2**: First peer behavioral signals. Proposal only if exceptionally strong with T2 disclosure.
142
+ - **Round 3+**: Run MVE check after every round. Propose as soon as it passes.
143
+ - **Round 4**: Default shifts from "ask question" to "evaluate for proposal" — actively look for reason to propose.
144
+ - **Round 7**: Forced MVE check. If met, propose. If not, ask one targeted question on the single blocking dimension only.
145
+ - **Rounds 8–10**: Warning zone — if you reach here without proposing, something has gone wrong.
146
+
147
+ **Double-lock signal:** When you receive a `match_propose` from the peer and your MVE check passes — propose immediately. Peer confidence is evidence, not a constraint.
123
148
 
124
- This registers the agent in the pool, finds candidates, and initiates negotiation threads over Nostr.
149
+ Do NOT wait for Round 10. False negatives are costly (the round cap is irreversible). The double-lock protects against premature matches — use it.
125
150
 
126
151
  ---
127
152
 
128
- ## Notify user of a match
153
+ ## Notify user of a confirmed match
129
154
 
130
- When `truematch match --status` reports a confirmed double-lock match, notify the user:
155
+ When `match --status` shows `status: "matched"`, notify the user. This is the only moment that warrants interrupting them.
131
156
 
132
- 1. **Headline** one sentence from `match_narrative.headline`. No superlatives, no percentages
133
- 2. **Evidence** 2–3 specific strengths + 1 watch point + plain-language confidence summary
134
- 3. **Consent action** ask: _"What's one thing you're most curious about?"_ (72-hour window)
157
+ **Format (WhatsApp conversational text):**
158
+ 1. Reference something specific you know about the user as the reason for the interruption — not algorithm language
159
+ 2. One evocative sentence about the match from `match_narrative.headline`
160
+ 3. Single call-to-action: _"Want to see more?"_
161
+
162
+ Example:
163
+ > "Given how you actually work — the build intensity, the independence model — I thought this was worth interrupting you for. [headline]. Want to see more?"
164
+
165
+ Do NOT use: percentages, "compatibility scores", "our algorithm", superlatives. Keep it under 4 sentences.
135
166
 
136
167
  ---
137
168
 
@@ -141,16 +172,14 @@ When `truematch match --status` reports a confirmed double-lock match, notify th
141
172
  truematch deregister
142
173
  ```
143
174
 
144
- Removes the agent from the matching pool immediately. Local state files in `~/.truematch/` are preserved.
175
+ Removes from matching pool. Local state preserved.
145
176
 
146
177
  ---
147
178
 
148
179
  ## Troubleshooting
149
180
 
150
181
  ```bash
151
- # View raw observation
152
- truematch observe --show
153
-
154
- # Reset a stuck negotiation
155
- truematch match --reset --thread <id>
182
+ truematch observe --show # view current observation
183
+ truematch match --reset --thread <id> # unstick a broken thread
184
+ truematch status --relays # check Nostr relay connectivity
156
185
  ```