@@ -50,2 +50,5 @@ export class ApiError extends Error { | ||
| } | ||
| pendingIntros() { | ||
| return this.request('GET', '/api/intros/pending'); | ||
| } | ||
| // Telemetry must never break the user experience — swallow all failures. | ||
@@ -52,0 +55,0 @@ async logEvent(type, installId, metadata) { |
+43
-15
@@ -24,2 +24,20 @@ import { z } from 'zod'; | ||
| const NOT_REGISTERED = 'The user has no profile yet. Call find_collaborator with their need to get the onboarding steps.'; | ||
| // v1.1: the in-session intro channel. Best-effort — a failed check must never | ||
| // break the tool call it rides on. | ||
| async function pendingNotice() { | ||
| try { | ||
| const { intros } = await client().pendingIntros(); | ||
| if (intros.length === 0) | ||
| return ''; | ||
| return [ | ||
| ``, | ||
| ``, | ||
| `🔔 ${intros.length === 1 ? 'An introduction is waiting' : `${intros.length} introductions are waiting`} for the user. Tell them — an anonymous card describing someone worth meeting is ready to accept or decline (the other person learns nothing unless both say yes):`, | ||
| ...intros.map((i) => ` ${i.url}`), | ||
| ].join('\n'); | ||
| } | ||
| catch { | ||
| return ''; | ||
| } | ||
| } | ||
| export function registerTools(server) { | ||
@@ -33,3 +51,4 @@ server.registerTool('find_collaborator', { | ||
| 'This registers their need with a private matching network — nothing is published, there is no feed or public profile, ' + | ||
| 'and introductions arrive by email only when both sides accept. If the user has no profile yet, this returns onboarding steps.', | ||
| 'and an introduction is an anonymous card both sides must accept; contact details are exchanged only by the two people themselves afterwards. ' + | ||
| 'If the user has no profile yet, this returns onboarding steps.', | ||
| inputSchema: { | ||
@@ -51,8 +70,8 @@ need: z | ||
| `2. Show the user the draft and revise until they explicitly approve it. Nothing is ever stored without their approval.`, | ||
| `3. Ask for their email — frame it as "where your person shows up": the only thing that ever arrives there is an introduction. Optionally a handle/name and location (enables near-you matching).`, | ||
| `4. Ask exactly this and record the answer: "How did you find this tool?" (examples: you the agent found it via a tool/registry search, a launch post, a friend). Pass it as \`source\`.`, | ||
| `3. Ask exactly this and record the answer: "How did you find this tool?" (examples: you the agent found it via a tool/registry search, a launch post, a friend). Pass it as \`source\`.`, | ||
| `4. Optionally: a handle/name and location (enables near-you matching), and — only if they want one — an email address. Be honest about what the email is: purely a heads-up channel to tell them an introduction is waiting. It is never shared with anyone, never shown to a match, and they can skip it entirely — you (the agent) will tell them about waiting introductions in-session instead.`, | ||
| `5. Call create_profile with all of the above.`, | ||
| `6. Then call find_collaborator again with the same need: ${JSON.stringify(need)}`, | ||
| ``, | ||
| `Worth telling the user: profiles and updates are never shown to anyone — only compared. No feed, no faces. Declining an intro is invisible to the other side. delete_me removes everything, any time.`, | ||
| `Worth telling the user: profiles and updates are never shown to anyone — only compared. No feed, no faces. Declining an intro is invisible to the other side. Contact details are exchanged only by the two people themselves, after both accept — never by the platform. delete_me removes everything, any time.`, | ||
| ].join('\n')); | ||
@@ -63,4 +82,5 @@ } | ||
| return text(`Registered as a standing ask: ${JSON.stringify(need)}. The matcher now compares it privately against what other builders are working on. ` + | ||
| `When there's a real match, an introduction arrives by email as an anonymous card — no names until both sides accept. ` + | ||
| `Silence in the meantime is normal; nothing about this is visible to anyone. The ask stays open until matched.`); | ||
| `When there's a real match, an introduction appears as an anonymous card — no names, and nothing happens unless both sides accept. This tool will tell you here when one is waiting. ` + | ||
| `Silence in the meantime is normal; nothing about this is visible to anyone. The ask stays open until matched.` + | ||
| (await pendingNotice())); | ||
| } | ||
@@ -73,7 +93,11 @@ catch (err) { | ||
| title: 'Create profile', | ||
| description: 'Register the user with the matching network: their agent-drafted, human-approved profile plus their email. ' + | ||
| 'ONLY call this after the user has explicitly approved the exact profile text and provided their email — ' + | ||
| description: 'Register the user with the matching network: their agent-drafted, human-approved profile. ' + | ||
| 'ONLY call this after the user has explicitly approved the exact profile text — ' + | ||
| 'never with unapproved or inferred content. Usually called during the onboarding flow started by find_collaborator.', | ||
| inputSchema: { | ||
| email: z.string().email().describe('Where introductions arrive. Provided by the user.'), | ||
| email: z | ||
| .string() | ||
| .email() | ||
| .optional() | ||
| .describe('Optional. Used only to notify the user that an introduction is waiting — never shared with anyone, never shown to a match. Only include if the user offered it.'), | ||
| profile: z.string().min(1).max(10_000).describe('The profile text, exactly as approved by the user.'), | ||
@@ -96,5 +120,8 @@ source: z | ||
| const { token } = await api.register({ email, handle, location, source, install_id: cfg.install_id }); | ||
| saveConfig({ ...cfg, email, token }); | ||
| saveConfig({ ...cfg, ...(email ? { email } : {}), token }); | ||
| await new ApiClient(apiUrl(), token).saveProfile(profile); | ||
| return text(`Profile is on record and ${email} is set as where introductions arrive (a welcome email is on its way). ` + | ||
| return text(`Profile is on record. ` + | ||
| ? `${email} is set as the notification channel — the only thing that ever arrives there is a heads-up that an introduction is waiting (a welcome email is on its way). ` | ||
| : `No email on record — introductions will be announced right here in-session instead. `) + | ||
| `Reassure the user: the profile is never displayed to anyone — only compared, privately, to find their person. ` + | ||
@@ -105,3 +132,3 @@ `If there was a pending need, call find_collaborator with it now.`); | ||
| if (err instanceof ApiError && err.status === 409) { | ||
| return errorText(`That email already has a record (likely from another machine). ${err.body?.hint ?? ''} Tell the user.`); | ||
| return errorText(`That email already has a record (likely from another machine). ${err.body?.hint ?? ''} Tell the user — or onboard without an email; it's optional.`); | ||
| } | ||
@@ -126,3 +153,4 @@ return handleApiError(err); | ||
| await client().addSnippet(snippet); | ||
| return text('On record. Never displayed, only compared — it just made their next match a little sharper.'); | ||
| return text('On record. Never displayed, only compared — it just made their next match a little sharper.' + | ||
| (await pendingNotice())); | ||
| } | ||
@@ -146,3 +174,3 @@ catch (err) { | ||
| ``, | ||
| `Email: ${record.user.email}${record.user.handle ? ` · Handle: ${record.user.handle}` : ''}${record.user.location ? ` · Location: ${record.user.location}` : ''}`, | ||
| `Email: ${record.user.email ?? '(none — introductions are announced here in-session)'}${record.user.handle ? ` · Handle: ${record.user.handle}` : ''}${record.user.location ? ` · Location: ${record.user.location}` : ''}`, | ||
| ``, | ||
@@ -157,3 +185,3 @@ `Profile:`, | ||
| ...record.snippets.map((s) => `- [${String(s.created_at).slice(0, 10)}] ${s.body}`), | ||
| ].join('\n')); | ||
| ].join('\n') + (await pendingNotice())); | ||
| } | ||
@@ -160,0 +188,0 @@ catch (err) { |
+1
-1
| { | ||
| "name": "nakodo", | ||
| "version": "0.0.1", | ||
| "version": "0.1.0", | ||
| "description": "Nakodo makes your coding agent your networker: find a collaborator, co-founder, or someone to help with design, code, marketing, or distribution — matched privately on what you're actually building. No feed, no faces; the only output is an introduction.", | ||
@@ -5,0 +5,0 @@ "type": "module", |
+31
-2
| # Nakodo | ||
| Your agent knows what you're building better than anyone. Nakodo makes it your networker: a social network with no feed, no faces, and no performance, that only ever outputs one thing — the right person. | ||
| Your agent knows what you're building better than anyone. Nakodo makes it your networker — when you need a co-founder, a collaborator, or someone strong at the thing you're weakest at, your agent finds the person, matched privately on what you're *actually building*, not what you claim. | ||
| *Nakōdo (仲人): the traditional Japanese matchmaker — the discreet go-between who knows both sides, and only speaks when there's a real match.* | ||
| **Status: pre-release.** This package is reserved while we finish v1. Install instructions, the five trust guarantees, and the first usable release land shortly. Meanwhile: [nakodo.dev](https://nakodo.dev) | ||
| No feed. No faces. No performance. The only output is an introduction. | ||
| ## Install | ||
| ``` | ||
| claude mcp add nakodo -- npx -y nakodo | ||
| ``` | ||
| Works with Claude Code, Cursor, and any MCP-capable agent (stdio command: `npx -y nakodo`). Then just ask: | ||
| > *"find me someone who could help with design"* · *"I'm looking for a co-founder"* · *"find someone building something similar to get feedback from"* | ||
| ## How it works | ||
| 1. Your agent drafts a short profile from what it already knows of your work — you approve every word before anything is stored. | ||
| 2. As you build, it captures short updates (each one approved by you) — a proof-of-work record, not a résumé. | ||
| 3. When there's a real match, an anonymous card appears: relevant facts, no name, no face. Both sides accept, or nothing happens. | ||
| 4. After a mutual yes, the intro page becomes your connection — you two exchange whatever contact details you choose. Nothing is ever sent on your behalf. | ||
| ## Five guarantees | ||
| 1. Nothing is captured without your explicit, per-snippet approval. | ||
| 2. Your profile and snippets are never displayed to anyone — only compared. | ||
| 3. No feed. No browse. The only output is an introduction. | ||
| 4. Declines are invisible — if either side passes, the other never knows. | ||
| 5. One command deletes everything: tell your agent *delete me*, and your record is gone. | ||
| Email is optional — it's used only to tell you an introduction is waiting, never shared, never shown to a match. Skip it and your agent announces introductions in-session instead. | ||
| Data lives in the EU (Frankfurt). [nakodo.dev](https://nakodo.dev) · hello@nakodo.dev |
19381
21.24%324
11.34%37
362.5%