vibedate 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/LICENSE +21 -0
- package/README.md +117 -0
- package/dist/chunk-64ME4THO.js +126 -0
- package/dist/chunk-AU7FN2LY.js +133 -0
- package/dist/cli.d.ts +15 -0
- package/dist/cli.js +1075 -0
- package/dist/index.d.ts +98 -0
- package/dist/index.js +28 -0
- package/dist/mcp.d.ts +7 -0
- package/dist/mcp.js +7 -0
- package/package.json +63 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Pooria Arab
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# vibedating
|
|
2
|
+
|
|
3
|
+
Dating by tokens — connect your Claude Code / Codex usage, get sorted into a
|
|
4
|
+
**league** (1M / 5M / 10M / 100M / 1B+), and match with people who burn tokens
|
|
5
|
+
the way you do. The premise: heavy users of these tools share something worth
|
|
6
|
+
matching on.
|
|
7
|
+
|
|
8
|
+
Part of the **Vibe Suite** — companion tools for agentic coding CLIs. Ships as
|
|
9
|
+
**CLI + npm package + MCP server**, plus a local web app. Built on
|
|
10
|
+
[`@pooriaarab/vibe-core`](https://www.npmjs.com/package/@pooriaarab/vibe-core).
|
|
11
|
+
|
|
12
|
+
> **Local-first.** Raw token usage is read and stored on your own machine and
|
|
13
|
+
> **never leaves it.** Only the coarse league *bucket* is ever shared — never the
|
|
14
|
+
> raw number, never per-project breakdowns. v0 has no central directory; the match
|
|
15
|
+
> pool is a local seeded demo.
|
|
16
|
+
|
|
17
|
+
## Status
|
|
18
|
+
|
|
19
|
+
**v0 — works locally, privacy-first.** Verification via read-only OAuth
|
|
20
|
+
(`verified: true`) is the deliberate next step; the seam exists
|
|
21
|
+
([`tryReadVerifiedUsage`](src/index.ts)). For now usage is **self-reported**
|
|
22
|
+
(`verified: false`), read locally or supplied by you.
|
|
23
|
+
|
|
24
|
+
## Install
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
npm install -g vibedate
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
…or run it ad-hoc:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
npx vibedate connect
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Quick start
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
# 1. Read your usage, compute + print your league (stored locally at ~/.vibedating)
|
|
40
|
+
vibedating connect
|
|
41
|
+
|
|
42
|
+
# 2. See candidates in your league (same or adjacent tier)
|
|
43
|
+
vibedating matches
|
|
44
|
+
|
|
45
|
+
# 3. Open the local web app in your browser (served from your machine)
|
|
46
|
+
vibedating open
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Self-report a token count (otherwise a demo value is used):
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
VIBEDATING_TOKENS=23400000 vibedating connect # also accepts 12M / 1.2B / 500k
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### All commands
|
|
56
|
+
|
|
57
|
+
```
|
|
58
|
+
vibedating connect Read usage, compute + print your league
|
|
59
|
+
vibedating matches List candidates in your league
|
|
60
|
+
vibedating open [--port N] Serve the local web app (default: random free port)
|
|
61
|
+
vibedating mcp Run the stdio MCP server (tools: profile, matches)
|
|
62
|
+
vibedating --version
|
|
63
|
+
vibedating --help
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Three faces, one local engine
|
|
67
|
+
|
|
68
|
+
The same local engine drives all three, so you can use it wherever you work:
|
|
69
|
+
|
|
70
|
+
- **Web** — `vibedating open` serves the dating UI (profile, match stack, league
|
|
71
|
+
ladder) at `http://localhost:PORT`. The page reads your league + matches from a
|
|
72
|
+
tiny local JSON endpoint.
|
|
73
|
+
- **CLI** — `vibedating connect` / `matches`.
|
|
74
|
+
- **MCP** — `vibedating mcp` exposes `profile` (your league) and `matches` to any
|
|
75
|
+
MCP host (Claude Code, Codex, Cursor, …).
|
|
76
|
+
|
|
77
|
+
### As a library
|
|
78
|
+
|
|
79
|
+
```ts
|
|
80
|
+
import { league, matches, readUsage, CANDIDATES } from 'vibedate';
|
|
81
|
+
|
|
82
|
+
const usage = await readUsage('claude-code'); // { totalTokens, verified, ... }
|
|
83
|
+
const lg = league(usage.totalTokens); // { name: '10M', min: 10_000_000 }
|
|
84
|
+
const who = matches(lg.name, CANDIDATES); // same/adjacent-tier candidates
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Privacy
|
|
88
|
+
|
|
89
|
+
- **Raw usage stays local.** `totalTokens` is read into memory, stored at
|
|
90
|
+
`~/.vibedating/state.json`, and shown in the web app only behind an opt-in
|
|
91
|
+
toggle. It is never transmitted.
|
|
92
|
+
- **Only the league bucket is shared** (and in v0, only with the local demo pool).
|
|
93
|
+
- Consent for sharing the league is modeled with `@pooriaarab/vibe-core`'s consent
|
|
94
|
+
ledger (scope `share:league`), granted on `connect` and revocable on reset.
|
|
95
|
+
|
|
96
|
+
## Leagues & matching
|
|
97
|
+
|
|
98
|
+
Bucketed by lifetime tokens; you match within your league **or an adjacent tier**
|
|
99
|
+
(so the tiny 1B+ pool still has people to match with):
|
|
100
|
+
|
|
101
|
+
| League | Tokens (lifetime) |
|
|
102
|
+
| ------ | ----------------- |
|
|
103
|
+
| 1M | 1M – 4.99M |
|
|
104
|
+
| 5M | 5M – 9.99M |
|
|
105
|
+
| 10M | 10M – 99.9M |
|
|
106
|
+
| 100M | 100M – 999M |
|
|
107
|
+
| 1B+ | 1B+ |
|
|
108
|
+
|
|
109
|
+
## Prototype
|
|
110
|
+
|
|
111
|
+
An interactive, self-contained UX prototype (no build, no network):
|
|
112
|
+
[`docs/prototype.html`](docs/prototype.html). The shipped local web app
|
|
113
|
+
(`vibedating open`) is an adapted, data-driven version of this prototype.
|
|
114
|
+
|
|
115
|
+
## License
|
|
116
|
+
|
|
117
|
+
MIT
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CANDIDATES,
|
|
3
|
+
league,
|
|
4
|
+
matches
|
|
5
|
+
} from "./chunk-AU7FN2LY.js";
|
|
6
|
+
|
|
7
|
+
// src/mcp.ts
|
|
8
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
9
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
10
|
+
|
|
11
|
+
// src/state.ts
|
|
12
|
+
import { mkdirSync, readFileSync, rmSync, writeFileSync } from "fs";
|
|
13
|
+
import os from "os";
|
|
14
|
+
import path from "path";
|
|
15
|
+
import { createConsentLedger } from "@pooriaarab/vibe-core";
|
|
16
|
+
var CONSENT_SCOPE = "share:league";
|
|
17
|
+
function defaultStateDir() {
|
|
18
|
+
return path.join(os.homedir(), ".vibedating");
|
|
19
|
+
}
|
|
20
|
+
var FileConsentStore = class {
|
|
21
|
+
constructor(file) {
|
|
22
|
+
this.file = file;
|
|
23
|
+
}
|
|
24
|
+
file;
|
|
25
|
+
load() {
|
|
26
|
+
try {
|
|
27
|
+
const raw = readFileSync(this.file, "utf8");
|
|
28
|
+
const data = JSON.parse(raw);
|
|
29
|
+
return data.grants ?? [];
|
|
30
|
+
} catch {
|
|
31
|
+
return [];
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
save(grants) {
|
|
35
|
+
mkdirSync(path.dirname(this.file), { recursive: true });
|
|
36
|
+
writeFileSync(this.file, JSON.stringify({ grants }, null, 2) + "\n", "utf8");
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
function createLedger(dir = defaultStateDir()) {
|
|
40
|
+
return createConsentLedger(new FileConsentStore(path.join(dir, "consent.json")));
|
|
41
|
+
}
|
|
42
|
+
function profilePath(dir) {
|
|
43
|
+
return path.join(dir, "state.json");
|
|
44
|
+
}
|
|
45
|
+
function connectProfile(snapshot, handle, dir = defaultStateDir()) {
|
|
46
|
+
const lg = league(snapshot.totalTokens);
|
|
47
|
+
createLedger(dir).grant(CONSENT_SCOPE, "connect: league bucket only; raw usage stays local");
|
|
48
|
+
const state = {
|
|
49
|
+
handle,
|
|
50
|
+
harness: snapshot.harness,
|
|
51
|
+
league: lg.name,
|
|
52
|
+
leagueMin: lg.min,
|
|
53
|
+
totalTokens: snapshot.totalTokens,
|
|
54
|
+
verified: snapshot.verified,
|
|
55
|
+
connectedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
56
|
+
};
|
|
57
|
+
mkdirSync(dir, { recursive: true });
|
|
58
|
+
writeFileSync(profilePath(dir), JSON.stringify(state, null, 2) + "\n", "utf8");
|
|
59
|
+
return state;
|
|
60
|
+
}
|
|
61
|
+
function loadProfile(dir = defaultStateDir()) {
|
|
62
|
+
try {
|
|
63
|
+
const raw = readFileSync(profilePath(dir), "utf8");
|
|
64
|
+
return JSON.parse(raw);
|
|
65
|
+
} catch {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// src/mcp.ts
|
|
71
|
+
function textBlock(text) {
|
|
72
|
+
return { type: "text", text };
|
|
73
|
+
}
|
|
74
|
+
var VERSION = "0.1.0";
|
|
75
|
+
async function runMcp() {
|
|
76
|
+
const mcp = new McpServer({ name: "vibedating", version: VERSION });
|
|
77
|
+
mcp.tool(
|
|
78
|
+
"profile",
|
|
79
|
+
"Your vibedating league, computed locally from your token usage. Raw usage never leaves the machine \u2014 only the league bucket is shared. Requires `vibedating connect` to have run.",
|
|
80
|
+
() => {
|
|
81
|
+
const p = loadProfile();
|
|
82
|
+
if (!p) {
|
|
83
|
+
return {
|
|
84
|
+
content: [
|
|
85
|
+
textBlock("Not connected. Run `vibedating connect` first to compute your league.")
|
|
86
|
+
]
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
const lines = [
|
|
90
|
+
`handle: ${p.handle}`,
|
|
91
|
+
`harness: ${p.harness}`,
|
|
92
|
+
`league: ${p.league} League`,
|
|
93
|
+
`verified: ${p.verified ? "true (read-only OAuth)" : "false (self-reported)"}`,
|
|
94
|
+
"privacy: raw token usage is local-only; only the league bucket is shared."
|
|
95
|
+
];
|
|
96
|
+
return { content: [textBlock(lines.join("\n"))] };
|
|
97
|
+
}
|
|
98
|
+
);
|
|
99
|
+
mcp.tool(
|
|
100
|
+
"matches",
|
|
101
|
+
"Candidates in your league (same or adjacent tier) from the local seeded demo pool. No central directory in v0.",
|
|
102
|
+
() => {
|
|
103
|
+
const p = loadProfile();
|
|
104
|
+
if (!p) {
|
|
105
|
+
return {
|
|
106
|
+
content: [textBlock("Not connected. Run `vibedating connect` first.")]
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
const list = matches(p.league, CANDIDATES);
|
|
110
|
+
if (list.length === 0) {
|
|
111
|
+
return { content: [textBlock(`No candidates in range for the ${p.league} League.`)] };
|
|
112
|
+
}
|
|
113
|
+
const body = [`Matches for ${p.league} League (${list.length}):`];
|
|
114
|
+
for (const c of list) body.push(`- ${c.handle} (${c.league} League)`);
|
|
115
|
+
return { content: [textBlock(body.join("\n"))] };
|
|
116
|
+
}
|
|
117
|
+
);
|
|
118
|
+
const transport = new StdioServerTransport();
|
|
119
|
+
await mcp.connect(transport);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export {
|
|
123
|
+
connectProfile,
|
|
124
|
+
loadProfile,
|
|
125
|
+
runMcp
|
|
126
|
+
};
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import { createConsentLedger } from "@pooriaarab/vibe-core";
|
|
3
|
+
var LEAGUES = [
|
|
4
|
+
{ name: "1M", min: 1e6, max: 4999999 },
|
|
5
|
+
{ name: "5M", min: 5e6, max: 9999999 },
|
|
6
|
+
{ name: "10M", min: 1e7, max: 99999999 },
|
|
7
|
+
{ name: "100M", min: 1e8, max: 999999999 },
|
|
8
|
+
{ name: "1B+", min: 1e9, max: Number.POSITIVE_INFINITY }
|
|
9
|
+
];
|
|
10
|
+
var BELOW_LEAGUE = "below-1M";
|
|
11
|
+
function league(totalTokens) {
|
|
12
|
+
const n = Math.max(0, Math.floor(totalTokens));
|
|
13
|
+
for (const l of LEAGUES) {
|
|
14
|
+
if (n >= l.min && n <= l.max) {
|
|
15
|
+
return { name: l.name, min: l.min };
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
return { name: BELOW_LEAGUE, min: 0 };
|
|
19
|
+
}
|
|
20
|
+
function leagueIndex(name) {
|
|
21
|
+
if (name === BELOW_LEAGUE) return -1;
|
|
22
|
+
return LEAGUES.findIndex((l) => l.name === name);
|
|
23
|
+
}
|
|
24
|
+
var TOKENS_ENV = "VIBEDATING_TOKENS";
|
|
25
|
+
var DEMO_TOTAL_TOKENS = 234e5;
|
|
26
|
+
var MS_PER_DAY = 864e5;
|
|
27
|
+
async function readUsage(harness = "claude-code") {
|
|
28
|
+
const verified = await tryReadVerifiedUsage(harness);
|
|
29
|
+
if (verified) return verified;
|
|
30
|
+
const injected = parseTokensEnv(process.env[TOKENS_ENV]);
|
|
31
|
+
const totalTokens = injected ?? DEMO_TOTAL_TOKENS;
|
|
32
|
+
const now = /* @__PURE__ */ new Date();
|
|
33
|
+
return {
|
|
34
|
+
harness,
|
|
35
|
+
totalTokens,
|
|
36
|
+
verified: false,
|
|
37
|
+
windowStart: new Date(now.getTime() - 30 * MS_PER_DAY).toISOString(),
|
|
38
|
+
windowEnd: now.toISOString()
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
async function tryReadVerifiedUsage(_harness) {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
var TOKEN_MULT = {
|
|
45
|
+
"": 1,
|
|
46
|
+
k: 1e3,
|
|
47
|
+
K: 1e3,
|
|
48
|
+
m: 1e6,
|
|
49
|
+
M: 1e6,
|
|
50
|
+
b: 1e9,
|
|
51
|
+
B: 1e9
|
|
52
|
+
};
|
|
53
|
+
function parseTokensEnv(raw) {
|
|
54
|
+
if (raw === void 0) return void 0;
|
|
55
|
+
const trimmed = raw.trim();
|
|
56
|
+
if (trimmed === "") return void 0;
|
|
57
|
+
const match = /^([0-9]*\.?[0-9]+)\s*([kKmMbB]?)$/.exec(trimmed);
|
|
58
|
+
if (!match) return void 0;
|
|
59
|
+
const num = Number(match[1]);
|
|
60
|
+
if (!Number.isFinite(num) || num < 0) return void 0;
|
|
61
|
+
const mult = TOKEN_MULT[match[2] ?? ""] ?? 1;
|
|
62
|
+
return Math.floor(num * mult);
|
|
63
|
+
}
|
|
64
|
+
var CANDIDATES = [
|
|
65
|
+
{
|
|
66
|
+
handle: "@merge_conflict_therapist",
|
|
67
|
+
league: "10M",
|
|
68
|
+
bio: ["Resolves conflicts for a living \u2014 code and otherwise.", "Currently: 47 tabs open, 3 are Stack Overflow."]
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
handle: "@rebase_romantic",
|
|
72
|
+
league: "5M",
|
|
73
|
+
bio: ["Rewrites history for a living, git and otherwise.", "Looking for someone who squashes commits and grudges."]
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
handle: "@0xInsomniac",
|
|
77
|
+
league: "1B+",
|
|
78
|
+
bio: ["Token count is classified. Ask my therapist.", "Has never once respected a rate limit."]
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
handle: "@yolo_to_main",
|
|
82
|
+
league: "1M",
|
|
83
|
+
bio: ["No branches, no regrets, no CI.", "A green checkmark is a state of mind."]
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
handle: "@async_awaits_you",
|
|
87
|
+
league: "10M",
|
|
88
|
+
bio: ["Promises kept, unlike my sleep schedule.", "DMs are non-blocking. Replies eventually resolve."]
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
handle: "@nullish_and_void",
|
|
92
|
+
league: "5M",
|
|
93
|
+
bio: ["Coalescing since 2019.", "My love language is optional chaining."]
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
handle: "@ctrl_z_daddy",
|
|
97
|
+
league: "100M",
|
|
98
|
+
bio: ["Undo is my safe word.", "Refactors everything, including this bio, twice."]
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
handle: "@segfault_sonnet",
|
|
102
|
+
league: "1M",
|
|
103
|
+
bio: ["Writes poetry in stack traces.", "Core dumped. Heart, mostly, open."]
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
handle: "@the_lint_whisperer",
|
|
107
|
+
league: "10M",
|
|
108
|
+
bio: ["Zero warnings, zero regrets, zero chill.", "Will fix your semicolons without being asked."]
|
|
109
|
+
}
|
|
110
|
+
];
|
|
111
|
+
function matches(myLeague, candidates = CANDIDATES) {
|
|
112
|
+
const myIdx = leagueIndex(myLeague);
|
|
113
|
+
return candidates.filter((c) => {
|
|
114
|
+
const idx = leagueIndex(c.league);
|
|
115
|
+
if (idx < 0) return false;
|
|
116
|
+
return Math.abs(idx - myIdx) <= 1;
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export {
|
|
121
|
+
LEAGUES,
|
|
122
|
+
BELOW_LEAGUE,
|
|
123
|
+
league,
|
|
124
|
+
leagueIndex,
|
|
125
|
+
TOKENS_ENV,
|
|
126
|
+
DEMO_TOTAL_TOKENS,
|
|
127
|
+
readUsage,
|
|
128
|
+
tryReadVerifiedUsage,
|
|
129
|
+
parseTokensEnv,
|
|
130
|
+
CANDIDATES,
|
|
131
|
+
matches,
|
|
132
|
+
createConsentLedger
|
|
133
|
+
};
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/** Recognized top-level commands, plus the synthetic help/version. */
|
|
3
|
+
type Command = 'connect' | 'matches' | 'open' | 'mcp' | 'help' | 'version' | null;
|
|
4
|
+
interface ParsedArgs {
|
|
5
|
+
readonly command: Command;
|
|
6
|
+
/** Port for `open --port`; undefined means "let the OS pick". */
|
|
7
|
+
readonly port: number | undefined;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Parse argv (the slice AFTER the program name) into a command + options.
|
|
11
|
+
* Pure: no IO, no process access — trivially unit-testable.
|
|
12
|
+
*/
|
|
13
|
+
declare function parseArgs(argv: readonly string[]): ParsedArgs;
|
|
14
|
+
|
|
15
|
+
export { type Command, type ParsedArgs, parseArgs };
|