archicore 0.1.5 → 0.1.6

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.
@@ -17,6 +17,57 @@ const state = {
17
17
  history: [],
18
18
  user: null,
19
19
  };
20
+ // Track token usage for session
21
+ let sessionTokensUsed = 0;
22
+ let lastOperationTokens = 0;
23
+ /**
24
+ * Estimate tokens from text (roughly 1 token = 4 chars)
25
+ */
26
+ function estimateTokens(text) {
27
+ return Math.ceil(text.length / 4);
28
+ }
29
+ /**
30
+ * Helper for authenticated API calls with token tracking
31
+ */
32
+ async function apiFetch(url, options = {}) {
33
+ const config = await loadConfig();
34
+ const headers = {
35
+ 'Content-Type': 'application/json',
36
+ 'Authorization': `Bearer ${config.accessToken}`,
37
+ ...options.headers,
38
+ };
39
+ // Estimate input tokens from request body
40
+ const inputTokens = options.body ? estimateTokens(String(options.body)) : 0;
41
+ const response = await fetch(url, { ...options, headers });
42
+ // Try to get tokens from header first
43
+ const tokensHeader = response.headers.get('X-Tokens-Used');
44
+ if (tokensHeader) {
45
+ lastOperationTokens = parseInt(tokensHeader, 10);
46
+ }
47
+ else {
48
+ // Estimate based on response size (will be updated when body is read)
49
+ lastOperationTokens = inputTokens;
50
+ }
51
+ return response;
52
+ }
53
+ /**
54
+ * Update token count from response data
55
+ */
56
+ function updateTokensFromResponse(data) {
57
+ const responseText = JSON.stringify(data);
58
+ const outputTokens = estimateTokens(responseText);
59
+ lastOperationTokens += outputTokens;
60
+ sessionTokensUsed += lastOperationTokens;
61
+ }
62
+ /**
63
+ * Display token usage after operation
64
+ */
65
+ function showTokenUsage() {
66
+ if (lastOperationTokens > 0) {
67
+ console.log(colors.dim(` [${lastOperationTokens.toLocaleString()} tokens]`));
68
+ lastOperationTokens = 0; // Reset for next operation
69
+ }
70
+ }
20
71
  export async function startInteractiveMode() {
21
72
  // Prevent process from exiting on unhandled errors
22
73
  process.on('uncaughtException', (error) => {
@@ -212,6 +263,10 @@ async function handleCommand(input) {
212
263
  case 'export':
213
264
  await handleExportCommand(args);
214
265
  break;
266
+ case 'docs':
267
+ case 'documentation':
268
+ await handleDocsCommand(args);
269
+ break;
215
270
  case 'status':
216
271
  await handleStatusCommand();
217
272
  break;
@@ -341,9 +396,8 @@ async function handleAnalyzeCommand(args) {
341
396
  const spinner = createSpinner('Analyzing impact...').start();
342
397
  try {
343
398
  const config = await loadConfig();
344
- const response = await fetch(`${config.serverUrl}/api/projects/${state.projectId}/analyze`, {
399
+ const response = await apiFetch(`${config.serverUrl}/api/projects/${state.projectId}/analyze`, {
345
400
  method: 'POST',
346
- headers: { 'Content-Type': 'application/json' },
347
401
  body: JSON.stringify({
348
402
  description,
349
403
  files: [],
@@ -394,6 +448,7 @@ async function handleAnalyzeCommand(args) {
394
448
  console.log(` ${colors.info(icons.info)} ${rec.description}`);
395
449
  }
396
450
  }
451
+ showTokenUsage();
397
452
  }
398
453
  catch (error) {
399
454
  spinner.fail('Analysis failed');
@@ -413,9 +468,8 @@ async function handleSearchCommand(query) {
413
468
  const spinner = createSpinner('Searching...').start();
414
469
  try {
415
470
  const config = await loadConfig();
416
- const response = await fetch(`${config.serverUrl}/api/projects/${state.projectId}/search`, {
471
+ const response = await apiFetch(`${config.serverUrl}/api/projects/${state.projectId}/search`, {
417
472
  method: 'POST',
418
- headers: { 'Content-Type': 'application/json' },
419
473
  body: JSON.stringify({ query, limit: 10 }),
420
474
  });
421
475
  if (!response.ok)
@@ -440,8 +494,8 @@ async function handleSearchCommand(query) {
440
494
  .replace(/\n/g, ' ');
441
495
  console.log(` ${colors.dim(preview)}${preview.length >= 100 ? '...' : ''}`);
442
496
  }
443
- console.log();
444
497
  }
498
+ showTokenUsage();
445
499
  }
446
500
  catch (error) {
447
501
  spinner.fail('Search failed');
@@ -457,14 +511,14 @@ async function handleQuery(query) {
457
511
  const spinner = createSpinner('Thinking...').start();
458
512
  try {
459
513
  const config = await loadConfig();
460
- const response = await fetch(`${config.serverUrl}/api/projects/${state.projectId}/ask`, {
514
+ const response = await apiFetch(`${config.serverUrl}/api/projects/${state.projectId}/ask`, {
461
515
  method: 'POST',
462
- headers: { 'Content-Type': 'application/json' },
463
516
  body: JSON.stringify({ question: query }),
464
517
  });
465
518
  if (!response.ok)
466
519
  throw new Error('Query failed');
467
520
  const data = await response.json();
521
+ updateTokensFromResponse(data);
468
522
  spinner.stop();
469
523
  console.log();
470
524
  console.log(colors.primary(' ' + icons.lightning + ' ArchiCore'));
@@ -475,6 +529,7 @@ async function handleQuery(query) {
475
529
  for (const line of lines) {
476
530
  console.log(' ' + line);
477
531
  }
532
+ showTokenUsage();
478
533
  }
479
534
  catch (error) {
480
535
  spinner.fail('Query failed');
@@ -489,7 +544,7 @@ async function handleDeadCodeCommand() {
489
544
  const spinner = createSpinner('Analyzing dead code...').start();
490
545
  try {
491
546
  const config = await loadConfig();
492
- const response = await fetch(`${config.serverUrl}/api/projects/${state.projectId}/dead-code`);
547
+ const response = await apiFetch(`${config.serverUrl}/api/projects/${state.projectId}/dead-code`);
493
548
  if (!response.ok)
494
549
  throw new Error('Analysis failed');
495
550
  const data = await response.json();
@@ -520,7 +575,7 @@ async function handleSecurityCommand() {
520
575
  const spinner = createSpinner('Analyzing security...').start();
521
576
  try {
522
577
  const config = await loadConfig();
523
- const response = await fetch(`${config.serverUrl}/api/projects/${state.projectId}/security`);
578
+ const response = await apiFetch(`${config.serverUrl}/api/projects/${state.projectId}/security`);
524
579
  if (!response.ok)
525
580
  throw new Error('Analysis failed');
526
581
  const data = await response.json();
@@ -562,7 +617,7 @@ async function handleMetricsCommand() {
562
617
  const spinner = createSpinner('Calculating metrics...').start();
563
618
  try {
564
619
  const config = await loadConfig();
565
- const response = await fetch(`${config.serverUrl}/api/projects/${state.projectId}/metrics`);
620
+ const response = await apiFetch(`${config.serverUrl}/api/projects/${state.projectId}/metrics`);
566
621
  if (!response.ok)
567
622
  throw new Error('Analysis failed');
568
623
  const data = await response.json();
@@ -591,9 +646,8 @@ async function handleExportCommand(args) {
591
646
  const spinner = createSpinner(`Exporting as ${format}...`).start();
592
647
  try {
593
648
  const config = await loadConfig();
594
- const response = await fetch(`${config.serverUrl}/api/projects/${state.projectId}/export`, {
649
+ const response = await apiFetch(`${config.serverUrl}/api/projects/${state.projectId}/export`, {
595
650
  method: 'POST',
596
- headers: { 'Content-Type': 'application/json' },
597
651
  body: JSON.stringify({ format }),
598
652
  });
599
653
  if (!response.ok)
@@ -647,6 +701,10 @@ async function handleStatusCommand() {
647
701
  catch {
648
702
  // Ignore subscription fetch errors
649
703
  }
704
+ // Show session token usage
705
+ if (sessionTokensUsed > 0) {
706
+ console.log(` Session tokens: ${colors.highlight(sessionTokensUsed.toLocaleString())}`);
707
+ }
650
708
  }
651
709
  if (state.projectId) {
652
710
  console.log(` Project: ${colors.primary(state.projectName || state.projectId)}`);
@@ -664,7 +722,7 @@ async function handleDuplicationCommand() {
664
722
  const spinner = createSpinner('Analyzing code duplication...').start();
665
723
  try {
666
724
  const config = await loadConfig();
667
- const response = await fetch(`${config.serverUrl}/api/projects/${state.projectId}/duplication`);
725
+ const response = await apiFetch(`${config.serverUrl}/api/projects/${state.projectId}/duplication`);
668
726
  if (!response.ok)
669
727
  throw new Error('Analysis failed');
670
728
  const data = await response.json();
@@ -701,7 +759,7 @@ async function handleRefactoringCommand() {
701
759
  const spinner = createSpinner('Generating refactoring suggestions...').start();
702
760
  try {
703
761
  const config = await loadConfig();
704
- const response = await fetch(`${config.serverUrl}/api/projects/${state.projectId}/refactoring`);
762
+ const response = await apiFetch(`${config.serverUrl}/api/projects/${state.projectId}/refactoring`);
705
763
  if (!response.ok)
706
764
  throw new Error('Analysis failed');
707
765
  const data = await response.json();
@@ -746,7 +804,7 @@ async function handleRulesCommand() {
746
804
  const spinner = createSpinner('Checking architectural rules...').start();
747
805
  try {
748
806
  const config = await loadConfig();
749
- const response = await fetch(`${config.serverUrl}/api/projects/${state.projectId}/rules`);
807
+ const response = await apiFetch(`${config.serverUrl}/api/projects/${state.projectId}/rules`);
750
808
  if (!response.ok)
751
809
  throw new Error('Analysis failed');
752
810
  const data = await response.json();
@@ -773,4 +831,35 @@ async function handleRulesCommand() {
773
831
  throw error;
774
832
  }
775
833
  }
834
+ async function handleDocsCommand(args) {
835
+ if (!state.projectId) {
836
+ printError('No project indexed');
837
+ printInfo('Use /index first');
838
+ return;
839
+ }
840
+ const format = args[0] || 'markdown';
841
+ const output = args[1] || `documentation.${format === 'markdown' ? 'md' : format}`;
842
+ const spinner = createSpinner('Generating documentation...').start();
843
+ try {
844
+ const config = await loadConfig();
845
+ const response = await apiFetch(`${config.serverUrl}/api/projects/${state.projectId}/documentation`, {
846
+ method: 'POST',
847
+ body: JSON.stringify({ format }),
848
+ });
849
+ if (!response.ok)
850
+ throw new Error('Documentation generation failed');
851
+ const data = await response.json();
852
+ updateTokensFromResponse(data);
853
+ const { writeFile } = await import('fs/promises');
854
+ await writeFile(output, data.documentation);
855
+ spinner.succeed('Documentation generated');
856
+ printKeyValue('Output', output);
857
+ printKeyValue('Format', format);
858
+ showTokenUsage();
859
+ }
860
+ catch (error) {
861
+ spinner.fail('Documentation generation failed');
862
+ throw error;
863
+ }
864
+ }
776
865
  //# sourceMappingURL=interactive.js.map
@@ -144,9 +144,41 @@ export class ASTParser {
144
144
  /^(?:\w+\s+)*(\w+)\s*\([^)]*\)\s*{/,
145
145
  /^struct\s+(\w+)/,
146
146
  /^typedef\s+.*\s+(\w+)\s*;/
147
- ]
147
+ ],
148
+ html: [
149
+ /<(\w+)\s+id="(\w+)"/, // Elements with id
150
+ /<script[^>]*>/, // Script tags
151
+ /<style[^>]*>/ // Style tags
152
+ ],
153
+ css: [
154
+ /^\.(\w[\w-]*)\s*\{/, // Class selectors
155
+ /^#(\w[\w-]*)\s*\{/, // ID selectors
156
+ /^@media\s+/, // Media queries
157
+ /^@keyframes\s+(\w+)/ // Animations
158
+ ],
159
+ scss: [
160
+ /^\.(\w[\w-]*)\s*\{/,
161
+ /^\$(\w[\w-]*)\s*:/, // Variables
162
+ /^@mixin\s+(\w+)/, // Mixins
163
+ /^@include\s+(\w+)/ // Include
164
+ ],
165
+ json: [], // JSON doesn't have functions/classes
166
+ yaml: [], // YAML doesn't have functions/classes
167
+ xml: [
168
+ /<(\w+)\s+.*?>/ // XML elements
169
+ ],
170
+ sql: [
171
+ /^CREATE\s+(?:TABLE|VIEW|FUNCTION|PROCEDURE)\s+(\w+)/i,
172
+ /^ALTER\s+TABLE\s+(\w+)/i,
173
+ /^INSERT\s+INTO\s+(\w+)/i
174
+ ],
175
+ shell: [
176
+ /^(\w+)\s*\(\)\s*\{/, // Function definitions
177
+ /^function\s+(\w+)/ // Function keyword
178
+ ],
179
+ markdown: [] // Markdown doesn't have functions/classes
148
180
  };
149
- const langPatterns = patterns[language] || patterns.javascript;
181
+ const langPatterns = patterns[language] || [];
150
182
  lines.forEach((line, index) => {
151
183
  const trimmedLine = line.trim();
152
184
  for (const pattern of langPatterns) {
@@ -71,6 +71,14 @@ export declare class GitHubService {
71
71
  * Disconnect repository
72
72
  */
73
73
  disconnectRepository(userId: string, repoId: string): Promise<boolean>;
74
+ /**
75
+ * Disconnect repository by project ID
76
+ */
77
+ disconnectRepositoryByProjectId(projectId: string): Promise<boolean>;
78
+ /**
79
+ * Update repository's project ID
80
+ */
81
+ updateRepositoryProjectId(repoId: string, projectId: string): Promise<void>;
74
82
  /**
75
83
  * Get connected repositories for user
76
84
  */
@@ -12,6 +12,8 @@ const CONNECTIONS_FILE = 'github-connections.json';
12
12
  const REPOSITORIES_FILE = 'github-repositories.json';
13
13
  // Encryption key (in production, use env variable)
14
14
  const ENCRYPTION_KEY = process.env.GITHUB_ENCRYPTION_KEY || 'archicore-github-key-32bytes!!';
15
+ // Singleton instance
16
+ let instance = null;
15
17
  export class GitHubService {
16
18
  dataDir;
17
19
  connections = [];
@@ -22,11 +24,16 @@ export class GitHubService {
22
24
  redirectUri;
23
25
  webhookBaseUrl;
24
26
  constructor(dataDir = DATA_DIR) {
27
+ // Singleton pattern - return existing instance
28
+ if (instance) {
29
+ return instance;
30
+ }
25
31
  this.dataDir = dataDir;
26
32
  this.clientId = process.env.GITHUB_CLIENT_ID || '';
27
33
  this.clientSecret = process.env.GITHUB_CLIENT_SECRET || '';
28
34
  this.redirectUri = process.env.GITHUB_REDIRECT_URI || 'http://localhost:3000/api/github/callback';
29
35
  this.webhookBaseUrl = process.env.ARCHICORE_WEBHOOK_URL || 'http://localhost:3000/api/github/webhook';
36
+ instance = this;
30
37
  }
31
38
  async ensureInitialized() {
32
39
  if (this.initialized)
@@ -222,7 +229,8 @@ export class GitHubService {
222
229
  let page = 1;
223
230
  const perPage = 100;
224
231
  while (true) {
225
- const response = await fetch(`https://api.github.com/user/repos?per_page=${perPage}&page=${page}&sort=updated`, {
232
+ // visibility=all includes private repos, affiliation includes all repo types
233
+ const response = await fetch(`https://api.github.com/user/repos?per_page=${perPage}&page=${page}&sort=updated&visibility=all&affiliation=owner,collaborator,organization_member`, {
226
234
  headers: {
227
235
  'Authorization': `Bearer ${accessToken}`,
228
236
  'Accept': 'application/vnd.github.v3+json',
@@ -230,7 +238,16 @@ export class GitHubService {
230
238
  }
231
239
  });
232
240
  if (!response.ok) {
233
- throw new Error('Failed to list repositories');
241
+ const errorBody = await response.text();
242
+ Logger.error(`GitHub API error ${response.status}: ${errorBody}`);
243
+ // Check for specific errors
244
+ if (response.status === 401) {
245
+ throw new Error('GitHub token expired or invalid. Please reconnect GitHub.');
246
+ }
247
+ else if (response.status === 403) {
248
+ throw new Error('GitHub rate limit exceeded or insufficient permissions.');
249
+ }
250
+ throw new Error(`Failed to list repositories: ${response.status}`);
234
251
  }
235
252
  const data = await response.json();
236
253
  repos.push(...data);
@@ -359,6 +376,40 @@ export class GitHubService {
359
376
  Logger.info(`Repository disconnected: ${repo.fullName}`);
360
377
  return true;
361
378
  }
379
+ /**
380
+ * Disconnect repository by project ID
381
+ */
382
+ async disconnectRepositoryByProjectId(projectId) {
383
+ await this.ensureInitialized();
384
+ const repo = this.repositories.find(r => r.projectId === projectId);
385
+ if (!repo)
386
+ return false;
387
+ // Delete webhook
388
+ if (repo.webhookId) {
389
+ try {
390
+ await this.deleteWebhook(repo.userId, repo.fullName, repo.webhookId);
391
+ }
392
+ catch (e) {
393
+ Logger.warn(`Failed to delete webhook: ${e}`);
394
+ }
395
+ }
396
+ this.repositories = this.repositories.filter(r => r.projectId !== projectId);
397
+ await this.saveRepositories();
398
+ Logger.info(`Repository disconnected by projectId: ${repo.fullName}`);
399
+ return true;
400
+ }
401
+ /**
402
+ * Update repository's project ID
403
+ */
404
+ async updateRepositoryProjectId(repoId, projectId) {
405
+ await this.ensureInitialized();
406
+ const repo = this.repositories.find(r => r.id === repoId);
407
+ if (repo) {
408
+ repo.projectId = projectId;
409
+ repo.updatedAt = new Date();
410
+ await this.saveRepositories();
411
+ }
412
+ }
362
413
  /**
363
414
  * Get connected repositories for user
364
415
  */
@@ -560,18 +611,36 @@ export class GitHubService {
560
611
  throw new Error('GitHub not connected');
561
612
  }
562
613
  const url = `https://api.github.com/repos/${fullName}/zipball/${ref || 'HEAD'}`;
614
+ Logger.info(`Downloading from: ${url}`);
563
615
  const response = await fetch(url, {
564
616
  headers: {
565
617
  'Authorization': `Bearer ${accessToken}`,
566
- 'Accept': 'application/vnd.github.v3+json',
567
- 'User-Agent': 'ArchiCore'
568
- }
618
+ 'Accept': 'application/vnd.github+json',
619
+ 'User-Agent': 'ArchiCore',
620
+ 'X-GitHub-Api-Version': '2022-11-28'
621
+ },
622
+ redirect: 'follow'
569
623
  });
624
+ Logger.info(`GitHub response status: ${response.status}, content-type: ${response.headers.get('content-type')}`);
570
625
  if (!response.ok) {
571
- throw new Error('Failed to download repository');
626
+ const errorText = await response.text();
627
+ Logger.error(`GitHub download error: ${response.status} - ${errorText}`);
628
+ throw new Error(`Failed to download repository: ${response.status}`);
572
629
  }
573
630
  const arrayBuffer = await response.arrayBuffer();
574
- return Buffer.from(arrayBuffer);
631
+ const buffer = Buffer.from(arrayBuffer);
632
+ // Validate that we got a ZIP file (should start with PK\x03\x04)
633
+ if (buffer.length < 100) {
634
+ Logger.error(`Downloaded content too small (${buffer.length} bytes), content: ${buffer.toString('utf8').substring(0, 200)}`);
635
+ throw new Error(`Downloaded file too small - possibly an error response`);
636
+ }
637
+ // Check ZIP magic bytes
638
+ if (buffer[0] !== 0x50 || buffer[1] !== 0x4B) {
639
+ Logger.error(`Invalid ZIP file, first bytes: ${buffer[0]}, ${buffer[1]}, content: ${buffer.toString('utf8').substring(0, 200)}`);
640
+ throw new Error('Downloaded file is not a valid ZIP archive');
641
+ }
642
+ Logger.info(`Valid ZIP downloaded: ${buffer.length} bytes`);
643
+ return buffer;
575
644
  }
576
645
  // ===== UTILITY =====
577
646
  /**