50c 2.5.2 → 2.6.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.
@@ -47,19 +47,19 @@ async function learningStats() {
47
47
  }
48
48
 
49
49
  async function roadmapAdd(idea, priority = 'medium', tags = []) {
50
- return apiRequest('POST', '/tools/roadmap', { action: 'add', idea, priority, tags });
50
+ return apiRequest('POST', '/tools/roadmap_add', { idea, priority, tags });
51
51
  }
52
52
 
53
53
  async function roadmapList(filter = null) {
54
- return apiRequest('POST', '/tools/roadmap', { action: 'list', filter });
54
+ return apiRequest('POST', '/tools/roadmap_list', { filter });
55
55
  }
56
56
 
57
57
  async function roadmapUpdate(id, updates) {
58
- return apiRequest('POST', '/tools/roadmap', { action: 'update', id, ...updates });
58
+ return apiRequest('POST', '/tools/roadmap_update', { id, ...updates });
59
59
  }
60
60
 
61
61
  async function roadmapDelete(id) {
62
- return apiRequest('POST', '/tools/roadmap', { action: 'delete', id });
62
+ return apiRequest('POST', '/tools/roadmap_delete', { id });
63
63
  }
64
64
 
65
65
  // Tool definitions - STARTER tier ($29/mo)
@@ -96,6 +96,210 @@ async function folderPlan(fileList, instruction) {
96
96
  });
97
97
  }
98
98
 
99
+ // ═══════════════════════════════════════════════════════════════
100
+ // BROWSER UTILITIES (LOCAL)
101
+ // ═══════════════════════════════════════════════════════════════
102
+
103
+ const fs = require('fs');
104
+ const path = require('path');
105
+ const https = require('https');
106
+ const http = require('http');
107
+
108
+ function getChromeDataPath(profile = 'Default') {
109
+ const platform = process.platform;
110
+ const home = process.env.HOME || process.env.USERPROFILE;
111
+
112
+ if (platform === 'win32') {
113
+ return path.join(process.env.LOCALAPPDATA, 'Google', 'Chrome', 'User Data', profile);
114
+ } else if (platform === 'darwin') {
115
+ return path.join(home, 'Library', 'Application Support', 'Google', 'Chrome', profile);
116
+ } else {
117
+ return path.join(home, '.config', 'google-chrome', profile);
118
+ }
119
+ }
120
+
121
+ function parseBookmarksFile(profile = 'Default') {
122
+ const bookmarksPath = path.join(getChromeDataPath(profile), 'Bookmarks');
123
+ if (!fs.existsSync(bookmarksPath)) {
124
+ throw new Error(`Chrome bookmarks not found at ${bookmarksPath}`);
125
+ }
126
+
127
+ const data = JSON.parse(fs.readFileSync(bookmarksPath, 'utf8'));
128
+ const urls = [];
129
+
130
+ function extractUrls(node, folder = '') {
131
+ if (node.url) {
132
+ urls.push({ url: node.url, title: node.name, folder });
133
+ }
134
+ if (node.children) {
135
+ const newFolder = node.name || folder;
136
+ node.children.forEach(child => extractUrls(child, newFolder));
137
+ }
138
+ }
139
+
140
+ Object.values(data.roots).forEach(root => extractUrls(root));
141
+ return urls;
142
+ }
143
+
144
+ function checkUrl(url, timeout = 10000) {
145
+ return new Promise((resolve) => {
146
+ const protocol = url.startsWith('https') ? https : http;
147
+ const req = protocol.get(url, {
148
+ timeout,
149
+ headers: { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/120.0.0.0' }
150
+ }, (res) => {
151
+ resolve({ url, status: res.statusCode, ok: res.statusCode >= 200 && res.statusCode < 400 });
152
+ });
153
+ req.on('error', (e) => resolve({ url, status: 0, ok: false, error: e.message }));
154
+ req.on('timeout', () => {
155
+ req.destroy();
156
+ resolve({ url, status: 0, ok: false, error: 'timeout' });
157
+ });
158
+ });
159
+ }
160
+
161
+ async function chromeBookmarksCheck(limit = 100, timeout = 10, profile = 'Default') {
162
+ const bookmarks = parseBookmarksFile(profile);
163
+ const toCheck = bookmarks.filter(b =>
164
+ b.url.startsWith('http') && !b.url.includes('localhost') && !b.url.includes('127.0.0.1')
165
+ ).slice(0, limit);
166
+
167
+ const results = { ok: [], dead: [], errors: [], skipped: bookmarks.length - toCheck.length };
168
+
169
+ // Check in batches of 10
170
+ for (let i = 0; i < toCheck.length; i += 10) {
171
+ const batch = toCheck.slice(i, i + 10);
172
+ const checks = await Promise.all(batch.map(b => checkUrl(b.url, timeout * 1000)));
173
+
174
+ checks.forEach((result, idx) => {
175
+ const bookmark = batch[idx];
176
+ if (result.ok) {
177
+ results.ok.push({ ...bookmark, status: result.status });
178
+ } else if (result.status === 404) {
179
+ results.dead.push({ ...bookmark, status: 404 });
180
+ } else {
181
+ results.errors.push({ ...bookmark, status: result.status, error: result.error });
182
+ }
183
+ });
184
+ }
185
+
186
+ return {
187
+ total: bookmarks.length,
188
+ checked: toCheck.length,
189
+ ok: results.ok.length,
190
+ dead: results.dead,
191
+ errors: results.errors.filter(e => e.status !== 403), // 403 = login required, not dead
192
+ login_required: results.errors.filter(e => e.status === 403),
193
+ skipped: results.skipped
194
+ };
195
+ }
196
+
197
+ function getExtensionsInfo(profile = 'Default') {
198
+ const extPath = path.join(getChromeDataPath(profile), 'Extensions');
199
+ if (!fs.existsSync(extPath)) {
200
+ return { error: 'Extensions folder not found' };
201
+ }
202
+
203
+ const extensions = [];
204
+ const extDirs = fs.readdirSync(extPath);
205
+
206
+ extDirs.forEach(extId => {
207
+ const extDir = path.join(extPath, extId);
208
+ if (fs.statSync(extDir).isDirectory()) {
209
+ const versions = fs.readdirSync(extDir).filter(v => !v.startsWith('.'));
210
+ if (versions.length > 0) {
211
+ const latestVersion = versions.sort().pop();
212
+ const manifestPath = path.join(extDir, latestVersion, 'manifest.json');
213
+
214
+ if (fs.existsSync(manifestPath)) {
215
+ try {
216
+ const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
217
+ extensions.push({
218
+ id: extId,
219
+ name: manifest.name || 'Unknown',
220
+ version: manifest.version,
221
+ description: manifest.description?.substring(0, 100),
222
+ permissions: manifest.permissions || [],
223
+ hostPermissions: manifest.host_permissions || manifest.optional_host_permissions || []
224
+ });
225
+ } catch (e) {
226
+ extensions.push({ id: extId, name: 'Unknown', error: 'Invalid manifest' });
227
+ }
228
+ }
229
+ }
230
+ }
231
+ });
232
+
233
+ // Flag risky extensions
234
+ const risky = extensions.filter(ext => {
235
+ const perms = [...(ext.permissions || []), ...(ext.hostPermissions || [])];
236
+ return perms.some(p =>
237
+ p.includes('<all_urls>') ||
238
+ p.includes('*://*/*') ||
239
+ p === 'webRequest' ||
240
+ p === 'webRequestBlocking'
241
+ );
242
+ });
243
+
244
+ return {
245
+ total: extensions.length,
246
+ extensions,
247
+ risky,
248
+ recommendations: risky.length > 0 ?
249
+ `Found ${risky.length} extensions with broad permissions. Review: ${risky.map(e => e.name).join(', ')}` :
250
+ 'No obviously risky extensions found'
251
+ };
252
+ }
253
+
254
+ async function passwordsCheck(exportFile, checkBreaches = false) {
255
+ if (!fs.existsSync(exportFile)) {
256
+ return { error: `File not found: ${exportFile}` };
257
+ }
258
+
259
+ const csv = fs.readFileSync(exportFile, 'utf8');
260
+ const lines = csv.split('\n').slice(1); // Skip header
261
+
262
+ const passwords = lines.filter(l => l.trim()).map(line => {
263
+ const parts = line.split(',');
264
+ return {
265
+ name: parts[0],
266
+ url: parts[1],
267
+ username: parts[2],
268
+ password: parts[3]
269
+ };
270
+ });
271
+
272
+ // Check for weak passwords
273
+ const weak = passwords.filter(p => {
274
+ const pwd = p.password || '';
275
+ return pwd.length < 8 ||
276
+ pwd === pwd.toLowerCase() ||
277
+ /^[0-9]+$/.test(pwd) ||
278
+ ['password', '123456', 'qwerty'].includes(pwd.toLowerCase());
279
+ });
280
+
281
+ // Check for reused passwords
282
+ const pwdCounts = {};
283
+ passwords.forEach(p => {
284
+ const pwd = p.password;
285
+ pwdCounts[pwd] = (pwdCounts[pwd] || 0) + 1;
286
+ });
287
+ const reused = Object.entries(pwdCounts).filter(([_, count]) => count > 1);
288
+
289
+ return {
290
+ total: passwords.length,
291
+ weak: weak.map(p => ({ name: p.name, url: p.url, reason: 'Weak password pattern' })),
292
+ reused: reused.map(([pwd, count]) => ({
293
+ count,
294
+ sites: passwords.filter(p => p.password === pwd).map(p => p.name).slice(0, 3)
295
+ })),
296
+ recommendations: [
297
+ weak.length > 0 ? `${weak.length} weak passwords need updating` : null,
298
+ reused.length > 0 ? `${reused.length} passwords are reused across sites` : null
299
+ ].filter(Boolean)
300
+ };
301
+ }
302
+
99
303
  // ═══════════════════════════════════════════════════════════════
100
304
  // TOOL DEFINITIONS
101
305
  // ═══════════════════════════════════════════════════════════════
@@ -439,6 +643,76 @@ const LIBRARIAN_TOOLS = [
439
643
  },
440
644
  cost: 0.08,
441
645
  tier: 'pro'
646
+ },
647
+
648
+ // ═══════════════════════════════════════════════════════════════
649
+ // BROWSER MANAGEMENT
650
+ // ═══════════════════════════════════════════════════════════════
651
+ {
652
+ name: 'chrome_bookmarks_check',
653
+ description: 'Check Chrome bookmarks for dead links (404, timeouts). Local scan. FREE.',
654
+ inputSchema: {
655
+ type: 'object',
656
+ properties: {
657
+ limit: { type: 'number', default: 100, description: 'Max URLs to check' },
658
+ timeout: { type: 'number', default: 10, description: 'Timeout per URL (seconds)' },
659
+ profile: { type: 'string', default: 'Default', description: 'Chrome profile name' }
660
+ }
661
+ },
662
+ cost: 0,
663
+ tier: 'pro'
664
+ },
665
+ {
666
+ name: 'chrome_bookmarks_report',
667
+ description: 'Full bookmark health report with categorization. $0.05',
668
+ inputSchema: {
669
+ type: 'object',
670
+ properties: {
671
+ include_recommendations: { type: 'boolean', default: true },
672
+ profile: { type: 'string', default: 'Default' }
673
+ }
674
+ },
675
+ cost: 0.05,
676
+ tier: 'pro'
677
+ },
678
+ {
679
+ name: 'chrome_extensions_audit',
680
+ description: 'Audit installed Chrome extensions for security/bloat. FREE.',
681
+ inputSchema: {
682
+ type: 'object',
683
+ properties: {
684
+ profile: { type: 'string', default: 'Default' }
685
+ }
686
+ },
687
+ cost: 0,
688
+ tier: 'pro'
689
+ },
690
+ {
691
+ name: 'browser_passwords_check',
692
+ description: 'Check for weak/reused passwords (via Puppeteer). $0.10',
693
+ inputSchema: {
694
+ type: 'object',
695
+ properties: {
696
+ export_file: { type: 'string', description: 'Path to exported passwords CSV' },
697
+ check_breaches: { type: 'boolean', default: false, description: 'Check haveibeenpwned (requires API key)' }
698
+ },
699
+ required: ['export_file']
700
+ },
701
+ cost: 0.10,
702
+ tier: 'pro'
703
+ },
704
+ {
705
+ name: 'browser_history_analyze',
706
+ description: 'Analyze browsing patterns for productivity insights. $0.08',
707
+ inputSchema: {
708
+ type: 'object',
709
+ properties: {
710
+ days: { type: 'number', default: 30, description: 'Days of history to analyze' },
711
+ profile: { type: 'string', default: 'Default' }
712
+ }
713
+ },
714
+ cost: 0.08,
715
+ tier: 'pro'
442
716
  }
443
717
  ];
444
718
 
@@ -471,6 +745,26 @@ async function handleTool(name, args) {
471
745
  return await folderDedup(args.files, args);
472
746
  case 'folder_plan':
473
747
  return await folderPlan(args.files, args.instruction);
748
+
749
+ // Browser tools (local execution)
750
+ case 'chrome_bookmarks_check':
751
+ return await chromeBookmarksCheck(args.limit || 100, args.timeout || 10, args.profile || 'Default');
752
+ case 'chrome_bookmarks_report':
753
+ const checkResult = await chromeBookmarksCheck(500, 8, args.profile || 'Default');
754
+ return apiRequest('POST', '/tools/librarian/bookmarks_report', {
755
+ check_result: checkResult,
756
+ include_recommendations: args.include_recommendations !== false
757
+ });
758
+ case 'chrome_extensions_audit':
759
+ return getExtensionsInfo(args.profile || 'Default');
760
+ case 'browser_passwords_check':
761
+ return await passwordsCheck(args.export_file, args.check_breaches);
762
+ case 'browser_history_analyze':
763
+ return apiRequest('POST', '/tools/librarian/history_analyze', {
764
+ days: args.days || 30,
765
+ profile: args.profile || 'Default'
766
+ });
767
+
474
768
  default:
475
769
  return { error: `Unknown librarian tool: ${name}` };
476
770
  }
@@ -492,5 +786,9 @@ module.exports = {
492
786
  folderAnalyze,
493
787
  folderOrganize,
494
788
  folderDedup,
495
- folderPlan
789
+ folderPlan,
790
+ // Browser tools
791
+ chromeBookmarksCheck,
792
+ getExtensionsInfo,
793
+ passwordsCheck
496
794
  };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "50c",
3
- "version": "2.5.2",
4
- "description": "AI toolkit with Dewey index. One install, 100+ tools.",
3
+ "version": "2.6.0",
4
+ "description": "AI toolkit with browser tools. One install, 100+ tools.",
5
5
  "main": "lib/index.js",
6
6
  "bin": {
7
7
  "50c": "./bin/50c.js"