50c 2.10.0 → 2.14.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/bin/50c.js +7 -0
- package/lib/index.js +78 -1
- package/lib/packs/grabr.js +5 -5
- package/lib/vault.js +165 -5
- package/package.json +2 -2
package/bin/50c.js
CHANGED
|
@@ -464,6 +464,13 @@ async function main() {
|
|
|
464
464
|
// MCP mode: --mcp flag or no args AND piped input
|
|
465
465
|
const isMCPMode = args[0] === '--mcp' || (args.length === 0 && !process.stdin.isTTY);
|
|
466
466
|
|
|
467
|
+
// Version check (non-blocking, only warns)
|
|
468
|
+
if (!isMCPMode && args[0] !== 'help' && args[0] !== '--help') {
|
|
469
|
+
lib.checkVersion().then(v => {
|
|
470
|
+
if (v.message) console.error(`\n Warning: ${v.message}\n`);
|
|
471
|
+
}).catch(() => {});
|
|
472
|
+
}
|
|
473
|
+
|
|
467
474
|
if (isMCPMode) {
|
|
468
475
|
await runMCP();
|
|
469
476
|
} else if (args.length === 0) {
|
package/lib/index.js
CHANGED
|
@@ -25,6 +25,79 @@ const ux = require('./packs/ux');
|
|
|
25
25
|
const promptEngine = require('./packs/prompt_engine');
|
|
26
26
|
const grabr = require('./packs/grabr');
|
|
27
27
|
|
|
28
|
+
// ═══════════════════════════════════════════════════════════════
|
|
29
|
+
// MANIFEST - SINGLE SOURCE OF TRUTH
|
|
30
|
+
// ═══════════════════════════════════════════════════════════════
|
|
31
|
+
// genxis.one/v1/manifest is the canonical tool registry
|
|
32
|
+
// CLI bootstraps from it, caches locally, validates versions
|
|
33
|
+
|
|
34
|
+
const MANIFEST_URL = 'https://api.50c.ai/v1/manifest';
|
|
35
|
+
const MANIFEST_CACHE_TTL = 3600000; // 1 hour
|
|
36
|
+
let manifestCache = null;
|
|
37
|
+
let manifestCacheTime = 0;
|
|
38
|
+
|
|
39
|
+
async function fetchManifest(staging = false) {
|
|
40
|
+
const now = Date.now();
|
|
41
|
+
if (manifestCache && (now - manifestCacheTime) < MANIFEST_CACHE_TTL) {
|
|
42
|
+
return manifestCache;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
const https = require('https');
|
|
47
|
+
const headers = staging ? { 'X-GenXis-Env': 'staging' } : {};
|
|
48
|
+
|
|
49
|
+
const data = await new Promise((resolve, reject) => {
|
|
50
|
+
const req = https.get(MANIFEST_URL, { headers, timeout: 10000 }, (res) => {
|
|
51
|
+
let body = '';
|
|
52
|
+
res.on('data', chunk => body += chunk);
|
|
53
|
+
res.on('end', () => {
|
|
54
|
+
try {
|
|
55
|
+
resolve(JSON.parse(body));
|
|
56
|
+
} catch (e) {
|
|
57
|
+
reject(new Error('Invalid manifest JSON'));
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
req.on('error', reject);
|
|
62
|
+
req.on('timeout', () => { req.destroy(); reject(new Error('Manifest timeout')); });
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
manifestCache = data;
|
|
66
|
+
manifestCacheTime = now;
|
|
67
|
+
return data;
|
|
68
|
+
} catch (e) {
|
|
69
|
+
// Fallback to local tools if manifest unavailable
|
|
70
|
+
console.error('Manifest fetch failed, using local tools:', e.message);
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function checkVersion() {
|
|
76
|
+
try {
|
|
77
|
+
const manifest = await fetchManifest();
|
|
78
|
+
if (!manifest) return { upToDate: true };
|
|
79
|
+
|
|
80
|
+
const pkg = require('../package.json');
|
|
81
|
+
const minVersion = manifest.meta?.min_client_version || '0.0.0';
|
|
82
|
+
const currentVersion = pkg.version;
|
|
83
|
+
|
|
84
|
+
const [minMajor, minMinor] = minVersion.split('.').map(Number);
|
|
85
|
+
const [curMajor, curMinor] = currentVersion.split('.').map(Number);
|
|
86
|
+
|
|
87
|
+
const upToDate = curMajor > minMajor || (curMajor === minMajor && curMinor >= minMinor);
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
upToDate,
|
|
91
|
+
current: currentVersion,
|
|
92
|
+
minimum: minVersion,
|
|
93
|
+
latest: manifest.meta?.api_version,
|
|
94
|
+
message: upToDate ? null : `Update available: npx 50c@latest`
|
|
95
|
+
};
|
|
96
|
+
} catch (e) {
|
|
97
|
+
return { upToDate: true, error: e.message };
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
28
101
|
// Tool name mappings by pack
|
|
29
102
|
const TOOL_PACKS = {
|
|
30
103
|
beacon: ['hints', 'hints_plus', 'roast', 'quick_vibe', 'one_liner', 'name_it', 'price_it', 'compute', 'ide_conversation', 'learning_stats'],
|
|
@@ -195,5 +268,9 @@ module.exports = {
|
|
|
195
268
|
labs,
|
|
196
269
|
labsPlus,
|
|
197
270
|
promptEngine,
|
|
198
|
-
grabr
|
|
271
|
+
grabr,
|
|
272
|
+
// Manifest - Single Source of Truth
|
|
273
|
+
fetchManifest,
|
|
274
|
+
checkVersion,
|
|
275
|
+
MANIFEST_URL
|
|
199
276
|
};
|
package/lib/packs/grabr.js
CHANGED
|
@@ -110,7 +110,7 @@ async function grabrScrape(url, depth = 1) {
|
|
|
110
110
|
|
|
111
111
|
try {
|
|
112
112
|
// Use 50c page_fetch via API
|
|
113
|
-
const result = await apiRequest('page_fetch', { url });
|
|
113
|
+
const result = await apiRequest('POST', '/tools/page_fetch', { url });
|
|
114
114
|
if (result.error) return { error: result.error };
|
|
115
115
|
|
|
116
116
|
const html = result.content || result.text || '';
|
|
@@ -130,7 +130,7 @@ async function grabrScrape(url, depth = 1) {
|
|
|
130
130
|
if (href) {
|
|
131
131
|
try {
|
|
132
132
|
const fullUrl = href.startsWith('http') ? href : new URL(href, url).href;
|
|
133
|
-
const subResult = await apiRequest('page_fetch', { url: fullUrl });
|
|
133
|
+
const subResult = await apiRequest('POST', '/tools/page_fetch', { url: fullUrl });
|
|
134
134
|
if (subResult.content) {
|
|
135
135
|
contacts.emails.push(...extractEmails(subResult.content));
|
|
136
136
|
contacts.phones.push(...extractPhones(subResult.content));
|
|
@@ -237,15 +237,15 @@ async function grabrSitemap(url) {
|
|
|
237
237
|
}
|
|
238
238
|
|
|
239
239
|
try {
|
|
240
|
-
const result = await apiRequest('page_fetch', { url: sitemapUrl });
|
|
240
|
+
const result = await apiRequest('POST', '/tools/page_fetch', { url: sitemapUrl });
|
|
241
241
|
if (result.error) {
|
|
242
242
|
// Try robots.txt fallback
|
|
243
243
|
const robotsUrl = url.replace(/\/$/, '') + '/robots.txt';
|
|
244
|
-
const robotsResult = await apiRequest('page_fetch', { url: robotsUrl });
|
|
244
|
+
const robotsResult = await apiRequest('POST', '/tools/page_fetch', { url: robotsUrl });
|
|
245
245
|
if (robotsResult.content) {
|
|
246
246
|
const sitemapMatch = robotsResult.content.match(/Sitemap:\s*(\S+)/i);
|
|
247
247
|
if (sitemapMatch) {
|
|
248
|
-
const altResult = await apiRequest('page_fetch', { url: sitemapMatch[1] });
|
|
248
|
+
const altResult = await apiRequest('POST', '/tools/page_fetch', { url: sitemapMatch[1] });
|
|
249
249
|
if (altResult.content) {
|
|
250
250
|
result.content = altResult.content;
|
|
251
251
|
}
|
package/lib/vault.js
CHANGED
|
@@ -15,6 +15,9 @@ const SALT_LENGTH = 32;
|
|
|
15
15
|
const IV_LENGTH = 16;
|
|
16
16
|
const AUTH_TAG_LENGTH = 16;
|
|
17
17
|
|
|
18
|
+
// Security: Auto-lock after 5 minutes of inactivity (300 seconds)
|
|
19
|
+
const AUTO_LOCK_INACTIVITY_MS = 5 * 60 * 1000;
|
|
20
|
+
|
|
18
21
|
// Local file paths
|
|
19
22
|
const MASTER_KEY_FILE = path.join(VAULT_DIR, 'master.key.enc');
|
|
20
23
|
const VAULT_DB_FILE = path.join(VAULT_DIR, 'vault.db');
|
|
@@ -54,10 +57,25 @@ function loadLocalSession() {
|
|
|
54
57
|
try {
|
|
55
58
|
if (fs.existsSync(SESSION_FILE)) {
|
|
56
59
|
const session = JSON.parse(fs.readFileSync(SESSION_FILE, 'utf8'));
|
|
57
|
-
|
|
58
|
-
|
|
60
|
+
const now = Date.now();
|
|
61
|
+
|
|
62
|
+
// Check TTL expiry
|
|
63
|
+
if (session.expires_at <= now) {
|
|
64
|
+
fs.unlinkSync(SESSION_FILE);
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Check inactivity auto-lock (5 minutes)
|
|
69
|
+
if (session.last_access && (now - session.last_access) > AUTO_LOCK_INACTIVITY_MS) {
|
|
70
|
+
fs.unlinkSync(SESSION_FILE);
|
|
71
|
+
return null;
|
|
59
72
|
}
|
|
60
|
-
|
|
73
|
+
|
|
74
|
+
// Update last_access timestamp
|
|
75
|
+
session.last_access = now;
|
|
76
|
+
fs.writeFileSync(SESSION_FILE, JSON.stringify(session), { mode: 0o600 });
|
|
77
|
+
|
|
78
|
+
return session;
|
|
61
79
|
}
|
|
62
80
|
} catch {}
|
|
63
81
|
return null;
|
|
@@ -291,11 +309,26 @@ async function remove(name) {
|
|
|
291
309
|
}
|
|
292
310
|
|
|
293
311
|
async function status() {
|
|
312
|
+
const session = MODE === 'local' ? loadLocalSession() : null;
|
|
313
|
+
const now = Date.now();
|
|
314
|
+
|
|
315
|
+
let autoLockIn = null;
|
|
316
|
+
if (session && session.last_access) {
|
|
317
|
+
const remaining = AUTO_LOCK_INACTIVITY_MS - (now - session.last_access);
|
|
318
|
+
autoLockIn = remaining > 0 ? Math.ceil(remaining / 1000) : 0;
|
|
319
|
+
}
|
|
320
|
+
|
|
294
321
|
return {
|
|
295
322
|
mode: MODE,
|
|
296
323
|
initialized: await isInitialized(),
|
|
297
324
|
locked: !(await isUnlocked()),
|
|
298
|
-
path: MODE === 'local' ? VAULT_DIR : 'cloud'
|
|
325
|
+
path: MODE === 'local' ? VAULT_DIR : 'cloud',
|
|
326
|
+
auto_lock_seconds: autoLockIn,
|
|
327
|
+
security: {
|
|
328
|
+
auto_lock_inactivity: '5 minutes',
|
|
329
|
+
encryption: 'AES-256-GCM',
|
|
330
|
+
pbkdf2_iterations: PBKDF2_ITERATIONS
|
|
331
|
+
}
|
|
299
332
|
};
|
|
300
333
|
}
|
|
301
334
|
|
|
@@ -364,10 +397,130 @@ async function listProxies() {
|
|
|
364
397
|
// Index cards for files, articles, books, anything
|
|
365
398
|
// ═══════════════════════════════════════════════════════════════
|
|
366
399
|
|
|
400
|
+
// Dewey Quality Gates - Noise Prevention
|
|
401
|
+
const DEWEY_NOISE_PATTERNS = [
|
|
402
|
+
/^todo:?\s*/i,
|
|
403
|
+
/^check\s+this/i,
|
|
404
|
+
/^read\s+later/i,
|
|
405
|
+
/^temp(orary)?:?\s*/i,
|
|
406
|
+
/^\?\?\?/,
|
|
407
|
+
/^untitled$/i,
|
|
408
|
+
/^test\s*\d*$/i,
|
|
409
|
+
/\(DEAD\)$/i,
|
|
410
|
+
/\(PARKED\)$/i,
|
|
411
|
+
/\(SHUTDOWN\)$/i,
|
|
412
|
+
];
|
|
413
|
+
|
|
414
|
+
const DEWEY_NOISE_TAGS = [
|
|
415
|
+
'dead-bookmark', 'remove', 'temp', 'todo', 'junk', 'spam', 'test'
|
|
416
|
+
];
|
|
417
|
+
|
|
418
|
+
const DEWEY_NOISE_CATEGORIES = [
|
|
419
|
+
'Bookmarks/Dead', 'temp', 'junk', 'spam'
|
|
420
|
+
];
|
|
421
|
+
|
|
422
|
+
function validateDeweyCard(card) {
|
|
423
|
+
const errors = [];
|
|
424
|
+
const warnings = [];
|
|
425
|
+
|
|
426
|
+
// Required: title must exist and be meaningful
|
|
427
|
+
if (!card.title || card.title.length < 3) {
|
|
428
|
+
errors.push('Title required (min 3 chars)');
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// Check for noise patterns in title
|
|
432
|
+
for (const pattern of DEWEY_NOISE_PATTERNS) {
|
|
433
|
+
if (pattern.test(card.title || '')) {
|
|
434
|
+
errors.push(`Noise pattern detected in title: "${card.title}". Use roadmap_add for TODOs.`);
|
|
435
|
+
break;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// Check for noise tags
|
|
440
|
+
const tags = card.tags || [];
|
|
441
|
+
const noiseTags = tags.filter(t => DEWEY_NOISE_TAGS.includes(t.toLowerCase()));
|
|
442
|
+
if (noiseTags.length > 0) {
|
|
443
|
+
errors.push(`Noise tags not allowed: ${noiseTags.join(', ')}. Dead bookmarks should not be indexed.`);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// Check for noise categories
|
|
447
|
+
if (DEWEY_NOISE_CATEGORIES.includes(card.category)) {
|
|
448
|
+
errors.push(`Category "${card.category}" is for temporary items. Use roadmap instead.`);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// Type-specific validation
|
|
452
|
+
const type = card.type || 'file';
|
|
453
|
+
|
|
454
|
+
switch (type) {
|
|
455
|
+
case 'url':
|
|
456
|
+
if (!card.location || !card.location.startsWith('http')) {
|
|
457
|
+
errors.push('URL type requires valid http/https location');
|
|
458
|
+
}
|
|
459
|
+
if (!card.summary && !card.notes) {
|
|
460
|
+
warnings.push('URL items should have a summary or notes explaining why it\'s worth keeping');
|
|
461
|
+
}
|
|
462
|
+
break;
|
|
463
|
+
|
|
464
|
+
case 'idea':
|
|
465
|
+
if (!card.summary && !card.notes) {
|
|
466
|
+
errors.push('Ideas require a summary or notes explaining the concept');
|
|
467
|
+
}
|
|
468
|
+
if (card.summary && card.summary.length < 20) {
|
|
469
|
+
warnings.push('Idea summary is very short. Consider adding more detail.');
|
|
470
|
+
}
|
|
471
|
+
break;
|
|
472
|
+
|
|
473
|
+
case 'snippet':
|
|
474
|
+
if (!card.tags || card.tags.length === 0) {
|
|
475
|
+
errors.push('Snippets require at least one tag for context');
|
|
476
|
+
}
|
|
477
|
+
break;
|
|
478
|
+
|
|
479
|
+
case 'file':
|
|
480
|
+
if (!card.location && !card.summary) {
|
|
481
|
+
warnings.push('Files should have a location or summary');
|
|
482
|
+
}
|
|
483
|
+
break;
|
|
484
|
+
|
|
485
|
+
case 'article':
|
|
486
|
+
case 'book':
|
|
487
|
+
if (!card.author && !card.metadata?.author) {
|
|
488
|
+
warnings.push(`${type}s should have an author`);
|
|
489
|
+
}
|
|
490
|
+
break;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// Rating validation
|
|
494
|
+
if (card.rating !== null && card.rating !== undefined) {
|
|
495
|
+
if (card.rating < 1 || card.rating > 5) {
|
|
496
|
+
errors.push('Rating must be 1-5');
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
return {
|
|
501
|
+
valid: errors.length === 0,
|
|
502
|
+
errors,
|
|
503
|
+
warnings,
|
|
504
|
+
quality: errors.length === 0 && warnings.length === 0 ? 'high' :
|
|
505
|
+
errors.length === 0 ? 'medium' : 'rejected'
|
|
506
|
+
};
|
|
507
|
+
}
|
|
508
|
+
|
|
367
509
|
async function deweyAdd(card) {
|
|
368
510
|
const masterKey = await getMasterKey();
|
|
369
511
|
if (!masterKey) throw new Error('Vault locked. Unlock first.');
|
|
370
512
|
|
|
513
|
+
// Quality gate validation
|
|
514
|
+
const validation = validateDeweyCard(card);
|
|
515
|
+
if (!validation.valid) {
|
|
516
|
+
return {
|
|
517
|
+
error: 'Quality gate failed',
|
|
518
|
+
errors: validation.errors,
|
|
519
|
+
warnings: validation.warnings,
|
|
520
|
+
hint: 'Use roadmap_add for TODOs, or improve the card metadata.'
|
|
521
|
+
};
|
|
522
|
+
}
|
|
523
|
+
|
|
371
524
|
const id = card.id || `dew_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
372
525
|
const now = Date.now();
|
|
373
526
|
|
|
@@ -403,7 +556,14 @@ async function deweyAdd(card) {
|
|
|
403
556
|
await apiRequest('POST', '/vault/credentials', { name: `dewey/${id}`, encrypted });
|
|
404
557
|
}
|
|
405
558
|
|
|
406
|
-
return {
|
|
559
|
+
return {
|
|
560
|
+
id,
|
|
561
|
+
title: indexCard.title,
|
|
562
|
+
type: indexCard.type,
|
|
563
|
+
indexed: true,
|
|
564
|
+
quality: validation.quality,
|
|
565
|
+
warnings: validation.warnings.length > 0 ? validation.warnings : undefined
|
|
566
|
+
};
|
|
407
567
|
}
|
|
408
568
|
|
|
409
569
|
async function deweyGet(id) {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "50c",
|
|
3
|
-
"version": "2.
|
|
4
|
-
"description": "AI toolkit
|
|
3
|
+
"version": "2.14.0",
|
|
4
|
+
"description": "AI toolkit. Fixed grabr apiRequest calls + web_search/page_fetch now working.",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"bin": {
|
|
7
7
|
"50c": "./bin/50c.js"
|