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 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
  };
@@ -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
- if (session.expires_at > Date.now()) {
58
- return session;
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
- fs.unlinkSync(SESSION_FILE);
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 { id, title: indexCard.title, type: indexCard.type, indexed: true };
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.10.0",
4
- "description": "AI toolkit with beacon context compression. 137+ tools.",
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"