archicore 0.1.5 → 0.1.7

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;
@@ -312,7 +367,64 @@ async function handleIndexCommand() {
312
367
  });
313
368
  if (!response.ok) {
314
369
  const errorData = await response.json().catch(() => ({}));
315
- throw new Error(errorData.error || 'Upload failed');
370
+ // If access denied, re-register project with current user
371
+ if (response.status === 403 && errorData.error?.includes('Access denied')) {
372
+ indexSpinner.update('Re-registering project with current user...');
373
+ // Clear old project ID and re-register
374
+ const reRegisterResponse = await fetch(`${config.serverUrl}/api/projects`, {
375
+ method: 'POST',
376
+ headers: {
377
+ 'Content-Type': 'application/json',
378
+ 'Authorization': `Bearer ${config.accessToken}`,
379
+ },
380
+ body: JSON.stringify({ name: state.projectName, path: state.projectPath }),
381
+ });
382
+ if (reRegisterResponse.ok) {
383
+ const reData = await reRegisterResponse.json();
384
+ state.projectId = reData.id || reData.project?.id;
385
+ // Retry upload with new project ID
386
+ indexSpinner.update('Uploading to server...');
387
+ const retryResponse = await fetch(`${config.serverUrl}/api/projects/${state.projectId}/upload-index`, {
388
+ method: 'POST',
389
+ headers: {
390
+ 'Content-Type': 'application/json',
391
+ 'Authorization': `Bearer ${config.accessToken}`,
392
+ },
393
+ body: JSON.stringify({
394
+ asts: astsArray,
395
+ symbols: symbolsArray,
396
+ graph: graphData,
397
+ fileContents,
398
+ statistics: {
399
+ totalFiles: asts.size,
400
+ totalSymbols: symbols.size,
401
+ },
402
+ }),
403
+ });
404
+ if (!retryResponse.ok) {
405
+ throw new Error('Upload failed after re-registration');
406
+ }
407
+ // Use retry response for data
408
+ const retryData = await retryResponse.json();
409
+ indexSpinner.succeed('Project re-registered and indexed');
410
+ printKeyValue('Files', String(retryData.statistics?.filesCount || asts.size));
411
+ printKeyValue('Symbols', String(retryData.statistics?.symbolsCount || symbols.size));
412
+ // Update local config with new project ID
413
+ const localProjectRetry = await getLocalProject(state.projectPath);
414
+ if (localProjectRetry && state.projectId) {
415
+ localProjectRetry.id = state.projectId;
416
+ localProjectRetry.indexed = true;
417
+ await fs.writeFile(pathModule.default.join(state.projectPath, '.archicore', 'project.json'), JSON.stringify(localProjectRetry, null, 2));
418
+ }
419
+ return; // Exit early - already handled
420
+ }
421
+ else {
422
+ throw new Error('Failed to re-register project');
423
+ }
424
+ }
425
+ else {
426
+ throw new Error(errorData.error || 'Upload failed');
427
+ }
316
428
  }
317
429
  const data = await response.json();
318
430
  indexSpinner.succeed('Project indexed and uploaded');
@@ -341,9 +453,8 @@ async function handleAnalyzeCommand(args) {
341
453
  const spinner = createSpinner('Analyzing impact...').start();
342
454
  try {
343
455
  const config = await loadConfig();
344
- const response = await fetch(`${config.serverUrl}/api/projects/${state.projectId}/analyze`, {
456
+ const response = await apiFetch(`${config.serverUrl}/api/projects/${state.projectId}/analyze`, {
345
457
  method: 'POST',
346
- headers: { 'Content-Type': 'application/json' },
347
458
  body: JSON.stringify({
348
459
  description,
349
460
  files: [],
@@ -394,6 +505,7 @@ async function handleAnalyzeCommand(args) {
394
505
  console.log(` ${colors.info(icons.info)} ${rec.description}`);
395
506
  }
396
507
  }
508
+ showTokenUsage();
397
509
  }
398
510
  catch (error) {
399
511
  spinner.fail('Analysis failed');
@@ -413,9 +525,8 @@ async function handleSearchCommand(query) {
413
525
  const spinner = createSpinner('Searching...').start();
414
526
  try {
415
527
  const config = await loadConfig();
416
- const response = await fetch(`${config.serverUrl}/api/projects/${state.projectId}/search`, {
528
+ const response = await apiFetch(`${config.serverUrl}/api/projects/${state.projectId}/search`, {
417
529
  method: 'POST',
418
- headers: { 'Content-Type': 'application/json' },
419
530
  body: JSON.stringify({ query, limit: 10 }),
420
531
  });
421
532
  if (!response.ok)
@@ -440,8 +551,8 @@ async function handleSearchCommand(query) {
440
551
  .replace(/\n/g, ' ');
441
552
  console.log(` ${colors.dim(preview)}${preview.length >= 100 ? '...' : ''}`);
442
553
  }
443
- console.log();
444
554
  }
555
+ showTokenUsage();
445
556
  }
446
557
  catch (error) {
447
558
  spinner.fail('Search failed');
@@ -457,14 +568,14 @@ async function handleQuery(query) {
457
568
  const spinner = createSpinner('Thinking...').start();
458
569
  try {
459
570
  const config = await loadConfig();
460
- const response = await fetch(`${config.serverUrl}/api/projects/${state.projectId}/ask`, {
571
+ const response = await apiFetch(`${config.serverUrl}/api/projects/${state.projectId}/ask`, {
461
572
  method: 'POST',
462
- headers: { 'Content-Type': 'application/json' },
463
573
  body: JSON.stringify({ question: query }),
464
574
  });
465
575
  if (!response.ok)
466
576
  throw new Error('Query failed');
467
577
  const data = await response.json();
578
+ updateTokensFromResponse(data);
468
579
  spinner.stop();
469
580
  console.log();
470
581
  console.log(colors.primary(' ' + icons.lightning + ' ArchiCore'));
@@ -475,6 +586,7 @@ async function handleQuery(query) {
475
586
  for (const line of lines) {
476
587
  console.log(' ' + line);
477
588
  }
589
+ showTokenUsage();
478
590
  }
479
591
  catch (error) {
480
592
  spinner.fail('Query failed');
@@ -489,7 +601,7 @@ async function handleDeadCodeCommand() {
489
601
  const spinner = createSpinner('Analyzing dead code...').start();
490
602
  try {
491
603
  const config = await loadConfig();
492
- const response = await fetch(`${config.serverUrl}/api/projects/${state.projectId}/dead-code`);
604
+ const response = await apiFetch(`${config.serverUrl}/api/projects/${state.projectId}/dead-code`);
493
605
  if (!response.ok)
494
606
  throw new Error('Analysis failed');
495
607
  const data = await response.json();
@@ -520,7 +632,7 @@ async function handleSecurityCommand() {
520
632
  const spinner = createSpinner('Analyzing security...').start();
521
633
  try {
522
634
  const config = await loadConfig();
523
- const response = await fetch(`${config.serverUrl}/api/projects/${state.projectId}/security`);
635
+ const response = await apiFetch(`${config.serverUrl}/api/projects/${state.projectId}/security`);
524
636
  if (!response.ok)
525
637
  throw new Error('Analysis failed');
526
638
  const data = await response.json();
@@ -562,7 +674,7 @@ async function handleMetricsCommand() {
562
674
  const spinner = createSpinner('Calculating metrics...').start();
563
675
  try {
564
676
  const config = await loadConfig();
565
- const response = await fetch(`${config.serverUrl}/api/projects/${state.projectId}/metrics`);
677
+ const response = await apiFetch(`${config.serverUrl}/api/projects/${state.projectId}/metrics`);
566
678
  if (!response.ok)
567
679
  throw new Error('Analysis failed');
568
680
  const data = await response.json();
@@ -591,9 +703,8 @@ async function handleExportCommand(args) {
591
703
  const spinner = createSpinner(`Exporting as ${format}...`).start();
592
704
  try {
593
705
  const config = await loadConfig();
594
- const response = await fetch(`${config.serverUrl}/api/projects/${state.projectId}/export`, {
706
+ const response = await apiFetch(`${config.serverUrl}/api/projects/${state.projectId}/export`, {
595
707
  method: 'POST',
596
- headers: { 'Content-Type': 'application/json' },
597
708
  body: JSON.stringify({ format }),
598
709
  });
599
710
  if (!response.ok)
@@ -647,6 +758,10 @@ async function handleStatusCommand() {
647
758
  catch {
648
759
  // Ignore subscription fetch errors
649
760
  }
761
+ // Show session token usage
762
+ if (sessionTokensUsed > 0) {
763
+ console.log(` Session tokens: ${colors.highlight(sessionTokensUsed.toLocaleString())}`);
764
+ }
650
765
  }
651
766
  if (state.projectId) {
652
767
  console.log(` Project: ${colors.primary(state.projectName || state.projectId)}`);
@@ -664,7 +779,7 @@ async function handleDuplicationCommand() {
664
779
  const spinner = createSpinner('Analyzing code duplication...').start();
665
780
  try {
666
781
  const config = await loadConfig();
667
- const response = await fetch(`${config.serverUrl}/api/projects/${state.projectId}/duplication`);
782
+ const response = await apiFetch(`${config.serverUrl}/api/projects/${state.projectId}/duplication`);
668
783
  if (!response.ok)
669
784
  throw new Error('Analysis failed');
670
785
  const data = await response.json();
@@ -701,7 +816,7 @@ async function handleRefactoringCommand() {
701
816
  const spinner = createSpinner('Generating refactoring suggestions...').start();
702
817
  try {
703
818
  const config = await loadConfig();
704
- const response = await fetch(`${config.serverUrl}/api/projects/${state.projectId}/refactoring`);
819
+ const response = await apiFetch(`${config.serverUrl}/api/projects/${state.projectId}/refactoring`);
705
820
  if (!response.ok)
706
821
  throw new Error('Analysis failed');
707
822
  const data = await response.json();
@@ -746,7 +861,7 @@ async function handleRulesCommand() {
746
861
  const spinner = createSpinner('Checking architectural rules...').start();
747
862
  try {
748
863
  const config = await loadConfig();
749
- const response = await fetch(`${config.serverUrl}/api/projects/${state.projectId}/rules`);
864
+ const response = await apiFetch(`${config.serverUrl}/api/projects/${state.projectId}/rules`);
750
865
  if (!response.ok)
751
866
  throw new Error('Analysis failed');
752
867
  const data = await response.json();
@@ -773,4 +888,35 @@ async function handleRulesCommand() {
773
888
  throw error;
774
889
  }
775
890
  }
891
+ async function handleDocsCommand(args) {
892
+ if (!state.projectId) {
893
+ printError('No project indexed');
894
+ printInfo('Use /index first');
895
+ return;
896
+ }
897
+ const format = args[0] || 'markdown';
898
+ const output = args[1] || `documentation.${format === 'markdown' ? 'md' : format}`;
899
+ const spinner = createSpinner('Generating documentation...').start();
900
+ try {
901
+ const config = await loadConfig();
902
+ const response = await apiFetch(`${config.serverUrl}/api/projects/${state.projectId}/documentation`, {
903
+ method: 'POST',
904
+ body: JSON.stringify({ format }),
905
+ });
906
+ if (!response.ok)
907
+ throw new Error('Documentation generation failed');
908
+ const data = await response.json();
909
+ updateTokensFromResponse(data);
910
+ const { writeFile } = await import('fs/promises');
911
+ await writeFile(output, data.documentation);
912
+ spinner.succeed('Documentation generated');
913
+ printKeyValue('Output', output);
914
+ printKeyValue('Format', format);
915
+ showTokenUsage();
916
+ }
917
+ catch (error) {
918
+ spinner.fail('Documentation generation failed');
919
+ throw error;
920
+ }
921
+ }
776
922
  //# 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
  /**