archicore 0.1.4 → 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,7 +17,67 @@ 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() {
72
+ // Prevent process from exiting on unhandled errors
73
+ process.on('uncaughtException', (error) => {
74
+ printError(`Unexpected error: ${error.message}`);
75
+ console.log();
76
+ });
77
+ process.on('unhandledRejection', (reason) => {
78
+ printError(`Unhandled promise rejection: ${reason}`);
79
+ console.log();
80
+ });
21
81
  // Check if initialized in current directory
22
82
  const initialized = await isInitialized(state.projectPath);
23
83
  if (!initialized) {
@@ -102,14 +162,34 @@ export async function startInteractiveMode() {
102
162
  });
103
163
  rl.on('close', () => {
104
164
  if (state.running) {
105
- // Unexpected close, try to restart
165
+ // Unexpected close - don't exit, restart readline
166
+ console.log();
167
+ printWarning('Input stream interrupted, restarting...');
168
+ // Recreate readline interface
169
+ const newRl = readline.createInterface({
170
+ input: process.stdin,
171
+ output: process.stdout,
172
+ terminal: true,
173
+ });
174
+ newRl.on('line', (input) => {
175
+ processLine(input).catch((err) => {
176
+ printError(String(err));
177
+ prompt();
178
+ });
179
+ });
180
+ newRl.on('close', () => {
181
+ if (!state.running) {
182
+ printGoodbye();
183
+ process.exit(0);
184
+ }
185
+ });
106
186
  console.log();
107
- printWarning('Input stream closed unexpectedly');
187
+ prompt();
108
188
  }
109
189
  else {
110
190
  printGoodbye();
191
+ process.exit(0);
111
192
  }
112
- process.exit(0);
113
193
  });
114
194
  // Handle SIGINT (Ctrl+C)
115
195
  process.on('SIGINT', () => {
@@ -183,6 +263,10 @@ async function handleCommand(input) {
183
263
  case 'export':
184
264
  await handleExportCommand(args);
185
265
  break;
266
+ case 'docs':
267
+ case 'documentation':
268
+ await handleDocsCommand(args);
269
+ break;
186
270
  case 'status':
187
271
  await handleStatusCommand();
188
272
  break;
@@ -312,9 +396,8 @@ async function handleAnalyzeCommand(args) {
312
396
  const spinner = createSpinner('Analyzing impact...').start();
313
397
  try {
314
398
  const config = await loadConfig();
315
- const response = await fetch(`${config.serverUrl}/api/projects/${state.projectId}/analyze`, {
399
+ const response = await apiFetch(`${config.serverUrl}/api/projects/${state.projectId}/analyze`, {
316
400
  method: 'POST',
317
- headers: { 'Content-Type': 'application/json' },
318
401
  body: JSON.stringify({
319
402
  description,
320
403
  files: [],
@@ -365,6 +448,7 @@ async function handleAnalyzeCommand(args) {
365
448
  console.log(` ${colors.info(icons.info)} ${rec.description}`);
366
449
  }
367
450
  }
451
+ showTokenUsage();
368
452
  }
369
453
  catch (error) {
370
454
  spinner.fail('Analysis failed');
@@ -384,9 +468,8 @@ async function handleSearchCommand(query) {
384
468
  const spinner = createSpinner('Searching...').start();
385
469
  try {
386
470
  const config = await loadConfig();
387
- const response = await fetch(`${config.serverUrl}/api/projects/${state.projectId}/search`, {
471
+ const response = await apiFetch(`${config.serverUrl}/api/projects/${state.projectId}/search`, {
388
472
  method: 'POST',
389
- headers: { 'Content-Type': 'application/json' },
390
473
  body: JSON.stringify({ query, limit: 10 }),
391
474
  });
392
475
  if (!response.ok)
@@ -411,8 +494,8 @@ async function handleSearchCommand(query) {
411
494
  .replace(/\n/g, ' ');
412
495
  console.log(` ${colors.dim(preview)}${preview.length >= 100 ? '...' : ''}`);
413
496
  }
414
- console.log();
415
497
  }
498
+ showTokenUsage();
416
499
  }
417
500
  catch (error) {
418
501
  spinner.fail('Search failed');
@@ -428,14 +511,14 @@ async function handleQuery(query) {
428
511
  const spinner = createSpinner('Thinking...').start();
429
512
  try {
430
513
  const config = await loadConfig();
431
- const response = await fetch(`${config.serverUrl}/api/projects/${state.projectId}/ask`, {
514
+ const response = await apiFetch(`${config.serverUrl}/api/projects/${state.projectId}/ask`, {
432
515
  method: 'POST',
433
- headers: { 'Content-Type': 'application/json' },
434
516
  body: JSON.stringify({ question: query }),
435
517
  });
436
518
  if (!response.ok)
437
519
  throw new Error('Query failed');
438
520
  const data = await response.json();
521
+ updateTokensFromResponse(data);
439
522
  spinner.stop();
440
523
  console.log();
441
524
  console.log(colors.primary(' ' + icons.lightning + ' ArchiCore'));
@@ -446,6 +529,7 @@ async function handleQuery(query) {
446
529
  for (const line of lines) {
447
530
  console.log(' ' + line);
448
531
  }
532
+ showTokenUsage();
449
533
  }
450
534
  catch (error) {
451
535
  spinner.fail('Query failed');
@@ -460,7 +544,7 @@ async function handleDeadCodeCommand() {
460
544
  const spinner = createSpinner('Analyzing dead code...').start();
461
545
  try {
462
546
  const config = await loadConfig();
463
- 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`);
464
548
  if (!response.ok)
465
549
  throw new Error('Analysis failed');
466
550
  const data = await response.json();
@@ -491,7 +575,7 @@ async function handleSecurityCommand() {
491
575
  const spinner = createSpinner('Analyzing security...').start();
492
576
  try {
493
577
  const config = await loadConfig();
494
- const response = await fetch(`${config.serverUrl}/api/projects/${state.projectId}/security`);
578
+ const response = await apiFetch(`${config.serverUrl}/api/projects/${state.projectId}/security`);
495
579
  if (!response.ok)
496
580
  throw new Error('Analysis failed');
497
581
  const data = await response.json();
@@ -533,7 +617,7 @@ async function handleMetricsCommand() {
533
617
  const spinner = createSpinner('Calculating metrics...').start();
534
618
  try {
535
619
  const config = await loadConfig();
536
- const response = await fetch(`${config.serverUrl}/api/projects/${state.projectId}/metrics`);
620
+ const response = await apiFetch(`${config.serverUrl}/api/projects/${state.projectId}/metrics`);
537
621
  if (!response.ok)
538
622
  throw new Error('Analysis failed');
539
623
  const data = await response.json();
@@ -562,9 +646,8 @@ async function handleExportCommand(args) {
562
646
  const spinner = createSpinner(`Exporting as ${format}...`).start();
563
647
  try {
564
648
  const config = await loadConfig();
565
- const response = await fetch(`${config.serverUrl}/api/projects/${state.projectId}/export`, {
649
+ const response = await apiFetch(`${config.serverUrl}/api/projects/${state.projectId}/export`, {
566
650
  method: 'POST',
567
- headers: { 'Content-Type': 'application/json' },
568
651
  body: JSON.stringify({ format }),
569
652
  });
570
653
  if (!response.ok)
@@ -618,6 +701,10 @@ async function handleStatusCommand() {
618
701
  catch {
619
702
  // Ignore subscription fetch errors
620
703
  }
704
+ // Show session token usage
705
+ if (sessionTokensUsed > 0) {
706
+ console.log(` Session tokens: ${colors.highlight(sessionTokensUsed.toLocaleString())}`);
707
+ }
621
708
  }
622
709
  if (state.projectId) {
623
710
  console.log(` Project: ${colors.primary(state.projectName || state.projectId)}`);
@@ -635,7 +722,7 @@ async function handleDuplicationCommand() {
635
722
  const spinner = createSpinner('Analyzing code duplication...').start();
636
723
  try {
637
724
  const config = await loadConfig();
638
- const response = await fetch(`${config.serverUrl}/api/projects/${state.projectId}/duplication`);
725
+ const response = await apiFetch(`${config.serverUrl}/api/projects/${state.projectId}/duplication`);
639
726
  if (!response.ok)
640
727
  throw new Error('Analysis failed');
641
728
  const data = await response.json();
@@ -672,7 +759,7 @@ async function handleRefactoringCommand() {
672
759
  const spinner = createSpinner('Generating refactoring suggestions...').start();
673
760
  try {
674
761
  const config = await loadConfig();
675
- const response = await fetch(`${config.serverUrl}/api/projects/${state.projectId}/refactoring`);
762
+ const response = await apiFetch(`${config.serverUrl}/api/projects/${state.projectId}/refactoring`);
676
763
  if (!response.ok)
677
764
  throw new Error('Analysis failed');
678
765
  const data = await response.json();
@@ -717,7 +804,7 @@ async function handleRulesCommand() {
717
804
  const spinner = createSpinner('Checking architectural rules...').start();
718
805
  try {
719
806
  const config = await loadConfig();
720
- const response = await fetch(`${config.serverUrl}/api/projects/${state.projectId}/rules`);
807
+ const response = await apiFetch(`${config.serverUrl}/api/projects/${state.projectId}/rules`);
721
808
  if (!response.ok)
722
809
  throw new Error('Analysis failed');
723
810
  const data = await response.json();
@@ -744,4 +831,35 @@ async function handleRulesCommand() {
744
831
  throw error;
745
832
  }
746
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
+ }
747
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
  /**