archicore 0.1.6 → 0.1.8

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.
@@ -126,6 +126,8 @@ export async function startInteractiveMode() {
126
126
  output: process.stdout,
127
127
  terminal: true,
128
128
  });
129
+ // Keep the process alive
130
+ rl.ref?.();
129
131
  const prompt = () => {
130
132
  const prefix = state.projectName
131
133
  ? colors.muted(`[${state.projectName}] `)
@@ -367,7 +369,64 @@ async function handleIndexCommand() {
367
369
  });
368
370
  if (!response.ok) {
369
371
  const errorData = await response.json().catch(() => ({}));
370
- throw new Error(errorData.error || 'Upload failed');
372
+ // If access denied, re-register project with current user
373
+ if (response.status === 403 && errorData.error?.includes('Access denied')) {
374
+ indexSpinner.update('Re-registering project with current user...');
375
+ // Clear old project ID and re-register
376
+ const reRegisterResponse = await fetch(`${config.serverUrl}/api/projects`, {
377
+ method: 'POST',
378
+ headers: {
379
+ 'Content-Type': 'application/json',
380
+ 'Authorization': `Bearer ${config.accessToken}`,
381
+ },
382
+ body: JSON.stringify({ name: state.projectName, path: state.projectPath }),
383
+ });
384
+ if (reRegisterResponse.ok) {
385
+ const reData = await reRegisterResponse.json();
386
+ state.projectId = reData.id || reData.project?.id;
387
+ // Retry upload with new project ID
388
+ indexSpinner.update('Uploading to server...');
389
+ const retryResponse = await fetch(`${config.serverUrl}/api/projects/${state.projectId}/upload-index`, {
390
+ method: 'POST',
391
+ headers: {
392
+ 'Content-Type': 'application/json',
393
+ 'Authorization': `Bearer ${config.accessToken}`,
394
+ },
395
+ body: JSON.stringify({
396
+ asts: astsArray,
397
+ symbols: symbolsArray,
398
+ graph: graphData,
399
+ fileContents,
400
+ statistics: {
401
+ totalFiles: asts.size,
402
+ totalSymbols: symbols.size,
403
+ },
404
+ }),
405
+ });
406
+ if (!retryResponse.ok) {
407
+ throw new Error('Upload failed after re-registration');
408
+ }
409
+ // Use retry response for data
410
+ const retryData = await retryResponse.json();
411
+ indexSpinner.succeed('Project re-registered and indexed');
412
+ printKeyValue('Files', String(retryData.statistics?.filesCount || asts.size));
413
+ printKeyValue('Symbols', String(retryData.statistics?.symbolsCount || symbols.size));
414
+ // Update local config with new project ID
415
+ const localProjectRetry = await getLocalProject(state.projectPath);
416
+ if (localProjectRetry && state.projectId) {
417
+ localProjectRetry.id = state.projectId;
418
+ localProjectRetry.indexed = true;
419
+ await fs.writeFile(pathModule.default.join(state.projectPath, '.archicore', 'project.json'), JSON.stringify(localProjectRetry, null, 2));
420
+ }
421
+ return; // Exit early - already handled
422
+ }
423
+ else {
424
+ throw new Error('Failed to re-register project');
425
+ }
426
+ }
427
+ else {
428
+ throw new Error(errorData.error || 'Upload failed');
429
+ }
371
430
  }
372
431
  const data = await response.json();
373
432
  indexSpinner.succeed('Project indexed and uploaded');
@@ -10,6 +10,7 @@ export class Spinner {
10
10
  text: colors.muted(text),
11
11
  spinner: 'dots',
12
12
  color: 'magenta',
13
+ discardStdin: false, // Important: don't interfere with stdin (fixes CLI exit bug on Linux)
13
14
  });
14
15
  }
15
16
  start(text) {
package/dist/index.js CHANGED
@@ -22,7 +22,11 @@ export class AIArhitector {
22
22
  constructor(config) {
23
23
  this.config = config;
24
24
  this.codeIndex = new CodeIndex();
25
- this.semanticMemory = new SemanticMemory({ provider: config.llm.provider }, config.vectorStore);
25
+ // Determine embedding provider: Jina (free) > OpenAI
26
+ const embeddingProvider = process.env.JINA_API_KEY ? 'jina'
27
+ : process.env.OPENAI_API_KEY ? 'openai'
28
+ : 'none';
29
+ this.semanticMemory = new SemanticMemory({ provider: embeddingProvider }, config.vectorStore);
26
30
  this.architecture = new ArchitectureKnowledge(config.architectureConfigPath || '.aiarhitector/architecture.json');
27
31
  this.impactEngine = new ImpactEngine();
28
32
  this.orchestrator = new LLMOrchestrator(config.llm);
@@ -1,5 +1,5 @@
1
1
  export interface EmbeddingConfig {
2
- provider: 'anthropic' | 'openai' | 'deepseek' | 'none';
2
+ provider: 'openai' | 'jina' | 'none';
3
3
  model?: string;
4
4
  }
5
5
  export declare class EmbeddingService {
@@ -7,10 +7,14 @@ export declare class EmbeddingService {
7
7
  private config;
8
8
  private initialized;
9
9
  private _isAvailable;
10
+ private jinaApiKey?;
11
+ private embeddingDimension;
10
12
  constructor(config: EmbeddingConfig);
13
+ getEmbeddingDimension(): number;
11
14
  private ensureInitialized;
12
15
  isAvailable(): boolean;
13
16
  generateEmbedding(text: string): Promise<number[]>;
17
+ private generateJinaEmbedding;
14
18
  generateBatchEmbeddings(texts: string[]): Promise<number[][]>;
15
19
  private generateOpenAIEmbedding;
16
20
  prepareCodeForEmbedding(code: string, context?: string): string;
@@ -5,9 +5,14 @@ export class EmbeddingService {
5
5
  config;
6
6
  initialized = false;
7
7
  _isAvailable = false;
8
+ jinaApiKey;
9
+ embeddingDimension = 1024; // Jina default, OpenAI small = 1536
8
10
  constructor(config) {
9
11
  this.config = config;
10
12
  }
13
+ getEmbeddingDimension() {
14
+ return this.embeddingDimension;
15
+ }
11
16
  ensureInitialized() {
12
17
  if (this.initialized)
13
18
  return;
@@ -17,26 +22,29 @@ export class EmbeddingService {
17
22
  return;
18
23
  }
19
24
  try {
20
- if (this.config.provider === 'deepseek') {
21
- const apiKey = process.env.DEEPSEEK_API_KEY;
22
- if (!apiKey) {
23
- Logger.warn('DEEPSEEK_API_KEY not set - semantic search disabled');
25
+ if (this.config.provider === 'jina') {
26
+ // Jina AI - free embeddings
27
+ this.jinaApiKey = process.env.JINA_API_KEY;
28
+ if (!this.jinaApiKey) {
29
+ Logger.warn('JINA_API_KEY not set - semantic search disabled');
30
+ Logger.info('Get free API key at: https://jina.ai/embeddings/');
24
31
  return;
25
32
  }
26
- this.openai = new OpenAI({
27
- apiKey,
28
- baseURL: 'https://api.deepseek.com'
29
- });
33
+ this.embeddingDimension = 1024;
30
34
  this._isAvailable = true;
35
+ Logger.success('Jina AI embeddings enabled');
31
36
  }
32
37
  else {
38
+ // OpenAI embeddings
33
39
  const apiKey = process.env.OPENAI_API_KEY;
34
40
  if (!apiKey) {
35
41
  Logger.warn('OPENAI_API_KEY not set - semantic search disabled');
36
42
  return;
37
43
  }
38
44
  this.openai = new OpenAI({ apiKey });
45
+ this.embeddingDimension = 1536;
39
46
  this._isAvailable = true;
47
+ Logger.success('OpenAI embeddings enabled');
40
48
  }
41
49
  }
42
50
  catch (error) {
@@ -50,23 +58,49 @@ export class EmbeddingService {
50
58
  async generateEmbedding(text) {
51
59
  this.ensureInitialized();
52
60
  if (!this._isAvailable)
53
- return new Array(1536).fill(0);
61
+ return new Array(this.embeddingDimension).fill(0);
54
62
  try {
55
- if (this.openai) {
63
+ if (this.config.provider === 'jina' && this.jinaApiKey) {
64
+ return await this.generateJinaEmbedding(text);
65
+ }
66
+ else if (this.openai) {
56
67
  return await this.generateOpenAIEmbedding(text);
57
68
  }
58
- return new Array(1536).fill(0);
69
+ return new Array(this.embeddingDimension).fill(0);
59
70
  }
60
71
  catch (error) {
61
72
  Logger.error('Failed to generate embedding', error);
62
- return new Array(1536).fill(0);
73
+ return new Array(this.embeddingDimension).fill(0);
74
+ }
75
+ }
76
+ async generateJinaEmbedding(text) {
77
+ if (!this.jinaApiKey)
78
+ throw new Error('Jina API key not set');
79
+ const response = await fetch('https://api.jina.ai/v1/embeddings', {
80
+ method: 'POST',
81
+ headers: {
82
+ 'Content-Type': 'application/json',
83
+ 'Authorization': `Bearer ${this.jinaApiKey}`
84
+ },
85
+ body: JSON.stringify({
86
+ model: this.config.model || 'jina-embeddings-v3',
87
+ task: 'text-matching',
88
+ dimensions: 1024,
89
+ input: [text.substring(0, 8000)]
90
+ })
91
+ });
92
+ if (!response.ok) {
93
+ const error = await response.text();
94
+ throw new Error(`Jina API error: ${response.status} ${error}`);
63
95
  }
96
+ const data = await response.json();
97
+ return data.data[0].embedding;
64
98
  }
65
99
  async generateBatchEmbeddings(texts) {
66
100
  this.ensureInitialized();
67
101
  if (!this._isAvailable) {
68
102
  Logger.warn('Embedding service not available, skipping');
69
- return texts.map(() => new Array(1536).fill(0));
103
+ return texts.map(() => new Array(this.embeddingDimension).fill(0));
70
104
  }
71
105
  Logger.progress('Generating embeddings for ' + texts.length + ' texts...');
72
106
  const embeddings = [];
@@ -19,8 +19,10 @@ export class SemanticMemory {
19
19
  }
20
20
  async initialize() {
21
21
  Logger.info('Initializing Semantic Memory...');
22
- await this.vectorStore.initialize();
23
- Logger.success('Semantic Memory initialized');
22
+ // Pass the correct embedding dimension based on provider (Jina=1024, OpenAI=1536)
23
+ const dimension = this.embeddingService.getEmbeddingDimension();
24
+ await this.vectorStore.initialize(dimension);
25
+ Logger.success(`Semantic Memory initialized (dimension: ${dimension})`);
24
26
  }
25
27
  async indexSymbols(symbols, asts) {
26
28
  Logger.progress('Indexing symbols into semantic memory...');
@@ -1,5 +1,15 @@
1
1
  import { QdrantClient } from '@qdrant/js-client-rest';
2
+ import { createHash } from 'crypto';
2
3
  import { Logger } from '../utils/logger.js';
4
+ /**
5
+ * Convert string ID to UUID format for Qdrant
6
+ * Uses MD5 hash to create deterministic UUID from any string
7
+ */
8
+ function stringToUuid(str) {
9
+ const hash = createHash('md5').update(str).digest('hex');
10
+ // Format as UUID: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
11
+ return `${hash.slice(0, 8)}-${hash.slice(8, 12)}-${hash.slice(12, 16)}-${hash.slice(16, 20)}-${hash.slice(20, 32)}`;
12
+ }
3
13
  export class VectorStore {
4
14
  client;
5
15
  collectionName;
@@ -44,9 +54,10 @@ export class VectorStore {
44
54
  await this.client.upsert(this.collectionName, {
45
55
  points: [
46
56
  {
47
- id: chunk.id,
57
+ id: stringToUuid(chunk.id), // Convert string ID to UUID
48
58
  vector: chunk.embedding,
49
59
  payload: {
60
+ originalId: chunk.id, // Store original ID
50
61
  content: chunk.content,
51
62
  metadata: chunk.metadata
52
63
  }
@@ -69,9 +80,10 @@ export class VectorStore {
69
80
  const batch = chunks.slice(i, i + batchSize);
70
81
  await this.client.upsert(this.collectionName, {
71
82
  points: batch.map(chunk => ({
72
- id: chunk.id,
83
+ id: stringToUuid(chunk.id), // Convert string ID to UUID
73
84
  vector: chunk.embedding,
74
85
  payload: {
86
+ originalId: chunk.id, // Store original ID
75
87
  content: chunk.content,
76
88
  metadata: chunk.metadata
77
89
  }
@@ -93,7 +105,7 @@ export class VectorStore {
93
105
  });
94
106
  return response.map(point => ({
95
107
  chunk: {
96
- id: String(point.id),
108
+ id: point.payload?.originalId || String(point.id), // Use original ID
97
109
  content: point.payload?.content || '',
98
110
  embedding: [],
99
111
  metadata: point.payload?.metadata || {
@@ -119,7 +131,7 @@ export class VectorStore {
119
131
  return;
120
132
  try {
121
133
  await this.client.delete(this.collectionName, {
122
- points: [id]
134
+ points: [stringToUuid(id)] // Convert to UUID
123
135
  });
124
136
  }
125
137
  catch (error) {
@@ -78,12 +78,17 @@ apiRouter.get('/projects/:id', authMiddleware, async (req, res) => {
78
78
  async function checkProjectAccess(req, res, next) {
79
79
  const { id } = req.params;
80
80
  const userId = getUserId(req);
81
+ Logger.info(`checkProjectAccess: projectId=${id}, userId=${userId}`);
81
82
  if (!userId) {
82
83
  res.status(401).json({ error: 'Authentication required' });
83
84
  return;
84
85
  }
85
86
  const isOwner = await projectService.isProjectOwner(id, userId);
87
+ Logger.info(`checkProjectAccess: isOwner=${isOwner}`);
86
88
  if (!isOwner) {
89
+ // Log project details for debugging
90
+ const project = await projectService.getProject(id);
91
+ Logger.warn(`Access denied: userId=${userId}, project.ownerId=${project?.ownerId}`);
87
92
  res.status(403).json({ error: 'Access denied to this project' });
88
93
  return;
89
94
  }
@@ -104,6 +109,22 @@ apiRouter.post('/projects/:id/index', authMiddleware, checkProjectAccess, async
104
109
  res.status(500).json({ error: 'Failed to index project' });
105
110
  }
106
111
  });
112
+ /**
113
+ * POST /api/projects/:id/reindex-embeddings
114
+ * Переиндексировать embeddings из сохранённых данных
115
+ * Используется когда добавили JINA_API_KEY или OPENAI_API_KEY
116
+ */
117
+ apiRouter.post('/projects/:id/reindex-embeddings', authMiddleware, checkProjectAccess, async (req, res) => {
118
+ try {
119
+ const { id } = req.params;
120
+ const result = await projectService.reindexEmbeddings(id);
121
+ res.json(result);
122
+ }
123
+ catch (error) {
124
+ Logger.error('Failed to reindex embeddings:', error);
125
+ res.status(500).json({ error: String(error) });
126
+ }
127
+ });
107
128
  /**
108
129
  * POST /api/projects/:id/upload-index
109
130
  * Загрузить индексированные данные с CLI
@@ -105,6 +105,14 @@ export declare class ProjectService {
105
105
  * Загрузить сохранённые индексные данные с диска
106
106
  */
107
107
  loadIndexedDataFromDisk(projectId: string): Promise<boolean>;
108
+ /**
109
+ * Переиндексировать embeddings из сохранённых данных
110
+ * Используется когда добавили API ключ для embeddings
111
+ */
112
+ reindexEmbeddings(projectId: string): Promise<{
113
+ success: boolean;
114
+ indexed: number;
115
+ }>;
108
116
  /**
109
117
  * Получить метрики кода
110
118
  */
@@ -121,24 +121,33 @@ export class ProjectService {
121
121
  maxTokens: 4096,
122
122
  baseURL: 'https://api.deepseek.com'
123
123
  };
124
- // SemanticMemory опционален - DeepSeek не поддерживает embeddings
124
+ // SemanticMemory - поддержка Jina (бесплатно) или OpenAI
125
125
  let semanticMemory = null;
126
- // Только если есть OpenAI ключ - используем embeddings
127
- if (process.env.OPENAI_API_KEY) {
126
+ const vectorStoreConfig = {
127
+ url: process.env.QDRANT_URL || 'http://localhost:6333',
128
+ apiKey: process.env.QDRANT_API_KEY,
129
+ collectionName: `archicore_${projectId.replace(/-/g, '_')}`
130
+ };
131
+ // Приоритет: JINA_API_KEY (бесплатный) > OPENAI_API_KEY
132
+ if (process.env.JINA_API_KEY) {
133
+ const embeddingConfig = {
134
+ provider: 'jina',
135
+ model: 'jina-embeddings-v3'
136
+ };
137
+ semanticMemory = new SemanticMemory(embeddingConfig, vectorStoreConfig);
138
+ Logger.info('SemanticMemory enabled (Jina AI embeddings - free)');
139
+ }
140
+ else if (process.env.OPENAI_API_KEY) {
128
141
  const embeddingConfig = {
129
142
  provider: 'openai',
130
143
  model: 'text-embedding-3-small'
131
144
  };
132
- const vectorStoreConfig = {
133
- url: process.env.QDRANT_URL || 'http://localhost:6333',
134
- apiKey: process.env.QDRANT_API_KEY,
135
- collectionName: `archicore_${projectId.replace(/-/g, '_')}`
136
- };
137
145
  semanticMemory = new SemanticMemory(embeddingConfig, vectorStoreConfig);
138
146
  Logger.info('SemanticMemory enabled (OpenAI embeddings)');
139
147
  }
140
148
  else {
141
- Logger.warn('SemanticMemory disabled - no OPENAI_API_KEY. Semantic search unavailable.');
149
+ Logger.warn('SemanticMemory disabled - no JINA_API_KEY or OPENAI_API_KEY');
150
+ Logger.info('Get free Jina API key at: https://jina.ai/embeddings/');
142
151
  }
143
152
  const data = {
144
153
  codeIndex: new CodeIndex(project.path),
@@ -547,6 +556,33 @@ export class ProjectService {
547
556
  return false;
548
557
  }
549
558
  }
559
+ /**
560
+ * Переиндексировать embeddings из сохранённых данных
561
+ * Используется когда добавили API ключ для embeddings
562
+ */
563
+ async reindexEmbeddings(projectId) {
564
+ const project = this.projects.get(projectId);
565
+ if (!project) {
566
+ throw new Error(`Project not found: ${projectId}`);
567
+ }
568
+ // Загружаем данные с диска если ещё не загружены
569
+ const loaded = await this.loadIndexedDataFromDisk(projectId);
570
+ if (!loaded) {
571
+ throw new Error('No indexed data found. Please index project first via CLI.');
572
+ }
573
+ const data = await this.getProjectData(projectId);
574
+ if (!data.symbols || !data.asts) {
575
+ throw new Error('No symbols/ASTs found. Please re-index via CLI.');
576
+ }
577
+ if (!data.semanticMemory) {
578
+ throw new Error('SemanticMemory not available. Check JINA_API_KEY or OPENAI_API_KEY.');
579
+ }
580
+ Logger.progress(`Re-indexing embeddings for: ${project.name}`);
581
+ await data.semanticMemory.initialize();
582
+ await data.semanticMemory.indexSymbols(data.symbols, data.asts);
583
+ Logger.success(`Embeddings indexed: ${data.symbols.size} symbols`);
584
+ return { success: true, indexed: data.symbols.size };
585
+ }
550
586
  /**
551
587
  * Получить метрики кода
552
588
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "archicore",
3
- "version": "0.1.6",
3
+ "version": "0.1.8",
4
4
  "description": "AI Software Architect - code analysis, impact prediction, semantic search",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",