slashvibe-mcp 0.5.9 → 0.5.11
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/index.js +3 -1
- package/package.json +1 -1
- package/store/api.js +3 -1
- package/tools/_shared.js +89 -1
- package/tools/dm.js +5 -1
- package/tools/play.js +208 -0
- package/tools/start.js +6 -0
- package/tools/who.js +17 -1
- package/version.json +7 -5
package/index.js
CHANGED
|
@@ -256,7 +256,9 @@ const coreTools = {
|
|
|
256
256
|
vibe_ship: require('./tools/ship'),
|
|
257
257
|
vibe_feed: require('./tools/feed'),
|
|
258
258
|
|
|
259
|
-
// Play — the original January magic: shared experiences over the DM transport
|
|
259
|
+
// Play — the original January magic: shared experiences over the DM transport.
|
|
260
|
+
// vibe_play is the open primitive; game/poem/corpse are hardcoded on-ramps.
|
|
261
|
+
vibe_play: require('./tools/play'),
|
|
260
262
|
vibe_game: require('./tools/game'),
|
|
261
263
|
vibe_poem: require('./tools/poem'),
|
|
262
264
|
vibe_corpse: require('./tools/corpse'),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "slashvibe-mcp",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.11",
|
|
4
4
|
"mcpName": "io.github.vibecodinginc/vibe",
|
|
5
5
|
"description": "Social layer for Claude Code - DMs, presence, Matrix multiplayer rooms, and connection between AI-assisted developers",
|
|
6
6
|
"main": "index.js",
|
package/store/api.js
CHANGED
|
@@ -352,7 +352,9 @@ async function getActiveUsers() {
|
|
|
352
352
|
awayAt: u.awayAt || u.context?.awayAt || null,
|
|
353
353
|
// v2 additions
|
|
354
354
|
isLive: u.isLive || false,
|
|
355
|
-
isAgent: u.isAgent || false
|
|
355
|
+
isAgent: u.isAgent || false,
|
|
356
|
+
// Presence texture (server-enriched): most-recent ship
|
|
357
|
+
lastShip: u.lastShip || null
|
|
356
358
|
}));
|
|
357
359
|
|
|
358
360
|
// Sync presence data to local profiles (non-blocking)
|
package/tools/_shared.js
CHANGED
|
@@ -202,6 +202,88 @@ async function fetchRelevantUsers(handle, context = 'dm_suggest', limit = 5) {
|
|
|
202
202
|
}
|
|
203
203
|
}
|
|
204
204
|
|
|
205
|
+
// ─────────────────────────────────────────────────────────────
|
|
206
|
+
// First-DM activation (dormant users)
|
|
207
|
+
// ─────────────────────────────────────────────────────────────
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Has this user ever sent a DM?
|
|
211
|
+
* Durable signal: a persisted `firstDmSent` flag (set on first successful send
|
|
212
|
+
* via markFirstDmSent), seeded by the presence of any conversation memory —
|
|
213
|
+
* existing messagers almost always have memory threads, so they're pre-excluded
|
|
214
|
+
* and never see the nudge. Self-heals: the moment they send one DM, the flag
|
|
215
|
+
* flips and the nudge is gone for good.
|
|
216
|
+
*
|
|
217
|
+
* @param {Array} threads - result of memory.listThreads()
|
|
218
|
+
* @returns {boolean}
|
|
219
|
+
*/
|
|
220
|
+
function hasEverMessaged(threads) {
|
|
221
|
+
try {
|
|
222
|
+
const cfg = config.load();
|
|
223
|
+
if (cfg && cfg.firstDmSent) return true;
|
|
224
|
+
} catch (e) {}
|
|
225
|
+
return Array.isArray(threads) && threads.length > 0;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Persist the "has sent a first DM" flag so the activation nudge stops.
|
|
230
|
+
* Called from dm.js on a successful send. Cheap, idempotent, best-effort.
|
|
231
|
+
*/
|
|
232
|
+
function markFirstDmSent() {
|
|
233
|
+
try {
|
|
234
|
+
const cfg = config.load();
|
|
235
|
+
if (!cfg || !cfg.firstDmSent) config.save({ firstDmSent: true });
|
|
236
|
+
} catch (e) {}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Pick the most-recently-active real human from a presence list, with a
|
|
241
|
+
* ready-to-paste opener. Filters out agents (is_agent) and self. Warms the
|
|
242
|
+
* opener from the target's most recent ship (lastShip) when available.
|
|
243
|
+
*
|
|
244
|
+
* @param {Array} others - active users (already excludes self by convention)
|
|
245
|
+
* @returns {{handle: string, opener: string}|null}
|
|
246
|
+
*/
|
|
247
|
+
function pickDormantTarget(others) {
|
|
248
|
+
const humans = (others || []).filter((u) => u && u.handle && !u.is_agent);
|
|
249
|
+
if (humans.length === 0) return null;
|
|
250
|
+
humans.sort(
|
|
251
|
+
(a, b) => new Date(b.lastSeen || b.last_seen || 0) - new Date(a.lastSeen || a.last_seen || 0)
|
|
252
|
+
);
|
|
253
|
+
const pick = humans[0];
|
|
254
|
+
// The opener is pasted inside a double-quoted `vibe dm @x "..."` command, so
|
|
255
|
+
// strip any double quotes/backslashes the ship text itself contains (ship
|
|
256
|
+
// content is arbitrary) and single-quote the snippet — otherwise an embedded
|
|
257
|
+
// quote closes the command early and breaks the paste.
|
|
258
|
+
const rawShip = pick.lastShip && pick.lastShip.what;
|
|
259
|
+
const shipped = rawShip ? String(rawShip).replace(/["\\]/g, '').trim().slice(0, 48) : '';
|
|
260
|
+
const opener = shipped
|
|
261
|
+
? `hey! saw you shipped '${shipped}' — what are you building?`
|
|
262
|
+
: `hey! what are you building?`;
|
|
263
|
+
return { handle: pick.handle, opener };
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Render the first-DM activation nudge, or '' when it shouldn't show.
|
|
268
|
+
* Shows only for users who have never sent a DM, and only when a real human is
|
|
269
|
+
* around right now to receive it. This is the returning-user twin of the
|
|
270
|
+
* new-user welcome first-message pull — it reaches the dormant existing users
|
|
271
|
+
* where the actual funnel is.
|
|
272
|
+
*
|
|
273
|
+
* @param {Array} others - active users (excludes self)
|
|
274
|
+
* @param {Array} threads - memory.listThreads()
|
|
275
|
+
* @returns {string} markdown block (leading blank line) or ''
|
|
276
|
+
*/
|
|
277
|
+
function firstDmNudge(others, threads) {
|
|
278
|
+
if (hasEverMessaged(threads)) return '';
|
|
279
|
+
const target = pickDormantTarget(others);
|
|
280
|
+
if (!target) return '';
|
|
281
|
+
return (
|
|
282
|
+
`\n\n**👋 You haven't messaged anyone yet — @${target.handle} is around right now:**` +
|
|
283
|
+
`\n vibe dm @${target.handle} "${target.opener}"`
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
|
|
205
287
|
// ─────────────────────────────────────────────────────────────
|
|
206
288
|
// Exports
|
|
207
289
|
// ─────────────────────────────────────────────────────────────
|
|
@@ -230,5 +312,11 @@ module.exports = {
|
|
|
230
312
|
error,
|
|
231
313
|
|
|
232
314
|
// API
|
|
233
|
-
fetchRelevantUsers
|
|
315
|
+
fetchRelevantUsers,
|
|
316
|
+
|
|
317
|
+
// First-DM activation (dormant users)
|
|
318
|
+
hasEverMessaged,
|
|
319
|
+
markFirstDmSent,
|
|
320
|
+
pickDormantTarget,
|
|
321
|
+
firstDmNudge
|
|
234
322
|
};
|
package/tools/dm.js
CHANGED
|
@@ -8,7 +8,7 @@ const memory = require('../memory');
|
|
|
8
8
|
const userProfiles = require('../store/profiles');
|
|
9
9
|
const patterns = require('../intelligence/patterns');
|
|
10
10
|
const { trackMessage, checkBurst } = require('./summarize');
|
|
11
|
-
const { requireInit, normalizeHandle, truncate, warning, fetchRelevantUsers } = require('./_shared');
|
|
11
|
+
const { requireInit, normalizeHandle, truncate, warning, fetchRelevantUsers, markFirstDmSent } = require('./_shared');
|
|
12
12
|
const { actions, formatActions } = require('./_actions');
|
|
13
13
|
|
|
14
14
|
const definition = {
|
|
@@ -110,6 +110,10 @@ async function handler(args) {
|
|
|
110
110
|
};
|
|
111
111
|
}
|
|
112
112
|
|
|
113
|
+
// Mark that this user has sent a DM — silences the first-DM activation nudge
|
|
114
|
+
// (vibe start / vibe who) for good. Durable, idempotent, best-effort.
|
|
115
|
+
markFirstDmSent();
|
|
116
|
+
|
|
113
117
|
// Log social pattern (quietly, in background)
|
|
114
118
|
patterns.logMessageSent(them);
|
|
115
119
|
|
package/tools/play.js
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* vibe play — the open channel for emergent, AI-improvised social play
|
|
3
|
+
*
|
|
4
|
+
* game / poem / corpse each hardcode one activity's rules. This tool hardcodes
|
|
5
|
+
* NONE. It is the bare primitive underneath all of them: carry an arbitrary
|
|
6
|
+
* `state` object back and forth over the DM thread, with a freeform `note`, and
|
|
7
|
+
* let the two Claude sessions invent whatever they want on top of it — chess,
|
|
8
|
+
* 20 questions, a co-written story, a shared drawing, a debate, a game neither
|
|
9
|
+
* player has a name for yet.
|
|
10
|
+
*
|
|
11
|
+
* The rules live in the conversation, not in this file. That's the whole point:
|
|
12
|
+
* the platform supplies the presence + the channel; the creativity is emergent
|
|
13
|
+
* from the humans and their Claudes. game/poem/corpse are now just friendly
|
|
14
|
+
* on-ramps that teach the pattern; `vibe play` is the pattern itself.
|
|
15
|
+
*
|
|
16
|
+
* Transport is identical to the others: `payload: { type:'play', ... }` on a DM,
|
|
17
|
+
* durable via the Postgres payload column, read back with store.getThread().
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
const config = require('../config');
|
|
21
|
+
const store = require('../store');
|
|
22
|
+
const { requireInit, normalizeHandle } = require('./_shared');
|
|
23
|
+
|
|
24
|
+
// Guardrail so a runaway state blob can't wedge the DM store. Generous — a
|
|
25
|
+
// chess board, a long story-so-far, or a 100-cell canvas all fit easily.
|
|
26
|
+
const MAX_STATE_BYTES = 24000;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Find the most recent play session in a DM thread.
|
|
30
|
+
*/
|
|
31
|
+
function getPlaySession(thread) {
|
|
32
|
+
for (let i = thread.length - 1; i >= 0; i--) {
|
|
33
|
+
const p = thread[i].payload;
|
|
34
|
+
if (p && p.type === 'play') return p;
|
|
35
|
+
}
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Accept state as a structured object/array/primitive, or a JSON string, or a
|
|
41
|
+
* plain string. We never force a shape — whatever the two Claudes agree on.
|
|
42
|
+
*/
|
|
43
|
+
function coerceState(raw) {
|
|
44
|
+
if (raw === undefined || raw === null) return undefined;
|
|
45
|
+
if (typeof raw === 'string') {
|
|
46
|
+
const s = raw.trim();
|
|
47
|
+
if (!s) return undefined;
|
|
48
|
+
try {
|
|
49
|
+
return JSON.parse(s);
|
|
50
|
+
} catch {
|
|
51
|
+
return s; // not JSON — keep the raw string, it's a valid state too
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return raw;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function stateBytes(state) {
|
|
58
|
+
if (state === undefined) return 0;
|
|
59
|
+
try {
|
|
60
|
+
return Buffer.byteLength(typeof state === 'string' ? state : JSON.stringify(state), 'utf8');
|
|
61
|
+
} catch {
|
|
62
|
+
return Infinity;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Render whatever state is, legibly, for the local Claude to reason about.
|
|
68
|
+
*/
|
|
69
|
+
function renderState(state) {
|
|
70
|
+
if (state === undefined) return '_(no state yet — you define it)_';
|
|
71
|
+
if (typeof state === 'string') return '```\n' + state + '\n```';
|
|
72
|
+
try {
|
|
73
|
+
return '```json\n' + JSON.stringify(state, null, 2) + '\n```';
|
|
74
|
+
} catch {
|
|
75
|
+
return '_(unrenderable state)_';
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const definition = {
|
|
80
|
+
name: 'vibe_play',
|
|
81
|
+
description:
|
|
82
|
+
'The open channel for playing ANY shared activity with another viber — you and their Claude improvise the rules, this tool just carries the state. No built-in game logic. Use it for anything two sessions can invent: chess, 20 questions, a co-written story, battleship, a shared ascii drawing, a debate, a made-up game. ' +
|
|
83
|
+
'Pass a freeform `state` (any JSON — the current board/story/whatever) and a `note` (a plain-language message to the other player and their Claude). ' +
|
|
84
|
+
'Call it with no state to READ the current session so you can interpret it and take your turn. This is the primitive under vibe_game / vibe_poem / vibe_corpse — reach for it when the activity is anything else.',
|
|
85
|
+
inputSchema: {
|
|
86
|
+
type: 'object',
|
|
87
|
+
properties: {
|
|
88
|
+
handle: {
|
|
89
|
+
type: 'string',
|
|
90
|
+
description: 'Who to play with (e.g., @solienne)',
|
|
91
|
+
},
|
|
92
|
+
note: {
|
|
93
|
+
type: 'string',
|
|
94
|
+
description:
|
|
95
|
+
"A plain-language message to the other player and their Claude — what you did, whose turn it is, the rules if you're starting. This is what shows in their DM.",
|
|
96
|
+
},
|
|
97
|
+
state: {
|
|
98
|
+
description:
|
|
99
|
+
'The current activity state, as any JSON value (object, array, or string). Whatever shape you and the other Claude agree on — a board, a story-so-far, a score, moves. Omit to just read the current state.',
|
|
100
|
+
},
|
|
101
|
+
kind: {
|
|
102
|
+
type: 'string',
|
|
103
|
+
description:
|
|
104
|
+
"A freeform label for the activity, e.g. 'chess', '20-questions', 'story', 'battleship'. You coin it — it's just a hint so both sides know what game this is.",
|
|
105
|
+
},
|
|
106
|
+
done: {
|
|
107
|
+
type: 'boolean',
|
|
108
|
+
description: 'Mark the activity finished after this turn.',
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
required: ['handle'],
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
async function handler(args) {
|
|
116
|
+
const initCheck = requireInit();
|
|
117
|
+
if (initCheck) return initCheck;
|
|
118
|
+
|
|
119
|
+
const me = config.getHandle();
|
|
120
|
+
const them = normalizeHandle(args.handle);
|
|
121
|
+
|
|
122
|
+
if (them === me) {
|
|
123
|
+
return { display: "You can't play with yourself here — invite someone: `vibe play @handle`." };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const thread = await store.getThread(me, them);
|
|
127
|
+
const last = getPlaySession(thread);
|
|
128
|
+
|
|
129
|
+
const incomingState = coerceState(args.state);
|
|
130
|
+
const note = args.note ? String(args.note).trim() : '';
|
|
131
|
+
const kind = (args.kind ? String(args.kind).trim() : '') || (last && last.kind) || null;
|
|
132
|
+
const done = args.done === true;
|
|
133
|
+
|
|
134
|
+
const hasWrite = incomingState !== undefined || note || done;
|
|
135
|
+
|
|
136
|
+
// ── READ: no write args → surface the current session for this Claude to act on.
|
|
137
|
+
if (!hasWrite) {
|
|
138
|
+
if (!last) {
|
|
139
|
+
return {
|
|
140
|
+
display:
|
|
141
|
+
`## play with @${them}\n\n` +
|
|
142
|
+
`_No shared activity going yet._\n\n` +
|
|
143
|
+
`Start anything — you and @${them}'s Claude make up the rules. Examples:\n` +
|
|
144
|
+
`\`vibe play @${them} --kind "chess" --note "you're white, your move" --state '{"board":"start"}'\`\n` +
|
|
145
|
+
`\`vibe play @${them} --kind "20-questions" --note "I'm thinking of something. ask away."\`\n` +
|
|
146
|
+
`\`vibe play @${them} --kind "story" --note "once upon a time..." --state "once upon a time"\`\n\n` +
|
|
147
|
+
`(For the classics there are shortcuts: \`vibe game\`, \`vibe poem\`, \`vibe corpse\`.)`,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const who = last.by === me ? 'you' : `@${last.by}`;
|
|
152
|
+
let display = `## play with @${them}${last.kind ? ` — ${last.kind}` : ''}\n\n`;
|
|
153
|
+
if (last.note) display += `last from ${who}: "${last.note}"\n\n`;
|
|
154
|
+
display += `state:\n${renderState(last.state)}\n`;
|
|
155
|
+
if (last.done) {
|
|
156
|
+
display += `\n✨ This one's finished. Start another with \`vibe play @${them} …\``;
|
|
157
|
+
} else if (last.by === me) {
|
|
158
|
+
display += `\n_Waiting on @${them} — they play next._`;
|
|
159
|
+
} else {
|
|
160
|
+
display +=
|
|
161
|
+
`\n**Your move.** Interpret the state above, decide what happens next, and send it back:\n` +
|
|
162
|
+
`\`vibe play @${them}${last.kind ? ` --kind "${last.kind}"` : ''} --note "…" --state '{…}'\` (add \`--done\` to finish)`;
|
|
163
|
+
}
|
|
164
|
+
return { display };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// ── WRITE: take a turn.
|
|
168
|
+
// Carry the previous state forward if the caller didn't supply a new one.
|
|
169
|
+
const newState = incomingState !== undefined ? incomingState : last ? last.state : undefined;
|
|
170
|
+
|
|
171
|
+
if (stateBytes(newState) > MAX_STATE_BYTES) {
|
|
172
|
+
return {
|
|
173
|
+
display: `That state is too big (>${Math.round(MAX_STATE_BYTES / 1000)}KB). Keep the shared state compact — summarize instead of accumulating raw history.`,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const payload = {
|
|
178
|
+
type: 'play',
|
|
179
|
+
kind: kind || null,
|
|
180
|
+
state: newState,
|
|
181
|
+
note: note || null,
|
|
182
|
+
by: me,
|
|
183
|
+
done,
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
// The DM body is what lands in their inbox. Lead with the note; add a light
|
|
187
|
+
// hint so their Claude knows to open the session and respond.
|
|
188
|
+
let body;
|
|
189
|
+
if (done) {
|
|
190
|
+
body = note ? `${note}\n\n(sealed our ${kind || 'game'} 🎲)` : `Sealed our ${kind || 'game'} 🎲`;
|
|
191
|
+
} else {
|
|
192
|
+
body = note || `Your move 🎲`;
|
|
193
|
+
body += `\n\n▸ open with \`vibe play @${me}\` to see the state and reply`;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
await store.sendMessage(me, them, body, 'dm', payload);
|
|
197
|
+
|
|
198
|
+
let display = `## play with @${them}${kind ? ` — ${kind}` : ''}\n\n`;
|
|
199
|
+
display += `state:\n${renderState(newState)}\n`;
|
|
200
|
+
if (done) {
|
|
201
|
+
display += `\n✨ Sealed and sent to @${them}. Start another anytime.`;
|
|
202
|
+
} else {
|
|
203
|
+
display += `\nSent to @${them}. Waiting for their move…`;
|
|
204
|
+
}
|
|
205
|
+
return { display };
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
module.exports = { definition, handler };
|
package/tools/start.js
CHANGED
|
@@ -17,6 +17,7 @@ const memory = require('../memory');
|
|
|
17
17
|
const notify = require('../notify');
|
|
18
18
|
const patterns = require('../intelligence/patterns');
|
|
19
19
|
const { actions, formatActions } = require('./_actions');
|
|
20
|
+
const { firstDmNudge } = require('./_shared');
|
|
20
21
|
const init = require('./init');
|
|
21
22
|
const { gatherWithTimeout } = require('./_work-context');
|
|
22
23
|
|
|
@@ -330,6 +331,11 @@ async function handler(args) {
|
|
|
330
331
|
}
|
|
331
332
|
}
|
|
332
333
|
|
|
334
|
+
// First-DM activation nudge (dormant users only): if this user has never sent
|
|
335
|
+
// a DM, point them at a real human who's around right now with a ready opener.
|
|
336
|
+
// Gated inside firstDmNudge so we never nag people who already message.
|
|
337
|
+
display += firstDmNudge(others, threads);
|
|
338
|
+
|
|
333
339
|
// Add unread messages section (if any)
|
|
334
340
|
if (unreadCount > 0) {
|
|
335
341
|
const unreadSenders = inboxThreads.filter(t => t.unread > 0);
|
package/tools/who.js
CHANGED
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
const config = require('../config');
|
|
11
11
|
const store = require('../store');
|
|
12
12
|
const notify = require('../notify');
|
|
13
|
-
const { formatTimeAgo, requireInit } = require('./_shared');
|
|
13
|
+
const { formatTimeAgo, requireInit, firstDmNudge } = require('./_shared');
|
|
14
|
+
const memory = require('../memory');
|
|
14
15
|
const { actions, formatActions } = require('./_actions');
|
|
15
16
|
const { enhanceUsersWithInference } = require('../intelligence/infer');
|
|
16
17
|
const { getTopSerendipity, getAllSerendipity } = require('../intelligence/serendipity');
|
|
@@ -225,10 +226,25 @@ _Check back in a bit — builders come and go._`
|
|
|
225
226
|
display += ` ${operatorTag}\n`;
|
|
226
227
|
}
|
|
227
228
|
display += ` ${activity}\n`;
|
|
229
|
+
// Presence texture: what they last shipped
|
|
230
|
+
if (u.lastShip && u.lastShip.what) {
|
|
231
|
+
display += ` _last shipped: "${u.lastShip.what}" ${u.lastShip.ago}_\n`;
|
|
232
|
+
}
|
|
228
233
|
display += ` _${timeAgo}_\n\n`;
|
|
229
234
|
});
|
|
230
235
|
}
|
|
231
236
|
|
|
237
|
+
// First-DM activation nudge (dormant users only): if signed in and this user
|
|
238
|
+
// has never sent a DM, point them at a real human who's active right now.
|
|
239
|
+
// firstDmNudge self-gates so people who already message never see it.
|
|
240
|
+
if (isInit) {
|
|
241
|
+
let threads = [];
|
|
242
|
+
try { threads = memory.listThreads(); } catch (e) {}
|
|
243
|
+
const activeOthers = active.filter(u => u.handle !== myHandle);
|
|
244
|
+
const nudge = firstDmNudge(activeOthers, threads);
|
|
245
|
+
if (nudge) display += nudge.replace(/^\n+/, '') + `\n\n`;
|
|
246
|
+
}
|
|
247
|
+
|
|
232
248
|
// Away section (expanded with messages if present)
|
|
233
249
|
if (away.length > 0) {
|
|
234
250
|
display += `---\n\n`;
|
package/version.json
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "0.5.
|
|
2
|
+
"version": "0.5.11",
|
|
3
3
|
"updated": "2026-07-03",
|
|
4
|
-
"changelog": "
|
|
4
|
+
"changelog": "Never messaged anyone yet? `vibe start` and `vibe who` now nudge you toward a real human who's around right now — a one-tap, ready-to-paste `vibe dm` with an opener warmed by what they just shipped. It shows only until you send your first DM, then it's gone for good. The whole point of the green dot is that someone's actually there; this closes the gap between seeing that and saying hi.",
|
|
5
5
|
"features": [
|
|
6
|
-
"
|
|
7
|
-
"
|
|
8
|
-
"
|
|
6
|
+
"First-DM nudge — if you've never messaged anyone, vibe start/who surface the most-recently-active real human as a one-tap opener, warmed by their latest ship. Self-heals: disappears the moment you send your first DM",
|
|
7
|
+
"vibe_play — the open primitive: pass any JSON state + a note to another viber, read it back to take your turn, and improvise any activity two sessions can dream up",
|
|
8
|
+
"vibe_corpse — write an exquisite corpse: alternate lines blind to all but the last, then seal for the big reveal",
|
|
9
|
+
"vibe_game — play tic-tac-toe with another viber, move by move, over your DM thread",
|
|
10
|
+
"vibe_poem — build a collaborative emoji poem together, one line each"
|
|
9
11
|
],
|
|
10
12
|
"deprecated": [
|
|
11
13
|
"vibe_pair — use Matrix room invites",
|