slashvibe-mcp 0.3.21 → 0.3.22
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 +280 -47
- package/config.js +36 -31
- package/crypto.js +1 -6
- package/discord.js +19 -19
- package/index.js +217 -207
- package/intelligence/index.js +2 -9
- package/intelligence/infer.js +10 -16
- package/intelligence/patterns.js +23 -18
- package/intelligence/proactive.js +16 -15
- package/intelligence/serendipity.js +57 -20
- package/memory.js +13 -8
- package/notify.js +39 -14
- package/package.json +27 -20
- package/presence.js +2 -2
- package/prompts.js +5 -9
- package/protocol/index.js +123 -87
- package/protocol/telegram-commands.js +36 -37
- package/store/api.js +358 -529
- package/store/local.js +9 -10
- package/store/profiles.js +48 -192
- package/store/reservations.js +2 -9
- package/store/skills.js +69 -71
- package/store/sqlite.js +355 -0
- package/tools/_actions.js +48 -387
- package/tools/_connection-queue.js +45 -56
- package/tools/_discovery-enhanced.js +52 -57
- package/tools/_discovery.js +87 -185
- package/tools/{l2-status.js → _experimental/l2-status.js} +68 -70
- package/tools/{shipback.js → _experimental/shipback.js} +4 -3
- package/tools/_proactive-discovery.js +60 -73
- package/tools/_shared/index.js +41 -64
- package/tools/admin-inbox.js +10 -15
- package/tools/agents.js +1 -1
- package/tools/artifact-create.js +13 -23
- package/tools/artifact-view.js +4 -4
- package/tools/{_deprecated/back.js → back.js} +1 -1
- package/tools/bye.js +3 -5
- package/tools/consent.js +2 -2
- package/tools/context.js +9 -10
- package/tools/crossword.js +3 -2
- package/tools/discover.js +94 -356
- package/tools/dm.js +27 -86
- package/tools/doctor.js +12 -41
- package/tools/drawing.js +34 -20
- package/tools/echo.js +11 -11
- package/tools/feed.js +30 -58
- package/tools/follow.js +64 -187
- package/tools/{_deprecated/forget.js → forget.js} +4 -7
- package/tools/game.js +144 -48
- package/tools/handoff.js +6 -8
- package/tools/help.js +3 -3
- package/tools/idea.js +15 -27
- package/tools/inbox.js +121 -293
- package/tools/init.js +54 -151
- package/tools/invite.js +8 -21
- package/tools/migrate.js +27 -24
- package/tools/multiplayer-game.js +50 -40
- package/tools/{_deprecated/mute.js → mute.js} +4 -3
- package/tools/notifications.js +58 -48
- package/tools/observe.js +12 -15
- package/tools/onboarding.js +8 -11
- package/tools/open.js +13 -144
- package/tools/party-game.js +23 -12
- package/tools/patterns.js +2 -1
- package/tools/ping.js +5 -7
- package/tools/react.js +28 -30
- package/tools/{_deprecated/recall.js → recall.js} +5 -10
- package/tools/release.js +4 -2
- package/tools/{_deprecated/remember.js → remember.js} +4 -6
- package/tools/report.js +2 -2
- package/tools/request.js +6 -26
- package/tools/reserve.js +1 -1
- package/tools/session-fork.js +97 -0
- package/tools/session-save.js +109 -0
- package/tools/settings.js +30 -99
- package/tools/ship.js +74 -56
- package/tools/{_deprecated/skills-exchange.js → skills-exchange.js} +38 -39
- package/tools/social-inbox.js +22 -28
- package/tools/social-post.js +24 -27
- package/tools/solo-game.js +54 -46
- package/tools/start.js +14 -148
- package/tools/status.js +21 -68
- package/tools/submit.js +4 -2
- package/tools/suggest-tags.js +36 -33
- package/tools/summarize.js +19 -16
- package/tools/tag-suggestions.js +72 -73
- package/tools/test.js +1 -1
- package/tools/{_deprecated/tictactoe.js → tictactoe.js} +26 -26
- package/tools/token.js +4 -4
- package/tools/update.js +1 -2
- package/tools/watch.js +132 -112
- package/tools/who.js +20 -40
- package/tools/{_deprecated/wordassociation.js → wordassociation.js} +23 -20
- package/tools/workshop-buddy.js +52 -53
- package/tools/x-mentions.js +0 -1
- package/tools/x-reply.js +0 -1
- package/twitter.js +14 -20
- package/version.json +8 -10
- package/analytics.js +0 -107
- package/auth-store.js +0 -148
- package/auto-update.js +0 -130
- package/bridges/bridge-monitor.js +0 -388
- package/bridges/discord-bot.js +0 -431
- package/bridges/farcaster.js +0 -299
- package/bridges/telegram.js +0 -261
- package/bridges/webhook-health.js +0 -420
- package/bridges/webhook-server.js +0 -437
- package/bridges/whatsapp.js +0 -441
- package/bridges/x-webhook.js +0 -423
- package/games/arcade.js +0 -406
- package/games/chess.js +0 -451
- package/games/colorguess.js +0 -343
- package/games/crossword-words.js +0 -171
- package/games/crossword.js +0 -461
- package/games/drawing.js +0 -347
- package/games/gameroulette.js +0 -300
- package/games/gamerouter.js +0 -336
- package/games/gamestatus.js +0 -337
- package/games/guessnumber.js +0 -209
- package/games/hangman.js +0 -279
- package/games/memory.js +0 -338
- package/games/multiplayer-tictactoe.js +0 -389
- package/games/pixelart.js +0 -399
- package/games/quickduel.js +0 -354
- package/games/riddle.js +0 -371
- package/games/rockpaperscissors.js +0 -291
- package/games/snake.js +0 -406
- package/games/storybuilder.js +0 -343
- package/games/tictactoe.js +0 -345
- package/games/twentyquestions.js +0 -286
- package/games/twotruths.js +0 -207
- package/games/werewolf.js +0 -508
- package/games/wordassociation.js +0 -247
- package/games/wordchain.js +0 -135
- package/intelligence/interests.js +0 -369
- package/notification-emitter.js +0 -77
- package/setup.js +0 -480
- package/smart-inbox.js +0 -276
- package/tools/_deprecated/auto-suggest-connections.js +0 -304
- package/tools/_deprecated/bootstrap-skills.js +0 -231
- package/tools/_deprecated/bridge-dashboard.js +0 -342
- package/tools/_deprecated/bridge-health.js +0 -400
- package/tools/_deprecated/bridge-live.js +0 -384
- package/tools/_deprecated/bridges.js +0 -383
- package/tools/_deprecated/colorguess.js +0 -281
- package/tools/_deprecated/discover-insights.js +0 -379
- package/tools/_deprecated/discover-momentum.js +0 -256
- package/tools/_deprecated/discovery-analytics.js +0 -345
- package/tools/_deprecated/discovery-auto-suggest.js +0 -275
- package/tools/_deprecated/discovery-bootstrap.js +0 -267
- package/tools/_deprecated/discovery-daily.js +0 -375
- package/tools/_deprecated/discovery-dashboard.js +0 -385
- package/tools/_deprecated/discovery-digest.js +0 -314
- package/tools/_deprecated/discovery-hub.js +0 -357
- package/tools/_deprecated/discovery-insights.js +0 -384
- package/tools/_deprecated/discovery-momentum.js +0 -281
- package/tools/_deprecated/discovery-monitor.js +0 -319
- package/tools/_deprecated/discovery-proactive.js +0 -300
- package/tools/_deprecated/draw.js +0 -317
- package/tools/_deprecated/farcaster.js +0 -307
- package/tools/_deprecated/games-catalog.js +0 -376
- package/tools/_deprecated/games.js +0 -313
- package/tools/_deprecated/guessnumber.js +0 -194
- package/tools/_deprecated/hangman.js +0 -129
- package/tools/_deprecated/multiplayer-tictactoe.js +0 -303
- package/tools/_deprecated/riddle.js +0 -240
- package/tools/_deprecated/run-bootstrap.js +0 -69
- package/tools/_deprecated/skills-analytics.js +0 -349
- package/tools/_deprecated/skills-bootstrap.js +0 -301
- package/tools/_deprecated/skills-dashboard.js +0 -268
- package/tools/_deprecated/skills.js +0 -380
- package/tools/_deprecated/smart-intro.js +0 -353
- package/tools/_deprecated/storybuilder.js +0 -331
- package/tools/_deprecated/telegram-bot.js +0 -183
- package/tools/_deprecated/telegram-setup.js +0 -214
- package/tools/_deprecated/twentyquestions.js +0 -143
- package/tools/_shared.js +0 -234
- package/tools/_work-context.js +0 -338
- package/tools/_work-context.manual-test.js +0 -199
- package/tools/_work-context.test.js +0 -260
- package/tools/activity.js +0 -220
- package/tools/agent-treasury.js +0 -288
- package/tools/analytics.js +0 -191
- package/tools/approve.js +0 -197
- package/tools/arcade.js +0 -173
- package/tools/artifacts-price.js +0 -107
- package/tools/ask-expert.js +0 -160
- package/tools/available.js +0 -120
- package/tools/become-expert.js +0 -150
- package/tools/broadcast.js +0 -325
- package/tools/chat.js +0 -202
- package/tools/collaborative-drawing.js +0 -286
- package/tools/connection-status.js +0 -178
- package/tools/earnings.js +0 -126
- package/tools/friends.js +0 -207
- package/tools/genesis.js +0 -233
- package/tools/gig-browse.js +0 -206
- package/tools/gig-complete.js +0 -144
- package/tools/health.js +0 -87
- package/tools/leaderboard.js +0 -117
- package/tools/lib/git-apply.js +0 -206
- package/tools/lib/git-bundle.js +0 -407
- package/tools/mint.js +0 -377
- package/tools/plan.js +0 -225
- package/tools/profile.js +0 -219
- package/tools/proof-of-work.js +0 -144
- package/tools/pulse.js +0 -218
- package/tools/reply.js +0 -166
- package/tools/reputation.js +0 -175
- package/tools/schedule.js +0 -367
- package/tools/search-messages.js +0 -123
- package/tools/session.js +0 -467
- package/tools/session_price.js +0 -128
- package/tools/smart-check.js +0 -201
- package/tools/social-processor.js +0 -445
- package/tools/streak.js +0 -147
- package/tools/stuck.js +0 -297
- package/tools/subscribe.js +0 -148
- package/tools/subscriptions.js +0 -134
- package/tools/tip.js +0 -193
- package/tools/wallet.js +0 -269
- package/tools/webhook-test.js +0 -388
- package/tools/withdraw.js +0 -145
- package/tools/work-summary.js +0 -96
- package/tools/workshop.js +0 -327
- /package/tools/{l2-bridge.js → _experimental/l2-bridge.js} +0 -0
- /package/tools/{l2.js → _experimental/l2.js} +0 -0
- /package/tools/{_deprecated/away.js → away.js} +0 -0
package/tools/_shared.js
DELETED
|
@@ -1,234 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* _shared.js — Common utilities for /vibe MCP tools
|
|
3
|
-
*
|
|
4
|
-
* This module provides shared helpers used across 90+ tools.
|
|
5
|
-
* Import only what you need to keep tool files clean.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
const config = require('../config');
|
|
9
|
-
const store = require('../store');
|
|
10
|
-
|
|
11
|
-
// ─────────────────────────────────────────────────────────────
|
|
12
|
-
// Authentication & Initialization
|
|
13
|
-
// ─────────────────────────────────────────────────────────────
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Check if user has initialized vibe. Returns error response if not.
|
|
17
|
-
* Use at the top of handler functions:
|
|
18
|
-
* const initCheck = requireInit();
|
|
19
|
-
* if (initCheck) return initCheck;
|
|
20
|
-
*
|
|
21
|
-
* @returns {Object|null} Error response object, or null if initialized
|
|
22
|
-
*/
|
|
23
|
-
function requireInit() {
|
|
24
|
-
if (!config.isInitialized()) {
|
|
25
|
-
return {
|
|
26
|
-
display: '⚠️ Not initialized. Run `vibe init` first.',
|
|
27
|
-
error: 'not_initialized'
|
|
28
|
-
};
|
|
29
|
-
}
|
|
30
|
-
return null;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
// ─────────────────────────────────────────────────────────────
|
|
34
|
-
// Handle Normalization
|
|
35
|
-
// ─────────────────────────────────────────────────────────────
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Normalize a handle string (remove @, lowercase, trim)
|
|
39
|
-
* @param {string} handle - Raw handle input
|
|
40
|
-
* @returns {string} Normalized handle
|
|
41
|
-
*/
|
|
42
|
-
function normalizeHandle(handle) {
|
|
43
|
-
if (!handle) return '';
|
|
44
|
-
return handle.toString().replace(/^@/, '').toLowerCase().trim();
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* Format handle for display (with @)
|
|
49
|
-
* @param {string} handle - Handle to format
|
|
50
|
-
* @returns {string} Display-formatted handle
|
|
51
|
-
*/
|
|
52
|
-
function displayHandle(handle) {
|
|
53
|
-
if (!handle) return '@unknown';
|
|
54
|
-
const normalized = normalizeHandle(handle);
|
|
55
|
-
return `@${normalized}`;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
// ─────────────────────────────────────────────────────────────
|
|
59
|
-
// Time Formatting
|
|
60
|
-
// ─────────────────────────────────────────────────────────────
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* Format timestamp as relative time (e.g., "5m ago", "2h ago")
|
|
64
|
-
* @param {number} timestamp - Unix timestamp in milliseconds
|
|
65
|
-
* @returns {string} Human-readable relative time
|
|
66
|
-
*/
|
|
67
|
-
function formatTimeAgo(timestamp) {
|
|
68
|
-
if (timestamp === undefined || timestamp === null || isNaN(timestamp)) return 'unknown';
|
|
69
|
-
|
|
70
|
-
const now = Date.now();
|
|
71
|
-
const seconds = Math.floor((now - timestamp) / 1000);
|
|
72
|
-
|
|
73
|
-
if (seconds < 0 || isNaN(seconds)) return 'unknown';
|
|
74
|
-
if (seconds < 60) return 'just now';
|
|
75
|
-
if (seconds < 3600) return `${Math.floor(seconds / 60)}m ago`;
|
|
76
|
-
if (seconds < 86400) return `${Math.floor(seconds / 3600)}h ago`;
|
|
77
|
-
return `${Math.floor(seconds / 86400)}d ago`;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
/**
|
|
81
|
-
* Format duration in human-readable form
|
|
82
|
-
* @param {number} ms - Duration in milliseconds
|
|
83
|
-
* @returns {string} Human-readable duration (e.g., "5 minutes", "2 hours")
|
|
84
|
-
*/
|
|
85
|
-
function formatDuration(ms) {
|
|
86
|
-
if (!ms || ms < 0) return 'unknown';
|
|
87
|
-
|
|
88
|
-
const seconds = Math.floor(ms / 1000);
|
|
89
|
-
if (seconds < 60) return `${seconds} second${seconds !== 1 ? 's' : ''}`;
|
|
90
|
-
|
|
91
|
-
const minutes = Math.floor(seconds / 60);
|
|
92
|
-
if (minutes < 60) return `${minutes} minute${minutes !== 1 ? 's' : ''}`;
|
|
93
|
-
|
|
94
|
-
const hours = Math.floor(minutes / 60);
|
|
95
|
-
if (hours < 24) return `${hours} hour${hours !== 1 ? 's' : ''}`;
|
|
96
|
-
|
|
97
|
-
const days = Math.floor(hours / 24);
|
|
98
|
-
return `${days} day${days !== 1 ? 's' : ''}`;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
// ─────────────────────────────────────────────────────────────
|
|
102
|
-
// Text Formatting
|
|
103
|
-
// ─────────────────────────────────────────────────────────────
|
|
104
|
-
|
|
105
|
-
/**
|
|
106
|
-
* Truncate text to max length with ellipsis
|
|
107
|
-
* @param {string} text - Text to truncate
|
|
108
|
-
* @param {number} maxLength - Maximum length (default: 100)
|
|
109
|
-
* @returns {string} Truncated text
|
|
110
|
-
*/
|
|
111
|
-
function truncate(text, maxLength = 100) {
|
|
112
|
-
if (!text) return '';
|
|
113
|
-
if (text.length <= maxLength) return text;
|
|
114
|
-
return text.slice(0, maxLength - 3) + '...';
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
// ─────────────────────────────────────────────────────────────
|
|
118
|
-
// Display Formatting (headers, dividers, status messages)
|
|
119
|
-
// ─────────────────────────────────────────────────────────────
|
|
120
|
-
|
|
121
|
-
/**
|
|
122
|
-
* Format a section header
|
|
123
|
-
* @param {string} text - Header text
|
|
124
|
-
* @returns {string} Formatted header
|
|
125
|
-
*/
|
|
126
|
-
function header(text) {
|
|
127
|
-
return `\n## ${text}\n`;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
/**
|
|
131
|
-
* Horizontal divider
|
|
132
|
-
* @returns {string} Divider string
|
|
133
|
-
*/
|
|
134
|
-
function divider() {
|
|
135
|
-
return '\n---\n';
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
/**
|
|
139
|
-
* Format empty state message
|
|
140
|
-
* @param {string} message - Message to display
|
|
141
|
-
* @returns {string} Formatted empty state
|
|
142
|
-
*/
|
|
143
|
-
function emptyState(message) {
|
|
144
|
-
return `\n_${message}_\n`;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
/**
|
|
148
|
-
* Format success message
|
|
149
|
-
* @param {string} message - Success message
|
|
150
|
-
* @returns {string} Formatted success message
|
|
151
|
-
*/
|
|
152
|
-
function success(message) {
|
|
153
|
-
return `✓ ${message}`;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
/**
|
|
157
|
-
* Format warning message
|
|
158
|
-
* @param {string} message - Warning message
|
|
159
|
-
* @returns {string} Formatted warning message
|
|
160
|
-
*/
|
|
161
|
-
function warning(message) {
|
|
162
|
-
return `⚠️ ${message}`;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
/**
|
|
166
|
-
* Format error message
|
|
167
|
-
* @param {string} message - Error message
|
|
168
|
-
* @returns {string} Formatted error message
|
|
169
|
-
*/
|
|
170
|
-
function error(message) {
|
|
171
|
-
return `❌ ${message}`;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
// ─────────────────────────────────────────────────────────────
|
|
175
|
-
// API Helpers
|
|
176
|
-
// ─────────────────────────────────────────────────────────────
|
|
177
|
-
|
|
178
|
-
/**
|
|
179
|
-
* Fetch relevant users for a given context
|
|
180
|
-
* Uses the relevancy API to find people worth connecting with
|
|
181
|
-
*
|
|
182
|
-
* @param {string} handle - Current user's handle
|
|
183
|
-
* @param {string} context - Context type: 'dm_suggest', 'notification', etc.
|
|
184
|
-
* @param {number} limit - Max number of results (default: 5)
|
|
185
|
-
* @returns {Promise<{matches: Array}>} Relevant users with reasons
|
|
186
|
-
*/
|
|
187
|
-
async function fetchRelevantUsers(handle, context = 'dm_suggest', limit = 5) {
|
|
188
|
-
try {
|
|
189
|
-
const apiUrl = config.getApiUrl();
|
|
190
|
-
const response = await fetch(
|
|
191
|
-
`${apiUrl}/api/relevancy?handle=${encodeURIComponent(handle)}&context=${context}&limit=${limit}`
|
|
192
|
-
);
|
|
193
|
-
|
|
194
|
-
if (!response.ok) {
|
|
195
|
-
return { matches: [] };
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
return await response.json();
|
|
199
|
-
} catch (e) {
|
|
200
|
-
console.warn('[_shared] fetchRelevantUsers error:', e.message);
|
|
201
|
-
return { matches: [] };
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
// ─────────────────────────────────────────────────────────────
|
|
206
|
-
// Exports
|
|
207
|
-
// ─────────────────────────────────────────────────────────────
|
|
208
|
-
|
|
209
|
-
module.exports = {
|
|
210
|
-
// Auth
|
|
211
|
-
requireInit,
|
|
212
|
-
|
|
213
|
-
// Handles
|
|
214
|
-
normalizeHandle,
|
|
215
|
-
displayHandle,
|
|
216
|
-
|
|
217
|
-
// Time
|
|
218
|
-
formatTimeAgo,
|
|
219
|
-
formatDuration,
|
|
220
|
-
|
|
221
|
-
// Text
|
|
222
|
-
truncate,
|
|
223
|
-
|
|
224
|
-
// Display
|
|
225
|
-
header,
|
|
226
|
-
divider,
|
|
227
|
-
emptyState,
|
|
228
|
-
success,
|
|
229
|
-
warning,
|
|
230
|
-
error,
|
|
231
|
-
|
|
232
|
-
// API
|
|
233
|
-
fetchRelevantUsers
|
|
234
|
-
};
|
package/tools/_work-context.js
DELETED
|
@@ -1,338 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Shared work context gathering — used by start.js and work-summary.js
|
|
3
|
-
*
|
|
4
|
-
* SECURITY: Uses execFileSync (not execSync) to prevent shell injection.
|
|
5
|
-
* A malicious branch name like "; rm -rf /" would be harmless with this approach.
|
|
6
|
-
*
|
|
7
|
-
* This module provides:
|
|
8
|
-
* - Git state (branch, recent commits, changed files, uncommitted status)
|
|
9
|
-
* - Project detection (package.json, Cargo.toml, pyproject.toml, or directory name)
|
|
10
|
-
* - Auto-generated suggestions for presence and messages
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
const { execFileSync } = require('child_process');
|
|
14
|
-
const fs = require('fs');
|
|
15
|
-
const path = require('path');
|
|
16
|
-
|
|
17
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
18
|
-
// SECURITY: Sanitization and limits
|
|
19
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
20
|
-
|
|
21
|
-
// Strip ANSI escape sequences and control characters (prevent terminal injection)
|
|
22
|
-
function sanitize(text) {
|
|
23
|
-
if (!text) return text;
|
|
24
|
-
return text
|
|
25
|
-
.replace(/\x1B\[[0-9;]*[a-zA-Z]/g, '') // ANSI escape sequences
|
|
26
|
-
.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, '') // Control chars (keep \t \n \r)
|
|
27
|
-
.trim();
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
// Character limits (DoS prevention)
|
|
31
|
-
const LIMITS = {
|
|
32
|
-
branch: 100,
|
|
33
|
-
commitMessage: 80,
|
|
34
|
-
fileName: 50,
|
|
35
|
-
totalFiles: 10,
|
|
36
|
-
totalSummary: 200,
|
|
37
|
-
};
|
|
38
|
-
|
|
39
|
-
function cap(text, limit) {
|
|
40
|
-
if (!text || text.length <= limit) return text;
|
|
41
|
-
return text.slice(0, limit - 1) + '…';
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
// Redaction patterns (prevent accidental secret exposure)
|
|
45
|
-
const REDACT_PATTERNS = [
|
|
46
|
-
/[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g, // emails
|
|
47
|
-
/[a-fA-F0-9]{32,}/g, // long hex (tokens, hashes)
|
|
48
|
-
/sk-[a-zA-Z0-9]{20,}/g, // OpenAI API keys
|
|
49
|
-
/ghp_[a-zA-Z0-9]{36}/g, // GitHub tokens
|
|
50
|
-
/\b(password|secret|token|api_key|apikey)\b/gi, // sensitive words in context
|
|
51
|
-
];
|
|
52
|
-
|
|
53
|
-
function redact(text) {
|
|
54
|
-
if (!text) return text;
|
|
55
|
-
let result = text;
|
|
56
|
-
for (const pattern of REDACT_PATTERNS) {
|
|
57
|
-
result = result.replace(pattern, '[REDACTED]');
|
|
58
|
-
}
|
|
59
|
-
return result;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
63
|
-
// GIT INFO GATHERING (Safe execution)
|
|
64
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* Execute git command safely (no shell, with timeout and output limits)
|
|
68
|
-
* @param {string[]} args - Git command arguments (e.g., ['branch', '--show-current'])
|
|
69
|
-
* @returns {string|null} - Command output or null on failure
|
|
70
|
-
*/
|
|
71
|
-
function safeGitExec(args) {
|
|
72
|
-
try {
|
|
73
|
-
return execFileSync('git', args, {
|
|
74
|
-
encoding: 'utf8',
|
|
75
|
-
timeout: 2000, // 2s timeout
|
|
76
|
-
maxBuffer: 100 * 1024, // 100KB max output
|
|
77
|
-
shell: false, // CRITICAL: no shell = no injection
|
|
78
|
-
stdio: ['pipe', 'pipe', 'pipe'] // Capture stderr too
|
|
79
|
-
}).trim();
|
|
80
|
-
} catch (e) {
|
|
81
|
-
return null;
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
/**
|
|
86
|
-
* Get comprehensive git information for the current directory
|
|
87
|
-
*/
|
|
88
|
-
function getGitInfo() {
|
|
89
|
-
// Check if we're in a git repo first
|
|
90
|
-
const branch = sanitize(safeGitExec(['branch', '--show-current']));
|
|
91
|
-
if (!branch) {
|
|
92
|
-
// Not a git repo or detached HEAD - try to get commit hash
|
|
93
|
-
const headHash = sanitize(safeGitExec(['rev-parse', '--short', 'HEAD']));
|
|
94
|
-
if (headHash) {
|
|
95
|
-
return {
|
|
96
|
-
branch: `detached:${cap(headHash, 10)}`,
|
|
97
|
-
recentCommits: [],
|
|
98
|
-
changedFiles: [],
|
|
99
|
-
hasUncommitted: false,
|
|
100
|
-
isDetached: true
|
|
101
|
-
};
|
|
102
|
-
}
|
|
103
|
-
return null; // Not a git repo at all
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
// Get recent commits (--no-pager prevents interactive mode)
|
|
107
|
-
const logOutput = sanitize(safeGitExec(['--no-pager', 'log', '--oneline', '-3'])) || '';
|
|
108
|
-
const recentCommits = logOutput.split('\n').filter(Boolean).slice(0, 3).map(line => {
|
|
109
|
-
const [hash, ...msg] = line.split(' ');
|
|
110
|
-
return {
|
|
111
|
-
hash: cap(hash, 7),
|
|
112
|
-
message: redact(cap(msg.join(' '), LIMITS.commitMessage))
|
|
113
|
-
};
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
// Get changed files (--no-ext-diff --no-textconv prevent custom diff handlers)
|
|
117
|
-
const diffOutput = sanitize(safeGitExec([
|
|
118
|
-
'--no-pager', 'diff', '--no-ext-diff', '--no-textconv',
|
|
119
|
-
'--name-only', 'HEAD~1'
|
|
120
|
-
])) || '';
|
|
121
|
-
const changedFiles = diffOutput.split('\n')
|
|
122
|
-
.filter(Boolean)
|
|
123
|
-
.slice(0, LIMITS.totalFiles)
|
|
124
|
-
.map(f => cap(path.basename(f), LIMITS.fileName)); // Only basename, not full path
|
|
125
|
-
|
|
126
|
-
// Check for uncommitted changes (porcelain = machine-readable, safe)
|
|
127
|
-
const statusOutput = safeGitExec(['status', '--porcelain']);
|
|
128
|
-
|
|
129
|
-
return {
|
|
130
|
-
branch: cap(branch, LIMITS.branch),
|
|
131
|
-
recentCommits,
|
|
132
|
-
changedFiles,
|
|
133
|
-
hasUncommitted: !!statusOutput,
|
|
134
|
-
isDetached: false
|
|
135
|
-
};
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
139
|
-
// PROJECT INFO GATHERING
|
|
140
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
141
|
-
|
|
142
|
-
/**
|
|
143
|
-
* Detect project type and name from manifest files
|
|
144
|
-
*/
|
|
145
|
-
function getProjectInfo() {
|
|
146
|
-
const cwd = process.cwd();
|
|
147
|
-
const dirName = path.basename(cwd);
|
|
148
|
-
|
|
149
|
-
let name = dirName;
|
|
150
|
-
let type = 'unknown';
|
|
151
|
-
|
|
152
|
-
// Try package.json (Node.js projects)
|
|
153
|
-
try {
|
|
154
|
-
const pkgPath = path.join(cwd, 'package.json');
|
|
155
|
-
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
156
|
-
name = pkg.name || dirName;
|
|
157
|
-
|
|
158
|
-
// Detect more specific type
|
|
159
|
-
if (pkg.dependencies?.next || pkg.devDependencies?.next) {
|
|
160
|
-
type = 'nextjs';
|
|
161
|
-
} else if (pkg.dependencies?.react || pkg.devDependencies?.react) {
|
|
162
|
-
type = 'react';
|
|
163
|
-
} else if (pkg.devDependencies?.typescript || pkg.dependencies?.typescript) {
|
|
164
|
-
type = 'typescript';
|
|
165
|
-
} else {
|
|
166
|
-
type = 'node';
|
|
167
|
-
}
|
|
168
|
-
} catch (e) {}
|
|
169
|
-
|
|
170
|
-
// Try Cargo.toml (Rust projects)
|
|
171
|
-
try {
|
|
172
|
-
const cargoPath = path.join(cwd, 'Cargo.toml');
|
|
173
|
-
fs.accessSync(cargoPath);
|
|
174
|
-
type = 'rust';
|
|
175
|
-
|
|
176
|
-
// Try to extract crate name
|
|
177
|
-
const cargoContent = fs.readFileSync(cargoPath, 'utf8');
|
|
178
|
-
const nameMatch = cargoContent.match(/name\s*=\s*"([^"]+)"/);
|
|
179
|
-
if (nameMatch) name = nameMatch[1];
|
|
180
|
-
} catch (e) {}
|
|
181
|
-
|
|
182
|
-
// Try pyproject.toml (Python projects)
|
|
183
|
-
try {
|
|
184
|
-
const pyPath = path.join(cwd, 'pyproject.toml');
|
|
185
|
-
fs.accessSync(pyPath);
|
|
186
|
-
type = 'python';
|
|
187
|
-
|
|
188
|
-
// Try to extract project name
|
|
189
|
-
const pyContent = fs.readFileSync(pyPath, 'utf8');
|
|
190
|
-
const nameMatch = pyContent.match(/name\s*=\s*"([^"]+)"/);
|
|
191
|
-
if (nameMatch) name = nameMatch[1];
|
|
192
|
-
} catch (e) {}
|
|
193
|
-
|
|
194
|
-
// Try go.mod (Go projects)
|
|
195
|
-
try {
|
|
196
|
-
const goPath = path.join(cwd, 'go.mod');
|
|
197
|
-
const goContent = fs.readFileSync(goPath, 'utf8');
|
|
198
|
-
type = 'go';
|
|
199
|
-
|
|
200
|
-
const moduleMatch = goContent.match(/module\s+(\S+)/);
|
|
201
|
-
if (moduleMatch) {
|
|
202
|
-
// Use last part of module path as name
|
|
203
|
-
const parts = moduleMatch[1].split('/');
|
|
204
|
-
name = parts[parts.length - 1];
|
|
205
|
-
}
|
|
206
|
-
} catch (e) {}
|
|
207
|
-
|
|
208
|
-
return {
|
|
209
|
-
name: cap(name, 50),
|
|
210
|
-
type,
|
|
211
|
-
directory: cwd
|
|
212
|
-
};
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
216
|
-
// SUGGESTION GENERATION
|
|
217
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
218
|
-
|
|
219
|
-
/**
|
|
220
|
-
* Generate human-readable summaries of current work
|
|
221
|
-
* @param {object} git - Git info from getGitInfo()
|
|
222
|
-
* @param {object} project - Project info from getProjectInfo()
|
|
223
|
-
* @returns {object} - { brief, detailed }
|
|
224
|
-
*/
|
|
225
|
-
function generateSuggestions(git, project) {
|
|
226
|
-
const suggestions = {};
|
|
227
|
-
|
|
228
|
-
if (git?.branch && !git.isDetached) {
|
|
229
|
-
// We have git context - build meaningful summary
|
|
230
|
-
const branchDesc = ['main', 'master', 'develop'].includes(git.branch) ? '' : ` on ${git.branch}`;
|
|
231
|
-
|
|
232
|
-
if (git.recentCommits.length > 0) {
|
|
233
|
-
// Use most recent commit message as the summary
|
|
234
|
-
suggestions.brief = `${git.recentCommits[0].message} — ${project.name}`;
|
|
235
|
-
} else {
|
|
236
|
-
suggestions.brief = `Working on ${project.name}${branchDesc}`;
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
// Detailed version includes more context
|
|
240
|
-
const parts = [];
|
|
241
|
-
if (git.recentCommits.length > 0) {
|
|
242
|
-
parts.push(`Just pushed: "${git.recentCommits[0].message}"`);
|
|
243
|
-
}
|
|
244
|
-
if (branchDesc) {
|
|
245
|
-
parts.push(branchDesc.trim());
|
|
246
|
-
}
|
|
247
|
-
if (git.changedFiles.length > 0) {
|
|
248
|
-
// Group by directory for context
|
|
249
|
-
const dirs = [...new Set(git.changedFiles.map(f => {
|
|
250
|
-
const dir = f.split('/')[0];
|
|
251
|
-
return dir === f ? '.' : dir;
|
|
252
|
-
}))];
|
|
253
|
-
const dirsStr = dirs.slice(0, 2).join(', ');
|
|
254
|
-
parts.push(`${git.changedFiles.length} files in ${dirsStr}`);
|
|
255
|
-
}
|
|
256
|
-
if (git.hasUncommitted) {
|
|
257
|
-
parts.push('uncommitted changes');
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
suggestions.detailed = parts.join('. ') || suggestions.brief;
|
|
261
|
-
} else if (git?.isDetached) {
|
|
262
|
-
// Detached HEAD state
|
|
263
|
-
suggestions.brief = `Working on ${project.name} (${git.branch})`;
|
|
264
|
-
suggestions.detailed = suggestions.brief;
|
|
265
|
-
} else {
|
|
266
|
-
// No git - just use project info
|
|
267
|
-
suggestions.brief = `Working on ${project.name}`;
|
|
268
|
-
suggestions.detailed = suggestions.brief;
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
// Apply final length cap
|
|
272
|
-
suggestions.brief = cap(suggestions.brief, LIMITS.totalSummary);
|
|
273
|
-
suggestions.detailed = cap(suggestions.detailed, LIMITS.totalSummary * 2);
|
|
274
|
-
|
|
275
|
-
return suggestions;
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
279
|
-
// MAIN EXPORT
|
|
280
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
281
|
-
|
|
282
|
-
/**
|
|
283
|
-
* Gather complete work context - the main entry point
|
|
284
|
-
* Safe, non-blocking, with graceful degradation
|
|
285
|
-
*/
|
|
286
|
-
function gatherWorkContext() {
|
|
287
|
-
const git = getGitInfo();
|
|
288
|
-
const project = getProjectInfo();
|
|
289
|
-
const suggestions = generateSuggestions(git, project);
|
|
290
|
-
|
|
291
|
-
return {
|
|
292
|
-
git,
|
|
293
|
-
project,
|
|
294
|
-
suggestions
|
|
295
|
-
};
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
/**
|
|
299
|
-
* Gather with timeout - for use in start.js where we can't block
|
|
300
|
-
* @param {number} timeoutMs - Maximum time to wait (default 2000ms)
|
|
301
|
-
*/
|
|
302
|
-
async function gatherWithTimeout(timeoutMs = 2000) {
|
|
303
|
-
return new Promise((resolve) => {
|
|
304
|
-
// Set a timeout that resolves with minimal fallback
|
|
305
|
-
const timer = setTimeout(() => {
|
|
306
|
-
resolve({
|
|
307
|
-
git: null,
|
|
308
|
-
project: { name: path.basename(process.cwd()), type: 'unknown', directory: process.cwd() },
|
|
309
|
-
suggestions: { brief: 'Working locally', detailed: 'Working locally' }
|
|
310
|
-
});
|
|
311
|
-
}, timeoutMs);
|
|
312
|
-
|
|
313
|
-
// Try to gather context
|
|
314
|
-
try {
|
|
315
|
-
const context = gatherWorkContext();
|
|
316
|
-
clearTimeout(timer);
|
|
317
|
-
resolve(context);
|
|
318
|
-
} catch (e) {
|
|
319
|
-
clearTimeout(timer);
|
|
320
|
-
resolve({
|
|
321
|
-
git: null,
|
|
322
|
-
project: { name: path.basename(process.cwd()), type: 'unknown', directory: process.cwd() },
|
|
323
|
-
suggestions: { brief: 'Working locally', detailed: 'Working locally' }
|
|
324
|
-
});
|
|
325
|
-
}
|
|
326
|
-
});
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
module.exports = {
|
|
330
|
-
gatherWorkContext,
|
|
331
|
-
gatherWithTimeout,
|
|
332
|
-
getGitInfo,
|
|
333
|
-
getProjectInfo,
|
|
334
|
-
// Exported for testing
|
|
335
|
-
sanitize,
|
|
336
|
-
redact,
|
|
337
|
-
cap
|
|
338
|
-
};
|