slashvibe-mcp 0.3.20 → 0.3.21
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 +47 -252
- package/analytics.js +107 -0
- package/auth-store.js +148 -0
- package/auto-update.js +130 -0
- package/bridges/bridge-monitor.js +388 -0
- package/bridges/discord-bot.js +431 -0
- package/bridges/farcaster.js +299 -0
- package/bridges/telegram.js +261 -0
- package/bridges/webhook-health.js +420 -0
- package/bridges/webhook-server.js +437 -0
- package/bridges/whatsapp.js +441 -0
- package/bridges/x-webhook.js +423 -0
- package/config.js +27 -15
- package/games/arcade.js +406 -0
- package/games/chess.js +451 -0
- package/games/colorguess.js +343 -0
- package/games/crossword-words.js +171 -0
- package/games/crossword.js +461 -0
- package/games/drawing.js +347 -0
- package/games/gameroulette.js +300 -0
- package/games/gamerouter.js +336 -0
- package/games/gamestatus.js +337 -0
- package/games/guessnumber.js +209 -0
- package/games/hangman.js +279 -0
- package/games/memory.js +338 -0
- package/games/multiplayer-tictactoe.js +389 -0
- package/games/pixelart.js +399 -0
- package/games/quickduel.js +354 -0
- package/games/riddle.js +371 -0
- package/games/rockpaperscissors.js +291 -0
- package/games/snake.js +406 -0
- package/games/storybuilder.js +343 -0
- package/games/tictactoe.js +345 -0
- package/games/twentyquestions.js +286 -0
- package/games/twotruths.js +207 -0
- package/games/werewolf.js +508 -0
- package/games/wordassociation.js +247 -0
- package/games/wordchain.js +135 -0
- package/index.js +116 -159
- package/intelligence/index.js +9 -2
- package/intelligence/interests.js +369 -0
- package/notification-emitter.js +77 -0
- package/notify.js +5 -1
- package/package.json +21 -16
- package/prompts.js +1 -1
- package/protocol/index.js +73 -0
- package/setup.js +480 -0
- package/smart-inbox.js +276 -0
- package/store/api.js +536 -215
- package/store/profiles.js +160 -12
- package/tools/_actions.js +362 -21
- package/tools/_discovery.js +119 -26
- package/tools/_shared/index.js +64 -0
- package/tools/_shared.js +234 -0
- package/tools/_work-context.js +338 -0
- package/tools/_work-context.manual-test.js +199 -0
- package/tools/_work-context.test.js +260 -0
- package/tools/activity.js +220 -0
- package/tools/analytics.js +191 -0
- package/tools/approve.js +197 -0
- package/tools/artifact-create.js +14 -3
- package/tools/artifacts-price.js +107 -0
- package/tools/available.js +120 -0
- package/tools/broadcast.js +325 -0
- package/tools/chat.js +202 -0
- package/tools/collaborative-drawing.js +1 -1
- package/tools/connection-status.js +178 -0
- package/tools/discover.js +350 -34
- package/tools/dm.js +80 -8
- package/tools/earnings.js +126 -0
- package/tools/feed.js +35 -4
- package/tools/follow.js +224 -0
- package/tools/friends.js +207 -0
- package/tools/gig-browse.js +206 -0
- package/tools/gig-complete.js +144 -0
- package/tools/health.js +87 -0
- package/tools/help.js +3 -3
- package/tools/idea.js +9 -2
- package/tools/inbox.js +289 -105
- package/tools/init.js +131 -34
- package/tools/invite.js +15 -4
- package/tools/leaderboard.js +117 -0
- package/tools/lib/git-apply.js +206 -0
- package/tools/lib/git-bundle.js +407 -0
- package/tools/migrate.js +3 -3
- package/tools/multiplayer-game.js +1 -1
- package/tools/onboarding.js +7 -7
- package/tools/open.js +143 -12
- package/tools/party-game.js +1 -1
- package/tools/plan.js +225 -0
- package/tools/proof-of-work.js +144 -0
- package/tools/reply.js +166 -0
- package/tools/report.js +1 -1
- package/tools/request.js +17 -3
- package/tools/schedule.js +367 -0
- package/tools/search-messages.js +123 -0
- package/tools/session.js +467 -0
- package/tools/session_price.js +128 -0
- package/tools/settings.js +90 -2
- package/tools/ship.js +30 -7
- package/tools/smart-check.js +201 -0
- package/tools/start.js +147 -12
- package/tools/status.js +53 -6
- package/tools/streak.js +147 -0
- package/tools/stuck.js +297 -0
- package/tools/subscribe.js +148 -0
- package/tools/subscriptions.js +134 -0
- package/tools/suggest-tags.js +6 -8
- package/tools/tag-suggestions.js +1 -1
- package/tools/tip.js +150 -77
- package/tools/token.js +4 -4
- package/tools/update.js +1 -1
- package/tools/wallet.js +221 -79
- package/tools/watch.js +157 -0
- package/tools/who.js +30 -1
- package/tools/withdraw.js +145 -0
- package/tools/work-summary.js +96 -0
- package/version.json +10 -8
- package/LICENSE +0 -21
- package/store/sqlite.js +0 -347
- /package/tools/{auto-suggest-connections.js → _deprecated/auto-suggest-connections.js} +0 -0
- /package/tools/{away.js → _deprecated/away.js} +0 -0
- /package/tools/{back.js → _deprecated/back.js} +0 -0
- /package/tools/{bootstrap-skills.js → _deprecated/bootstrap-skills.js} +0 -0
- /package/tools/{bridge-dashboard.js → _deprecated/bridge-dashboard.js} +0 -0
- /package/tools/{bridge-health.js → _deprecated/bridge-health.js} +0 -0
- /package/tools/{bridge-live.js → _deprecated/bridge-live.js} +0 -0
- /package/tools/{bridges.js → _deprecated/bridges.js} +0 -0
- /package/tools/{colorguess.js → _deprecated/colorguess.js} +0 -0
- /package/tools/{discover-insights.js → _deprecated/discover-insights.js} +0 -0
- /package/tools/{discover-momentum.js → _deprecated/discover-momentum.js} +0 -0
- /package/tools/{discovery-analytics.js → _deprecated/discovery-analytics.js} +0 -0
- /package/tools/{discovery-auto-suggest.js → _deprecated/discovery-auto-suggest.js} +0 -0
- /package/tools/{discovery-bootstrap.js → _deprecated/discovery-bootstrap.js} +0 -0
- /package/tools/{discovery-daily.js → _deprecated/discovery-daily.js} +0 -0
- /package/tools/{discovery-dashboard.js → _deprecated/discovery-dashboard.js} +0 -0
- /package/tools/{discovery-digest.js → _deprecated/discovery-digest.js} +0 -0
- /package/tools/{discovery-hub.js → _deprecated/discovery-hub.js} +0 -0
- /package/tools/{discovery-insights.js → _deprecated/discovery-insights.js} +0 -0
- /package/tools/{discovery-momentum.js → _deprecated/discovery-momentum.js} +0 -0
- /package/tools/{discovery-monitor.js → _deprecated/discovery-monitor.js} +0 -0
- /package/tools/{discovery-proactive.js → _deprecated/discovery-proactive.js} +0 -0
- /package/tools/{draw.js → _deprecated/draw.js} +0 -0
- /package/tools/{farcaster.js → _deprecated/farcaster.js} +0 -0
- /package/tools/{forget.js → _deprecated/forget.js} +0 -0
- /package/tools/{games-catalog.js → _deprecated/games-catalog.js} +0 -0
- /package/tools/{games.js → _deprecated/games.js} +0 -0
- /package/tools/{guessnumber.js → _deprecated/guessnumber.js} +0 -0
- /package/tools/{hangman.js → _deprecated/hangman.js} +0 -0
- /package/tools/{multiplayer-tictactoe.js → _deprecated/multiplayer-tictactoe.js} +0 -0
- /package/tools/{mute.js → _deprecated/mute.js} +0 -0
- /package/tools/{recall.js → _deprecated/recall.js} +0 -0
- /package/tools/{remember.js → _deprecated/remember.js} +0 -0
- /package/tools/{riddle.js → _deprecated/riddle.js} +0 -0
- /package/tools/{run-bootstrap.js → _deprecated/run-bootstrap.js} +0 -0
- /package/tools/{skills-analytics.js → _deprecated/skills-analytics.js} +0 -0
- /package/tools/{skills-bootstrap.js → _deprecated/skills-bootstrap.js} +0 -0
- /package/tools/{skills-dashboard.js → _deprecated/skills-dashboard.js} +0 -0
- /package/tools/{skills-exchange.js → _deprecated/skills-exchange.js} +0 -0
- /package/tools/{skills.js → _deprecated/skills.js} +0 -0
- /package/tools/{smart-intro.js → _deprecated/smart-intro.js} +0 -0
- /package/tools/{storybuilder.js → _deprecated/storybuilder.js} +0 -0
- /package/tools/{telegram-bot.js → _deprecated/telegram-bot.js} +0 -0
- /package/tools/{telegram-setup.js → _deprecated/telegram-setup.js} +0 -0
- /package/tools/{tictactoe.js → _deprecated/tictactoe.js} +0 -0
- /package/tools/{twentyquestions.js → _deprecated/twentyquestions.js} +0 -0
- /package/tools/{wordassociation.js → _deprecated/wordassociation.js} +0 -0
|
@@ -0,0 +1,338 @@
|
|
|
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
|
+
};
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Manual verification tests for Ambient Context feature
|
|
4
|
+
*
|
|
5
|
+
* Run: node tools/_work-context.manual-test.js
|
|
6
|
+
*
|
|
7
|
+
* These tests verify the full integration flow as described in the plan.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const { gatherWorkContext, gatherWithTimeout } = require('./_work-context');
|
|
11
|
+
const path = require('path');
|
|
12
|
+
const fs = require('fs');
|
|
13
|
+
|
|
14
|
+
console.log('═══════════════════════════════════════════════════════════════');
|
|
15
|
+
console.log(' AMBIENT CONTEXT - MANUAL VERIFICATION');
|
|
16
|
+
console.log('═══════════════════════════════════════════════════════════════\n');
|
|
17
|
+
|
|
18
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
19
|
+
// TEST 1: vibe_start includes work context
|
|
20
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
21
|
+
|
|
22
|
+
console.log('TEST 1: Work context gathering in current directory');
|
|
23
|
+
console.log('─────────────────────────────────────────────────────');
|
|
24
|
+
|
|
25
|
+
const ctx = gatherWorkContext();
|
|
26
|
+
|
|
27
|
+
console.log('✓ Project name:', ctx.project?.name || '(none)');
|
|
28
|
+
console.log('✓ Project type:', ctx.project?.type || '(unknown)');
|
|
29
|
+
console.log('✓ Git branch:', ctx.git?.branch || '(not a git repo)');
|
|
30
|
+
console.log('✓ Recent commits:', ctx.git?.recentCommits?.length || 0);
|
|
31
|
+
console.log('✓ Changed files:', ctx.git?.changedFiles?.length || 0);
|
|
32
|
+
console.log('✓ Has uncommitted:', ctx.git?.hasUncommitted ? 'yes' : 'no');
|
|
33
|
+
console.log('');
|
|
34
|
+
console.log('✓ Brief summary:', ctx.suggestions?.brief);
|
|
35
|
+
console.log('✓ Detailed summary:', ctx.suggestions?.detailed);
|
|
36
|
+
|
|
37
|
+
const test1Pass = ctx.project && ctx.suggestions?.brief;
|
|
38
|
+
console.log('\n' + (test1Pass ? '✅ TEST 1 PASSED' : '❌ TEST 1 FAILED') + '\n');
|
|
39
|
+
|
|
40
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
41
|
+
// TEST 2: Timeout behavior
|
|
42
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
43
|
+
|
|
44
|
+
console.log('TEST 2: Timeout and fallback behavior');
|
|
45
|
+
console.log('─────────────────────────────────────────────────────');
|
|
46
|
+
|
|
47
|
+
async function testTimeout() {
|
|
48
|
+
// Test normal timeout
|
|
49
|
+
const start1 = Date.now();
|
|
50
|
+
const ctx1 = await gatherWithTimeout(5000);
|
|
51
|
+
const elapsed1 = Date.now() - start1;
|
|
52
|
+
console.log(`✓ Normal gather: ${elapsed1}ms (should be < 5000ms)`);
|
|
53
|
+
|
|
54
|
+
// Test very short timeout (should use fallback)
|
|
55
|
+
const start2 = Date.now();
|
|
56
|
+
const ctx2 = await gatherWithTimeout(1);
|
|
57
|
+
const elapsed2 = Date.now() - start2;
|
|
58
|
+
console.log(`✓ Fallback gather: ${elapsed2}ms (should have fallback data)`);
|
|
59
|
+
console.log(' Fallback brief:', ctx2.suggestions?.brief);
|
|
60
|
+
|
|
61
|
+
const test2Pass = elapsed1 < 5000 && ctx2.suggestions?.brief;
|
|
62
|
+
console.log('\n' + (test2Pass ? '✅ TEST 2 PASSED' : '❌ TEST 2 FAILED') + '\n');
|
|
63
|
+
return test2Pass;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
67
|
+
// TEST 3: Security - Redaction
|
|
68
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
69
|
+
|
|
70
|
+
console.log('TEST 3: Security - Sensitive data redaction');
|
|
71
|
+
console.log('─────────────────────────────────────────────────────');
|
|
72
|
+
|
|
73
|
+
const { redact, sanitize, cap } = require('./_work-context');
|
|
74
|
+
|
|
75
|
+
const testCases = [
|
|
76
|
+
{ input: 'email: user@example.com', expected: 'email: [REDACTED]', name: 'Email' },
|
|
77
|
+
{ input: 'ghp_1234567890abcdefghijklmnopqrstuvwxyz', expected: '[REDACTED]', name: 'GitHub token' },
|
|
78
|
+
{ input: 'sk-abcdefghijklmnopqrstuvwxyz12', expected: '[REDACTED]', name: 'OpenAI key' },
|
|
79
|
+
{ input: 'my password is secret', expected: 'my [REDACTED] is [REDACTED]', name: 'Sensitive words' },
|
|
80
|
+
];
|
|
81
|
+
|
|
82
|
+
let test3Pass = true;
|
|
83
|
+
testCases.forEach(tc => {
|
|
84
|
+
const result = redact(tc.input);
|
|
85
|
+
const passed = result === tc.expected;
|
|
86
|
+
if (!passed) test3Pass = false;
|
|
87
|
+
console.log(`${passed ? '✓' : '✗'} ${tc.name}: "${tc.input}" → "${result}"`);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
console.log('\n' + (test3Pass ? '✅ TEST 3 PASSED' : '❌ TEST 3 FAILED') + '\n');
|
|
91
|
+
|
|
92
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
93
|
+
// TEST 4: Security - ANSI escape removal
|
|
94
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
95
|
+
|
|
96
|
+
console.log('TEST 4: Security - ANSI escape sequence removal');
|
|
97
|
+
console.log('─────────────────────────────────────────────────────');
|
|
98
|
+
|
|
99
|
+
const ansiTests = [
|
|
100
|
+
{ input: '\x1B[31mRed text\x1B[0m', expected: 'Red text', name: 'Color codes' },
|
|
101
|
+
{ input: '\x1B[1m\x1B[4mBold underline\x1B[0m', expected: 'Bold underline', name: 'Style codes' },
|
|
102
|
+
{ input: 'Normal text', expected: 'Normal text', name: 'No escapes' },
|
|
103
|
+
];
|
|
104
|
+
|
|
105
|
+
let test4Pass = true;
|
|
106
|
+
ansiTests.forEach(tc => {
|
|
107
|
+
const result = sanitize(tc.input);
|
|
108
|
+
const passed = result === tc.expected;
|
|
109
|
+
if (!passed) test4Pass = false;
|
|
110
|
+
console.log(`${passed ? '✓' : '✗'} ${tc.name}: got "${result}"`);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
console.log('\n' + (test4Pass ? '✅ TEST 4 PASSED' : '❌ TEST 4 FAILED') + '\n');
|
|
114
|
+
|
|
115
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
116
|
+
// TEST 5: Fallbacks (non-git directory)
|
|
117
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
118
|
+
|
|
119
|
+
console.log('TEST 5: Fallback behavior in different directories');
|
|
120
|
+
console.log('─────────────────────────────────────────────────────');
|
|
121
|
+
|
|
122
|
+
// Save current directory
|
|
123
|
+
const originalCwd = process.cwd();
|
|
124
|
+
|
|
125
|
+
// Test in /tmp (non-git directory)
|
|
126
|
+
try {
|
|
127
|
+
process.chdir('/tmp');
|
|
128
|
+
const tmpCtx = gatherWorkContext();
|
|
129
|
+
console.log('✓ In /tmp (non-git):');
|
|
130
|
+
console.log(' Project:', tmpCtx.project?.name || 'tmp');
|
|
131
|
+
console.log(' Git:', tmpCtx.git ? 'detected' : 'null (correct)');
|
|
132
|
+
console.log(' Brief:', tmpCtx.suggestions?.brief);
|
|
133
|
+
|
|
134
|
+
const test5Pass = tmpCtx.git === null && tmpCtx.suggestions?.brief;
|
|
135
|
+
console.log('\n' + (test5Pass ? '✅ TEST 5 PASSED' : '❌ TEST 5 FAILED') + '\n');
|
|
136
|
+
} catch (e) {
|
|
137
|
+
console.log('✗ Error testing in /tmp:', e.message);
|
|
138
|
+
console.log('\n❌ TEST 5 FAILED\n');
|
|
139
|
+
} finally {
|
|
140
|
+
process.chdir(originalCwd);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
144
|
+
// TEST 6: Work summary tool
|
|
145
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
146
|
+
|
|
147
|
+
console.log('TEST 6: Work summary tool integration');
|
|
148
|
+
console.log('─────────────────────────────────────────────────────');
|
|
149
|
+
|
|
150
|
+
try {
|
|
151
|
+
const workSummary = require('./work-summary');
|
|
152
|
+
console.log('✓ Tool name:', workSummary.definition.name);
|
|
153
|
+
console.log('✓ Tool description exists:', !!workSummary.definition.description);
|
|
154
|
+
|
|
155
|
+
// Test handler
|
|
156
|
+
const result = workSummary.handler({ detail_level: 'brief' });
|
|
157
|
+
console.log('✓ Handler returns promise/result:', typeof result);
|
|
158
|
+
|
|
159
|
+
console.log('\n✅ TEST 6 PASSED\n');
|
|
160
|
+
} catch (e) {
|
|
161
|
+
console.log('✗ Error:', e.message);
|
|
162
|
+
console.log('\n❌ TEST 6 FAILED\n');
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
166
|
+
// Run async tests
|
|
167
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
168
|
+
|
|
169
|
+
(async () => {
|
|
170
|
+
await testTimeout();
|
|
171
|
+
|
|
172
|
+
console.log('═══════════════════════════════════════════════════════════════');
|
|
173
|
+
console.log(' VERIFICATION COMPLETE');
|
|
174
|
+
console.log('═══════════════════════════════════════════════════════════════\n');
|
|
175
|
+
|
|
176
|
+
console.log('NEXT STEPS FOR FULL TESTING:');
|
|
177
|
+
console.log('');
|
|
178
|
+
console.log('1. Start a vibe session in a git repo:');
|
|
179
|
+
console.log(' $ cd /path/to/git/repo');
|
|
180
|
+
console.log(' $ claude');
|
|
181
|
+
console.log(' > vibe');
|
|
182
|
+
console.log(' → Verify: Response includes workContext with summary');
|
|
183
|
+
console.log(' → Verify: First AskUserQuestion offers "Share your progress"');
|
|
184
|
+
console.log('');
|
|
185
|
+
console.log('2. Check presence auto-set:');
|
|
186
|
+
console.log(' → Have another user run: vibe who');
|
|
187
|
+
console.log(' → Verify: Your presence shows work summary');
|
|
188
|
+
console.log('');
|
|
189
|
+
console.log('3. Test compose with context:');
|
|
190
|
+
console.log(' > message @seth about what I\'m working on');
|
|
191
|
+
console.log(' → Verify: Draft includes real git/project context');
|
|
192
|
+
console.log('');
|
|
193
|
+
console.log('4. Test settings:');
|
|
194
|
+
console.log(' > vibe settings');
|
|
195
|
+
console.log(' → Verify: Shows "Auto Context: on"');
|
|
196
|
+
console.log(' > vibe settings --auto_context false');
|
|
197
|
+
console.log(' → Verify: Settings updated');
|
|
198
|
+
console.log('');
|
|
199
|
+
})();
|