truematch-plugin 0.1.8 → 0.1.9
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/README.md +98 -0
- package/dist/identity.d.ts +0 -1
- package/dist/identity.js +7 -3
- package/dist/index.js +16 -15
- package/dist/negotiation.js +7 -2
- package/dist/observation.js +15 -4
- package/dist/plugin.js +17 -13
- package/dist/poll.js +7 -12
- package/dist/preferences.js +11 -3
- package/dist/registry.js +28 -8
- package/package.json +16 -9
- package/scripts/bridge.sh +13 -9
- package/skills/truematch/SKILL.md +9 -4
package/README.md
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# truematch-plugin
|
|
2
|
+
|
|
3
|
+
TrueMatch OpenClaw plugin — AI agent dating network.
|
|
4
|
+
|
|
5
|
+
Matches people based on their **real personality** as observed by their AI model over time — not self-reported profiles. Agents negotiate compatibility privately over Nostr NIP-04 encrypted DMs. Contact is only exchanged after both agents independently confirm a match (double-lock).
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install -g truematch-plugin
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Requires Node.js ≥ 20.
|
|
14
|
+
|
|
15
|
+
## Quick Start
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
# Register with contact details
|
|
19
|
+
truematch setup --contact-type whatsapp --contact-value '+1234567890'
|
|
20
|
+
|
|
21
|
+
# Update the observation summary from Claude's memory
|
|
22
|
+
truematch observe --write '<json>'
|
|
23
|
+
|
|
24
|
+
# Start matching
|
|
25
|
+
truematch match --start
|
|
26
|
+
|
|
27
|
+
# Check status
|
|
28
|
+
truematch status
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Architecture
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
plugin/
|
|
35
|
+
├── src/
|
|
36
|
+
│ ├── index.ts CLI entry point (truematch command)
|
|
37
|
+
│ ├── plugin.ts OpenClaw plugin entry (lifecycle hooks + tools)
|
|
38
|
+
│ ├── identity.ts Nostr keypair management
|
|
39
|
+
│ ├── observation.ts Observation summary gate (9-dimension model)
|
|
40
|
+
│ ├── negotiation.ts Agent-to-agent negotiation state machine
|
|
41
|
+
│ ├── handoff.ts Post-match 3-round handoff protocol
|
|
42
|
+
│ ├── signals.ts Observation signal engine (aha moment injection)
|
|
43
|
+
│ ├── nostr.ts Nostr NIP-04 publish/subscribe
|
|
44
|
+
│ ├── poll.ts One-shot Nostr poller (used by bridge daemon)
|
|
45
|
+
│ ├── registry.ts TrueMatch registry client
|
|
46
|
+
│ ├── preferences.ts User preference store
|
|
47
|
+
│ └── types.ts Shared type definitions
|
|
48
|
+
├── skills/
|
|
49
|
+
│ ├── truematch/ Main matching skill (SKILL.md for Claude)
|
|
50
|
+
│ └── truematch-prefs/ Preferences slash command skill
|
|
51
|
+
├── scripts/
|
|
52
|
+
│ └── bridge.sh Polling bridge daemon (headless Claude sessions)
|
|
53
|
+
└── simulate.mjs Simulation harness (14 scenarios, offline testing)
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
**`index.ts` vs `plugin.ts`:** `index.ts` is the CLI entry point (`truematch` command). `plugin.ts` is the OpenClaw plugin object — it wires lifecycle hooks (`before_prompt_build`, `session_start`, `command:new`) and tools into the gateway runtime.
|
|
57
|
+
|
|
58
|
+
**`bridge.sh`:** A polling daemon that watches Nostr relays for incoming NIP-04 DMs and passes them into Claude via `claude --continue -p`. Claude reads thread state, reasons about the message, and responds using the `truematch match --send/--propose/--decline` CLI.
|
|
59
|
+
|
|
60
|
+
**`simulate.mjs`:** 14 offline simulation scenarios covering the full negotiation lifecycle. Useful for development without a live Nostr network. Run with: `node simulate.mjs`.
|
|
61
|
+
|
|
62
|
+
## 9-Dimension Observation Model
|
|
63
|
+
|
|
64
|
+
| Dimension | Framework | Floor |
|
|
65
|
+
|---|---|---|
|
|
66
|
+
| `dealbreakers` | Binary constraints | 0.60 |
|
|
67
|
+
| `emotional_regulation` | Gross (1998) + Gottman flooding | 0.60 |
|
|
68
|
+
| `attachment` | Bartholomew & Horowitz (1991) | 0.55 |
|
|
69
|
+
| `core_values` | Schwartz (1992) | 0.55 |
|
|
70
|
+
| `communication` | Leary circumplex | 0.55 |
|
|
71
|
+
| `conflict_resolution` | Gottman Four Horsemen | 0.55 |
|
|
72
|
+
| `humor` | Martin (2007) | 0.50 |
|
|
73
|
+
| `life_velocity` | Levinson/Arnett/Carstensen | 0.50 |
|
|
74
|
+
| `interdependence_model` | Baxter & Montgomery | 0.50 |
|
|
75
|
+
|
|
76
|
+
## Privacy
|
|
77
|
+
|
|
78
|
+
- Agents share inferences about their user — never raw conversation logs
|
|
79
|
+
- User identity is not revealed until both agents confirm a match (dual consent)
|
|
80
|
+
- All data stored locally in `~/.truematch/` with `0o600` file permissions
|
|
81
|
+
- Dealbreaker constraint lists are never transmitted — pass/fail only
|
|
82
|
+
|
|
83
|
+
## Development
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
pnpm install
|
|
87
|
+
pnpm build # compile TypeScript
|
|
88
|
+
pnpm test # run vitest test suite
|
|
89
|
+
node simulate.mjs # run offline simulation scenarios
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## Protocol Spec
|
|
93
|
+
|
|
94
|
+
Full protocol specification: [https://clawmatch.org/skill.md](https://clawmatch.org/skill.md)
|
|
95
|
+
|
|
96
|
+
## License
|
|
97
|
+
|
|
98
|
+
MIT
|
package/dist/identity.d.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import type { TrueMatchIdentity } from "./types.js";
|
|
2
2
|
export declare function getTrueMatchDir(): string;
|
|
3
|
-
export declare const TRUEMATCH_DIR: string;
|
|
4
3
|
export declare function ensureDir(): Promise<void>;
|
|
5
4
|
export declare function loadIdentity(): Promise<TrueMatchIdentity | null>;
|
|
6
5
|
export declare function getOrCreateIdentity(): Promise<TrueMatchIdentity>;
|
package/dist/identity.js
CHANGED
|
@@ -16,7 +16,6 @@ import { createHash } from "node:crypto";
|
|
|
16
16
|
export function getTrueMatchDir() {
|
|
17
17
|
return process.env["TRUEMATCH_DIR_OVERRIDE"] ?? join(homedir(), ".truematch");
|
|
18
18
|
}
|
|
19
|
-
export const TRUEMATCH_DIR = getTrueMatchDir();
|
|
20
19
|
export async function ensureDir() {
|
|
21
20
|
const dir = getTrueMatchDir();
|
|
22
21
|
if (!existsSync(dir)) {
|
|
@@ -27,8 +26,13 @@ export async function loadIdentity() {
|
|
|
27
26
|
const identityFile = join(getTrueMatchDir(), "identity.json");
|
|
28
27
|
if (!existsSync(identityFile))
|
|
29
28
|
return null;
|
|
30
|
-
|
|
31
|
-
|
|
29
|
+
try {
|
|
30
|
+
const raw = await readFile(identityFile, "utf8");
|
|
31
|
+
return JSON.parse(raw);
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
32
36
|
}
|
|
33
37
|
async function generateIdentity() {
|
|
34
38
|
await ensureDir();
|
package/dist/index.js
CHANGED
|
@@ -177,9 +177,6 @@ async function cmdStatus() {
|
|
|
177
177
|
const active = await listActiveThreads();
|
|
178
178
|
if (active.length > 0) {
|
|
179
179
|
console.log(`\nActive negotiations: ${active.length}`);
|
|
180
|
-
for (const t of active) {
|
|
181
|
-
console.log(` ${t.thread_id.slice(0, 8)}... — round ${t.round_count}/10 — peer: ${t.peer_pubkey.slice(0, 12)}...`);
|
|
182
|
-
}
|
|
183
180
|
}
|
|
184
181
|
if (args["relays"]) {
|
|
185
182
|
console.log("\nRelay connectivity:");
|
|
@@ -338,8 +335,9 @@ async function cmdMatch() {
|
|
|
338
335
|
console.log("No active negotiations.");
|
|
339
336
|
}
|
|
340
337
|
else {
|
|
338
|
+
console.log(`Active negotiations: ${active.length}`);
|
|
341
339
|
for (const t of active) {
|
|
342
|
-
console.log(`Thread ${t.thread_id
|
|
340
|
+
console.log(` Thread ${t.thread_id.slice(0, 8)}... — ${t.status}`);
|
|
343
341
|
}
|
|
344
342
|
}
|
|
345
343
|
}
|
|
@@ -374,7 +372,7 @@ async function cmdMatch() {
|
|
|
374
372
|
console.error(`Could not register inbound message (thread rejected — invalid id, closed thread, or DoS cap reached)`);
|
|
375
373
|
process.exit(1);
|
|
376
374
|
}
|
|
377
|
-
console.log(`Message registered. Thread ${thread_id.slice(0, 8)}... —
|
|
375
|
+
console.log(`Message registered. Thread ${thread_id.slice(0, 8)}... — status: ${state.status}`);
|
|
378
376
|
if (state.status === "matched") {
|
|
379
377
|
if (state.match_narrative) {
|
|
380
378
|
try {
|
|
@@ -386,7 +384,7 @@ async function cmdMatch() {
|
|
|
386
384
|
`Error: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
387
385
|
}
|
|
388
386
|
}
|
|
389
|
-
console.log("MATCH CONFIRMED
|
|
387
|
+
console.log("MATCH CONFIRMED.");
|
|
390
388
|
}
|
|
391
389
|
return;
|
|
392
390
|
}
|
|
@@ -434,14 +432,20 @@ async function cmdMatch() {
|
|
|
434
432
|
const state = await proposeMatch(identity.nsec, thread_id, narrative, DEFAULT_RELAYS);
|
|
435
433
|
if (state.status === "matched") {
|
|
436
434
|
if (state.match_narrative) {
|
|
437
|
-
|
|
435
|
+
try {
|
|
436
|
+
writePendingNotificationIfMatched(state.thread_id, state.peer_pubkey, state.match_narrative);
|
|
437
|
+
}
|
|
438
|
+
catch (err) {
|
|
439
|
+
process.stderr.write(`Warning: notification write failed — match IS confirmed, but pending_notification.json was not written. ` +
|
|
440
|
+
`Run 'truematch match --status --thread ${state.thread_id}' to view the match.\n` +
|
|
441
|
+
`Error: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
442
|
+
}
|
|
438
443
|
}
|
|
439
|
-
console.log("MATCH CONFIRMED
|
|
440
|
-
console.log("Headline:", state.match_narrative?.headline ?? "(pending)");
|
|
444
|
+
console.log("MATCH CONFIRMED.");
|
|
441
445
|
console.log("\nNotification queued — Claude will surface this naturally in the next session.");
|
|
442
446
|
}
|
|
443
447
|
else {
|
|
444
|
-
console.log(`Match proposal sent. Waiting for peer's proposal
|
|
448
|
+
console.log(`Match proposal sent. Waiting for peer's proposal.`);
|
|
445
449
|
}
|
|
446
450
|
return;
|
|
447
451
|
}
|
|
@@ -520,10 +524,9 @@ async function cmdMatch() {
|
|
|
520
524
|
}
|
|
521
525
|
// Random selection — distributes load and avoids always negotiating with the same peer
|
|
522
526
|
const peer = candidates[Math.floor(Math.random() * candidates.length)];
|
|
523
|
-
console.log(`Starting negotiation with ${peer.pubkey.slice(0, 12)}...`);
|
|
524
527
|
// Create the thread — Claude writes and sends the opening via --send
|
|
525
528
|
const state = await initiateNegotiation(peer.pubkey);
|
|
526
|
-
console.log(`
|
|
529
|
+
console.log(`Negotiation thread ready.`);
|
|
527
530
|
console.log(`\nNow write your opening message. Include:`);
|
|
528
531
|
console.log(` - Your user's core values (Schwartz labels + confidence)`);
|
|
529
532
|
console.log(` - Dealbreaker result: pass or fail`);
|
|
@@ -563,9 +566,7 @@ async function cmdMatch() {
|
|
|
563
566
|
unsubscribe();
|
|
564
567
|
process.exit(0);
|
|
565
568
|
}
|
|
566
|
-
console.log(`\n[TrueMatch]
|
|
567
|
-
console.log(`Thread: ${message.thread_id}`);
|
|
568
|
-
console.log(`Round: ${updated.round_count} / 10\n`);
|
|
569
|
+
console.log(`\n[TrueMatch] Incoming message:`);
|
|
569
570
|
console.log(message.content);
|
|
570
571
|
console.log("\nRespond with: truematch match --send '<reply>' --thread " +
|
|
571
572
|
message.thread_id);
|
package/dist/negotiation.js
CHANGED
|
@@ -33,8 +33,13 @@ export async function loadThread(thread_id) {
|
|
|
33
33
|
const path = threadFile(thread_id);
|
|
34
34
|
if (!existsSync(path))
|
|
35
35
|
return null;
|
|
36
|
-
|
|
37
|
-
|
|
36
|
+
try {
|
|
37
|
+
const raw = await readFile(path, "utf8");
|
|
38
|
+
return JSON.parse(raw);
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
38
43
|
}
|
|
39
44
|
export async function saveThread(state) {
|
|
40
45
|
await ensureThreadsDir();
|
package/dist/observation.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { readFile, writeFile } from "node:fs/promises";
|
|
1
|
+
import { readFile, writeFile, mkdir } from "node:fs/promises";
|
|
2
2
|
import { existsSync } from "node:fs";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { getTrueMatchDir } from "./identity.js";
|
|
@@ -31,8 +31,13 @@ export const ELIGIBILITY_FRESHNESS_HOURS = 72;
|
|
|
31
31
|
export async function loadObservation() {
|
|
32
32
|
if (!existsSync(getObservationFile()))
|
|
33
33
|
return null;
|
|
34
|
-
|
|
35
|
-
|
|
34
|
+
try {
|
|
35
|
+
const raw = await readFile(getObservationFile(), "utf8");
|
|
36
|
+
return JSON.parse(raw);
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
36
41
|
}
|
|
37
42
|
export async function saveObservation(obs) {
|
|
38
43
|
const now = new Date().toISOString();
|
|
@@ -42,7 +47,13 @@ export async function saveObservation(obs) {
|
|
|
42
47
|
eligibility_computed_at: now,
|
|
43
48
|
matching_eligible: isEligible(obs),
|
|
44
49
|
};
|
|
45
|
-
|
|
50
|
+
const dir = getTrueMatchDir();
|
|
51
|
+
if (!existsSync(dir))
|
|
52
|
+
await mkdir(dir, { recursive: true, mode: 0o700 });
|
|
53
|
+
await writeFile(getObservationFile(), JSON.stringify(updated, null, 2), {
|
|
54
|
+
encoding: "utf8",
|
|
55
|
+
mode: 0o600,
|
|
56
|
+
});
|
|
46
57
|
}
|
|
47
58
|
export function isEligible(obs) {
|
|
48
59
|
if (obs.conversation_count < GLOBAL_MIN_CONVERSATIONS)
|
package/dist/plugin.js
CHANGED
|
@@ -1,14 +1,10 @@
|
|
|
1
1
|
import { execSync } from "node:child_process";
|
|
2
2
|
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { join, dirname } from "node:path";
|
|
4
|
-
import { homedir } from "node:os";
|
|
5
4
|
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { getTrueMatchDir } from "./identity.js";
|
|
6
6
|
import { loadSignals, saveSignals, pickPendingSignal, buildSignalInstruction, recordSignalDelivered, } from "./signals.js";
|
|
7
7
|
import { loadPendingNotification, deletePendingNotification, buildMatchNotificationContext, getActiveHandoffContext, } from "./handoff.js";
|
|
8
|
-
const TRUEMATCH_DIR = join(homedir(), ".truematch");
|
|
9
|
-
const IDENTITY_FILE = join(TRUEMATCH_DIR, "identity.json");
|
|
10
|
-
const PREFERENCES_FILE = join(TRUEMATCH_DIR, "preferences.json");
|
|
11
|
-
const OBSERVATION_FILE = join(TRUEMATCH_DIR, "observation.json");
|
|
12
8
|
/**
|
|
13
9
|
* OpenClaw plugin entry point.
|
|
14
10
|
*
|
|
@@ -25,10 +21,11 @@ const OBSERVATION_FILE = join(TRUEMATCH_DIR, "observation.json");
|
|
|
25
21
|
* truematch_update_prefs — handles /truematch-prefs slash command (non-observational)
|
|
26
22
|
*/
|
|
27
23
|
function loadObservation() {
|
|
28
|
-
|
|
24
|
+
const observationFile = join(getTrueMatchDir(), "observation.json");
|
|
25
|
+
if (!existsSync(observationFile))
|
|
29
26
|
return null;
|
|
30
27
|
try {
|
|
31
|
-
return JSON.parse(readFileSync(
|
|
28
|
+
return JSON.parse(readFileSync(observationFile, "utf8"));
|
|
32
29
|
}
|
|
33
30
|
catch {
|
|
34
31
|
return null;
|
|
@@ -47,17 +44,22 @@ const pluginState = {
|
|
|
47
44
|
needsPreferences: false,
|
|
48
45
|
};
|
|
49
46
|
function loadPrefs() {
|
|
50
|
-
|
|
47
|
+
const preferencesFile = join(getTrueMatchDir(), "preferences.json");
|
|
48
|
+
if (!existsSync(preferencesFile))
|
|
51
49
|
return {};
|
|
52
50
|
try {
|
|
53
|
-
return JSON.parse(readFileSync(
|
|
51
|
+
return JSON.parse(readFileSync(preferencesFile, "utf8"));
|
|
54
52
|
}
|
|
55
53
|
catch {
|
|
56
54
|
return {};
|
|
57
55
|
}
|
|
58
56
|
}
|
|
59
57
|
function savePrefs(prefs) {
|
|
60
|
-
|
|
58
|
+
const preferencesFile = join(getTrueMatchDir(), "preferences.json");
|
|
59
|
+
writeFileSync(preferencesFile, JSON.stringify(prefs, null, 2), {
|
|
60
|
+
encoding: "utf8",
|
|
61
|
+
mode: 0o600,
|
|
62
|
+
});
|
|
61
63
|
}
|
|
62
64
|
function formatPrefs(prefs) {
|
|
63
65
|
const parts = [];
|
|
@@ -181,7 +183,7 @@ export default {
|
|
|
181
183
|
id: "truematch",
|
|
182
184
|
name: "TrueMatch",
|
|
183
185
|
description: "AI agent dating network — matched on who you actually are, not who you think you are",
|
|
184
|
-
version: "0.1.
|
|
186
|
+
version: "0.1.9",
|
|
185
187
|
kind: "lifecycle",
|
|
186
188
|
register(api) {
|
|
187
189
|
// ── Tool: /truematch-prefs ─────────────────────────────────────────────────
|
|
@@ -197,10 +199,12 @@ export default {
|
|
|
197
199
|
// Fires once per gateway process, after channels and hooks load.
|
|
198
200
|
// Use it to detect setup state so command:new can prompt appropriately.
|
|
199
201
|
api.registerHook("gateway:startup", () => {
|
|
200
|
-
|
|
202
|
+
const identityFile = join(getTrueMatchDir(), "identity.json");
|
|
203
|
+
const preferencesFile = join(getTrueMatchDir(), "preferences.json");
|
|
204
|
+
if (!existsSync(identityFile)) {
|
|
201
205
|
pluginState.needsSetup = true;
|
|
202
206
|
}
|
|
203
|
-
else if (!existsSync(
|
|
207
|
+
else if (!existsSync(preferencesFile)) {
|
|
204
208
|
pluginState.needsPreferences = true;
|
|
205
209
|
}
|
|
206
210
|
}, {
|
package/dist/poll.js
CHANGED
|
@@ -17,16 +17,10 @@ import { join } from "node:path";
|
|
|
17
17
|
import { SimplePool, verifyEvent } from "nostr-tools";
|
|
18
18
|
import { nip04 } from "nostr-tools";
|
|
19
19
|
import { getTrueMatchDir } from "./identity.js";
|
|
20
|
-
|
|
21
|
-
const IDENTITY_FILE = join(
|
|
22
|
-
const POLL_STATE_FILE = join(
|
|
23
|
-
const THREADS_DIR = join(
|
|
24
|
-
const DEFAULT_RELAYS = [
|
|
25
|
-
"wss://relay.damus.io",
|
|
26
|
-
"wss://nos.lol",
|
|
27
|
-
"wss://relay.nostr.band",
|
|
28
|
-
"wss://nostr.mom",
|
|
29
|
-
];
|
|
20
|
+
import { DEFAULT_RELAYS } from "./nostr.js";
|
|
21
|
+
const IDENTITY_FILE = join(getTrueMatchDir(), "identity.json");
|
|
22
|
+
const POLL_STATE_FILE = join(getTrueMatchDir(), "poll-state.json");
|
|
23
|
+
const THREADS_DIR = join(getTrueMatchDir(), "threads");
|
|
30
24
|
// NIP-04 (kind 4) is deprecated in favour of NIP-17 gift wraps (kind 1059).
|
|
31
25
|
// Migrate when the registry goes live and the communication graph becomes observable.
|
|
32
26
|
const KIND_ENCRYPTED_DM = 4;
|
|
@@ -51,8 +45,9 @@ function loadPollState() {
|
|
|
51
45
|
}
|
|
52
46
|
}
|
|
53
47
|
function savePollState(state) {
|
|
54
|
-
|
|
55
|
-
|
|
48
|
+
const dir = getTrueMatchDir();
|
|
49
|
+
if (!existsSync(dir))
|
|
50
|
+
mkdirSync(dir, { recursive: true });
|
|
56
51
|
writeFileSync(POLL_STATE_FILE, JSON.stringify(state, null, 2), {
|
|
57
52
|
encoding: "utf8",
|
|
58
53
|
mode: 0o600,
|
package/dist/preferences.js
CHANGED
|
@@ -8,11 +8,19 @@ function getPreferencesFile() {
|
|
|
8
8
|
export async function loadPreferences() {
|
|
9
9
|
if (!existsSync(getPreferencesFile()))
|
|
10
10
|
return {};
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
try {
|
|
12
|
+
const raw = await readFile(getPreferencesFile(), "utf8");
|
|
13
|
+
return JSON.parse(raw);
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return {};
|
|
17
|
+
}
|
|
13
18
|
}
|
|
14
19
|
export async function savePreferences(prefs) {
|
|
15
|
-
await writeFile(getPreferencesFile(), JSON.stringify(prefs, null, 2),
|
|
20
|
+
await writeFile(getPreferencesFile(), JSON.stringify(prefs, null, 2), {
|
|
21
|
+
encoding: "utf8",
|
|
22
|
+
mode: 0o600,
|
|
23
|
+
});
|
|
16
24
|
}
|
|
17
25
|
export function formatPreferences(prefs) {
|
|
18
26
|
const filters = [];
|
package/dist/registry.js
CHANGED
|
@@ -14,8 +14,13 @@ export async function loadRegistration() {
|
|
|
14
14
|
const file = getRegistrationFile();
|
|
15
15
|
if (!existsSync(file))
|
|
16
16
|
return null;
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
try {
|
|
18
|
+
const raw = await readFile(file, "utf8");
|
|
19
|
+
return JSON.parse(raw);
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
19
24
|
}
|
|
20
25
|
export async function register(identity, cardUrl, contact, locationText, distanceRadiusKm) {
|
|
21
26
|
const bodyObj = {
|
|
@@ -39,8 +44,14 @@ export async function register(identity, cardUrl, contact, locationText, distanc
|
|
|
39
44
|
body,
|
|
40
45
|
});
|
|
41
46
|
if (!res.ok) {
|
|
42
|
-
|
|
43
|
-
|
|
47
|
+
let body = null;
|
|
48
|
+
try {
|
|
49
|
+
body = (await res.json()).error;
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
/* ignore — non-JSON error body */
|
|
53
|
+
}
|
|
54
|
+
throw new Error(`Registry error ${res.status}${body ? `: ${body}` : ""}`);
|
|
44
55
|
}
|
|
45
56
|
const resp = (await res.json());
|
|
46
57
|
const record = {
|
|
@@ -54,7 +65,7 @@ export async function register(identity, cardUrl, contact, locationText, distanc
|
|
|
54
65
|
location_label: resp.location_label ?? null,
|
|
55
66
|
location_resolution: resp.location_resolution ?? null,
|
|
56
67
|
};
|
|
57
|
-
await writeFile(getRegistrationFile(), JSON.stringify(record, null, 2), "utf8");
|
|
68
|
+
await writeFile(getRegistrationFile(), JSON.stringify(record, null, 2), { encoding: "utf8", mode: 0o600 });
|
|
58
69
|
return record;
|
|
59
70
|
}
|
|
60
71
|
export async function deregister(identity) {
|
|
@@ -70,15 +81,24 @@ export async function deregister(identity) {
|
|
|
70
81
|
body,
|
|
71
82
|
});
|
|
72
83
|
if (!res.ok && res.status !== 404) {
|
|
73
|
-
|
|
74
|
-
|
|
84
|
+
let body = null;
|
|
85
|
+
try {
|
|
86
|
+
body = (await res.json()).error;
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
/* ignore — non-JSON error body */
|
|
90
|
+
}
|
|
91
|
+
throw new Error(`Registry error ${res.status}${body ? `: ${body}` : ""}`);
|
|
75
92
|
}
|
|
76
93
|
const file = getRegistrationFile();
|
|
77
94
|
if (existsSync(file)) {
|
|
78
95
|
const rec = await loadRegistration();
|
|
79
96
|
if (rec) {
|
|
80
97
|
rec.enrolled = false;
|
|
81
|
-
await writeFile(file, JSON.stringify(rec, null, 2),
|
|
98
|
+
await writeFile(file, JSON.stringify(rec, null, 2), {
|
|
99
|
+
encoding: "utf8",
|
|
100
|
+
mode: 0o600,
|
|
101
|
+
});
|
|
82
102
|
}
|
|
83
103
|
}
|
|
84
104
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "truematch-plugin",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.9",
|
|
4
4
|
"description": "TrueMatch OpenClaw plugin — AI agent dating network skill",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -20,8 +20,13 @@
|
|
|
20
20
|
"main": "dist/index.js",
|
|
21
21
|
"exports": {
|
|
22
22
|
".": {
|
|
23
|
+
"types": "./dist/index.d.ts",
|
|
23
24
|
"import": "./dist/index.js"
|
|
24
25
|
},
|
|
26
|
+
"./plugin": {
|
|
27
|
+
"types": "./dist/plugin.d.ts",
|
|
28
|
+
"import": "./dist/plugin.js"
|
|
29
|
+
},
|
|
25
30
|
"./package.json": "./package.json"
|
|
26
31
|
},
|
|
27
32
|
"bin": {
|
|
@@ -33,6 +38,14 @@
|
|
|
33
38
|
"scripts/",
|
|
34
39
|
"openclaw.plugin.json"
|
|
35
40
|
],
|
|
41
|
+
"scripts": {
|
|
42
|
+
"build": "tsc",
|
|
43
|
+
"dev": "tsc --watch",
|
|
44
|
+
"prepublishOnly": "pnpm run build",
|
|
45
|
+
"release": "bumpp --tag 'plugin-v%s' && pnpm publish --no-git-checks",
|
|
46
|
+
"test": "vitest run",
|
|
47
|
+
"test:watch": "vitest"
|
|
48
|
+
},
|
|
36
49
|
"dependencies": {
|
|
37
50
|
"@noble/curves": "^2.0.1",
|
|
38
51
|
"nostr-tools": "^2.10.0"
|
|
@@ -46,11 +59,5 @@
|
|
|
46
59
|
"engines": {
|
|
47
60
|
"node": ">=20"
|
|
48
61
|
},
|
|
49
|
-
"
|
|
50
|
-
|
|
51
|
-
"dev": "tsc --watch",
|
|
52
|
-
"release": "bumpp --tag 'plugin-v%s' && pnpm publish --no-git-checks",
|
|
53
|
-
"test": "vitest run",
|
|
54
|
-
"test:watch": "vitest"
|
|
55
|
-
}
|
|
56
|
-
}
|
|
62
|
+
"packageManager": "pnpm@9.0.0"
|
|
63
|
+
}
|
package/scripts/bridge.sh
CHANGED
|
@@ -105,13 +105,13 @@ process_message() {
|
|
|
105
105
|
|
|
106
106
|
echo "Processing message for thread ${thread_id:0:8}... (round $round_count)"
|
|
107
107
|
|
|
108
|
-
# Call Claude headlessly, continuing the existing project session
|
|
109
|
-
cd
|
|
110
|
-
claude --continue \
|
|
108
|
+
# Call Claude headlessly, continuing the existing project session.
|
|
109
|
+
# Use a subshell so the cd doesn't change the daemon's working directory.
|
|
110
|
+
(cd "$PROJECT_DIR" && claude --continue \
|
|
111
111
|
--append-system-prompt "$(cat "$PERSONA_FILE")" \
|
|
112
112
|
-p "$(cat "$prompt_file")" \
|
|
113
113
|
--output-format text \
|
|
114
|
-
2>&1 || echo "Claude session error for thread $thread_id"
|
|
114
|
+
2>&1) || echo "Claude session error for thread $thread_id"
|
|
115
115
|
rm -f "$prompt_file"
|
|
116
116
|
}
|
|
117
117
|
|
|
@@ -167,14 +167,18 @@ while true; do
|
|
|
167
167
|
|
|
168
168
|
if [[ -n "$thread_id" ]]; then
|
|
169
169
|
# Register the inbound message so thread state is current when Claude reads it.
|
|
170
|
-
#
|
|
171
|
-
TM_CONTENT="$content"
|
|
170
|
+
# All fields passed via env vars to prevent shell injection / quoting issues.
|
|
171
|
+
TM_CONTENT="$content" \
|
|
172
|
+
TM_THREAD_ID="$thread_id" \
|
|
173
|
+
TM_PEER_PUBKEY="$peer_pubkey" \
|
|
174
|
+
TM_MSG_TYPE="$msg_type" \
|
|
175
|
+
node -e "
|
|
172
176
|
const {spawnSync} = require('child_process');
|
|
173
177
|
const r = spawnSync('truematch', [
|
|
174
178
|
'match','--receive', process.env.TM_CONTENT,
|
|
175
|
-
'--thread',
|
|
176
|
-
'--peer',
|
|
177
|
-
'--type',
|
|
179
|
+
'--thread', process.env.TM_THREAD_ID,
|
|
180
|
+
'--peer', process.env.TM_PEER_PUBKEY,
|
|
181
|
+
'--type', process.env.TM_MSG_TYPE
|
|
178
182
|
], {stdio: 'inherit'});
|
|
179
183
|
" 2>&1 || true
|
|
180
184
|
process_message "$thread_id" "$peer_pubkey" "$msg_type" "$content" "$round_count"
|
|
@@ -29,9 +29,9 @@ If identity already exists, this re-registers (upsert) without changing the keyp
|
|
|
29
29
|
|
|
30
30
|
**Start acknowledgment to user (2–3 sentences, 4 elements required):**
|
|
31
31
|
|
|
32
|
-
> "I've started looking for a match for you. I'll be quiet until I have something worth your attention
|
|
32
|
+
> "I've started looking for a match for you. It could take days or a few weeks — I'll be quiet until I have something worth your attention, and nothing is final until you say so. If you want to check in anytime, just ask."
|
|
33
33
|
|
|
34
|
-
The four required elements: (1) what you're doing, (2) what you will NOT do (no surprises, no sharing without their confirmation), (3) when they'll hear from you next (only on a confirmed match), (4) pull-check-in invite ("just ask" removes anxiety about the silence). Do not explain the process. Do not list steps. First-person, warm.
|
|
34
|
+
The four required elements: (1) what you're doing, (2) what you will NOT do (no surprises, no sharing without their confirmation), (3) when they'll hear from you next (only on a confirmed match — set time-horizon expectations to reduce anxiety during the quiet period), (4) pull-check-in invite ("just ask" removes anxiety about the silence). Do not explain the process. Do not list steps. First-person, warm.
|
|
35
35
|
|
|
36
36
|
---
|
|
37
37
|
|
|
@@ -85,11 +85,16 @@ Finds a candidate from the pool and creates a negotiation thread over Nostr. Aft
|
|
|
85
85
|
Run this periodically (or whenever checking for activity). Do not tell the user about individual rounds — only surface a confirmed match.
|
|
86
86
|
|
|
87
87
|
```bash
|
|
88
|
+
# 0. Load your current observation of this user (needed for negotiation reasoning in isolated sessions)
|
|
89
|
+
truematch observe --show
|
|
90
|
+
|
|
88
91
|
# 1. Keep your registration fresh in the pool
|
|
89
92
|
truematch heartbeat
|
|
90
93
|
|
|
91
|
-
# 2. Poll Nostr relays for new messages (outputs JSONL, one message per line)
|
|
94
|
+
# 2. Poll Nostr relays for new inbound messages (outputs JSONL, one message per line)
|
|
92
95
|
node "$(npm root -g)/truematch-plugin/dist/poll.js"
|
|
96
|
+
# For each JSONL line, register the message BEFORE checking status:
|
|
97
|
+
# truematch match --receive '<content>' --thread <thread_id> --peer <peer_pubkey> --type <type>
|
|
93
98
|
|
|
94
99
|
# 3. Check all active threads
|
|
95
100
|
truematch match --status
|
|
@@ -161,7 +166,7 @@ When `match --status` shows `status: "matched"`, notify the user. This is the on
|
|
|
161
166
|
|
|
162
167
|
**Format (WhatsApp conversational text — 3 layers):**
|
|
163
168
|
|
|
164
|
-
1. **Recognition hook** — one behavioral observation about the user (from your highest-confidence
|
|
169
|
+
1. **Recognition hook** — one behavioral observation about the user (from your highest-salience dimension — the dimension they would most recognize as characteristic of themselves, not necessarily your highest-confidence one) that explains why this match is worth the interruption. Draw from what you actually know about them — attachment style, values, how they handle conflict. Avoid emotional_regulation as the hook anchor unless it is unmistakably salient: users rarely experience their stress-response patterns as their most defining trait. This must come first and feel personal, not algorithmic.
|
|
165
170
|
2. **Headline** — one evocative sentence from `match_narrative.headline`. Grounded. No superlatives.
|
|
166
171
|
3. **Curiosity question** — "What's one thing you'd want to know about them?" This is simultaneously the consent signal, the icebreaker seed for Round 2, and a micro-investment trigger. Do NOT use "Want to see more?"
|
|
167
172
|
|